diff --git a/css/apple2.css b/css/apple2.css index ff90928..237d243 100644 --- a/css/apple2.css +++ b/css/apple2.css @@ -393,6 +393,10 @@ a.button:hover { width: 570px; } +#keyboard.layout-pravetz82 .row { + width: 598px; +} + .apple2e #keyboard .row { width: 610px; } @@ -401,6 +405,10 @@ a.button:hover { margin-left: 20px; } +#keyboard.layout-pravetz82 .row0 { + margin-left: 10px; +} + #keyboard .row2 { margin-left: 10px; } @@ -409,6 +417,10 @@ a.button:hover { margin-left: 10px; } +#keyboard.layout-pravetz82 .row3 { + margin-left: 25px; +} + #keyboard .row4 { margin-left: 10px; } @@ -514,11 +526,79 @@ a.button:hover { bottom: 15px; } +#keyboard.layout-pravetz82 .key-RST { + border-left-color: #d00; + border-top-color: #d00; + border-right-color: #700; + border-bottom-color: #700; + background-color: #a00; +} + +#keyboard.layout-pravetz82 .key-RST:active { + border-left-color: #800; + border-top-color: #800; + border-right-color: #000; + border-bottom-color: #000; + background-color: #500; +} + +#keyboard.layout-pravetz82 .key-ОСВ, #keyboard .key-МК { + border-left-color: #aaa; + border-top-color: #aaa; + border-right-color: #444; + border-bottom-color: #444; + background-color: #777; +} + +#keyboard.layout-pravetz82 .key-ОСВ:active, #keyboard .key-МК:active { + border-left-color: #777; + border-top-color: #777; + border-right-color: #000; + border-bottom-color: #000; + background-color: #333; +} + +#keyboard.layout-pravetz82 .key-ЛАТ { + border-left-color: #ddd; + border-top-color: #ddd; + border-right-color: #888; + border-bottom-color: #888; + background-color: #bbb; +} + +#keyboard.layout-pravetz82 .key-ЛАТ:active { + border-left-color: #888; + border-top-color: #888; + border-right-color: #222; + border-bottom-color: #222; + background-color: #999; +} + +#keyboard.layout-pravetz82 .key-ЛАТ2 { + border-left-color: #dd0; + border-top-color: #dd0; + border-right-color: #880; + border-bottom-color: #880; + background-color: #aa0; +} + +#keyboard.layout-pravetz82 .key-ЛАТ2:active { + border-left-color: #770; + border-top-color: #770; + border-right-color: #000; + border-bottom-color: #000; + background-color: #550; +} + #keyboard .key-nbsp { margin-left: 62px; width: 330px; } +#keyboard.layout-pravetz82 .key-nbsp { + margin-left: 72px; +} + .apple2e #display { margin: 5px; } diff --git a/js/components/Apple2.tsx b/js/components/Apple2.tsx index f840153..0d2188d 100644 --- a/js/components/Apple2.tsx +++ b/js/components/Apple2.tsx @@ -35,6 +35,7 @@ export interface Apple2Props { gl: boolean; rom: string; sectors: SupportedSectors; + keyboardLayout: string; } /** @@ -47,7 +48,7 @@ export interface Apple2Props { * @returns */ export const Apple2 = (props: Apple2Props) => { - const { e, enhanced, sectors } = props; + const { e, enhanced, sectors, keyboardLayout } = props; const screenRef = useRef(null); const [apple2, setApple2] = useState(); const [error, setError] = useState(); @@ -161,7 +162,7 @@ export const Apple2 = (props: Apple2Props) => { - + diff --git a/js/components/Keyboard.tsx b/js/components/Keyboard.tsx index 61785e4..232d54f 100644 --- a/js/components/Keyboard.tsx +++ b/js/components/Keyboard.tsx @@ -96,7 +96,7 @@ export const Key = ({ */ export interface KeyboardProps { apple2: Apple2Impl | undefined; - e: boolean; + layout: string; } /** @@ -107,10 +107,10 @@ export interface KeyboardProps { * @param apple2 Apple2 object * @returns Keyboard component */ -export const Keyboard = ({ apple2, e }: KeyboardProps) => { +export const Keyboard = ({ apple2, layout }: KeyboardProps) => { const [pressed, setPressed] = useState([]); const [active, setActive] = useState(['LOCK']); - const keys = useMemo(() => keysAsTuples(e ? keys2e : keys2), [e]); + const keys = useMemo(() => keysAsTuples(layout==='apple2e' ? keys2e : keys2), [layout]); // Set global keystroke handler useEffect(() => { diff --git a/js/components/util/systems.ts b/js/components/util/systems.ts index c45e2dd..57f46ac 100644 --- a/js/components/util/systems.ts +++ b/js/components/util/systems.ts @@ -4,6 +4,7 @@ export interface SystemType { e: boolean; enhanced: boolean; sectors: 13 | 16; + keyboardLayout: string; } // Enhanced Apple //e @@ -14,6 +15,7 @@ export const defaultSystem = { e: true, enhanced: true, sectors: 16, + keyboardLayout: 'apple2e', } as const; export const systemTypes: Record> = { @@ -36,41 +38,49 @@ export const systemTypes: Record> = { rom: 'intbasic', characterRom: 'apple2_char', e: false, + keyboardLayout: 'apple2', }, apple213: { rom: 'intbasic', characterRom: 'apple2_char', e: false, sectors: 13, + keyboardLayout: 'apple2', }, original: { rom: 'original', characterRom: 'apple2_char', e: false, + keyboardLayout: 'apple2', }, apple2jplus: { rom: 'apple2j', characterRom: 'apple2j_char', e: false, + keyboardLayout: 'apple2', }, apple2pig: { rom: 'fpbasic', characterRom: 'pigfont_char', e: false, + keyboardLayout: 'apple2', }, apple2lc:{ rom: 'fpbasic', characterRom: 'apple2lc_char', e: false, + keyboardLayout: 'apple2', }, apple2plus: { rom: 'fpbasic', characterRom: 'apple2_char', e: false, + keyboardLayout: 'apple2', }, pravetz82: { rom: 'pravetz82', characterRom: 'pravetz82_char', e: false, + keyboardLayout: 'apple2', } } as const; diff --git a/js/main2.ts b/js/main2.ts index 5f0e726..8ef4969 100644 --- a/js/main2.ts +++ b/js/main2.ts @@ -19,40 +19,49 @@ const romVersion = prefs.readPref('computer_type2'); let rom: string; let characterRom: string; let sectors: SupportedSectors = 16; +let keyboardLayout: string; switch (romVersion) { case 'apple2': rom = 'intbasic'; characterRom = 'apple2_char'; + keyboardLayout = 'apple2'; break; case 'apple213': rom = 'intbasic'; characterRom = 'apple2_char'; sectors = 13; + keyboardLayout = 'apple2'; break; case 'original': rom = 'original'; characterRom = 'apple2_char'; + keyboardLayout = 'apple2'; break; case 'apple2jplus': rom = 'apple2j'; characterRom = 'apple2j_char'; + keyboardLayout = 'apple2'; break; case 'apple2pig': rom = 'fpbasic'; characterRom = 'pigfont_char'; + keyboardLayout = 'apple2'; break; case 'apple2lc': rom = 'fpbasic'; characterRom = 'apple2lc_char'; + keyboardLayout = 'apple2'; break; case 'pravetz82': rom = 'pravetz82'; characterRom = 'pravetz82_char'; + keyboardLayout = 'pravetz82'; break; default: rom = 'fpbasic'; characterRom = 'apple2_char'; + keyboardLayout = 'apple2'; } const options = { @@ -91,5 +100,5 @@ apple2.ready.then(() => { cpu.addPageHandler(lc); - initUI(apple2, disk2, smartport, printer, false); + initUI(apple2, disk2, smartport, printer, false, keyboardLayout); }).catch(console.error); diff --git a/js/main2e.ts b/js/main2e.ts index 2280d58..919ee4c 100644 --- a/js/main2e.ts +++ b/js/main2e.ts @@ -18,26 +18,31 @@ const romVersion = prefs.readPref('computer_type2e'); let enhanced = false; let rom: string; let characterRom: string; +let keyboardLayout: string; switch (romVersion) { case 'apple2e': rom = 'apple2e'; characterRom = 'apple2e_char'; + keyboardLayout = 'apple2e'; break; case 'apple2rm': rom = 'apple2e'; characterRom = 'rmfont_char'; enhanced = true; + keyboardLayout = 'apple2e'; break; case 'apple2ex': rom = 'apple2ex'; characterRom = 'apple2enh_char'; enhanced = true; + keyboardLayout = 'apple2e'; break; default: rom = 'apple2enh'; characterRom = 'apple2enh_char'; enhanced = true; + keyboardLayout = 'apple2e'; } const options = { @@ -73,5 +78,5 @@ apple2.ready.then(() => { io.setSlot(6, disk2); io.setSlot(7, smartport); - initUI(apple2, disk2, smartport, printer, options.e); + initUI(apple2, disk2, smartport, printer, options.e, keyboardLayout); }).catch(console.error); diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index 39d7dc0..7715faf 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -851,7 +851,9 @@ function onLoaded( apple2: Apple2, disk2: DiskII, massStorage: MassStorage, - printer: Printer, e: boolean + printer: Printer, + e: boolean, + keyboardLayout: string ) { _apple2 = apple2; cpu = _apple2.getCPU(); @@ -881,7 +883,7 @@ function onLoaded( MicroModal.init(); - keyboard = new KeyBoard(cpu, io, e); + keyboard = new KeyBoard(cpu, io, keyboardLayout); keyboard.create('#keyboard'); keyboard.setFunction('F1', () => cpu.reset()); keyboard.setFunction('F2', (event) => { @@ -988,8 +990,10 @@ export function initUI( apple2: Apple2, disk2: DiskII, massStorage: MassStorage, - printer: Printer, e: boolean) { + printer: Printer, + e: boolean, + keyboardLayout: string) { window.addEventListener('load', () => { - onLoaded(apple2, disk2, massStorage, printer, e); + onLoaded(apple2, disk2, massStorage, printer, e, keyboardLayout); }); } diff --git a/js/ui/keyboard.ts b/js/ui/keyboard.ts index b715060..4ba594a 100644 --- a/js/ui/keyboard.ts +++ b/js/ui/keyboard.ts @@ -200,7 +200,25 @@ const keys2e = [ type Key2e = DeepMemberOf; -type Key = Key2 | Key2e; +const keyspravetz82 = [ + [ + ['!', '"', '#', '¤', '%', '&', '\'', '(', ')', '0', '*', '=', '﹁', 'RST'], + ['ОСВ', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '@', 'RPT', 'RETURN'], + ['МК', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', '+', '[', ']', '↓'], + ['ЛАТ', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 'ЛАТ', 'ЛАТ2'], + ['ВКЛ', ' '] + ], [ + ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', ':', '-', 'Ч', 'RST'], + ['ОСВ', 'Я', 'В', 'Е', 'Р', 'Т', 'Ъ', 'У', 'И', 'О', 'П', 'Ю', 'RPT', 'RETURN'], + ['МК', 'А', 'С', 'Д', 'Ф', 'Г', 'Х', 'Й', 'К', 'Л', ';', 'Ш', 'Щ', '↓'], + ['ЛАТ', 'З', 'Ь', 'Ц', 'Ж', 'Б', 'Н', 'М', ',', '.', '/', 'ЛАТ', 'ЛАТ2'], + ['ВКЛ', ' '] + ] +] as const; + +type KeyPravetz82 = DeepMemberOf; + +type Key = Key2 | Key2e | KeyPravetz82; type KeyFunction = (key: KeyboardEvent) => void; @@ -220,8 +238,20 @@ export default class KeyBoard { private functions: Record = {}; - constructor(private cpu: CPU6502, private io: Apple2IO, private e: boolean) { - this.keys = e ? keys2e : keys2; + constructor(private cpu: CPU6502, private io: Apple2IO, private layout: string) { + switch (this.layout) { + case 'apple2e': + this.keys = keys2e; + break; + case 'pravetz82': + this.keys = keyspravetz82; + this.capslocked = false; // Pravetz 82 starts with CAPS LOCK off. + break; + default: + this.keys = keys2; + break; + + } window.addEventListener('keydown', this.keydown); window.addEventListener('keyup', this.keyup); @@ -260,7 +290,7 @@ export default class KeyBoard { } shiftKey(down: boolean) { - const shiftKeys = this.kb.querySelectorAll('.key-SHIFT'); + const shiftKeys = this.kb.querySelectorAll(this.layout !== 'pravetz82' ? '.key-SHIFT' : '.key-ЛАТ'); this.shifted = down; if (down) { this.io.buttonUp(2); @@ -272,7 +302,7 @@ export default class KeyBoard { } controlKey(down: boolean) { - const ctrlKey = this.kb.querySelector('.key-CTRL'); + const ctrlKey = this.kb.querySelector(this.layout !== 'pravetz82' ? '.key-CTRL' : '.key-МК'); this.controlled = down; if (down) { ctrlKey!.classList.add('active'); @@ -319,7 +349,7 @@ export default class KeyBoard { * otherwise the used state is set to true. */ capslockKey(down?: boolean | undefined) { - const capsLock = this.kb.querySelector('.key-LOCK'); + const capsLock = this.kb.querySelector(this.layout !== 'pravetz82' ? '.key-LOCK' : '.key-ЛАТ2'); if (arguments.length === 0) { if (this.capslockKeyUsed) { @@ -349,6 +379,7 @@ export default class KeyBoard { create(el: string) { this.kb = document.querySelector(el)!; + this.kb.classList.add('layout-' + this.layout); let x, y, row, key, label, label1, label2; const buildLabel = (k: string) => { @@ -419,7 +450,15 @@ export default class KeyBoard { ev.preventDefault(); target.classList.add('pressed'); - let key: string = this.shifted ? key2 : key1; + let key: string; + if (this.layout !== 'pravetz82') { + key = this.shifted ? key2 : key1; + } + else { + // In Pravetz 82, the operation of the shift key is inverted. + // The top row (cyrillic) is used by default and shift switches to using the bottow row (latin). + key = this.shifted ? key1 : key2; + } switch (key) { case 'BELL': key = 'G'; @@ -440,7 +479,12 @@ export default class KeyBoard { key = '\x15'; break; case '↓': - key = '\x0A'; + if (this.layout !== 'pravetz82') { + key = '\x0A'; + } + else { + // On Pravetz 82 this key has no action. + } break; case '↑': key = '\x0B'; @@ -455,24 +499,152 @@ export default class KeyBoard { break; } + if (this.layout === 'pravetz82') { + // Pravetz 82 specific remapping. + // Lower-case lattin letters are replaced with cyrillic capital letters. + switch (key) { + + // First row. + case 'Ч': + key = '^'; + break; + case '﹁': + // FIXME: Which character should this map to? + break; + + // Second row. + case 'ОСВ': // Pravetz 82 ESC key in cyrillic. + key = '\x1B'; + break; + case 'Я': + key = 'q'; + break; + case 'В': + key = 'w'; + break; + case 'Е': + key = 'e'; + break; + case 'Р': + key = 'r'; + break; + case 'Т': + key = 't'; + break; + case 'Ъ': + key = 'y'; + break; + case 'У': + key = 'u'; + break; + case 'И': + key = 'i'; + break; + case 'О': + key = 'o'; + break; + case 'П': + key = 'p'; + break; + case 'Ю': + key = '@'; + break; + case '@': + key = '`'; + break; + + // Third row. + case 'А': + key = 'a'; + break; + case 'С': + key = 's'; + break; + case 'Д': + key = 'd'; + break; + case 'Ф': + key = 'f'; + break; + case 'Г': + key = 'g'; + break; + case 'Х': + key = 'h'; + break; + case 'Й': + key = 'j'; + break; + case 'К': + key = 'k'; + break; + case 'Л': + key = 'l'; + break; + case 'Ш': + key = '['; + break; + case 'Щ': + key = ']'; + break; + case '[': + key = '{'; + break; + case ']': + key = '}'; + break; + + // Fourth row. + case 'З': + key = 'z'; + break; + case 'Ь': + key = 'x'; + break; + case 'Ц': + key = 'c'; + break; + case 'Ж': + key = 'v'; + break; + case 'Б': + key = 'b'; + break; + case 'Н': + key = 'n'; + break; + case 'М': + key = 'm'; + break; + + default: + break; + } + } + if (key.length > 1) { switch (key) { case 'SHIFT': + case 'ЛАТ': // Shift on Pravetz 82 switches to cyrillic. this.shiftKey(!this.shifted); break; case 'CTRL': + case 'МК': // Pravetz 82 CTRL key in cyrillic. this.controlKey(!this.controlled); break; case 'CAPS': case 'LOCK': + case 'ЛАТ2': // CAPS LOCK on Pravetz 82 switches between cyrillic and latin. this.capslockKey(undefined); break; case 'POW': case 'POWER': + case 'ВКЛ': // Pravetz 82 power key in cyrillic. if (window.confirm('Power Cycle?')) window.location.reload(); break; case 'RESET': + case 'RST': this.cpu.reset(); break; case 'OPEN_APPLE': @@ -481,15 +653,22 @@ export default class KeyBoard { case 'CLOSED_APPLE': this.optionKey(!this.optioned); break; + case 'RPT': // Pravetz 82 "repeat" key. + // Do nothing. + break; default: break; } } else { if (this.controlled && key >= '@' && key <= '_') { this.io.keyDown(key.charCodeAt(0) - 0x40); - } else if (this.e && !this.shifted && !this.capslocked && + } else if (this.layout==='apple2e' && !this.shifted && !this.capslocked && key >= 'A' && key <= 'Z') { this.io.keyDown(key.charCodeAt(0) + 0x20); + } else if (this.layout==='pravetz82' && !this.shifted && this.capslocked && + key >= 'a' && key <= 'z') { + // CAPS LOCK on Pravetz 82 switches between cyrillic and latin. + this.io.keyDown(key.charCodeAt(0) - 0x20); } else { this.io.keyDown(key.charCodeAt(0)); } @@ -506,7 +685,7 @@ export default class KeyBoard { } private keydown = (evt: KeyboardEvent) => { - if (!this.dialogOpen() && (!evt.metaKey || evt.ctrlKey || this.e)) { + if (!this.dialogOpen() && (!evt.metaKey || evt.ctrlKey || this.layout==='apple2e')) { evt.preventDefault(); const key = this.mapKeyEvent(evt);