diff --git a/common/web/keyboard-processor/src/keyboards/activeLayout.ts b/common/web/keyboard-processor/src/keyboards/activeLayout.ts index 579c9424c0..35f4264683 100644 --- a/common/web/keyboard-processor/src/keyboards/activeLayout.ts +++ b/common/web/keyboard-processor/src/keyboards/activeLayout.ts @@ -57,7 +57,9 @@ const KeyTypesOfKeyMap = { default: 'boolean' } as const; -const KeyTypesOfFlickList = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'] as const; +// Keep in this specific order: it's the ordering of priority for default hint selection when +// based on available hints. (i.e., `layout.defaultHint == 'flick'`) +const KeyTypesOfFlickList = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const; export class ActiveKeyBase { static readonly DEFAULT_PAD=15; // Padding to left of key, in virtual units @@ -75,6 +77,11 @@ export class ActiveKeyBase { /** WARNING - DO NOT USE DIRECTLY outside of @keymanapp/keyboard-processor! */ id: TouchLayout.TouchLayoutKeyId; text: string; + hint?: string; + hintSrc?: TouchLayout.TouchLayoutSubKey | TouchLayout.TouchLayoutKey; + + font?: string; + fontsize?: string; // These are fine. width?: number; @@ -348,10 +355,66 @@ export class ActiveKeyBase { aKey.displayLayer = displayLayer; aKey.layer = aKey.layer || displayLayer; + ActiveKeyBase.determineHint(aKey, layout.defaultHint); + // Compute the key's base KeyEvent properties for use in future event generation aKey.constructBaseKeyEvent(keyboard, layout, displayLayer); } + private static determineHint(spec: ActiveKey, defaultHint: TouchLayout.TouchLayoutDefaultHint): void { + // If a hint was directly specified, don't override it. + if(spec.hint) { + spec.hintSrc = spec; + return; + } + + // Is more compact than writing 8 separate cases. + if(defaultHint?.includes('flick-')) { + if(spec.flick) { + // 6 = length of 'flick-' + const dir = defaultHint.substring(6); + + if(spec.flick[dir]?.text) { + spec.hintSrc = spec.flick[dir]; + } + } + + return; + } + + switch(defaultHint) { + case 'none': + return; + case 'multitap': + if(spec.multitap) { + spec.hintSrc = spec.multitap[0]; + } + return; + case 'flick': + if(spec.flick) { + for(const key of KeyTypesOfFlickList) { + if(spec.flick[key]) { + spec.hintSrc = spec.flick[key]; + return; + } + } + } + return; + case 'longpress': + if(spec.sk) { + spec.hintSrc = spec.sk[0]; + } + return; + case 'dot': + default: + if(spec.sk) { + spec.hint = '\u2022'; + spec.hintSrc = spec; + } + return; + } + } + private constructBaseKeyEvent(keyboard: Keyboard, layout: ActiveLayout, displayLayer: string) { // Get key name and keyboard shift state (needed only for default layouts and physical keyboard handling) // Note - virtual keys should be treated case-insensitive, so we force uppercasing here. diff --git a/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts b/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts index 5f2c091c2c..9c1fb1a019 100644 --- a/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts +++ b/web/src/engine/osk/src/keyboard-layout/oskBaseKey.ts @@ -5,6 +5,7 @@ import OSKKey from './oskKey.js'; import { KeyData, KeyElement, link } from '../keyElement.js'; import OSKRow from './oskRow.js'; import VisualKeyboard from '../visualKeyboard.js'; +import { ParsedLengthStyle } from '../lengthStyle.js'; export default class OSKBaseKey extends OSKKey { @@ -81,12 +82,6 @@ export default class OSKBaseKey extends OSKKey { bsk[bsn].layer = btn.key.layer } } - - // If a subkey array is defined, add an icon - var skIcon = document.createElement('div'); - skIcon.className='kmw-key-popup-icon'; - //kDiv.appendChild(skIcon); - btn.appendChild(skIcon); } construct(vkbd: VisualKeyboard): HTMLDivElement { @@ -120,6 +115,10 @@ export default class OSKBaseKey extends OSKKey { btn['subKeys']=null; } + // If a subkey array is defined, add an icon + const skIcon = this.generateHint(); + btn.appendChild(skIcon); + // Add text to button and button to placeholder div kDiv.appendChild(btn); @@ -127,6 +126,43 @@ export default class OSKBaseKey extends OSKKey { return this.square = kDiv; } + public generateHint(): HTMLDivElement { + // If a hint is defined, add an icon + const skIcon = document.createElement('div'); + // Ensure that we use the keyboard's text font for hints. + skIcon.className='kmw-key-popup-icon'; + + const hintSpec = this.spec.hintSrc; + if(!hintSpec) { + return skIcon; + } + + if(hintSpec.font) { + skIcon.style.fontFamily = hintSpec.font; + } else { + skIcon.classList.add('kmw-key-text'); + } + + if(hintSpec.fontsize) { + const parsed = new ParsedLengthStyle(hintSpec.fontsize); + // From kmwosk.css: .kmw-key-popup-icon { font-size: 0.5em } + // The spec says to overwrite that, but we still want half-size compared to the text + // as a key-cap. + skIcon.style.fontSize = parsed.scaledBy(0.5).styleString; + } + + // If the base key itself is the source of the hint text, we use `hint` directly. + // Otherwise, we present the source subkey's key cap as the hint. + const text = hintSpec == this.spec ? this.spec.hint : hintSpec.text; + if(text == '\u2022') { + // The original, pre-17.0 longpress dot-hint used bold-face styling. + skIcon.style.fontWeight='bold'; + } + skIcon.textContent = text; + + return skIcon; + } + public refreshLayout(vkbd: VisualKeyboard) { let key = this.spec as ActiveKey; this.square.style.width = vkbd.layoutWidth.scaledBy(key.proportionalWidth).styleString; diff --git a/web/src/resources/osk/kmwosk.css b/web/src/resources/osk/kmwosk.css index 029a0f7466..9177f0b272 100644 --- a/web/src/resources/osk/kmwosk.css +++ b/web/src/resources/osk/kmwosk.css @@ -196,7 +196,9 @@ .tablet .kmw-key {border-radius:8px;overflow:visible;border:solid 2px #999999;box-shadow: 0px -1px 1px 0px rgba(0,0,0,1) inset;} .tablet .kmw-key-label{position:absolute;left:7%;top:2%;font:0.5em Arial;color:#aaa;background-color:transparent;z-index:10000;} -.tablet .kmw-key-text{position:relative;left:0;top:0;-webkit-user-select:none;} +.tablet .kmw-key-text { + -webkit-user-select:none; +} .tablet.ios .kmw-key-layer-group {background-color: #cfd3d9} .tablet.ios .kmw-key {border: none; border-bottom: solid 1px #8a8d90; box-shadow:none; border-radius: 5px;} @@ -271,10 +273,9 @@ .desktop .kmw-key-label{position:absolute;left:2px;top:2px;font:0.5em Arial;color:#888;background-color:transparent;} /* Popup icon style (and content)*/ -.kmw-key-popup-icon:before{content:'\2022';} - .kmw-key-popup-icon{position:absolute;display:block;visibility:visible;right:4%;top:1%;/*width:8px;height:8px;*/ - font:bold 0.5em Arial;color:#aaa;} + font-size:0.5em;color:#aaa; line-height:initial} + /*.phone .kmw-key-popup-icon{right:6%;top:-2px;width:8px;height:8px;text-align:right;} .desktop .kmw-key-popup-icon{right:4%;top:0;width:8px;height:8px;} */