1// This file is part of Golly.
2// See docs/License.html for the copyright notice.
3
4#include "viewport.h"    // for MAX_MAG
5
6#include "utils.h"       // for Warning, event_checker
7#include "status.h"      // for SetMessage, DisplayMessage
8#include "algos.h"       // for InitAlgorithms, algoinfo
9#include "prefs.h"       // for GetPrefs, SavePrefs, userdir, etc
10#include "layer.h"       // for AddLayer, currlayer
11#include "file.h"        // for NewPattern
12#include "control.h"     // for generating, StartGenerating, etc
13#include "view.h"        // for nopattupdate, SmallScroll, drawingcells
14#include "undo.h"        // for currlayer->undoredo->...
15
16#import "GollyAppDelegate.h"        // for EnableTabBar
17#import "InfoViewController.h"
18#import "SaveViewController.h"
19#import "RuleViewController.h"
20#import "PatternViewController.h"
21
22@implementation PatternViewController
23
24// -----------------------------------------------------------------------------
25
26// global stuff needed in UpdatePattern, UpdateStatus, etc
27
28static PatternViewController *globalController = nil;
29static PatternView *globalPatternView = nil;
30static StatusView *globalStatusView = nil;
31static StateView *globalStateView = nil;
32static UIView *globalProgress = nil;
33static UILabel *globalTitle = nil;
34
35static bool cancelProgress = false;     // cancel progress dialog?
36static double progstart, prognext;      // for progress timing
37static int progresscount = 0;           // if > 0 then BeginProgress has been called
38static int pausecount = 0;              // if > 0 then genTimer needs to be restarted
39
40// -----------------------------------------------------------------------------
41
42- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
43{
44    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
45    if (self) {
46        self.title = @"Pattern";
47        self.tabBarItem.image = [UIImage imageNamed:@"pattern.png"];
48    }
49    return self;
50}
51
52// -----------------------------------------------------------------------------
53
54- (void)didReceiveMemoryWarning
55{
56    [super didReceiveMemoryWarning];
57
58    // free up as much memory as possible
59    if (numlayers > 0) DeleteOtherLayers();
60    if (waitingforpaste) AbortPaste();
61    [self doNew:self];
62
63    // Warning hangs if called here, so use DisplayMessage for now!!!
64    // (may need non-modal version via extra flag???)
65    // Warning("Memory warning occurred, so empty universe created.");
66    DisplayMessage("Memory warning occurred, so empty universe created.");
67}
68
69// -----------------------------------------------------------------------------
70
71static void CreateDocSubdir(NSString *subdirname)
72{
73    // create given subdirectory inside Documents
74    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
75    NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:subdirname];
76    if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
77        // do nothing if subdir already exists
78    } else {
79        NSError *error;
80        if (![[NSFileManager defaultManager] createDirectoryAtPath:path
81                                       withIntermediateDirectories:NO
82                                                        attributes:nil
83                                                             error:&error]) {
84            NSLog(@"Error creating %@ subdirectory: %@", subdirname, error);
85        }
86    }
87}
88
89// -----------------------------------------------------------------------------
90
91static void InitPaths()
92{
93    // init userdir to directory containing user's data
94    userdir = [NSHomeDirectory() cStringUsingEncoding:NSUTF8StringEncoding];
95    userdir += "/";
96
97    // init savedir to userdir/Documents/Saved/
98    savedir = userdir + "Documents/Saved/";
99    CreateDocSubdir(@"Saved");
100
101    // init downloaddir to userdir/Documents/Downloads/
102    downloaddir = userdir + "Documents/Downloads/";
103    CreateDocSubdir(@"Downloads");
104
105    // init userrules to userdir/Documents/Rules/
106    userrules = userdir + "Documents/Rules/";
107    CreateDocSubdir(@"Rules");
108
109    // supplied patterns, rules, help are bundled inside Golly.app
110    NSString *appdir = [[NSBundle mainBundle] resourcePath];
111    supplieddir = [appdir cStringUsingEncoding:NSUTF8StringEncoding];
112    supplieddir += "/";
113    patternsdir = supplieddir + "Patterns/";
114    rulesdir = supplieddir + "Rules/";
115    helpdir = supplieddir + "Help/";
116
117    // init tempdir to userdir/tmp/
118    tempdir = userdir + "tmp/";
119
120    // init path to file that stores clipboard data
121    clipfile = tempdir + "golly_clipboard";
122
123    // init path to file that stores user preferences
124    prefsfile = userdir + "Library/Preferences/GollyPrefs";
125
126#if 0
127    NSLog(@"userdir =     %s", userdir.c_str());
128    NSLog(@"savedir =     %s", savedir.c_str());
129    NSLog(@"downloaddir = %s", downloaddir.c_str());
130    NSLog(@"userrules =   %s", userrules.c_str());
131    NSLog(@"supplieddir = %s", supplieddir.c_str());
132    NSLog(@"patternsdir = %s", patternsdir.c_str());
133    NSLog(@"rulesdir =    %s", rulesdir.c_str());
134    NSLog(@"helpdir =     %s", helpdir.c_str());
135    NSLog(@"tempdir =     %s", tempdir.c_str());
136    NSLog(@"clipfile =    %s", clipfile.c_str());
137    NSLog(@"prefsfile =   %s", prefsfile.c_str());
138#endif
139}
140
141// -----------------------------------------------------------------------------
142
143- (void)viewDidLoad
144{
145    [super viewDidLoad];
146
147	// now do additional setup after loading the view from the xib file
148
149    // init global pointers
150    globalController = self;
151    globalPatternView = pattView;
152    globalStatusView = statView;
153    globalStateView = stateView;
154    globalProgress = progressView;
155    globalTitle = progressTitle;
156
157    static bool firstload = true;
158    if (firstload) {
159        firstload = false;          // only do the following once
160        SetMessage("This is Golly 1.2 for iOS.  Copyright 2017 The Golly Gang.");
161        MAX_MAG = 5;                // maximum cell size = 32x32
162        InitAlgorithms();           // must initialize algoinfo first
163        InitPaths();                // init userdir, etc (must be before GetPrefs)
164        GetPrefs();                 // load user's preferences
165        SetMinimumStepExponent();   // for slowest speed
166        AddLayer();                 // create initial layer (sets currlayer)
167        NewPattern();               // create new, empty universe
168    }
169
170    // ensure pattView is composited underneath toolbar, otherwise
171    // bottom toolbar can be obscured when device is rotated left/right
172    // (no longer needed)
173    // [[pattView layer] setZPosition:-1];
174
175    // we draw background areas of pattView and statView so set to transparent (presumably a tad faster)
176    [pattView setBackgroundColor:nil];
177    [statView setBackgroundColor:nil];
178
179    progressView.hidden = YES;
180
181    genTimer = nil;
182
183    // init buttons
184    [self toggleStartStopButton];
185    resetButton.enabled = NO;
186    undoButton.enabled = NO;
187    redoButton.enabled = NO;
188    actionButton.enabled = NO;
189    infoButton.enabled = NO;
190    restoreButton.hidden = YES;
191
192    // deselect step/scale controls (these have Momentary attribute set)
193    [stepControl setSelectedSegmentIndex:-1];
194    [scaleControl setSelectedSegmentIndex:-1];
195
196    // init touch mode
197    [modeControl setSelectedSegmentIndex:currlayer->touchmode];
198
199    [self updateDrawingState];
200}
201
202// -----------------------------------------------------------------------------
203
204- (void)viewDidUnload
205{
206    [super viewDidUnload];
207
208    SavePrefs();
209
210    // release all outlets
211    pattView = nil;
212    statView = nil;
213    startStopButton = nil;
214    restoreButton = nil;
215    resetButton = nil;
216    undoButton = nil;
217    redoButton = nil;
218    actionButton = nil;
219    infoButton = nil;
220    stepControl = nil;
221    scaleControl = nil;
222    modeControl = nil;
223    stateLabel = nil;
224    stateView = nil;
225    topBar = nil;
226    editBar = nil;
227    bottomBar = nil;
228    progressView = nil;
229    progressTitle = nil;
230    progressMessage = nil;
231    progressBar = nil;
232    cancelButton = nil;
233}
234
235// -----------------------------------------------------------------------------
236
237- (void)viewWillDisappear:(BOOL)animated
238{
239	[super viewWillDisappear:animated];
240
241    SetMessage("");
242
243    // stop generating, but only if PauseGenTimer() hasn't been called
244    if (pausecount == 0) [self stopIfGenerating];
245}
246
247// -----------------------------------------------------------------------------
248
249- (void)viewDidAppear:(BOOL)animated
250{
251	[super viewDidAppear:animated];
252
253    UpdateEverything();     // calls [pattView refreshPattern]
254
255    // restart genTimer if PauseGenTimer() was called earlier
256    if (pausecount > 0) RestartGenTimer();
257}
258
259// -----------------------------------------------------------------------------
260
261- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
262{
263    // return YES for supported orientations
264    return YES;
265}
266
267// -----------------------------------------------------------------------------
268
269- (void)updateDrawingState;
270{
271    // reset drawing state if it's no longer valid (due to algo/rule change)
272    if (currlayer->drawingstate >= currlayer->algo->NumCellStates()) {
273        currlayer->drawingstate = 1;
274    }
275
276    [stateLabel setText:[NSString stringWithFormat:@"State=%d", currlayer->drawingstate]];
277    [stateView setNeedsDisplay];
278}
279
280// -----------------------------------------------------------------------------
281
282- (void)updateButtons
283{
284    resetButton.enabled = currlayer->algo->getGeneration() > currlayer->startgen;
285    undoButton.enabled = currlayer->undoredo->CanUndo();
286    redoButton.enabled = currlayer->undoredo->CanRedo();
287    actionButton.enabled = SelectionExists();
288    infoButton.enabled = currlayer->currname != "untitled";
289    [modeControl setSelectedSegmentIndex:currlayer->touchmode];
290}
291
292// -----------------------------------------------------------------------------
293
294- (void)toggleStartStopButton
295{
296    if (generating) {
297        [startStopButton setTitle:@"STOP"
298                         forState:UIControlStateNormal];
299        [startStopButton setTitleColor:[UIColor redColor]
300                              forState:UIControlStateNormal];
301    } else {
302        [startStopButton setTitle:@"START"
303                         forState:UIControlStateNormal];
304        [startStopButton setTitleColor:[UIColor colorWithRed:0.2 green:0.7 blue:0.0 alpha:1.0]
305                              forState:UIControlStateNormal];
306    }
307}
308
309// -----------------------------------------------------------------------------
310
311- (void)stopIfGenerating
312{
313    if (generating) {
314        [self stopGenTimer];
315        StopGenerating();
316        // generating now false
317        [self toggleStartStopButton];
318    }
319}
320
321// -----------------------------------------------------------------------------
322
323- (IBAction)doNew:(id)sender
324{
325    if (drawingcells) return;
326
327    // undo/redo history is about to be cleared so no point calling RememberGenFinish
328    // if we're generating a (possibly large) pattern
329    bool saveundo = allowundo;
330    allowundo = false;
331    [self stopIfGenerating];
332    allowundo = saveundo;
333
334    if (event_checker > 0) {
335        // try again after a short delay that gives time for NextGeneration() to terminate
336        [self performSelector:@selector(doNew:) withObject:sender afterDelay:0.01];
337        return;
338    }
339
340    ClearMessage();
341    NewPattern();
342
343    [modeControl setSelectedSegmentIndex:currlayer->touchmode];
344
345    [self updateDrawingState];
346    [self updateButtons];
347
348    [statView setNeedsDisplay];
349    [pattView refreshPattern];
350}
351
352// -----------------------------------------------------------------------------
353
354- (IBAction)doInfo:(id)sender
355{
356    if (drawingcells) return;
357
358    // if generating then just pause the timer so doGeneration won't be called;
359    // best not to call PauseGenerating() because it calls StopGenerating()
360    // which might result in a lengthy file save for undo/redo
361    PauseGenTimer();
362
363    ClearMessage();
364
365    // display contents of current pattern file in a modal view
366    InfoViewController *modalInfoController = [[InfoViewController alloc] initWithNibName:nil bundle:nil];
367
368    [modalInfoController setModalPresentationStyle:UIModalPresentationFullScreen];
369    [self presentViewController:modalInfoController animated:YES completion:nil];
370
371    modalInfoController = nil;
372
373    // RestartGenTimer() will be called in viewDidAppear
374}
375
376// -----------------------------------------------------------------------------
377
378- (IBAction)doSave:(id)sender
379{
380    if (drawingcells) return;
381
382    [self stopIfGenerating];
383
384    if (event_checker > 0) {
385        // try again after a short delay
386        [self performSelector:@selector(doSave:) withObject:sender afterDelay:0.01];
387        return;
388    }
389
390    ClearMessage();
391
392    // let user save current pattern in a file via a modal view
393    SaveViewController *modalSaveController = [[SaveViewController alloc] initWithNibName:nil bundle:nil];
394
395    [modalSaveController setModalPresentationStyle:UIModalPresentationFormSheet];
396    [self presentViewController:modalSaveController animated:YES completion:nil];
397
398    modalSaveController = nil;
399}
400
401// -----------------------------------------------------------------------------
402
403- (IBAction)doUndo:(id)sender
404{
405    if (drawingcells) return;
406
407    [self stopIfGenerating];
408
409    if (event_checker > 0) {
410        // try again after a short delay
411        [self performSelector:@selector(doUndo:) withObject:sender afterDelay:0.01];
412        return;
413    }
414
415    ClearMessage();
416    currlayer->undoredo->UndoChange();
417    UpdateEverything();
418}
419
420// -----------------------------------------------------------------------------
421
422- (IBAction)doRedo:(id)sender
423{
424    if (drawingcells) return;
425
426    [self stopIfGenerating];
427
428    if (event_checker > 0) {
429        // try again after a short delay
430        [self performSelector:@selector(doRedo:) withObject:sender afterDelay:0.01];
431        return;
432    }
433
434    ClearMessage();
435    currlayer->undoredo->RedoChange();
436    UpdateEverything();
437}
438
439// -----------------------------------------------------------------------------
440
441- (IBAction)doReset:(id)sender
442{
443    if (drawingcells) return;
444
445    [self stopIfGenerating];
446
447    if (event_checker > 0) {
448        // try again after a short delay
449        [self performSelector:@selector(doReset:) withObject:sender afterDelay:0.01];
450        return;
451    }
452
453    ClearMessage();
454    ResetPattern();
455    UpdateEverything();
456}
457
458// -----------------------------------------------------------------------------
459
460- (IBAction)doStartStop:(id)sender
461{
462    if (drawingcells) return;
463    // this can happen on iPad if user taps Start/Stop button while another finger
464    // is currently drawing cells
465
466    ClearMessage();
467    if (generating) {
468        [self stopGenTimer];
469        StopGenerating();
470        // generating is now false
471        [self toggleStartStopButton];
472        // can't call [self updateButtons] here because if event_checker is > 0
473        // then StopGenerating hasn't called RememberGenFinish, and so CanUndo/CanRedo
474        // might not return correct results
475        bool canreset = currlayer->algo->getGeneration() > currlayer->startgen;
476        resetButton.enabled = canreset;
477        undoButton.enabled = allowundo && (canreset || currlayer->undoredo->CanUndo());
478        redoButton.enabled = NO;
479
480    } else if (StartGenerating()) {
481        // generating is now true
482        [self toggleStartStopButton];
483        [self startGenTimer];
484        // don't call [self updateButtons] here because we want user to
485        // be able to stop generating by tapping Reset/Undo buttons
486        resetButton.enabled = YES;
487        undoButton.enabled = allowundo;
488        redoButton.enabled = NO;
489    }
490    pausecount = 0;     // play safe
491}
492
493// -----------------------------------------------------------------------------
494
495- (void)startGenTimer
496{
497    float interval = 1.0f/60.0f;
498
499    // increase interval if user wants a delay
500    if (currlayer->currexpo < 0) {
501        interval = GetCurrentDelay() / 1000.0f;
502    }
503
504    // create a repeating timer
505    genTimer = [NSTimer scheduledTimerWithTimeInterval:interval
506                                                target:self
507                                              selector:@selector(doGeneration:)
508                                              userInfo:nil
509                                               repeats:YES];
510}
511
512// -----------------------------------------------------------------------------
513
514- (void)stopGenTimer
515{
516    [genTimer invalidate];
517    genTimer = nil;
518}
519
520// -----------------------------------------------------------------------------
521
522// called after genTimer fires
523
524- (void)doGeneration:(NSTimer*)timer
525{
526    if (event_checker > 0 || progresscount > 0) return;
527
528    // advance by current step size
529    NextGeneration(true);
530
531    [statView setNeedsDisplay];
532    [pattView refreshPattern];
533}
534
535// -----------------------------------------------------------------------------
536
537- (IBAction)doNext:(id)sender
538{
539    if (drawingcells) return;
540
541    [self stopIfGenerating];
542
543    if (event_checker > 0) {
544        // try again after a short delay
545        [self performSelector:@selector(doNext:) withObject:sender afterDelay:0.01];
546        return;
547    }
548
549    ClearMessage();
550    NextGeneration(false);
551
552    [statView setNeedsDisplay];
553    [pattView refreshPattern];
554    [self updateButtons];
555}
556
557// -----------------------------------------------------------------------------
558
559- (IBAction)doStep:(id)sender
560{
561    if (drawingcells) return;
562
563    [self stopIfGenerating];
564
565    if (event_checker > 0) {
566        // try again after a short delay
567        [self performSelector:@selector(doStep:) withObject:sender afterDelay:0.01];
568        return;
569    }
570
571    ClearMessage();
572    NextGeneration(true);
573
574    [statView setNeedsDisplay];
575    [pattView refreshPattern];
576    [self updateButtons];
577}
578
579// -----------------------------------------------------------------------------
580
581- (IBAction)doFit:(id)sender
582{
583    ClearMessage();
584    FitInView(1);
585
586    [statView setNeedsDisplay];
587    [pattView refreshPattern];
588}
589
590// -----------------------------------------------------------------------------
591
592- (IBAction)doChangeStep:(id)sender
593{
594    ClearMessage();
595    switch([sender selectedSegmentIndex])
596    {
597        case 0:
598        {
599            // go slower by decrementing step exponent
600            if (currlayer->currexpo > minexpo) {
601                currlayer->currexpo--;
602                SetGenIncrement();
603                if (generating && currlayer->currexpo < 0) {
604                    // increase timer interval
605                    [self stopGenTimer];
606                    [self startGenTimer];
607                }
608            } else {
609                Beep();
610            }
611        } break;
612
613        case 1:
614        {
615            // reset step exponent to 0
616            currlayer->currexpo = 0;
617            SetGenIncrement();
618            if (generating) {
619                // reset timer interval to max speed
620                [self stopGenTimer];
621                [self startGenTimer];
622            }
623        } break;
624
625        case 2:
626        {
627            // go faster by incrementing step exponent
628            currlayer->currexpo++;
629            SetGenIncrement();
630            if (generating && currlayer->currexpo <= 0) {
631                // reduce timer interval
632                [self stopGenTimer];
633                [self startGenTimer];
634            }
635        } break;
636    }
637
638    // only need to update status info
639    [statView setNeedsDisplay];
640}
641
642// -----------------------------------------------------------------------------
643
644- (IBAction)doChangeScale:(id)sender
645{
646    ClearMessage();
647    switch([sender selectedSegmentIndex])
648    {
649        case 0:
650        {
651            // zoom out
652            currlayer->view->unzoom();
653            [statView setNeedsDisplay];
654            [pattView refreshPattern];
655        } break;
656
657        case 1:
658        {
659            // set scale to 1:1
660            if (currlayer->view->getmag() != 0) {
661                currlayer->view->setmag(0);
662                [statView setNeedsDisplay];
663                [pattView refreshPattern];
664            }
665        } break;
666
667        case 2:
668        {
669            // zoom in
670            if (currlayer->view->getmag() < MAX_MAG) {
671                currlayer->view->zoom();
672                [statView setNeedsDisplay];
673                [pattView refreshPattern];
674            } else {
675                Beep();
676            }
677        } break;
678    }
679}
680
681// -----------------------------------------------------------------------------
682
683- (IBAction)doChangeMode:(id)sender
684{
685    ClearMessage();
686    switch([sender selectedSegmentIndex])
687    {
688        case 0: currlayer->touchmode = drawmode; break;
689        case 1: currlayer->touchmode = pickmode; break;
690        case 2: currlayer->touchmode = selectmode; break;
691        case 3: currlayer->touchmode = movemode; break;
692    }
693}
694
695// -----------------------------------------------------------------------------
696
697- (IBAction)doMiddle:(id)sender
698{
699    ClearMessage();
700    if (currlayer->originx == bigint::zero && currlayer->originy == bigint::zero) {
701        currlayer->view->center();
702    } else {
703        // put cell saved by ChangeOrigin (not yet implemented!!!) in middle
704        currlayer->view->setpositionmag(currlayer->originx, currlayer->originy,
705                                        currlayer->view->getmag());
706    }
707    [statView setNeedsDisplay];
708    [pattView refreshPattern];
709}
710
711// -----------------------------------------------------------------------------
712
713- (IBAction)doSelectAll:(id)sender
714{
715    ClearMessage();
716    SelectAll();
717}
718
719// -----------------------------------------------------------------------------
720
721- (IBAction)doAction:(id)sender
722{
723    ClearMessage();
724    [pattView doSelectionAction];
725}
726
727// -----------------------------------------------------------------------------
728
729- (IBAction)doPaste:(id)sender
730{
731    if (drawingcells) return;
732
733    [self stopIfGenerating];
734
735    if (event_checker > 0) {
736        // try again after a short delay
737        [self performSelector:@selector(doPaste:) withObject:sender afterDelay:0.01];
738        return;
739    }
740
741    ClearMessage();
742    if (waitingforpaste) {
743        [pattView doPasteAction];
744    } else {
745        PasteClipboard();
746        [pattView refreshPattern];
747    }
748}
749
750// -----------------------------------------------------------------------------
751
752- (IBAction)doRule:(id)sender
753{
754    if (drawingcells) return;
755
756    [self stopIfGenerating];
757
758    if (event_checker > 0) {
759        // try again after a short delay
760        [self performSelector:@selector(doRule:) withObject:sender afterDelay:0.01];
761        return;
762    }
763
764    ClearMessage();
765
766    // let user change current algo/rule in a modal view
767    RuleViewController *modalRuleController = [[RuleViewController alloc] initWithNibName:nil bundle:nil];
768
769    [modalRuleController setModalPresentationStyle:UIModalPresentationFullScreen];
770    [self presentViewController:modalRuleController animated:YES completion:nil];
771
772    modalRuleController = nil;
773}
774
775// -----------------------------------------------------------------------------
776
777- (IBAction)toggleFullScreen:(id)sender
778{
779    if (fullscreen) {
780        ShowTabBar(YES);
781        topBar.hidden = NO;
782        editBar.hidden = NO;
783        bottomBar.hidden = NO;
784        statView.hidden = NO;
785        // recalculate pattView's frame from scratch (in case device was rotated)
786        CGFloat barht = topBar.bounds.size.height;
787        CGFloat topht = barht * 2 + statView.bounds.size.height;
788        CGFloat x = pattView.superview.frame.origin.x;
789        CGFloat y = pattView.superview.frame.origin.y + topht;
790        CGFloat wd = pattView.superview.frame.size.width;
791        CGFloat ht = pattView.superview.frame.size.height - (topht + barht + TabBarHeight());
792        pattView.frame = CGRectMake(x, y, wd, ht);
793        [statView setNeedsDisplay];
794    } else {
795        ShowTabBar(NO);
796        topBar.hidden = YES;
797        editBar.hidden = YES;
798        bottomBar.hidden = YES;
799        statView.hidden = YES;
800        pattView.frame = pattView.superview.frame;
801    }
802
803    fullscreen = !fullscreen;
804    restoreButton.hidden = !fullscreen;
805    [pattView refreshPattern];
806    UpdateEditBar();
807}
808
809// -----------------------------------------------------------------------------
810
811- (IBAction)doCancel:(id)sender
812{
813    cancelProgress = true;
814}
815
816// -----------------------------------------------------------------------------
817
818- (void)enableInteraction:(BOOL)enable
819{
820    topBar.userInteractionEnabled = enable;
821    editBar.userInteractionEnabled = enable;
822    bottomBar.userInteractionEnabled = enable;
823    pattView.userInteractionEnabled = enable;
824    EnableTabBar(enable);
825}
826
827// -----------------------------------------------------------------------------
828
829- (void)updateProgressBar:(float)fraction
830{
831    progressBar.progress = fraction;
832
833    // show estimate of time remaining
834    double elapsecs = TimeInSeconds() - progstart;
835    double remsecs = fraction > 0.0 ? (elapsecs / fraction) - elapsecs : 999.0;
836    [progressMessage setText:[NSString stringWithFormat:@"Estimated time remaining (in secs): %d", int(remsecs+0.5)]];
837
838    // need following to see progress view and bar update
839    event_checker++;
840    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
841    event_checker--;
842}
843
844@end
845
846// =============================================================================
847
848// global routines used by external code:
849
850void UpdatePattern()
851{
852    [globalPatternView refreshPattern];
853}
854
855// -----------------------------------------------------------------------------
856
857void UpdateStatus()
858{
859    if (inscript || currlayer->undoredo->doingscriptchanges) return;
860    if (!fullscreen) {
861        [globalStatusView setNeedsDisplay];
862    }
863}
864
865// -----------------------------------------------------------------------------
866
867void UpdateEditBar()
868{
869    if (!fullscreen) {
870        [globalController updateButtons];
871        [globalController updateDrawingState];
872    }
873}
874
875// -----------------------------------------------------------------------------
876
877void CloseStatePicker()
878{
879    [globalStateView dismissStatePopover];
880}
881
882// -----------------------------------------------------------------------------
883
884void PauseGenTimer()
885{
886    if (generating) {
887        // use pausecount to handle nested calls
888        if (pausecount == 0) [globalController stopGenTimer];
889        pausecount++;
890    }
891}
892
893// -----------------------------------------------------------------------------
894
895void RestartGenTimer()
896{
897    if (pausecount > 0) {
898        pausecount--;
899        if (pausecount == 0) [globalController startGenTimer];
900    }
901}
902
903// -----------------------------------------------------------------------------
904
905static bool wasgenerating = false;      // generating needs to be resumed?
906
907void PauseGenerating()
908{
909    if (generating) {
910        // stop generating without changing STOP button
911        [globalController stopGenTimer];
912        StopGenerating();
913        // generating now false
914        wasgenerating = true;
915    }
916}
917
918// -----------------------------------------------------------------------------
919
920void ResumeGenerating()
921{
922    if (wasgenerating) {
923        // generating should be false but play safe
924        if (!generating) {
925            if (StartGenerating()) {
926                // generating now true
927                [globalController startGenTimer];
928            } else {
929                // this can happen if pattern is empty
930                [globalController toggleStartStopButton];
931                [globalController updateButtons];
932            }
933        }
934        wasgenerating = false;
935    }
936}
937
938// -----------------------------------------------------------------------------
939
940void StopIfGenerating()
941{
942    [globalController stopIfGenerating];
943}
944
945// -----------------------------------------------------------------------------
946
947void BeginProgress(const char* title)
948{
949    if (progresscount == 0) {
950        // disable interaction with all views but don't show progress view just yet
951        [globalController enableInteraction:NO];
952        [globalTitle setText:[NSString stringWithCString:title encoding:NSUTF8StringEncoding]];
953        cancelProgress = false;
954        progstart = TimeInSeconds();
955    }
956    progresscount++;    // handles nested calls
957}
958
959// -----------------------------------------------------------------------------
960
961bool AbortProgress(double fraction_done, const char* message)
962{
963    if (progresscount <= 0) Fatal("Bug detected in AbortProgress!");
964    double secs = TimeInSeconds() - progstart;
965    if (!globalProgress.hidden) {
966        if (secs < prognext) return false;
967        prognext = secs + 0.1;     // update progress bar about 10 times per sec
968        if (fraction_done < 0.0) {
969            // show indeterminate progress gauge???
970        } else {
971            [globalController updateProgressBar:fraction_done];
972        }
973        return cancelProgress;
974    } else {
975        // note that fraction_done is not always an accurate estimator for how long
976        // the task will take, especially when we use nextcell for cut/copy
977        if ( (secs > 1.0 && fraction_done < 0.3) || secs > 2.0 ) {
978            // task is probably going to take a while so show progress view
979            globalProgress.hidden = NO;
980            [globalController updateProgressBar:fraction_done];
981        }
982        prognext = secs + 0.01;     // short delay until 1st progress update
983    }
984    return false;
985}
986
987// -----------------------------------------------------------------------------
988
989void EndProgress()
990{
991    if (progresscount <= 0) Fatal("Bug detected in EndProgress!");
992    progresscount--;
993    if (progresscount == 0) {
994        // hide the progress view and enable interaction with other views
995        globalProgress.hidden = YES;
996        [globalController enableInteraction:YES];
997    }
998}
999