forked from jeffpar/pcjs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
keyboard.js
1430 lines (1369 loc) · 58.9 KB
/
keyboard.js
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
/**
* @fileoverview Implements the PCx80 Keyboard component
* @author Jeff Parsons <Jeff@pcjs.org>
* @copyright © 2012-2024 Jeff Parsons
* @license MIT <https://www.pcjs.org/LICENSE.txt>
*
* This file is part of PCjs, a computer emulation software project at <https://www.pcjs.org>.
*/
import ChipSetX80 from "./chipset.js";
import MESSAGE from "./message.js";
import Component from "../../../modules/v2/component.js";
import Keys from "../../../modules/v2/keys.js";
import State from "../../../modules/v2/state.js";
import StrLib from "../../../modules/v2/strlib.js";
import WebLib from "../../../modules/v2/weblib.js";
import { APPCLASS, COMPILED, MAXDEBUG, globals } from "./defines.js";
/**
* TODO: The Closure Compiler treats ES6 classes as 'struct' rather than 'dict' by default,
* which would force us to declare all class properties in the constructor, as well as prevent
* us from defining any named properties. So, for now, we mark all our classes as 'unrestricted'.
*
* @class KeyboardX80
* @unrestricted
*/
export default class KeyboardX80 extends Component {
/**
* KeyboardX80(parmsKbd)
*
* The KeyboardX80 component has the following component-specific (parmsKbd) properties:
*
* model: eg, "VT100" (should be a member of KeyboardX80.MODELS)
*
* @this {KeyboardX80}
* @param {Object} parmsKbd
*/
constructor(parmsKbd)
{
super("Keyboard", parmsKbd, MESSAGE.KBD);
let model = parmsKbd['model'];
if (model && !KeyboardX80.MODELS[model]) {
Component.printf(MESSAGE.NOTICE, "Unrecognized KeyboardX80 model: %s\n", model);
}
this.config = KeyboardX80.MODELS[model] || {};
this.reset();
this.setReady();
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {KeyboardX80}
* @param {string} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "esc")
* @param {HTMLElement} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @returns {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
/*
* There's a special binding that the Video component uses ("screen") to effectively bind its
* screen to the entire keyboard, in Video.powerUp(); ie:
*
* video.kbd.setBinding("canvas", "screen", video.canvasScreen);
* or:
* video.kbd.setBinding("textarea", "screen", video.textareaScreen);
*
* However, it's also possible for the keyboard XML definition to define a control that serves
* a similar purpose; eg:
*
* <control type="text" binding="kbd" width="2em">Keyboard</control>
*
* The latter is purely experimental, while we work on finding ways to trigger the soft keyboard on
* certain pesky devices (like the Kindle Fire). Note that even if you use the latter, the former will
* still be enabled (there's currently no way to configure the Video component to not bind its screen,
* but we could certainly add one if the need ever arose).
*/
let kbd = this;
let id = sHTMLType + '-' + sBinding;
if (this.bindings[id] === undefined) {
if (sHTMLType == "led" && this.config.LEDCODES[sBinding]) {
this.bindings[id] = control;
return true;
}
switch (sBinding) {
case "kbd":
case "screen":
/*
* Recording the binding ID prevents multiple controls (or components) from attempting to erroneously
* bind a control to the same ID, but in the case of a "dual display" configuration, we actually want
* to allow BOTH video components to call setBinding() for "screen", so that it doesn't matter which
* display the user gives focus to.
*
* this.bindings[id] = control;
*/
if (WebLib.isUserAgent("iOS")) {
/*
* For iOS devices, it's best to deal only with keypress events. The main reason is that we don't
* get shift-key events, so we have no way of distinguishing between certain keys, such as ':' and
* ';', unless we are monitoring key presses. Another reason is that, under certain poorly documented
* conditions, an iOS keyup event will not contain any keyCode; this is most easily reproduced with
* the iOS simulator and a physical keyboard (not the pop-up keyboard). When this happens, we think
* the key is stuck. Finally, certain other problems that we have tried to resolve when using a physical
* keyboard (eg, keeping the physical and virtual CAPS-LOCK states in sync) simply don't exist in the
* iOS environment.
*
* So, with all that mind, it seems best to have a separate iOS keypress handler and forego keydown
* and keyup events entirely. The iOS keypress handler must also perform some additional checks, such
* as watching for keys that can only be typed on the emulated device when a shift key is down, and
* simulating "fake" shift-key down and up events.
*
* Perhaps we can eventually standardize on this alternate keypress-centric approach for ALL devices,
* but until then, it's safer to have these two code paths.
*
* UPDATE: So much for the best laid plans. iOS won't deliver BACKSPACE events to the keypress handler,
* so we have to deal with keydown/keyup events after all.
*/
control.onkeypress = function oniOSKeyPress(event)
{
return kbd.oniOSKeyPress(event);
};
control.onkeydown = function oniOSKeyDown(event)
{
return kbd.oniOSKeyDown(event, true);
};
control.onkeyup = function oniOSKeyUp(event)
{
return kbd.oniOSKeyDown(event, false);
};
}
else {
control.onkeydown = function onKeyDown(event)
{
return kbd.onKeyDown(event, true);
};
control.onkeyup = function onKeyUp(event)
{
return kbd.onKeyDown(event, false);
};
control.onkeypress = function onKeyPress(event)
{
return kbd.onKeyPress(event);
};
}
control.onpaste = function onKeyPaste(event)
{
return kbd.onPaste(event);
};
return true;
default:
if (this.config.SOFTCODES && this.config.SOFTCODES[sBinding] !== undefined) {
this.bindings[id] = control;
control.onclick = function(kbd, keyCode) {
return function onKeyboardBindingDown(event) {
/*
* iOS usability improvement: calling preventDefault() prevents rapid clicks from
* also being (mis)interpreted as a desire to "zoom" in on the machine.
*/
if (event.preventDefault) event.preventDefault();
/*
* TODO: Add some additional SOFTCODES configuration info that will tell us which soft
* keys (eg, CTRL) should be treated as toggles, instead of hard-coding that knowledge below.
*
* Moreover, if a *real* CTRL or CAPS-LOCK key is pressed or released, it would be nice
* to update the state of these on-screen controls, too (ie, not just when the controls are
* clicked).
*/
let fDown = true, bit = 0;
if (keyCode == Keys.KEYCODE.CTRL) {
bit = KeyboardX80.STATE.CTRL;
}
else if (keyCode == Keys.KEYCODE.CAPS_LOCK) {
bit = KeyboardX80.STATE.CAPS_LOCK;
}
if (bit) {
control.style.fontWeight = "normal";
fDown = !(kbd.bitsState & bit);
if (fDown) control.style.fontWeight = "bold";
kbd.checkModifierKeys(keyCode, fDown);
}
kbd.onSoftKeyDown(keyCode, fDown, !bit);
if (kbd.cmp) kbd.cmp.updateFocus();
};
}(this, this.config.SOFTCODES[sBinding]);
//
// var fnUp = function (kbd, keyCode) {
// return function onKeyboardBindingUp(event) {
// kbd.onSoftKeyDown(keyCode, false);
// /*
// * Give focus back to the machine (since clicking the button takes focus away).
// *
// * if (kbd.cmp) kbd.cmp.updateFocus();
// *
// * iOS Usability Improvement: NOT calling updateFocus() keeps the soft keyboard down
// * (assuming it was already down).
// */
// };
// }(this, this.config.SOFTCODES[sBinding]);
//
// if ('ontouchstart' in globals.window) {
// control.ontouchstart = fnDown;
// control.ontouchend = fnUp;
// } else {
// control.onmousedown = fnDown;
// control.onmouseup = control.onmouseout = fnUp;
// }
//
// UPDATE: Since the only controls that we explicitly bind to SOFTCODES are buttons, I'm simplifying
// the above code with a conventional "onclick" handler. The only corresponding change I had to make
// to the onclick (formerly fnDown) function was to set fAutoRelease on its call to onSoftKeyDown(),
// since we're no longer attempting to detect when the control (ie, the button) is actually released.
//
// This change also resolves a problem I ran into with the Epiphany (WebKit-based) web browser running
// on the "elementary" (Ubuntu-based) OS, where clicks on the SET-UP button were ignored; perhaps its
// buttons don't generate mouse and/or touch events. Anyway, an argument for keeping things simple.
//
return true;
}
break;
}
}
return false;
}
/**
* initBus(cmp, bus, cpu, dbg)
*
* @this {KeyboardX80}
* @param {ComputerX80} cmp
* @param {BusX80} bus
* @param {CPUStateX80} cpu
* @param {DebuggerX80} dbg
*/
initBus(cmp, bus, cpu, dbg)
{
this.cmp = cmp;
this.cpu = cpu;
this.dbg = dbg; // NOTE: The "dbg" property must be set for the message functions to work
let kbd = this;
this.timerReleaseKeys = this.cpu.addTimer(this.id, function() {
kbd.checkSoftKeysToRelease();
});
this.chipset = /** @type {ChipSetX80} */ (cmp.getMachineComponent("ChipSet"));
this.serial = /** @type {SerialPortX80} */ (cmp.getMachineComponent("SerialPort"));
bus.addPortInputTable(this, this.config.portsInput);
bus.addPortOutputTable(this, this.config.portsOutput);
}
/**
* powerUp(data, fRepower)
*
* @this {KeyboardX80}
* @param {Object|null} data
* @param {boolean} [fRepower]
* @returns {boolean} true if successful, false if failure
*/
powerUp(data, fRepower)
{
if (!fRepower) {
if (!data) {
this.reset();
} else {
if (!this.restore(data)) return false;
}
}
return true;
}
/**
* powerDown(fSave, fShutdown)
*
* @this {KeyboardX80}
* @param {boolean} [fSave]
* @param {boolean} [fShutdown]
* @returns {Object|boolean} component state if fSave; otherwise, true if successful, false if failure
*/
powerDown(fSave, fShutdown)
{
return fSave? this.save() : true;
}
/**
* reset()
*
* @this {KeyboardX80}
*/
reset()
{
/*
* As keyDown events are encountered, a corresponding "softCode" is looked up. If one is found,
* then an entry for the key is added to the aKeysActive array. Each "key" entry in aKeysActive contains:
*
* softCode: number or string representing the key pressed
* msDown: timestamp of the most recent "down" event
* fAutoRelease: true to auto-release the key after MINPRESSTIME (set when "up" occurs too quickly)
*
* When the key is finally released (or auto-released), its entry is removed from the array.
*/
this.aKeysActive = [];
/*
* The current (assumed) physical (and simulated) states of the various shift/lock keys.
*
* TODO: Determine how (or whether) we can query the browser's initial shift/lock key states.
*/
this.bitsState = 0;
if (this.config.INIT && !this.restore(this.config.INIT)) {
this.printf(MESSAGE.NOTICE, "reset error\n");
}
}
/**
* save()
*
* This implements save support for the Keyboard component.
*
* @this {KeyboardX80}
* @returns {Object}
*/
save()
{
let state = new State(this);
switch(this.config.MODEL) {
case KeyboardX80.SI1978.MODEL:
break;
case KeyboardX80.VT100.MODEL:
state.set(0, [this.bVT100Status, this.bVT100Address, this.fVT100UARTBusy, this.nVT100UARTCycleSnap, -1]);
break;
}
return state.data();
}
/**
* restore(data)
*
* This implements restore support for the Keyboard component.
*
* @this {KeyboardX80}
* @param {Object} data
* @returns {boolean} true if successful, false if failure
*/
restore(data)
{
let a;
if (data && (a = data[0]) && a.length) {
switch(this.config.MODEL) {
case KeyboardX80.SI1978.MODEL:
return true;
case KeyboardX80.VT100.MODEL:
this.bVT100Status = a[0];
this.updateLEDs(this.bVT100Status & KeyboardX80.VT100.STATUS.LEDS);
this.bVT100Address = a[1];
this.fVT100UARTBusy = a[2];
this.nVT100UARTCycleSnap = a[3];
this.iKeyNext = a[4];
return true;
}
}
return false;
}
/**
* setLED(control, f, color)
*
* TODO: Add support for user-definable LED colors
*
* @this {KeyboardX80}
* @param {Object} control is an HTML control DOM object
* @param {boolean|number} f is true if the LED represented by control should be "on", false if "off"
* @param {number} color (ie, 0xff0000 for RED, or 0x00ff00 for GREEN)
*/
setLED(control, f, color)
{
control.style.backgroundColor = (f? ('#' + StrLib.toHex(color, 6)) : "#000000");
}
/**
* updateLEDs(bLEDs)
*
* @this {KeyboardX80}
* @param {number} [bLEDs]
*/
updateLEDs(bLEDs)
{
let id, control;
if (bLEDs != null) {
this.bLEDs = bLEDs;
} else {
bLEDs = this.bLEDs;
}
for (let sBinding in this.config.LEDCODES) {
id = "led-" + sBinding;
control = this.bindings[id];
if (control) {
let bitLED = this.config.LEDCODES[sBinding];
let fOn = !!(bLEDs & bitLED);
if (bitLED & (bitLED-1)) {
fOn = !(bLEDs & ~bitLED);
}
this.setLED(control, fOn, 0xff0000);
}
}
id = "led-caps-lock";
control = this.bindings[id];
if (control) {
this.setLED(control, (this.bitsState & KeyboardX80.STATE.CAPS_LOCK), 0x00ff00);
}
}
/**
* checkModifierKeys(keyCode, fDown, fRight)
*
* @this {KeyboardX80}
* @param {number} keyCode (ie, either a keycode or string ID)
* @param {boolean} fDown (true if key going down, false if key going up)
* @param {boolean} [fRight] (true if key is on the right, false if not or unknown or n/a)
* @returns {boolean} (fDown updated as needed for CAPS-LOCK weirdness)
*/
checkModifierKeys(keyCode, fDown, fRight)
{
let bit = 0;
switch(keyCode) {
case Keys.KEYCODE.SHIFT:
bit = fRight? KeyboardX80.STATE.RSHIFT : KeyboardX80.STATE.SHIFT;
break;
case Keys.KEYCODE.CTRL:
bit = fRight? KeyboardX80.STATE.RCTRL : KeyboardX80.STATE.CTRL;
break;
case Keys.KEYCODE.ALT:
bit = fRight? KeyboardX80.STATE.RALT : KeyboardX80.STATE.ALT;
break;
case Keys.KEYCODE.CMD:
bit = fRight? KeyboardX80.STATE.RCMD : KeyboardX80.STATE.CMD;
break;
case Keys.KEYCODE.CAPS_LOCK:
bit = KeyboardX80.STATE.CAPS_LOCK;
/*
* WARNING: You have an entered a browser weirdness zone. In Chrome, pressing-and-releasing
* CAPS-LOCK generates a "down" event when it turns the lock on and an "up" event when it turns
* the lock off. Firefox, OTOH, generates only "down" events, so we have to "manufacture"
* the fDown parameter ourselves -- which means we also have to propagate it back to the caller.
*
* And, while this isn't necessary for Chrome, it doesn't appear to hurt anything in Chrome, so
* we're not going to bother making it browser-specific.
*/
fDown = !(this.bitsState & bit);
break;
}
if (bit) {
if (fDown) {
this.bitsState |= bit;
} else {
this.bitsState &= ~bit;
}
}
return fDown;
}
/**
* getSoftCode(keyCode)
*
* Returns a number if the keyCode exists in the KEYMAP, or a string if the keyCode has a string ID.
*
* @this {KeyboardX80}
* @returns {string|number|null}
*/
getSoftCode(keyCode)
{
keyCode = this.config.ALTCODES[keyCode] || keyCode;
if (this.config.KEYMAP[keyCode]) {
return keyCode;
}
for (let sSoftCode in this.config.SOFTCODES) {
if (this.config.SOFTCODES[sSoftCode] === keyCode) {
return sSoftCode;
}
}
return null;
}
/**
* onKeyDown(event, fDown)
*
* @this {KeyboardX80}
* @param {Object} event
* @param {boolean} fDown is true for a keyDown event, false for up
* @returns {boolean} true to pass the event along, false to consume it
*/
onKeyDown(event, fDown)
{
let fPass = true;
let keyCode = event.keyCode;
this.printf(MESSAGE.KEY, "onKey%s(%d)\n", (fDown? "Down" : "Up"), keyCode);
/*
* A note about Firefox: it uses different keyCodes for certain keys; there's a logic to the differences
* (they use ASCII codes), but since other browsers didn't follow suit, we must use a mapping table to
* convert their keyCodes to the more traditional values.
*/
keyCode = Keys.FF_KEYCODES[keyCode] || keyCode;
/*
* We now keep track of physical keyboard modifier keys. This makes it possible for new services
* to eventually be implemented (simulateKeysDown() and simulateKeysUp()), to map special ALT-key
* combinations to VT100 keys, etc.
*/
fDown = this.checkModifierKeys(keyCode, fDown, event.location == Keys.LOCATION.RIGHT);
let softCode = this.getSoftCode(keyCode);
if (softCode) {
/*
* Key combinations involving the "meta" key (ie, the Windows or Command key) are meaningless to
* the VT100, so we ignore them. The "meta" key itself is already effectively ignored, because it's
* not acknowledged by getSoftCode(), but we also don't want any of the keys combined with "meta"
* slipping through either.
*/
if (!event.metaKey) {
/*
* The LINE-FEED key is an important key on the VT100, and while we DO map a host function key
* to it (F7), I like the idea of making ALT-ENTER an alias for LINE-FEED as well. Ditto for
* making ALT-DELETE an alias for BACKSPACE (and no, I don't mean ALT-BACKSPACE as an alias for
* DELETE; see my earlier discussion involving BACKSPACE and DELETE).
*
* Of course, as experienced VT100 users know, it's always possible to type CTRL-J for LINE-FEED
* and CTRL-H for BACKSPACE, too. But not all our users are that experienced.
*
* I was also tempted to use CTRL-ENTER or SHIFT-ENTER, but those are composable VT100 key
* sequences, so it's best not to muck with those.
*
* Finally, this hack is complicated by the fact that if the ALT key is released first, we run
* the risk of the remapped key being stuck "down". Hence the new REMAPPED bit, which should
* remain set (as a "proxy" for the ALT bit) as long as a remapped key is down.
*/
let fRemapped = false;
if (this.bitsState & (KeyboardX80.STATE.ALTS | KeyboardX80.STATE.REMAPPED)) {
if (softCode == Keys.KEYCODE.CR) {
softCode = Keys.KEYCODE.F7;
fRemapped = true;
}
else if (softCode == Keys.KEYCODE.BS) {
softCode = Keys.KEYCODE.DEL;
fRemapped = true;
}
if (fRemapped) {
if (fDown) {
this.bitsState |= KeyboardX80.STATE.REMAPPED;
} else {
this.bitsState &= ~KeyboardX80.STATE.REMAPPED;
}
}
}
fPass = this.onSoftKeyDown(softCode, fDown);
/*
* As onKeyPress() explains, the only key presses we're interested in are letters, which provide
* an important clue regarding the CAPS-LOCK state. For all other keys, we call preventDefault(),
* which normally "suppresses" the keyPress event, as well as other unwanted browser behaviors
* (eg, the SPACE key, which browsers interpret as a desire to scroll the entire web page down).
*
* And, even if the key IS a letter, we STILL want to call preventDefault() if a CTRL key is down,
* so that Windows-based browsers (eg, Edge) don't interfere with their stupid CTRL-based shortcuts. ;-)
*
* NOTE: We COULD check event.ctrlKey too, but it's six of one, half a dozen of another.
*/
if (!(softCode >= Keys.ASCII.A && softCode <= Keys.ASCII.Z) || (this.bitsState | KeyboardX80.STATE.CTRLS)) {
if (event.preventDefault) event.preventDefault();
}
}
}
this.printf(MESSAGE.KEY, "onKey%s(%d): softCode=%s, pass=%b\n", (fDown? "Down" : "Up"), keyCode, softCode, fPass);
return fPass;
}
/**
* onKeyPress(event)
*
* For now, our only interest in keyPress events is letters, as a means of detecting the CAPS-LOCK state.
*
* @this {KeyboardX80}
* @param {Object} event
* @returns {boolean} true to pass the event along, false to consume it
*/
onKeyPress(event)
{
/*
* A note about Firefox: the KeyboardEvent they pass to a keypress handler doesn't set 'keyCode', so
* we have to fallback to 'charCode' (or 'which'), both of which are deprecated but realistically can't
* really go away.
*
* TODO: Consider "upgrading" this code to use the new 'key' property. Note, however, that it's a string,
* not a number; for example; if the colon key is pressed, 'key' will be ":", whereas 'charCode' and 'which'
* will be 58.
*/
let charCode = event.keyCode || event.charCode;
if (charCode >= Keys.ASCII.A && charCode <= Keys.ASCII.Z) {
if (!(this.bitsState & (KeyboardX80.STATE.SHIFTS | KeyboardX80.STATE.CAPS_LOCK))) {
this.bitsState |= KeyboardX80.STATE.CAPS_LOCK;
this.onSoftKeyDown(Keys.KEYCODE.CAPS_LOCK, true);
this.updateLEDs();
}
}
else if (charCode >= Keys.ASCII.a && charCode <= Keys.ASCII.z) {
if (this.bitsState & KeyboardX80.STATE.CAPS_LOCK) {
this.bitsState &= ~KeyboardX80.STATE.CAPS_LOCK;
this.onSoftKeyDown(Keys.KEYCODE.CAPS_LOCK, false);
this.updateLEDs();
}
}
this.printf(MESSAGE.KEY, "onKeyPress(%d)\n", charCode);
return true;
}
/**
* oniOSKeyDown(event, fDown)
*
* @this {KeyboardX80}
* @param {Object} event
* @param {boolean} fDown is true for a keyDown event, false for up
* @returns {boolean} true to pass the event along, false to consume it
*/
oniOSKeyDown(event, fDown)
{
let fPass = true;
/*
* Because keydown/keyup events on iOS are inherently "fake", they can be delivered so quickly that
* if we generated matching down/up events, then the emulated machine might not see the key transition.
* So we now deliver only down events, with fAutoRelease always set (see below).
*
* Also, because of iOS weirdness discussed in setBinding() when using a physical keyboard, the keyup
* event may not provide a valid keyCode, which is another reason we have no choice but to always deliver
* keys with fAutoRelease set to true.
*/
if (fDown) {
let keyCode = event.keyCode;
let bMapping = this.config.KEYMAP[keyCode];
if (bMapping) {
/*
* If this is a mappable key, but the mapping isn't in the CHARMAP table, then we have to process
* it now; the most common reason is that the key doesn't generate a keypress event (eg, BACKSPACE).
*/
if (!this.indexOfCharMap(bMapping)) {
fPass = this.onSoftKeyDown(keyCode, fDown, true);
if (event.preventDefault) event.preventDefault();
this.printf(MESSAGE.KEY, "oniOSKey%s(%d): pass=%b\n", (fDown ? "Down" : "Up"), keyCode, fPass);
}
}
}
return fPass;
}
/**
* oniOSKeyPress(event)
*
* @this {KeyboardX80}
* @param {Object} event
* @returns {boolean} true to pass the event along, false to consume it
*/
oniOSKeyPress(event)
{
/*
* A note about Firefox: the KeyboardEvent they pass to a keypress handler doesn't set 'keyCode', so
* we have to fallback to 'charCode' (or 'which'), both of which are deprecated but realistically can't
* really go away.
*
* TODO: Consider "upgrading" this code to use the new 'key' property. Note, however, that it's a string,
* not a number; for example; if the colon key is pressed, 'key' will be ":", whereas 'charCode' and 'which'
* will be 58.
*/
let charCode = event.keyCode || event.charCode;
let fShifted = false;
let bMapping = this.config.CHARMAP[charCode];
if (bMapping) {
if (bMapping & 0x80) {
bMapping &= 0x7f;
fShifted = true;
}
/*
* Since the rest of our code was built around keyCodes, not charCodes, we look up the CHARMAP byte
* in the KEYMAP table to find a corresponding keyCode, and that's what we'll use to simulate the key
* press/release.
*/
let softCode = this.indexOfKeyMap(bMapping);
if (softCode) {
if (!fShifted) {
this.onSoftKeyDown(Keys.KEYCODE.SHIFT, false);
} else {
this.onSoftKeyDown(Keys.KEYCODE.SHIFT, true, true);
}
this.onSoftKeyDown(softCode, true, true);
}
}
this.printf(MESSAGE.KEY, "oniOSKeyPress(%d)\n", charCode);
return true;
}
/**
* onPaste(event)
*
* @this {KeyboardX80}
* @param {Object} event
* @returns {boolean} true to pass the event along, false to consume it
*/
onPaste(event)
{
/*
* TODO: In a perfect world, we would have implemented simulateKeysDown() and simulateKeysUp(),
* which would transform any given text into the appropriate keystrokes. But for now, we're going
* to leapfrog all that and try invoking the SerialPort's sendData() function, which if available,
* is nothing more than a call into a connected machine's receiveData() function.
*
* Besides, paste functionality doesn't seem to be consistently implemented across all browsers
* (partly out of security concerns, apparently) so it may not make sense to expend much more
* effort on this right now. If you want to paste a lot of text into a machine, you're better off
* pasting into a machine that's been configured to use a textarea as part of its Control Panel.
* A visible textarea seems to have less issues than the hidden textarea overlaid on top of our
* Video display.
*/
if (this.serial && this.serial.sendData) {
if (event.stopPropagation) event.stopPropagation();
if (event.preventDefault) event.preventDefault();
let clipboardData = event.clipboardData || globals.window.clipboardData;
if (clipboardData) {
this.serial.transmitData(clipboardData.getData('Text'));
return false;
}
}
return true;
}
/**
* indexOfKeyMap(bMapping)
*
* @this {KeyboardX80}
* @param {number} bMapping
* @returns {number}
*/
indexOfKeyMap(bMapping)
{
for (let keyCode in this.config.KEYMAP) {
if (this.config.KEYMAP[keyCode] == bMapping) return +keyCode;
}
return 0;
}
/**
* indexOfCharMap(bMapping)
*
* @this {KeyboardX80}
* @param {number} bMapping
* @returns {number}
*/
indexOfCharMap(bMapping)
{
for (let charCode in this.config.CHARMAP) {
if (this.config.CHARMAP[charCode] == bMapping) return +charCode;
}
return 0;
}
/**
* indexOfSoftKey(softCode)
*
* @this {KeyboardX80}
* @param {number|string} softCode
* @returns {number} index of softCode in aKeysActive, or -1 if not found
*/
indexOfSoftKey(softCode)
{
for (let i = 0; i < this.aKeysActive.length; i++) {
if (this.aKeysActive[i].softCode == softCode) return i;
}
return -1;
}
/**
* onSoftKeyDown(softCode, fDown, fAutoRelease)
*
* @this {KeyboardX80}
* @param {number|string} softCode
* @param {boolean} fDown is true for a down event, false for up
* @param {boolean} [fAutoRelease] is true only if we know we want the key to auto-release
* @returns {boolean} true to pass the event along, false to consume it
*/
onSoftKeyDown(softCode, fDown, fAutoRelease)
{
let i = this.indexOfSoftKey(softCode);
if (fDown) {
// this.printf("%s down\n", softCode);
if (i < 0) {
this.aKeysActive.push({
softCode: softCode,
msDown: Date.now(),
fAutoRelease: fAutoRelease || false
});
} else {
this.aKeysActive[i].msDown = Date.now();
this.aKeysActive[i].fAutoRelease = fAutoRelease || false;
}
if (fAutoRelease) this.checkSoftKeysToRelease(); // prime the pump
} else if (i >= 0) {
// this.printf("%s up\n", softCode);
if (!this.aKeysActive[i].fAutoRelease) {
let msDown = this.aKeysActive[i].msDown;
if (msDown) {
let msElapsed = Date.now() - msDown;
if (msElapsed < KeyboardX80.MINPRESSTIME) {
// this.printf("%s released after only %dms\n", softCode, msElapsed);
this.aKeysActive[i].fAutoRelease = true;
this.checkSoftKeysToRelease();
return true;
}
}
}
this.aKeysActive.splice(i, 1);
} else {
// this.printf("%s up with no down?\n", softCode);
}
if (this.chipset) {
let bit = 0;
switch(softCode) {
case '1p':
bit = ChipSetX80.SI1978.STATUS1.P1;
break;
case '2p':
bit = ChipSetX80.SI1978.STATUS1.P2;
break;
case 'coin':
bit = ChipSetX80.SI1978.STATUS1.CREDIT;
break;
case 'left':
bit = ChipSetX80.SI1978.STATUS1.P1_LEFT;
break;
case 'right':
bit = ChipSetX80.SI1978.STATUS1.P1_RIGHT;
break;
case 'fire':
bit = ChipSetX80.SI1978.STATUS1.P1_FIRE;
break;
}
if (bit) {
this.chipset.updateStatus1(bit, fDown);
}
}
return true;
}
/**
* checkSoftKeysToRelease()
*
* @this {KeyboardX80}
*/
checkSoftKeysToRelease()
{
let i = 0;
let msDelayMin = -1;
while (i < this.aKeysActive.length) {
if (this.aKeysActive[i].fAutoRelease) {
let softCode = this.aKeysActive[i].softCode;
let msDown = this.aKeysActive[i].msDown;
let msElapsed = Date.now() - msDown;
let msDelay = KeyboardX80.MINPRESSTIME - msElapsed;
if (msDelay > 0) {
if (msDelayMin < 0 || msDelayMin > msDelay) {
msDelayMin = msDelay;
}
} else {
/*
* Because the key is already in the auto-release state, this next call guarantees that the
* key will be removed from the array; a consequence of that removal, however, is that we must
* reset our array index to zero.
*/
this.onSoftKeyDown(softCode, false);
i = 0;
continue;
}
}
i++;
}
if (msDelayMin >= 0) {
/*
* Replaced the klunky browser setTimeout() call with our own timer service.
*
* var kbd = this;
* setTimeout(function() { kbd.checkSoftKeysToRelease(); }, msDelayMin);
*/
this.cpu.setTimer(this.timerReleaseKeys, msDelayMin);
}
}
/**
* isVT100TransmitterReady()
*
* Called whenever the VT100 ChipSet circuit needs the Keyboard UART's transmitter status.
*
* From p. 4-32 of the VT100 Technical Manual (July 1982):
*
* The operating clock for the keyboard interface comes from an address line in the video processor (LBA4).
* This signal has an average period of 7.945 microseconds. Each data byte is transmitted with one start bit
* and one stop bit, and each bit lasts 16 clock periods. The total time for each data byte is 160 times 7.945
* or 1.27 milliseconds. Each time the Transmit Buffer Empty flag on the terminal's UART gets set (when the
* current byte is being transmitted), the microprocessor loads another byte into the transmit buffer. In this
* way, the stream of status bytes to the keyboard is continuous.
*
* We used to always return true (after all, what's wrong with an infinitely fast UART?), but unfortunately,
* the VT100 firmware relies on the UART's slow transmission speed to drive cursor blink rate. We have several
* options:
*
* 1) Snapshot the CPU cycle count each time a byte is transmitted (see outVT100UARTStatus()) and then every
* time this is polled, see if the cycle count has exceeded the snapshot value by the necessary threshold;
* if we assume 361.69ns per CPU cycle, there are 22 CPU cycles for every 1 LBA4 cycle, and since transmission
* time is supposed to last for 160 LBA4 cycles, the threshold is 22*160 CPU cycles, or 3520 cycles.
*
* 2) Set a CPU timer using the new setTimer() interface, which can be passed the number of milliseconds to
* wait before firing (in this case, roughly 1.27ms).
*
* 3) Call the ChipSet's getVT100LBA(4) function for the state of the simulated LBA4, and count 160 LBA4
* transitions; however, that would be the worst solution, because there's no guarantee that the firmware's
* UART polling will occur regularly and/or frequently enough for us to catch every LBA4 transition.
*
* I'm going with solution #1 because it's less overhead.
*
* @this {KeyboardX80}
* @returns {boolean} (true if ready, false if not)
*/
isVT100TransmitterReady()
{
if (this.fVT100UARTBusy) {
/*
* NOTE: getMSCycles(1.2731488) should work out to 3520 cycles for a CPU clocked at 361.69ns per cycle,
* which is roughly 2.76Mhz. We could just hard-code 3520 instead of calling getMSCycles(), but this helps
* maintain a reasonable blink rate for the cursor even when the user cranks up the CPU speed.
*/
if (this.cpu.getCycles() >= this.nVT100UARTCycleSnap + this.cpu.getMSCycles(1.2731488)) {
this.fVT100UARTBusy = false;
}
}
return !this.fVT100UARTBusy;
}
/**
* inVT100UARTAddress(port, addrFrom)
*
* We take our cue from iKeyNext. If it's -1 (default), we simply return the last value latched
* in bVT100Address. Otherwise, if iKeyNext is a valid index into aKeysActive, we look up the key
* in the VT100.KEYMAP, latch it, and increment iKeyNext. Failing that, we latch KeyboardX80.VT100.KEYLAST
* and reset iKeyNext to -1.
*
* @this {KeyboardX80}
* @param {number} port (0x82)
* @param {number} [addrFrom] (not defined if the Debugger is trying to write the specified port)
* @returns {number} simulated port value
*/
inVT100UARTAddress(port, addrFrom)
{
let b = this.bVT100Address;
if (this.iKeyNext >= 0) {
if (this.iKeyNext < this.aKeysActive.length) {
let key = this.aKeysActive[this.iKeyNext];
if (!MAXDEBUG) {
this.iKeyNext++;
} else {
/*
* In MAXDEBUG builds, this code removes the key as soon as it's been reported, because
* when debugging, it's easy for the window to lose focus and never receive the keyUp event,
* thereby leaving us with a stuck key. However, this may cause more problems than it solves,
* because the VT100's ROM seems to require that key presses persist for more than a single poll.
*/
this.aKeysActive.splice(this.iKeyNext, 1);
}
b = KeyboardX80.VT100.KEYMAP[key.softCode];
if (b & 0x80) {
/*
* TODO: This code is supposed to be accompanied by a SHIFT key; make sure that it is.
*/
b &= 0x7F;
}
} else {
this.iKeyNext = -1;
b = KeyboardX80.VT100.KEYLAST;
}