1 // This file is part of Golly.
2 // See docs/License.html for the copyright notice.
3
4 #include "wx/wxprec.h" // for compilers that support precompilation
5 #ifndef WX_PRECOMP
6 #include "wx/wx.h" // for all others include the necessary headers
7 #endif
8
9 #include "wx/dir.h" // for wxDir
10 #include "wx/file.h" // for wxFile
11 #include "wx/filename.h" // for wxFileName
12
13 #include "bigint.h"
14 #include "lifealgo.h"
15 #include "qlifealgo.h"
16 #include "hlifealgo.h"
17 #include "util.h" // for linereader
18
19 #include "wxgolly.h" // for wxGetApp, statusptr, viewptr, bigview
20 #include "wxutils.h" // for BeginProgress, GetString, etc
21 #include "wxprefs.h" // for allowundo, etc
22 #include "wxrule.h" // for ChangeRule
23 #include "wxhelp.h" // for LoadLexiconPattern
24 #include "wxstatus.h" // for statusptr->...
25 #include "wxselect.h" // for Selection
26 #include "wxview.h" // for viewptr->...
27 #include "wxscript.h" // for inscript, PassKeyToScript
28 #include "wxmain.h" // for MainFrame
29 #include "wxundo.h" // for undoredo->...
30 #include "wxalgos.h" // for *_ALGO, algo_type, CreateNewUniverse, etc
31 #include "wxlayer.h" // for currlayer, etc
32 #include "wxtimeline.h" // for TimelineExists, UpdateTimelineBar, etc
33
34 #include <stdexcept> // for std::runtime_error and std::exception
35 #include <sstream> // for std::ostringstream
36
37 #ifdef __WXMAC__
38 // we need to convert filepath to decomposed UTF8 so fopen will work
39 #define OPENFILE(filepath) fopen(filepath.fn_str(),"r")
40 #else
41 #define OPENFILE(filepath) fopen(filepath.mb_str(wxConvLocal),"r")
42 #endif
43
44 // This module implements Control menu functions.
45
46 // -----------------------------------------------------------------------------
47
SaveStartingPattern()48 bool MainFrame::SaveStartingPattern()
49 {
50 if ( currlayer->algo->getGeneration() > currlayer->startgen ) {
51 // don't do anything if current gen count > starting gen
52 return true;
53 }
54
55 // save current name, rule, dirty flag, scale, location, etc
56 currlayer->startname = currlayer->currname;
57 currlayer->startrule = wxString(currlayer->algo->getrule(), wxConvLocal);
58 currlayer->startdirty = currlayer->dirty;
59 currlayer->startmag = viewptr->GetMag();
60 viewptr->GetPos(currlayer->startx, currlayer->starty);
61 currlayer->startbase = currlayer->currbase;
62 currlayer->startexpo = currlayer->currexpo;
63 currlayer->startalgo = currlayer->algtype;
64
65 // if this layer is a clone then save some settings in other clones
66 if (currlayer->cloneid > 0) {
67 for ( int i = 0; i < numlayers; i++ ) {
68 Layer* cloneptr = GetLayer(i);
69 if (cloneptr != currlayer && cloneptr->cloneid == currlayer->cloneid) {
70 cloneptr->startname = cloneptr->currname;
71 cloneptr->startx = cloneptr->view->x;
72 cloneptr->starty = cloneptr->view->y;
73 cloneptr->startmag = cloneptr->view->getmag();
74 cloneptr->startbase = cloneptr->currbase;
75 cloneptr->startexpo = cloneptr->currexpo;
76 }
77 }
78 }
79
80 // save current selection
81 currlayer->startsel = currlayer->currsel;
82
83 if ( !currlayer->savestart ) {
84 // no need to save pattern (use currlayer->currfile as the starting pattern)
85 if (currlayer->currfile.IsEmpty())
86 Warning(_("Bug in SaveStartingPattern: currfile is empty!"));
87 return true;
88 }
89
90 currlayer->currfile = currlayer->tempstart; // ResetPattern will load tempstart
91
92 // save starting pattern in tempstart file
93 if ( currlayer->algo->hyperCapable() ) {
94 // much faster to save pattern in a macrocell file
95 const char* err = WritePattern(currlayer->tempstart, MC_format,
96 no_compression, 0, 0, 0, 0);
97 if (err) {
98 statusptr->ErrorMessage(wxString(err,wxConvLocal));
99 // don't allow user to continue generating
100 return false;
101 }
102 } else {
103 // can only save as RLE if edges are within getcell/setcell limits
104 bigint top, left, bottom, right;
105 currlayer->algo->findedges(&top, &left, &bottom, &right);
106 if ( viewptr->OutsideLimits(top, left, bottom, right) ) {
107 statusptr->ErrorMessage(_("Starting pattern is outside +/- 10^9 boundary."));
108 // don't allow user to continue generating
109 return false;
110 }
111 int itop = top.toint();
112 int ileft = left.toint();
113 int ibottom = bottom.toint();
114 int iright = right.toint();
115 // use XRLE format so the pattern's top left location and the current
116 // generation count are stored in the file
117 const char* err = WritePattern(currlayer->tempstart, XRLE_format, no_compression,
118 itop, ileft, ibottom, iright);
119 if (err) {
120 statusptr->ErrorMessage(wxString(err,wxConvLocal));
121 // don't allow user to continue generating
122 return false;
123 }
124 }
125
126 return true;
127 }
128
129 // -----------------------------------------------------------------------------
130
ResetPattern(bool resetundo)131 void MainFrame::ResetPattern(bool resetundo)
132 {
133 if (currlayer->algo->getGeneration() == currlayer->startgen) return;
134
135 if (generating) {
136 command_pending = true;
137 cmdevent.SetId(ID_RESET);
138 Stop();
139 return;
140 }
141
142 if (inscript) stop_after_script = true;
143
144 if (currlayer->algo->getGeneration() < currlayer->startgen) {
145 // if this happens then startgen logic is wrong
146 Warning(_("Current gen < starting gen!"));
147 return;
148 }
149
150 if (currlayer->currfile.IsEmpty()) {
151 // if this happens then savestart or currfile logic is wrong
152 Warning(_("Starting pattern cannot be restored!"));
153 return;
154 }
155
156 if (allowundo && !currlayer->stayclean && inscript) {
157 // script called reset()
158 SavePendingChanges();
159 currlayer->undoredo->RememberGenStart();
160 }
161
162 // save current algo and rule
163 algo_type oldalgo = currlayer->algtype;
164 wxString oldrule = wxString(currlayer->algo->getrule(), wxConvLocal);
165
166 // restore pattern and settings saved by SaveStartingPattern;
167 // first restore algorithm
168 currlayer->algtype = currlayer->startalgo;
169
170 // restore starting pattern
171 LoadPattern(currlayer->currfile, wxEmptyString);
172
173 if (currlayer->algo->getGeneration() != currlayer->startgen) {
174 // LoadPattern failed to reset the gen count to startgen
175 // (probably because the user deleted the starting pattern)
176 // so best to clear the pattern and reset the gen count
177 CreateUniverse();
178 currlayer->algo->setGeneration(currlayer->startgen);
179 Warning(_("Failed to reset pattern from this file:\n") + currlayer->currfile);
180 }
181
182 // restore settings saved by SaveStartingPattern
183 RestoreRule(currlayer->startrule);
184 currlayer->currname = currlayer->startname;
185 currlayer->dirty = currlayer->startdirty;
186 if (restoreview) {
187 viewptr->SetPosMag(currlayer->startx, currlayer->starty, currlayer->startmag);
188 }
189
190 // restore step size and set increment
191 currlayer->currbase = currlayer->startbase;
192 currlayer->currexpo = currlayer->startexpo;
193 SetGenIncrement();
194
195 // if this layer is a clone then restore some settings in other clones
196 if (currlayer->cloneid > 0) {
197 for ( int i = 0; i < numlayers; i++ ) {
198 Layer* cloneptr = GetLayer(i);
199 if (cloneptr != currlayer && cloneptr->cloneid == currlayer->cloneid) {
200 cloneptr->currname = cloneptr->startname;
201 if (restoreview) {
202 cloneptr->view->setpositionmag(cloneptr->startx, cloneptr->starty,
203 cloneptr->startmag);
204 }
205 cloneptr->currbase = cloneptr->startbase;
206 cloneptr->currexpo = cloneptr->startexpo;
207 // also synchronize dirty flags and update items in Layer menu
208 cloneptr->dirty = currlayer->dirty;
209 UpdateLayerItem(i);
210 }
211 }
212 }
213
214 // restore selection
215 currlayer->currsel = currlayer->startsel;
216
217 // switch to default colors if algo/rule changed
218 wxString newrule = wxString(currlayer->algo->getrule(), wxConvLocal);
219 if (oldalgo != currlayer->algtype || oldrule != newrule) {
220 UpdateLayerColors();
221 }
222
223 // update window title in case currname, rule or dirty flag changed;
224 // note that UpdateLayerItem(currindex) gets called
225 SetWindowTitle(currlayer->currname);
226 UpdateEverything();
227
228 if (allowundo && !currlayer->stayclean) {
229 if (inscript) {
230 // script called reset() so remember gen change (RememberGenStart was called above)
231 currlayer->undoredo->RememberGenFinish();
232 } else if (resetundo) {
233 // wind back the undo history to the starting pattern
234 currlayer->undoredo->SyncUndoHistory();
235 }
236 }
237 }
238
239 // -----------------------------------------------------------------------------
240
RestorePattern(bigint & gen,const wxString & filename,bigint & x,bigint & y,int mag,int base,int expo)241 void MainFrame::RestorePattern(bigint& gen, const wxString& filename,
242 bigint& x, bigint& y, int mag, int base, int expo)
243 {
244 // called to undo/redo a generating change
245 if (gen == currlayer->startgen) {
246 // restore starting pattern (false means don't call SyncUndoHistory)
247 ResetPattern(false);
248 } else {
249 // restore pattern in given filename;
250 // false means don't update status bar (algorithm should NOT change)
251 LoadPattern(filename, wxEmptyString, false);
252
253 if (currlayer->algo->getGeneration() != gen) {
254 // best to clear the pattern and set the expected gen count
255 CreateUniverse();
256 currlayer->algo->setGeneration(gen);
257 Warning(_("Could not restore pattern from this file:\n") + filename);
258 }
259
260 // restore step size and set increment
261 currlayer->currbase = base;
262 currlayer->currexpo = expo;
263 SetGenIncrement();
264
265 // restore position and scale, if allowed
266 if (restoreview) viewptr->SetPosMag(x, y, mag);
267
268 UpdatePatternAndStatus();
269 }
270 }
271
272 // -----------------------------------------------------------------------------
273
ChangeGenCount(const char * genstring,bool inundoredo)274 const char* MainFrame::ChangeGenCount(const char* genstring, bool inundoredo)
275 {
276 // disallow alphabetic chars in genstring
277 for (unsigned int i = 0; i < strlen(genstring); i++)
278 if ( (genstring[i] >= 'a' && genstring[i] <= 'z') ||
279 (genstring[i] >= 'A' && genstring[i] <= 'Z') )
280 return "Alphabetic character is not allowed in generation string.";
281
282 bigint oldgen = currlayer->algo->getGeneration();
283 bigint newgen(genstring);
284
285 if (genstring[0] == '+' || genstring[0] == '-') {
286 // leading +/- sign so make newgen relative to oldgen
287 bigint relgen = newgen;
288 newgen = oldgen;
289 newgen += relgen;
290 if (newgen < bigint::zero) newgen = bigint::zero;
291 }
292
293 // set stop_after_script BEFORE testing newgen == oldgen so scripts
294 // can call setgen("+0") to prevent further generating
295 if (inscript) stop_after_script = true;
296
297 if (newgen == oldgen) return NULL;
298
299 if (!inundoredo && allowundo && !currlayer->stayclean && inscript) {
300 // script called setgen()
301 SavePendingChanges();
302 }
303
304 // need IsParityShifted() method???
305 if (currlayer->algtype == QLIFE_ALGO && newgen.odd() != oldgen.odd()) {
306 // qlife stores pattern in different bits depending on gen parity,
307 // so we need to create a new qlife universe, set its gen, copy the
308 // current pattern to the new universe, then switch to that universe
309 bigint top, left, bottom, right;
310 currlayer->algo->findedges(&top, &left, &bottom, &right);
311 if ( viewptr->OutsideLimits(top, left, bottom, right) ) {
312 return "Pattern is too big to copy.";
313 }
314 // create a new universe of same type and same rule
315 lifealgo* newalgo = CreateNewUniverse(currlayer->algtype);
316 const char* err = newalgo->setrule(currlayer->algo->getrule());
317 if (err) {
318 delete newalgo;
319 return "Current rule is no longer valid!";
320 }
321 newalgo->setGeneration(newgen);
322 // copy pattern
323 if ( !viewptr->CopyRect(top.toint(), left.toint(), bottom.toint(), right.toint(),
324 currlayer->algo, newalgo, false, _("Copying pattern")) ) {
325 delete newalgo;
326 return "Failed to copy pattern.";
327 }
328 // switch to new universe
329 delete currlayer->algo;
330 currlayer->algo = newalgo;
331 SetGenIncrement();
332 } else {
333 currlayer->algo->setGeneration(newgen);
334 }
335
336 if (!inundoredo) {
337 // save some settings for RememberSetGen below
338 bigint oldstartgen = currlayer->startgen;
339 bool oldsave = currlayer->savestart;
340
341 // may need to change startgen and savestart
342 if (oldgen == currlayer->startgen || newgen <= currlayer->startgen) {
343 currlayer->startgen = newgen;
344 currlayer->savestart = true;
345 }
346
347 if (allowundo && !currlayer->stayclean) {
348 currlayer->undoredo->RememberSetGen(oldgen, newgen, oldstartgen, oldsave);
349 }
350 }
351
352 UpdateStatus();
353 return NULL;
354 }
355
356 // -----------------------------------------------------------------------------
357
SetGeneration()358 void MainFrame::SetGeneration()
359 {
360 if (generating) {
361 command_pending = true;
362 cmdevent.SetId(ID_SETGEN);
363 Stop();
364 return;
365 }
366
367 bigint oldgen = currlayer->algo->getGeneration();
368 wxString result;
369 wxString prompt = _("Enter a new generation count:");
370 prompt += _("\n(+n/-n is relative to current count)");
371 if ( GetString(_("Set Generation"), prompt,
372 wxString(oldgen.tostring(), wxConvLocal), result) ) {
373
374 const char* err = ChangeGenCount(result.mb_str(wxConvLocal));
375
376 if (err) {
377 Warning(wxString(err,wxConvLocal));
378 } else {
379 // Reset/Undo/Redo items might become enabled or disabled
380 // (we need to do this if user clicked "Generation=..." text)
381 UpdateMenuItems();
382 }
383 }
384 }
385
386 // -----------------------------------------------------------------------------
387
SetGenIncrement()388 void MainFrame::SetGenIncrement()
389 {
390 if (currlayer->currexpo > 0) {
391 bigint inc = 1;
392 int maxexpo = 1 ;
393 if (currlayer->currbase <= 10000) {
394 int mantissa = currlayer->currbase ;
395 int himantissa = 0x7fffffff ;
396 while (mantissa > 1 && 0 == (mantissa & 1))
397 mantissa >>= 1 ;
398 if (mantissa == 1) {
399 maxexpo = 0x7fffffff ;
400 } else {
401 int p = mantissa ;
402 while (p <= himantissa / mantissa) {
403 p *= mantissa ;
404 maxexpo++ ;
405 }
406 }
407 }
408 if (currlayer->currexpo > maxexpo)
409 currlayer->currexpo = maxexpo ;
410 // set increment to currbase^currexpo
411 int i = currlayer->currexpo;
412 while (i > 0) {
413 if (currlayer->currbase > 10000) {
414 inc = currlayer->currbase ;
415 } else {
416 inc.mul_smallint(currlayer->currbase);
417 }
418 i--;
419 }
420 currlayer->algo->setIncrement(inc);
421 } else {
422 currlayer->algo->setIncrement(1);
423 }
424 }
425
426 // -----------------------------------------------------------------------------
427
StartGenTimer()428 void MainFrame::StartGenTimer()
429 {
430 int interval = SIXTY_HERTZ; // do ~60 calls of OnGenTimer per sec
431
432 // increase interval if user wants a delay
433 if (currlayer->currexpo < 0) {
434 interval = statusptr->GetCurrentDelay();
435 if (interval < SIXTY_HERTZ) interval += SIXTY_HERTZ;
436 }
437
438 if (gentimer->IsRunning()) gentimer->Stop();
439 gentimer->Start(interval, wxTIMER_CONTINUOUS);
440 }
441
442 // -----------------------------------------------------------------------------
443
GoFaster()444 void MainFrame::GoFaster()
445 {
446 if (TimelineExists()) {
447 PlayTimelineFaster();
448 } else {
449 currlayer->currexpo++;
450 SetGenIncrement();
451 // only need to refresh status bar
452 UpdateStatus();
453 if (generating && currlayer->currexpo <= 0) {
454 // decrease gentimer interval
455 StartGenTimer();
456 }
457 }
458 }
459
460 // -----------------------------------------------------------------------------
461
GoSlower()462 void MainFrame::GoSlower()
463 {
464 if (TimelineExists()) {
465 PlayTimelineSlower();
466 } else {
467 if (currlayer->currexpo > minexpo) {
468 currlayer->currexpo--;
469 SetGenIncrement();
470 // only need to refresh status bar
471 UpdateStatus();
472 if (generating && currlayer->currexpo < 0) {
473 // increase gentimer interval
474 StartGenTimer();
475 }
476 } else {
477 Beep();
478 }
479 }
480 }
481
482 // -----------------------------------------------------------------------------
483
SetMinimumStepExponent()484 void MainFrame::SetMinimumStepExponent()
485 {
486 // set minexpo depending on mindelay and maxdelay
487 minexpo = 0;
488 if (mindelay > 0) {
489 int d = mindelay;
490 minexpo--;
491 while (d < maxdelay) {
492 d *= 2;
493 minexpo--;
494 }
495 }
496 }
497
498 // -----------------------------------------------------------------------------
499
UpdateStepExponent()500 void MainFrame::UpdateStepExponent()
501 {
502 SetMinimumStepExponent();
503 if (currlayer->currexpo < minexpo) currlayer->currexpo = minexpo;
504 SetGenIncrement();
505
506 if (generating && currlayer->currexpo <= 0) {
507 // update gentimer interval
508 StartGenTimer();
509 }
510 }
511
512 // -----------------------------------------------------------------------------
513
SetStepExponent(int newexpo)514 void MainFrame::SetStepExponent(int newexpo)
515 {
516 currlayer->currexpo = newexpo;
517 if (currlayer->currexpo < minexpo) currlayer->currexpo = minexpo;
518 SetGenIncrement();
519
520 if (generating && currlayer->currexpo <= 0) {
521 // update gentimer interval
522 StartGenTimer();
523 }
524 }
525
526 // -----------------------------------------------------------------------------
527
SetBaseStep()528 void MainFrame::SetBaseStep()
529 {
530 int i;
531 if ( GetInteger(_("Set Base Step"),
532 _("Temporarily change the current base step:"),
533 currlayer->currbase, 2, MAX_BASESTEP, &i) ) {
534 currlayer->currbase = i;
535 SetGenIncrement();
536 UpdateStatus();
537 }
538 }
539
540 // -----------------------------------------------------------------------------
541
DisplayPattern()542 void MainFrame::DisplayPattern()
543 {
544 // this routine is similar to UpdatePatternAndStatus() but if tiled windows
545 // exist it only updates the current tile if possible; ie. it's not a clone
546 // and tile views aren't synchronized
547
548 if (IsIconized()) return;
549
550 if (tilelayers && numlayers > 1 && !syncviews && currlayer->cloneid == 0) {
551 // only update the current tile
552 viewptr->Refresh(false);
553 } else {
554 // update main viewport window, possibly including all tile windows
555 // (tile windows are children of bigview)
556 if (numlayers > 1 && (stacklayers || tilelayers)) {
557 bigview->Refresh(false);
558 } else {
559 viewptr->Refresh(false);
560 }
561 }
562
563 if (showstatus) {
564 statusptr->CheckMouseLocation(infront);
565 statusptr->Refresh(false);
566 }
567 }
568
569 // -----------------------------------------------------------------------------
570
StepPattern()571 bool MainFrame::StepPattern()
572 {
573 lifealgo* curralgo = currlayer->algo;
574 if (curralgo->unbounded && (curralgo->gridwd > 0 || curralgo->gridht > 0)) {
575 // bounded grid, so temporarily set the increment to 1 so we can call
576 // CreateBorderCells() and DeleteBorderCells() around each step()
577 int savebase = currlayer->currbase;
578 int saveexpo = currlayer->currexpo;
579 bigint inc = curralgo->getIncrement();
580 curralgo->setIncrement(1);
581 while (inc > 0) {
582 if (wxGetApp().Poller()->checkevents()) {
583 SetGenIncrement(); // restore correct increment
584 return false;
585 }
586 if (savebase != currlayer->currbase || saveexpo != currlayer->currexpo) {
587 // user changed step base/exponent, so best to simply exit loop
588 break;
589 }
590 if (!curralgo->CreateBorderCells()) {
591 SetGenIncrement(); // restore correct increment
592 return false;
593 }
594 curralgo->step();
595 if (!curralgo->DeleteBorderCells()) {
596 SetGenIncrement(); // restore correct increment
597 return false;
598 }
599 if (curralgo->isrecording()) curralgo->extendtimeline();
600 inc -= 1;
601 }
602 // safe way to restore correct increment in case user altered step base/exponent
603 SetGenIncrement();
604 } else {
605 if (wxGetApp().Poller()->checkevents()) return false;
606 curralgo->step();
607 if (curralgo->isrecording()) curralgo->extendtimeline();
608 }
609
610 if (currlayer->autofit) viewptr->FitInView(0);
611
612 if (!IsIconized()) DisplayPattern();
613
614 /* enable this code if we ever implement isPeriodic()
615 if (autostop) {
616 int period = curralgo->isPeriodic();
617 if (period > 0) {
618 if (period == 1) {
619 if (curralgo->isEmpty()) {
620 statusptr->DisplayMessage(_("Pattern is empty."));
621 } else {
622 statusptr->DisplayMessage(_("Pattern is stable."));
623 }
624 } else {
625 wxString s;
626 s.Printf(_("Pattern is oscillating (period = %d)."), period);
627 statusptr->DisplayMessage(s);
628 }
629 return false;
630 }
631 }
632 */
633
634 return true;
635 }
636
637 // -----------------------------------------------------------------------------
638
DoPendingAction(bool restart)639 void MainFrame::DoPendingAction(bool restart)
640 {
641 if (command_pending) {
642 command_pending = false;
643
644 int id = cmdevent.GetId();
645 switch (id) {
646 // don't restart the generating loop after some commands
647 case wxID_NEW: NewPattern(); break;
648 case wxID_OPEN: OpenPattern(); break;
649 case ID_OPEN_CLIP: OpenClipboard(); break;
650 case ID_RESET: ResetPattern(); break;
651 case ID_SETGEN: SetGeneration(); break;
652 case ID_UNDO: currlayer->undoredo->UndoChange(); break;
653 case ID_ADD_LAYER: AddLayer(); break;
654 case ID_DUPLICATE: DuplicateLayer(); break;
655 case ID_LOAD_LEXICON: LoadLexiconPattern(); break;
656 default:
657 if ( id > ID_OPEN_RECENT && id <= ID_OPEN_RECENT + numpatterns ) {
658 OpenRecentPattern(id);
659
660 } else if ( id > ID_RUN_RECENT && id <= ID_RUN_RECENT + numscripts ) {
661 OpenRecentScript(id);
662 if (restart && !stop_after_script) {
663 wxCommandEvent goevt(wxEVT_COMMAND_MENU_SELECTED, ID_START);
664 wxPostEvent(this->GetEventHandler(), goevt);
665 // avoid clearing status message due to script like density.py
666 keepmessage = true;
667 }
668
669 } else if ( id == ID_RUN_SCRIPT ) {
670 OpenScript();
671 if (restart && !stop_after_script) {
672 wxCommandEvent goevt(wxEVT_COMMAND_MENU_SELECTED, ID_START);
673 wxPostEvent(this->GetEventHandler(), goevt);
674 // avoid clearing status message due to script like density.py
675 keepmessage = true;
676 }
677
678 } else if ( id == ID_RUN_CLIP ) {
679 RunClipboard();
680 if (restart && !stop_after_script) {
681 wxCommandEvent goevt(wxEVT_COMMAND_MENU_SELECTED, ID_START);
682 wxPostEvent(this->GetEventHandler(), goevt);
683 // avoid clearing status message due to script like density.py
684 keepmessage = true;
685 }
686
687 } else if ( id >= ID_LAYER0 && id <= ID_LAYERMAX ) {
688 int oldcloneid = currlayer->cloneid;
689 SetLayer(id - ID_LAYER0);
690 // continue generating if new layer is a clone of old layer
691 if (restart && currlayer->cloneid > 0 && currlayer->cloneid == oldcloneid) {
692 wxCommandEvent goevt(wxEVT_COMMAND_MENU_SELECTED, ID_START);
693 wxPostEvent(this->GetEventHandler(), goevt);
694 }
695
696 } else if ( id == ID_DEL_LAYER ) {
697 int wasclone = currlayer->cloneid > 0 &&
698 ((currindex == 0 && currlayer->cloneid == GetLayer(1)->cloneid) ||
699 (currindex > 0 && currlayer->cloneid == GetLayer(currindex-1)->cloneid));
700 DeleteLayer();
701 // continue generating if new layer is/was a clone of old layer
702 if (restart && wasclone) {
703 wxCommandEvent goevt(wxEVT_COMMAND_MENU_SELECTED, ID_START);
704 wxPostEvent(this->GetEventHandler(), goevt);
705 }
706
707 } else {
708 // temporarily pretend the tool/layer/edit bars are not showing
709 // to avoid Update[Tool/Layer/Edit]Bar changing button states
710 bool saveshowtool = showtool; showtool = false;
711 bool saveshowlayer = showlayer; showlayer = false;
712 bool saveshowedit = showedit; showedit = false;
713
714 // process the pending command
715 cmdevent.SetEventType(wxEVT_COMMAND_MENU_SELECTED);
716 cmdevent.SetEventObject(this);
717 this->GetEventHandler()->ProcessEvent(cmdevent);
718
719 // restore tool/layer/edit bar flags
720 showtool = saveshowtool;
721 showlayer = saveshowlayer;
722 showedit = saveshowedit;
723
724 if (restart) {
725 // call StartGenerating again
726 wxCommandEvent goevt(wxEVT_COMMAND_MENU_SELECTED, ID_START);
727 wxPostEvent(this->GetEventHandler(), goevt);
728 }
729 }
730 }
731 }
732
733 if (draw_pending) {
734 draw_pending = false;
735
736 // temporarily pretend the tool/layer/edit bars are not showing
737 // to avoid Update[Tool/Layer/Edit]Bar changing button states
738 bool saveshowtool = showtool; showtool = false;
739 bool saveshowlayer = showlayer; showlayer = false;
740 bool saveshowedit = showedit; showedit = false;
741
742 UpdateEverything();
743
744 // do the drawing by creating a mouse down event so PatternView::OnMouseDown is called again,
745 // but we need to reset mouseisdown flag which is currently true
746 viewptr->mouseisdown = false;
747 mouseevent.SetEventType(wxEVT_LEFT_DOWN);
748 mouseevent.SetEventObject(viewptr);
749 viewptr->GetEventHandler()->ProcessEvent(mouseevent);
750 while (viewptr->drawingcells) {
751 insideYield = true;
752 wxGetApp().Yield(true);
753 insideYield = false;
754 wxMilliSleep(5); // don't hog CPU
755 }
756
757 // restore tool/layer/edit bar flags
758 showtool = saveshowtool;
759 showlayer = saveshowlayer;
760 showedit = saveshowedit;
761
762 if (restart) {
763 // call StartGenerating again
764 wxCommandEvent goevt(wxEVT_COMMAND_MENU_SELECTED, ID_START);
765 wxPostEvent(this->GetEventHandler(), goevt);
766 }
767 }
768 }
769
770 // -----------------------------------------------------------------------------
771
DisplayTimingInfo()772 void MainFrame::DisplayTimingInfo()
773 {
774 if (viewptr->waitingforclick) return;
775 if (generating) {
776 endtime = stopwatch->Time();
777 endgen = currlayer->algo->getGeneration().todouble();
778 }
779 if (endtime > begintime) {
780 double secs = (double)(endtime - begintime) / 1000.0;
781 double gens = endgen - begingen;
782 wxString s;
783 s.Printf(_("%g gens in %g secs (%g gens/sec)."), gens, secs, gens/secs);
784 statusptr->DisplayMessage(s);
785 }
786 }
787
788 // -----------------------------------------------------------------------------
789
StartGenerating()790 void MainFrame::StartGenerating()
791 {
792 if (insideYield || viewptr->drawingcells || viewptr->waitingforclick) {
793 return;
794 }
795
796 if (currlayer->algo->isEmpty()) {
797 statusptr->ErrorMessage(empty_pattern);
798 return;
799 }
800
801 if (currlayer->algo->isrecording()) {
802 // don't attempt to save starting pattern here (let DeleteTimeline do it)
803 } else if (!SaveStartingPattern()) {
804 return;
805 }
806
807 // no need to test inscript or currlayer->stayclean
808 if (allowundo && !currlayer->algo->isrecording()) currlayer->undoredo->RememberGenStart();
809
810 // for DisplayTimingInfo
811 begintime = stopwatch->Time();
812 begingen = currlayer->algo->getGeneration().todouble();
813
814 // for hyperspeed
815 hypdown = 64;
816
817 generating = true;
818 wxGetApp().PollerReset();
819 UpdateUserInterface();
820
821 // only show hashing info while generating
822 lifealgo::setVerbose(currlayer->showhashinfo);
823
824 StartGenTimer();
825 }
826
827 // -----------------------------------------------------------------------------
828
FinishUp()829 void MainFrame::FinishUp()
830 {
831 // display the final pattern
832 if (currlayer->autofit) viewptr->FitInView(0);
833 if (command_pending || draw_pending) {
834 // let the pending command/draw do the update below
835 } else {
836 UpdateEverything();
837 }
838
839 // note that we must call RememberGenFinish BEFORE processing any pending command
840 if (allowundo && !currlayer->algo->isrecording()) currlayer->undoredo->RememberGenFinish();
841
842 // stop recording any timeline before processing any pending command
843 if (currlayer->algo->isrecording()) {
844 currlayer->algo->stoprecording();
845 if (currlayer->algo->getframecount() > 0) {
846 // probably best to go to last frame
847 currlayer->currframe = currlayer->algo->getframecount() - 1;
848 currlayer->autoplay = 0;
849 currlayer->tlspeed = 0;
850 currlayer->algo->gotoframe(currlayer->currframe);
851 if (currlayer->autofit) viewptr->FitInView(1);
852 }
853 if (!showtimeline) ToggleTimelineBar();
854 UpdateUserInterface();
855 }
856
857 DoPendingAction(true); // true means we can restart generating
858 }
859
860 // -----------------------------------------------------------------------------
861
StopGenerating()862 void MainFrame::StopGenerating()
863 {
864 if (gentimer->IsRunning()) gentimer->Stop();
865 generating = false;
866 wxGetApp().PollerInterrupt();
867 lifealgo::setVerbose(0);
868
869 // for DisplayTimingInfo
870 endtime = stopwatch->Time();
871 endgen = currlayer->algo->getGeneration().todouble();
872
873 if (insideYield) {
874 // we're currently in the event poller somewhere inside step(), so we must let
875 // step() complete and only call FinishUp after StepPattern has finished
876 } else {
877 FinishUp();
878 }
879 }
880
881 // -----------------------------------------------------------------------------
882
883 // this flag is used to avoid re-entrancy in OnGenTimer (note that on Windows
884 // the timer can fire while a wxMessageBox dialog is open)
885 static bool in_timer = false;
886
OnGenTimer(wxTimerEvent & WXUNUSED (event))887 void MainFrame::OnGenTimer(wxTimerEvent& WXUNUSED(event))
888 {
889 if (in_timer) return;
890 in_timer = true;
891
892 if (!StepPattern()) {
893 if (generating) {
894 // call StopGenerating() to stop gentimer
895 Stop();
896 } else {
897 // StopGenerating() was called while insideYield
898 FinishUp();
899 }
900 in_timer = false;
901 return;
902 }
903
904 if (currlayer->algo->isrecording()) {
905 if (showtimeline) UpdateTimelineBar();
906 if (currlayer->algo->getframecount() == MAX_FRAME_COUNT) {
907 if (generating) {
908 // call StopGenerating() to stop gentimer
909 Stop();
910 } else {
911 // StopGenerating() was called while insideYield
912 FinishUp();
913 }
914 wxString msg;
915 msg.Printf(_("No more frames can be recorded (maximum = %d)."), MAX_FRAME_COUNT);
916 Warning(msg);
917 in_timer = false;
918 return;
919 }
920 } else if (currlayer->hyperspeed && currlayer->algo->hyperCapable()) {
921 hypdown--;
922 if (hypdown == 0) {
923 hypdown = 64;
924 GoFaster();
925 }
926 }
927
928 if (!generating) {
929 // StopGenerating() was called while insideYield
930 FinishUp();
931 }
932
933 in_timer = false;
934 }
935
936 // -----------------------------------------------------------------------------
937
Stop()938 void MainFrame::Stop()
939 {
940 if (inscript) {
941 PassKeyToScript(WXK_ESCAPE);
942 } else if (generating) {
943 StopGenerating();
944 }
945 }
946
947 // -----------------------------------------------------------------------------
948
StartOrStop()949 void MainFrame::StartOrStop()
950 {
951 if (inscript || generating) {
952 Stop();
953 } else if (TimelineExists()) {
954 if (currlayer->algo->isrecording()) {
955 // should never happen if generating is false
956 Warning(_("Bug: recording but not generating!"));
957 } else {
958 PlayTimeline(1); // play forwards or stop if already playing
959 }
960 } else {
961 StartGenerating();
962 }
963 }
964
965 // -----------------------------------------------------------------------------
966
967 // this global flag is used to avoid re-entrancy in NextGeneration()
968 // due to holding down the space/tab key
969 static bool inNextGen = false;
970
NextGeneration(bool useinc)971 void MainFrame::NextGeneration(bool useinc)
972 {
973 if (inNextGen) return;
974 inNextGen = true;
975
976 if (!inscript && generating) {
977 Stop();
978 inNextGen = false;
979 return;
980 }
981
982 if (insideYield) {
983 // avoid calling step() recursively
984 inNextGen = false;
985 return;
986 }
987
988 if (viewptr->drawingcells || viewptr->waitingforclick) {
989 Beep();
990 inNextGen = false;
991 return;
992 }
993
994 // best if generating stops after running a script like oscar.py or goto.py
995 if (inscript) stop_after_script = true;
996
997 lifealgo* curralgo = currlayer->algo;
998 if (curralgo->isEmpty()) {
999 statusptr->ErrorMessage(empty_pattern);
1000 inNextGen = false;
1001 return;
1002 }
1003
1004 if (!SaveStartingPattern()) {
1005 inNextGen = false;
1006 return;
1007 }
1008
1009 if (allowundo) {
1010 if (currlayer->stayclean) {
1011 // script has called run/step after a new/open command has set
1012 // stayclean true by calling MarkLayerClean
1013 if (curralgo->getGeneration() == currlayer->startgen) {
1014 // starting pattern has just been saved so we need to remember
1015 // this gen change in case user does a Reset after script ends
1016 // (RememberGenFinish will be called at the end of RunScript)
1017 if (currlayer->undoredo->savegenchanges) {
1018 // script must have called reset command, so we need to call
1019 // RememberGenFinish to match earlier RememberGenStart
1020 currlayer->undoredo->savegenchanges = false;
1021 currlayer->undoredo->RememberGenFinish();
1022 }
1023 currlayer->undoredo->RememberGenStart();
1024 }
1025 } else {
1026 // !currlayer->stayclean
1027 if (inscript) {
1028 // pass in false so we don't test savegenchanges flag;
1029 // ie. we only want to save pending cell changes here
1030 SavePendingChanges(false);
1031 }
1032 currlayer->undoredo->RememberGenStart();
1033 }
1034 }
1035
1036 // curralgo->step() calls checkevents() so set generating flag
1037 generating = true;
1038
1039 // only show hashing info while generating
1040 lifealgo::setVerbose( currlayer->showhashinfo );
1041
1042 // avoid doing some things if NextGeneration is called from a script;
1043 // ie. by a run/step command
1044 if (!inscript) {
1045 if (useinc) {
1046 // for DisplayTimingInfo
1047 begintime = stopwatch->Time();
1048 begingen = curralgo->getGeneration().todouble();
1049 }
1050 wxGetApp().PollerReset();
1051 viewptr->CheckCursor(infront);
1052 }
1053
1054 bool boundedgrid = curralgo->unbounded && (curralgo->gridwd > 0 || curralgo->gridht > 0);
1055
1056 if (useinc) {
1057 // step by current increment
1058 if (curralgo->getIncrement() > bigint::one && !inscript) {
1059 UpdateToolBar();
1060 UpdateMenuItems();
1061 }
1062 if (boundedgrid) {
1063 // temporarily set the increment to 1 so we can call CreateBorderCells()
1064 // and DeleteBorderCells() around each step()
1065 int savebase = currlayer->currbase;
1066 int saveexpo = currlayer->currexpo;
1067 bigint inc = curralgo->getIncrement();
1068 curralgo->setIncrement(1);
1069 while (inc > 0) {
1070 if (wxGetApp().Poller()->checkevents()) break;
1071 if (savebase != currlayer->currbase || saveexpo != currlayer->currexpo) {
1072 // user changed step base/exponent, so reset increment to 1
1073 inc = curralgo->getIncrement();
1074 curralgo->setIncrement(1);
1075 }
1076 if (!curralgo->CreateBorderCells()) break;
1077 curralgo->step();
1078 if (!curralgo->DeleteBorderCells()) break;
1079 inc -= 1;
1080 }
1081 // safe way to restore correct increment in case user altered base/expo in above loop
1082 SetGenIncrement();
1083 } else {
1084 curralgo->step();
1085 }
1086 } else {
1087 // step by 1 gen
1088 bigint saveinc = curralgo->getIncrement();
1089 curralgo->setIncrement(1);
1090 if (boundedgrid) curralgo->CreateBorderCells();
1091 curralgo->step();
1092 if (boundedgrid) curralgo->DeleteBorderCells();
1093 curralgo->setIncrement(saveinc);
1094 }
1095
1096 generating = false;
1097
1098 lifealgo::setVerbose(0);
1099
1100 if (!inscript) {
1101 if (useinc) {
1102 // for DisplayTimingInfo (we add 1 millisec here in case it took < 1 millisec)
1103 endtime = stopwatch->Time() + 1;
1104 endgen = curralgo->getGeneration().todouble();
1105 }
1106 // autofit is only used when doing many gens
1107 if (currlayer->autofit && useinc && curralgo->getIncrement() > bigint::one)
1108 viewptr->FitInView(0);
1109 UpdateEverything();
1110 }
1111
1112 // we must call RememberGenFinish BEFORE processing any pending command
1113 if (allowundo && !currlayer->stayclean)
1114 currlayer->undoredo->RememberGenFinish();
1115
1116 // process any pending command seen via checkevents() in curralgo->step()
1117 if (!inscript)
1118 DoPendingAction(false); // false means don't restart generating loop
1119
1120 inNextGen = false;
1121 }
1122
1123 // -----------------------------------------------------------------------------
1124
ToggleAutoFit()1125 void MainFrame::ToggleAutoFit()
1126 {
1127 currlayer->autofit = !currlayer->autofit;
1128
1129 // we only use autofit when generating; that's why the Auto Fit item
1130 // is in the Control menu and not in the View menu
1131 if (generating && currlayer->autofit) {
1132 viewptr->FitInView(0);
1133 UpdateEverything();
1134 }
1135 }
1136
1137 // -----------------------------------------------------------------------------
1138
ToggleHyperspeed()1139 void MainFrame::ToggleHyperspeed()
1140 {
1141 currlayer->hyperspeed = !currlayer->hyperspeed;
1142 }
1143
1144 // -----------------------------------------------------------------------------
1145
ToggleHashInfo()1146 void MainFrame::ToggleHashInfo()
1147 {
1148 currlayer->showhashinfo = !currlayer->showhashinfo;
1149
1150 // only show hashing info while generating
1151 if (generating) lifealgo::setVerbose( currlayer->showhashinfo );
1152 }
1153
1154 // -----------------------------------------------------------------------------
1155
ToggleShowPopulation()1156 void MainFrame::ToggleShowPopulation()
1157 {
1158 showpopulation = !showpopulation;
1159
1160 if (generating && showstatus && !IsIconized()) statusptr->Refresh(false);
1161 }
1162
1163 // -----------------------------------------------------------------------------
1164
ClearOutsideGrid()1165 void MainFrame::ClearOutsideGrid()
1166 {
1167 // check current pattern and clear any live cells outside bounded grid
1168 bool patternchanged = false;
1169 bool savechanges = allowundo && !currlayer->stayclean;
1170
1171 // might also need to truncate selection
1172 currlayer->currsel.CheckGridEdges();
1173
1174 if (!currlayer->algo->unbounded) {
1175 if (currlayer->algo->clipped_cells.size() > 0) {
1176 // cells outside the grid were clipped
1177 if (savechanges) {
1178 for (size_t i = 0; i < currlayer->algo->clipped_cells.size(); i += 3) {
1179 int x = currlayer->algo->clipped_cells[i];
1180 int y = currlayer->algo->clipped_cells[i+1];
1181 int s = currlayer->algo->clipped_cells[i+2];
1182 currlayer->undoredo->SaveCellChange(x, y, s, 0);
1183 }
1184 }
1185 currlayer->algo->clipped_cells.clear();
1186 patternchanged = true;
1187 }
1188
1189 } else {
1190 // algo uses an unbounded grid
1191 if (currlayer->algo->isEmpty()) return;
1192
1193 // check if current pattern is too big to use nextcell/setcell
1194 bigint top, left, bottom, right;
1195 currlayer->algo->findedges(&top, &left, &bottom, &right);
1196 if ( viewptr->OutsideLimits(top, left, bottom, right) ) {
1197 statusptr->ErrorMessage(_("Pattern too big to check (outside +/- 10^9 boundary)."));
1198 return;
1199 }
1200
1201 int itop = top.toint();
1202 int ileft = left.toint();
1203 int ibottom = bottom.toint();
1204 int iright = right.toint();
1205
1206 // no need to do anything if pattern is entirely within grid
1207 int gtop = currlayer->algo->gridtop.toint();
1208 int gleft = currlayer->algo->gridleft.toint();
1209 int gbottom = currlayer->algo->gridbottom.toint();
1210 int gright = currlayer->algo->gridright.toint();
1211 if (currlayer->algo->gridwd == 0) {
1212 // grid has infinite width
1213 gleft = INT_MIN;
1214 gright = INT_MAX;
1215 }
1216 if (currlayer->algo->gridht == 0) {
1217 // grid has infinite height
1218 gtop = INT_MIN;
1219 gbottom = INT_MAX;
1220 }
1221 if (itop >= gtop && ileft >= gleft && ibottom <= gbottom && iright <= gright) {
1222 return;
1223 }
1224
1225 int ht = ibottom - itop + 1;
1226 int cx, cy;
1227
1228 // for showing accurate progress we need to add pattern height to pop count
1229 // in case this is a huge pattern with many blank rows
1230 double maxcount = currlayer->algo->getPopulation().todouble() + ht;
1231 double accumcount = 0;
1232 int currcount = 0;
1233 bool abort = false;
1234 int v = 0;
1235 BeginProgress(_("Checking cells outside grid"));
1236
1237 lifealgo* curralgo = currlayer->algo;
1238 for ( cy=itop; cy<=ibottom; cy++ ) {
1239 currcount++;
1240 for ( cx=ileft; cx<=iright; cx++ ) {
1241 int skip = curralgo->nextcell(cx, cy, v);
1242 if (skip >= 0) {
1243 // found next live cell in this row
1244 cx += skip;
1245 if (cx < gleft || cx > gright || cy < gtop || cy > gbottom) {
1246 // clear cell outside grid
1247 if (savechanges) currlayer->undoredo->SaveCellChange(cx, cy, v, 0);
1248 curralgo->setcell(cx, cy, 0);
1249 patternchanged = true;
1250 }
1251 currcount++;
1252 } else {
1253 cx = iright; // done this row
1254 }
1255 if (currcount > 1024) {
1256 accumcount += currcount;
1257 currcount = 0;
1258 abort = AbortProgress(accumcount / maxcount, wxEmptyString);
1259 if (abort) break;
1260 }
1261 }
1262 if (abort) break;
1263 }
1264
1265 curralgo->endofpattern();
1266 EndProgress();
1267 }
1268
1269 if (patternchanged) {
1270 statusptr->ErrorMessage(_("Pattern was truncated (live cells were outside grid)."));
1271 }
1272 }
1273
1274 // -----------------------------------------------------------------------------
1275
ReduceCellStates(int newmaxstate)1276 void MainFrame::ReduceCellStates(int newmaxstate)
1277 {
1278 // check current pattern and reduce any cell states > newmaxstate
1279 bool patternchanged = false;
1280 bool savechanges = allowundo && !currlayer->stayclean;
1281
1282 // check if current pattern is too big to use nextcell/setcell
1283 bigint top, left, bottom, right;
1284 currlayer->algo->findedges(&top, &left, &bottom, &right);
1285 if ( viewptr->OutsideLimits(top, left, bottom, right) ) {
1286 statusptr->ErrorMessage(_("Pattern too big to check (outside +/- 10^9 boundary)."));
1287 return;
1288 }
1289
1290 int itop = top.toint();
1291 int ileft = left.toint();
1292 int ibottom = bottom.toint();
1293 int iright = right.toint();
1294 int ht = ibottom - itop + 1;
1295 int cx, cy;
1296
1297 // for showing accurate progress we need to add pattern height to pop count
1298 // in case this is a huge pattern with many blank rows
1299 double maxcount = currlayer->algo->getPopulation().todouble() + ht;
1300 double accumcount = 0;
1301 int currcount = 0;
1302 bool abort = false;
1303 int v = 0;
1304 BeginProgress(_("Checking cell states"));
1305
1306 lifealgo* curralgo = currlayer->algo;
1307 for ( cy=itop; cy<=ibottom; cy++ ) {
1308 currcount++;
1309 for ( cx=ileft; cx<=iright; cx++ ) {
1310 int skip = curralgo->nextcell(cx, cy, v);
1311 if (skip >= 0) {
1312 // found next live cell in this row
1313 cx += skip;
1314 if (v > newmaxstate) {
1315 // reduce cell's current state to largest state
1316 if (savechanges) currlayer->undoredo->SaveCellChange(cx, cy, v, newmaxstate);
1317 curralgo->setcell(cx, cy, newmaxstate);
1318 patternchanged = true;
1319 }
1320 currcount++;
1321 } else {
1322 cx = iright; // done this row
1323 }
1324 if (currcount > 1024) {
1325 accumcount += currcount;
1326 currcount = 0;
1327 abort = AbortProgress(accumcount / maxcount, wxEmptyString);
1328 if (abort) break;
1329 }
1330 }
1331 if (abort) break;
1332 }
1333
1334 curralgo->endofpattern();
1335 EndProgress();
1336
1337 if (patternchanged) {
1338 statusptr->ErrorMessage(_("Pattern has changed (new rule has fewer states)."));
1339 }
1340 }
1341
1342 // -----------------------------------------------------------------------------
1343
ShowRuleDialog()1344 void MainFrame::ShowRuleDialog()
1345 {
1346 if (inscript || viewptr->waitingforclick) return;
1347
1348 if (generating) {
1349 command_pending = true;
1350 cmdevent.SetId(ID_SETRULE);
1351 Stop();
1352 return;
1353 }
1354
1355 algo_type oldalgo = currlayer->algtype;
1356 wxString oldrule = wxString(currlayer->algo->getrule(), wxConvLocal);
1357 int oldmaxstate = currlayer->algo->NumCellStates() - 1;
1358
1359 // selection might change if grid becomes smaller,
1360 // so save current selection for RememberRuleChange/RememberAlgoChange
1361 viewptr->SaveCurrentSelection();
1362
1363 if (ChangeRule()) {
1364 if (currlayer->algtype != oldalgo) {
1365 // ChangeAlgorithm was called so we're almost done;
1366 // we just have to call UpdateEverything now that the main window is active
1367 UpdateEverything();
1368 return;
1369 }
1370
1371 // show new rule in window title (but don't change file name);
1372 // even if the rule didn't change we still need to do this because
1373 // the user might have simply added/deleted a named rule
1374 SetWindowTitle(wxEmptyString);
1375
1376 // check if the rule string changed, or the number of states changed
1377 // (the latter might happen if user modified .rule file)
1378 wxString newrule = wxString(currlayer->algo->getrule(), wxConvLocal);
1379 int newmaxstate = currlayer->algo->NumCellStates() - 1;
1380 if (oldrule != newrule || oldmaxstate != newmaxstate) {
1381
1382 // if pattern exists and is at starting gen then ensure savestart is true
1383 // so that SaveStartingPattern will save pattern to suitable file
1384 // (and thus undo/reset will work correctly)
1385 if (currlayer->algo->getGeneration() == currlayer->startgen && !currlayer->algo->isEmpty()) {
1386 currlayer->savestart = true;
1387 }
1388
1389 // if grid is bounded then remove any live cells outside grid edges
1390 if (currlayer->algo->gridwd > 0 || currlayer->algo->gridht > 0) {
1391 ClearOutsideGrid();
1392 }
1393
1394 // rule change might have changed the number of cell states;
1395 // if there are fewer states then pattern might change
1396 if (newmaxstate < oldmaxstate && !currlayer->algo->isEmpty()) {
1397 ReduceCellStates(newmaxstate);
1398 }
1399
1400 if (allowundo) {
1401 currlayer->undoredo->RememberRuleChange(oldrule);
1402 }
1403 }
1404
1405 // switch to default colors and icons for new rule (we need to do this even if
1406 // oldrule == newrule in case colors or icons changed)
1407 UpdateLayerColors();
1408
1409 // pattern or colors or icons might have changed
1410 UpdateEverything();
1411 }
1412 }
1413
1414 // -----------------------------------------------------------------------------
1415
ChangeAlgorithm(algo_type newalgotype,const wxString & newrule,bool inundoredo)1416 void MainFrame::ChangeAlgorithm(algo_type newalgotype, const wxString& newrule, bool inundoredo)
1417 {
1418 if (newalgotype == currlayer->algtype) return;
1419
1420 // check if current pattern is too big to use nextcell/setcell
1421 bigint top, left, bottom, right;
1422 if ( !currlayer->algo->isEmpty() ) {
1423 currlayer->algo->findedges(&top, &left, &bottom, &right);
1424 if ( viewptr->OutsideLimits(top, left, bottom, right) ) {
1425 statusptr->ErrorMessage(_("Pattern cannot be converted (outside +/- 10^9 boundary)."));
1426 return;
1427 }
1428 }
1429
1430 if (generating) {
1431 command_pending = true;
1432 cmdevent.SetId(ID_ALGO0 + newalgotype);
1433 Stop();
1434 return;
1435 }
1436
1437 // save changes if undo/redo is enabled and script isn't constructing a pattern
1438 // and we're not undoing/redoing an earlier algo change
1439 bool savechanges = allowundo && !currlayer->stayclean && !inundoredo;
1440 if (savechanges && inscript) {
1441 // note that we must save pending gen changes BEFORE changing algo type
1442 // otherwise temporary files won't be the correct type (mc or rle)
1443 SavePendingChanges();
1444 }
1445
1446 // selection might change if grid becomes smaller,
1447 // so save current selection for RememberAlgoChange
1448 if (savechanges) viewptr->SaveCurrentSelection();
1449
1450 bool rulechanged = false;
1451 wxString oldrule = wxString(currlayer->algo->getrule(), wxConvLocal);
1452
1453 // change algorithm type, reset step size, and update status bar immediately
1454 algo_type oldalgo = currlayer->algtype;
1455 currlayer->algtype = newalgotype;
1456 currlayer->currbase = algoinfo[newalgotype]->defbase;
1457 currlayer->currexpo = 0;
1458 UpdateStatus();
1459
1460 // create a new universe of the requested flavor
1461 lifealgo* newalgo = CreateNewUniverse(newalgotype);
1462
1463 if (inundoredo) {
1464 // switch to given newrule
1465 const char* err = newalgo->setrule( newrule.mb_str(wxConvLocal) );
1466 if (err) newalgo->setrule( newalgo->DefaultRule() );
1467 } else {
1468 const char* err;
1469 if (newrule.IsEmpty()) {
1470 // try to use same rule
1471 err = newalgo->setrule( currlayer->algo->getrule() );
1472 } else {
1473 // switch to newrule
1474 err = newalgo->setrule( newrule.mb_str(wxConvLocal) );
1475 rulechanged = true;
1476 }
1477 if (err) {
1478 wxString defrule = wxString(newalgo->DefaultRule(), wxConvLocal);
1479 if (newrule.IsEmpty() && oldrule.Find(':') >= 0) {
1480 // switch to new algo's default rule, but preserve the topology in oldrule
1481 // so we can do things like switch from "LifeHistory:T30,20" in RuleLoader
1482 // to "B3/S23:T30,20" in QuickLife
1483 if (defrule.Find(':') >= 0) {
1484 // default rule shouldn't have a suffix but play safe and remove it
1485 defrule = defrule.BeforeFirst(':');
1486 }
1487 defrule += wxT(":");
1488 defrule += oldrule.AfterFirst(':');
1489 }
1490 err = newalgo->setrule( defrule.mb_str(wxConvLocal) );
1491 // shouldn't ever fail but play safe
1492 if (err) newalgo->setrule( newalgo->DefaultRule() );
1493 rulechanged = true;
1494 }
1495 }
1496
1497 // set same gen count
1498 newalgo->setGeneration( currlayer->algo->getGeneration() );
1499
1500 bool patternchanged = false;
1501 if ( !currlayer->algo->isEmpty() ) {
1502 // copy pattern in current universe to new universe
1503 int itop = top.toint();
1504 int ileft = left.toint();
1505 int ibottom = bottom.toint();
1506 int iright = right.toint();
1507 int ht = ibottom - itop + 1;
1508 int cx, cy;
1509
1510 // for showing accurate progress we need to add pattern height to pop count
1511 // in case this is a huge pattern with many blank rows
1512 double maxcount = currlayer->algo->getPopulation().todouble() + ht;
1513 double accumcount = 0;
1514 int currcount = 0;
1515 bool abort = false;
1516 int v = 0;
1517 BeginProgress(_("Converting pattern"));
1518
1519 // set newalgo's grid edges so we can save cells that are outside grid
1520 int gtop = newalgo->gridtop.toint();
1521 int gleft = newalgo->gridleft.toint();
1522 int gbottom = newalgo->gridbottom.toint();
1523 int gright = newalgo->gridright.toint();
1524 if (newalgo->gridwd == 0) {
1525 // grid has infinite width
1526 gleft = INT_MIN;
1527 gright = INT_MAX;
1528 }
1529 if (newalgo->gridht == 0) {
1530 // grid has infinite height
1531 gtop = INT_MIN;
1532 gbottom = INT_MAX;
1533 }
1534
1535 // need to check for state change if new algo has fewer states than old algo
1536 int newmaxstate = newalgo->NumCellStates() - 1;
1537
1538 lifealgo* curralgo = currlayer->algo;
1539 for ( cy=itop; cy<=ibottom; cy++ ) {
1540 currcount++;
1541 for ( cx=ileft; cx<=iright; cx++ ) {
1542 int skip = curralgo->nextcell(cx, cy, v);
1543 if (skip >= 0) {
1544 // found next live cell in this row
1545 cx += skip;
1546 if (cx < gleft || cx > gright || cy < gtop || cy > gbottom) {
1547 // cx,cy is outside grid
1548 if (savechanges) currlayer->undoredo->SaveCellChange(cx, cy, v, 0);
1549 // no need to clear cell from curralgo (that universe will soon be deleted)
1550 patternchanged = true;
1551 } else {
1552 if (v > newmaxstate) {
1553 // reduce v to largest state in new algo
1554 if (savechanges) currlayer->undoredo->SaveCellChange(cx, cy, v, newmaxstate);
1555 v = newmaxstate;
1556 patternchanged = true;
1557 }
1558 newalgo->setcell(cx, cy, v);
1559 }
1560 currcount++;
1561 } else {
1562 cx = iright; // done this row
1563 }
1564 if (currcount > 1024) {
1565 accumcount += currcount;
1566 currcount = 0;
1567 abort = AbortProgress(accumcount / maxcount, wxEmptyString);
1568 if (abort) break;
1569 }
1570 }
1571 if (abort) break;
1572 }
1573
1574 newalgo->endofpattern();
1575 EndProgress();
1576 }
1577
1578 // delete old universe and point current universe to new universe
1579 delete currlayer->algo;
1580 currlayer->algo = newalgo;
1581 SetGenIncrement();
1582
1583 // if new grid is bounded then we might need to truncate the selection
1584 if (currlayer->algo->gridwd > 0 || currlayer->algo->gridht > 0) {
1585 currlayer->currsel.CheckGridEdges();
1586 }
1587
1588 // switch to default colors for new algo+rule
1589 UpdateLayerColors();
1590
1591 if (!inundoredo) {
1592 // if pattern exists and is at starting gen then set savestart true
1593 // so that SaveStartingPattern will save pattern to suitable file
1594 // (and thus ResetPattern will work correctly)
1595 if (currlayer->algo->getGeneration() == currlayer->startgen && !currlayer->algo->isEmpty()) {
1596 currlayer->savestart = true;
1597 }
1598
1599 if (rulechanged) {
1600 // show new rule in window title (but don't change file name)
1601 SetWindowTitle(wxEmptyString);
1602 if (newrule.IsEmpty()) {
1603 if (patternchanged) {
1604 statusptr->ErrorMessage(_("Rule has changed and pattern has changed."));
1605 } else {
1606 // don't beep
1607 statusptr->DisplayMessage(_("Rule has changed."));
1608 }
1609 } else {
1610 if (patternchanged) {
1611 statusptr->ErrorMessage(_("Algorithm has changed and pattern has changed."));
1612 } else {
1613 // don't beep
1614 statusptr->DisplayMessage(_("Algorithm has changed."));
1615 }
1616 }
1617 } else if (patternchanged) {
1618 statusptr->ErrorMessage(_("Pattern has changed."));
1619 }
1620
1621 if (!inscript) {
1622 UpdateEverything();
1623 }
1624 }
1625
1626 if (savechanges) {
1627 currlayer->undoredo->RememberAlgoChange(oldalgo, oldrule);
1628 }
1629 }
1630
1631 // -----------------------------------------------------------------------------
1632
CreateTABLE(const wxString & tablepath)1633 static wxString CreateTABLE(const wxString& tablepath)
1634 {
1635 wxString contents = wxT("\n@TABLE\n\n");
1636 // append contents of .table file
1637 FILE* f = OPENFILE(tablepath);
1638 if (f) {
1639 const int MAXLINELEN = 4095;
1640 char linebuf[MAXLINELEN + 1];
1641 linereader reader(f);
1642 while (true) {
1643 if (reader.fgets(linebuf, MAXLINELEN) == 0) break;
1644 contents += wxString(linebuf, wxConvLocal);
1645 contents += wxT("\n");
1646 }
1647 reader.close();
1648 } else {
1649 std::ostringstream oss;
1650 oss << "Could not read .table file:\n" << tablepath.mb_str(wxConvLocal);
1651 throw std::runtime_error(oss.str().c_str());
1652 }
1653 return contents;
1654 }
1655
1656 // -----------------------------------------------------------------------------
1657
CreateEmptyTABLE(const wxString & folder,const wxString & prefix,const wxSortedArrayString & allfiles)1658 static wxString CreateEmptyTABLE(const wxString& folder, const wxString& prefix,
1659 const wxSortedArrayString& allfiles)
1660 {
1661 // create a valid table that does nothing
1662 wxString contents = wxT("\nThis file contains colors and/or icons shared by ");
1663 contents += prefix;
1664 contents += wxT("-* rules.\n");
1665 contents += wxT("\n@TABLE\n\n");
1666
1667 // search allfiles for 1st prefix-*.table/tree file and extract numstates and neighborhood
1668 wxString numstates, neighborhood;
1669 for (size_t n = 0; n < allfiles.GetCount(); n++) {
1670 wxString filename = allfiles[n];
1671 if (filename.EndsWith(wxT(".table")) || filename.EndsWith(wxT(".tree"))) {
1672 if (prefix == filename.BeforeLast('-')) {
1673 wxString filepath = folder + filename;
1674 FILE* f = OPENFILE(filepath);
1675 if (f) {
1676 const int MAXLINELEN = 4095;
1677 char linebuf[MAXLINELEN + 1];
1678 linereader reader(f);
1679 while (true) {
1680 if (reader.fgets(linebuf, MAXLINELEN) == 0) break;
1681 if (strncmp(linebuf, "n_states:", 9) == 0) {
1682 numstates = wxString(linebuf,wxConvLocal) + wxT("\n");
1683 } else if (strncmp(linebuf, "num_states=", 11) == 0) {
1684 // convert to table syntax
1685 numstates = wxT("n_states:") + wxString(linebuf+11,wxConvLocal);
1686 numstates += wxT("\n");
1687 } else if (strncmp(linebuf, "neighborhood:", 13) == 0) {
1688 neighborhood = wxString(linebuf,wxConvLocal) + wxT("\n");
1689 break;
1690 } else if (strncmp(linebuf, "num_neighbors=", 14) == 0) {
1691 // convert to table syntax
1692 neighborhood = wxT("neighborhood:");
1693 if (linebuf[14] == '4')
1694 neighborhood += wxT("vonNeumann\n");
1695 else
1696 neighborhood += wxT("Moore\n");
1697 break;
1698 }
1699 }
1700 reader.close();
1701 } else {
1702 std::ostringstream oss;
1703 oss << "Could not read .table/tree file:\n" << filepath.mb_str(wxConvLocal);
1704 throw std::runtime_error(oss.str().c_str());
1705 }
1706 }
1707 }
1708 }
1709
1710 if (numstates.length() == 0) {
1711 numstates = wxT("n_states:256\n");
1712 wxString msg = _("Could not find ") + prefix;
1713 msg += _("-*.table/tree to set n_states in ");
1714 msg += prefix;
1715 msg += _("-shared.rule.");
1716 Warning(msg);
1717 }
1718
1719 contents += numstates;
1720 contents += neighborhood;
1721 contents += wxT("symmetries:none\n"); // anything valid would do
1722 contents += wxT("# do nothing\n"); // no transitions
1723
1724 return contents;
1725 }
1726
1727 // -----------------------------------------------------------------------------
1728
CreateTREE(const wxString & treepath)1729 static wxString CreateTREE(const wxString& treepath)
1730 {
1731 wxString contents = wxT("\n@TREE\n\n");
1732 // append contents of .tree file
1733 FILE* f = OPENFILE(treepath);
1734 if (f) {
1735 const int MAXLINELEN = 4095;
1736 char linebuf[MAXLINELEN + 1];
1737 linereader reader(f);
1738 while (true) {
1739 if (reader.fgets(linebuf, MAXLINELEN) == 0) break;
1740 contents += wxString(linebuf, wxConvLocal);
1741 contents += wxT("\n");
1742 }
1743 reader.close();
1744 } else {
1745 std::ostringstream oss;
1746 oss << "Could not read .tree file:\n" << treepath.mb_str(wxConvLocal);
1747 throw std::runtime_error(oss.str().c_str());
1748 }
1749 return contents;
1750 }
1751
1752 // -----------------------------------------------------------------------------
1753
CreateCOLORS(const wxString & colorspath)1754 static wxString CreateCOLORS(const wxString& colorspath)
1755 {
1756 wxString contents = wxT("\n@COLORS\n\n");
1757 FILE* f = OPENFILE(colorspath);
1758 if (f) {
1759 const int MAXLINELEN = 4095;
1760 char linebuf[MAXLINELEN + 1];
1761 linereader reader(f);
1762 while (true) {
1763 if (reader.fgets(linebuf, MAXLINELEN) == 0) break;
1764 int skip = 0;
1765 if (strncmp(linebuf, "color", 5) == 0 ||
1766 strncmp(linebuf, "gradient", 8) == 0) {
1767 // strip off everything before 1st digit
1768 while (linebuf[skip] && (linebuf[skip] < '0' || linebuf[skip] > '9')) {
1769 skip++;
1770 }
1771 }
1772 contents += wxString(linebuf + skip, wxConvLocal);
1773 contents += wxT("\n");
1774 }
1775 reader.close();
1776 } else {
1777 std::ostringstream oss;
1778 oss << "Could not read .colors file:\n" << colorspath.mb_str(wxConvLocal);
1779 throw std::runtime_error(oss.str().c_str());
1780 }
1781 return contents;
1782 }
1783
1784 // -----------------------------------------------------------------------------
1785
CreateStateColors(wxImage image,int numicons)1786 static wxString CreateStateColors(wxImage image, int numicons)
1787 {
1788 wxString contents = wxT("\n@COLORS\n\n");
1789
1790 // if the last icon has only 1 color then assume it is the extra 15x15 icon
1791 // supplied to set the color of state 0
1792 if (numicons > 1) {
1793 wxImage icon = image.GetSubImage(wxRect((numicons-1)*15, 0, 15, 15));
1794 if (icon.CountColours(1) == 1) {
1795 unsigned char* idata = icon.GetData();
1796 unsigned char R = idata[0];
1797 unsigned char G = idata[1];
1798 unsigned char B = idata[2];
1799 contents += wxString::Format(wxT("0 %d %d %d\n"), R, G, B);
1800 numicons--;
1801 }
1802 }
1803
1804 // set non-icon colors for each live state to the average of the non-black pixels
1805 // in each 15x15 icon (note we've skipped the extra icon detected above)
1806 for (int i = 0; i < numicons; i++) {
1807 wxImage icon = image.GetSubImage(wxRect(i*15, 0, 15, 15));
1808 int nbcount = 0; // non-black pixels
1809 int totalR = 0;
1810 int totalG = 0;
1811 int totalB = 0;
1812 unsigned char* idata = icon.GetData();
1813 for (int y = 0; y < 15; y++) {
1814 for (int x = 0; x < 15; x++) {
1815 long pos = (y * 15 + x) * 3;
1816 unsigned char R = idata[pos];
1817 unsigned char G = idata[pos+1];
1818 unsigned char B = idata[pos+2];
1819 if (R > 0 || G > 0 || B > 0) {
1820 // non-black pixel
1821 nbcount++;
1822 totalR += R;
1823 totalG += G;
1824 totalB += B;
1825 }
1826 }
1827 }
1828 if (nbcount > 0) {
1829 contents += wxString::Format(wxT("%d %d %d %d\n"), i+1, totalR/nbcount, totalG/nbcount, totalB/nbcount);
1830 } else {
1831 // unlikely, but avoid div by zero
1832 contents += wxString::Format(wxT("%d 0 0 0\n"), i+1);
1833 }
1834 }
1835
1836 return contents;
1837 }
1838
1839 // -----------------------------------------------------------------------------
1840
hex2(int i)1841 static wxString hex2(int i)
1842 {
1843 // convert given number from 0..255 into 2 hex digits
1844 wxString result = wxT("xx");
1845 const char* hexdigit = "0123456789ABCDEF";
1846 result[0] = hexdigit[i / 16];
1847 result[1] = hexdigit[i % 16];
1848 return result;
1849 }
1850
1851 // -----------------------------------------------------------------------------
1852
CreateXPM(const wxString & iconspath,wxImage image,int size,int numicons)1853 static wxString CreateXPM(const wxString& iconspath, wxImage image, int size, int numicons)
1854 {
1855 // create XPM data for given set of icons
1856 wxString contents = wxT("\nXPM\n");
1857
1858 int charsperpixel = 1;
1859 const char* cindex = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1860 wxImageHistogram histogram;
1861 int numcolors = image.ComputeHistogram(histogram);
1862 if (numcolors > 256) {
1863 std::ostringstream oss;
1864 oss << "Image in " << iconspath.mb_str(wxConvLocal) << " has more than 256 colors.";
1865 throw std::runtime_error(oss.str().c_str());
1866 }
1867 if (numcolors > 26) charsperpixel = 2; // AABA..PA, ABBB..PB, ... , APBP..PP
1868
1869 contents += wxT("/* width height num_colors chars_per_pixel */\n");
1870 contents += wxString::Format(wxT("\"%d %d %d %d\"\n"), size, size*numicons, numcolors, charsperpixel);
1871
1872 contents += wxT("/* colors */\n");
1873 int n = 0;
1874 for (wxImageHistogram::iterator entry = histogram.begin(); entry != histogram.end(); ++entry) {
1875 unsigned long key = entry->first;
1876 unsigned char R = (key & 0xFF0000) >> 16;
1877 unsigned char G = (key & 0x00FF00) >> 8;
1878 unsigned char B = (key & 0x0000FF);
1879 if (R == 0 && G == 0 && B == 0) {
1880 // nicer to show . or .. for black pixels
1881 contents += wxT("\".");
1882 if (charsperpixel == 2) contents += wxT(".");
1883 contents += wxT(" c #000000\"\n");
1884 } else {
1885 wxString hexcolor = wxT("#");
1886 hexcolor += hex2(R);
1887 hexcolor += hex2(G);
1888 hexcolor += hex2(B);
1889 contents += wxT("\"");
1890 if (charsperpixel == 1) {
1891 contents += cindex[n];
1892 } else {
1893 contents += cindex[n % 16];
1894 contents += cindex[n / 16];
1895 }
1896 contents += wxT(" c ");
1897 contents += hexcolor;
1898 contents += wxT("\"\n");
1899 }
1900 n++;
1901 }
1902
1903 for (int i = 0; i < numicons; i++) {
1904 contents += wxString::Format(wxT("/* icon for state %d */\n"), i+1);
1905 wxImage icon = image.GetSubImage(wxRect(i*15, 0, size, size));
1906 unsigned char* idata = icon.GetData();
1907 for (int y = 0; y < size; y++) {
1908 contents += wxT("\"");
1909 for (int x = 0; x < size; x++) {
1910 long pos = (y * size + x) * 3;
1911 unsigned char R = idata[pos];
1912 unsigned char G = idata[pos+1];
1913 unsigned char B = idata[pos+2];
1914 if (R == 0 && G == 0 && B == 0) {
1915 // nicer to show . or .. for black pixels
1916 contents += wxT(".");
1917 if (charsperpixel == 2) contents += wxT(".");
1918 } else {
1919 n = 0;
1920 unsigned long thisRGB = wxImageHistogram::MakeKey(R,G,B);
1921 for (wxImageHistogram::iterator entry = histogram.begin(); entry != histogram.end(); ++entry) {
1922 if (thisRGB == entry->first) break;
1923 n++;
1924 }
1925 if (charsperpixel == 1) {
1926 contents += cindex[n];
1927 } else {
1928 contents += cindex[n % 16];
1929 contents += cindex[n / 16];
1930 }
1931 }
1932 }
1933 contents += wxT("\"\n");
1934 }
1935 }
1936
1937 return contents;
1938 }
1939
1940 // -----------------------------------------------------------------------------
1941
CreateICONS(const wxString & iconspath,bool nocolors)1942 static wxString CreateICONS(const wxString& iconspath, bool nocolors)
1943 {
1944 wxString contents = wxT("\n@ICONS\n");
1945 wxImage image;
1946 if (image.LoadFile(iconspath)) {
1947 int wd = image.GetWidth();
1948 int ht = image.GetHeight();
1949 if (ht != 15 && ht != 22) {
1950 std::ostringstream oss;
1951 oss << "Image in " << iconspath.mb_str(wxConvLocal) <<
1952 " has incorrect height (should be 15 or 22).";
1953 throw std::runtime_error(oss.str().c_str());
1954 }
1955 if (wd % 15 > 0) {
1956 std::ostringstream oss;
1957 oss << "Image in " << iconspath.mb_str(wxConvLocal) <<
1958 " has incorrect width (should be multiple of 15).";
1959 throw std::runtime_error(oss.str().c_str());
1960 }
1961 int numicons = wd / 15;
1962
1963 // WARNING: MultiColorImage must be called 1st in the next test because
1964 // we want image to be converted to black-and-white if it only uses 2 colors
1965 // (for compatibility with Golly 2.4 and older)
1966 if (MultiColorImage(image) && nocolors) {
1967 // the .icons file is multi-color and there was no .colors file,
1968 // so prepend a @COLORS section that sets non-icon colors
1969 contents = CreateStateColors(image.GetSubImage(wxRect(0,0,wd,15)), numicons) + contents;
1970 }
1971
1972 if (ht == 15) {
1973 contents += CreateXPM(iconspath, image, 15, numicons);
1974 } else {
1975 contents += CreateXPM(iconspath, image.GetSubImage(wxRect(0,0,wd,15)), 15, numicons);
1976 contents += CreateXPM(iconspath, image.GetSubImage(wxRect(0,15,wd,7)), 7, numicons);
1977 }
1978 } else {
1979 std::ostringstream oss;
1980 oss << "Could not load image from .icons file:\n" << iconspath.mb_str(wxConvLocal);
1981 throw std::runtime_error(oss.str().c_str());
1982 }
1983 return contents;
1984 }
1985
1986 // -----------------------------------------------------------------------------
1987
CreateOneRule(const wxString & rulefile,const wxString & folder,const wxSortedArrayString & allfiles,wxString & htmlinfo)1988 static void CreateOneRule(const wxString& rulefile, const wxString& folder,
1989 const wxSortedArrayString& allfiles, wxString& htmlinfo)
1990 {
1991 wxString tabledata, treedata, colordata, icondata;
1992 wxString rulename = rulefile.BeforeLast('.');
1993
1994 if (rulename.EndsWith(wxT("-shared"))) {
1995 // create a .rule file with colors and/or icons shared by other .rule files
1996 wxString prefix = rulename.BeforeLast('-');
1997
1998 tabledata = CreateEmptyTABLE(folder, prefix, allfiles);
1999
2000 wxString sharedcolors = prefix + wxT(".colors");
2001 if (allfiles.Index(sharedcolors) != wxNOT_FOUND)
2002 colordata = CreateCOLORS(folder + sharedcolors);
2003
2004 wxString sharedicons = prefix + wxT(".icons");
2005 if (allfiles.Index(sharedicons) != wxNOT_FOUND)
2006 icondata = CreateICONS(folder + sharedicons, colordata.length() == 0);
2007
2008 } else {
2009 wxString tablefile = rulename + wxT(".table");
2010 wxString treefile = rulename + wxT(".tree");
2011 wxString colorsfile = rulename + wxT(".colors");
2012 wxString iconsfile = rulename + wxT(".icons");
2013
2014 if (allfiles.Index(tablefile) != wxNOT_FOUND)
2015 tabledata = CreateTABLE(folder + tablefile);
2016
2017 if (allfiles.Index(treefile) != wxNOT_FOUND)
2018 treedata = CreateTREE(folder + treefile);
2019
2020 if (allfiles.Index(colorsfile) != wxNOT_FOUND)
2021 colordata = CreateCOLORS(folder + colorsfile);
2022
2023 if (allfiles.Index(iconsfile) != wxNOT_FOUND)
2024 icondata = CreateICONS(folder + iconsfile, colordata.length() == 0);
2025 }
2026
2027 wxString contents = wxT("@RULE ") + rulename + wxT("\n");
2028 contents += tabledata;
2029 contents += treedata;
2030 contents += colordata;
2031 contents += icondata;
2032
2033 // write contents to .rule file
2034 wxString rulepath = folder + rulefile;
2035 wxFile outfile(rulepath, wxFile::write);
2036 if (outfile.IsOpened()) {
2037 outfile.Write(contents);
2038 outfile.Close();
2039 } else {
2040 std::ostringstream oss;
2041 oss << "Could not create rule file:\n" << rulepath.mb_str(wxConvLocal);
2042 throw std::runtime_error(oss.str().c_str());
2043 }
2044
2045 // append created file to htmlinfo
2046 htmlinfo += _("<a href=\"open:");
2047 htmlinfo += folder;
2048 htmlinfo += rulefile;
2049 htmlinfo += _("\">");
2050 htmlinfo += rulefile;
2051 htmlinfo += _("</a><br>\n");
2052 }
2053
2054 // -----------------------------------------------------------------------------
2055
SharedColorsIcons(const wxString & prefix,const wxSortedArrayString & allfiles)2056 static bool SharedColorsIcons(const wxString& prefix, const wxSortedArrayString& allfiles)
2057 {
2058 // return true if prefix.colors/icons is shared by at least one prefix-*.table/tree file
2059 for (size_t n = 0; n < allfiles.GetCount(); n++) {
2060 wxString filename = allfiles[n];
2061 if (filename.EndsWith(wxT(".table")) || filename.EndsWith(wxT(".tree"))) {
2062 if (prefix == filename.BeforeLast('-')) {
2063 return true;
2064 }
2065 }
2066 }
2067
2068 // prefix-*.table/tree does not exist in allfiles
2069 return false;
2070 }
2071
2072 // -----------------------------------------------------------------------------
2073
ConvertRules(const wxString & folder,bool supplied,wxString & htmlinfo)2074 static int ConvertRules(const wxString& folder, bool supplied, wxString& htmlinfo)
2075 {
2076 wxString filename, rulefile, rulename;
2077 int depcount = 0;
2078
2079 if (!wxFileName::DirExists(folder)) {
2080 // this might happen if user deleted/renamed folder while Golly is running
2081 std::ostringstream oss;
2082 oss << "Directory does not exist:\n" << folder.mb_str(wxConvLocal);
2083 throw std::runtime_error(oss.str().c_str());
2084 }
2085
2086 wxDir dir(folder);
2087 if (!dir.IsOpened()) {
2088 std::ostringstream oss;
2089 oss << "Failed to open directory:\n" << folder.mb_str(wxConvLocal);
2090 throw std::runtime_error(oss.str().c_str());
2091 }
2092
2093 htmlinfo += wxT("<p>\n");
2094 if (supplied) {
2095 htmlinfo += _("New .rule files created in the supplied Rules folder:<br>\n(");
2096 } else {
2097 htmlinfo += _("New .rule files created in your rules folder:<br>\n(");
2098 }
2099 htmlinfo += folder;
2100 htmlinfo += _(")<p>\n");
2101
2102 // build an array of all files in the given folder (using a sorted array speeds up Index calls)
2103 wxSortedArrayString allfiles;
2104 bool found = dir.GetFirst(&filename, wxEmptyString, wxDIR_FILES | wxDIR_HIDDEN);
2105 while (found) {
2106 allfiles.Add(filename);
2107 found = dir.GetNext(&filename);
2108 }
2109
2110 // create an array of candidate .rule files to be created
2111 wxSortedArrayString candidates;
2112 for (size_t n = 0; n < allfiles.GetCount(); n++) {
2113 filename = allfiles[n];
2114 rulename = filename.BeforeLast('.');
2115 bool colorsicons = filename.EndsWith(wxT(".colors")) || filename.EndsWith(wxT(".icons"));
2116 bool tabletree = filename.EndsWith(wxT(".table")) || filename.EndsWith(wxT(".tree"));
2117 if (colorsicons || tabletree) {
2118 depcount++;
2119 if (colorsicons) {
2120 if (SharedColorsIcons(rulename, allfiles)) {
2121 // colors and/or icons will be shared by one or more rulename-*.rule files
2122 rulefile = rulename + wxT("-shared.rule");
2123 } else {
2124 rulefile = rulename + wxT(".rule");
2125 }
2126 } else {
2127 // .table/tree file
2128 rulefile = rulename + wxT(".rule");
2129 }
2130 // add .rule file to candidates if it hasn't been added yet
2131 // and if it doesn't already exist
2132 if (candidates.Index(rulefile) == wxNOT_FOUND &&
2133 allfiles.Index(rulefile) == wxNOT_FOUND) {
2134 candidates.Add(rulefile);
2135 }
2136 }
2137 }
2138
2139 // create the new .rule files
2140 for (size_t n = 0; n < candidates.GetCount(); n++) {
2141 CreateOneRule(candidates[n], folder, allfiles, htmlinfo);
2142 }
2143
2144 if (candidates.GetCount() == 0) {
2145 htmlinfo += _("None.\n");
2146 }
2147
2148 return depcount;
2149 }
2150
2151 // -----------------------------------------------------------------------------
2152
ShowCreatedRules(wxString & htmlinfo)2153 static void ShowCreatedRules(wxString& htmlinfo)
2154 {
2155 wxString header = _("<html><title>Converted Rules</title>\n");
2156 header += _("<body bgcolor=\"#FFFFCE\">\n");
2157 htmlinfo = header + htmlinfo;
2158 htmlinfo += _("\n</body></html>");
2159
2160 wxString htmlfile = tempdir + _("converted-rules.html");
2161 wxFile outfile(htmlfile, wxFile::write);
2162 if (outfile.IsOpened()) {
2163 outfile.Write(htmlinfo);
2164 outfile.Close();
2165 ShowHelp(htmlfile);
2166 } else {
2167 Warning(_("Could not create html file:\n") + htmlfile);
2168 }
2169 }
2170
2171 // -----------------------------------------------------------------------------
2172
DeleteOldRules(const wxString & folder)2173 static void DeleteOldRules(const wxString& folder)
2174 {
2175 if (wxFileName::DirExists(folder)) {
2176 wxDir dir(folder);
2177 if (dir.IsOpened()) {
2178 // build an array of all files in the given folder
2179 wxArrayString allfiles;
2180 wxString filename;
2181 bool found = dir.GetFirst(&filename, wxEmptyString, wxDIR_FILES | wxDIR_HIDDEN);
2182 while (found) {
2183 allfiles.Add(filename);
2184 found = dir.GetNext(&filename);
2185 }
2186 // delete all the .table/tree/colors/icons files
2187 for (size_t n = 0; n < allfiles.GetCount(); n++) {
2188 filename = allfiles[n];
2189 if ( filename.EndsWith(wxT(".colors")) || filename.EndsWith(wxT(".icons")) ||
2190 filename.EndsWith(wxT(".table")) || filename.EndsWith(wxT(".tree")) ) {
2191 wxRemoveFile(folder + filename);
2192 }
2193 }
2194 }
2195 }
2196 }
2197
2198 // -----------------------------------------------------------------------------
2199
ConvertOldRules()2200 void MainFrame::ConvertOldRules()
2201 {
2202 if (inscript || viewptr->waitingforclick) return;
2203
2204 if (generating) {
2205 command_pending = true;
2206 cmdevent.SetId(ID_CONVERT);
2207 Stop();
2208 return;
2209 }
2210
2211 // look for deprecated .table/tree/colors/icons files and create corresponding .rule files
2212
2213 wxString htmlinfo;
2214 bool aborted = false;
2215 int depcount = 0; // number of deprecated files
2216 try {
2217 // look in the supplied Rules folder first, then in the user's rules folder
2218 depcount += ConvertRules(rulesdir, true, htmlinfo);
2219 depcount += ConvertRules(userrules, false, htmlinfo);
2220 }
2221 catch(const std::exception& e) {
2222 // display message set by throw std::runtime_error(...)
2223 Warning(wxString(e.what(),wxConvLocal));
2224 aborted = true;
2225 // nice to also show error message in help window
2226 htmlinfo += _("\n<p>*** CONVERSION ABORTED DUE TO ERROR ***\n<p>");
2227 htmlinfo += wxString(e.what(),wxConvLocal);
2228 }
2229
2230 // display the results in the help window
2231 ShowCreatedRules(htmlinfo);
2232
2233 if (!aborted && depcount > 0) {
2234 // ask user if it's ok to delete all the deprecated files
2235 int answer = wxMessageBox(_("Do you want to delete all the old .table/tree/colors/icons files?"),
2236 _("Delete deprecated files?"),
2237 wxICON_QUESTION | wxYES_NO, wxGetActiveWindow());
2238 if (answer == wxYES) {
2239 DeleteOldRules(rulesdir);
2240 DeleteOldRules(userrules);
2241 }
2242 }
2243 }
2244
2245 // -----------------------------------------------------------------------------
2246
CreateRuleFiles(const wxSortedArrayString & deprecated,const wxSortedArrayString & ziprules)2247 wxString MainFrame::CreateRuleFiles(const wxSortedArrayString& deprecated,
2248 const wxSortedArrayString& ziprules)
2249 {
2250 // use the given list of deprecated .table/tree/colors/icons files
2251 // (recently extracted from a .zip file and installed in userrules)
2252 // to create new .rule files, except those in ziprules (they were in
2253 // the .zip file and have already been installed)
2254 wxString htmlinfo;
2255 bool aborted = false;
2256 try {
2257 // create an array of candidate .rule files to be created
2258 wxString rulefile, filename, rulename;
2259 wxSortedArrayString candidates;
2260 for (size_t n = 0; n < deprecated.GetCount(); n++) {
2261 filename = deprecated[n];
2262 rulename = filename.BeforeLast('.');
2263 if (filename.EndsWith(wxT(".colors")) || filename.EndsWith(wxT(".icons"))) {
2264 if (SharedColorsIcons(rulename, deprecated)) {
2265 // colors and/or icons will be shared by one or more rulename-*.rule files
2266 rulefile = rulename + wxT("-shared.rule");
2267 } else {
2268 rulefile = rulename + wxT(".rule");
2269 }
2270 } else {
2271 // .table/tree file
2272 rulefile = rulename + wxT(".rule");
2273 }
2274 // add .rule file to candidates if it hasn't been added yet
2275 // and if it isn't in the zip file (ie. already installed)
2276 if (candidates.Index(rulefile) == wxNOT_FOUND &&
2277 ziprules.Index(rulefile) == wxNOT_FOUND) {
2278 candidates.Add(rulefile);
2279 }
2280 }
2281
2282 // create the new .rule files (unlike ConvertRules, here we overwrite any
2283 // existing .rule files in case the zip file's contents have changed)
2284 for (size_t n = 0; n < candidates.GetCount(); n++) {
2285 CreateOneRule(candidates[n], userrules, deprecated, htmlinfo);
2286 }
2287 }
2288 catch(const std::exception& e) {
2289 // display message set by throw std::runtime_error(...)
2290 Warning(wxString(e.what(),wxConvLocal));
2291 aborted = true;
2292 // nice to also show error message in help window
2293 htmlinfo += _("\n<p>*** CONVERSION ABORTED DUE TO ERROR ***\n<p>");
2294 htmlinfo += wxString(e.what(),wxConvLocal);
2295 }
2296
2297 if (!aborted) {
2298 // delete all the deprecated files
2299 for (size_t n = 0; n < deprecated.GetCount(); n++) {
2300 wxRemoveFile(userrules + deprecated[n]);
2301 }
2302 }
2303
2304 return htmlinfo;
2305 }
2306