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("&nbsp;&nbsp;&nbsp;&nbsp;");
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