Skip to content

Commit

Permalink
port cat-printer keyboard-driven control (use tab to activate)
Browse files Browse the repository at this point in the history
  • Loading branch information
NaitLee committed Feb 17, 2024
1 parent d7dd77f commit a7f213e
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 21 deletions.
5 changes: 5 additions & 0 deletions common/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ function mktranslate(): (string: string, things?: Thing | Things) => Signal<stri
export const _ = mktranslate();
export const __ = i18n._;

//@ts-ignore:
globalThis._ = __;
//@ts-ignore:
globalThis.i18n = __;

export function updateI18nFromRequest(request: Request) {
i18n.reset();
for (const lang of acceptsLanguages(request))
Expand Down
4 changes: 2 additions & 2 deletions components/Printer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,10 @@ export default function Printer(props: PrinterProps) {
};
const print_menu = <div>
<div class="print-menu">
<button class="stuff stuff--button" style={{width:"80%"}} aria-label={_('print')} onClick={print}>
<button class="stuff stuff--button" style={{width:"80%"}} aria-label={_('print')} onClick={print} data-key="Enter">
<Icons.IconPrinter />
</button>
<button class="stuff stuff--button" style={{width:"20%"}} aria-label={_('settings')} onClick={()=>setSettingsVisible(!settingsVisible)}>
<button class="stuff stuff--button" style={{width:"20%"}} aria-label={_('settings')} onClick={()=>setSettingsVisible(!settingsVisible)} data-key="\">
<Icons.IconSettings />
</button>
</div>
Expand Down
10 changes: 5 additions & 5 deletions components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,30 @@ export default function ({ visible }: { visible: boolean }) {
return <div className={`${visible ? "print__options-container--visible" : ""} print__options-container`}>
<div className="stuff__option">
<span className="option__title">{_('finish-feed')}</span>
<input className="option__item" type="range" min={0} max={DEF_DPI * 2}
<input className="option__item" type="range" min={0} max={DEF_DPI * 2} data-key={visible ? "" : undefined}
step={DEF_DPI / IN_TO_CM / 2} value={finish_feed} onInput={mkset(set_finish_feed)} />
<span>{unit_label(finish_feed)}</span>
</div>

<div className="stuff__option">
<span className="option__title">{_('speed')}</span>
{Object.entries(SPEED_RANGE).map(([label, speed_]) => <button className="option__item" value={speed_}
onClick={() => set_speed(speed_)}
onClick={() => set_speed(speed_)} data-key={visible ? "" : undefined}
data-selected={speed === speed_}>
<span className="stuff__label">{_(label)}</span>
</button>)}
<input className="option__item" type="number" min={4} max={0xff} value={speed} onInput={mkset(set_speed)}/>
<input className="option__item" type="number" min={4} max={0xff} value={speed} onInput={mkset(set_speed)} data-key={visible ? "" : undefined} />
</div>

<div className="stuff__option">
<span className="option__title">{_('strength')}</span>
{Object.entries(ENERGY_RANGE).map(([label, energy_]) => <button className="option__item" value={energy_}
onClick={()=>set_energy(energy_)}
onClick={()=>set_energy(energy_)} data-key={visible ? "" : undefined}
data-selected={energy === energy_}>
<span className="stuff__label">{_(label)}</span>
</button>)}

<input className="option__item" type="number" min={0} max={0xFFFF} value={energy} onInput={mkset(set_energy)}/>
<input className="option__item" type="number" min={0} max={0xFFFF} value={energy} onInput={mkset(set_energy)} data-key={visible ? "" : undefined} />
</div>
</div>;
}
7 changes: 4 additions & 3 deletions components/Stuff.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export default function Stuff(props: StuffProps) {
onKeyUp={mkmodify('textContent')}
onBlur={mkmodify('textContent')}
placeholder={_('hello-world')}
data-key=""
>{stuff.textContent}</textarea>
</>;
options = <>
Expand Down Expand Up @@ -189,7 +190,7 @@ export default function Stuff(props: StuffProps) {
rotate(${stuff.rotate}deg)
scaleX(${stuff.flipH ? -1 : 1})
scaleY(${stuff.flipV ? -1 : 1});
`} width={384} height={384} />
`} width={384} height={384} data-key="" />
</div>
</>;
options = <>
Expand Down Expand Up @@ -262,7 +263,7 @@ export default function Stuff(props: StuffProps) {
<div class="stuff__title">
<StuffIcon size={32} />
{!show_options ? <>
<select onChange={change_type} name={'stuff-type-' + stuff.id.toString()}>
<select onChange={change_type} name={'stuff-type-' + stuff.id.toString()} data-key="">
<option value="text" selected={type === 'text'}>{_('text')}</option>
<option value="pic" selected={type === 'pic'}>{_('picture')}</option>
</select>
Expand Down Expand Up @@ -295,7 +296,7 @@ export default function Stuff(props: StuffProps) {
<button key="remove" class="stuff__button" aria-label={_("remove")} onClick={() => dispatch({
action: 'remove',
stuff: stuff
})}>
})} data-key="">
<Icons.IconX />
</button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion islands/KittyCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export default function KittyCanvas(props: KittyCanvasProps) {
action: 'add',
stuff: { type: 'text', id: 0 }
});
}}>
}} data-key=" ">
<Icons.IconPlus size={36} />
</button>
</div>
Expand Down
11 changes: 6 additions & 5 deletions islands/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { createRef } from "preact";
import { _ } from "../common/i18n.tsx";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { NavProps } from "../common/types.ts";
import { useState } from "preact/hooks";

