1 // This file is part of Golly.
2 // See docs/License.html for the copyright notice.
3
4 #include "lifealgo.h"
5 #include "viewport.h" // for MAX_MAG
6 #include "util.h" // for linereader, lifeerrors
7
8 #include "utils.h" // for gColor, SetColor, Warning, Fatal, Beep
9 #include "status.h" // for DisplayMessage
10 #include "algos.h" // for NumAlgos, algoinfo, etc
11 #include "layer.h" // for currlayer
12 #include "prefs.h"
13
14 #ifdef ANDROID_GUI
15 #include "jnicalls.h" // for BeginProgress, etc
16 #endif
17
18 #ifdef WEB_GUI
19 #include "webcalls.h" // for BeginProgress, etc
20 #endif
21
22 #ifdef IOS_GUI
23 #import "PatternViewController.h" // for BeginProgress, etc
24 #endif
25
26 // -----------------------------------------------------------------------------
27
28 // Golly's preferences file is a simple text file.
29
30 const int PREFS_VERSION = 1; // increment if necessary due to changes in syntax/semantics
31 int currversion = PREFS_VERSION; // might be changed by prefs_version
32 const int PREF_LINE_SIZE = 5000; // must be quite long for storing file paths
33
34 std::string supplieddir; // path of parent directory for supplied help/patterns/rules
35 std::string helpdir; // path of directory for supplied help
36 std::string patternsdir; // path of directory for supplied patterns
37 std::string rulesdir; // path of directory for supplied rules
38 std::string userdir; // path of parent directory for user's rules/patterns/downloads
39 std::string userrules; // path of directory for user's rules
40 std::string savedir; // path of directory for user's saved patterns
41 std::string downloaddir; // path of directory for user's downloaded files
42 std::string tempdir; // path of directory for temporary data
43 std::string clipfile; // path of temporary file for storing clipboard data
44 std::string prefsfile; // path of file for storing user's preferences
45
46 // initialize exported preferences:
47
48 int debuglevel = 0; // for displaying debug info if > 0
49 int helpfontsize = 10; // font size in help window
50 char initrule[256] = "B3/S23"; // initial rule
51 bool initautofit = false; // initial autofit setting
52 bool inithyperspeed = false; // initial hyperspeed setting
53 bool initshowhashinfo = false; // initial showhashinfo setting
54 bool savexrle = true; // save RLE file using XRLE format?
55 bool showtool = true; // show tool bar?
56 bool showlayer = false; // show layer bar?
57 bool showedit = true; // show edit bar?
58 bool showallstates = false; // show all cell states in edit bar?
59 bool showstatus = true; // show status bar?
60 bool showexact = false; // show exact numbers in status bar?
61 bool showtimeline = false; // show timeline bar?
62 bool showtiming = false; // show timing messages?
63 bool showgridlines = true; // display grid lines?
64 bool showicons = false; // display icons for cell states?
65 bool swapcolors = false; // swap colors used for cell states?
66 bool allowundo = true; // allow undo/redo?
67 bool allowbeep = true; // okay to play beep sound?
68 bool restoreview = true; // should reset/undo restore view?
69 int canchangerule = 1; // if > 0 then paste can change rule if pattern is empty
70 int randomfill = 50; // random fill percentage (1..100)
71 int opacity = 80; // percentage opacity of live cells in overlays (1..100)
72 int tileborder = 3; // thickness of tiled window borders
73 int mingridmag = 2; // minimum mag to draw grid lines
74 int boldspacing = 10; // spacing of bold grid lines
75 bool showboldlines = true; // show bold grid lines?
76 bool mathcoords = false; // show Y values increasing upwards?
77 bool syncviews = false; // synchronize viewports?
78 bool syncmodes = true; // synchronize touch modes?
79 bool stacklayers = false; // stack all layers?
80 bool tilelayers = false; // tile all layers?
81 bool asktosave = true; // ask to save changes?
82 int newmag = 5; // mag setting for new pattern
83 bool newremovesel = true; // new pattern removes selection?
84 bool openremovesel = true; // opening pattern removes selection?
85 int mindelay = 250; // minimum millisec delay
86 int maxdelay = 2000; // maximum millisec delay
87 int maxhashmem = 100; // maximum memory (in MB) for hashlife-based algos
88
89 int numpatterns = 0; // current number of recent pattern files
90 int maxpatterns = 20; // maximum number of recent pattern files
91 std::list<std::string> recentpatterns; // list of recent pattern files
92
93 gColor borderrgb; // color for border around bounded grid
94 gColor selectrgb; // color for selected cells
95 gColor pastergb; // color for pasted pattern
96
97 paste_mode pmode = Or; // logical paste mode
98
99 // -----------------------------------------------------------------------------
100
GetPasteMode()101 const char* GetPasteMode()
102 {
103 switch (pmode) {
104 case And: return "AND";
105 case Copy: return "COPY";
106 case Or: return "OR";
107 case Xor: return "XOR";
108 default: return "unknown";
109 }
110 }
111
112 // -----------------------------------------------------------------------------
113
SetPasteMode(const char * s)114 void SetPasteMode(const char* s)
115 {
116 if (strcmp(s, "AND") == 0) {
117 pmode = And;
118 } else if (strcmp(s, "COPY") == 0) {
119 pmode = Copy;
120 } else if (strcmp(s, "OR") == 0) {
121 pmode = Or;
122 } else {
123 pmode = Xor;
124 }
125 }
126
127 // -----------------------------------------------------------------------------
128
CreateDefaultColors()129 void CreateDefaultColors()
130 {
131 SetColor(borderrgb, 128, 128, 128); // 50% gray
132 SetColor(selectrgb, 75, 175, 0); // dark green
133 SetColor(pastergb, 255, 0, 0); // red
134 }
135
136 // -----------------------------------------------------------------------------
137
GetColor(const char * value,gColor & rgb)138 void GetColor(const char* value, gColor& rgb)
139 {
140 unsigned int r, g, b;
141 sscanf(value, "%u,%u,%u", &r, &g, &b);
142 SetColor(rgb, r, g, b);
143 }
144
145 // -----------------------------------------------------------------------------
146
SaveColor(FILE * f,const char * name,const gColor rgb)147 void SaveColor(FILE* f, const char* name, const gColor rgb)
148 {
149 fprintf(f, "%s=%u,%u,%u\n", name, rgb.r, rgb.g, rgb.b);
150 }
151
152 // -----------------------------------------------------------------------------
153
SavePrefs()154 void SavePrefs()
155 {
156 if (currlayer == NULL) {
157 // should never happen but play safe
158 Warning("Bug: currlayer is NULL!");
159 return;
160 }
161
162 FILE* f = fopen(prefsfile.c_str(), "w");
163 if (f == NULL) {
164 Warning("Could not save preferences file!");
165 return;
166 }
167
168 fprintf(f, "prefs_version=%d\n", PREFS_VERSION);
169 fprintf(f, "debug_level=%d\n", debuglevel);
170 fprintf(f, "help_font_size=%d (%d..%d)\n", helpfontsize, minfontsize, maxfontsize);
171 fprintf(f, "allow_undo=%d\n", allowundo ? 1 : 0);
172 fprintf(f, "allow_beep=%d\n", allowbeep ? 1 : 0);
173 fprintf(f, "restore_view=%d\n", restoreview ? 1 : 0);
174 fprintf(f, "paste_mode=%s\n", GetPasteMode());
175 fprintf(f, "can_change_rule=%d (0..2)\n", canchangerule);
176 fprintf(f, "random_fill=%d (1..100)\n", randomfill);
177 fprintf(f, "min_delay=%d (0..%d millisecs)\n", mindelay, MAX_DELAY);
178 fprintf(f, "max_delay=%d (0..%d millisecs)\n", maxdelay, MAX_DELAY);
179 fprintf(f, "auto_fit=%d\n", currlayer->autofit ? 1 : 0);
180 fprintf(f, "hyperspeed=%d\n", currlayer->hyperspeed ? 1 : 0);
181 fprintf(f, "hash_info=%d\n", currlayer->showhashinfo ? 1 : 0);
182 fprintf(f, "max_hash_mem=%d\n", maxhashmem);
183
184 fputs("\n", f);
185
186 fprintf(f, "init_algo=%s\n", GetAlgoName(currlayer->algtype));
187 for (int i = 0; i < NumAlgos(); i++) {
188 fputs("\n", f);
189 fprintf(f, "algorithm=%s\n", GetAlgoName(i));
190 fprintf(f, "base_step=%d\n", algoinfo[i]->defbase);
191 SaveColor(f, "status_rgb", algoinfo[i]->statusrgb);
192 SaveColor(f, "from_rgb", algoinfo[i]->fromrgb);
193 SaveColor(f, "to_rgb", algoinfo[i]->torgb);
194 fprintf(f, "use_gradient=%d\n", algoinfo[i]->gradient ? 1 : 0);
195 fputs("colors=", f);
196 for (int state = 0; state < algoinfo[i]->maxstates; state++) {
197 // only write out state,r,g,b tuple if color is different to default
198 if (algoinfo[i]->algor[state] != algoinfo[i]->defr[state] ||
199 algoinfo[i]->algog[state] != algoinfo[i]->defg[state] ||
200 algoinfo[i]->algob[state] != algoinfo[i]->defb[state] ) {
201 fprintf(f, "%d,%d,%d,%d,", state, algoinfo[i]->algor[state],
202 algoinfo[i]->algog[state],
203 algoinfo[i]->algob[state]);
204 }
205 }
206 fputs("\n", f);
207 }
208
209 fputs("\n", f);
210
211 fprintf(f, "rule=%s\n", currlayer->algo->getrule());
212 fprintf(f, "show_tool=%d\n", showtool ? 1 : 0);
213 fprintf(f, "show_layer=%d\n", showlayer ? 1 : 0);
214 fprintf(f, "show_edit=%d\n", showedit ? 1 : 0);
215 fprintf(f, "show_states=%d\n", showallstates ? 1 : 0);
216 fprintf(f, "show_status=%d\n", showstatus ? 1 : 0);
217 fprintf(f, "show_exact=%d\n", showexact ? 1 : 0);
218 fprintf(f, "show_timeline=%d\n", showtimeline ? 1 : 0);
219 fprintf(f, "show_timing=%d\n", showtiming ? 1 : 0);
220 fprintf(f, "grid_lines=%d\n", showgridlines ? 1 : 0);
221 fprintf(f, "min_grid_mag=%d (2..%d)\n", mingridmag, MAX_MAG);
222 fprintf(f, "bold_spacing=%d (2..%d)\n", boldspacing, MAX_SPACING);
223 fprintf(f, "show_bold_lines=%d\n", showboldlines ? 1 : 0);
224 fprintf(f, "math_coords=%d\n", mathcoords ? 1 : 0);
225
226 fputs("\n", f);
227
228 fprintf(f, "sync_views=%d\n", syncviews ? 1 : 0);
229 fprintf(f, "sync_modes=%d\n", syncmodes ? 1 : 0);
230 fprintf(f, "stack_layers=%d\n", stacklayers ? 1 : 0);
231 fprintf(f, "tile_layers=%d\n", tilelayers ? 1 : 0);
232 fprintf(f, "tile_border=%d (1..10)\n", tileborder);
233 fprintf(f, "ask_to_save=%d\n", asktosave ? 1 : 0);
234
235 fputs("\n", f);
236
237 fprintf(f, "show_icons=%d\n", showicons ? 1 : 0);
238 fprintf(f, "swap_colors=%d\n", swapcolors ? 1 : 0);
239 fprintf(f, "opacity=%d (1..100)\n", opacity);
240 SaveColor(f, "border_rgb", borderrgb);
241 SaveColor(f, "select_rgb", selectrgb);
242 SaveColor(f, "paste_rgb", pastergb);
243
244 fputs("\n", f);
245
246 fprintf(f, "new_mag=%d (0..%d)\n", newmag, MAX_MAG);
247 fprintf(f, "new_remove_sel=%d\n", newremovesel ? 1 : 0);
248 fprintf(f, "open_remove_sel=%d\n", openremovesel ? 1 : 0);
249 fprintf(f, "save_xrle=%d\n", savexrle ? 1 : 0);
250 fprintf(f, "max_patterns=%d (1..%d)\n", maxpatterns, MAX_RECENT);
251
252 if (!recentpatterns.empty()) {
253 fputs("\n", f);
254 std::list<std::string>::iterator next = recentpatterns.begin();
255 while (next != recentpatterns.end()) {
256 std::string path = *next;
257 fprintf(f, "recent_pattern=%s\n", path.c_str());
258 next++;
259 }
260 }
261
262 fclose(f);
263 }
264
265 // -----------------------------------------------------------------------------
266
GetKeywordAndValue(linereader & lr,char * line,char ** keyword,char ** value)267 bool GetKeywordAndValue(linereader& lr, char* line, char** keyword, char** value)
268 {
269 // the linereader class handles all line endings (CR, CR+LF, LF)
270 // and terminates line buffer with \0
271 while ( lr.fgets(line, PREF_LINE_SIZE) != 0 ) {
272 if ( line[0] == '#' || line[0] == 0 ) {
273 // skip comment line or empty line
274 } else {
275 // line should have format keyword=value
276 *keyword = line;
277 *value = line;
278 while ( **value != '=' && **value != 0 ) *value += 1;
279 **value = 0; // terminate keyword
280 *value += 1;
281 return true;
282 }
283 }
284 return false;
285 }
286
287 // -----------------------------------------------------------------------------
288
ReplaceDeprecatedAlgo(char * algoname)289 char* ReplaceDeprecatedAlgo(char* algoname)
290 {
291 if (strcmp(algoname, "RuleTable") == 0 ||
292 strcmp(algoname, "RuleTree") == 0) {
293 // RuleTable and RuleTree algos have been replaced by RuleLoader
294 return (char*)"RuleLoader";
295 } else {
296 return algoname;
297 }
298 }
299
300 // -----------------------------------------------------------------------------
301
302 // let gollybase code call Fatal, Warning, BeginProgress, etc
303
304 class my_errors : public lifeerrors
305 {
306 public:
fatal(const char * s)307 virtual void fatal(const char* s) {
308 Fatal(s);
309 }
310
warning(const char * s)311 virtual void warning(const char* s) {
312 Warning(s);
313 }
314
status(const char * s)315 virtual void status(const char* s) {
316 DisplayMessage(s);
317 }
318
beginprogress(const char * s)319 virtual void beginprogress(const char* s) {
320 BeginProgress(s);
321 // init flag for isaborted() calls
322 aborted = false;
323 }
324
abortprogress(double f,const char * s)325 virtual bool abortprogress(double f, const char* s) {
326 return AbortProgress(f, s);
327 }
328
endprogress()329 virtual void endprogress() {
330 EndProgress();
331 }
332
getuserrules()333 virtual const char* getuserrules() {
334 return (const char*) userrules.c_str();
335 }
336
getrulesdir()337 virtual const char* getrulesdir() {
338 return (const char*) rulesdir.c_str();
339 }
340 };
341
342 static my_errors myerrhandler; // create instance
343
344 // -----------------------------------------------------------------------------
345
GetPrefs()346 void GetPrefs()
347 {
348 int algoindex = -1; // unknown algorithm
349
350 // let gollybase code call Fatal, Warning, BeginProgress, etc
351 lifeerrors::seterrorhandler(&myerrhandler);
352
353 CreateDefaultColors();
354
355 FILE* f = fopen(prefsfile.c_str(), "r");
356 if (f == NULL) {
357 // should only happen 1st time app is run
358 return;
359 }
360
361 linereader reader(f);
362 char line[PREF_LINE_SIZE];
363 char* keyword;
364 char* value;
365 while ( GetKeywordAndValue(reader, line, &keyword, &value) ) {
366
367 if (strcmp(keyword, "prefs_version") == 0) {
368 sscanf(value, "%d", &currversion);
369
370 } else if (strcmp(keyword, "debug_level") == 0) {
371 sscanf(value, "%d", &debuglevel);
372
373 } else if (strcmp(keyword, "help_font_size") == 0) {
374 sscanf(value, "%d", &helpfontsize);
375 if (helpfontsize < minfontsize) helpfontsize = minfontsize;
376 if (helpfontsize > maxfontsize) helpfontsize = maxfontsize;
377
378 } else if (strcmp(keyword, "allow_undo") == 0) {
379 allowundo = value[0] == '1';
380
381 } else if (strcmp(keyword, "allow_beep") == 0) {
382 allowbeep = value[0] == '1';
383
384 } else if (strcmp(keyword, "restore_view") == 0) {
385 restoreview = value[0] == '1';
386
387 } else if (strcmp(keyword, "paste_mode") == 0) {
388 SetPasteMode(value);
389
390 } else if (strcmp(keyword, "can_change_rule") == 0) {
391 sscanf(value, "%d", &canchangerule);
392 if (canchangerule < 0) canchangerule = 0;
393 if (canchangerule > 2) canchangerule = 2;
394
395 } else if (strcmp(keyword, "random_fill") == 0) {
396 sscanf(value, "%d", &randomfill);
397 if (randomfill < 1) randomfill = 1;
398 if (randomfill > 100) randomfill = 100;
399
400 } else if (strcmp(keyword, "algorithm") == 0) {
401 if (strcmp(value, "RuleTable") == 0) {
402 // use deprecated RuleTable settings for RuleLoader
403 // (deprecated RuleTree settings will simply be ignored)
404 value = (char*)"RuleLoader";
405 }
406 algoindex = -1;
407 for (int i = 0; i < NumAlgos(); i++) {
408 if (strcmp(value, GetAlgoName(i)) == 0) {
409 algoindex = i;
410 break;
411 }
412 }
413
414 } else if (strcmp(keyword, "base_step") == 0) {
415 if (algoindex >= 0 && algoindex < NumAlgos()) {
416 int base;
417 sscanf(value, "%d", &base);
418 if (base < 2) base = 2;
419 if (base > MAX_BASESTEP) base = MAX_BASESTEP;
420 algoinfo[algoindex]->defbase = base;
421 }
422
423 } else if (strcmp(keyword, "status_rgb") == 0) {
424 if (algoindex >= 0 && algoindex < NumAlgos())
425 GetColor(value, algoinfo[algoindex]->statusrgb);
426
427 } else if (strcmp(keyword, "from_rgb") == 0) {
428 if (algoindex >= 0 && algoindex < NumAlgos())
429 GetColor(value, algoinfo[algoindex]->fromrgb);
430
431 } else if (strcmp(keyword, "to_rgb") == 0) {
432 if (algoindex >= 0 && algoindex < NumAlgos())
433 GetColor(value, algoinfo[algoindex]->torgb);
434
435 } else if (strcmp(keyword, "use_gradient") == 0) {
436 if (algoindex >= 0 && algoindex < NumAlgos())
437 algoinfo[algoindex]->gradient = value[0] == '1';
438
439 } else if (strcmp(keyword, "colors") == 0) {
440 if (algoindex >= 0 && algoindex < NumAlgos()) {
441 int state, r, g, b;
442 while (sscanf(value, "%d,%d,%d,%d,", &state, &r, &g, &b) == 4) {
443 if (state >= 0 && state < algoinfo[algoindex]->maxstates) {
444 algoinfo[algoindex]->algor[state] = r;
445 algoinfo[algoindex]->algog[state] = g;
446 algoinfo[algoindex]->algob[state] = b;
447 }
448 while (*value != ',') value++; value++;
449 while (*value != ',') value++; value++;
450 while (*value != ',') value++; value++;
451 while (*value != ',') value++; value++;
452 }
453 }
454
455 } else if (strcmp(keyword, "min_delay") == 0) {
456 sscanf(value, "%d", &mindelay);
457 if (mindelay < 0) mindelay = 0;
458 if (mindelay > MAX_DELAY) mindelay = MAX_DELAY;
459
460 } else if (strcmp(keyword, "max_delay") == 0) {
461 sscanf(value, "%d", &maxdelay);
462 if (maxdelay < 0) maxdelay = 0;
463 if (maxdelay > MAX_DELAY) maxdelay = MAX_DELAY;
464
465 } else if (strcmp(keyword, "auto_fit") == 0) {
466 initautofit = value[0] == '1';
467
468 } else if (strcmp(keyword, "init_algo") == 0) {
469 value = ReplaceDeprecatedAlgo(value);
470 int i = staticAlgoInfo::nameToIndex(value);
471 if (i >= 0 && i < NumAlgos())
472 initalgo = i;
473
474 } else if (strcmp(keyword, "hyperspeed") == 0) {
475 inithyperspeed = value[0] == '1';
476
477 } else if (strcmp(keyword, "hash_info") == 0) {
478 initshowhashinfo = value[0] == '1';
479
480 } else if (strcmp(keyword, "max_hash_mem") == 0) {
481 sscanf(value, "%d", &maxhashmem);
482 if (maxhashmem < MIN_MEM_MB) maxhashmem = MIN_MEM_MB;
483 if (maxhashmem > MAX_MEM_MB) maxhashmem = MAX_MEM_MB;
484
485 } else if (strcmp(keyword, "rule") == 0) {
486 strncpy(initrule, value, sizeof(initrule));
487
488 } else if (strcmp(keyword, "show_tool") == 0) {
489 showtool = value[0] == '1';
490
491 } else if (strcmp(keyword, "show_layer") == 0) {
492 showlayer = value[0] == '1';
493
494 } else if (strcmp(keyword, "show_edit") == 0) {
495 showedit = value[0] == '1';
496
497 } else if (strcmp(keyword, "show_states") == 0) {
498 showallstates = value[0] == '1';
499
500 } else if (strcmp(keyword, "show_status") == 0) {
501 showstatus = value[0] == '1';
502
503 } else if (strcmp(keyword, "show_exact") == 0) {
504 showexact = value[0] == '1';
505
506 } else if (strcmp(keyword, "show_timeline") == 0) {
507 showtimeline = value[0] == '1';
508
509 } else if (strcmp(keyword, "show_timing") == 0) {
510 showtiming = value[0] == '1';
511
512 } else if (strcmp(keyword, "grid_lines") == 0) {
513 showgridlines = value[0] == '1';
514
515 } else if (strcmp(keyword, "min_grid_mag") == 0) {
516 sscanf(value, "%d", &mingridmag);
517 if (mingridmag < 2) mingridmag = 2;
518 if (mingridmag > MAX_MAG) mingridmag = MAX_MAG;
519
520 } else if (strcmp(keyword, "bold_spacing") == 0) {
521 sscanf(value, "%d", &boldspacing);
522 if (boldspacing < 2) boldspacing = 2;
523 if (boldspacing > MAX_SPACING) boldspacing = MAX_SPACING;
524
525 } else if (strcmp(keyword, "show_bold_lines") == 0) {
526 showboldlines = value[0] == '1';
527
528 } else if (strcmp(keyword, "math_coords") == 0) {
529 mathcoords = value[0] == '1';
530
531 } else if (strcmp(keyword, "sync_views") == 0) {
532 syncviews = value[0] == '1';
533
534 } else if (strcmp(keyword, "sync_modes") == 0) {
535 syncmodes = value[0] == '1';
536
537 } else if (strcmp(keyword, "stack_layers") == 0) {
538 stacklayers = value[0] == '1';
539
540 } else if (strcmp(keyword, "tile_layers") == 0) {
541 tilelayers = value[0] == '1';
542
543 } else if (strcmp(keyword, "tile_border") == 0) {
544 sscanf(value, "%d", &tileborder);
545 if (tileborder < 1) tileborder = 1;
546 if (tileborder > 10) tileborder = 10;
547
548 } else if (strcmp(keyword, "ask_to_save") == 0) {
549 asktosave = value[0] == '1';
550
551 } else if (strcmp(keyword, "show_icons") == 0) {
552 showicons = value[0] == '1';
553
554 } else if (strcmp(keyword, "swap_colors") == 0) {
555 swapcolors = value[0] == '1';
556
557 } else if (strcmp(keyword, "opacity") == 0) {
558 sscanf(value, "%d", &opacity);
559 if (opacity < 1) opacity = 1;
560 if (opacity > 100) opacity = 100;
561
562 } else if (strcmp(keyword, "border_rgb") == 0) { GetColor(value, borderrgb);
563 } else if (strcmp(keyword, "select_rgb") == 0) { GetColor(value, selectrgb);
564 } else if (strcmp(keyword, "paste_rgb") == 0) { GetColor(value, pastergb);
565
566 } else if (strcmp(keyword, "new_mag") == 0) {
567 sscanf(value, "%d", &newmag);
568 if (newmag < 0) newmag = 0;
569 if (newmag > MAX_MAG) newmag = MAX_MAG;
570
571 } else if (strcmp(keyword, "new_remove_sel") == 0) {
572 newremovesel = value[0] == '1';
573
574 } else if (strcmp(keyword, "open_remove_sel") == 0) {
575 openremovesel = value[0] == '1';
576
577 } else if (strcmp(keyword, "save_xrle") == 0) {
578 savexrle = value[0] == '1';
579
580 } else if (strcmp(keyword, "max_patterns") == 0) {
581 sscanf(value, "%d", &maxpatterns);
582 if (maxpatterns < 1) maxpatterns = 1;
583 if (maxpatterns > MAX_RECENT) maxpatterns = MAX_RECENT;
584
585 } else if (strcmp(keyword, "recent_pattern") == 0) {
586 if (numpatterns < maxpatterns && value[0]) {
587 // append path to recentpatterns if file exists
588 std::string path = value;
589 if (path.find("Patterns/") == 0 || FileExists(userdir + path)) {
590 recentpatterns.push_back(path);
591 numpatterns++;
592 }
593 }
594 }
595 }
596 reader.close();
597
598 // stacklayers and tilelayers must not both be true
599 if (stacklayers && tilelayers) tilelayers = false;
600 }
601