jsbeeb 1.22.4 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +16 -11
- package/src/6502.js +39 -42
- package/src/6502.opcodes.js +198 -123
- package/src/6847.js +4 -5
- package/src/6847_fontdata.js +0 -2
- package/src/acia.js +2 -3
- package/src/app/app.js +0 -1
- package/src/app/args.js +0 -2
- package/src/app/electron.js +4 -6
- package/src/app-bench.js +1 -3
- package/src/archive.js +243 -0
- package/src/basic-loader.js +0 -7
- package/src/basic-tokenise.js +19 -6
- package/src/bbc-palette.js +0 -2
- package/src/bbcdiscs.js +0 -2
- package/src/bem-snapshot.js +1 -3
- package/src/binary.js +175 -0
- package/src/cmos.js +0 -2
- package/src/ddnoise.js +0 -1
- package/src/debounce.js +7 -0
- package/src/disc-surface.js +0 -2
- package/src/disc.js +10 -9
- package/src/fake6502.js +7 -23
- package/src/fdc.js +9 -51
- package/src/filestore.js +10 -17
- package/src/hex.js +38 -0
- package/src/index.js +11 -0
- package/src/intel-fdc.js +18 -20
- package/src/jsbeeb.css +33 -0
- package/src/{utils_atom.js → keymap-atom.js} +2 -18
- package/src/{utils.js → keymap.js} +47 -610
- package/src/loader.js +63 -0
- package/src/machine-session.js +37 -177
- package/src/machine-spec.js +59 -0
- package/src/main.js +69 -184
- package/src/media-resolver.js +68 -0
- package/src/mmc.js +3 -2
- package/src/models.js +40 -4
- package/src/music5000.js +0 -2
- package/src/ppia.js +3 -4
- package/src/printer.js +7 -14
- package/src/relaynoise.js +0 -1
- package/src/rewind.js +0 -2
- package/src/sample-player.js +2 -3
- package/src/serial.js +0 -2
- package/src/snapshot-helpers.js +0 -2
- package/src/snapshot.js +0 -2
- package/src/soundchip.js +1 -1
- package/src/state-utils.js +0 -2
- package/src/sth.js +2 -4
- package/src/tapes.js +10 -4
- package/src/teletext.js +2 -3
- package/src/teletext_adaptor.js +2 -4
- package/src/teletext_data.js +0 -2
- package/src/test-machine.js +307 -0
- package/src/touchscreen.js +2 -4
- package/src/tube.js +5 -6
- package/src/typist.js +103 -0
- package/src/uef-snapshot.js +0 -2
- package/src/url-params.js +4 -5
- package/src/vdu-capture.js +113 -0
- package/src/via.js +3 -4
- package/src/video-filters/pal-composite.js +0 -2
- package/src/video-filters/passthrough-filter.js +0 -2
- package/src/video-filters/pixel-grid.js +0 -2
- package/src/video-filters/shader-program.js +0 -2
- package/src/video-filters/xbr-filter.js +0 -2
- package/src/video.js +12 -11
- package/src/wd-fdc.js +20 -20
- package/src/web/analogue-inputs.js +6 -8
- package/src/web/analytics.js +13 -0
- package/src/web/audio-handler.js +2 -2
- package/src/{audio-utils.js → web/audio-utils.js} +0 -2
- package/src/web/autoboot.js +10 -13
- package/src/{canvas.js → web/canvas.js} +4 -5
- package/src/web/config.js +42 -72
- package/src/web/console-surface.js +25 -0
- package/src/web/debug.js +8 -49
- package/src/web/disc-visualiser.js +0 -2
- package/src/web/display.js +21 -4
- package/src/{dom-utils.js → web/dom-utils.js} +1 -11
- package/src/web/drives.js +1 -1
- package/src/web/emulation-loop.js +54 -33
- package/src/web/front-panel.js +13 -12
- package/src/{gamepads.js → web/gamepads.js} +2 -6
- package/src/web/google-drive-picker.js +14 -9
- package/src/{google-drive.js → web/google-drive.js} +9 -10
- package/src/web/hfe-picker.js +5 -4
- package/src/web/icons.js +42 -0
- package/src/web/keyboard-setup.js +29 -40
- package/src/{keyboard.js → web/keyboard.js} +18 -119
- package/src/web/layout.js +4 -0
- package/src/web/local-disc.js +44 -0
- package/src/web/machine.js +27 -20
- package/src/web/media-loader.js +38 -147
- package/src/{microphone-input.js → web/microphone-input.js} +1 -1
- package/src/web/modals.js +6 -9
- package/src/web/page-actions.js +88 -0
- package/src/web/quick-settings.js +16 -15
- package/src/web/reporting.js +0 -7
- package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
- package/src/web/rewind-ui.js +29 -29
- package/src/web/run-controls.js +39 -0
- package/src/web/settings.js +64 -135
- package/src/web/snapshot-ui.js +10 -16
- package/src/{speech-output.js → web/speech-output.js} +0 -2
- package/src/web/sth-picker.js +9 -7
- package/src/web/url-state.js +20 -6
- package/src/econet/L3FS.dat +0 -0
- package/src/econet/scsi.dat +0 -0
- package/tests/test-machine.js +0 -609
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import * as utils from "../utils.js";
|
|
2
1
|
import * as bootstrap from "bootstrap";
|
|
3
2
|
import * as disc from "../fdc.js";
|
|
4
|
-
import { GoogleDriveLoader } from "
|
|
3
|
+
import { GoogleDriveLoader } from "./google-drive.js";
|
|
5
4
|
import { toast } from "./toast.js";
|
|
6
5
|
import { errorText } from "./reporting.js";
|
|
6
|
+
import { replaceOrAddExtension } from "../archive.js";
|
|
7
|
+
import { noteEvent } from "./analytics.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The Google Drive picker: signing in, listing the user's discs, loading one
|
|
@@ -32,8 +33,12 @@ export class GoogleDrivePicker {
|
|
|
32
33
|
else this.authReject?.(new Error("Unable to authorize Google Drive"));
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
// Closing the loading dialog
|
|
36
|
-
|
|
36
|
+
// Closing the loading dialog puts the auth panel away; while the panel
|
|
37
|
+
// waits, that close is a cancellation, not an error.
|
|
38
|
+
document.getElementById("loading-dialog").addEventListener("hidden.bs.modal", () => {
|
|
39
|
+
this.authEl.style.display = "none";
|
|
40
|
+
this.authResolve?.(false);
|
|
41
|
+
});
|
|
37
42
|
|
|
38
43
|
// Loading the Google client holds the main thread for ~100ms, so it waits for
|
|
39
44
|
// someone to ask for Drive.
|
|
@@ -94,7 +99,7 @@ export class GoogleDrivePicker {
|
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
|
|
97
|
-
const ssd = await this.googleDrive.load(
|
|
102
|
+
const ssd = await this.googleDrive.load(cat.id, layout);
|
|
98
103
|
console.log("Google Drive loading finished");
|
|
99
104
|
this.modals.loadingFinished();
|
|
100
105
|
if (!ssd.savesChanges) {
|
|
@@ -133,7 +138,7 @@ export class GoogleDrivePicker {
|
|
|
133
138
|
dbList.appendChild(row);
|
|
134
139
|
row.querySelector(".name").textContent = item.name;
|
|
135
140
|
row.addEventListener("click", async () => {
|
|
136
|
-
|
|
141
|
+
noteEvent("google-drive", "click", item.name);
|
|
137
142
|
this.media.setDisc1Image(`gd:${item.id}/${item.name}`);
|
|
138
143
|
this.modal.hide();
|
|
139
144
|
try {
|
|
@@ -165,10 +170,10 @@ export class GoogleDrivePicker {
|
|
|
165
170
|
this.modals.loadingFinished(`Unable to create ${name} on Google Drive: ${errorText(e)}`);
|
|
166
171
|
return;
|
|
167
172
|
}
|
|
168
|
-
name =
|
|
173
|
+
name = replaceOrAddExtension(name, discType.extension);
|
|
169
174
|
console.log(`Saving existing disc: ${name}`);
|
|
170
175
|
} else {
|
|
171
|
-
// TODO
|
|
176
|
+
// TODO(#1070) blank HFE images have no fixed byteSize, so only SSD and DSD blanks can be made here.
|
|
172
177
|
const discType = disc.guessDiscTypeFromName(name);
|
|
173
178
|
if (!discType.byteSize) {
|
|
174
179
|
this.modals.loadingFinished(
|
|
@@ -184,7 +189,7 @@ export class GoogleDrivePicker {
|
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
try {
|
|
187
|
-
const result = await this.googleDrive.create(
|
|
192
|
+
const result = await this.googleDrive.create(name, data);
|
|
188
193
|
this.media.setDisc1Image("gd:" + result.fileId + "/" + name);
|
|
189
194
|
this.drives.putDiscIn(0, result.disc);
|
|
190
195
|
this.modals.loadingFinished();
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { debounce
|
|
4
|
-
import { discFor } from "./fdc.js";
|
|
1
|
+
import { discFor } from "../fdc.js";
|
|
2
|
+
import { uint8ArrayToString } from "../binary.js";
|
|
3
|
+
import { debounce } from "../debounce.js";
|
|
5
4
|
|
|
6
5
|
const MIME_TYPE = "application/vnd.jsbeeb.disc-image";
|
|
7
6
|
const CLIENT_ID = "356883185894-bhim19837nroivv18p0j25gecora60r5.apps.googleusercontent.com";
|
|
@@ -138,14 +137,14 @@ export class GoogleDriveLoader {
|
|
|
138
137
|
});
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
async create(
|
|
140
|
+
async create(name, data) {
|
|
142
141
|
console.log(`Google Drive: creating disc image: '${name}'`);
|
|
143
142
|
const response = await this.saveFile(name, data);
|
|
144
143
|
const meta = response.result;
|
|
145
|
-
return { fileId: meta.id, disc: this.makeDisc(
|
|
144
|
+
return { fileId: meta.id, disc: this.makeDisc(data, meta) };
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
makeDisc(
|
|
147
|
+
makeDisc(data, meta, layout) {
|
|
149
148
|
let flusher = null;
|
|
150
149
|
const name = meta.name;
|
|
151
150
|
const id = meta.id;
|
|
@@ -159,12 +158,12 @@ export class GoogleDriveLoader {
|
|
|
159
158
|
} else {
|
|
160
159
|
console.log("Making read-only disc");
|
|
161
160
|
}
|
|
162
|
-
return discFor(
|
|
161
|
+
return discFor(name, data, flusher, layout);
|
|
163
162
|
}
|
|
164
163
|
|
|
165
|
-
async load(
|
|
164
|
+
async load(fileId, layout) {
|
|
166
165
|
const meta = (await this.driveClient.files.get({ fileId, fields: FILE_FIELDS })).result;
|
|
167
166
|
const data = (await this.driveClient.files.get({ fileId, alt: "media" })).body;
|
|
168
|
-
return this.makeDisc(
|
|
167
|
+
return this.makeDisc(data, meta, layout);
|
|
169
168
|
}
|
|
170
169
|
}
|
package/src/web/hfe-picker.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import * as utils from "../utils.js";
|
|
2
1
|
import * as bootstrap from "bootstrap";
|
|
3
2
|
import { BbcDiscArchive, Provenance, describe as describeHfe, matches, provenancesIn } from "../bbcdiscs.js";
|
|
4
3
|
import { errorText } from "./reporting.js";
|
|
5
4
|
import { clearArchiveList, showArchiveMessage } from "./archive-list.js";
|
|
5
|
+
import { noteEvent } from "./analytics.js";
|
|
6
6
|
|
|
7
7
|
const HfeProvenanceLabels = {
|
|
8
8
|
[Provenance.Captured]: ["Captured", "Direct from disc"],
|
|
@@ -57,15 +57,16 @@ export class HfePicker {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
async pick(file) {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
noteEvent("hfe", "click", file.path);
|
|
61
|
+
const image = "hfe:" + file.path;
|
|
62
|
+
this.media.setDisc1Image(image);
|
|
62
63
|
const needsAutoboot = this.urlState.params.autoboot !== undefined;
|
|
63
64
|
if (needsAutoboot) this.processor.reset(true);
|
|
64
65
|
|
|
65
66
|
const name = describeHfe(file).title;
|
|
66
67
|
this.modals.popupLoading("Loading " + name);
|
|
67
68
|
try {
|
|
68
|
-
const loaded = await this.media.loadDiscImage(
|
|
69
|
+
const loaded = await this.media.loadDiscImage(image, this.drives.layoutForDrive(0));
|
|
69
70
|
this.drives.putDiscIn(0, loaded);
|
|
70
71
|
this.modals.loadingFinished();
|
|
71
72
|
if (needsAutoboot) this.autoboot(name);
|
package/src/web/icons.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import arrowCounterclockwise from "bootstrap-icons/icons/arrow-counterclockwise.svg?raw";
|
|
2
|
+
import cassette from "bootstrap-icons/icons/cassette.svg?raw";
|
|
3
|
+
import clockHistory from "bootstrap-icons/icons/clock-history.svg?raw";
|
|
4
|
+
import display from "bootstrap-icons/icons/display.svg?raw";
|
|
5
|
+
import floppy from "bootstrap-icons/icons/floppy.svg?raw";
|
|
6
|
+
import gear from "bootstrap-icons/icons/gear.svg?raw";
|
|
7
|
+
import headphones from "bootstrap-icons/icons/headphones.svg?raw";
|
|
8
|
+
import pauseFill from "bootstrap-icons/icons/pause-fill.svg?raw";
|
|
9
|
+
import playFill from "bootstrap-icons/icons/play-fill.svg?raw";
|
|
10
|
+
import soundwave from "bootstrap-icons/icons/soundwave.svg?raw";
|
|
11
|
+
import stars from "bootstrap-icons/icons/stars.svg?raw";
|
|
12
|
+
import threeDots from "bootstrap-icons/icons/three-dots.svg?raw";
|
|
13
|
+
import tv from "bootstrap-icons/icons/tv.svg?raw";
|
|
14
|
+
import volumeUp from "bootstrap-icons/icons/volume-up.svg?raw";
|
|
15
|
+
|
|
16
|
+
/** The Bootstrap Icons the page uses, by their upstream names; nothing else of the set is bundled. */
|
|
17
|
+
const Icons = {
|
|
18
|
+
"arrow-counterclockwise": arrowCounterclockwise,
|
|
19
|
+
cassette,
|
|
20
|
+
"clock-history": clockHistory,
|
|
21
|
+
display,
|
|
22
|
+
floppy,
|
|
23
|
+
gear,
|
|
24
|
+
headphones,
|
|
25
|
+
"pause-fill": pauseFill,
|
|
26
|
+
"play-fill": playFill,
|
|
27
|
+
soundwave,
|
|
28
|
+
stars,
|
|
29
|
+
"three-dots": threeDots,
|
|
30
|
+
tv,
|
|
31
|
+
"volume-up": volumeUp,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const IconNames = Object.freeze(Object.keys(Icons));
|
|
35
|
+
|
|
36
|
+
/** Puts the named SVG inside every `[data-icon]` element under `root`; an unknown name leaves them all untouched. */
|
|
37
|
+
export function installIcons(root = document) {
|
|
38
|
+
const placeholders = [...root.querySelectorAll("[data-icon]")];
|
|
39
|
+
const unknown = placeholders.find((el) => !(el.dataset.icon in Icons));
|
|
40
|
+
if (unknown) throw new Error(`No icon named ${unknown.dataset.icon}`);
|
|
41
|
+
for (const el of placeholders) el.innerHTML = Icons[el.dataset.icon];
|
|
42
|
+
}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Keyboard } from "../keyboard.js";
|
|
1
|
+
import { Keyboard } from "./keyboard.js";
|
|
3
2
|
import { showNotice } from "./reporting.js";
|
|
3
|
+
import { noteEvent } from "./analytics.js";
|
|
4
|
+
import { keyCodes } from "../keymap.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const PasteBoxId = "paste-text";
|
|
7
|
+
const TypingTargets = 'input, textarea, select, [contenteditable]:not([contenteditable="false"])';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds the emulated keyboard and wires the browser's shortcuts around it,
|
|
11
|
+
* exposing it as `keyboard` for whoever needs the machine's keys.
|
|
12
|
+
*/
|
|
6
13
|
export class KeyboardSetup {
|
|
7
14
|
/**
|
|
8
15
|
* @param {object} opts
|
|
9
16
|
* @param {object} opts.actions what each shortcut does, supplied late-bound:
|
|
10
17
|
* enterDebugger, reload, toggleFast, openRewind, openPrinter,
|
|
11
|
-
* pause, resume, onAnyKeyDown
|
|
18
|
+
* pause, resume, paste, onAnyKeyDown
|
|
12
19
|
* @param {import("./accessibility-switches.js").AccessibilitySwitches} opts.accessibilitySwitches
|
|
13
20
|
*/
|
|
14
21
|
constructor({ actions, accessibilitySwitches, processor, dbgr, keyLayout }) {
|
|
15
22
|
const keyboard = (this.keyboard = new Keyboard({
|
|
16
23
|
processor,
|
|
17
|
-
inputEnabledFunction: () => document.activeElement && document.activeElement.id ===
|
|
24
|
+
inputEnabledFunction: () => document.activeElement && document.activeElement.id === PasteBoxId,
|
|
18
25
|
keyLayout,
|
|
19
26
|
dbgr,
|
|
20
27
|
}));
|
|
@@ -23,28 +30,28 @@ export class KeyboardSetup {
|
|
|
23
30
|
keyboard.addEventListener("resume", () => actions.resume());
|
|
24
31
|
keyboard.addEventListener("break", (e) => {
|
|
25
32
|
// F12/Break: Reset processor
|
|
26
|
-
if (e.detail)
|
|
33
|
+
if (e.detail) noteEvent("keyboard", "press", "break");
|
|
27
34
|
});
|
|
28
35
|
|
|
29
36
|
const onDown = (note, action) => (down) => {
|
|
30
37
|
if (down) {
|
|
31
|
-
if (note)
|
|
38
|
+
if (note) noteEvent("keyboard", "press", note);
|
|
32
39
|
action();
|
|
33
40
|
}
|
|
34
41
|
};
|
|
35
42
|
const alt = { alt: true, ctrl: false };
|
|
36
43
|
const ctrl = { alt: false, ctrl: true };
|
|
37
|
-
keyboard.registerKeyHandler(
|
|
38
|
-
keyboard.registerKeyHandler(
|
|
39
|
-
keyboard.registerKeyHandler(
|
|
40
|
-
keyboard.registerKeyHandler(
|
|
44
|
+
keyboard.registerKeyHandler(keyCodes.S, onDown("S", actions.enterDebugger), alt);
|
|
45
|
+
keyboard.registerKeyHandler(keyCodes.R, onDown(null, actions.reload), alt);
|
|
46
|
+
keyboard.registerKeyHandler(keyCodes.HOME, onDown("home", actions.enterDebugger), ctrl);
|
|
47
|
+
keyboard.registerKeyHandler(keyCodes.INSERT, onDown("insert", actions.toggleFast), ctrl);
|
|
41
48
|
keyboard.registerKeyHandler(
|
|
42
|
-
|
|
49
|
+
keyCodes.END,
|
|
43
50
|
onDown("end", () => keyboard.pauseEmulation()),
|
|
44
51
|
ctrl,
|
|
45
52
|
);
|
|
46
|
-
keyboard.registerKeyHandler(
|
|
47
|
-
keyboard.registerKeyHandler(
|
|
53
|
+
keyboard.registerKeyHandler(keyCodes.PAGEDOWN, onDown("pagedown", actions.openRewind), alt);
|
|
54
|
+
keyboard.registerKeyHandler(keyCodes.B, onDown(null, actions.openPrinter), ctrl);
|
|
48
55
|
|
|
49
56
|
// Alt+1-8 and Alt+F1-F8 trigger the accessibility switches. Using Alt means
|
|
50
57
|
// the underlying key is never forwarded to the BBC Micro (keyboard.js bails
|
|
@@ -52,8 +59,8 @@ export class KeyboardSetup {
|
|
|
52
59
|
// works normally.
|
|
53
60
|
const handleSwitch = (index) => (down) => accessibilitySwitches.setSwitch(index, down);
|
|
54
61
|
for (let i = 0; i < 8; i++) {
|
|
55
|
-
keyboard.registerKeyHandler(
|
|
56
|
-
keyboard.registerKeyHandler(
|
|
62
|
+
keyboard.registerKeyHandler(keyCodes.K1 + i, handleSwitch(i), alt);
|
|
63
|
+
keyboard.registerKeyHandler(keyCodes.F1 + i, handleSwitch(i), alt);
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
document.addEventListener("keydown", (evt) => {
|
|
@@ -62,29 +69,11 @@ export class KeyboardSetup {
|
|
|
62
69
|
});
|
|
63
70
|
document.addEventListener("keypress", (evt) => keyboard.keyPress(evt));
|
|
64
71
|
document.addEventListener("keyup", (evt) => keyboard.keyUp(evt));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
clearKeys() {
|
|
72
|
-
this.keyboard.clearKeys();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
setKeyLayout(keyLayout) {
|
|
76
|
-
this.keyboard.setKeyLayout(keyLayout);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
resumeEmulation() {
|
|
80
|
-
this.keyboard.resumeEmulation();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
setRunning(running) {
|
|
84
|
-
this.keyboard.setRunning(running);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
postFrameShouldPause() {
|
|
88
|
-
return this.keyboard.postFrameShouldPause();
|
|
72
|
+
document.addEventListener("paste", (evt) => {
|
|
73
|
+
const target = document.activeElement;
|
|
74
|
+
if (target && target.id !== PasteBoxId && target.matches(TypingTargets)) return;
|
|
75
|
+
const text = evt.clipboardData?.getData("text/plain");
|
|
76
|
+
if (text) actions.paste(text);
|
|
77
|
+
});
|
|
89
78
|
}
|
|
90
79
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import { ATOM } from "./utils_atom.js";
|
|
1
|
+
import { keyCodes } from "../keymap.js";
|
|
2
|
+
import { Typist } from "../typist.js";
|
|
4
3
|
|
|
5
4
|
const isMac = typeof window !== "undefined" && /^Mac/i.test(window.navigator?.platform || "");
|
|
6
5
|
|
|
@@ -29,13 +28,8 @@ export class Keyboard extends EventTarget {
|
|
|
29
28
|
this.inputEnabledFunction = inputEnabledFunction;
|
|
30
29
|
this.dbgr = dbgr;
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// clearKeys, disableKeyboard, enableKeyboard.
|
|
35
|
-
this.keyInterface = processor.model.isAtom ? processor.atomppia : processor.sysvia;
|
|
36
|
-
// The SHIFT key constant used by stringToMachineKeys in the paste key array.
|
|
37
|
-
// Compared by reference in _deliverPasteKey to avoid toggling shift off.
|
|
38
|
-
this._shiftKey = processor.model.isAtom ? ATOM.SHIFT : utils.BBC.SHIFT;
|
|
31
|
+
this.keyInterface = processor.keyboardInterface;
|
|
32
|
+
this.typist = new Typist(processor);
|
|
39
33
|
|
|
40
34
|
// State
|
|
41
35
|
this.emuKeyHandlers = {};
|
|
@@ -49,13 +43,6 @@ export class Keyboard extends EventTarget {
|
|
|
49
43
|
this.lastShiftLocation = 1;
|
|
50
44
|
this.lastCtrlLocation = 1;
|
|
51
45
|
this.lastAltLocation = 1;
|
|
52
|
-
|
|
53
|
-
// Paste state — uses a scheduler task instead of a debugInstruction
|
|
54
|
-
// hook so the CPU can remain on the fast execution path during paste.
|
|
55
|
-
this._pasteKeys = [];
|
|
56
|
-
this._pasteLastChar = undefined;
|
|
57
|
-
this._pasteClocksPerMs = 0;
|
|
58
|
-
this._pasteTask = this.processor.scheduler.newTask(() => this._deliverPasteKey());
|
|
59
46
|
}
|
|
60
47
|
|
|
61
48
|
/**
|
|
@@ -65,7 +52,6 @@ export class Keyboard extends EventTarget {
|
|
|
65
52
|
*/
|
|
66
53
|
keyCode(evt) {
|
|
67
54
|
const ret = evt.which || evt.charCode || evt.keyCode;
|
|
68
|
-
const keyCodes = utils.keyCodes;
|
|
69
55
|
|
|
70
56
|
switch (evt.location) {
|
|
71
57
|
default:
|
|
@@ -108,9 +94,9 @@ export class Keyboard extends EventTarget {
|
|
|
108
94
|
case 3: // numpad
|
|
109
95
|
switch (ret) {
|
|
110
96
|
case keyCodes.ENTER:
|
|
111
|
-
return
|
|
97
|
+
return keyCodes.NUMPADENTER;
|
|
112
98
|
case keyCodes.DELETE:
|
|
113
|
-
return
|
|
99
|
+
return keyCodes.NUMPAD_DECIMAL_POINT;
|
|
114
100
|
}
|
|
115
101
|
break;
|
|
116
102
|
}
|
|
@@ -143,7 +129,7 @@ export class Keyboard extends EventTarget {
|
|
|
143
129
|
*/
|
|
144
130
|
setKeyLayout(layout) {
|
|
145
131
|
this.keyLayout = layout;
|
|
146
|
-
this.
|
|
132
|
+
this.processor.setKeyLayout(layout);
|
|
147
133
|
}
|
|
148
134
|
|
|
149
135
|
/**
|
|
@@ -226,7 +212,7 @@ export class Keyboard extends EventTarget {
|
|
|
226
212
|
const code = this.keyCode(evt);
|
|
227
213
|
evt.preventDefault();
|
|
228
214
|
|
|
229
|
-
if (this.isPasting && code ===
|
|
215
|
+
if (this.isPasting && code === keyCodes.ESCAPE) {
|
|
230
216
|
this.cancelPaste();
|
|
231
217
|
return;
|
|
232
218
|
}
|
|
@@ -255,11 +241,11 @@ export class Keyboard extends EventTarget {
|
|
|
255
241
|
* @private
|
|
256
242
|
*/
|
|
257
243
|
_handleSpecialKeys(code) {
|
|
258
|
-
if (code ===
|
|
244
|
+
if (code === keyCodes.F12 || code === keyCodes.BREAK) {
|
|
259
245
|
this.dispatchEvent(new CustomEvent("break", { detail: true }));
|
|
260
246
|
this.processor.setReset(true);
|
|
261
247
|
return true;
|
|
262
|
-
} else if (isMac && code ===
|
|
248
|
+
} else if (isMac && code === keyCodes.CAPSLOCK) {
|
|
263
249
|
// Special CapsLock handling for Mac
|
|
264
250
|
this.handleMacCapsLock();
|
|
265
251
|
return true;
|
|
@@ -286,11 +272,11 @@ export class Keyboard extends EventTarget {
|
|
|
286
272
|
evt.preventDefault();
|
|
287
273
|
|
|
288
274
|
// Handle special key cases
|
|
289
|
-
if (code ===
|
|
275
|
+
if (code === keyCodes.F12 || code === keyCodes.BREAK) {
|
|
290
276
|
this.dispatchEvent(new CustomEvent("break", { detail: false }));
|
|
291
277
|
this.processor.setReset(false);
|
|
292
278
|
return;
|
|
293
|
-
} else if (isMac && code ===
|
|
279
|
+
} else if (isMac && code === keyCodes.CAPSLOCK) {
|
|
294
280
|
// Special CapsLock handling for Mac
|
|
295
281
|
this.handleMacCapsLock();
|
|
296
282
|
return;
|
|
@@ -311,10 +297,10 @@ export class Keyboard extends EventTarget {
|
|
|
311
297
|
|
|
312
298
|
// Mac browsers seem to model caps lock as a physical key that's down when capslock is on, and up when it's off.
|
|
313
299
|
// No event is generated when it is physically released on the keyboard. So, we simulate a "tap" here.
|
|
314
|
-
this.keyInterface.keyDown(
|
|
300
|
+
this.keyInterface.keyDown(keyCodes.CAPSLOCK);
|
|
315
301
|
|
|
316
302
|
// Simulate a key release after a short delay
|
|
317
|
-
setTimeout(() => this.keyInterface.keyUp(
|
|
303
|
+
setTimeout(() => this.keyInterface.keyUp(keyCodes.CAPSLOCK), CAPS_LOCK_DELAY);
|
|
318
304
|
|
|
319
305
|
if (this.saidCapsLockIsTapped) return;
|
|
320
306
|
this.saidCapsLockIsTapped = true;
|
|
@@ -331,104 +317,17 @@ export class Keyboard extends EventTarget {
|
|
|
331
317
|
);
|
|
332
318
|
}
|
|
333
319
|
|
|
334
|
-
/**
|
|
335
|
-
* Send raw keyboard input to the emulated machine (for paste/autotype).
|
|
336
|
-
* @param {Array} keysToSend - Array of machine-specific key codes to send
|
|
337
|
-
* @param {boolean} checkCapsAndShiftLocks - Whether to check caps and shift locks
|
|
338
|
-
*/
|
|
320
|
+
/** Sends raw keys, and millisecond delays, to the machine: paste and autoboot come through here. */
|
|
339
321
|
sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
this.keyInterface.disableKeyboard();
|
|
343
|
-
// The paste task lives on the processor's scheduler, which is polled with peripheral
|
|
344
|
-
// cycles, so paste delays stay in real time whatever the CPU multiplier is.
|
|
345
|
-
this._pasteClocksPerMs = this.processor.peripheralCyclesPerSecond / 1000;
|
|
346
|
-
|
|
347
|
-
if (checkCapsAndShiftLocks) {
|
|
348
|
-
let toggleKey = null;
|
|
349
|
-
if (!this.keyInterface.capsLockLight) toggleKey = utils.BBC.CAPSLOCK;
|
|
350
|
-
else if (this.keyInterface.shiftLockLight) toggleKey = utils.BBC.SHIFTLOCK;
|
|
351
|
-
if (toggleKey) {
|
|
352
|
-
keysToSend.unshift(toggleKey);
|
|
353
|
-
keysToSend.push(toggleKey);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
this._pasteKeys = keysToSend;
|
|
358
|
-
this._pasteLastChar = undefined;
|
|
359
|
-
this._pasteTask.schedule(0);
|
|
322
|
+
this.typist.type(keysToSend, checkCapsAndShiftLocks);
|
|
360
323
|
}
|
|
361
324
|
|
|
362
|
-
/**
|
|
363
|
-
* Scheduler callback that delivers one key per invocation, rescheduling
|
|
364
|
-
* itself for the next key. Replaces the old debugInstruction hook so the
|
|
365
|
-
* CPU stays on the fast execution path during paste.
|
|
366
|
-
* @private
|
|
367
|
-
*/
|
|
368
|
-
_deliverPasteKey() {
|
|
369
|
-
if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey) {
|
|
370
|
-
this.keyInterface.keyToggleRaw(this._pasteLastChar);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (this._pasteKeys.length === 0) {
|
|
374
|
-
this._pasteLastChar = undefined;
|
|
375
|
-
this.keyInterface.enableKeyboard();
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Atom's PPIA keyboard is polled, not interrupt-driven like the BBC's
|
|
380
|
-
// SysVIA. Insert a debounce gap after every key release so the ROM
|
|
381
|
-
// sees the key-up before the next key-down arrives.
|
|
382
|
-
if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey && this.processor.model.isAtom) {
|
|
383
|
-
this._pasteLastChar = undefined;
|
|
384
|
-
this._pasteTask.schedule(30 * this._pasteClocksPerMs);
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const ch = this._pasteKeys[0];
|
|
389
|
-
const debounce = this._pasteLastChar === ch;
|
|
390
|
-
this._pasteLastChar = ch;
|
|
391
|
-
if (debounce) {
|
|
392
|
-
this._pasteLastChar = undefined;
|
|
393
|
-
this._pasteTask.schedule(30 * this._pasteClocksPerMs);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Atom ROM polls the keyboard once per VSync (~16ms at 60 Hz).
|
|
398
|
-
// 80ms gives the ROM ~5 scan cycles to detect, debounce, and
|
|
399
|
-
// process each keypress.
|
|
400
|
-
let delayMs = this.processor.model.isAtom ? 80 : 50;
|
|
401
|
-
if (typeof this._pasteLastChar === "number") {
|
|
402
|
-
delayMs = this._pasteLastChar;
|
|
403
|
-
this._pasteLastChar = undefined;
|
|
404
|
-
} else {
|
|
405
|
-
this.keyInterface.keyToggleRaw(this._pasteLastChar);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
this._pasteKeys.shift();
|
|
409
|
-
this._pasteTask.schedule(delayMs * this._pasteClocksPerMs);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Cancel any in-progress paste operation.
|
|
414
|
-
*/
|
|
415
325
|
cancelPaste() {
|
|
416
|
-
|
|
417
|
-
this._pasteTask.cancel();
|
|
418
|
-
if (this._pasteLastChar && this._pasteLastChar !== this._shiftKey) {
|
|
419
|
-
this.keyInterface.keyToggleRaw(this._pasteLastChar);
|
|
420
|
-
}
|
|
421
|
-
this._pasteLastChar = undefined;
|
|
422
|
-
this._pasteKeys = [];
|
|
423
|
-
this.keyInterface.enableKeyboard();
|
|
326
|
+
this.typist.cancel();
|
|
424
327
|
}
|
|
425
328
|
|
|
426
|
-
/**
|
|
427
|
-
* Whether a paste operation is currently in progress.
|
|
428
|
-
* @returns {boolean}
|
|
429
|
-
*/
|
|
430
329
|
get isPasting() {
|
|
431
|
-
return this.
|
|
330
|
+
return this.typist.isTyping;
|
|
432
331
|
}
|
|
433
332
|
|
|
434
333
|
/**
|
package/src/web/layout.js
CHANGED
|
@@ -78,6 +78,10 @@ export class Layout {
|
|
|
78
78
|
this.cubMonitorPic = document.getElementById("cub-monitor-pic");
|
|
79
79
|
this.borderReservedSize = embed ? 0 : 100;
|
|
80
80
|
this.bottomReservedSize = embed ? 0 : 68;
|
|
81
|
+
if (embed) {
|
|
82
|
+
for (const el of document.querySelectorAll(".embed-hide")) el.style.display = "none";
|
|
83
|
+
document.body.style.backgroundColor = "transparent";
|
|
84
|
+
}
|
|
81
85
|
|
|
82
86
|
window.addEventListener("resize", () => this.resize());
|
|
83
87
|
window.setTimeout(() => this.resize(), 1);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DiscLayout } from "../disc.js";
|
|
2
|
+
import { discFor, guessDiscTypeFromName } from "../fdc.js";
|
|
3
|
+
import { stringToUint8Array, uint8ArrayToString } from "../binary.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create or open a disc held in the browser's local storage.
|
|
7
|
+
* @param {string} name - The file name with extension
|
|
8
|
+
* @param {string} [layout] - one of DiscLayout; by default the image is asked what it is
|
|
9
|
+
* @param {function(*): void} [onSaveError] - called with whatever was thrown, the first time a write
|
|
10
|
+
* cannot be stored
|
|
11
|
+
* @returns {Disc} The loaded disc object
|
|
12
|
+
*/
|
|
13
|
+
export function localDisc(name, layout = DiscLayout.auto, onSaveError = () => {}) {
|
|
14
|
+
const discName = "disc_" + name;
|
|
15
|
+
let data;
|
|
16
|
+
const dataString = window.localStorage[discName];
|
|
17
|
+
if (!dataString) {
|
|
18
|
+
console.log("Creating browser-local disc " + name);
|
|
19
|
+
const discType = guessDiscTypeFromName(name);
|
|
20
|
+
if (!discType.byteSize) {
|
|
21
|
+
throw new Error(`Cannot create blank disc of type ${discType.extension} - unknown size`);
|
|
22
|
+
}
|
|
23
|
+
data = new Uint8Array(discType.byteSize);
|
|
24
|
+
if (discType.supportsCatalogue) {
|
|
25
|
+
discType.setDiscName(data, name);
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
console.log("Loading browser-local disc " + name);
|
|
29
|
+
data = stringToUint8Array(dataString);
|
|
30
|
+
}
|
|
31
|
+
let reportedSaveError = false;
|
|
32
|
+
const onChange = (data) => {
|
|
33
|
+
try {
|
|
34
|
+
const str = uint8ArrayToString(data);
|
|
35
|
+
window.localStorage.setItem(discName, str);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log(`Unable to save browser-local disc ${name}: ${e}`);
|
|
38
|
+
if (reportedSaveError) return;
|
|
39
|
+
reportedSaveError = true;
|
|
40
|
+
onSaveError(e);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
return discFor(name, data, onChange, layout);
|
|
44
|
+
}
|