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,690 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* base-window.js - Base window class for all windows
|
|
3
|
+
*
|
|
4
|
+
* Written by
|
|
5
|
+
* Mike Daley <michael_daley@icloud.com>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class BaseWindow {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.id = config.id;
|
|
11
|
+
this.title = config.title;
|
|
12
|
+
this.minWidth = config.minWidth || 280;
|
|
13
|
+
this.minHeight = config.minHeight || 200;
|
|
14
|
+
this.maxWidth = config.maxWidth || Infinity;
|
|
15
|
+
this.maxHeight = config.maxHeight || Infinity;
|
|
16
|
+
this.defaultWidth = config.defaultWidth || 400;
|
|
17
|
+
this.defaultHeight = config.defaultHeight || 300;
|
|
18
|
+
this.defaultPosition = config.defaultPosition || null;
|
|
19
|
+
this.closable = config.closable !== false;
|
|
20
|
+
this.focusCanvas = config.focusCanvas || false;
|
|
21
|
+
|
|
22
|
+
// Customizable CSS class names (defaults to debug-window style)
|
|
23
|
+
this.cssClasses = {
|
|
24
|
+
window: config.cssClasses?.window || "debug-window",
|
|
25
|
+
header: config.cssClasses?.header || "debug-window-header",
|
|
26
|
+
title: config.cssClasses?.title || "debug-window-title",
|
|
27
|
+
close: config.cssClasses?.close || "debug-window-close",
|
|
28
|
+
content: config.cssClasses?.content || "debug-window-content",
|
|
29
|
+
resizeHandle: config.cssClasses?.resizeHandle || "debug-resize-handle",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Resize directions to create handles for
|
|
33
|
+
this.resizeDirections = config.resizeDirections || [
|
|
34
|
+
"n",
|
|
35
|
+
"e",
|
|
36
|
+
"s",
|
|
37
|
+
"w",
|
|
38
|
+
"ne",
|
|
39
|
+
"nw",
|
|
40
|
+
"se",
|
|
41
|
+
"sw",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// Optional localStorage key for persisting window state
|
|
45
|
+
this.storageKey = config.storageKey || null;
|
|
46
|
+
|
|
47
|
+
this.element = null;
|
|
48
|
+
this.headerElement = null;
|
|
49
|
+
this.contentElement = null;
|
|
50
|
+
this.zIndex = 1000;
|
|
51
|
+
this.isVisible = false;
|
|
52
|
+
this.isDragging = false;
|
|
53
|
+
this.isResizing = false;
|
|
54
|
+
this.dragOffset = { x: 0, y: 0 };
|
|
55
|
+
this.resizeStart = { x: 0, y: 0, width: 0, height: 0, left: 0, top: 0 };
|
|
56
|
+
this.resizeDirection = null;
|
|
57
|
+
|
|
58
|
+
// Track current position/size (needed because getBoundingClientRect returns zeros for hidden elements)
|
|
59
|
+
this.currentX = config.defaultPosition?.x ?? 0;
|
|
60
|
+
this.currentY = config.defaultPosition?.y ?? 0;
|
|
61
|
+
this.currentWidth = config.defaultWidth || 400;
|
|
62
|
+
this.currentHeight = config.defaultHeight || 300;
|
|
63
|
+
|
|
64
|
+
// Track distance from right/bottom edges for maintaining position on resize
|
|
65
|
+
this.distanceFromRight = null;
|
|
66
|
+
this.distanceFromBottom = null;
|
|
67
|
+
this.lastViewportWidth = window.innerWidth;
|
|
68
|
+
this.lastViewportHeight = window.innerHeight;
|
|
69
|
+
|
|
70
|
+
// Bind event handlers
|
|
71
|
+
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
72
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
73
|
+
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create the window DOM structure
|
|
78
|
+
*/
|
|
79
|
+
create() {
|
|
80
|
+
// Calculate centered position if no explicit default was provided
|
|
81
|
+
if (!this.defaultPosition) {
|
|
82
|
+
const header = document.querySelector("header");
|
|
83
|
+
const footer = document.querySelector("footer");
|
|
84
|
+
const minTop = header ? header.offsetHeight : 0;
|
|
85
|
+
const footerHeight = footer ? footer.offsetHeight : 0;
|
|
86
|
+
const availHeight = window.innerHeight - minTop - footerHeight;
|
|
87
|
+
this.defaultPosition = {
|
|
88
|
+
x: Math.max(0, Math.round((window.innerWidth - this.defaultWidth) / 2)),
|
|
89
|
+
y: Math.max(minTop, Math.round(minTop + (availHeight - this.defaultHeight) / 2)),
|
|
90
|
+
};
|
|
91
|
+
this.currentX = this.defaultPosition.x;
|
|
92
|
+
this.currentY = this.defaultPosition.y;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Create main window element
|
|
96
|
+
this.element = document.createElement("div");
|
|
97
|
+
this.element.id = this.id;
|
|
98
|
+
this.element.className = `${this.cssClasses.window} hidden`;
|
|
99
|
+
this.element.style.width = `${this.defaultWidth}px`;
|
|
100
|
+
this.element.style.height = `${this.defaultHeight}px`;
|
|
101
|
+
this.element.style.left = `${this.defaultPosition.x}px`;
|
|
102
|
+
this.element.style.top = `${this.defaultPosition.y}px`;
|
|
103
|
+
|
|
104
|
+
// Header (draggable area)
|
|
105
|
+
this.headerElement = document.createElement("div");
|
|
106
|
+
this.headerElement.className = this.cssClasses.header;
|
|
107
|
+
this.headerElement.innerHTML = `
|
|
108
|
+
<span class="${this.cssClasses.title}">${this.title}</span>
|
|
109
|
+
${this.closable ? `<button class="${this.cssClasses.close}" title="Close">×</button>` : ""}
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
// Content area
|
|
113
|
+
this.contentElement = document.createElement("div");
|
|
114
|
+
this.contentElement.className = this.cssClasses.content;
|
|
115
|
+
this.contentElement.innerHTML = this.renderContent();
|
|
116
|
+
|
|
117
|
+
// Resize handles
|
|
118
|
+
this.resizeDirections.forEach((dir) => {
|
|
119
|
+
const handle = document.createElement("div");
|
|
120
|
+
handle.className = `${this.cssClasses.resizeHandle} ${dir}`;
|
|
121
|
+
handle.dataset.direction = dir;
|
|
122
|
+
this.element.appendChild(handle);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Assemble
|
|
126
|
+
this.element.appendChild(this.headerElement);
|
|
127
|
+
this.element.appendChild(this.contentElement);
|
|
128
|
+
document.body.appendChild(this.element);
|
|
129
|
+
|
|
130
|
+
// Set up event listeners
|
|
131
|
+
this.setupEventListeners();
|
|
132
|
+
|
|
133
|
+
// Call hook for subclasses to set up after content is rendered
|
|
134
|
+
if (typeof this.onContentRendered === "function") {
|
|
135
|
+
this.onContentRendered();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Set up drag, resize, and close event listeners
|
|
141
|
+
*/
|
|
142
|
+
setupEventListeners() {
|
|
143
|
+
// Close button
|
|
144
|
+
const closeBtn = this.headerElement.querySelector(
|
|
145
|
+
`.${this.cssClasses.close}`,
|
|
146
|
+
);
|
|
147
|
+
if (closeBtn) {
|
|
148
|
+
closeBtn.addEventListener("click", () => this.hide());
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Drag start on header
|
|
152
|
+
this.headerElement.addEventListener("mousedown", (e) => {
|
|
153
|
+
if (e.target.classList.contains(this.cssClasses.close)) return;
|
|
154
|
+
this.startDrag(e);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Resize start on handles
|
|
158
|
+
this.element
|
|
159
|
+
.querySelectorAll(`.${this.cssClasses.resizeHandle}`)
|
|
160
|
+
.forEach((handle) => {
|
|
161
|
+
handle.addEventListener("mousedown", (e) => {
|
|
162
|
+
this.startResize(e, handle.dataset.direction);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Bring to front on click — if the window isn't focused, consume the
|
|
167
|
+
// first click so it only brings the window to front without interacting
|
|
168
|
+
// with content like text areas. Buttons and interactive controls are
|
|
169
|
+
// allowed through so they respond on the first click.
|
|
170
|
+
this._consumeNextClick = false;
|
|
171
|
+
this.element.addEventListener("mousedown", (e) => {
|
|
172
|
+
const wasFocused = this.element.classList.contains("focused");
|
|
173
|
+
if (this.onFocus) this.onFocus(this.id);
|
|
174
|
+
if (this.focusCanvas && !this.headerElement.contains(e.target)) {
|
|
175
|
+
// Focus the emulator canvas so keystrokes go to the emulator.
|
|
176
|
+
// Blur first to release any focused element (e.g. BASIC textarea),
|
|
177
|
+
// then focus the canvas on next frame after browser focus handling.
|
|
178
|
+
if (document.activeElement && document.activeElement !== document.body) {
|
|
179
|
+
document.activeElement.blur();
|
|
180
|
+
}
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
const canvas = document.getElementById("screen");
|
|
183
|
+
if (canvas) setTimeout(() => canvas.focus(), 0);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!wasFocused && !this.headerElement.contains(e.target)) {
|
|
187
|
+
const clickable = e.target.closest("button, input, select, label, a, .toggle-switch");
|
|
188
|
+
if (!clickable) {
|
|
189
|
+
this._consumeNextClick = true;
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
e.stopPropagation();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, true);
|
|
195
|
+
this.element.addEventListener("click", (e) => {
|
|
196
|
+
if (this._consumeNextClick) {
|
|
197
|
+
this._consumeNextClick = false;
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
e.stopPropagation();
|
|
200
|
+
}
|
|
201
|
+
}, true);
|
|
202
|
+
|
|
203
|
+
// Global mouse events for drag/resize
|
|
204
|
+
document.addEventListener("mousemove", this.handleMouseMove);
|
|
205
|
+
document.addEventListener("mouseup", this.handleMouseUp);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Handle mouse down for drag/resize detection
|
|
210
|
+
*/
|
|
211
|
+
handleMouseDown(e) {
|
|
212
|
+
// Handled by specific listeners
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Handle mouse move for dragging and resizing
|
|
217
|
+
*/
|
|
218
|
+
handleMouseMove(e) {
|
|
219
|
+
if (this.isDragging) {
|
|
220
|
+
this.drag(e);
|
|
221
|
+
} else if (this.isResizing) {
|
|
222
|
+
this.resize(e);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Handle mouse up to end drag/resize
|
|
228
|
+
*/
|
|
229
|
+
handleMouseUp(e) {
|
|
230
|
+
if (this.isDragging || this.isResizing) {
|
|
231
|
+
this.isDragging = false;
|
|
232
|
+
this.isResizing = false;
|
|
233
|
+
this.element.classList.remove("dragging", "resizing");
|
|
234
|
+
if (this.onStateChange) this.onStateChange();
|
|
235
|
+
this.saveSettings();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Start dragging the window
|
|
241
|
+
*/
|
|
242
|
+
startDrag(e) {
|
|
243
|
+
this.isDragging = true;
|
|
244
|
+
this.element.classList.add("dragging");
|
|
245
|
+
const rect = this.element.getBoundingClientRect();
|
|
246
|
+
this.dragOffset = {
|
|
247
|
+
x: e.clientX - rect.left,
|
|
248
|
+
y: e.clientY - rect.top,
|
|
249
|
+
};
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Handle drag movement
|
|
255
|
+
*/
|
|
256
|
+
drag(e) {
|
|
257
|
+
let x = e.clientX - this.dragOffset.x;
|
|
258
|
+
let y = e.clientY - this.dragOffset.y;
|
|
259
|
+
|
|
260
|
+
// Get header and footer heights to prevent dragging under/over them
|
|
261
|
+
const header = document.querySelector("header");
|
|
262
|
+
const footer = document.querySelector("footer");
|
|
263
|
+
const minY = header ? header.offsetHeight : 0;
|
|
264
|
+
const footerHeight = footer ? footer.offsetHeight : 0;
|
|
265
|
+
|
|
266
|
+
// Keep window on screen, below header, and above footer
|
|
267
|
+
const maxX = window.innerWidth - this.element.offsetWidth;
|
|
268
|
+
const maxY = window.innerHeight - footerHeight - this.element.offsetHeight;
|
|
269
|
+
x = Math.max(0, Math.min(x, maxX));
|
|
270
|
+
y = Math.max(minY, Math.min(y, maxY));
|
|
271
|
+
|
|
272
|
+
this.element.style.left = `${x}px`;
|
|
273
|
+
this.element.style.top = `${y}px`;
|
|
274
|
+
this.currentX = x;
|
|
275
|
+
this.currentY = y;
|
|
276
|
+
|
|
277
|
+
// Update edge distances after drag
|
|
278
|
+
this.updateEdgeDistances();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Update tracked distances from right and bottom edges
|
|
283
|
+
*/
|
|
284
|
+
updateEdgeDistances() {
|
|
285
|
+
const viewportWidth = window.innerWidth;
|
|
286
|
+
const viewportHeight = window.innerHeight;
|
|
287
|
+
const footer = document.querySelector("footer");
|
|
288
|
+
const footerHeight = footer ? footer.offsetHeight : 0;
|
|
289
|
+
const maxBottom = viewportHeight - footerHeight;
|
|
290
|
+
const centerX = this.currentX + this.currentWidth / 2;
|
|
291
|
+
const centerY = this.currentY + this.currentHeight / 2;
|
|
292
|
+
|
|
293
|
+
// Track distance from right edge if window is on the right half
|
|
294
|
+
if (centerX > viewportWidth / 2) {
|
|
295
|
+
this.distanceFromRight =
|
|
296
|
+
viewportWidth - (this.currentX + this.currentWidth);
|
|
297
|
+
} else {
|
|
298
|
+
this.distanceFromRight = null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Track distance from bottom edge (above footer) if window is on the bottom half
|
|
302
|
+
if (centerY > viewportHeight / 2) {
|
|
303
|
+
this.distanceFromBottom =
|
|
304
|
+
maxBottom - (this.currentY + this.currentHeight);
|
|
305
|
+
} else {
|
|
306
|
+
this.distanceFromBottom = null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
this.lastViewportWidth = viewportWidth;
|
|
310
|
+
this.lastViewportHeight = viewportHeight;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Start resizing the window
|
|
315
|
+
*/
|
|
316
|
+
startResize(e, direction) {
|
|
317
|
+
if (this.onFocus) this.onFocus(this.id);
|
|
318
|
+
this.isResizing = true;
|
|
319
|
+
this.resizeDirection = direction;
|
|
320
|
+
this.element.classList.add("resizing");
|
|
321
|
+
const rect = this.element.getBoundingClientRect();
|
|
322
|
+
this.resizeStart = {
|
|
323
|
+
x: e.clientX,
|
|
324
|
+
y: e.clientY,
|
|
325
|
+
width: rect.width,
|
|
326
|
+
height: rect.height,
|
|
327
|
+
left: rect.left,
|
|
328
|
+
top: rect.top,
|
|
329
|
+
};
|
|
330
|
+
e.preventDefault();
|
|
331
|
+
e.stopPropagation();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Handle resize movement
|
|
336
|
+
*/
|
|
337
|
+
resize(e) {
|
|
338
|
+
const dx = e.clientX - this.resizeStart.x;
|
|
339
|
+
const dy = e.clientY - this.resizeStart.y;
|
|
340
|
+
const dir = this.resizeDirection;
|
|
341
|
+
|
|
342
|
+
// Get header and footer heights for bounds checking
|
|
343
|
+
const header = document.querySelector("header");
|
|
344
|
+
const footer = document.querySelector("footer");
|
|
345
|
+
const minTop = header ? header.offsetHeight : 0;
|
|
346
|
+
const footerHeight = footer ? footer.offsetHeight : 0;
|
|
347
|
+
const maxBottom = window.innerHeight - footerHeight;
|
|
348
|
+
|
|
349
|
+
let newWidth = this.resizeStart.width;
|
|
350
|
+
let newHeight = this.resizeStart.height;
|
|
351
|
+
let newLeft = this.resizeStart.left;
|
|
352
|
+
let newTop = this.resizeStart.top;
|
|
353
|
+
|
|
354
|
+
// Calculate new dimensions based on direction
|
|
355
|
+
if (dir.includes("e")) {
|
|
356
|
+
newWidth = Math.min(this.maxWidth, Math.max(this.minWidth, this.resizeStart.width + dx));
|
|
357
|
+
}
|
|
358
|
+
if (dir.includes("w")) {
|
|
359
|
+
const proposedWidth = Math.min(this.maxWidth, this.resizeStart.width - dx);
|
|
360
|
+
if (proposedWidth >= this.minWidth) {
|
|
361
|
+
newWidth = proposedWidth;
|
|
362
|
+
newLeft = this.resizeStart.left + (this.resizeStart.width - proposedWidth);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (dir.includes("s")) {
|
|
366
|
+
newHeight = Math.min(this.maxHeight, Math.max(this.minHeight, this.resizeStart.height + dy));
|
|
367
|
+
}
|
|
368
|
+
if (dir.includes("n")) {
|
|
369
|
+
const proposedHeight = Math.min(this.maxHeight, this.resizeStart.height - dy);
|
|
370
|
+
if (proposedHeight >= this.minHeight) {
|
|
371
|
+
newHeight = proposedHeight;
|
|
372
|
+
newTop = this.resizeStart.top + (this.resizeStart.height - proposedHeight);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Keep on screen (respect header and footer)
|
|
377
|
+
newLeft = Math.max(0, newLeft);
|
|
378
|
+
newTop = Math.max(minTop, newTop);
|
|
379
|
+
if (newLeft + newWidth > window.innerWidth) {
|
|
380
|
+
newWidth = window.innerWidth - newLeft;
|
|
381
|
+
}
|
|
382
|
+
if (newTop + newHeight > maxBottom) {
|
|
383
|
+
newHeight = maxBottom - newTop;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Re-apply minimum constraints after viewport clamping, shifting
|
|
387
|
+
// position if needed so the window stays on screen at its min size
|
|
388
|
+
if (newWidth < this.minWidth) {
|
|
389
|
+
newWidth = this.minWidth;
|
|
390
|
+
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - newWidth));
|
|
391
|
+
}
|
|
392
|
+
if (newHeight < this.minHeight) {
|
|
393
|
+
newHeight = this.minHeight;
|
|
394
|
+
newTop = Math.max(minTop, Math.min(newTop, maxBottom - newHeight));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
this.element.style.width = `${newWidth}px`;
|
|
398
|
+
this.element.style.height = `${newHeight}px`;
|
|
399
|
+
this.element.style.left = `${newLeft}px`;
|
|
400
|
+
this.element.style.top = `${newTop}px`;
|
|
401
|
+
this.currentWidth = newWidth;
|
|
402
|
+
this.currentHeight = newHeight;
|
|
403
|
+
this.currentX = newLeft;
|
|
404
|
+
this.currentY = newTop;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Show the window
|
|
409
|
+
*/
|
|
410
|
+
show() {
|
|
411
|
+
this.element.classList.remove("hidden");
|
|
412
|
+
this.isVisible = true;
|
|
413
|
+
// Ensure window is within viewport when shown
|
|
414
|
+
this.constrainToViewport();
|
|
415
|
+
if (this.onFocus) this.onFocus(this.id);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Hide the window
|
|
420
|
+
*/
|
|
421
|
+
hide() {
|
|
422
|
+
// Set visibility flag first so getState() returns correct value
|
|
423
|
+
this.isVisible = false;
|
|
424
|
+
// Save state BEFORE adding hidden class, since getBoundingClientRect returns zeros for display:none
|
|
425
|
+
if (this.onStateChange) this.onStateChange();
|
|
426
|
+
this.saveSettings();
|
|
427
|
+
this.element.classList.add("hidden");
|
|
428
|
+
// Refocus canvas for keyboard input
|
|
429
|
+
const canvas = document.getElementById("screen");
|
|
430
|
+
if (canvas) {
|
|
431
|
+
setTimeout(() => canvas.focus(), 0);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Toggle window visibility
|
|
437
|
+
*/
|
|
438
|
+
toggle() {
|
|
439
|
+
if (this.isVisible) {
|
|
440
|
+
this.hide();
|
|
441
|
+
} else {
|
|
442
|
+
this.show();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Set window z-index
|
|
448
|
+
*/
|
|
449
|
+
setZIndex(z) {
|
|
450
|
+
this.zIndex = z;
|
|
451
|
+
this.element.style.zIndex = z;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Whether this window supports resizing
|
|
456
|
+
*/
|
|
457
|
+
get isResizable() {
|
|
458
|
+
return this.resizeDirections.length > 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get window state for persistence
|
|
463
|
+
*/
|
|
464
|
+
getState() {
|
|
465
|
+
// Use tracked values instead of getBoundingClientRect which returns zeros for hidden elements
|
|
466
|
+
const state = {
|
|
467
|
+
x: this.currentX,
|
|
468
|
+
y: this.currentY,
|
|
469
|
+
visible: this.isVisible,
|
|
470
|
+
zIndex: this.zIndex,
|
|
471
|
+
};
|
|
472
|
+
// Only persist size for resizable windows; fixed-size windows always use their defaults
|
|
473
|
+
if (this.isResizable) {
|
|
474
|
+
state.width = this.currentWidth;
|
|
475
|
+
state.height = this.currentHeight;
|
|
476
|
+
}
|
|
477
|
+
return state;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Restore window state from persistence
|
|
482
|
+
*/
|
|
483
|
+
restoreState(state) {
|
|
484
|
+
if (state.x !== undefined) {
|
|
485
|
+
this.element.style.left = `${state.x}px`;
|
|
486
|
+
this.currentX = state.x;
|
|
487
|
+
}
|
|
488
|
+
if (state.y !== undefined) {
|
|
489
|
+
this.element.style.top = `${state.y}px`;
|
|
490
|
+
this.currentY = state.y;
|
|
491
|
+
}
|
|
492
|
+
// Only restore size for resizable windows; fixed-size windows keep their defaults
|
|
493
|
+
if (this.isResizable) {
|
|
494
|
+
if (state.width !== undefined) {
|
|
495
|
+
const width = Math.min(this.maxWidth, Math.max(state.width, this.minWidth));
|
|
496
|
+
this.element.style.width = `${width}px`;
|
|
497
|
+
this.currentWidth = width;
|
|
498
|
+
}
|
|
499
|
+
if (state.height !== undefined) {
|
|
500
|
+
const height = Math.min(this.maxHeight, Math.max(state.height, this.minHeight));
|
|
501
|
+
this.element.style.height = `${height}px`;
|
|
502
|
+
this.currentHeight = height;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Ensure window is within current viewport bounds
|
|
507
|
+
this.constrainToViewport();
|
|
508
|
+
|
|
509
|
+
// Calculate edge distances based on restored position
|
|
510
|
+
this.updateEdgeDistances();
|
|
511
|
+
|
|
512
|
+
if (state.visible) {
|
|
513
|
+
this.show();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Constrain window position to keep it within the visible viewport
|
|
519
|
+
* Maintains distance from right/bottom edges for windows on those sides
|
|
520
|
+
*/
|
|
521
|
+
constrainToViewport() {
|
|
522
|
+
if (!this.element) return;
|
|
523
|
+
|
|
524
|
+
const viewportWidth = window.innerWidth;
|
|
525
|
+
const viewportHeight = window.innerHeight;
|
|
526
|
+
const width = this.currentWidth;
|
|
527
|
+
const height = this.currentHeight;
|
|
528
|
+
|
|
529
|
+
// Get header and footer heights to prevent windows going under/over them
|
|
530
|
+
const header = document.querySelector("header");
|
|
531
|
+
const footer = document.querySelector("footer");
|
|
532
|
+
const minTop = header ? header.offsetHeight : 0;
|
|
533
|
+
const footerHeight = footer ? footer.offsetHeight : 0;
|
|
534
|
+
const maxBottom = viewportHeight - footerHeight;
|
|
535
|
+
|
|
536
|
+
let newLeft = this.currentX;
|
|
537
|
+
let newTop = this.currentY;
|
|
538
|
+
let changed = false;
|
|
539
|
+
|
|
540
|
+
// If window was on the right side, maintain distance from right edge
|
|
541
|
+
if (this.distanceFromRight !== null) {
|
|
542
|
+
const targetLeft = viewportWidth - width - this.distanceFromRight;
|
|
543
|
+
if (targetLeft !== newLeft) {
|
|
544
|
+
newLeft = targetLeft;
|
|
545
|
+
changed = true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// If window was on the bottom side, maintain distance from bottom edge
|
|
550
|
+
if (this.distanceFromBottom !== null) {
|
|
551
|
+
const targetTop = maxBottom - height - this.distanceFromBottom;
|
|
552
|
+
if (targetTop !== newTop) {
|
|
553
|
+
newTop = targetTop;
|
|
554
|
+
changed = true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Ensure window stays within viewport bounds
|
|
559
|
+
if (width >= viewportWidth) {
|
|
560
|
+
newLeft = 0;
|
|
561
|
+
changed = true;
|
|
562
|
+
} else if (newLeft + width > viewportWidth) {
|
|
563
|
+
newLeft = viewportWidth - width;
|
|
564
|
+
changed = true;
|
|
565
|
+
} else if (newLeft < 0) {
|
|
566
|
+
newLeft = 0;
|
|
567
|
+
changed = true;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (height >= maxBottom - minTop) {
|
|
571
|
+
newTop = minTop;
|
|
572
|
+
changed = true;
|
|
573
|
+
} else if (newTop + height > maxBottom) {
|
|
574
|
+
newTop = maxBottom - height;
|
|
575
|
+
changed = true;
|
|
576
|
+
} else if (newTop < minTop) {
|
|
577
|
+
newTop = minTop;
|
|
578
|
+
changed = true;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (changed) {
|
|
582
|
+
this.element.style.left = `${newLeft}px`;
|
|
583
|
+
this.element.style.top = `${newTop}px`;
|
|
584
|
+
this.currentX = newLeft;
|
|
585
|
+
this.currentY = newTop;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Update viewport tracking
|
|
589
|
+
this.lastViewportWidth = viewportWidth;
|
|
590
|
+
this.lastViewportHeight = viewportHeight;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Override in subclasses to provide window content HTML
|
|
595
|
+
*/
|
|
596
|
+
renderContent() {
|
|
597
|
+
return "<p>Override renderContent() in subclass</p>";
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Override in subclasses to update window content.
|
|
602
|
+
* Only called when the window is visible.
|
|
603
|
+
*/
|
|
604
|
+
update(wasmModule) {
|
|
605
|
+
// Override in subclasses
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Override in subclasses to set up additional event listeners
|
|
610
|
+
*/
|
|
611
|
+
setupContentEventListeners() {
|
|
612
|
+
// Override in subclasses
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Helper to format a hex byte
|
|
617
|
+
*/
|
|
618
|
+
formatHex(value, digits = 2) {
|
|
619
|
+
return value.toString(16).toUpperCase().padStart(digits, "0");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Helper to format a hex address
|
|
624
|
+
*/
|
|
625
|
+
formatAddr(value) {
|
|
626
|
+
return "$" + this.formatHex(value, 4);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Save window state to localStorage (if storageKey is configured)
|
|
631
|
+
*/
|
|
632
|
+
saveSettings() {
|
|
633
|
+
if (!this.storageKey) return;
|
|
634
|
+
try {
|
|
635
|
+
const settings = {
|
|
636
|
+
x: this.currentX,
|
|
637
|
+
y: this.currentY,
|
|
638
|
+
visible: this.isVisible,
|
|
639
|
+
};
|
|
640
|
+
if (this.isResizable) {
|
|
641
|
+
settings.width = this.currentWidth;
|
|
642
|
+
settings.height = this.currentHeight;
|
|
643
|
+
}
|
|
644
|
+
localStorage.setItem(this.storageKey, JSON.stringify(settings));
|
|
645
|
+
} catch (e) {
|
|
646
|
+
console.warn(`Failed to save ${this.storageKey} settings:`, e.message);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Load window state from localStorage (if storageKey is configured)
|
|
652
|
+
*/
|
|
653
|
+
loadSettings() {
|
|
654
|
+
if (!this.storageKey) return;
|
|
655
|
+
try {
|
|
656
|
+
const saved = localStorage.getItem(this.storageKey);
|
|
657
|
+
if (saved) {
|
|
658
|
+
const state = JSON.parse(saved);
|
|
659
|
+
if (state.x !== undefined) this.currentX = state.x;
|
|
660
|
+
if (state.y !== undefined) this.currentY = state.y;
|
|
661
|
+
|
|
662
|
+
this.element.style.left = `${this.currentX}px`;
|
|
663
|
+
this.element.style.top = `${this.currentY}px`;
|
|
664
|
+
|
|
665
|
+
// Only restore size for resizable windows; fixed-size windows keep their defaults
|
|
666
|
+
if (this.isResizable) {
|
|
667
|
+
if (state.width !== undefined)
|
|
668
|
+
this.currentWidth = Math.min(this.maxWidth, Math.max(state.width, this.minWidth));
|
|
669
|
+
if (state.height !== undefined)
|
|
670
|
+
this.currentHeight = Math.min(this.maxHeight, Math.max(state.height, this.minHeight));
|
|
671
|
+
this.element.style.width = `${this.currentWidth}px`;
|
|
672
|
+
this.element.style.height = `${this.currentHeight}px`;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} catch (e) {
|
|
676
|
+
console.warn(`Failed to load ${this.storageKey} settings:`, e.message);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Clean up event listeners and remove element from DOM
|
|
682
|
+
*/
|
|
683
|
+
destroy() {
|
|
684
|
+
document.removeEventListener("mousemove", this.handleMouseMove);
|
|
685
|
+
document.removeEventListener("mouseup", this.handleMouseUp);
|
|
686
|
+
if (this.element && this.element.parentNode) {
|
|
687
|
+
this.element.parentNode.removeChild(this.element);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|