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/file.h" // for wxFile
10 #include "wx/filename.h" // for wxFileName
11 #include "wx/menuitem.h" // for SetText
12 #include "wx/clipbrd.h" // for wxTheClipboard
13 #include "wx/dataobj.h" // for wxTextDataObject
14 #include "wx/zipstrm.h" // for wxZipEntry, wxZipInputStream
15 #include "wx/wfstream.h" // for wxFFileInputStream
16
17 #include "bigint.h"
18 #include "lifealgo.h"
19 #include "qlifealgo.h"
20 #include "hlifealgo.h"
21 #include "readpattern.h" // for readpattern
22 #include "writepattern.h" // for writepattern, pattern_format
23
24 #include "wxgolly.h" // for wxGetApp, statusptr, viewptr, bigview
25 #include "wxutils.h" // for Warning
26 #include "wxprefs.h" // for SavePrefs, allowundo, userrules, etc
27 #include "wxrule.h" // for GetRuleName
28 #include "wxinfo.h" // for GetInfoFrame
29 #include "wxstatus.h" // for statusptr->...
30 #include "wxview.h" // for viewptr->...
31 #include "wxscript.h" // for RunScript, inscript, scripttitle
32 #include "wxmain.h" // for MainFrame, etc
33 #include "wxundo.h" // for currlayer->undoredo->...
34 #include "wxalgos.h" // for CreateNewUniverse, algo_type, algoinfo, etc
35 #include "wxlayer.h" // for currlayer, etc
36 #include "wxoverlay.h" // for curroverlay
37 #include "wxhelp.h" // for ShowHelp, LoadRule
38 #include "wxtimeline.h" // for InitTimelineFrame, ToggleTimelineBar, etc
39
40 #ifdef __WXMAC__
41 // convert path to decomposed UTF8 so fopen will work
42 #define FILEPATH path.fn_str()
43 #else
44 #define FILEPATH path.mb_str(wxConvLocal)
45 #endif
46
47 #if wxCHECK_VERSION(2,9,0)
48 // some wxMenuItem method names have changed in wx 2.9
49 #define GetText GetItemLabel
50 #define SetText SetItemLabel
51 #endif
52
53 // File menu functions:
54
55 // -----------------------------------------------------------------------------
56
GetBaseName(const wxString & path)57 wxString MainFrame::GetBaseName(const wxString& path)
58 {
59 // extract basename from given path
60 return path.AfterLast(wxFILE_SEP_PATH);
61 }
62
63 // -----------------------------------------------------------------------------
64
SetWindowTitle(const wxString & filename)65 void MainFrame::SetWindowTitle(const wxString& filename)
66 {
67 if ( !scripttitle.IsEmpty() ) {
68 // script has called settitle command
69 return;
70 }
71
72 if ( !filename.IsEmpty() ) {
73 // remember current file name
74 currlayer->currname = filename;
75 // show currname in current layer's menu item
76 UpdateLayerItem(currindex);
77 }
78
79 if (inscript) {
80 // avoid window title flashing; eg. script might be switching layers
81 ShowTitleLater();
82 return;
83 }
84
85 wxString prefix = wxEmptyString;
86
87 // display asterisk if pattern has been modified
88 if (currlayer->dirty) prefix += wxT("*");
89
90 int cid = currlayer->cloneid;
91 while (cid > 0) {
92 // display one or more "=" chars to indicate this is a cloned layer
93 prefix += wxT("=");
94 cid--;
95 }
96
97 wxString rule = GetRuleName( wxString(currlayer->algo->getrule(),wxConvLocal) );
98 wxString wtitle;
99 #ifdef __WXMAC__
100 wtitle.Printf(_("%s%s [%s]"),
101 prefix.c_str(), currlayer->currname.c_str(), rule.c_str());
102 #else
103 wtitle.Printf(_("%s%s [%s] - Golly"),
104 prefix.c_str(), currlayer->currname.c_str(), rule.c_str());
105 #endif
106
107 // nicer to truncate a really long title???
108 SetTitle(wtitle);
109 }
110
111 // -----------------------------------------------------------------------------
112
CreateUniverse()113 void MainFrame::CreateUniverse()
114 {
115 // save current rule
116 wxString oldrule = wxString(currlayer->algo->getrule(), wxConvLocal);
117
118 // delete old universe and create new one of same type
119 delete currlayer->algo;
120 currlayer->algo = CreateNewUniverse(currlayer->algtype);
121
122 // ensure new universe uses same rule (and thus same # of cell states)
123 RestoreRule(oldrule);
124
125 // increment has been reset to 1 but that's probably not always desirable
126 // so set increment using current step size
127 SetGenIncrement();
128 }
129
130 // -----------------------------------------------------------------------------
131
NewPattern(const wxString & title)132 void MainFrame::NewPattern(const wxString& title)
133 {
134 if (generating) {
135 command_pending = true;
136 cmdevent.SetId(wxID_NEW);
137 Stop();
138 return;
139 }
140
141 if (askonnew && currlayer->dirty && !SaveCurrentLayer()) return;
142
143 if (inscript) stop_after_script = true;
144 currlayer->savestart = false;
145 currlayer->currfile.Clear();
146 currlayer->startgen = 0;
147
148 // reset step size before CreateUniverse calls SetGenIncrement
149 currlayer->currbase = algoinfo[currlayer->algtype]->defbase;
150 currlayer->currexpo = 0;
151
152 // create new, empty universe of same type and using same rule
153 CreateUniverse();
154
155 // reset timing info used in DisplayTimingInfo
156 endtime = begintime = 0;
157
158 // clear all undo/redo history
159 currlayer->undoredo->ClearUndoRedo();
160
161 if (newremovesel) currlayer->currsel.Deselect();
162 if (newcurs) currlayer->curs = newcurs;
163 viewptr->SetPosMag(bigint::zero, bigint::zero, newmag);
164
165 // best to restore true origin
166 if (currlayer->originx != bigint::zero || currlayer->originy != bigint::zero) {
167 currlayer->originx = 0;
168 currlayer->originy = 0;
169 statusptr->SetMessage(origin_restored);
170 }
171
172 // restore default colors for current algo/rule
173 UpdateLayerColors();
174
175 MarkLayerClean(title); // calls SetWindowTitle
176 UpdateEverything();
177 }
178
179 // -----------------------------------------------------------------------------
180
IsImageFile(const wxString & path)181 static bool IsImageFile(const wxString& path)
182 {
183 wxString ext = path.AfterLast('.');
184 // if path has no extension then ext == path
185 if (ext == path) return false;
186
187 // supported extensions match image handlers added in GollyApp::OnInit()
188 return ext.IsSameAs(wxT("bmp"),false) ||
189 ext.IsSameAs(wxT("gif"),false) ||
190 ext.IsSameAs(wxT("png"),false) ||
191 ext.IsSameAs(wxT("tif"),false) ||
192 ext.IsSameAs(wxT("tiff"),false) ||
193 ext.IsSameAs(wxT("icons"),false) ||
194 // we don't actually support JPEG files but let LoadImage handle them
195 ext.IsSameAs(wxT("jpg"),false) ||
196 ext.IsSameAs(wxT("jpeg"),false);
197 }
198
199 // -----------------------------------------------------------------------------
200
LoadImage(const wxString & path)201 bool MainFrame::LoadImage(const wxString& path)
202 {
203 // don't try to load JPEG file
204 wxString ext = path.AfterLast('.');
205 if ( ext.IsSameAs(wxT("jpg"),false) ||
206 ext.IsSameAs(wxT("jpeg"),false) ) {
207 Warning(_("Golly cannot import JPEG data, only BMP/GIF/PNG/TIFF."));
208 // pattern will be empty
209 return true;
210 }
211
212 wxImage image;
213 if ( image.LoadFile(path) ) {
214 // don't change the current rule here -- that way the image can
215 // be loaded into any algo
216 unsigned char maskr, maskg, maskb;
217 bool hasmask = image.GetOrFindMaskColour(&maskr, &maskg, &maskb);
218 int wd = image.GetWidth();
219 int ht = image.GetHeight();
220 unsigned char* idata = image.GetData();
221 int x, y;
222 lifealgo* curralgo = currlayer->algo;
223 for (y = 0; y < ht; y++) {
224 for (x = 0; x < wd; x++) {
225 long pos = (y * wd + x) * 3;
226 unsigned char r = idata[pos];
227 unsigned char g = idata[pos+1];
228 unsigned char b = idata[pos+2];
229 if ( hasmask && r == maskr && g == maskg && b == maskb ) {
230 // treat transparent pixel as a dead cell
231 } else if ( r < 255 || g < 255 || b < 255 ) {
232 // treat non-white pixel as a live cell
233 curralgo->setcell(x, y, 1);
234 }
235 }
236 }
237 curralgo->endofpattern();
238 } else {
239 Warning(_("Could not load image from file!"));
240 }
241 return true;
242 }
243
244 // -----------------------------------------------------------------------------
245
LoadPattern(const wxString & path,const wxString & newtitle,bool updatestatus,bool updateall)246 void MainFrame::LoadPattern(const wxString& path, const wxString& newtitle,
247 bool updatestatus, bool updateall)
248 {
249 if ( !wxFileName::FileExists(path) ) {
250 Warning(_("The file does not exist:\n") + path);
251 return;
252 }
253
254 // newtitle is only empty if called from ResetPattern/RestorePattern
255 if (!newtitle.IsEmpty()) {
256 if (askonload && currlayer->dirty && !SaveCurrentLayer()) return;
257
258 if (inscript) stop_after_script = true;
259 currlayer->savestart = false;
260 currlayer->currfile = path;
261
262 // reset step size now in case UpdateStatus is called below
263 currlayer->currbase = algoinfo[currlayer->algtype]->defbase;
264 currlayer->currexpo = 0;
265
266 if (GetInfoFrame()) {
267 // comments will no longer be relevant so close info window
268 GetInfoFrame()->Close(true);
269 }
270
271 // reset timing info used in DisplayTimingInfo
272 endtime = begintime = 0;
273
274 // clear all undo/redo history
275 currlayer->undoredo->ClearUndoRedo();
276 }
277
278 if (!showbanner) statusptr->ClearMessage();
279
280 // set nopattupdate BEFORE UpdateStatus() call so we see gen=0 and pop=0;
281 // in particular, it avoids getPopulation being called which would
282 // slow down hlife pattern loading
283 viewptr->nopattupdate = true;
284
285 if (updatestatus) {
286 // update all of status bar so we don't see different colored lines;
287 // on Mac, DrawView also gets called if there are pending updates
288 UpdateStatus();
289 }
290
291 // save current algo and rule
292 algo_type oldalgo = currlayer->algtype;
293 wxString oldrule = wxString(currlayer->algo->getrule(), wxConvLocal);
294
295 // delete old universe and create new one of same type
296 delete currlayer->algo;
297 currlayer->algo = CreateNewUniverse(currlayer->algtype);
298
299 if (!newtitle.IsEmpty() && !inscript) {
300 // show new file name in window title but no rule (which readpattern can change);
301 // nicer if user can see file name while loading a very large pattern
302 SetTitle(_("Loading ") + newtitle);
303 }
304
305 if (IsImageFile(path)) {
306 // ensure new universe uses same rule
307 RestoreRule(oldrule);
308 LoadImage(path);
309 viewptr->nopattupdate = false;
310 } else {
311 const char* err = readpattern(FILEPATH, *currlayer->algo);
312 if (err) {
313 wxString bigerr = _("File could not be loaded by any algorithm.");
314 wxString algoname = wxString(GetAlgoName(currlayer->algtype), wxConvLocal);
315 bigerr += wxString::Format(_("\n\nError from %s:\n"), algoname.c_str());
316 bigerr += wxString(err, wxConvLocal);
317
318 // cycle thru all other algos until readpattern succeeds
319 for (int i = 0; i < NumAlgos(); i++) {
320 if (i != oldalgo) {
321 currlayer->algtype = i;
322 delete currlayer->algo;
323 currlayer->algo = CreateNewUniverse(currlayer->algtype);
324 // readpattern will call setrule
325 err = readpattern(FILEPATH, *currlayer->algo);
326 if (err) {
327 algoname = wxString(GetAlgoName(currlayer->algtype), wxConvLocal);
328 bigerr += wxString::Format(_("\n\nError from %s:\n"), algoname.c_str());
329 bigerr += wxString(err, wxConvLocal);
330 } else {
331 break;
332 }
333 }
334 }
335 viewptr->nopattupdate = false;
336 if (err) {
337 // no algo could read pattern so restore original algo and rule
338 currlayer->algtype = oldalgo;
339 delete currlayer->algo;
340 currlayer->algo = CreateNewUniverse(currlayer->algtype);
341 RestoreRule(oldrule);
342 // also show full path to file (useful when debugging!)
343 bigerr += wxString::Format(_("\n\nFile path:\n%s"), wxString(FILEPATH,wxConvLocal).c_str());
344 Warning(bigerr);
345 }
346 }
347 viewptr->nopattupdate = false;
348 }
349
350 if (!newtitle.IsEmpty()) {
351 MarkLayerClean(newtitle); // calls SetWindowTitle
352
353 if (TimelineExists()) {
354 // we've loaded a .mc file with a timeline so go to 1st frame
355 InitTimelineFrame();
356 if (!showtimeline) ToggleTimelineBar();
357 // switch to the base step and exponent used to record the timeline
358 pair<int, int> be = currlayer->algo->getbaseexpo();
359 currlayer->currbase = be.first;
360 currlayer->currexpo = be.second;
361 } else {
362 // restore default base step for current algo
363 // (currlayer->currexpo was set to 0 above)
364 currlayer->currbase = algoinfo[currlayer->algtype]->defbase;
365 }
366 SetGenIncrement();
367
368 // restore default colors for current algo/rule
369 UpdateLayerColors();
370
371 if (openremovesel) currlayer->currsel.Deselect();
372 if (opencurs) currlayer->curs = opencurs;
373
374 viewptr->FitInView(1);
375 currlayer->startgen = currlayer->algo->getGeneration(); // might be > 0
376 if (updateall) UpdateEverything();
377 showbanner = false;
378 } else {
379 // ResetPattern/RestorePattern does the update
380 }
381 }
382
383 // -----------------------------------------------------------------------------
384
CheckBeforeRunning(const wxString & scriptpath,bool remember,const wxString & zippath)385 void MainFrame::CheckBeforeRunning(const wxString& scriptpath, bool remember,
386 const wxString& zippath)
387 {
388 bool ask;
389 if (zippath.IsEmpty()) {
390 // script was downloaded via "get:" link (script is in downloaddir --
391 // see GetURL in wxhelp.cpp) so always ask user if it's okay to run
392 ask = true;
393 } else {
394 // script is included in zip file (scriptpath starts with tempdir) so only
395 // ask user if zip file was downloaded via "get:" link
396 ask = zippath.StartsWith(downloaddir);
397 }
398
399 if (ask) {
400 UpdateEverything(); // in case OpenZipFile called LoadPattern
401
402 // create our own dialog with a View button??? probably no need now that
403 // user can ctrl/right-click on link to open script in their text editor
404 wxString msg = scriptpath + _("\n\nClick \"No\" if the script is from an untrusted source.");
405 int answer = wxMessageBox(msg, _("Do you want to run this script?"),
406 wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT,
407 wxGetActiveWindow());
408 switch (answer) {
409 case wxYES: break;
410 case wxNO: return;
411 default: return; // No
412 }
413 }
414
415 // also do this???
416 // save script info (download path or zip path + script entry) in list of safe scripts
417 // (stored in prefs file) so we can search for this script and not ask again
418
419 Raise();
420 if (remember) AddRecentScript(scriptpath);
421 RunScript(scriptpath);
422 }
423
424 // -----------------------------------------------------------------------------
425
ExtractZipEntry(const wxString & zippath,const wxString & entryname,const wxString & outfile)426 bool MainFrame::ExtractZipEntry(const wxString& zippath,
427 const wxString& entryname,
428 const wxString& outfile)
429 {
430 wxFFileInputStream instream(zippath);
431 if (!instream.Ok()) {
432 Warning(_("Could not create input stream for zip file:\n") + zippath);
433 return false;
434 }
435 wxZipInputStream zip(instream);
436
437 wxZipEntry* entry;
438 while ((entry = zip.GetNextEntry()) != NULL) {
439 wxString thisname = entry->GetName();
440 if (thisname == entryname) {
441 // we've found the desired entry so copy entry data to given output file
442 wxFileOutputStream outstream(outfile);
443 if (outstream.Ok()) {
444 // read and write in chunks so we can show a progress dialog
445 const int BUFFER_SIZE = 4000;
446 char buf[BUFFER_SIZE];
447 size_t incount = 0;
448 size_t outcount = 0;
449 size_t lastread, lastwrite;
450 double filesize = (double) entry->GetSize();
451 if (filesize <= 0.0) filesize = -1.0; // show indeterminate progress
452
453 BeginProgress(_("Extracting file"));
454 while (true) {
455 zip.Read(buf, BUFFER_SIZE);
456 lastread = zip.LastRead();
457 if (lastread == 0) break;
458 outstream.Write(buf, lastread);
459 lastwrite = outstream.LastWrite();
460 incount += lastread;
461 outcount += lastwrite;
462 if (incount != outcount) {
463 Warning(_("Error occurred while writing file:\n") + outfile);
464 break;
465 }
466 char msg[128];
467 sprintf(msg, "File size: %.2f MB", double(incount) / 1048576.0);
468 if (AbortProgress((double)incount / filesize, wxString(msg,wxConvLocal))) {
469 outcount = 0;
470 break;
471 }
472 }
473 EndProgress();
474
475 if (incount == outcount) {
476 // successfully copied entry data to outfile
477 delete entry;
478 return true;
479 } else {
480 // delete incomplete outfile
481 if (wxFileExists(outfile)) wxRemoveFile(outfile);
482 }
483 } else {
484 Warning(_("Could not open output stream for file:\n") + outfile);
485 }
486 delete entry;
487 return false;
488 }
489 delete entry;
490 }
491
492 // should not get here
493 Warning(_("Could not find zip file entry:\n") + entryname);
494 return false;
495 }
496
497 // -----------------------------------------------------------------------------
498
RuleInstalled(wxZipInputStream & zip,const wxString & rulepath)499 static bool RuleInstalled(wxZipInputStream& zip, const wxString& rulepath)
500 {
501 wxFileOutputStream outstream(rulepath);
502 bool ok = outstream.Ok();
503 if (ok) {
504 zip.Read(outstream);
505 ok = (outstream.GetLastError() == wxSTREAM_NO_ERROR);
506 }
507 return ok;
508 }
509
510 // -----------------------------------------------------------------------------
511
OpenZipFile(const wxString & zippath)512 void MainFrame::OpenZipFile(const wxString& zippath)
513 {
514 // Process given zip file in the following manner:
515 // - If it contains any .rule files then extract and install those
516 // files into userrules (the user's rules directory).
517 // - If the zip file is "complex" (contains any folders, rule files, text files,
518 // or more than one pattern, or more than one script), build a temporary html
519 // file with clickable links to each file entry and show it in the help window.
520 // - If the zip file contains at most one pattern and at most one script (both
521 // at the root level) then load the pattern (if present) and then run the script
522 // (if present and if allowed).
523
524 const wxString indent = wxT(" ");
525 bool dirseen = false;
526 bool diffdirs = (userrules != rulesdir);
527 wxString firstdir = wxEmptyString;
528 wxString lastpattern = wxEmptyString;
529 wxString lastscript = wxEmptyString;
530 int patternseps = 0; // # of separators in lastpattern
531 int scriptseps = 0; // # of separators in lastscript
532 int patternfiles = 0;
533 int scriptfiles = 0;
534 int textfiles = 0; // includes html files
535 int rulefiles = 0;
536 int deprecated = 0; // # of .table/tree/colors/icons files
537 wxSortedArrayString deplist; // list of installed deprecated files
538 wxSortedArrayString rulelist; // list of installed .rule files
539
540 // note that we need to create the HTML file with UTF-8 encoding
541 // in case zippath contains non-ASCII characters
542 wxString contents = wxT("<html><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">");
543 contents += wxT("<title>") + GetBaseName(zippath);
544 contents += wxT("</title>\n");
545 contents += wxT("<body bgcolor=\"#FFFFCE\">\n");
546 contents += wxT("<p>\n");
547 contents += wxT("Zip file: ");
548 contents += zippath;
549 contents += wxT("<p>\n");
550 contents += wxT("Contents:<br>\n");
551
552 wxFFileInputStream instream(zippath);
553 if (!instream.Ok()) {
554 Warning(_("Could not create input stream for zip file:\n") + zippath);
555 return;
556 }
557 wxZipInputStream zip(instream);
558
559 // examine each entry in zip file and build contents string;
560 // also install any .rule files
561 wxZipEntry* entry;
562 while ((entry = zip.GetNextEntry()) != NULL) {
563 wxString name = entry->GetName();
564 if (name.StartsWith(wxT("__MACOSX")) || name.EndsWith(wxT(".DS_Store"))) {
565 // ignore meta-data stuff in zip file created on Mac
566 } else {
567 // indent depending on # of separators in name
568 unsigned int sepcount = 0;
569 unsigned int i = 0;
570 unsigned int len = (unsigned int)name.length();
571 while (i < len) {
572 if (name[i] == wxFILE_SEP_PATH) sepcount++;
573 i++;
574 }
575 // check if 1st directory has multiple separators (eg. in jslife.zip)
576 if (entry->IsDir() && !dirseen && sepcount > 1) {
577 firstdir = name.BeforeFirst(wxFILE_SEP_PATH);
578 contents += firstdir;
579 contents += wxT("<br>\n");
580 }
581 for (i = 1; i < sepcount; i++) contents += indent;
582
583 if (entry->IsDir()) {
584 // remove terminating separator from directory name
585 name = name.BeforeLast(wxFILE_SEP_PATH);
586 name = name.AfterLast(wxFILE_SEP_PATH);
587 if (dirseen && name == firstdir) {
588 // ignore dir already output earlier (eg. in jslife.zip)
589 } else {
590 contents += name;
591 contents += wxT("<br>\n");
592 }
593 dirseen = true;
594
595 } else {
596 // entry is for some sort of file
597 wxString filename = name.AfterLast(wxFILE_SEP_PATH);
598 if (dirseen) contents += indent;
599
600 if ( IsRuleFile(filename) && !filename.EndsWith(wxT(".rule")) ) {
601 // this is a deprecated .table/tree/colors/icons file
602 contents += filename;
603 contents += indent;
604 contents += wxT("[deprecated]");
605 deprecated++;
606 // install it into userrules so it can be used below to create a .rule file
607 wxString outfile = userrules + filename;
608 if (RuleInstalled(zip, outfile)) {
609 deplist.Add(filename);
610 } else {
611 contents += indent;
612 contents += wxT("INSTALL FAILED!");
613 }
614
615 } else {
616 // user can extract file via special "unzip:" link
617 contents += wxT("<a href=\"unzip:");
618 contents += zippath;
619 contents += wxT(":");
620 contents += name;
621 contents += wxT("\">");
622 contents += filename;
623 contents += wxT("</a>");
624
625 if ( IsRuleFile(filename) ) {
626 // extract and install .rule file into userrules
627 wxString outfile = userrules + filename;
628 if (RuleInstalled(zip, outfile)) {
629 // file successfully installed
630 rulelist.Add(filename);
631 contents += indent;
632 contents += wxT("[installed]");
633 if (diffdirs) {
634 // check if this file overrides similarly named file in rulesdir
635 wxString clashfile = rulesdir + filename;
636 if (wxFileExists(clashfile)) {
637 contents += indent;
638 contents += wxT("(overrides file in Rules folder)");
639 }
640 }
641 } else {
642 // file could not be installed
643 contents += indent;
644 contents += wxT("[NOT installed]");
645 // file is probably incomplete so best to delete it
646 if (wxFileExists(outfile)) wxRemoveFile(outfile);
647 }
648 rulefiles++;
649
650 } else if ( IsHTMLFile(filename) || IsTextFile(filename) ) {
651 textfiles++;
652
653 } else if ( IsScriptFile(filename) ) {
654 scriptfiles++;
655 lastscript = name;
656 scriptseps = sepcount;
657
658 } else {
659 patternfiles++;
660 lastpattern = name;
661 patternseps = sepcount;
662 }
663 }
664 contents += wxT("<br>\n");
665 }
666 }
667 delete entry;
668 } // end while
669
670 if (rulefiles > 0) {
671 contents += wxT("<p>Files marked as \"[installed]\" have been stored in your rules folder:<br>\n");
672 contents += userrules;
673 contents += wxT("\n");
674 }
675 if (deprecated > 0) {
676 wxString newrules = CreateRuleFiles(deplist, rulelist);
677 if (newrules.length() > 0) {
678 contents += wxT("<p>Files marked as \"[deprecated]\" have been used to create new .rule files:<br>\n");
679 contents += newrules;
680 }
681 }
682 contents += wxT("\n</body></html>");
683
684 if (dirseen || rulefiles > 0 || deprecated > 0 || textfiles > 0 || patternfiles > 1 || scriptfiles > 1) {
685 // complex zip, so write contents to a temporary html file and display it in help window;
686 // use a unique file name so user can go back/forwards
687 wxString htmlfile = wxFileName::CreateTempFileName(tempdir + wxT("zip_contents_"));
688 wxRemoveFile(htmlfile);
689 htmlfile += wxT(".html");
690 wxFile outfile(htmlfile, wxFile::write);
691 if (outfile.IsOpened()) {
692 outfile.Write(contents);
693 outfile.Close();
694 ShowHelp(htmlfile);
695 } else {
696 Warning(_("Could not create html file:\n") + htmlfile);
697 }
698 }
699
700 if (patternfiles <= 1 && scriptfiles <= 1 && patternseps == 0 && scriptseps == 0) {
701 // load lastpattern (if present), then run lastscript (if present);
702 // the script might be a long-running one that allows user interaction,
703 // so it's best to run it AFTER calling ShowHelp above
704 if (patternfiles == 1) {
705 wxString tempfile = tempdir + lastpattern.AfterLast(wxFILE_SEP_PATH);
706 if (ExtractZipEntry(zippath, lastpattern, tempfile)) {
707 Raise();
708 // don't call AddRecentPattern(tempfile) here; OpenFile has added
709 // zippath to recent patterns
710 LoadPattern(tempfile, GetBaseName(tempfile), true, scriptfiles == 0);
711 }
712 }
713 if (scriptfiles == 1) {
714 wxString tempfile = tempdir + lastscript.AfterLast(wxFILE_SEP_PATH);
715 if (ExtractZipEntry(zippath, lastscript, tempfile)) {
716 // run script depending on safety check
717 CheckBeforeRunning(tempfile, false, zippath);
718 } else {
719 // should never happen but play safe
720 UpdateEverything();
721 }
722 }
723 }
724 }
725
726 // -----------------------------------------------------------------------------
727
RuleCanBeFound(const wxString & path)728 static bool RuleCanBeFound(const wxString& path)
729 {
730 // ensure given path to .rule file is a full path
731 wxString fullpath = path;
732 wxFileName fname(fullpath);
733 if (!fname.IsAbsolute()) fullpath = gollydir + path;
734
735 // check that .rule file is in userrules or rulesdir
736 wxString dir = fullpath.BeforeLast(wxFILE_SEP_PATH);
737 dir += wxFILE_SEP_PATH;
738 if (dir == userrules || dir == rulesdir) {
739 return true;
740 } else {
741 wxString msg = _("You need to move ");
742 msg += fullpath.AfterLast(wxFILE_SEP_PATH);
743 msg += _(" into your rules folder (");
744 msg += userrules;
745 msg += _(") so the RuleLoader algorithm can find it.");
746 Warning(msg);
747 return false;
748 }
749 }
750
751 // -----------------------------------------------------------------------------
752
OpenFile(const wxString & path,bool remember)753 void MainFrame::OpenFile(const wxString& path, bool remember)
754 {
755 if (IsHTMLFile(path)) {
756 // show HTML file in help window
757 ShowHelp(path);
758 return;
759 }
760
761 if (IsTextFile(path)) {
762 // open text file in user's preferred text editor
763 EditFile(path);
764 return;
765 }
766
767 if (generating) {
768 command_pending = true;
769 // assume remember is true (should only be false if called from a script)
770 if ( IsScriptFile(path) ) {
771 AddRecentScript(path);
772 cmdevent.SetId(ID_RUN_RECENT + 1);
773 } else {
774 AddRecentPattern(path);
775 cmdevent.SetId(ID_OPEN_RECENT + 1);
776 }
777 Stop();
778 return;
779 }
780
781 // note that pass_file_events is false if OpenFile was called from GSF_open
782 if (inscript && pass_file_events) {
783 // ensure path is a full path
784 wxString newpath = path;
785 wxFileName fname(newpath);
786 if (!fname.IsAbsolute()) newpath = gollydir + path;
787 PassFileToScript(newpath);
788 return;
789 }
790
791 if (!inscript && path.EndsWith(wxT(".rle3"))) {
792 if (remember) AddRecentPattern(path);
793
794 // create a full path in rle3path for GSF_getevent to use
795 rle3path = path;
796 wxFileName fname(rle3path);
797 if (!fname.IsAbsolute()) rle3path = gollydir + path;
798
799 // start up 3D.lua (it will get a file event containing rle3path)
800 wxString path3D = gollydir + wxT("Scripts");
801 path3D += wxFILE_SEP_PATH;
802 path3D += wxT("Lua");
803 path3D += wxFILE_SEP_PATH;
804 path3D += wxT("3D.lua");
805 RunScript(path3D);
806 return;
807 }
808
809 if (IsScriptFile(path)) {
810 // execute script
811 if (remember) AddRecentScript(path);
812 RunScript(path);
813
814 } else if (IsZipFile(path)) {
815 // process zip file
816 if (remember) AddRecentPattern(path); // treat it like a pattern
817 OpenZipFile(path);
818
819 } else if (IsRuleFile(path)) {
820 // switch to rule, but only if it's in rulesdir or userrules
821 if (RuleCanBeFound(path))
822 LoadRule(path.AfterLast(wxFILE_SEP_PATH).BeforeLast('.'));
823
824 } else {
825 // load pattern
826 if (remember) AddRecentPattern(path);
827
828 // ensure path is a full path because a script might want to reset() to it
829 // (in which case the cwd is the script's directory, not gollydir)
830 wxString newpath = path;
831 wxFileName fname(newpath);
832 if (!fname.IsAbsolute()) newpath = gollydir + path;
833
834 LoadPattern(newpath, GetBaseName(path));
835 }
836 }
837
838 // -----------------------------------------------------------------------------
839
AddRecentPattern(const wxString & inpath)840 void MainFrame::AddRecentPattern(const wxString& inpath)
841 {
842 if (inpath.IsEmpty()) return;
843 wxString path = inpath;
844 if (path.StartsWith(gollydir)) {
845 // remove gollydir from start of path
846 path.erase(0, gollydir.length());
847 }
848
849 // duplicate any ampersands so they appear in menu
850 path.Replace(wxT("&"), wxT("&&"));
851
852 // put given path at start of patternSubMenu
853 #ifdef __WXGTK__
854 // avoid wxGTK bug in FindItem if path contains underscores
855 int id = wxNOT_FOUND;
856 for (int i = 0; i < numpatterns; i++) {
857 wxMenuItem* item = patternSubMenu->FindItemByPosition(i);
858 wxString temp = item->GetText();
859 temp.Replace(wxT("__"), wxT("_"));
860 temp.Replace(wxT("&"), wxT("&&"));
861 if (temp == path) {
862 id = ID_OPEN_RECENT + 1 + i;
863 break;
864 }
865 }
866 #else
867 int id = patternSubMenu->FindItem(path);
868 #endif
869 if ( id == wxNOT_FOUND ) {
870 if ( numpatterns < maxpatterns ) {
871 // add new path
872 numpatterns++;
873 id = ID_OPEN_RECENT + numpatterns;
874 patternSubMenu->Insert(numpatterns - 1, id, path);
875 } else {
876 // replace last item with new path
877 wxMenuItem* item = patternSubMenu->FindItemByPosition(maxpatterns - 1);
878 item->SetText(path);
879 id = ID_OPEN_RECENT + maxpatterns;
880 }
881 }
882 // path exists in patternSubMenu
883 if ( id > ID_OPEN_RECENT + 1 ) {
884 // move path to start of menu
885 wxMenuItem* item;
886 while ( id > ID_OPEN_RECENT + 1 ) {
887 wxMenuItem* previtem = patternSubMenu->FindItem(id - 1);
888 wxString prevpath = previtem->GetText();
889 #ifdef __WXGTK__
890 // remove duplicate underscores
891 prevpath.Replace(wxT("__"), wxT("_"));
892 prevpath.Replace(wxT("&"), wxT("&&"));
893 #endif
894 item = patternSubMenu->FindItem(id);
895 item->SetText(prevpath);
896 id--;
897 }
898 item = patternSubMenu->FindItem(id);
899 item->SetText(path);
900 }
901 }
902
903 // -----------------------------------------------------------------------------
904
AddRecentScript(const wxString & inpath)905 void MainFrame::AddRecentScript(const wxString& inpath)
906 {
907 if (inpath.IsEmpty()) return;
908 wxString path = inpath;
909 if (path.StartsWith(gollydir)) {
910 // remove gollydir from start of path
911 path.erase(0, gollydir.length());
912 }
913
914 // duplicate ampersands so they appear in menu
915 path.Replace(wxT("&"), wxT("&&"));
916
917 // put given path at start of scriptSubMenu
918 #ifdef __WXGTK__
919 // avoid wxGTK bug in FindItem if path contains underscores
920 int id = wxNOT_FOUND;
921 for (int i = 0; i < numscripts; i++) {
922 wxMenuItem* item = scriptSubMenu->FindItemByPosition(i);
923 wxString temp = item->GetText();
924 temp.Replace(wxT("__"), wxT("_"));
925 temp.Replace(wxT("&"), wxT("&&"));
926 if (temp == path) {
927 id = ID_RUN_RECENT + 1 + i;
928 break;
929 }
930 }
931 #else
932 int id = scriptSubMenu->FindItem(path);
933 #endif
934 if ( id == wxNOT_FOUND ) {
935 if ( numscripts < maxscripts ) {
936 // add new path
937 numscripts++;
938 id = ID_RUN_RECENT + numscripts;
939 scriptSubMenu->Insert(numscripts - 1, id, path);
940 } else {
941 // replace last item with new path
942 wxMenuItem* item = scriptSubMenu->FindItemByPosition(maxscripts - 1);
943 item->SetText(path);
944 id = ID_RUN_RECENT + maxscripts;
945 }
946 }
947 // path exists in scriptSubMenu
948 if ( id > ID_RUN_RECENT + 1 ) {
949 // move path to start of menu
950 wxMenuItem* item;
951 while ( id > ID_RUN_RECENT + 1 ) {
952 wxMenuItem* previtem = scriptSubMenu->FindItem(id - 1);
953 wxString prevpath = previtem->GetText();
954 #ifdef __WXGTK__
955 // remove duplicate underscores
956 prevpath.Replace(wxT("__"), wxT("_"));
957 prevpath.Replace(wxT("&"), wxT("&&"));
958 #endif
959 item = scriptSubMenu->FindItem(id);
960 item->SetText(prevpath);
961 id--;
962 }
963 item = scriptSubMenu->FindItem(id);
964 item->SetText(path);
965 }
966 }
967
968 // -----------------------------------------------------------------------------
969
OpenPattern()970 void MainFrame::OpenPattern()
971 {
972 if (generating) {
973 command_pending = true;
974 cmdevent.SetId(wxID_OPEN);
975 Stop();
976 return;
977 }
978
979 wxString filetypes = _("All files (*)|*");
980 filetypes += _("|RLE (*.rle)|*.rle");
981 filetypes += _("|RLE3 (*.rle3)|*.rle3");
982 filetypes += _("|Macrocell (*.mc)|*.mc");
983 filetypes += _("|Gzip (*.gz)|*.gz");
984 filetypes += _("|Life 1.05/1.06 (*.lif)|*.lif");
985 filetypes += _("|dblife (*.l)|*.l");
986 filetypes += _("|MCell (*.mcl)|*.mcl");
987 filetypes += _("|Zip (*.zip;*.gar)|*.zip;*.gar");
988 filetypes += _("|BMP (*.bmp)|*.bmp");
989 filetypes += _("|GIF (*.gif)|*.gif");
990 filetypes += _("|PNG (*.png)|*.png");
991 filetypes += _("|TIFF (*.tiff;*.tif)|*.tiff;*.tif");
992
993 wxFileDialog opendlg(this, _("Choose a pattern"),
994 opensavedir, wxEmptyString, filetypes,
995 wxFD_OPEN | wxFD_FILE_MUST_EXIST);
996 int button = opendlg.ShowModal();
997 viewptr->ResetMouseDown();
998 if (button == wxID_OK) {
999 wxFileName fullpath( opendlg.GetPath() );
1000 opensavedir = fullpath.GetPath();
1001 OpenFile( opendlg.GetPath() );
1002 }
1003 }
1004
1005 // -----------------------------------------------------------------------------
1006
OpenScript()1007 void MainFrame::OpenScript()
1008 {
1009 if (generating) {
1010 command_pending = true;
1011 cmdevent.SetId(ID_RUN_SCRIPT);
1012 Stop();
1013 return;
1014 }
1015
1016 wxString filetypes = _("Lua or Python (*.lua;*.py)|*.lua;*.py");
1017 filetypes += _("|Lua (*.lua)|*.lua");
1018 filetypes += _("|Python (*.py)|*.py");
1019 #ifdef ENABLE_PERL
1020 filetypes += _("|Perl (*.pl)|*.pl");
1021 #endif
1022
1023 wxFileDialog opendlg(this, _("Choose a script"),
1024 rundir, wxEmptyString, filetypes,
1025 wxFD_OPEN | wxFD_FILE_MUST_EXIST);
1026 int button = opendlg.ShowModal();
1027 viewptr->ResetMouseDown();
1028 if (button == wxID_OK) {
1029 wxFileName fullpath( opendlg.GetPath() );
1030 rundir = fullpath.GetPath();
1031 AddRecentScript( opendlg.GetPath() );
1032 RunScript( opendlg.GetPath() );
1033 }
1034 }
1035
1036 // -----------------------------------------------------------------------------
1037
CopyTextToClipboard(const wxString & text)1038 bool MainFrame::CopyTextToClipboard(const wxString& text)
1039 {
1040 bool result = true;
1041 if (wxTheClipboard->Open()) {
1042 if ( !wxTheClipboard->SetData(new wxTextDataObject(text)) ) {
1043 Warning(_("Could not copy text to clipboard!"));
1044 result = false;
1045 }
1046 wxTheClipboard->Close();
1047 } else {
1048 Warning(_("Could not open clipboard!"));
1049 result = false;
1050 }
1051 return result;
1052 }
1053
1054 // -----------------------------------------------------------------------------
1055
1056 #if wxCHECK_VERSION(2,9,0)
1057 // wxTextDataObject also has GetText and SetText methods so don't change them
1058 #undef GetText
1059 #undef SetText
1060 #endif
1061
GetTextFromClipboard(wxTextDataObject * textdata)1062 bool MainFrame::GetTextFromClipboard(wxTextDataObject* textdata)
1063 {
1064 bool gotdata = false;
1065
1066 if ( wxTheClipboard->Open() ) {
1067 if ( wxTheClipboard->IsSupported( wxDF_TEXT ) ) {
1068 gotdata = wxTheClipboard->GetData( *textdata );
1069 if (!gotdata) {
1070 statusptr->ErrorMessage(_("Could not get clipboard text!"));
1071 }
1072
1073 } else if ( wxTheClipboard->IsSupported( wxDF_BITMAP ) ) {
1074 wxBitmapDataObject bmapdata;
1075 gotdata = wxTheClipboard->GetData( bmapdata );
1076 if (gotdata) {
1077 // convert bitmap data to text data
1078 wxString str;
1079 wxBitmap bmap = bmapdata.GetBitmap();
1080 wxImage image = bmap.ConvertToImage();
1081 if (image.Ok()) {
1082 /* there doesn't seem to be any mask or alpha info, at least on Mac
1083 if (bmap.GetMask() != NULL) Warning(_("Bitmap has mask!"));
1084 if (image.HasMask()) Warning(_("Image has mask!"));
1085 if (image.HasAlpha()) Warning(_("Image has alpha!"));
1086 */
1087 int wd = image.GetWidth();
1088 int ht = image.GetHeight();
1089 unsigned char* idata = image.GetData();
1090 int x, y;
1091 for (y = 0; y < ht; y++) {
1092 for (x = 0; x < wd; x++) {
1093 long pos = (y * wd + x) * 3;
1094 if ( idata[pos] < 255 || idata[pos+1] < 255 || idata[pos+2] < 255 ) {
1095 // non-white pixel is a live cell
1096 str += 'o';
1097 } else {
1098 // white pixel is a dead cell
1099 str += '.';
1100 }
1101 }
1102 str += '\n';
1103 }
1104 textdata->SetText(str);
1105 } else {
1106 statusptr->ErrorMessage(_("Could not convert clipboard bitmap!"));
1107 gotdata = false;
1108 }
1109 } else {
1110 statusptr->ErrorMessage(_("Could not get clipboard bitmap!"));
1111 }
1112
1113 } else {
1114 statusptr->ErrorMessage(_("No data in clipboard."));
1115 }
1116 wxTheClipboard->Close();
1117
1118 } else {
1119 statusptr->ErrorMessage(_("Could not open clipboard!"));
1120 }
1121
1122 return gotdata;
1123 }
1124
1125 // -----------------------------------------------------------------------------
1126
ClipboardContainsRule()1127 bool MainFrame::ClipboardContainsRule()
1128 {
1129 wxTextDataObject data;
1130 if (!GetTextFromClipboard(&data)) return false;
1131
1132 wxString cliptext = data.GetText();
1133 if (!cliptext.StartsWith(wxT("@RULE "))) return false;
1134
1135 // extract rule name
1136 wxString rulename;
1137 int i = 6;
1138 while (cliptext[i] > ' ') {
1139 rulename += cliptext[i];
1140 i++;
1141 }
1142
1143 // check if rulename.rule already exists
1144 wxString rulepath = userrules + rulename;
1145 rulepath += wxT(".rule");
1146 if (wxFileExists(rulepath)) {
1147 wxString question = _("Do you want to replace the existing ") + rulename;
1148 question += _(".rule with the version in the clipboard?");
1149 int answer = wxMessageBox(question, _("Replace existing .rule file?"),
1150 wxICON_QUESTION | wxYES_NO | wxNO_DEFAULT, wxGetActiveWindow());
1151 if (answer == wxNO) {
1152 // don't overwrite existing .rule file
1153 return true;
1154 }
1155 }
1156
1157 // create rulename.rule in user-specific rules folder
1158 wxFile rulefile(rulepath, wxFile::write);
1159 if (!rulefile.IsOpened()) {
1160 Warning(_("Could not open .rule file for writing:\n") + rulepath);
1161 return true;
1162 }
1163 if (!rulefile.Write(data.GetText())) {
1164 Warning(_("Could not write clipboard data to .rule file!"));
1165 rulefile.Close();
1166 return true;
1167 }
1168 rulefile.Close();
1169 statusptr->DisplayMessage(_("Created ") + rulepath);
1170
1171 // now switch to the newly created rule
1172 LoadRule(rulename);
1173
1174 return true;
1175 }
1176
1177 // -----------------------------------------------------------------------------
1178
ClipboardContainsRLE3()1179 bool MainFrame::ClipboardContainsRLE3()
1180 {
1181 wxTextDataObject data;
1182 if (!GetTextFromClipboard(&data)) return false;
1183
1184 wxString cliptext = data.GetText();
1185 if (!cliptext.StartsWith(wxT("3D version"))) return false;
1186
1187 // copy clipboard data to a temporary .rle3 file
1188 wxString filepath = tempdir + wxT("clipboard.rle3");
1189 wxFile rle3file(filepath, wxFile::write);
1190 if (!rle3file.IsOpened()) {
1191 Warning(_("Could not open clipboard.rle3 for writing!"));
1192 return true;
1193 }
1194 if (!rle3file.Write(data.GetText())) {
1195 Warning(_("Could not write clipboard data to file!"));
1196 rle3file.Close();
1197 return true;
1198 }
1199 rle3file.Close();
1200
1201 // set rle3path for GSF_getevent
1202 rle3path = filepath;
1203
1204 // start up 3D.lua (it will get a file event containing rle3path)
1205 wxString path3D = gollydir + wxT("Scripts");
1206 path3D += wxFILE_SEP_PATH;
1207 path3D += wxT("Lua");
1208 path3D += wxFILE_SEP_PATH;
1209 path3D += wxT("3D.lua");
1210 RunScript(path3D);
1211
1212 return true;
1213 }
1214
1215 // -----------------------------------------------------------------------------
1216
OpenClipboard()1217 void MainFrame::OpenClipboard()
1218 {
1219 if (generating) {
1220 command_pending = true;
1221 cmdevent.SetId(ID_OPEN_CLIP);
1222 Stop();
1223 return;
1224 }
1225
1226 // if clipboard text starts with "@RULE rulename" then install rulename.rule
1227 // and switch to that rule
1228 if (ClipboardContainsRule()) return;
1229
1230 // if clipboard text starts with "3D version" then start up 3D.lua
1231 // and load the RLE3 pattern
1232 if (ClipboardContainsRLE3()) return;
1233
1234 // load and view pattern data stored in clipboard
1235 wxTextDataObject data;
1236 if (GetTextFromClipboard(&data)) {
1237 // copy clipboard data to tempstart so we can handle all formats
1238 // supported by readpattern
1239 wxFile outfile(currlayer->tempstart, wxFile::write);
1240 if ( outfile.IsOpened() ) {
1241 outfile.Write( data.GetText() );
1242 outfile.Close();
1243 LoadPattern(currlayer->tempstart, _("clipboard"));
1244 // do NOT delete tempstart -- it can be reloaded by ResetPattern
1245 // or used by ShowPatternInfo
1246 } else {
1247 statusptr->ErrorMessage(_("Could not create tempstart file!"));
1248 }
1249 }
1250 }
1251
1252 // -----------------------------------------------------------------------------
1253
GetScriptFileName(const wxString & text)1254 wxString MainFrame::GetScriptFileName(const wxString& text)
1255 {
1256 // examine given text to see if it contains Lua, Perl or Python code:
1257 // if "--" or "local" or "require" is at start of line then we assume Lua,
1258 // if "use" or "my" is at start of line then we assume Perl,
1259 // if "import" or "from" is at start of line then we assume Python,
1260 // otherwise we compare counts for dollars + semicolons vs colons
1261 int dollars = 0;
1262 int semicolons = 0;
1263 int colons = 0;
1264 int linelen = 0;
1265
1266 // need to be careful converting Unicode wxString to char*
1267 wxCharBuffer buff = text.mb_str(wxConvUTF8);
1268 const char* p = (const char*) buff;
1269 while (*p) {
1270 switch (*p) {
1271 case '#':
1272 // probably a comment, so ignore rest of line
1273 while (*p && *p != 13 && *p != 10) p++;
1274 linelen = 0;
1275 if (*p) p++;
1276 break;
1277 case 34: // double quote -- ignore until quote closes, even multiple lines
1278 p++;
1279 while (*p && *p != 34) p++;
1280 linelen = 0;
1281 if (*p) p++;
1282 break;
1283 case 39: // single quote -- ignore until quote closes
1284 p++;
1285 while (*p && *p != 39 && *p != 13 && *p != 10) p++;
1286 linelen = 0;
1287 if (*p) p++;
1288 break;
1289 case '$': dollars++; linelen++; p++;
1290 break;
1291 case ':': colons++; linelen++; p++;
1292 break;
1293 case ';': semicolons++; linelen++; p++;
1294 break;
1295 case 13: case 10:
1296 // if colon/semicolon is at eol then count it twice
1297 if (linelen > 0 && p[-1] == ':') colons++;
1298 if (linelen > 0 && p[-1] == ';') semicolons++;
1299 linelen = 0;
1300 p++;
1301 break;
1302 case '-':
1303 if (linelen == 1 && p[-1] == '-') return luafile; // Lua comment at start of line
1304 linelen++; p++;
1305 break;
1306 case ' ':
1307 // look for language-specific keyword at start of line
1308 if (linelen == 2 && strncmp(p-2,"my",2) == 0) return perlfile;
1309 if (linelen == 3 && strncmp(p-3,"use",3) == 0) return perlfile;
1310
1311 if (linelen == 5 && strncmp(p-5,"local",5) == 0) return luafile;
1312 if (linelen == 7 && strncmp(p-7,"require",7) == 0) return luafile;
1313
1314 if (linelen == 4 && strncmp(p-4,"from",4) == 0) return pythonfile;
1315 if (linelen == 6 && strncmp(p-6,"import",6) == 0) return pythonfile;
1316 // don't break
1317 default:
1318 if (linelen == 0 && (*p == ' ' || *p == 9)) {
1319 // ignore spaces/tabs at start of line
1320 } else {
1321 linelen++;
1322 }
1323 p++;
1324 }
1325 }
1326
1327 /* check totals:
1328 char msg[128];
1329 sprintf(msg, "dollars=%d semicolons=%d colons=%d", dollars, semicolons, colons);
1330 Note(wxString(msg,wxConvLocal));
1331 */
1332
1333 if (dollars + semicolons > colons)
1334 return perlfile;
1335 else
1336 return pythonfile;
1337 }
1338
1339 // -----------------------------------------------------------------------------
1340
RunClipboard()1341 void MainFrame::RunClipboard()
1342 {
1343 if (generating) {
1344 command_pending = true;
1345 cmdevent.SetId(ID_RUN_CLIP);
1346 Stop();
1347 return;
1348 }
1349
1350 // run script stored in clipboard
1351 wxTextDataObject data;
1352 if (GetTextFromClipboard(&data)) {
1353 // scriptfile extension depends on whether the clipboard data
1354 // contains Perl or Python code
1355 wxString scriptfile = GetScriptFileName( data.GetText() );
1356 // copy clipboard data to scriptfile
1357 wxFile outfile(scriptfile, wxFile::write);
1358 if (outfile.IsOpened()) {
1359 outfile.Write( data.GetText() );
1360 outfile.Close();
1361 RunScript(scriptfile);
1362 } else {
1363 statusptr->ErrorMessage(_("Could not create script file!"));
1364 }
1365 }
1366 }
1367
1368 #if wxCHECK_VERSION(2,9,0)
1369 // restore new wxMenuItem method names in wx 2.9
1370 #define GetText GetItemLabel
1371 #define SetText SetItemLabel
1372 #endif
1373
1374 // -----------------------------------------------------------------------------
1375
OpenRecentPattern(int id)1376 void MainFrame::OpenRecentPattern(int id)
1377 {
1378 if (generating) {
1379 command_pending = true;
1380 cmdevent.SetId(id);
1381 Stop();
1382 return;
1383 }
1384
1385 wxMenuItem* item = patternSubMenu->FindItem(id);
1386 if (item) {
1387 wxString path = item->GetText();
1388 #ifdef __WXGTK__
1389 // remove duplicate underscores
1390 path.Replace(wxT("__"), wxT("_"));
1391 #endif
1392 // remove duplicate ampersands
1393 path.Replace(wxT("&&"), wxT("&"));
1394
1395 // if path isn't absolute then prepend Golly directory
1396 wxFileName fname(path);
1397 if (!fname.IsAbsolute()) path = gollydir + path;
1398
1399 // path might be a zip file so call OpenFile rather than LoadPattern
1400 OpenFile(path);
1401 }
1402 }
1403
1404 // -----------------------------------------------------------------------------
1405
OpenRecentScript(int id)1406 void MainFrame::OpenRecentScript(int id)
1407 {
1408 if (generating) {
1409 command_pending = true;
1410 cmdevent.SetId(id);
1411 Stop();
1412 return;
1413 }
1414
1415 wxMenuItem* item = scriptSubMenu->FindItem(id);
1416 if (item) {
1417 wxString path = item->GetText();
1418 #ifdef __WXGTK__
1419 // remove duplicate underscores
1420 path.Replace(wxT("__"), wxT("_"));
1421 #endif
1422 // remove duplicate ampersands
1423 path.Replace(wxT("&&"), wxT("&"));
1424
1425 // if path isn't absolute then prepend Golly directory
1426 wxFileName fname(path);
1427 if (!fname.IsAbsolute()) path = gollydir + path;
1428
1429 AddRecentScript(path);
1430 RunScript(path);
1431 }
1432 }
1433
1434 // -----------------------------------------------------------------------------
1435
ClearMissingPatterns()1436 void MainFrame::ClearMissingPatterns()
1437 {
1438 int pos = 0;
1439 while (pos < numpatterns) {
1440 wxMenuItem* item = patternSubMenu->FindItemByPosition(pos);
1441 wxString path = item->GetText();
1442 #ifdef __WXGTK__
1443 // remove duplicate underscores
1444 path.Replace(wxT("__"), wxT("_"));
1445 #endif
1446 // remove duplicate ampersands
1447 path.Replace(wxT("&&"), wxT("&"));
1448
1449 // if path isn't absolute then prepend Golly directory
1450 wxFileName fname(path);
1451 if (!fname.IsAbsolute()) path = gollydir + path;
1452
1453 if (wxFileExists(path)) {
1454 // keep this item
1455 pos++;
1456 } else {
1457 // remove this item by shifting up later items
1458 int nextpos = pos + 1;
1459 while (nextpos < numpatterns) {
1460 wxMenuItem* nextitem = patternSubMenu->FindItemByPosition(nextpos);
1461 #ifdef __WXGTK__
1462 // avoid wxGTK bug if item contains underscore
1463 wxString temp = nextitem->GetText();
1464 temp.Replace(wxT("__"), wxT("_"));
1465 temp.Replace(wxT("&"), wxT("&&"));
1466 item->SetText( temp );
1467 #else
1468 item->SetText( nextitem->GetText() );
1469 #endif
1470 item = nextitem;
1471 nextpos++;
1472 }
1473 // delete last item
1474 patternSubMenu->Delete(item);
1475 numpatterns--;
1476 }
1477 }
1478 wxMenuBar* mbar = GetMenuBar();
1479 if (mbar) mbar->Enable(ID_OPEN_RECENT, numpatterns > 0);
1480 }
1481
1482 // -----------------------------------------------------------------------------
1483
ClearMissingScripts()1484 void MainFrame::ClearMissingScripts()
1485 {
1486 int pos = 0;
1487 while (pos < numscripts) {
1488 wxMenuItem* item = scriptSubMenu->FindItemByPosition(pos);
1489 wxString path = item->GetText();
1490 #ifdef __WXGTK__
1491 // remove duplicate underscores
1492 path.Replace(wxT("__"), wxT("_"));
1493 #endif
1494 // remove duplicate ampersands
1495 path.Replace(wxT("&&"), wxT("&"));
1496
1497 // if path isn't absolute then prepend Golly directory
1498 wxFileName fname(path);
1499 if (!fname.IsAbsolute()) path = gollydir + path;
1500
1501 if (wxFileExists(path)) {
1502 // keep this item
1503 pos++;
1504 } else {
1505 // remove this item by shifting up later items
1506 int nextpos = pos + 1;
1507 while (nextpos < numscripts) {
1508 wxMenuItem* nextitem = scriptSubMenu->FindItemByPosition(nextpos);
1509 #ifdef __WXGTK__
1510 // avoid wxGTK bug if item contains underscore
1511 wxString temp = nextitem->GetText();
1512 temp.Replace(wxT("__"), wxT("_"));
1513 temp.Replace(wxT("&"), wxT("&&"));
1514 item->SetText( temp );
1515 #else
1516 item->SetText( nextitem->GetText() );
1517 #endif
1518 item = nextitem;
1519 nextpos++;
1520 }
1521 // delete last item
1522 scriptSubMenu->Delete(item);
1523 numscripts--;
1524 }
1525 }
1526 wxMenuBar* mbar = GetMenuBar();
1527 if (mbar) mbar->Enable(ID_RUN_RECENT, numscripts > 0);
1528 }
1529
1530 // -----------------------------------------------------------------------------
1531
ClearAllPatterns()1532 void MainFrame::ClearAllPatterns()
1533 {
1534 while (numpatterns > 0) {
1535 patternSubMenu->Delete( patternSubMenu->FindItemByPosition(0) );
1536 numpatterns--;
1537 }
1538 wxMenuBar* mbar = GetMenuBar();
1539 if (mbar) mbar->Enable(ID_OPEN_RECENT, false);
1540 }
1541
1542 // -----------------------------------------------------------------------------
1543
ClearAllScripts()1544 void MainFrame::ClearAllScripts()
1545 {
1546 while (numscripts > 0) {
1547 scriptSubMenu->Delete( scriptSubMenu->FindItemByPosition(0) );
1548 numscripts--;
1549 }
1550 wxMenuBar* mbar = GetMenuBar();
1551 if (mbar) mbar->Enable(ID_RUN_RECENT, false);
1552 }
1553
1554 // -----------------------------------------------------------------------------
1555
WritePattern(const wxString & path,pattern_format format,output_compression compression,int top,int left,int bottom,int right)1556 const char* MainFrame::WritePattern(const wxString& path,
1557 pattern_format format,
1558 output_compression compression,
1559 int top, int left, int bottom, int right)
1560 {
1561 // if the format is RLE_format and the grid is bounded then force XRLE_format so that
1562 // position info is recorded (this position will be used when the file is read)
1563 if (format == RLE_format && (currlayer->algo->gridwd > 0 || currlayer->algo->gridht > 0))
1564 format = XRLE_format;
1565 const char* err = writepattern(FILEPATH, *currlayer->algo, format,
1566 compression, top, left, bottom, right);
1567 return err;
1568 }
1569
1570 // -----------------------------------------------------------------------------
1571
SavePattern()1572 bool MainFrame::SavePattern()
1573 {
1574 if (generating) {
1575 command_pending = true;
1576 cmdevent.SetId(wxID_SAVE);
1577 Stop();
1578 return false;
1579 }
1580
1581 if (warn_on_save && currlayer->dirty && currlayer->algo->getGeneration() > currlayer->startgen &&
1582 !TimelineExists()) {
1583 wxString msg = _("Saving this generation will not save the changes you made earlier, ");
1584 msg += _("so you might want to select Reset or Undo and save those changes.");
1585 msg += _("\n\n(This warning can be disabled in Preferences > Layer.)");
1586 Warning(msg);
1587 }
1588
1589 wxString filetypes;
1590 int MCindex, RLEindex;
1591
1592 // initially all formats are not allowed (use any -ve number)
1593 MCindex = RLEindex = -1;
1594
1595 // adding "*.gz" to the file types avoids a duplication bug in the wxOSX app
1596 wxString MCfiles, RLEfiles;
1597 MCfiles = _("Macrocell (*.mc)|*.mc");
1598 MCfiles += _("|Compressed Macrocell (*.mc.gz)|*.mc.gz;*.gz");
1599 if (savexrle) {
1600 RLEfiles = _("Extended RLE (*.rle)|*.rle");
1601 RLEfiles += _("|Compressed Extended RLE (*.rle.gz)|*.rle.gz;*.gz");
1602 } else {
1603 RLEfiles = _("RLE (*.rle)|*.rle");
1604 RLEfiles += _("|Compressed RLE (*.rle.gz)|*.rle.gz;*.gz");
1605 }
1606
1607 bigint top, left, bottom, right;
1608 int itop, ileft, ibottom, iright;
1609 currlayer->algo->findedges(&top, &left, &bottom, &right);
1610
1611 if (currlayer->algo->hyperCapable()) {
1612 // algorithm uses hashlife
1613 if ( viewptr->OutsideLimits(top, left, bottom, right) ) {
1614 // too big so only allow saving as MC file
1615 itop = ileft = ibottom = iright = 0;
1616 filetypes = MCfiles;
1617 MCindex = 0;
1618 } else {
1619 // allow saving as MC or RLE file
1620 itop = top.toint();
1621 ileft = left.toint();
1622 ibottom = bottom.toint();
1623 iright = right.toint();
1624 filetypes = MCfiles;
1625 filetypes += _("|");
1626 filetypes += RLEfiles;
1627 MCindex = 0;
1628 RLEindex = 1;
1629 }
1630 } else {
1631 // allow saving file only if pattern is small enough
1632 if ( viewptr->OutsideLimits(top, left, bottom, right) ) {
1633 statusptr->ErrorMessage(_("Pattern is outside +/- 10^9 boundary."));
1634 return false;
1635 }
1636 itop = top.toint();
1637 ileft = left.toint();
1638 ibottom = bottom.toint();
1639 iright = right.toint();
1640 filetypes = RLEfiles;
1641 RLEindex = 0;
1642 }
1643
1644 wxFileDialog savedlg( this, _("Save pattern"),
1645 opensavedir, currlayer->currname, filetypes,
1646 wxFD_SAVE | wxFD_OVERWRITE_PROMPT );
1647 int button = savedlg.ShowModal();
1648 viewptr->ResetMouseDown();
1649 if (button == wxID_OK) {
1650 wxFileName fullpath( savedlg.GetPath() );
1651 opensavedir = fullpath.GetPath();
1652 wxString ext = fullpath.GetExt();
1653 pattern_format format;
1654 output_compression compression = no_compression;
1655 // detect if user supplied a compression suffix (.gz)
1656 if ( ext.IsSameAs(wxT("gz"),false) ) {
1657 compression = gzip_compression;
1658 ext = wxFileName(fullpath.GetName()).GetExt();
1659 }
1660 // if user supplied a known extension then use that format if it is
1661 // allowed, otherwise use current format specified in filter menu
1662 if ( ext.IsSameAs(wxT("rle"),false) && RLEindex >= 0 ) {
1663 format = savexrle ? XRLE_format : RLE_format;
1664 } else if ( ext.IsSameAs(wxT("mc"),false) && MCindex >= 0 ) {
1665 format = MC_format;
1666 } else if ( savedlg.GetFilterIndex()/2 == MCindex ) {
1667 format = MC_format;
1668 if (savedlg.GetFilterIndex()%2) compression = gzip_compression;
1669 } else if ( savedlg.GetFilterIndex()/2 == RLEindex ) {
1670 format = savexrle ? XRLE_format : RLE_format;
1671 if (savedlg.GetFilterIndex()%2) compression = gzip_compression;
1672 } else {
1673 statusptr->ErrorMessage(_("Bug in SavePattern!"));
1674 return false;
1675 }
1676
1677 const char* err = WritePattern(savedlg.GetPath(), format, compression,
1678 itop, ileft, ibottom, iright);
1679 if (err) {
1680 statusptr->ErrorMessage(wxString(err,wxConvLocal));
1681 } else {
1682 statusptr->DisplayMessage(_("Pattern saved in file: ") + savedlg.GetPath());
1683 AddRecentPattern(savedlg.GetPath());
1684 SaveSucceeded(savedlg.GetPath());
1685 return true;
1686 }
1687 }
1688 return false;
1689 }
1690
1691 // -----------------------------------------------------------------------------
1692
1693 // called by script command to save current pattern to given file
SaveFile(const wxString & path,const wxString & fileformat,bool remember)1694 const char* MainFrame::SaveFile(const wxString& path, const wxString& fileformat, bool remember)
1695 {
1696 bigint top, left, bottom, right;
1697 int itop, ileft, ibottom, iright;
1698 currlayer->algo->findedges(&top, &left, &bottom, &right);
1699
1700 wxString format = fileformat.Lower();
1701 output_compression compression = no_compression;
1702 if (format.EndsWith(wxT(".gz"))) {
1703 compression = gzip_compression;
1704 }
1705
1706 // check that given file format is valid
1707 pattern_format pattfmt;
1708 if (format.StartsWith(wxT("rle"))) {
1709 if ( viewptr->OutsideLimits(top, left, bottom, right) ) {
1710 return "Pattern is too big to save as RLE.";
1711 }
1712 pattfmt = savexrle ? XRLE_format : RLE_format;
1713 itop = top.toint();
1714 ileft = left.toint();
1715 ibottom = bottom.toint();
1716 iright = right.toint();
1717 } else if (format.StartsWith(wxT("mc"))) {
1718 if (!currlayer->algo->hyperCapable()) {
1719 return "Macrocell format is not supported by the current algorithm.";
1720 }
1721 pattfmt = MC_format;
1722 // writepattern will ignore itop, ileft, ibottom, iright
1723 itop = ileft = ibottom = iright = 0;
1724 } else {
1725 return "Unknown pattern format.";
1726 }
1727
1728 const char* err = WritePattern(path, pattfmt, compression,
1729 itop, ileft, ibottom, iright);
1730 if (!err) {
1731 if (remember) AddRecentPattern(path);
1732 SaveSucceeded(path);
1733 }
1734
1735 return err;
1736 }
1737
1738 // -----------------------------------------------------------------------------
1739
SaveSucceeded(const wxString & path)1740 void MainFrame::SaveSucceeded(const wxString& path)
1741 {
1742 // save old info for RememberNameChange
1743 wxString oldname = currlayer->currname;
1744 wxString oldfile = currlayer->currfile;
1745 bool oldsave = currlayer->savestart;
1746 bool olddirty = currlayer->dirty;
1747
1748 if (allowundo && !currlayer->stayclean && inscript) {
1749 SavePendingChanges();
1750 }
1751
1752 if ( currlayer->algo->getGeneration() == currlayer->startgen ) {
1753 // no need to save starting pattern (ResetPattern can load currfile)
1754 currlayer->currfile = path;
1755 currlayer->savestart = false;
1756 }
1757
1758 // set dirty flag false and update currlayer->currname
1759 MarkLayerClean(GetBaseName(path));
1760
1761 if (allowundo && !currlayer->stayclean) {
1762 currlayer->undoredo->RememberNameChange(oldname, oldfile, oldsave, olddirty);
1763 }
1764 }
1765
1766 // -----------------------------------------------------------------------------
1767
SaveOverlay()1768 void MainFrame::SaveOverlay()
1769 {
1770 if (showoverlay && curroverlay->GetOverlayData()) {
1771 wxFileDialog savedlg(this, _("Save overlay as PNG file"),
1772 overlaydir, _("overlay.png"), _("PNG (*.png)|*.png"),
1773 wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
1774 int button = savedlg.ShowModal();
1775 viewptr->ResetMouseDown();
1776 if (button == wxID_OK) {
1777 wxString pngpath = savedlg.GetPath();
1778 wxFileName fullpath(pngpath);
1779 overlaydir = fullpath.GetPath();
1780 curroverlay->SaveOverlay(pngpath);
1781 }
1782 }
1783 }
1784
1785 // -----------------------------------------------------------------------------
1786
1787 static wxString oldfiledir;
1788
ToggleShowFiles()1789 void MainFrame::ToggleShowFiles()
1790 {
1791 if (splitwin->IsSplit()) dirwinwd = splitwin->GetSashPosition();
1792 showfiles = !showfiles;
1793 if (splitwin->IsSplit()) {
1794 // hide left pane
1795 splitwin->Unsplit(filectrl);
1796 oldfiledir = filedir;
1797 } else {
1798 splitwin->SplitVertically(filectrl, RightPane(), dirwinwd);
1799 }
1800 if (showfiles && filedir != oldfiledir) {
1801 // show new file directory
1802 SimplifyTree(filedir, filectrl->GetTreeCtrl(), filectrl->GetRootId());
1803 }
1804 viewptr->SetFocus();
1805 }
1806
1807 // -----------------------------------------------------------------------------
1808
ChangeFileDir()1809 void MainFrame::ChangeFileDir()
1810 {
1811 wxDirDialog dirdlg(this, _("Choose a new file folder"), filedir, wxDD_NEW_DIR_BUTTON);
1812 int button = dirdlg.ShowModal();
1813 viewptr->ResetMouseDown();
1814 if (button == wxID_OK) {
1815 SetFileDir(dirdlg.GetPath());
1816 }
1817 }
1818
1819 // -----------------------------------------------------------------------------
1820
SetFileDir(const wxString & newdir)1821 void MainFrame::SetFileDir(const wxString& newdir)
1822 {
1823 if (filedir != newdir) {
1824 filedir = newdir;
1825 if (showfiles) {
1826 // show new file directory
1827 SimplifyTree(filedir, filectrl->GetTreeCtrl(), filectrl->GetRootId());
1828 }
1829 }
1830 }
1831
1832 // -----------------------------------------------------------------------------
1833
ShowPrefsDialog(const wxString & page)1834 void MainFrame::ShowPrefsDialog(const wxString& page)
1835 {
1836 if (viewptr->waitingforclick) return;
1837
1838 if (generating) {
1839 command_pending = true;
1840 cmdevent.SetId(wxID_PREFERENCES);
1841 Stop();
1842 return;
1843 }
1844
1845 if (inscript) {
1846 // safe to allow prefs dialog while script is running???
1847 // if so, maybe we need some sort of warning like this:
1848 // Warning(_("The currently running script might clobber any changes you make."));
1849 }
1850
1851 int oldtileborder = tileborder;
1852 int oldcontrolspos = controlspos;
1853
1854 if (ChangePrefs(page)) {
1855 // user hit OK button
1856
1857 // if maxpatterns was reduced then we may need to remove some paths
1858 while (numpatterns > maxpatterns) {
1859 numpatterns--;
1860 patternSubMenu->Delete( patternSubMenu->FindItemByPosition(numpatterns) );
1861 }
1862
1863 // if maxscripts was reduced then we may need to remove some paths
1864 while (numscripts > maxscripts) {
1865 numscripts--;
1866 scriptSubMenu->Delete( scriptSubMenu->FindItemByPosition(numscripts) );
1867 }
1868
1869 // randomfill might have changed
1870 SetRandomFillPercentage();
1871
1872 // if mindelay/maxdelay changed then may need to change minexpo and currexpo
1873 UpdateStepExponent();
1874
1875 // maximum memory might have changed
1876 for (int i = 0; i < numlayers; i++) {
1877 Layer* layer = GetLayer(i);
1878 AlgoData* ad = algoinfo[layer->algtype];
1879 if (ad->algomem >= 0)
1880 layer->algo->setMaxMemory(ad->algomem);
1881 }
1882
1883 // tileborder might have changed
1884 if (tilelayers && numlayers > 1 && tileborder != oldtileborder) {
1885 int wd, ht;
1886 bigview->GetClientSize(&wd, &ht);
1887 // wd or ht might be < 1 on Windows
1888 if (wd < 1) wd = 1;
1889 if (ht < 1) ht = 1;
1890 ResizeLayers(wd, ht);
1891 }
1892
1893 // position of translucent controls might have changed
1894 if (controlspos != oldcontrolspos) {
1895 int wd, ht;
1896 if (tilelayers && numlayers > 1) {
1897 for (int i = 0; i < numlayers; i++) {
1898 Layer* layer = GetLayer(i);
1899 layer->tilewin->GetClientSize(&wd, &ht);
1900 layer->tilewin->SetViewSize(wd, ht);
1901 }
1902 }
1903 bigview->GetClientSize(&wd, &ht);
1904 bigview->SetViewSize(wd, ht);
1905 }
1906
1907 SavePrefs();
1908 }
1909
1910 // safer to update everything even if user hit Cancel
1911 UpdateEverything();
1912 }
1913