-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathAppController.j
More file actions
686 lines (597 loc) · 28.4 KB
/
AppController.j
File metadata and controls
686 lines (597 loc) · 28.4 KB
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
/*
This file is part of FrACT10, a vision test battery.
© 2021 Michael Bach, bach@uni-freiburg.de, <https://michaelbach.de>
AppController.j
Created by mb on 2017-07-12.
*/
@import <Foundation/Foundation.j>
@import <AppKit/CPView.j> //don't need to include the full `AppKit.j`
@import <AppKit/CPWindowController.j>
@import <AppKit/CPAlert.j>
@import <AppKit/CPWebView.j>
@import "Globals.j"
@import "Misc.j"
@import "FractView.j"
@import "FractController.j"
@import "FractControllerAcuityLetters.j"
@import "FractControllerAcuityLandolt.j"
@import "FractControllerAcuityE.j"
@import "FractControllerAcuityTAO.j"
@import "FractControllerAcuityVernier.j"
@import "FractControllerContrastLetters.j"
@import "FractControllerContrastC.j"
@import "FractControllerContrastE.j"
@import "FractControllerContrastG.j"
@import "FractControllerContrastDitherUnittest.j"
@import "FractControllerAcuityLineByLine.j"
@import "FractControllerBalmLight.j"
@import "FractControllerBalmLocation.j"
@import "FractControllerBalmMotion.j"
@import "RewardsController.j"
@import "TAOController.j"
@import "Sound.j"
@import "GammaView.j"
@import "MDBSetHandCursor.j"
@import "MDBButton_Test.j"
@import "CPTextField_Category.j"
@import "MDBLabel.j"
@import "MDBAlert.j"
@import "QRPanel.j"
@import "Presets.j"
@import "ResponseBoxController.j"
@import "ControlDispatcher.j"
@import "CardController.j"
@import "AboutAndHelpController.j"
@import "PlotController.j"
@import "CheckingContrastController.j"
@import "ResponseInfoPanelController.j"
/**
AppController
*/
@implementation AppController: CPWindowController {
@outlet CPWindow fractControllerWindow;
@outlet CPPanel settingsPanel;
CPPanel currentInfoPanel;
@outlet MDBButton_Test buttonAcuityTAO;
@outlet CPButton buttonExportClip, buttonExportPDF, buttonPlot;
@outlet CPButton radioButtonAcuityBW, radioButtonAcuityColor;
@outlet GammaView gammaView;
@outlet CPPopUpButton settingsPanePresetsPopUpButton; Presets presets;
@outlet CPPopUpButton settingsPaneSoundsTrialStartPopUp;
@outlet CPPopUpButton settingsPaneMiscSoundsTrialYesPopUp;
@outlet CPPopUpButton settingsPaneMiscSoundsTrialNoPopUp;
@outlet CPPopUpButton settingsPaneMiscSoundsRunEndPopUp;
@outlet MDBLabel resultStringField;
CPString versionDateString @accessors; //for the main Xib window top right
CPString currentTestResultUnit @accessors;
CPString currentTestResultExportString @accessors;
CPString currentTestResultsHistoryExportString @accessors;
CPString currentUUID @accessors;
Sound sound;
CPImageView rewardImageView;
RewardsController rewardsController;
TAOController taoController;
FractController currentFractController;
BOOL settingsNeededNewDefaults;
BOOL runAborted @accessors;
BOOL has4orientations @accessors;
BOOL has2orientations @accessors;
BOOL is2alternatives @accessors;
BOOL is4alternatives @accessors;
BOOL is8plusAlternatives @accessors;
id allPanels;
int settingsPaneTabViewSelectedIndex @accessors;
CPColor colorOfBestPossibleAcuity @accessors;
CPNumberFormatter numberFormatter;
@outlet CPTextField contrastMaxLogCSWeberField;
@outlet CPTextField gammaValueField;
int decimalMarkCharIndexPrevious;
@outlet CPTextField 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]; [[self window] setBackgroundColor: col];
}
/**
Our main initialisation begins here
*/
- (id) init { //console.info("AppController>init");
gAppController = self; //so others can reference via global variable
[Misc CPLogSetup];
settingsNeededNewDefaults = [Settings needNewDefaults];
[Settings checkDefaults]; //important to do this very early, before nib loading, otherwise the updates don't populate the settings panel
//create UUID with optional fallback
gCurrentUUID = window.crypto?.randomUUID?.() ??
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = window.crypto?.getRandomValues
? window.crypto.getRandomValues(new Uint8Array(1))[0] % 16
: (Math.random() * 16) | 0;
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
}); //The ?.() optional call short-circuits to undefined if randomUUID doesn't exist, letting ?? kick in and run the fallback inline.
[self setCurrentUUID: gCurrentUUID]; //for the GUI
return self;
}
#pragma mark
/** runs after "init" above */
- (void) applicationDidFinishLaunching: (CPNotification) aNotification { //console.info("AppController>…Launching");
currentFractController = null; //making sure, is used to check whether inRun
[Misc randomizeRandomGenerator];
[[self window] setFullPlatformWindow: YES];
[[self window] setBackgroundColor: [self windowBackgroundColor]];
[[self window] setTitle: "FrACT10"];
[self setVersionDateString: gTestDetails[td_vsFrACT]];
[CPMenu setMenuBarVisible: NO];
[self setupEventListeners];
[Settings checkDefaults]; //what was the reason to put this here???
allPanels = [settingsPanel];
if (currentInfoPanel) [allPanels addObject: currentInfoPanel];
for (const p of allPanels) [p setMovable: NO];
[self setSettingsPaneTabViewSelectedIndex: 0]; //select the "General" tab in Settings
rewardImageView = [[CPImageView alloc] initWithFrame: CGRectMake(100, 0, 600, 600)];
[[[self window] contentView] addSubview: rewardImageView positioned: CPWindowBelow relativeTo: nil];
[self setupControllers];
[buttonExportClip setEnabled: NO]; [buttonExportPDF setEnabled: NO];
[buttonPlot setEnabled: (gTestingPlottingAcuity1Contrast2 !== 0)];
[[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
[Settings setPatID: gPatIDdefault]; //clear ID string
numberFormatter = [[CPNumberFormatter alloc] init];
[numberFormatter setNumberStyle: CPNumberFormatterDecimalStyle];
[numberFormatter setMinimumFractionDigits: 1];
[contrastMaxLogCSWeberField setFormatter: numberFormatter];
[gammaValueField setFormatter: numberFormatter];
[Settings setupSoundPopups: [settingsPaneSoundsTrialStartPopUp, 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: [[self window] contentView]]; //→center
[[self window] orderFront: self]; //ensures that it will receive clicks w/o activating
[resultStringField setVerticalAlignment: CPTopVerticalTextAlignment];
[self setResultStringFieldTo: "– Result displayed here –"];
[resultStringField setEnabled: NO];
if ([Settings isAutoPreset]) {
[presets applyPresetNamed: [Settings presetName]];
[Settings setIsAutoPreset: YES];
}
}
- (void) setupEventListeners { //called from `applicationDidFinishLaunching`
window.addEventListener('error', function(e) {
console.error("Error details:", e);
alert("An error occured, I'm sorry. Error message:\r\r" + e.message + "\r\rIf it recurs, please notify bach@uni-freiburg.de, 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 respondsToMobileOrientation]) {
//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
}
}
});
/*if ([Settings autoFullScreen]) { //does not work because it needs user interaction
[Misc fullScreenOn: YES];
}*/
window.addEventListener("resize", (event) => {
if ([Misc isInRun]) return; //don't do ⇙this while "inRun"
[Misc centerWindowOrPanel: [[self window] contentView]];
/* //https://ua.hexalys.com
console.info("scale", window.visualViewport.scale);
console.info("window.devicePixelRatio", window.devicePixelRatio);
console.info("window.outerWidth / window.innerWidth", window.outerWidth / window.innerWidth);*/
});
}
- (void) setupControllers { //called from `applicationDidFinishLaunching`
rewardsController = [[RewardsController alloc] initWithView: rewardImageView];
taoController = [[TAOController alloc] initWithButton2Enable: buttonAcuityTAO];
sound = [[Sound alloc] init];
presets = [[Presets alloc] initWithPopup: settingsPanePresetsPopUpButton];
}
/**
Observe changes in the settings panel, making sure dependencies are updated
Caution: endless loops need to be avoided
*/
- (void) settingsDidChange: (CPNotification) aNotification { //console.info("settingsDidChange");
[self setHas4orientations: ([Settings nAlternatives] === 4)];
[self setHas2orientations: ([Settings nAlternatives] === 2)];
if (is2alternatives !== ([Settings nAlternatives] === 2)) {
[self setIs2alternatives: ([Settings nAlternatives] === 2)];
} //this complicated way avoids endless loop
if (is4alternatives !== ([Settings nAlternatives] === 4)) {
[self setIs4alternatives: ([Settings nAlternatives] === 4)];
}
if (is8plusAlternatives !== ([Settings nAlternatives] >= 8)) {
[self setIs8plusAlternatives: ([Settings nAlternatives] >= 8)];
}
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 setFont: [CPFont systemFontOfSize: 20]]; //need more visibility
[decimalMarkCharField sizeToFit]; //doesn't work ↑ in IB
}
let rct = CGRectMake(0, 560, 800, 84); //placeResultStringField
if ([Settings showIdAndEyeOnMain]) rct = CGRectOffset(rct, 84 / 2, 0);
[resultStringField setFrame: rct];
gColorFore = [Settings acuityForeColor];
gColorBack = [Settings acuityBackColor];
if (![[self acuityForeColor] isEqual: gColorFore]) { //avoid endless loop
[self setAcuityForeColor: gColorFore];
[self setAcuityBackColor: gColorBack];
}
if (![[self gratingForeColor] isEqual: [Settings gratingForeColor]]) {
[self setGratingForeColor: [Settings gratingForeColor]];
[self setGratingBackColor: [Settings gratingBackColor]];
}
if (![[self windowBackgroundColor] isEqual: [Settings windowBackgroundColor]]) {
[self setWindowBackgroundColor: [Settings windowBackgroundColor]];
}
[[self window] setBackgroundColor: [self windowBackgroundColor]];
}
- (void) closeAllPanels { //console.info("closeAllPanels")
if (settingsPanel) [settingsPanel close];
if (currentInfoPanel) [currentInfoPanel close];
}
- (void) centerAllPanels {
if (settingsPanel) [Misc centerWindowOrPanel: settingsPanel];
if (currentInfoPanel) [Misc centerWindowOrPanel: currentInfoPanel];
}
- (void) setResultStringFieldTo: (CPString) s {
[resultStringField setStringValue: s]; [resultStringField setEnabled: YES];
}
/**
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");
[ControlDispatcher sendChar: "\r"]; //commit possible change in patID
let frontWindow = [[CPApp orderedWindows] objectAtIndex:0];
if ([frontWindow title] !== "FrACT10") [frontWindow close]; //close overlaying window if any
if (currentFractController !== null) return; //got here by accident, already inRun?
[buttonExportClip setEnabled: NO]; [buttonExportPDF setEnabled: NO]; [buttonPlot setEnabled: NO];
[sound initAfterUserinteraction];
gCurrentTestID = testNr;
if ([Settings isNotCalibrated]) {
gLatestAlert = [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 the present obnoxious warning dialog."];
[gLatestAlert 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;
}
gLatestAlert = null;
}];
} else {
[self runFractController2];
}
}
/**
The above prerequisites were met, so let's run the test specified in the global`gCurrentTestID`
*/
- (void) runFractController2 { //console.info("AppController>runFractController2");
[self closeAllPanels];
if ([Settings showResponseInfoAtStart] && !([kTestContrastDitherUnittest, kTestBalmGeneral, kTestBalmLight, kTestBalmLocation, kTestBalmMotion].includes(gCurrentTestID))) {
currentInfoPanel = [ResponseInfoPanelController panelForTestID: gCurrentTestID];
[self centerAllPanels];
[currentInfoPanel 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];
if (currentFractController) {
[currentFractController release]; currentFractController = null;
}
const className = gTestRegistry[gCurrentTestID]?.className;
const testClass = CPClassFromString(className);
if (testClass) {
currentFractController = [[testClass alloc] initWithWindow: fractControllerWindow];
[currentFractController setSound: sound];
currentTestResultExportString = "";
[currentFractController runStart];
} else {
console.error("Unknown test class for ID:", gCurrentTestID);
}
}
/**
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");
[resultStringField setEnabled: YES];
[currentFractController release]; currentFractController = nil;
if (!runAborted) {
if ([Settings showRewardPicturesWhenDone]) {
[rewardsController drawRandom];
}
[self exportCurrentTestResult];
}
[ControlDispatcher runDoneSuccessful: !runAborted];
//allow 1 eventloop
const UI_UPDATE_DELAY = 1; //1 millisecond is enough
setTimeout(() => {[[[self window] contentView] setNeedsDisplay: YES];}, UI_UPDATE_DELAY);
//console.info(gTestDetails);
}
- (void) exportCurrentTestResult { //console.info("AppController>exportCurrentTestResult");
try {//in localStorage we don't want to localise
let temp = currentTestResultExportString.replace(/,/g, ".");
localStorage.setItem(gFilename4ResultStorage, temp);
temp = currentTestResultsHistoryExportString.replace(/,/g, ".");
localStorage.setItem(gFilename4ResultsHistoryStorage, temp);
} catch (e) {
console.warn("localStorage not available:", e);
// Fallback behavior not really availabe
}
let string4clipboard = currentTestResultExportString;
switch ([Settings resultsToClipboardIndex]) {
case kResultsToClipNone: break;
case kResultsToClipFullHistory:
string4clipboard += currentTestResultsHistoryExportString;
//purposefully "fall throught" to next:
case kResultsToClipFinalOnly:
if ([Settings putResultsToClipboardSilent]) {
[Misc copyString2Clipboard: string4clipboard];
} else {
[Misc copyString2ClipboardWithDialog: string4clipboard];
}
break;
case kResultsToClipFullHistory2PDF: [Misc export2PDF: currentTestResultExportString withHistory: currentTestResultsHistoryExportString]; break;
}
[buttonExportClip setEnabled: ([currentTestResultExportString length] > 1)];
[buttonExportPDF setEnabled: ([currentTestResultExportString length] > 1)];
if ([kTestAcuityLetters, kTestAcuityLandolt, kTestAcuityE, kTestAcuityTAO, kTestContrastLetters, kTestContrastLandolt, kTestContrastE, kTestContrastG].includes(gCurrentTestID)){
[buttonPlot setEnabled: ([currentTestResultExportString length] > 1)];
}
}
- (void) drawStimulusInRect: (CGRect) dirtyRect forView: (FractView) fractView { //CPLog("AppController>drawStimulusInRect");
[currentFractController drawStimulusInRect: dirtyRect forView: fractView];
}
/**
Perform a "health check" verifying that all outlets are connected and the global state is consistent. Useful for finding issues after refactorings or Xib changes.
@return YES if all checks pass
*/
- (BOOL) unittestAppC {
let success = YES, report = crlf + "AppController▸unittestAppC:" + crlf;
if (gAppController !== self) { //Check global controller
report += " ERROR: gAppController is not self!" + crlf; success = NO;
}
//Check critical outlets
const criticalOutlets = [
{name: "fractControllerWindow", val: fractControllerWindow},
{name: "settingsPanel", val: settingsPanel},
{name: "buttonAcuityTAO", val: buttonAcuityTAO},
{name: "gammaView", val: gammaView},
{name: "resultStringField", val: resultStringField}
];
for (const outlet of criticalOutlets) {
if (!outlet.val) {
report += " ERROR: Outlet '" + outlet.name + "' is nil!" + crlf; success = NO;
}
}
if (![[self window] isKeyWindow] && !currentFractController) { //Check window state
report += " WARNING: Main window is not Key, but no test is running." + crlf;
}
if (success) {
report += " All critical outlets and global states are OK." + crlf;
}
console.info(report);
return success;
}
/*- (void) controlTextDidChange: (CPNotification) notification {
}*/
/**
Called from some text fields in the Settings panel, to update dependencies
*/
- (void) controlTextDidEndEditing: (CPNotification) notification {
[Settings calculateMinMaxPossibleAcuity];
[Settings calculateAcuityForeBackColorsFromContrast];
}
#pragma mark
- (void) keyDown: (CPEvent) theEvent { //console.info("AppController>keyDown");
const key = [[[theEvent charactersIgnoringModifiers] characterAtIndex: 0] uppercaseString];
for (const testID in gTestRegistry) {
if (gTestRegistry[testID].shortcut === key) {
[self runFractControllerTest: parseInt(testID)];
return;
}
}
switch(key) { //many keys are dealt with via the "Key Equivalent" in IB
case "W" : [Misc infoAllWindows]; break;
case "Q": case "X": case "-": //Quit or eXit
[self buttonDoExit_action: nil]; break;
case "5":
const sto5 = [Settings testOnFive];
if (sto5 > 0) [self runFractControllerTest: sto5];
break;
// case "R":
// [Settings setAutoRunIndex: [Settings autoRunIndex] === kAutoRunIndexNone ? kAutoRunIndexMid : kAutoRunIndexNone];
// break;
case "U":
[Misc allUnittests];
break;
default:
[super keyDown: theEvent]; break;
}
}
- (BOOL) alertShowHelp: (id) sender { //DOESN'T WORK
console.info("alertShowHelp");
return YES;
}
- (void) balmSwitch {
gLatestAlert = [MDBAlert alertWithMessageText: "BaLM@FrACT₁₀" defaultButton: "Cancel" alternateButton: "❓Help" otherButton: "Motion (‘3’)" informativeTextWithFormat: "“Basic Assessment of Light, Location & Motion”\rfor ultra low vision.\r\r\r↓ Which BaLM test?"];
[gLatestAlert addButtonWithTitle: "Location (‘2’)"]; //returnCode === 3
[gLatestAlert addButtonWithTitle: "Light (‘1’)"]; //returnCode === 2
[gLatestAlert setDelegate: self];
//[alert setShowsHelp: YES]; //doesn't work
[[gLatestAlert buttons][0] setKeyEquivalent: "1"]; //yes, 1/2 inverted…
[[gLatestAlert buttons][1] setKeyEquivalent: "2"];
[[gLatestAlert buttons][2] setKeyEquivalent: "3"];
[[gLatestAlert buttons][3] setKeyEquivalent: "h"]; //help
[[gLatestAlert buttons][4] setKeyEquivalent: CPEscapeFunctionKey]; //esc
[gLatestAlert runModalWithDidEndBlock: function(alert, returnCode) {
switch (returnCode) {
case 4: //console.info(returnCode); //Light
[self runFractControllerTest: kTestBalmLight];
break;
case 3: //console.info(returnCode); //Location
[self runFractControllerTest: kTestBalmLocation];
break;
case 2: //console.info(returnCode); //Motion
[self runFractControllerTest: kTestBalmMotion];
break;
case 1: //console.info(returnCode); //help
const url = "https://michaelbach.de/sci/stim/balm/index.html";
if ([Misc existsUrl: url]) window.open(url, "_blank");
break;
default: //console.info(returnCode); //0=cancel
}
gLatestAlert = null;
}];
}
- (IBAction) buttonFullScreen_action: (id) sender { //console.info("AppController>buttonFullScreen");
[Misc fullScreenOn: ![Misc isFullScreen]]; //toggle
}
/**
All test buttons land here, discriminated by their tag values (→Globals for `TestIDType`)
*/
- (IBAction) buttonDoTest_action: (id) sender { //console.info("buttonDoTest_action ", [sender tag])
if ([sender tag] === kTestBalmGeneral) {
[self balmSwitch]; return;
}
[self runFractControllerTest: [sender tag]];
}
- (IBAction) buttonRemoteResponse_action: (id) sender {
if (!navigator.onLine) { //skip if browser thinks we're offline
const s = "We're not online, but that's required for the" + crlf + "Remote Response Box";
gLatestAlert = [CPAlert alertWithMessageText: "WARNING" defaultButton: "OK" alternateButton: nil otherButton: nil informativeTextWithFormat: s];
[gLatestAlert runModalWithDidEndBlock: function(alert, returnCode) {
gLatestAlert = null;
}];
return;
}
[ResponseBoxController init];
let url = "https://michaelbach.de/fract/respond.html";
url += "?session=" + currentUUID;
const panel = [[QRPanel alloc] initWithQRString: url];
[panel makeKeyAndOrderFront: self];
}
/**
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;
gLatestAlert = [CPAlert alertWithMessageText: "WARNING"
defaultButton: "OK" alternateButton: nil otherButton: nil
informativeTextWithFormat: "\r\rAll settings were (re)set to their default values.\r\r"];
[gLatestAlert runModalWithDidEndBlock: function(alert, returnCode) {
gLatestAlert = null;
}];
}
}
- (IBAction) buttonSettingsClose_action: (id) sender {
[Settings checkDefaults]; [settingsPanel close];
}
- (IBAction) buttonSettingsTestSound_action: (id) sender { //console.info("buttonSettingsTestSound_action", [sender tag]);
[sound updateSoundFiles];
[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];
}
- (IBAction) buttonSettingsExportImport_action: (id) sender {
switch ([sender tag]) {
case 1: [Settings exportAllSettings]; break;
case 2: [Settings importAllSettings]; break;
}
}
#pragma mark
/**
And more buttons…
*/
- (IBAction) buttonExportClip_action: (id) sender { //CPLog("AppController>buttonExportClip_action");
[Misc copyString2Clipboard: currentTestResultExportString];
[buttonExportClip setEnabled: NO];
}
- (IBAction) buttonExportPDF_action: (id) sender { //CPLog("AppController>buttonExportPDF_action");
[Misc export2PDF: currentTestResultExportString withHistory: currentTestResultsHistoryExportString];
}
- (IBAction) buttonDoExit_action: (id) sender { //console.info("AppController>buttonExit_action");
[Misc fullScreenOn: NO];
[Settings setPatID: gPatIDdefault]; //clear ID string
[[self window] close]; [CPApp terminate: nil]; window.close();
}
- (IBAction) radioButtonsAcuityBwOrColor_action: (id) sender {
if (sender !== null)
[Settings setIsAcuityColor: [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