web-a2e 1.0.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/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- package/wiki/Video-Rendering.md +658 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ui-controller.js - Main UI controller and menu wiring
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* UIController - Manages all toolbar and control UI interactions
|
|
10
|
+
* Handles button clicks, popups, menus, and UI state updates
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { clearStateFromStorage } from "../state/state-persistence.js";
|
|
14
|
+
import { ThemeManager } from "./theme-manager.js";
|
|
15
|
+
|
|
16
|
+
// Timing constants
|
|
17
|
+
const REMINDER_DISMISS_DELAY_MS = 2000;
|
|
18
|
+
const NOTIFICATION_DISPLAY_MS = 3000;
|
|
19
|
+
const STATE_BUTTON_FLASH_MS = 600;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} UIControllerDeps
|
|
23
|
+
* @property {Object} emulator - The emulator instance (for start/stop/running state)
|
|
24
|
+
* @property {Object} wasmModule - The WASM module
|
|
25
|
+
* @property {Object} audioDriver - Audio driver for volume/mute
|
|
26
|
+
* @property {Object} diskManager - Disk manager for drive sounds
|
|
27
|
+
* @property {Object} fileExplorer - File explorer window
|
|
28
|
+
* @property {Object} windowManager - Debug window manager
|
|
29
|
+
* @property {Object} screenWindow - Screen window instance
|
|
30
|
+
* @property {Object} reminderController - Reminder controller
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export class UIController {
|
|
34
|
+
/**
|
|
35
|
+
* @param {UIControllerDeps} deps - Dependencies
|
|
36
|
+
*/
|
|
37
|
+
constructor(deps) {
|
|
38
|
+
this.emulator = deps.emulator;
|
|
39
|
+
this.wasmModule = deps.wasmModule;
|
|
40
|
+
this.audioDriver = deps.audioDriver;
|
|
41
|
+
this.diskManager = deps.diskManager;
|
|
42
|
+
this.fileExplorer = deps.fileExplorer;
|
|
43
|
+
this.windowManager = deps.windowManager;
|
|
44
|
+
this.screenWindow = deps.screenWindow;
|
|
45
|
+
this.reminderController = deps.reminderController;
|
|
46
|
+
this.inputHandler = deps.inputHandler;
|
|
47
|
+
this.themeManager = deps.themeManager;
|
|
48
|
+
this.windowSwitcher = deps.windowSwitcher;
|
|
49
|
+
|
|
50
|
+
this.isFullPageMode = false;
|
|
51
|
+
this.canvas = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize all UI controls
|
|
56
|
+
*/
|
|
57
|
+
init() {
|
|
58
|
+
this.canvas = document.getElementById("screen");
|
|
59
|
+
if (!this.canvas) {
|
|
60
|
+
console.error("Required DOM element not found: screen");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.setupMenus();
|
|
65
|
+
this.setupPowerControls();
|
|
66
|
+
this.setupAgentButton();
|
|
67
|
+
this.setupFullPageModeControls();
|
|
68
|
+
this.setupSoundControls();
|
|
69
|
+
this.setupSystemMenuActions();
|
|
70
|
+
this.setupHardwareMenuActions();
|
|
71
|
+
this.setupDebugMenuActions();
|
|
72
|
+
this.setupDevMenuActions();
|
|
73
|
+
this.setupHelpMenuActions();
|
|
74
|
+
this.setupThemeSelector();
|
|
75
|
+
this.setupWindowSwitcher();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Helper to refocus canvas after button clicks
|
|
80
|
+
*/
|
|
81
|
+
refocusCanvas() {
|
|
82
|
+
if (this.canvas) {
|
|
83
|
+
setTimeout(() => this.canvas.focus(), 0);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Close all open header menus
|
|
89
|
+
*/
|
|
90
|
+
closeAllMenus() {
|
|
91
|
+
document.querySelectorAll(".header-menu-container.open").forEach((c) => {
|
|
92
|
+
c.classList.remove("open");
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Set up generic menu open/close behavior for all header-menu-container elements
|
|
98
|
+
*/
|
|
99
|
+
setupMenus() {
|
|
100
|
+
const containers = document.querySelectorAll(".header-menu-container");
|
|
101
|
+
|
|
102
|
+
containers.forEach((container) => {
|
|
103
|
+
const trigger = container.querySelector(".header-menu-trigger");
|
|
104
|
+
if (!trigger) return;
|
|
105
|
+
|
|
106
|
+
trigger.addEventListener("click", (e) => {
|
|
107
|
+
e.stopPropagation();
|
|
108
|
+
const wasOpen = container.classList.contains("open");
|
|
109
|
+
|
|
110
|
+
// Close all menus first
|
|
111
|
+
this.closeAllMenus();
|
|
112
|
+
|
|
113
|
+
// Toggle this one
|
|
114
|
+
if (!wasOpen) {
|
|
115
|
+
container.classList.add("open");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Hovering over a different menu trigger while one is open switches menus
|
|
120
|
+
trigger.addEventListener("mouseenter", () => {
|
|
121
|
+
const anyOpen = document.querySelector(".header-menu-container.open");
|
|
122
|
+
if (anyOpen && anyOpen !== container) {
|
|
123
|
+
this.closeAllMenus();
|
|
124
|
+
container.classList.add("open");
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Close menus when clicking outside
|
|
130
|
+
document.addEventListener("click", (e) => {
|
|
131
|
+
const inMenu = e.target.closest(".header-menu-container");
|
|
132
|
+
if (!inMenu) {
|
|
133
|
+
this.closeAllMenus();
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Close menus on Escape
|
|
138
|
+
document.addEventListener("keydown", (e) => {
|
|
139
|
+
if (e.key === "Escape") {
|
|
140
|
+
this.closeAllMenus();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Close menus on window resize
|
|
145
|
+
window.addEventListener("resize", () => {
|
|
146
|
+
this.closeAllMenus();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Set up power button and reset controls (System menu items)
|
|
152
|
+
*/
|
|
153
|
+
setupPowerControls() {
|
|
154
|
+
const powerBtn = document.getElementById("btn-power");
|
|
155
|
+
if (!powerBtn) {
|
|
156
|
+
console.error("Required DOM element not found: btn-power");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Power button - simple on/off
|
|
161
|
+
powerBtn.addEventListener("click", () => {
|
|
162
|
+
this.reminderController.dismissPowerReminder();
|
|
163
|
+
if (this.emulator.isRunning()) {
|
|
164
|
+
this.emulator.stop();
|
|
165
|
+
this.reminderController.showBasicReminder(false);
|
|
166
|
+
} else {
|
|
167
|
+
this.emulator.start();
|
|
168
|
+
this.reminderController.showBasicReminder(true);
|
|
169
|
+
}
|
|
170
|
+
this.refocusCanvas();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Set up agent button for MCP server connection
|
|
176
|
+
*/
|
|
177
|
+
setupAgentButton() {
|
|
178
|
+
console.log("[UIController] setupAgentButton() called");
|
|
179
|
+
const agentBtn = document.getElementById("btn-agent");
|
|
180
|
+
console.log("[UIController] agentBtn:", agentBtn);
|
|
181
|
+
if (!agentBtn) {
|
|
182
|
+
console.warn("[UIController] Agent button not found");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const agentManager = this.emulator.agentManager;
|
|
187
|
+
if (!agentManager) {
|
|
188
|
+
console.warn("[UIController] AgentManager not available");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log("[UIController] Agent button initialized");
|
|
193
|
+
|
|
194
|
+
// Hide button by default
|
|
195
|
+
agentBtn.classList.add("hidden");
|
|
196
|
+
|
|
197
|
+
const updateButtonState = () => {
|
|
198
|
+
const state = agentManager.getState();
|
|
199
|
+
|
|
200
|
+
// Remove all state classes
|
|
201
|
+
agentBtn.classList.remove("connected", "disconnected", "severed", "hidden");
|
|
202
|
+
|
|
203
|
+
if (!state.serverAvailable) {
|
|
204
|
+
// Hide button when server is not available
|
|
205
|
+
agentBtn.classList.add("hidden");
|
|
206
|
+
agentBtn.title = "MCP Server not available";
|
|
207
|
+
} else if (state.connected) {
|
|
208
|
+
// Connected - yellow
|
|
209
|
+
agentBtn.classList.add("connected");
|
|
210
|
+
agentBtn.title = "Disconnect";
|
|
211
|
+
} else if (state.reconnecting) {
|
|
212
|
+
// Connection severed but reconnecting - light red
|
|
213
|
+
agentBtn.classList.add("severed");
|
|
214
|
+
agentBtn.title = "Connection lost - Click to abort reconnection";
|
|
215
|
+
} else {
|
|
216
|
+
// Server available but not connected - default appearance
|
|
217
|
+
agentBtn.classList.add("disconnected");
|
|
218
|
+
agentBtn.title = "Connect to Agent";
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Set up callbacks
|
|
223
|
+
agentManager.onServerAvailable = () => {
|
|
224
|
+
console.log("[UIController] Server became available - showing button");
|
|
225
|
+
updateButtonState();
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
agentManager.onServerUnavailable = () => {
|
|
229
|
+
console.log("[UIController] Server became unavailable - hiding button");
|
|
230
|
+
updateButtonState();
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
agentManager.onConnectionChange = (connected) => {
|
|
234
|
+
console.log(`[UIController] Connection changed: ${connected}`);
|
|
235
|
+
updateButtonState();
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Handle button click
|
|
239
|
+
agentBtn.addEventListener("click", () => {
|
|
240
|
+
console.log("[UIController] Agent button clicked");
|
|
241
|
+
const state = agentManager.getState();
|
|
242
|
+
|
|
243
|
+
if (agentManager.isConnected()) {
|
|
244
|
+
// Connected - disconnect
|
|
245
|
+
console.log("[UIController] Disconnecting...");
|
|
246
|
+
agentManager.disconnect();
|
|
247
|
+
// Resume heartbeat polling after manual disconnect
|
|
248
|
+
agentManager.startHeartbeatPolling();
|
|
249
|
+
} else if (state.reconnecting) {
|
|
250
|
+
// Reconnecting - abort reconnection and reset to disconnected
|
|
251
|
+
console.log("[UIController] Aborting reconnection attempts...");
|
|
252
|
+
agentManager.disconnect();
|
|
253
|
+
// Don't auto-connect, let user click again if they want
|
|
254
|
+
} else {
|
|
255
|
+
// Disconnected - connect
|
|
256
|
+
console.log("[UIController] Connecting...");
|
|
257
|
+
agentManager.connect();
|
|
258
|
+
setTimeout(() => updateButtonState(), 100);
|
|
259
|
+
}
|
|
260
|
+
updateButtonState();
|
|
261
|
+
this.refocusCanvas();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Start heartbeat polling to detect when server becomes available
|
|
265
|
+
agentManager.startHeartbeatPolling();
|
|
266
|
+
|
|
267
|
+
updateButtonState();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Set up reset buttons and System menu state management actions
|
|
272
|
+
*/
|
|
273
|
+
setupSystemMenuActions() {
|
|
274
|
+
// Warm reset button (top-level, preserves memory)
|
|
275
|
+
const warmResetBtn = document.getElementById("btn-warm-reset");
|
|
276
|
+
if (warmResetBtn) {
|
|
277
|
+
warmResetBtn.addEventListener("click", () => {
|
|
278
|
+
if (this.inputHandler) this.inputHandler.cancelPaste();
|
|
279
|
+
this.wasmModule._warmReset();
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
this.reminderController.dismissBasicReminder();
|
|
282
|
+
}, REMINDER_DISMISS_DELAY_MS);
|
|
283
|
+
this.refocusCanvas();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Cold reset button (top-level, full restart)
|
|
288
|
+
const coldResetBtn = document.getElementById("btn-cold-reset");
|
|
289
|
+
if (coldResetBtn) {
|
|
290
|
+
coldResetBtn.addEventListener("click", async () => {
|
|
291
|
+
if (this.inputHandler) this.inputHandler.cancelPaste();
|
|
292
|
+
this.wasmModule._reset();
|
|
293
|
+
await clearStateFromStorage();
|
|
294
|
+
this.refocusCanvas();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Save States window button
|
|
299
|
+
const saveStatesBtn = document.getElementById("btn-save-states");
|
|
300
|
+
if (saveStatesBtn) {
|
|
301
|
+
saveStatesBtn.addEventListener("click", () => {
|
|
302
|
+
this.windowManager.toggleWindow("save-states");
|
|
303
|
+
this.closeAllMenus();
|
|
304
|
+
this.refocusCanvas();
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Prevent system menu from closing when clicking toggle items
|
|
309
|
+
const systemMenu = document.getElementById("file-menu");
|
|
310
|
+
if (systemMenu) {
|
|
311
|
+
systemMenu.querySelectorAll(".menu-toggle-item").forEach((item) => {
|
|
312
|
+
item.addEventListener("click", (e) => {
|
|
313
|
+
e.stopPropagation();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Set up Hardware menu action items
|
|
321
|
+
*/
|
|
322
|
+
setupHardwareMenuActions() {
|
|
323
|
+
const drivesBtn = document.getElementById("btn-drives");
|
|
324
|
+
if (drivesBtn) {
|
|
325
|
+
drivesBtn.addEventListener("click", () => {
|
|
326
|
+
this.windowManager.toggleWindow("disk-drives");
|
|
327
|
+
this.closeAllMenus();
|
|
328
|
+
this.refocusCanvas();
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const hardDrivesBtn = document.getElementById("btn-hard-drives");
|
|
333
|
+
if (hardDrivesBtn) {
|
|
334
|
+
hardDrivesBtn.addEventListener("click", () => {
|
|
335
|
+
this.windowManager.toggleWindow("hard-drives");
|
|
336
|
+
this.closeAllMenus();
|
|
337
|
+
this.refocusCanvas();
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
const fileExplorerBtn = document.getElementById("btn-file-explorer");
|
|
343
|
+
if (fileExplorerBtn) {
|
|
344
|
+
fileExplorerBtn.addEventListener("click", () => {
|
|
345
|
+
this.windowManager.toggleWindow("file-explorer-window");
|
|
346
|
+
this.closeAllMenus();
|
|
347
|
+
this.refocusCanvas();
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const displayBtn = document.getElementById("btn-display");
|
|
352
|
+
if (displayBtn) {
|
|
353
|
+
displayBtn.addEventListener("click", () => {
|
|
354
|
+
this.windowManager.toggleWindow("display-settings");
|
|
355
|
+
this.closeAllMenus();
|
|
356
|
+
this.refocusCanvas();
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const joystickBtn = document.getElementById("btn-joystick");
|
|
361
|
+
if (joystickBtn) {
|
|
362
|
+
joystickBtn.addEventListener("click", () => {
|
|
363
|
+
this.windowManager.toggleWindow("joystick");
|
|
364
|
+
this.closeAllMenus();
|
|
365
|
+
this.refocusCanvas();
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const slotsBtn = document.getElementById("btn-slots");
|
|
370
|
+
if (slotsBtn) {
|
|
371
|
+
slotsBtn.addEventListener("click", () => {
|
|
372
|
+
this.windowManager.toggleWindow("slot-configuration");
|
|
373
|
+
this.closeAllMenus();
|
|
374
|
+
this.refocusCanvas();
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Set up debug menu dropdown actions
|
|
383
|
+
*/
|
|
384
|
+
setupDebugMenuActions() {
|
|
385
|
+
const debugMenu = document.getElementById("debug-menu");
|
|
386
|
+
if (!debugMenu) return;
|
|
387
|
+
|
|
388
|
+
const windowMap = {
|
|
389
|
+
cpu: "cpu-debugger",
|
|
390
|
+
switches: "soft-switches",
|
|
391
|
+
memmap: "memory-map",
|
|
392
|
+
memory: "memory-browser",
|
|
393
|
+
heatmap: "memory-heatmap",
|
|
394
|
+
stack: "stack-viewer",
|
|
395
|
+
zeropage: "zeropage-watch",
|
|
396
|
+
trace: "trace-panel",
|
|
397
|
+
mockingboard: "mockingboard-debug",
|
|
398
|
+
"mouse-card": "mouse-card-debug",
|
|
399
|
+
"basic-debugger": "basic-debugger",
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
debugMenu.querySelectorAll(".header-menu-item").forEach((item) => {
|
|
403
|
+
item.addEventListener("click", () => {
|
|
404
|
+
const windowType = item.dataset.window;
|
|
405
|
+
if (windowMap[windowType]) {
|
|
406
|
+
this.windowManager.toggleWindow(windowMap[windowType]);
|
|
407
|
+
}
|
|
408
|
+
this.closeAllMenus();
|
|
409
|
+
this.refocusCanvas();
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Set up dev menu dropdown actions
|
|
416
|
+
*/
|
|
417
|
+
setupDevMenuActions() {
|
|
418
|
+
const devMenu = document.getElementById("dev-menu");
|
|
419
|
+
if (!devMenu) return;
|
|
420
|
+
|
|
421
|
+
const windowMap = {
|
|
422
|
+
basic: "basic-program",
|
|
423
|
+
assembler: "assembler-editor",
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
devMenu.querySelectorAll(".header-menu-item").forEach((item) => {
|
|
427
|
+
item.addEventListener("click", () => {
|
|
428
|
+
const windowType = item.dataset.window;
|
|
429
|
+
if (windowMap[windowType]) {
|
|
430
|
+
this.windowManager.toggleWindow(windowMap[windowType]);
|
|
431
|
+
}
|
|
432
|
+
this.closeAllMenus();
|
|
433
|
+
this.refocusCanvas();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Set up Help menu actions
|
|
440
|
+
*/
|
|
441
|
+
setupHelpMenuActions() {
|
|
442
|
+
// Documentation (btn-help) is handled by DocumentationWindow via F1 and direct click
|
|
443
|
+
// btn-help is now inside the help menu - DocumentationWindow binds to it in its own create()
|
|
444
|
+
|
|
445
|
+
// Release notes menu item
|
|
446
|
+
const releaseNotesMenuBtn = document.getElementById("btn-release-notes-menu");
|
|
447
|
+
if (releaseNotesMenuBtn) {
|
|
448
|
+
releaseNotesMenuBtn.addEventListener("click", () => {
|
|
449
|
+
this.windowManager.toggleWindow("release-notes");
|
|
450
|
+
this.closeAllMenus();
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Update/refresh button - only visible when installed as PWA
|
|
455
|
+
const updateBtn = document.getElementById("btn-update");
|
|
456
|
+
const isInstalled = window.matchMedia("(display-mode: standalone)").matches || navigator.standalone;
|
|
457
|
+
if (updateBtn && !isInstalled) {
|
|
458
|
+
updateBtn.style.display = "none";
|
|
459
|
+
const separator = updateBtn.previousElementSibling;
|
|
460
|
+
if (separator && separator.classList.contains("header-menu-separator")) {
|
|
461
|
+
separator.style.display = "none";
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (updateBtn && isInstalled) {
|
|
465
|
+
updateBtn.addEventListener("click", async () => {
|
|
466
|
+
this.closeAllMenus();
|
|
467
|
+
if ("serviceWorker" in navigator) {
|
|
468
|
+
try {
|
|
469
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
470
|
+
if (registration) {
|
|
471
|
+
await registration.unregister();
|
|
472
|
+
const cacheNames = await caches.keys();
|
|
473
|
+
await Promise.all(cacheNames.map((name) => caches.delete(name)));
|
|
474
|
+
this.showNotification("Updating... page will reload");
|
|
475
|
+
setTimeout(() => window.location.reload(true), 500);
|
|
476
|
+
} else {
|
|
477
|
+
this.showNotification("No service worker registered");
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
console.error("Update failed:", error);
|
|
481
|
+
this.showNotification("Update failed: " + error.message);
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
window.location.reload(true);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Set up theme selector buttons in View menu
|
|
492
|
+
*/
|
|
493
|
+
setupThemeSelector() {
|
|
494
|
+
const buttons = document.querySelectorAll(".theme-btn");
|
|
495
|
+
if (!buttons.length || !this.themeManager) return;
|
|
496
|
+
|
|
497
|
+
const updateActive = () => {
|
|
498
|
+
const pref = this.themeManager.getPreference();
|
|
499
|
+
buttons.forEach((btn) => {
|
|
500
|
+
btn.classList.toggle("active", btn.dataset.theme === pref);
|
|
501
|
+
});
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
buttons.forEach((btn) => {
|
|
505
|
+
btn.addEventListener("click", (e) => {
|
|
506
|
+
e.stopPropagation();
|
|
507
|
+
this.themeManager.setPreference(btn.dataset.theme);
|
|
508
|
+
updateActive();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
updateActive();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Set up Ctrl+` shortcut to toggle the window switcher
|
|
517
|
+
*/
|
|
518
|
+
setupWindowSwitcher() {
|
|
519
|
+
if (!this.windowSwitcher) return;
|
|
520
|
+
|
|
521
|
+
document.addEventListener('keydown', (e) => {
|
|
522
|
+
if (e.ctrlKey && e.code === 'Backquote') {
|
|
523
|
+
e.preventDefault();
|
|
524
|
+
e.stopPropagation();
|
|
525
|
+
|
|
526
|
+
// Exit full-page mode first so the selected window is visible
|
|
527
|
+
if (this.isFullPageMode) {
|
|
528
|
+
const fullscreenBtn = document.getElementById('btn-fullscreen');
|
|
529
|
+
if (fullscreenBtn) fullscreenBtn.click();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
this.windowSwitcher.toggle();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Option+Tab / Option+Shift+Tab to cycle through visible windows
|
|
536
|
+
if (e.altKey && e.code === 'Tab') {
|
|
537
|
+
e.preventDefault();
|
|
538
|
+
e.stopPropagation();
|
|
539
|
+
this.windowManager.cycleWindow(e.shiftKey);
|
|
540
|
+
}
|
|
541
|
+
}, { capture: true });
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Sync the full-page toolbar power button with emulator state
|
|
546
|
+
*/
|
|
547
|
+
syncFullPagePowerButton() {
|
|
548
|
+
const fpPower = document.getElementById("fp-power");
|
|
549
|
+
if (!fpPower) return;
|
|
550
|
+
|
|
551
|
+
if (this.emulator.isRunning()) {
|
|
552
|
+
fpPower.classList.remove("off");
|
|
553
|
+
fpPower.title = "Power Off";
|
|
554
|
+
} else {
|
|
555
|
+
fpPower.classList.add("off");
|
|
556
|
+
fpPower.title = "Power On";
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Set up fullscreen/full page mode controls
|
|
562
|
+
*/
|
|
563
|
+
setupFullPageModeControls() {
|
|
564
|
+
const fullscreenBtn = document.getElementById("btn-fullscreen");
|
|
565
|
+
if (!fullscreenBtn) return;
|
|
566
|
+
|
|
567
|
+
const exitFullPageMode = () => {
|
|
568
|
+
document.body.classList.remove("full-page-mode");
|
|
569
|
+
this.isFullPageMode = false;
|
|
570
|
+
|
|
571
|
+
// Restore ScreenWindow and re-attach canvas from monitor-frame
|
|
572
|
+
if (this.screenWindow) {
|
|
573
|
+
this.screenWindow.show();
|
|
574
|
+
this.screenWindow.attachCanvas();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Restore windows that were visible before entering full page mode
|
|
578
|
+
if (this._windowsBeforeFullPage) {
|
|
579
|
+
for (const id of this._windowsBeforeFullPage) {
|
|
580
|
+
this.windowManager.showWindow(id);
|
|
581
|
+
}
|
|
582
|
+
this._windowsBeforeFullPage = null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
this.refocusCanvas();
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const enterFullPageMode = () => {
|
|
589
|
+
// Close any open menus
|
|
590
|
+
this.closeAllMenus();
|
|
591
|
+
|
|
592
|
+
// Remember which windows are visible before hiding them
|
|
593
|
+
this._windowsBeforeFullPage = this.windowManager.getVisibleWindowIds();
|
|
594
|
+
|
|
595
|
+
// Detach canvas from ScreenWindow into monitor-frame for full-page rendering
|
|
596
|
+
if (this.screenWindow && this.screenWindow.isVisible) {
|
|
597
|
+
this.screenWindow.detachCanvas();
|
|
598
|
+
this.screenWindow.isVisible = false;
|
|
599
|
+
this.screenWindow.element.classList.add('hidden');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
this.windowManager.hideAll();
|
|
603
|
+
document.body.classList.add("full-page-mode");
|
|
604
|
+
this.isFullPageMode = true;
|
|
605
|
+
this.syncFullPagePowerButton();
|
|
606
|
+
this.refocusCanvas();
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
fullscreenBtn.addEventListener("click", () => {
|
|
610
|
+
if (this.isFullPageMode) {
|
|
611
|
+
exitFullPageMode();
|
|
612
|
+
} else {
|
|
613
|
+
enterFullPageMode();
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Exit full page mode on Ctrl+Escape
|
|
618
|
+
document.addEventListener("keydown", (e) => {
|
|
619
|
+
if (e.key === "Escape" && e.ctrlKey && this.isFullPageMode) {
|
|
620
|
+
e.preventDefault();
|
|
621
|
+
exitFullPageMode();
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Exit full page mode on click (on the black background area)
|
|
626
|
+
const mainEl = document.querySelector("main");
|
|
627
|
+
if (mainEl) {
|
|
628
|
+
mainEl.addEventListener("click", (e) => {
|
|
629
|
+
if (this.isFullPageMode && e.target.tagName !== "CANVAS") {
|
|
630
|
+
exitFullPageMode();
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// --- Full-page toolbar button handlers ---
|
|
636
|
+
|
|
637
|
+
const fpPower = document.getElementById("fp-power");
|
|
638
|
+
if (fpPower) {
|
|
639
|
+
fpPower.addEventListener("click", (e) => {
|
|
640
|
+
e.stopPropagation();
|
|
641
|
+
this.reminderController.dismissPowerReminder();
|
|
642
|
+
if (this.emulator.isRunning()) {
|
|
643
|
+
this.emulator.stop();
|
|
644
|
+
} else {
|
|
645
|
+
this.emulator.start();
|
|
646
|
+
}
|
|
647
|
+
this.syncFullPagePowerButton();
|
|
648
|
+
this.refocusCanvas();
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const fpWarmReset = document.getElementById("fp-warm-reset");
|
|
653
|
+
if (fpWarmReset) {
|
|
654
|
+
fpWarmReset.addEventListener("click", (e) => {
|
|
655
|
+
e.stopPropagation();
|
|
656
|
+
if (this.inputHandler) this.inputHandler.cancelPaste();
|
|
657
|
+
this.wasmModule._warmReset();
|
|
658
|
+
this.refocusCanvas();
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const fpColdReset = document.getElementById("fp-cold-reset");
|
|
663
|
+
if (fpColdReset) {
|
|
664
|
+
fpColdReset.addEventListener("click", async (e) => {
|
|
665
|
+
e.stopPropagation();
|
|
666
|
+
if (this.inputHandler) this.inputHandler.cancelPaste();
|
|
667
|
+
this.wasmModule._reset();
|
|
668
|
+
await clearStateFromStorage();
|
|
669
|
+
this.refocusCanvas();
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const fpExit = document.getElementById("fp-exit");
|
|
674
|
+
if (fpExit) {
|
|
675
|
+
fpExit.addEventListener("click", (e) => {
|
|
676
|
+
e.stopPropagation();
|
|
677
|
+
exitFullPageMode();
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Set up sound controls popup
|
|
684
|
+
*/
|
|
685
|
+
setupSoundControls() {
|
|
686
|
+
const soundBtn = document.getElementById("btn-sound");
|
|
687
|
+
const soundPopup = document.getElementById("sound-popup");
|
|
688
|
+
const volumeSlider = document.getElementById("volume-slider");
|
|
689
|
+
const volumeValue = document.getElementById("volume-value");
|
|
690
|
+
const muteToggle = document.getElementById("mute-toggle");
|
|
691
|
+
const driveSoundsToggle = document.getElementById("drive-sounds-toggle");
|
|
692
|
+
if (!soundBtn || !soundPopup) return;
|
|
693
|
+
|
|
694
|
+
// Load saved drive sounds setting
|
|
695
|
+
const savedDriveSounds = localStorage.getItem("a2e-drive-sounds");
|
|
696
|
+
const driveSoundsEnabled = savedDriveSounds !== "false";
|
|
697
|
+
if (driveSoundsToggle) {
|
|
698
|
+
driveSoundsToggle.checked = driveSoundsEnabled;
|
|
699
|
+
}
|
|
700
|
+
this.diskManager.setSeekSoundEnabled(driveSoundsEnabled);
|
|
701
|
+
this.diskManager.setMotorSoundEnabled(driveSoundsEnabled);
|
|
702
|
+
|
|
703
|
+
// Toggle popup on button click
|
|
704
|
+
soundBtn.addEventListener("click", (e) => {
|
|
705
|
+
e.stopPropagation();
|
|
706
|
+
// Close header menus when opening sound popup
|
|
707
|
+
this.closeAllMenus();
|
|
708
|
+
soundPopup.classList.toggle("hidden");
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Close popup when clicking outside
|
|
712
|
+
document.addEventListener("click", (e) => {
|
|
713
|
+
if (!soundPopup.contains(e.target) && e.target !== soundBtn) {
|
|
714
|
+
soundPopup.classList.add("hidden");
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// Prevent popup from closing when clicking inside
|
|
719
|
+
soundPopup.addEventListener("click", (e) => {
|
|
720
|
+
e.stopPropagation();
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// Volume slider
|
|
724
|
+
if (volumeSlider && volumeValue) {
|
|
725
|
+
const initialVolume = Math.round(this.audioDriver.getVolume() * 100);
|
|
726
|
+
volumeSlider.value = initialVolume;
|
|
727
|
+
volumeValue.textContent = `${initialVolume}%`;
|
|
728
|
+
this.diskManager.setMasterVolume(this.audioDriver.isMuted() ? 0 : this.audioDriver.getVolume());
|
|
729
|
+
|
|
730
|
+
volumeSlider.addEventListener("input", (e) => {
|
|
731
|
+
const volume = parseInt(e.target.value, 10);
|
|
732
|
+
volumeValue.textContent = `${volume}%`;
|
|
733
|
+
this.audioDriver.setVolume(volume / 100);
|
|
734
|
+
if (!this.audioDriver.isMuted()) {
|
|
735
|
+
this.diskManager.setMasterVolume(volume / 100);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Mute toggle
|
|
741
|
+
if (muteToggle) {
|
|
742
|
+
muteToggle.checked = this.audioDriver.isMuted();
|
|
743
|
+
muteToggle.addEventListener("change", (e) => {
|
|
744
|
+
if (e.target.checked) {
|
|
745
|
+
this.audioDriver.mute();
|
|
746
|
+
this.diskManager.setMasterVolume(0);
|
|
747
|
+
} else {
|
|
748
|
+
this.audioDriver.unmute();
|
|
749
|
+
this.diskManager.setMasterVolume(this.audioDriver.getVolume());
|
|
750
|
+
}
|
|
751
|
+
this.updateSoundButton();
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Sync sound button icon with persisted mute state
|
|
756
|
+
this.updateSoundButton();
|
|
757
|
+
|
|
758
|
+
// Drive sounds toggle
|
|
759
|
+
if (driveSoundsToggle) {
|
|
760
|
+
driveSoundsToggle.addEventListener("change", (e) => {
|
|
761
|
+
const enabled = e.target.checked;
|
|
762
|
+
this.diskManager.setSeekSoundEnabled(enabled);
|
|
763
|
+
this.diskManager.setMotorSoundEnabled(enabled);
|
|
764
|
+
localStorage.setItem("a2e-drive-sounds", enabled);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Character set toggle (UK/US) - screen window header
|
|
769
|
+
const screenWindowCharsetToggle = document.getElementById("screen-window-charset-toggle");
|
|
770
|
+
|
|
771
|
+
const syncCharsetToggle = (isUK) => {
|
|
772
|
+
this.wasmModule._setUKCharacterSet(isUK);
|
|
773
|
+
localStorage.setItem("a2e-charset", isUK ? "uk" : "us");
|
|
774
|
+
if (screenWindowCharsetToggle) screenWindowCharsetToggle.checked = !isUK;
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// Initialize from saved setting
|
|
778
|
+
const savedCharset = localStorage.getItem("a2e-charset");
|
|
779
|
+
const isUKInitial = savedCharset === "uk";
|
|
780
|
+
this.wasmModule._setUKCharacterSet(isUKInitial);
|
|
781
|
+
if (screenWindowCharsetToggle) screenWindowCharsetToggle.checked = !isUKInitial;
|
|
782
|
+
|
|
783
|
+
// Screen window header toggle listener
|
|
784
|
+
if (screenWindowCharsetToggle) {
|
|
785
|
+
screenWindowCharsetToggle.addEventListener("change", (e) => {
|
|
786
|
+
syncCharsetToggle(!e.target.checked);
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Update sound button icon based on mute state
|
|
793
|
+
*/
|
|
794
|
+
updateSoundButton() {
|
|
795
|
+
const soundBtn = document.getElementById("btn-sound");
|
|
796
|
+
if (!soundBtn) return;
|
|
797
|
+
|
|
798
|
+
const iconUnmuted = soundBtn.querySelector(".icon-unmuted");
|
|
799
|
+
const iconMuted = soundBtn.querySelector(".icon-muted");
|
|
800
|
+
|
|
801
|
+
if (this.audioDriver.isMuted()) {
|
|
802
|
+
iconUnmuted?.classList.add("hidden");
|
|
803
|
+
iconMuted?.classList.remove("hidden");
|
|
804
|
+
} else {
|
|
805
|
+
iconUnmuted?.classList.remove("hidden");
|
|
806
|
+
iconMuted?.classList.add("hidden");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Update power button appearance based on running state
|
|
812
|
+
* @param {boolean} isRunning - Whether the emulator is running
|
|
813
|
+
*/
|
|
814
|
+
updatePowerButton(isRunning) {
|
|
815
|
+
const powerBtn = document.getElementById("btn-power");
|
|
816
|
+
|
|
817
|
+
if (isRunning) {
|
|
818
|
+
powerBtn?.classList.remove("off");
|
|
819
|
+
if (powerBtn) powerBtn.title = "Power Off";
|
|
820
|
+
} else {
|
|
821
|
+
powerBtn?.classList.add("off");
|
|
822
|
+
if (powerBtn) powerBtn.title = "Power On";
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
this.syncFullPagePowerButton();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Flash the system menu trigger to indicate saving
|
|
830
|
+
*/
|
|
831
|
+
flashStateButton() {
|
|
832
|
+
const trigger = document.getElementById("btn-file-menu");
|
|
833
|
+
if (!trigger) return;
|
|
834
|
+
|
|
835
|
+
trigger.classList.add("saving");
|
|
836
|
+
setTimeout(() => {
|
|
837
|
+
trigger.classList.remove("saving");
|
|
838
|
+
}, STATE_BUTTON_FLASH_MS);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Show a notification message
|
|
843
|
+
* @param {string} message - The message to display
|
|
844
|
+
*/
|
|
845
|
+
showNotification(message) {
|
|
846
|
+
let notification = document.getElementById("state-notification");
|
|
847
|
+
if (!notification) {
|
|
848
|
+
notification = document.createElement("div");
|
|
849
|
+
notification.id = "state-notification";
|
|
850
|
+
notification.className = "state-notification";
|
|
851
|
+
document.body.appendChild(notification);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
notification.textContent = message;
|
|
855
|
+
notification.classList.add("visible");
|
|
856
|
+
|
|
857
|
+
setTimeout(() => {
|
|
858
|
+
notification.classList.remove("visible");
|
|
859
|
+
}, NOTIFICATION_DISPLAY_MS);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Check if in full page mode
|
|
864
|
+
* @returns {boolean}
|
|
865
|
+
*/
|
|
866
|
+
isInFullPageMode() {
|
|
867
|
+
return this.isFullPageMode;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Get the ID of the window that currently has focus, or null if none
|
|
872
|
+
* @returns {string|null}
|
|
873
|
+
*/
|
|
874
|
+
get hasFocus() {
|
|
875
|
+
for (const [id, win] of this.windowManager.windows) {
|
|
876
|
+
if (win.isVisible && win.element.classList.contains('focused')) {
|
|
877
|
+
return id;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Bring a window to the front and give it focus
|
|
885
|
+
* @param {string} id - The window ID to focus
|
|
886
|
+
*/
|
|
887
|
+
focusWindow(id) {
|
|
888
|
+
const win = this.windowManager.getWindow(id);
|
|
889
|
+
if (win) {
|
|
890
|
+
if (!win.isVisible) {
|
|
891
|
+
this.windowManager.showWindow(id);
|
|
892
|
+
} else {
|
|
893
|
+
this.windowManager.bringToFront(id);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|