export default function NavBar(props: NavProps) {
const ref_about = createRef();
const [show, set_show] = useState(false);
const url = new URL(props.url);
return <>
<nav class="nav">
Expand All @@ -14,15 +15,15 @@ export default function NavBar(props: NavProps) {
<span class="title__tag">{_('version^alpha')}</span>
</div>
<div class="nav-links">
<a class="nav-links__link" href="javascript:" onClick={() => ref_about.current.classList.toggle('about--visible')}>{_('about')}</a>
<a class="nav-links__link" href="javascript:" data-key="a" onClick={() => set_show(!show)}>{_('about')}</a>
</div>
</nav>
<div class="about" ref={ref_about}>
<div class={"about" + (show ? ' about--visible' : '')}>
<h2>{_('kitty-printer')}</h2>
<p>{_('web-app-for-bluetooth-kitty-printers')}</p>
<p>
<a class="inline-link" href="https://github.com/NaitLee/kitty-printer" target="_blank">{_('check-source-code')}</a>
<a href="https://fresh.deno.dev" class="fresh-logo">
<a class="inline-link" href="https://github.com/NaitLee/kitty-printer" target="_blank" data-key={show ? "s" : undefined}>{_('check-source-code')}</a>
<a href="https://fresh.deno.dev" class="fresh-logo" data-key={show ? "f" : undefined}>
<img
width="197"
height="37"
Expand Down
2 changes: 2 additions & 0 deletions routes/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export default async function App(req: Request, ctx: FreshContext) {
</head>
<body>
<ctx.Component />
<div id="keyboard-shortcuts-layer"></div>
<script src="accessibility.js"></script>
</body>
</html>
);
Expand Down
128 changes: 128 additions & 0 deletions static/accessibility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
`
No rights reserved.
License CC0-1.0-only: https://directory.fsf.org/wiki/License:CC0
`;

'use strict';

function isHidden(element) {
const parents = [element];
while (parents[0].parentElement)
parents.unshift(parents[0].parentElement);
return parents.some(e => {
const rect = e.getBoundingClientRect();
return (
e.classList.contains('hidden') ||
e.classList.contains('hard-hidden') ||
e.style.display == 'none' ||
rect.width == 0 || rect.height == 0 ||
// rect.x < 0 || rect.y < 0 ||
e.style.visibility == 'none' ||
e.style.opacity == '0'
);
});
}

function toLocaleKey(key) {
if (typeof i18n === 'undefined') return key;
const qwerty = '1234567890qwertyuiopasdfghjklzxcvbnm';
let keys, index;
if (key.length !== 1 ||
(keys = i18n('KeyboardLayout')) === 'KeyboardLayout' ||
(index = qwerty.indexOf(key)) === -1
) return key;
return keys[index];
}

function keyToLetter(key) {
const map = {
' ': 'SPACE',
',': 'COMMA',
'.': "DOT"
};
return map[key] || key;
}

function keyFromCode(code) {
const map = {
9: 'Tab'
};
return map[code] || String.fromCharCode(code);
}

function initKeyboardShortcuts() {
const layer = document.getElementById('keyboard-shortcuts-layer');
const dialog = document.getElementById('dialog');
const keys = 'qwertyuiopasdfghjklzxcvbnm';
let focusing = false, started = false;
let shortcuts = {};
let focus, inputs;
const mark_keys = () => {
document.querySelectorAll(':focus').forEach(e => e.isSameNode(focus) || e.blur());
let index, key;
inputs = Array.from(document.querySelectorAll(
dialog === null || dialog.classList.contains('hidden')
? '*[data-key]'
: '#dialog *[data-key]')
);
/** @type {{ [key: string]: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement }} */
const keys2 = keys.split('');
shortcuts = {};
if (focusing) shortcuts = { 'ESC': focus };
else
for (const input of inputs) {
if (isHidden(input)) continue;
key = input.getAttribute('data-key');
if ((index = keys2.indexOf(key)) !== -1) keys2.splice(index, 1);
key = toLocaleKey(key || keys2.shift());
shortcuts[key] = input;
}
// Array.from(layer.children).forEach(e => e.remove());
for (let i = layer.children.length; i <= inputs.length; i++) {
const span = document.createElement('span');
layer.appendChild(span);
}
index = 0;
for (key in shortcuts) {
const span = layer.children[index++];
const input = shortcuts[key];
const position = input.getBoundingClientRect();
const text = i18n(keyToLetter(key.toUpperCase()));
if (span.innerText !== text) span.innerText = text;
span.style.top = (position.y || position.top) + 'px';
span.style.left = (position.x || position.left) + 'px';
span.style.display = '';
}
for (let i = index; i < layer.children.length; i++) {
layer.children[i].style.display = 'none';
}
}
const start = () => setInterval(mark_keys, 1000);
const types_to_click = ['submit', 'file', 'checkbox', 'radio', 'A', 'IMG'];
document.body.addEventListener('keyup', (event) => {
const key = event.key || keyFromCode(event.keyCode);
if (!started) {
if (key !== 'Tab') return;
mark_keys();
start();
started = true;
}
requestAnimationFrame(mark_keys)
const input = shortcuts[key];
if (input) {
if (types_to_click.includes(input.type || input.tagName))
input.dispatchEvent(new MouseEvent(event.shiftKey ? 'contextmenu' : 'click'));
else {
input.focus();
focusing = true;
}
focus = input;
} else if ((key === 'Escape' || !event.isTrusted) && focus) {
focus.blur();
focusing = !focusing;
}
});
}

initKeyboardShortcuts();
9 changes: 8 additions & 1 deletion static/lang/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,12 @@
"click-to-load-fonts": "Click to load fonts",
"serif": "Serif",
"sans-serif": "Sans-Serif",
"monospace": "Monospace"
"monospace": "Monospace",
"ENTER": "Enter",
"SPACE": "Space",
"ESCAPE": "Esc",
"TAB": "Tab",
"COMMA": "Comma",
"DOT": "Dot",
"": ""
}
9 changes: 8 additions & 1 deletion static/lang/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,12 @@
"click-to-load-fonts": "点击以加载字体",
"serif": "衬线",
"sans-serif": "无衬线",
"monospace": "等宽"
"monospace": "等宽",
"ENTER": "回车",
"SPACE": "空格",
"ESCAPE": "ESC",
"TAB": "Tab",
"COMMA": "逗号",
"DOT": "句号",
"": ""
}
9 changes: 8 additions & 1 deletion static/lang/zh-HK.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,12 @@
"click-to-load-fonts": "點擊以加載字體",
"serif": "襯線",
"sans-serif": "無襯線",
"monospace": "等寬"
"monospace": "等寬",
"ENTER": "回車",
"SPACE": "空格",
"ESCAPE": "ESC",
"TAB": "Tab",
"COMMA": "逗號",
"DOT": "句號",
"": ""
}
9 changes: 8 additions & 1 deletion static/lang/zh-Hant-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,12 @@
"click-to-load-fonts": "點擊以加載字體",
"serif": "襯線",
"sans-serif": "無襯線",
"monospace": "等寬"
"monospace": "等寬",
"ENTER": "回車",
"SPACE": "空格",
"ESCAPE": "ESC",
"TAB": "Tab",
"COMMA": "逗號",
"DOT": "句號",
"": ""
}
9 changes: 8 additions & 1 deletion static/lang/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,12 @@
"click-to-load-fonts": "點選以載入字型",
"serif": "襯線",
"sans-serif": "無襯線",
"monospace": "等寬"
"monospace": "等寬",
"ENTER": "回車",
"SPACE": "空格",
"ESCAPE": "ESC",
"TAB": "Tab",
"COMMA": "逗號",
"DOT": "句號",
"": ""
}
22 changes: 22 additions & 0 deletions static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,28 @@ p {
overflow-x: auto;
}

#keyboard-shortcuts-layer {
position: fixed;
top: 0;
left: 0;
width: 100%;
overflow: visible;
pointer-events: all;
z-index: 2;
}
#keyboard-shortcuts-layer span {
display: inline-block;
position: absolute;
/* border: var(--border) dotted var(--fore-color); */
background-color: var(--bottom);
opacity: 0.75;
padding: 4px 8px;
white-space: pre;
line-height: 1em;
font-family: 'DejaVu Sans Mono', 'Hack', 'Consolas', monospace;
transform: translate(-1em, calc(0.33em * -1));
}

@media (prefers-color-scheme: dark) {
:root {
--fore: #e0e0e0;
Expand Down

0 comments on commit a7f213e

Please sign in to comment.