Fix copy paste outside of screen (#143)

This commit is contained in:
Will Scullin 2022-07-16 20:50:15 -07:00 committed by GitHub
parent c0ff1e8129
commit 087dbd3602
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 93 additions and 39 deletions

View File

@ -46,7 +46,7 @@
</div> </div>
<div id="display"> <div id="display">
<div class="overscan"> <div class="overscan">
<canvas id="screen" width="592" height="416"></canvas> <canvas id="screen" width="592" height="416" tabindex="-1"></canvas>
</div> </div>
</div> </div>
<div class="inset"> <div class="inset">
@ -265,7 +265,7 @@
</button> </button>
</header> </header>
<main class="modal__content" id="printer-modal-content"> <main class="modal__content" id="printer-modal-content">
<div class="paper"></div> <div class="paper" tabindex="-1"></div>
</main> </main>
<footer class="modal__footer"> <footer class="modal__footer">
<a id="raw_printer_output" class="button">Download Raw Output</a> <a id="raw_printer_output" class="button">Download Raw Output</a>

View File

@ -46,7 +46,7 @@
</div> </div>
<div id="display"> <div id="display">
<div class="overscan"> <div class="overscan">
<canvas id="screen" width="592" height="416"></canvas> <canvas id="screen" width="592" height="416" tabindex="-1"></canvas>
</div> </div>
</div> </div>
<div class="inset"> <div class="inset">
@ -270,7 +270,7 @@
</button> </button>
</header> </header>
<main class="modal__content" id="printer-modal-content"> <main class="modal__content" id="printer-modal-content">
<div class="paper"></div> <div class="paper" tabindex="-1"></div>
</main> </main>
<footer class="modal__footer"> <footer class="modal__footer">
<a id="raw_printer_output" class="button">Download Raw Output</a> <a id="raw_printer_output" class="button">Download Raw Output</a>

View File

@ -9,6 +9,7 @@ img {
#badge { #badge {
cursor: pointer; cursor: pointer;
user-select: none;
} }
#subtitle { #subtitle {
@ -18,6 +19,7 @@ img {
font-family: "Adobe Garamond Pro",Garamond,Times; font-family: "Adobe Garamond Pro",Garamond,Times;
font-size: 13px; font-size: 13px;
font-weight: normal; font-weight: normal;
user-select: none;
} }
.motter { .motter {
@ -135,6 +137,7 @@ body {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
flex-grow: 1; flex-grow: 1;
user-select: none;
} }
.code { .code {
@ -180,6 +183,7 @@ th {
color: #0f0; color: #0f0;
border: 2px inset #888; border: 2px inset #888;
border-radius: 4px; border-radius: 4px;
user-select: none;
} }
canvas { canvas {
@ -308,6 +312,7 @@ canvas {
padding: 5px 11px; padding: 5px 11px;
border: 1px outset #66594E; border: 1px outset #66594E;
border-radius: 3px; border-radius: 3px;
user-select: none;
} }
.modal__title { .modal__title {
@ -338,28 +343,34 @@ canvas {
.modal__footer { .modal__footer {
text-align: right; text-align: right;
user-select: none;
} }
button { button,
a.button {
background: #44372C; background: #44372C;
color: #fff; color: #fff;
padding: 2px 8px; padding: 2px 8px;
border: 1px outset #66594E; border: 1px outset #66594E;
border-radius: 3px; border-radius: 3px;
font-size: 15px; font-size: 15px;
text-decoration: none;
} }
button:hover { button:hover,
a.button:hover {
background-color: #55473D; background-color: #55473D;
border: 1px outset #66594E; border: 1px outset #66594E;
} }
button:active { button:active,
a.button:active {
background-color: #22150A; background-color: #22150A;
border: 1px outset #44372C; border: 1px outset #44372C;
} }
button:focus { button:focus,
a.button:hover {
outline: none; outline: none;
} }

View File

@ -47,7 +47,7 @@ export interface Apple2Props {
*/ */
export const Apple2 = (props: Apple2Props) => { export const Apple2 = (props: Apple2Props) => {
const { e, enhanced, sectors } = props; const { e, enhanced, sectors } = props;
const screen = useRef<HTMLCanvasElement>(null); const screenRef = useRef<HTMLCanvasElement>(null);
const [apple2, setApple2] = useState<Apple2Impl>(); const [apple2, setApple2] = useState<Apple2Impl>();
const [error, setError] = useState<unknown>(); const [error, setError] = useState<unknown>();
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
@ -60,6 +60,12 @@ export const Apple2 = (props: Apple2Props) => {
const rom = apple2?.getROM(); const rom = apple2?.getROM();
const doPaste = useCallback((event: Event) => { const doPaste = useCallback((event: Event) => {
if (
(document.activeElement !== screenRef.current) &&
(document.activeElement !== document.body)
) {
return;
}
if (io) { if (io) {
const paste = (event.clipboardData || window.clipboardData)?.getData('text'); const paste = (event.clipboardData || window.clipboardData)?.getData('text');
if (paste) { if (paste) {
@ -70,6 +76,12 @@ export const Apple2 = (props: Apple2Props) => {
}, [io]); }, [io]);
const doCopy = useCallback((event: Event) => { const doCopy = useCallback((event: Event) => {
if (
(document.activeElement !== screenRef.current) &&
(document.activeElement !== document.body)
) {
return;
}
if (vm) { if (vm) {
event.clipboardData?.setData('text/plain', vm.getText()); event.clipboardData?.setData('text/plain', vm.getText());
} }
@ -77,9 +89,9 @@ export const Apple2 = (props: Apple2Props) => {
}, [vm]); }, [vm]);
useEffect(() => { useEffect(() => {
if (screen.current) { if (screenRef.current) {
const options = { const options = {
canvas: screen.current, canvas: screenRef.current,
tick: () => { /* do nothing */ }, tick: () => { /* do nothing */ },
...props, ...props,
}; };
@ -111,12 +123,20 @@ export const Apple2 = (props: Apple2Props) => {
}, [props, drivesReady]); }, [props, drivesReady]);
useEffect(() => { useEffect(() => {
const { current } = screenRef;
window.addEventListener('paste', doPaste); window.addEventListener('paste', doPaste);
window.addEventListener('copy', doCopy); window.addEventListener('copy', doCopy);
current?.addEventListener('paste', doPaste);
current?.addEventListener('copy', doCopy);
return () => { return () => {
window.removeEventListener('paste', doPaste); window.removeEventListener('paste', doPaste);
window.removeEventListener('copy', doCopy); window.removeEventListener('copy', doCopy);
current?.removeEventListener('paste', doPaste);
current?.removeEventListener('copy', doCopy);
}; };
}, [doCopy, doPaste]); }, [doCopy, doPaste]);
@ -124,23 +144,16 @@ export const Apple2 = (props: Apple2Props) => {
setShowDebug((on) => !on); setShowDebug((on) => !on);
}, []); }, []);
const removeFocus = useCallback(() => {
if (document?.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, []);
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div <div
className={cs(styles.outer, { apple2e: e, [styles.ready]: ready })} className={cs(styles.outer, { apple2e: e, [styles.ready]: ready })}
onClick={removeFocus}
> >
<Screen screen={screen} /> <Screen screenRef={screenRef} />
{!e ? <LanguageCard cpu={cpu} io={io} rom={rom} slot={0} /> : null} {!e ? <LanguageCard cpu={cpu} io={io} rom={rom} slot={0} /> : null}
<Slinky io={io} slot={2} /> <Slinky io={io} slot={2} />
{!e ? <Videoterm io={io} slot={3} /> : null} {!e ? <Videoterm io={io} slot={3} /> : null}
<Mouse cpu={cpu} screen={screen} io={io} slot={4} /> <Mouse cpu={cpu} screenRef={screenRef} io={io} slot={4} />
<ThunderClock io={io} slot={5} /> <ThunderClock io={io} slot={5} />
<Inset> <Inset>
<Drives cpu={cpu} io={io} sectors={sectors} enhanced={enhanced} ready={drivesReady} /> <Drives cpu={cpu} io={io} sectors={sectors} enhanced={enhanced} ready={drivesReady} />

View File

@ -11,7 +11,7 @@ import { useEffect } from 'preact/hooks';
export interface MouseProps { export interface MouseProps {
cpu: CPU6502 | undefined; cpu: CPU6502 | undefined;
io: Apple2IO | undefined; io: Apple2IO | undefined;
screen: RefObject<HTMLCanvasElement>; screenRef: RefObject<HTMLCanvasElement>;
slot: slot; slot: slot;
} }
@ -24,14 +24,14 @@ export interface MouseProps {
* @param slot Slot to register card in * @param slot Slot to register card in
* @returns Mouse component * @returns Mouse component
*/ */
export const Mouse = ({ cpu, screen, io, slot }: MouseProps) => { export const Mouse = ({ cpu, screenRef, io, slot }: MouseProps) => {
useEffect(() => { useEffect(() => {
if (cpu && io && screen.current) { if (cpu && io && screenRef.current) {
const mouseUI = new MouseUI(screen.current); const mouseUI = new MouseUI(screenRef.current);
const mouse = new MouseCard(cpu, mouseUI); const mouse = new MouseCard(cpu, mouseUI);
io.setSlot(slot, mouse); io.setSlot(slot, mouse);
} }
}, [cpu, io, screen, slot]); }, [cpu, io, screenRef, slot]);
return null; return null;
}; };

View File

@ -80,7 +80,7 @@ export const Printer = ({ io, slot }: PrinterProps) => {
<> <>
<Modal isOpen={isOpen} onClose={onClose} title="Printer"> <Modal isOpen={isOpen} onClose={onClose} title="Printer">
<ModalContent> <ModalContent>
<pre className={styles.printer}> <pre className={styles.printer} tabIndex={-1} >
{content} {content}
</pre> </pre>
</ModalContent> </ModalContent>

View File

@ -6,7 +6,7 @@ import styles from './css/Screen.module.css';
* Screen properties * Screen properties
*/ */
export interface ScreenProps { export interface ScreenProps {
screen: Ref<HTMLCanvasElement>; screenRef: Ref<HTMLCanvasElement>;
} }
/** /**
@ -16,7 +16,7 @@ export interface ScreenProps {
* @param screen Canvas element reference * @param screen Canvas element reference
* @returns * @returns
*/ */
export const Screen = ({ screen }: ScreenProps) => { export const Screen = ({ screenRef }: ScreenProps) => {
return ( return (
<div className={styles.display}> <div className={styles.display}>
<div className={styles.overscan}> <div className={styles.overscan}>
@ -24,7 +24,7 @@ export const Screen = ({ screen }: ScreenProps) => {
className={styles.screen} className={styles.screen}
width="592" width="592"
height="416" height="416"
ref={screen} ref={screenRef}
/> />
</div> </div>
</div> </div>

View File

@ -26,6 +26,7 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
flex-grow: 1; flex-grow: 1;
user-select: none;
} }
@media only screen and (min-resolution: 1.25dppx) { @media only screen and (min-resolution: 1.25dppx) {

View File

@ -11,4 +11,5 @@
color: #0f0; color: #0f0;
border: 2px inset #888; border: 2px inset #888;
border-radius: 4px; border-radius: 4px;
user-select: none;
} }

View File

@ -16,6 +16,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
user-select: none;
} }
.reset:hover { .reset:hover {

View File

@ -26,6 +26,7 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
flex-grow: 1; flex-grow: 1;
user-select: none;
} }
@media only screen and (min-resolution: 1.25dppx) { @media only screen and (min-resolution: 1.25dppx) {

View File

@ -19,6 +19,7 @@
.badge { .badge {
cursor: pointer; cursor: pointer;
user-select: none;
} }
.subtitle { .subtitle {
@ -28,4 +29,5 @@
font-family: "Adobe Garamond Pro", Garamond, Times, serif; font-family: "Adobe Garamond Pro", Garamond, Times, serif;
font-size: 13px; font-size: 13px;
font-weight: normal; font-weight: normal;
user-select: none;
} }

View File

@ -47,7 +47,7 @@ const Variable = ({ variable }: { variable: ApplesoftVariable }) => {
<tr> <tr>
<td>{name}{TYPE_SYMBOL[type]}{arrayStr}</td> <td>{name}{TYPE_SYMBOL[type]}{arrayStr}</td>
<td>{TYPE_NAME[type]}{isArray ? ' Array' : ''}</td> <td>{TYPE_NAME[type]}{isArray ? ' Array' : ''}</td>
<td>{isArray ? formatArray(value) : value}</td> <td><pre tabIndex={-1}>{isArray ? formatArray(value) : value}</pre></td>
</tr> </tr>
); );
}; };
@ -103,7 +103,7 @@ export const Applesoft = ({ apple2 }: ApplesoftProps) => {
return ( return (
<div className={styles.column}> <div className={styles.column}>
<span className={debuggerStyles.subHeading}>Listing</span> <span className={debuggerStyles.subHeading}>Listing</span>
<pre className={styles.listing}>{listing}</pre> <pre className={styles.listing} tabIndex={-1}>{listing}</pre>
<span className={debuggerStyles.subHeading}>Variables</span> <span className={debuggerStyles.subHeading}>Variables</span>
<div className={styles.variables}> <div className={styles.variables}>
<table> <table>

View File

@ -156,21 +156,21 @@ export const CPU = ({ apple2 }: CPUProps) => {
<div className={debuggerStyles.row}> <div className={debuggerStyles.row}>
<div className={debuggerStyles.column}> <div className={debuggerStyles.column}>
<span className={debuggerStyles.subHeading}>Registers</span> <span className={debuggerStyles.subHeading}>Registers</span>
<pre> <pre tabIndex={-1}>
{registers} {registers}
</pre> </pre>
<span className={debuggerStyles.subHeading}>Trace</span> <span className={debuggerStyles.subHeading}>Trace</span>
<pre className={styles.trace}> <pre className={styles.trace} tabIndex={-1}>
{trace} {trace}
</pre> </pre>
<span className={debuggerStyles.subHeading}>ZP</span> <span className={debuggerStyles.subHeading}>ZP</span>
<pre className={styles.zeroPage}> <pre className={styles.zeroPage} tabIndex={-1}>
{zeroPage} {zeroPage}
</pre> </pre>
</div> </div>
<div className={debuggerStyles.column}> <div className={debuggerStyles.column}>
<span className={debuggerStyles.subHeading}>Stack</span> <span className={debuggerStyles.subHeading}>Stack</span>
<pre className={styles.stack}> <pre className={styles.stack} tabIndex={-1}>
{stack} {stack}
</pre> </pre>
</div> </div>
@ -185,7 +185,7 @@ export const CPU = ({ apple2 }: CPUProps) => {
className={cs({ [styles.invalid]: !memoryPageValid })} className={cs({ [styles.invalid]: !memoryPageValid })}
/> />
{memoryPageValid ? null : ERROR_ICON} {memoryPageValid ? null : ERROR_ICON}
<pre className={styles.zp}> <pre className={styles.zp} tabIndex={-1}>
{memory} {memory}
</pre> </pre>
</div> </div>

View File

@ -908,15 +908,39 @@ function onLoaded(
* Input Handling * Input Handling
*/ */
window.addEventListener('paste', (event: Event) => { const screenElement = document.querySelector('#screen')!;
const doPaste = (event: Event) => {
const paste = (event.clipboardData || window.clipboardData)!.getData('text'); const paste = (event.clipboardData || window.clipboardData)!.getData('text');
io.setKeyBuffer(paste); io.setKeyBuffer(paste);
event.preventDefault(); event.preventDefault();
};
const doCopy = (event: Event) => {
event.clipboardData!.setData('text/plain', vm.getText());
event.preventDefault();
};
window.addEventListener('paste', (event: Event) => {
if (document.activeElement && document.activeElement !== document.body) {
return;
}
doPaste(event);
}); });
window.addEventListener('copy', (event: Event) => { window.addEventListener('copy', (event: Event) => {
event.clipboardData!.setData('text/plain', vm.getText()); if (document.activeElement && document.activeElement !== document.body) {
event.preventDefault(); return;
}
doCopy(event);
});
screenElement.addEventListener('copy', (event: Event) => {
doCopy(event);
});
screenElement.addEventListener('paste', (event: Event) => {
doPaste(event);
}); });
if (navigator.standalone) { if (navigator.standalone) {