-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathAppController.j
489 lines (414 loc) · 22.2 KB
/
AppController.j
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
/*
This file is part of FrACT10, a vision test battery.
Copyright © 2021 Michael Bach, [email protected], <https://michaelbach.de>
AppController.j
Created by mb on 2017-07-12.
*/
@import "HierarchyController.j"
@import "FractView.j"
@import "FractController.j"
@import "FractControllerAcuityC.j"
@import "FractControllerAcuityL.j"
@import "FractControllerAcuityE.j"
@import "FractControllerAcuityTAO.j"
@import "FractControllerAcuityVernier.j"
@import "FractControllerContrastLett.j"
@import "FractControllerContrastC.j"
@import "FractControllerContrastE.j"
@import "FractControllerContrastG.j"
@import "FractControllerContrastDitherUnittest.j"
@import "FractControllerAcuityLineByLine.j"
@import "RewardsController.j"
@import "TAOController.j"
@import "Sound.j"
@import "GammaView.j"
@import "MDBButton.j"
@import "MDBTextField.j"
@import "MDBLabel.j"
@import "Presets.j"
@import "ControlDispatcher.j"
@import "CardController.j"
@import "AboutAndHelpController.j"
@import "CheckingContrastController.j"
/**
AppController
The main controller. It inherits from HierarchyController
to make communication with some classes which do not inherit from AppController easier.
*/
@implementation AppController : HierarchyController {
@outlet CPWindow fractControllerWindow;
@outlet CPPanel settingsPanel, responseinfoPanelAcuityL, responseinfoPanelAcuity4C, responseinfoPanelAcuity8C, responseinfoPanelAcuityE, responseinfoPanelAcuityTAO, responseinfoPanelAcuityVernier, responseinfoPanelContrastLett, responseinfoPanelContrastC, responseinfoPanelContrastE, responseinfoPanelContrastG, responseinfoPanelAcuityLineByLine;
@outlet MDBButton buttonAcuityLett, buttonAcuityC, buttonAcuityE, buttonAcuityTAO, buttonAcuityVernier, buttCntLett, buttCntC, buttCntE, buttCntG, buttonAcuityLineByLine;
@outlet CPButton buttonExport;
@outlet CPButton radioButtonAcuityBW, radioButtonAcuityColor;
@outlet GammaView gammaView;
@outlet CPPopUpButton settingsPanePresetsPopUpButton; Presets presets;
@outlet CPPopUpButton settingsPaneMiscSoundsTrialYesPopUp;
@outlet CPPopUpButton settingsPaneMiscSoundsTrialNoPopUp;
@outlet CPPopUpButton settingsPaneMiscSoundsRunEndPopUp;
Sound sound;
CPImageView rewardImageView;
RewardsController rewardsController;
TAOController taoController;
FractController currentFractController;
BOOL settingsNeededNewDefaults;
BOOL runAborted @accessors;
BOOL has4orientations @accessors;
id allPanels, allTestControllers;
int settingsPaneTabViewSelectedIndex @accessors;
float calBarLengthInMMbefore;
CPColor colorOfBestPossibleAcuity @accessors;
CPNumberFormatter numberFormatter;
@outlet CPTextField contrastMaxLogCSWeberField;
@outlet CPTextField gammaValueField;
int decimalMarkCharIndexPrevious;
@outlet MDBTextField decimalMarkCharField;
}
/**
Accessing the foreground/background color for acuity optotypes as saved across restart in Settings.
Within FrACT use globals gColorFore/gColorBack; need to synchronise [Gratings have their own].
@return the current foreground color
Colors cannot be saved as objects in userdefaults, probably because serialiser not implemented
NSUnarchiveFromData, Error message [CPData encodeWithCoder:] unrecognized selector
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DrawColor/Tasks/StoringNSColorInDefaults.html
*/
- (CPColor) acuityForeColor {
gColorFore = [Settings acuityForeColor];
return gColorFore;}
- (void) setAcuityForeColor: (CPColor) col {
gColorFore = col;
[Settings setAcuityForeColor: gColorFore];
}
- (CPColor) acuityBackColor {
gColorBack = [Settings acuityBackColor];
return gColorBack;
}
- (void) setAcuityBackColor: (CPColor) col {
gColorBack = col;
[Settings setAcuityBackColor: gColorBack];
}
- (void) setGratingForeColor: (CPColor) col {[Settings setGratingForeColor: col];}
- (CPColor) gratingForeColor {return [Settings gratingForeColor];}
- (void) setGratingBackColor: (CPColor) col {[Settings setGratingBackColor: col];}
- (CPColor) gratingBackColor {return [Settings gratingBackColor];}
- (CPColor) windowBackgroundColor {return [Settings windowBackgroundColor];}
- (void) setWindowBackgroundColor: (CPColor) col { //console.info("AppController>setAcuityBackColor");
[Settings setWindowBackgroundColor: col]; [selfWindow setBackgroundColor: col];
}
/**
Our main initialisation begins here
*/
- (id) init { //console.info("AppController>init");
settingsNeededNewDefaults = [Settings needNewDefaults];
[Settings checkDefaults]; //important to do this very early, before nib loading, otherwise the updates don't populate the settings panel
return self;
}
#pragma mark
/** runs after "init" above */
- (void) applicationDidFinishLaunching: (CPNotification) aNotification { //console.info("AppController>…Launching");
'use strict';
gAppController = self; // so others can reference via global variable
currentFractController = null; // making sure, is used to check whether inRun
selfWindow = [self window];
[selfWindow setFullPlatformWindow: YES]; [selfWindow setBackgroundColor: [self windowBackgroundColor]];
[CPMenu setMenuBarVisible: NO];
window.addEventListener('error', function(e) {
alert("An error occured, I'm sorry. Error message:\r\r" + e.message + "\r\rIf it recurs, please notify [email protected], ideally relating the message, e.g. via a screeshot.\rI will look into it and endeavour to provide a fix ASAP.\r\rOn “Close”, the window will reload and you can retry.");
window.location.reload(NO);
});
window.addEventListener("orientationchange", function(e) {
if ([Settings mobileOrientation]) {
//alert("Orientation change, now "+e.target.screen.orientation.angle+"°.\r\rOn “Close”, the window will reload to fit.");
window.location.reload(NO);
}
});
window.addEventListener("fullscreenchange", (event) => { // called _after_ the change
//console.info("isFullScreen: ", [Misc isFullScreen]);
if (![Misc isFullScreen]) { // so it was full before, possibly we're in a run
if (currentFractController !== null) {//need to end run when leaving fullscreen
[currentFractController runEnd]; //because the <esc> was consumed
}
}
[Misc centerWindowOrPanel: [selfWindow contentView]];
});
window.addEventListener("resize", (event) => {
[Misc centerWindowOrPanel: [selfWindow contentView]];
});
const allTestButtons = [buttonAcuityLett, buttonAcuityC, buttonAcuityE, buttonAcuityTAO, buttonAcuityVernier, buttCntLett, buttCntC, buttCntE, buttCntG, buttonAcuityLineByLine];
for (const b of allTestButtons) [Misc makeFrameSquareFromWidth: b];
allTestControllers = [nil, FractControllerAcuityL, FractControllerAcuityC, FractControllerAcuityE, FractControllerAcuityTAO, FractControllerAcuityVernier, FractControllerContrastLett, FractControllerContrastC, FractControllerContrastE, FractControllerContrastG, FractControllerAcuityLineByLine, FractControllerContrastDitherUnittest]; // sequence like Hierachy kTest#s
allPanels = [responseinfoPanelAcuityL, responseinfoPanelAcuity4C, responseinfoPanelAcuity8C, responseinfoPanelAcuityE, responseinfoPanelAcuityTAO, responseinfoPanelAcuityVernier, responseinfoPanelContrastLett, responseinfoPanelContrastC, responseinfoPanelContrastE, responseinfoPanelContrastG, responseinfoPanelAcuityLineByLine, settingsPanel];
for (const p of allPanels) [p setMovable: NO];
[self setSettingsPaneTabViewSelectedIndex: 0]; // select the "General" tab in Settings
[selfWindow setTitle: "FrACT10"];
[self setVersionDateString: gVersionStringOfFract + "·" + gVersionDateOfFrACT];
[Settings checkDefaults]; // what was the reason to put this here???
rewardImageView = [[CPImageView alloc] initWithFrame: CGRectMake(100, 0, 600, 600)];
[[selfWindow contentView] addSubview: rewardImageView positioned: CPWindowBelow relativeTo: nil];
rewardsController = [[RewardsController alloc] initWithView: rewardImageView];
taoController = [[TAOController alloc] initWithButton2Enable: buttonAcuityTAO];
sound = [[Sound alloc] init];
presets = [[Presets alloc] initWithPopup: settingsPanePresetsPopUpButton];
for (let i = 0; i < (Math.round([[CPDate date] timeIntervalSince1970]) % 33); i++)
Math.random(); // randomising the pseudorandom sequence
[[CPNotificationCenter defaultCenter] addObserver: self selector: @selector(buttonExportEnableYESorNO:) name: "buttonExportEnableYESorNO" object: nil];
[self postNotificationName: "buttonExportEnableYESorNO" object: 0];
[[CPNotificationCenter defaultCenter] addObserver: self selector: @selector(copyColorsFromSettings:) name: "copyColorsFromSettings" object: nil];
[[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(settingsDidChange:) name:CPUserDefaultsDidChangeNotification object: nil];
[self radioButtonsAcuityBwOrColor_action: null];
[Settings setAutoRunIndex: kAutoRunIndexNone]; // make sure it's not accidentally on
numberFormatter = [[CPNumberFormatter alloc] init];
[numberFormatter setNumberStyle: CPNumberFormatterDecimalStyle];
[numberFormatter setMinimumFractionDigits: 1];
[contrastMaxLogCSWeberField setFormatter: numberFormatter];
[gammaValueField setFormatter: numberFormatter];
[Settings setupSoundPopups: [settingsPaneMiscSoundsTrialYesPopUp, settingsPaneMiscSoundsTrialNoPopUp, settingsPaneMiscSoundsRunEndPopUp]];
// set up control dispatcher (HTML messages to FrACT10 when embedded as iframe)
[[CPNotificationCenter defaultCenter] addObserver: self selector: @selector(notificationRunFractControllerTest:) name: "notificationRunFractControllerTest" object: nil];
[ControlDispatcher init];
[Misc centerWindowOrPanel: [selfWindow contentView]]; // →center
[selfWindow orderFront: self]; // ensures that it will receive clicks w/o activating
}
/**
Observe changes in the settings panel, making sure dependencies are updated
*/
- (void) settingsDidChange: (CPNotification) aNotification { //console.info("settingsDidChange");
[self setHas4orientations: ([Settings nAlternatives] == 4)];
[selfWindow setBackgroundColor: [self windowBackgroundColor]];
if ([Settings minPossibleLogMAR] > 0) { // red: not good enough for normal vision
[self setColorOfBestPossibleAcuity: [CPColor redColor]];
} else {
[self setColorOfBestPossibleAcuity: [CPColor colorWithRed: 0 green: 0.4 blue: 0 alpha: 1]];
}
[self radioButtonsAcuityBwOrColor_action: null];
// ↓ complicated to ensure the character is updated (and well visible) in the GUI
const decimalMarkCharIndexCurrent = [Settings decimalMarkCharIndex];// check for change
if (decimalMarkCharIndexCurrent != decimalMarkCharIndexPrevious) {// startup value is always null
decimalMarkCharIndexPrevious = decimalMarkCharIndexCurrent;//save for next time
[Settings setDecimalMarkChar: [Settings decimalMarkChar]];// this updates in GUI
[decimalMarkCharField setTextColor: [CPColor blueColor]];// while we're here…
[decimalMarkCharField setFont: [CPFont systemFontOfSize: 24]];//need more visibility
[decimalMarkCharField sizeToFit];// can't change font size of CPTextField, so →MDBTextField,
let r = [decimalMarkCharField bounds]; r.size.height = 30; r.origin.y = 12;
[decimalMarkCharField setBounds: r];
}
}
- (void) buttonExportEnableYESorNO: (CPNotification) aNotification { //console.info("buttonExportEnableYESorNO");
[buttonExport setEnabled: !([aNotification object] == 0)];
}
/**
Synchronising userdefaults & Appcontroller
This mirroring is necessary, because the Settingspanel cannot read the stored colors, because the Archiver does not work
*/
- (void) copyColorsFromSettings: (CPNotification) aNotification { //console.info("mirrorForeBackColors");
gColorFore = [Settings acuityForeColor]; [self setAcuityForeColor: gColorFore];
gColorBack = [Settings acuityBackColor]; [self setAcuityBackColor: gColorBack];
[self setGratingForeColor: [Settings gratingForeColor]]; [self setGratingBackColor: [Settings gratingBackColor]];
[self setWindowBackgroundColor: [Settings windowBackgroundColor]];
}
- (void) closeAllPanels {
for (const p of allPanels) [p close];
}
- (void) centerAllPanels {
for (const p of allPanels) [Misc centerWindowOrPanel: p];
}
/**
One of the tests should run, but let's test some prerequisites first
*/
- (void) notificationRunFractControllerTest: (CPNotification) aNotification { //called from ControlDispatcher
[self runFractControllerTest: [aNotification object]];
}
- (void) runFractControllerTest: (int) testNr { //console.info("AppController>runFractController");
if (currentFractController != null) return; // got here by accident, already inRun?
[sound initAfterUserinteraction];
currentTestID = testNr;
if ([Settings isNotCalibrated]) {
const alert = [CPAlert alertWithMessageText: "Calibration is mandatory for valid results!"
defaultButton: "I just want to try…" alternateButton: "OK, go to Settings" otherButton: "Cancel"
informativeTextWithFormat: "\rGoto 'Settings' and enter appropriate values for \r«Observer distance» and «Length of blue ruler».\r\rThis will also get rid of this obnoxious warning dialog."];
[alert runModalWithDidEndBlock: function(alert, returnCode) {
switch (returnCode) {
case 1: // alternateButton: go to Settings
[self setSettingsPaneTabViewSelectedIndex: 0]; // ensure "General" tab
[self buttonSettings_action: nil]; break;
case 0: // defaultButton
[self runFractController2]; break;
}
}];
} else {
[self runFractController2];
}
}
/**
The above prerequisites were met, so let's run the test specified in the class-global`currentTestID`
*/
- (void) runFractController2 { //console.info("AppController>runFractController2");
[self closeAllPanels]; [self centerAllPanels];
const allInfoPanels = {[kTestAcuityLett]: responseinfoPanelAcuityL, [kTestAcuityC]: responseinfoPanelAcuity8C, [kTestAcuityE]: responseinfoPanelAcuityE, [kTestAcuityTAO]: responseinfoPanelAcuityTAO, [kTestAcuityVernier]: responseinfoPanelAcuityVernier, [kTestContrastLett]: responseinfoPanelContrastLett, [kTestContrastC]: responseinfoPanelContrastC, [kTestContrastE]: responseinfoPanelContrastE, [kTestContrastG]: responseinfoPanelContrastG, [kTestAcuityLineByLine]: responseinfoPanelAcuityLineByLine};
if ([Settings responseInfoAtStart]) {
if (currentTestID in allInfoPanels) {
[allInfoPanels[currentTestID] makeKeyAndOrderFront: self];
if ((currentTestID == kTestAcuityC) && ([Settings nAlternatives] == 4)) {
[responseinfoPanelAcuity4C makeKeyAndOrderFront: self];
}
}
} else {
[self runFractController2_actionOK: nil];
}
}
/**
Info panels (above) were not needed, or OKed, so lets now REALLY run the test.
*/
- (IBAction) runFractController2_actionOK: (id) sender {
[self closeAllPanels]; [currentFractController release]; currentFractController = null;
if ([Settings autoFullScreen]) {
[Misc fullScreenOn: YES];
}
currentFractController = [[allTestControllers[currentTestID] alloc] initWithWindow: fractControllerWindow];
[currentFractController setSound: sound];
[currentFractController setCurrentTestID: currentTestID]; // while it has inherited currentTestID, it hasn't inherited its value
[currentFractController runStart];
}
/**
ok, so let's not run this test after all
*/
- (IBAction) runFractController2_actionCancel: (id) sender { //console.info("AppController>runFractController2_actionCancel");
[self closeAllPanels];
}
- (void) runEnd { //console.info("AppController>runEnd");
[currentFractController release]; currentFractController = nil;
if ([Settings autoFullScreen]) { // possible problem: if autoF is on,
[Misc fullScreenOn: NO]; // but screen was manually to fullscr., here will exit fullscr.
}
if (!runAborted) {
if ([Settings rewardPicturesWhenDone]) {
[rewardsController drawRandom];
}
[self exportCurrentTestResult];
}
[ControlDispatcher runDoneSuccessful: !runAborted];
// allow 1 eventloop
setTimeout(() => {[[selfWindow contentView] setNeedsDisplay: YES];}, 1);
}
- (void) exportCurrentTestResult { //console.info("AppController>exportCurrentTestResult");
let temp = currentTestResultExportString.replace(/,/g, "."); // in localStorage we don't want to localise
localStorage.setItem(gFilename4ResultStorage, temp);
temp = currentTestResultsHistoryExportString.replace(/,/g, ".");
localStorage.setItem(gFilename4ResultsHistoryStorage, temp);
if ([Settings results2clipboard] != kResults2ClipNone) {
if ([Settings results2clipboard] == kResults2ClipFullHistory) {
currentTestResultExportString += currentTestResultsHistoryExportString;
}
if ([Settings results2clipboardSilent]) {
[Misc copyString2Clipboard: currentTestResultExportString];
} else {
[Misc copyString2ClipboardWithDialog: currentTestResultExportString];
}
}
[self postNotificationName: "buttonExportEnableYESorNO" object: ([currentTestResultExportString length] > 1)];
}
- (void) drawStimulusInRect: (CGRect) dirtyRect forView: (FractView) fractView { //console.info("AppController>drawStimulusInRect");
[currentFractController drawStimulusInRect: dirtyRect forView: fractView];
}
/*- (void) controlTextDidChange: (CPNotification) notification { //console.info(@"controlTextDidChange: stringValue == %@", [[notification object] stringValue]);[Settings calculateMinMaxPossibleAcuity];
}*/
/**
Called from some text fields in the Settings panel, to update dependencies
*/
- (void) controlTextDidEndEditing: (CPNotification) notification { //console.info(@"controlTextDidChange: stringValue == %@", [[notification object] stringValue]);
[Settings calculateMinMaxPossibleAcuity];
[Settings calculateAcuityForeBackColorsFromContrast];
}
#pragma mark
- (void) keyDown: (CPEvent) theEvent { //console.info("AppController>keyDown");
const key = [[[theEvent charactersIgnoringModifiers] characterAtIndex: 0] uppercaseString];
if (kShortcutKeys4TestsArray[key]) {
[self runFractControllerTest: kShortcutKeys4TestsArray[key]]; return;
}
switch(key) {
case "Q": case "X": case "-": // Quit or eXit
[self buttonDoExit_action: nil]; break;
case "S": // Settings
// this complicated version avoids propagation of the "s"
[[CPRunLoop currentRunLoop] performSelector: @selector(buttonSettings_action:) target: self argument: nil order: 10000 modes: [CPDefaultRunLoopMode]]; break;
case "F":
[self buttonFullScreen_action: nil]; break;
case "5":
const sto5 = [Settings testOnFive];
if (sto5 > 0) [self runFractControllerTest: sto5];
break;
case "R":
[Settings toggleAutoRunIndex]; break;
default:
[super keyDown: theEvent]; break;
}
}
- (IBAction) buttonFullScreen_action: (id) sender { //console.info("AppController>buttonFullScreen");
[Misc fullScreenOn: ![Misc isFullScreen]]; // toggle
// the origin changes are handled by the eventListener("fullscreenchange" above.
}
/**
All test buttons land here, discriminated by their tag values (→HierarchyController for `TestIDType`)
*/
- (IBAction) buttonDoTest_action: (id) sender { //console.info("buttonDoTest_action ", [sender tag])
[self runFractControllerTest: [sender tag]];
}
/**
Deal with the Settings panel
*/
- (IBAction) buttonSettings_action: (id) sender { //console.info("AppController>buttonSettings");
[sound initAfterUserinteraction];
[Settings checkDefaults]; [settingsPanel makeKeyAndOrderFront: self];
[Misc centerWindowOrPanel: settingsPanel];
if (settingsNeededNewDefaults) {
settingsNeededNewDefaults = NO;
const alert = [CPAlert alertWithMessageText: "WARNING"
defaultButton: "OK" alternateButton: nil otherButton: nil
informativeTextWithFormat: "\r\rAll settings were (re)set to their default values.\r\r"];
[alert runModalWithDidEndBlock: function(alert, returnCode) {}];
}
[self postNotificationName: "copyColorsFromSettings" object: nil];
}
- (IBAction) buttonSettingsClose_action: (id) sender {
[Settings checkDefaults]; [settingsPanel close];
}
- (IBAction) buttonSettingsTestSound_action: (id) sender {
[self postNotificationName: "updateSoundFiles" object: nil];
[sound playDelayedNumber: [sender tag]]; // delay because new buffer to be loaded; 0.02 would be enough.
}
- (IBAction) buttonSettingsContrastAcuityMaxMin_action: (id) sender {
switch ([sender tag]) {
case 1: [Settings setContrastAcuityWeber: 100]; break;
case 2: [Settings setContrastAcuityWeber: -10000]; break;
}
}
- (IBAction) popupPreset_action: (id) sender {//console.info("popupPreset_action: ", sender)
[presets apply: sender];
}
#pragma mark
/**
And more buttons…
*/
- (IBAction) buttonExport_action: (id) sender { //console.info("AppController>buttonExport_action");
[Misc copyString2Clipboard: currentTestResultExportString];
[self postNotificationName: "buttonExportEnableYESorNO" object: 0];
}
- (IBAction) buttonDoExit_action: (id) sender { //console.info("AppController>buttonExit_action");
if ([Misc isFullScreen]) {
[Misc fullScreenOn: NO];
}
[selfWindow close]; [CPApp terminate: nil]; window.close();
}
- (IBAction) radioButtonsAcuityBwOrColor_action: (id) sender {
if (sender != null)
[Settings setAcuityColor: [sender tag] == 1];
else { // this is to preset the radio buttons
[radioButtonAcuityBW setState: ([Settings isAcuityColor] ? CPOffState : CPOnState)];
[radioButtonAcuityColor setState: ([Settings isAcuityColor] ? CPOnState : CPOffState)];
}
}
- (IBAction) buttonGamma_action: (id) sender {
[Settings setGammaValue: [Settings gammaValue] + ([sender tag] == 1 ? 0.1 : -0.1)];
[gammaView setNeedsDisplay: YES];
}
@end