-
Notifications
You must be signed in to change notification settings - Fork 31
/
AnimatedGifView.m
2183 lines (1918 loc) · 89.2 KB
/
AnimatedGifView.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// AnimatedGifView.m
// AnimatedGif
//
// Created by Marco Köhler on 09.11.15.
// Copyright (c) 2015 Marco Köhler. All rights reserved.
//
#import "AnimatedGifView.h"
@implementation AnimatedGifView
- (instancetype)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview
{
mtlView = nil;
glView = nil;
trigByTimer = FALSE;
lastDuration = 0;
animationImages = nil;
currFrameCount = FRAME_COUNT_NOT_USED;
arrayLock = [[NSLock alloc] init];
self = [super initWithFrame:frame isPreview:isPreview];
// workaround for Sonoma
if ([self isPreview] == FALSE)
{
[[NSDistributedNotificationCenter defaultCenter]
addObserver:self
selector:@selector(sonomaQuitWorkaround)
name:@"com.apple.screensaver.willstop"
object:nil];
}
// initialize screensaver defaults with an default value
ScreenSaverDefaults *defaults = [ScreenSaverDefaults defaultsForModuleWithName:[[NSBundle bundleForClass: [self class]] bundleIdentifier]];
[defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
@"file:///please/select/an/gif/animation.gif", @"GifFileName", @"30.0", @"GifFrameRate", @"NO", @"GifFrameRateManual", @"0", @"ViewOpt", @"4", @"ScaleOpt", @"1", @"FilterOpt", @"0", @"TileOpt", @"0.0", @"BackgrRed", @"0.0", @"BackgrGreen", @"0.0", @"BackgrBlue", @"YES", @"LoadAniToMem", @"3", @"ChangeInterval",nil]];
if (self) {
mtlView = [self createViewMTL];
if (mtlView==nil)
{
NSLog(@"Since Metal setup was not possible try to use OpenGL setup.");
glView = [self createViewGL]; // only use OpenGL in case there is no Metal
if (glView==nil)
{
NSLog(@"OpenGL setup was not possible.");
}
else
{
NSLog(@"OpenGL setup done.");
}
}
// this is just an high dummy value for animateOneFrame (not longer used)
[self setAnimationTimeInterval:600];
}
// get the program arguments of the process
NSArray *args = [[NSProcessInfo processInfo] arguments];
// check if process was startet with argument -window for window mode of screensaver and is the first instance (has only two arguments, program name and the -"window")
if ((args.count==2) && ([args[1] isEqualToString:@"-window"]))
{
// Workaround: disable clock before start, since this leads to a crash with option "-window" of ScreenSaverEngine
NSString *cmdstr = [[NSString alloc] initWithFormat:@"%@", @"defaults -currentHost write com.apple.screensaver showClock -bool NO"];
system([cmdstr cStringUsingEncoding:NSUTF8StringEncoding]);
NSString *pathToScreenSaverEngine = @"/System/Library/Frameworks/ScreenSaver.framework/Resources/ScreenSaverEngine.app/Contents/MacOS/ScreenSaverEngine";
NSOperatingSystemVersion osVer = [[NSProcessInfo processInfo] operatingSystemVersion];
if ((osVer.majorVersion > 10) || ((osVer.minorVersion > 12) && (osVer.majorVersion == 10)))
{
pathToScreenSaverEngine = @"/System/Library/CoreServices/ScreenSaverEngine.app/Contents/MacOS/ScreenSaverEngine";
}
// This is a hell of a heck for a workaround. Since "-window" only starts one instance of ScreenSaverView, we start here additional instances of the ScreenSaverEngine itself by system call.
NSUInteger countScreens = [NSScreen screens].count;
if (countScreens > 1)
{
// additional instances needs start counting with "1" here(!) otherwise other part of code will be broken
for(NSUInteger scr=1;scr<countScreens;scr++)
{
// we start additional instances of ScreenSaverEngine, but with an extra number as argument that is invalid for the parser of the ScreenSaverEngine(see Log) but that wee use to differenceate beween the instances afterwards.
NSString *cmdstr2 = [[NSString alloc] initWithFormat:@"%@ %@ %ld &", pathToScreenSaverEngine, @"-window", scr];
system([cmdstr2 cStringUsingEncoding:NSUTF8StringEncoding]);
}
}
}
return self;
}
- (void) sonomaQuitWorkaround
{
// workaround for Sonoma:
// quit legacyScreenSaver when screensaver is stoped instead of keeping instance running in background as it is the case in Sonoma
// problem with this running background legacyScreenSaver is that it keeps using old vales of ScreenSaverDefaults and ignores new options the user has done in the meantime.
exit(0);
}
- (NSOpenGLView *)createViewGL
{
NSOpenGLPixelFormatAttribute attribs[] = {
NSOpenGLPFADoubleBuffer, NSOpenGLPFAAccelerated,
0
};
NSOpenGLPixelFormat* format = [[NSOpenGLPixelFormat alloc] initWithAttributes:attribs];
NSOpenGLView* glview = [[NSOpenGLView alloc] initWithFrame:NSZeroRect pixelFormat:format];
GLint swapInterval = SYNC_TO_VERTICAL;
[[glview openGLContext] setValues:&swapInterval forParameter: NSOpenGLCPSwapInterval];
return glview;
}
- (MTKView *) createViewMTL
{
// Does the user have any working Metal API(start with OS X 10.11 or later)?
if (MTLCopyAllDevices == NULL)
{
NSLog(@"Your version of the OS does not support Metal. Requires OS X 10.11 or later.");
return nil;
}
else
{
NSArray *devices = MTLCopyAllDevices();
// Does the user have any Metal devices available? (This should be yes on all Macs made after mid-2012.)
if (!devices || devices.count == 0)
{
NSLog(@"No Metal device could be found.");
return nil;
}
else
{
NSLog(@"Metal devices could be found.");
// the easy way is just using the system default metal device
deviceMTL = MTLCreateSystemDefaultDevice();
// create an Metal View that uses the metal device
MTKView* mtlView = [[MTKView alloc] initWithFrame:NSZeroRect];
mtlView.device = deviceMTL;
mtlView.clearColor = MTLClearColorMake(0, 0, 0, 1);
mtlView.colorPixelFormat = MTLPixelFormatBGRA8Unorm;
mtlView.framebufferOnly = NO;
mtlView.autoResizeDrawable = NO;
// here the presentaion framerate of metal is setup. it should be equal or higher as the framerate of the gif
mtlView.preferredFramesPerSecond = MAX_FRAME_RATE;
// get an metal command queue
commandQueueMTL = [deviceMTL newCommandQueue];
// load the metal libary from the bundle (contains the shader code for the GPU)
NSError *err = nil;
defaultLibraryMTL = [deviceMTL newLibraryWithFile:[[NSBundle bundleForClass:self.class] pathForResource:@"default" ofType:@"metallib"] error:&err];
// create an piple descriptor (defines porperties of an metal pipeline ) for creating an metal pipeline with it
pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineStateDescriptor.label = @"AnimatedGifPipeline";
// also add the shader codes that we load from the resource bundle to the metal pipeline
pipelineStateDescriptor.vertexFunction = [defaultLibraryMTL newFunctionWithName:@"myVertexShader"];
pipelineStateDescriptor.fragmentFunction = [defaultLibraryMTL newFunctionWithName:@"myFragmentShader"];
// setup parameters for alpha blending, but not enable it here (this is done in drawImageMTL)
pipelineStateDescriptor.colorAttachments[0].blendingEnabled = NO;
pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne;
pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
// define the pixel format we will use with metal
pipelineStateDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
// with that descriptor informations we can finaly create the metal pipeline
pipelineStateMTL = [deviceMTL newRenderPipelineStateWithDescriptor:pipelineStateDescriptor error:&err];
NSLog(@"Metal setup done.");
return mtlView;
}
}
}
- (void) drawRect:(NSRect)rect
{
// not needed since we use timerAnimateOneFrame and left empty so that super method of NSView is not called to save same CPU time
}
- (void)setFrameSize:(NSSize)newSize
{
[super setFrameSize:newSize];
if (mtlView == nil)
{
[glView setFrameSize:newSize];
}
else
{
[mtlView setFrameSize:newSize];
}
}
- (BOOL)isOpaque
{
// this keeps Cocoa from unnecessarily redrawing our superview
return YES;
}
- (void)dealloc
{
if (mtlView == nil)
{
[glView removeFromSuperview];
glView = nil;
}
else
{
[mtlView removeFromSuperview];
mtlView = nil;
}
}
- (void)timerMethod
{
// after change timer is running out this method is called
// the animation of last GIF is stopped an memory cleaned, but without destroying GL view or telling the screensaver engine about it (no call of super method; handled by trigByTimer=TRUE)
trigByTimer = TRUE;
[self stopAnimation];
// the animation is start again witch randomly pics a new GIF from folder and start the change timer again, but without telling the screensaver engine about it (no call of super method; handled by trigByTimer=TRUE)
[self startAnimation];
trigByTimer = FALSE;
}
- (void)startAnimation
{
// get the program arguments of the process
NSArray *args = [[NSProcessInfo processInfo] arguments];
if (trigByTimer == FALSE)
{
// only call super method in case startAnimation is not called by timerMethod
[super startAnimation];
if (mtlView == nil)
{
[self addSubview:glView];
}
else
{
[self addSubview:mtlView];
}
// bug of OSX: since 10.13 the background mode of screensaver is brocken (the ScreenSaverEngine uses for background-mode its own space that is in foreground and this space can't be accessed from the ScreenSaverView)
// workaround: AnimatedGif use the window-mode of the ScreenSaverEngine and change the behavior of that window to an background window
if ([self isPreview] == FALSE)
{
// check if process was startet with argument -window for window mode of screensaver
if ((args.count>=2) && ([args[1] isEqualToString:@"-window"]))
{
int scrNum = 0; // this "0" is for the first instance, that hast no number in "args[2]"
// only the additional instances have 3 arguments and a number in "args[2]" that starts with "1".
// unfortunly we can not start the main instance with an 3 agrument of "0" since this breaks the start with launchd, because for ScreenSaverEngine this argument is invalid.
if (args.count>2)
{
scrNum = [args[2] intValue];
}
// get the one the multiple screens
NSScreen *theScreen = [NSScreen screens][scrNum];
// now we move the window to background level and maximize it as we need it
[self.window setFrame:[theScreen frame] display:TRUE];
[super setFrame:[theScreen frame]];
[super setFrameOrigin:NSZeroPoint];
[self.window setStyleMask:NSFullSizeContentViewWindowMask];
[self.window setCollectionBehavior: NSWindowCollectionBehaviorStationary|NSWindowCollectionBehaviorCanJoinAllSpaces];
[self.window setLevel:kCGDesktopWindowLevel];
[self.window setFrame:[theScreen frame] display:TRUE];
}
}
if ([self isPreview] == FALSE)
{
// check if process was startet with argument -window for window mode of screensaver
if ((args.count>=2) && ([args[1] isEqualToString:@"-window"]))
{
// hide window since next steps need some time and look ugly
[self.window orderOut:self];
}
}
}
// get filename from screensaver defaults
ScreenSaverDefaults *defaults = [ScreenSaverDefaults defaultsForModuleWithName:[[NSBundle bundleForClass: [self class]] bundleIdentifier]];
NSString *gifFileName = [defaults objectForKey:@"GifFileName"];
manualFrameRate = [defaults floatForKey:@"GifFrameRate"];
isManualFrameRate = [defaults boolForKey:@"GifFrameRateManual"];
loadAnimationToMem = [defaults boolForKey:@"LoadAniToMem"];
viewOption = [defaults integerForKey:@"ViewOpt"];
NSInteger scaleOption = [defaults integerForKey:@"ScaleOpt"];
NSInteger filterOption = [defaults integerForKey:@"FilterOpt"];
NSInteger tileOption = [defaults integerForKey:@"TileOpt"];
backgrRed = [defaults floatForKey:@"BackgrRed"];
backgrGreen = [defaults floatForKey:@"BackgrGreen"];
backgrBlue = [defaults floatForKey:@"BackgrBlue"];
NSInteger changeIntervalOption = [defaults integerForKey:@"ChangeInterval"];
// In case if preview window never use the 'load into memory' feature since it initially needs much CPU time witch is bad inside the system preferences app
if ([self isPreview])
{
loadAnimationToMem = FALSE;
}
// select a random file from directory or keep the file if it was already a file
NSString *selectedGifFileName = [self getRandomGifFile:gifFileName];
// load GIF image
BOOL isFileLoaded = [self loadGifFromFile:selectedGifFileName];
if (isFileLoaded)
{
currFrameCount = FIRST_FRAME;
}
else
{
currFrameCount = FRAME_COUNT_NOT_USED;
// stop an old timer if there is one
if (animateTimer != nil) {
[animateTimer invalidate];
}
// start a repeated timer that triggers timerAnimateOneFrame to print an indication message
animateTimer = [NSTimer scheduledTimerWithTimeInterval:0.05
target:self
selector:@selector(timerAnimateOneFrame)
userInfo:nil
repeats:YES];
}
// calculate target and screen rectangle size
screenRect = [self bounds];
targetRect = [self calcTargetRectFromOption:viewOption];
filter = filterOption;
tiles = tileOption;
if (viewOption==VIEW_OPT_SCALE_SIZE)
{
scale = [self calcScaleFromScaleOption:scaleOption];
}
else
{
scale = 1.0;
}
// get change interval in seconds from change interval option
NSInteger changeIntervalInSec = [self getIntervalFromOption:changeIntervalOption];
// check if it is a file or a directory
if (isFileLoaded && [self isDir:gifFileName] && ((changeIntervalOption) != CHANGE_INT_NEVER))
{
// start a one-time timer at end of startAnimation otherwise the time for loading the GIF is part of the timer
changeTimer = [NSTimer scheduledTimerWithTimeInterval:(changeIntervalInSec)
target:self
selector:@selector(timerMethod)
userInfo:nil
repeats:NO];
}
if (trigByTimer == FALSE)
{
if ([self isPreview] == FALSE)
{
// check if process was startet with argument -window for window mode of screensaver
if ((args.count>=2) && ([args[1] isEqualToString:@"-window"]))
{
// unhide window
[self.window orderBack:self];
}
}
}
// Register for notification something with the display has changed
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(receiveDisplaysChangeNote:)
name: NSApplicationDidChangeScreenParametersNotification
object: nil];
}
- (void) receiveDisplaysChangeNote: (NSNotification*) note
{
// Event is fired after a change of displays
// get the program arguments of the process
NSArray *args = [[NSProcessInfo processInfo] arguments];
// only terminate instances startet with "-window"
if ((args.count>=2) && ([args[1] isEqualToString:@"-window"]))
{
// this ScreenSaverEngine instance terminates itself
[NSApp terminate:self];
}
}
- (void)stopAnimation
{
if (trigByTimer == FALSE)
{
// only call super method in case stopAnimation is not called by timerMethod
[super stopAnimation];
// stop change timer with end of animation (otherwise in the screensaver preview will running multiple timer each time congigure panel is closed)
if (changeTimer != nil) {
[changeTimer invalidate];
}
// stop an old timer if there is one
if (animateTimer != nil) {
[animateTimer invalidate];
}
lastDuration = 0;
}
if (loadAnimationToMem == TRUE)
{
/*clean all pre-calculated bitmap images*/
[arrayLock lock]; // NSMutableArray isn't thread-safe
[animationImages removeAllObjects];
animationImages = nil;
[arrayLock unlock];
}
[animationDurations removeAllObjects];
animationDurations = nil;
img = nil;
currFrameCount = FRAME_COUNT_NOT_USED;
}
- (void)animateOneFrame
{
/*
not longer used since animationTimeInterval can not be changed during animation, but GIFs have this feature. As new way timerAnimateOneFrame is used that is triggert by animateTimer.
*/
}
- (void)timerAnimateOneFrame
{
@autoreleasepool {
[self lockFocus];
if (currFrameCount == FRAME_COUNT_NOT_USED)
{
// FRAME_COUNT_NOT_USED means no image is loaded and so we clear the screen with the set background color and print an indication message
if (mtlView == nil)
{
[self animateNoGifGL];
}
else
{
[self animateNoGifMTL];
}
}
else
{
// draw the selected frame
if (mtlView == nil)
{
[self animateWithGifGL];
}
else
{
[self animateWithGifMTL];
}
//calculate next frame of GIF to show
if (currFrameCount < maxFrameCount-1)
{
currFrameCount++;
}
else
{
currFrameCount = FIRST_FRAME;
}
[self setAnimationIntervalAtFrame:currFrameCount];
}
[self unlockFocus];
return;
}
}
-(IBAction)aboutClick:(id)sender {
if (!_aboutWindowController)
{
// load about window from nib
[[NSBundle bundleForClass:[self class]] loadNibNamed:@"About" owner:self topLevelObjects:nil];
// prepare about window with content
NSString *version = [[NSBundle bundleForClass:[self class]] objectForInfoDictionaryKey: @"CFBundleShortVersionString"];
[self.labelVersion2 setStringValue:version];
[self.textLicence setEditable:NO];
NSError *err = nil;
NSString *path =[[NSBundle bundleForClass:[self class]] pathForResource:@"LICENSE" ofType:@"md"];
NSString *contents = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&err];
if (contents)
{
NSString *osxMode = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"];
NSMutableAttributedString *attrString;
if ([osxMode isEqualToString:@"Dark"])
{
NSDictionary *attributes = @{ NSForegroundColorAttributeName : [NSColor lightGrayColor]};
attrString = [[NSMutableAttributedString alloc] initWithString:contents attributes:attributes];
}
else
{
attrString = [[NSMutableAttributedString alloc] initWithString:contents];
}
[[self.textLicence textStorage] appendAttributedString:attrString];
}
// Create the modal controller for about window
_aboutWindowController = [[NSWindowController alloc] initWithWindow:self.aboutWindow];
// Show window
[_aboutWindowController showWindow:self];
// unfortunately the modal not longer works since 10.14 and crashes the whole app
//[NSApp runModalForWindow:self.aboutWindow];
}
else
{
// make it visible again
if (self.aboutWindow.visible == NO)
{
[_aboutWindowController showWindow:self];
}
else
{
[_aboutWindowController close];
[_aboutWindowController showWindow:self];
}
}
}
- (BOOL)hasConfigureSheet
{
// tell ScreenSaverEngine that screensaver has an Options dialog
return YES;
}
- (NSWindow*)configureSheet
{
// Load XIB File that contains the Options dialog
[[NSBundle bundleForClass:[self class]] loadNibNamed:@"Options" owner:self topLevelObjects:nil];
// get filename from screensaver defaults
ScreenSaverDefaults *defaults = [ScreenSaverDefaults defaultsForModuleWithName:[[NSBundle bundleForClass: [self class]] bundleIdentifier]];
NSString *gifFileName = [defaults objectForKey:@"GifFileName"];
float frameRate = [defaults floatForKey:@"GifFrameRate"];
BOOL frameRateManual = [defaults boolForKey:@"GifFrameRateManual"];
BOOL loadAniToMem = [defaults boolForKey:@"LoadAniToMem"];
float bgrRed = [defaults floatForKey:@"BackgrRed"];
float bgrGreen = [defaults floatForKey:@"BackgrGreen"];
float bgrBlue = [defaults floatForKey:@"BackgrBlue"];
NSInteger viewOpt = [defaults integerForKey:@"ViewOpt"];
NSInteger scaleOpt = [defaults integerForKey:@"ScaleOpt"];
NSInteger filterOpt = [defaults integerForKey:@"FilterOpt"];
NSInteger tileOpt = [defaults integerForKey:@"TileOpt"];
NSInteger changeInter = [defaults integerForKey:@"ChangeInterval"];
// in the rarely case of an invalid value from default file we set an valid option
if (viewOpt > MAX_VIEW_OPT)
{
viewOpt = VIEW_OPT_STRETCH_OPTIMAL;
}
// in the rarely case of an invalid value from default file we set an valid option
if (scaleOpt > MAX_SCALE_OPT)
{
scaleOpt = SCALE_OPT_1;
}
// in the rarely case of an invalid value from default file we set an valid option
if (filterOpt > MAX_FILTER_OPT)
{
filterOpt = FILTER_OPT_SHARP;
}
// in the rarely case of an invalid value from default file we set an valid option
if (tileOpt > MAX_TILE_OPT)
{
tileOpt = TILE_OPT_1;
}
if (viewOpt == VIEW_OPT_SCALE_SIZE)
{
[self.popupButtonScaleOptions setEnabled:YES];
}
else
{
[self.popupButtonScaleOptions setEnabled:NO];
}
if ([self isDir:gifFileName])
{
// if we have an directory an fps value for a file makes not much sense
// we could calculate it for an randomly selected file but this would make thinks to complex
[self.labelFpsGif setStringValue:NSLocalizedStringFromTableInBundle(@"dir",@"Localizable",[NSBundle bundleForClass:[self class]],nil)];
[self hideFpsFromFile:YES];
// enable time interval slider only in case that an directory is selected
[self enableChangeInterval:YES];
}
else
{
// set file fps in GUI
NSTimeInterval duration = [self getDurationFromFile:gifFileName atFrame:FIRST_FRAME];
float fps = 1/duration;
[self.labelFpsGif setStringValue:[NSString stringWithFormat:@"%2.1f", fps]];
[self hideFpsFromFile:NO];
// disable time interval slider in case an file is selected
[self enableChangeInterval:NO];
}
if (mtlView==nil)
{
if (glView==nil)
{
[self.labelRender setStringValue:NSLocalizedStringFromTableInBundle(@"renderNO", @"Localizable",[NSBundle bundleForClass:[self class]], nil)];
}
else
{
[self.labelRender setStringValue:NSLocalizedStringFromTableInBundle(@"renderGL", @"Localizable",[NSBundle bundleForClass:[self class]], nil)];
}
}
else
{
[self.labelRender setStringValue:NSLocalizedStringFromTableInBundle(@"renderMTL", @"Localizable",[NSBundle bundleForClass:[self class]], nil)];
}
[self.labelRender setTextColor:[NSColor lightGrayColor]];
// set the visible value in dialog to the last saved value
NSString *version = [[NSBundle bundleForClass:[self class]] objectForInfoDictionaryKey: @"CFBundleShortVersionString"];
[self.labelVersion setStringValue:version];
[self.textFieldFileUrl setStringValue:gifFileName];
[self.sliderFpsManual setDoubleValue:frameRate];
if (tileOpt == TILE_OPT_1)
{
[self.checkButtonTileOptions setState:NO];
}
else
{
[self.checkButtonTileOptions setState:YES];
}
[self.checkButtonSetFpsManual setState:frameRateManual];
[self.checkButtonLoadIntoMem setState:loadAniToMem];
[self.popupButtonViewOptions selectItemWithTag:viewOpt];
[self.popupButtonScaleOptions selectItemWithTag:scaleOpt];
[self.popupButtonFilterOptions selectItemWithTag:filterOpt];
[self.popupIntervalOptions selectItemWithTag:changeInter];
[self enableSliderFpsManual:frameRateManual];
[self.labelFpsManual setStringValue:[self.sliderFpsManual stringValue]];
[self.colorWellBackgrColor setColor:[NSColor colorWithRed:bgrRed green:bgrGreen blue:bgrBlue alpha:NS_ALPHA_OPAQUE]];
NSOperatingSystemVersion osVer = [[NSProcessInfo processInfo] operatingSystemVersion];
if ((osVer.majorVersion > 10) || ((osVer.minorVersion > 14) && (osVer.majorVersion == 10)))
{
// disable segment button for Catalina, Big Sur or later OS since Sandbox prohibits this feature
self.segmentButtonLaunchAgent.enabled = false;
self.segmentButtonLaunchAgent.toolTip = @"Not longer supported";
}
else
{
// still allow this feature for Mojave or lower OS
// set segment button depending if the launch-agent is active or not
NSString *userLaunchAgentsPath = [[NSString alloc] initWithFormat:@"%@%@%@", @"/Users/", NSUserName(), @"/Library/LaunchAgents/com.waitsnake.animatedgif.plist"];
BOOL launchAgentFileExists = [[NSFileManager defaultManager] fileExistsAtPath:userLaunchAgentsPath];
if (launchAgentFileExists == YES)
{
self.segmentButtonLaunchAgent.selectedSegment = LOAD_BTN;
}
else
{
self.segmentButtonLaunchAgent.selectedSegment = UNLOAD_BTN;
}
}
// return the new created options dialog
return self.optionsPanel;
}
- (IBAction)changeViewOption:(id)sender {
NSPopUpButton *control = (NSPopUpButton *)sender;
NSInteger viewOption = [control selectedTag];
if (viewOption == VIEW_OPT_SCALE_SIZE)
{
[self.popupButtonScaleOptions setEnabled:YES];
}
else
{
[self.popupButtonScaleOptions setEnabled:NO];
}
}
- (IBAction)navigateSegmentButton:(id)sender
{
// check witch segment of segment button was pressed and than start the according method
NSSegmentedControl *control = (NSSegmentedControl *)sender;
NSInteger selectedSeg = [control selectedSegment];
switch (selectedSeg) {
case LOAD_BTN:
[self loadAgent];
break;
case UNLOAD_BTN:
[self unloadAgent];
break;
default:
break;
}
}
- (IBAction)closeConfigOk:(id)sender
{
// read values from GUI elements
BOOL defaultsChanged = FALSE;
float frameRate = [self.sliderFpsManual floatValue];
NSString *gifFileName = [self.textFieldFileUrl stringValue];
BOOL frameRateManual = self.checkButtonSetFpsManual.state;
BOOL loadAniToMem = self.checkButtonLoadIntoMem.state;
NSInteger viewOpt = self.popupButtonViewOptions.selectedTag;
NSInteger scaleOpt = self.popupButtonScaleOptions.selectedTag;
NSInteger filterOpt = self.popupButtonFilterOptions.selectedTag;
NSColor *colorPicked = self.colorWellBackgrColor.color;
NSInteger changeInt = self.popupIntervalOptions.selectedTag;
NSInteger tileOpt = 0;
if (self.checkButtonTileOptions.state == NO)
{
tileOpt = TILE_OPT_1;
}
else
{
tileOpt = TILE_OPT_3_BY_3;
}
// init access to screensaver defaults
ScreenSaverDefaults *defaults = [ScreenSaverDefaults defaultsForModuleWithName:[[NSBundle bundleForClass: [self class]] bundleIdentifier]];
// check for changes in default values first
if ([gifFileName isEqualToString:[defaults objectForKey:@"GifFileName"]]==FALSE)
{
defaultsChanged = TRUE;
}
if (fabsf([defaults floatForKey:@"GifFrameRate"]-frameRate)>0.01)
{
defaultsChanged = TRUE;
}
if ([defaults boolForKey:@"GifFrameRateManual"] != frameRateManual)
{
defaultsChanged = TRUE;
}
if ([defaults boolForKey:@"LoadAniToMem"] != loadAniToMem)
{
defaultsChanged = TRUE;
}
if ([defaults integerForKey:@"ViewOpt"] != viewOpt)
{
defaultsChanged = TRUE;
}
if ([defaults integerForKey:@"ScaleOpt"] != scaleOpt)
{
defaultsChanged = TRUE;
}
if ([defaults integerForKey:@"FilterOpt"] != filterOpt)
{
defaultsChanged = TRUE;
}
if ([defaults integerForKey:@"TileOpt"] != tileOpt)
{
defaultsChanged = TRUE;
}
if ([defaults integerForKey:@"ChangeInterval"] != changeInt)
{
defaultsChanged = TRUE;
}
if (fabs([defaults floatForKey:@"BackgrRed"]-colorPicked.redComponent)>0.01)
{
defaultsChanged = TRUE;
}
if (fabs([defaults floatForKey:@"BackgrGreen"]-colorPicked.greenComponent)>0.01)
{
defaultsChanged = TRUE;
}
if (fabs([defaults floatForKey:@"BackgrBlue"]-colorPicked.blueComponent)>0.01)
{
defaultsChanged = TRUE;
}
// write new default values
[defaults setObject:gifFileName forKey:@"GifFileName"];
[defaults setFloat:frameRate forKey:@"GifFrameRate"];
[defaults setBool:frameRateManual forKey:@"GifFrameRateManual"];
[defaults setBool:loadAniToMem forKey:@"LoadAniToMem"];
[defaults setInteger:viewOpt forKey:@"ViewOpt"];
[defaults setInteger:scaleOpt forKey:@"ScaleOpt"];
[defaults setInteger:filterOpt forKey:@"FilterOpt"];
[defaults setInteger:tileOpt forKey:@"TileOpt"];
[defaults setFloat:colorPicked.redComponent forKey:@"BackgrRed"];
[defaults setFloat:colorPicked.greenComponent forKey:@"BackgrGreen"];
[defaults setFloat:colorPicked.blueComponent forKey:@"BackgrBlue"];
[defaults setInteger:changeInt forKey:@"ChangeInterval"];
[defaults synchronize];
// calculate target and screen rectangle size
screenRect = [self bounds];
targetRect = [self calcTargetRectFromOption:viewOpt];
// set new values to object attributes
backgrRed = colorPicked.redComponent;
backgrGreen = colorPicked.greenComponent;
backgrBlue = colorPicked.blueComponent;
viewOption = viewOpt;
filter = filterOpt;
tiles = tileOpt;
if (viewOption==VIEW_OPT_SCALE_SIZE)
{
scale = [self calcScaleFromScaleOption:scaleOpt];
}
else
{
scale = 1.0;
}
// close color dialog and options dialog
[[NSColorPanel sharedColorPanel] close];
[[NSApplication sharedApplication] endSheet:self.optionsPanel];
// check if any default value has changed and background mode is active
if ((defaultsChanged==TRUE) && (self.segmentButtonLaunchAgent.selectedSegment == LOAD_BTN))
{
// in this case stop and restart ScreenSaverEngine
[self unloadAgent];
[self loadAgent];
}
if (_aboutWindowController)
{
[_aboutWindowController close];
_aboutWindowController = nil;
}
if (openDlg)
{
[openDlg close];
openDlg = nil;
}
}
- (IBAction)closeConfigCancel:(id)sender
{
// close color dialog and options dialog
[[NSColorPanel sharedColorPanel] close];
[[NSApplication sharedApplication] endSheet:self.optionsPanel];
if (_aboutWindowController)
{
[_aboutWindowController close];
_aboutWindowController = nil;
}
if (openDlg)
{
[openDlg close];
openDlg = nil;
}
}
- (IBAction)pressCheckboxSetFpsManual:(id)sender
{
// enable or disable slider depending on checkbox
BOOL frameRateManual = self.checkButtonSetFpsManual.state;
if (frameRateManual)
{
[self enableSliderFpsManual:YES];
}
else
{
[self enableSliderFpsManual:NO];
}
}
- (IBAction)selectSliderFpsManual:(id)sender
{
// update label with actual selected value of slider
[self.labelFpsManual setStringValue:[self.sliderFpsManual stringValue]];
}
- (void)enableChangeInterval:(BOOL)enable
{
if (enable==TRUE)
{
[self.labelChIntT1 setTextColor:[NSColor blackColor]];
[self.popupIntervalOptions setEnabled:YES];
}
else
{
[self.labelChIntT1 setTextColor:[NSColor lightGrayColor]];
[self.popupIntervalOptions setEnabled:NO];
}
}
- (void)enableSliderFpsManual:(BOOL)enable
{
if (enable==TRUE)
{
[self.sliderFpsManual setEnabled:YES];
[self.labelFpsGif setTextColor:[NSColor blackColor]];
[self.labelFpsManual setTextColor:[NSColor blackColor]];
[self.labelFpsT1 setTextColor:[NSColor blackColor]];
[self.labelFpsT2 setTextColor:[NSColor blackColor]];
[self.labelFpsT3 setTextColor:[NSColor blackColor]];
[self.labelFpsT4 setTextColor:[NSColor blackColor]];
[self.labelFpsT5 setTextColor:[NSColor blackColor]];
[self.labelFpsT6 setTextColor:[NSColor blackColor]];
}
else
{
[self.sliderFpsManual setEnabled:NO];
[self.labelFpsGif setTextColor:[NSColor lightGrayColor]];
[self.labelFpsManual setTextColor:[NSColor lightGrayColor]];
[self.labelFpsT1 setTextColor:[NSColor lightGrayColor]];
[self.labelFpsT2 setTextColor:[NSColor lightGrayColor]];
[self.labelFpsT3 setTextColor:[NSColor lightGrayColor]];
[self.labelFpsT4 setTextColor:[NSColor lightGrayColor]];
[self.labelFpsT5 setTextColor:[NSColor lightGrayColor]];
[self.labelFpsT6 setTextColor:[NSColor lightGrayColor]];
}
}
- (void)hideFpsFromFile:(BOOL)hide
{
if (hide==TRUE)
{
[self.labelFpsGif setHidden:YES];
[self.labelFpsT2 setHidden:YES];
[self.labelFpsT3 setHidden:YES];
}
else
{
[self.labelFpsGif setHidden:NO];
[self.labelFpsT2 setHidden:NO];
[self.labelFpsT3 setHidden:NO];
}
}
- (IBAction)sendFileButtonAction:(id)sender
{
if (!openDlg)
{
// if there is no OpenWindow create a new one
openDlg = [OpenWindow openPanel];
// Since the open dialog can no longer opend modal since 10.14 at least we give it a title
[openDlg setMessage:NSLocalizedStringFromTableInBundle(@"titleopendlg", @"Localizable",[NSBundle bundleForClass:[self class]], nil)];
// Enable the selection of files in the dialog.
[openDlg setCanChooseFiles:YES];
// Enable the selection of directories in the dialog.
[openDlg setCanChooseDirectories:YES];
// Disable the selection of more than one file
[openDlg setAllowsMultipleSelection:NO];
// set dialog to one level above of last selected file/directory
if ([self isDir:[self.textFieldFileUrl stringValue]])
{
// in case of an directory remove one level of path before open it
[openDlg setDirectoryURL:[[NSURL URLWithString:[self.textFieldFileUrl stringValue]] URLByDeletingLastPathComponent]];
}
else
{
// in case of an file remove two level of path before open it
[openDlg setDirectoryURL:[[[NSURL URLWithString:[self.textFieldFileUrl stringValue]] URLByDeletingLastPathComponent] URLByDeletingLastPathComponent]];
}
// try to 'focus' only on GIF files (Yes, I know all image types are working with NSImage)
[openDlg setAllowedFileTypes:[[NSArray alloc] initWithObjects:@"gif", @"GIF", @"png", @"PNG", nil]];