Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change(web): initializes OSK with keyboard if available during init 🪠 #11174

Merged
merged 8 commits into from
Apr 19, 2024
6 changes: 5 additions & 1 deletion android/KMEA/app/src/main/assets/android-host.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ function init() {
keyman.beepKeyboard = beepKeyboard;

// Readies the keyboard stub for instant loading during the init process.
KeymanWeb.registerStub(JSON.parse(jsInterface.initialKeyboard()));
try {
KeymanWeb.registerStub(JSON.parse(jsInterface.initialKeyboard()));
} catch(error) {
console.error(error);
}

keyman.init({
'embeddingApp':device,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ export default class PredictionContext extends EventEmitter<PredictionContextEve
this.connect();
}

public get modelState() {
return this.langProcessor.state;
}

private connect() {
this.langProcessor.addListener('invalidatesuggestions', this.invalidateSuggestions);
this.langProcessor.addListener('suggestionsready', this.updateSuggestions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@ function getOskWidth() {

function getOskHeight() {
var height = oskHeight;
if(keyman.osk.banner._activeType != 'blank') {
if(keyman.osk && keyman.osk.banner._activeType != 'blank') {
height = height - keyman.osk.banner.height;
} else {
height = height - (bannerHeight ? bannerHeight : 0);
}
return height;
}
Expand Down
4 changes: 3 additions & 1 deletion web/src/app/browser/src/contextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -799,7 +799,9 @@ export default class ContextManager extends ContextManagerBase<BrowserConfigurat

// Sets the default stub (as specified with the `getSavedKeyboard` call) as active.
if(stub) {
this.activateKeyboard(t[0], t[1]);
return this.activateKeyboard(stub.id, stub.langId);
} else {
return null;
}
}

Expand Down
48 changes: 30 additions & 18 deletions web/src/app/browser/src/keymanEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
// Scrolls the document-body to ensure that a focused element remains visible after the OSK appears.
this.contextManager.on('targetchange', (target: OutputTarget<any>) => {
const e = target?.getElement();
(this.osk.activationModel as TwoStateActivator<HTMLElement>).activationTrigger = e;
if(this.osk) {
(this.osk.activationModel as TwoStateActivator<HTMLElement>).activationTrigger = e;
}

if(this.config.hostDevice.touchable && target) {
this.ensureElementVisibility(e);
Expand Down Expand Up @@ -198,14 +200,6 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
// or do anything that would mutate the value.
const savedKeyboardStr = this.contextManager.getSavedKeyboardRaw();

if(device.touchable) {
this.osk = new views.AnchoredOSKView(this);
} else {
this.osk = new views.FloatingOSKView(this);
}

setupOskListeners(this, this.osk, this.contextManager);

// Automatically performs related handler setup & maintains references
// needed for related cleanup / shutdown.
this.pageIntegration = new PageIntegrationHandlers(window, this);
Expand All @@ -219,16 +213,34 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
this._initialized = 2;

// Let any deferred, pre-init stubs complete registration
await Promise.resolve();

// Attempt to restore the user's last-used keyboard from their previous session.
//
// Note: any cloud stubs will probably not be available yet.
// If we tracked cloud requests and awaited a Promise.all on pending queries,
// we could handle that too.
this.contextManager.restoreSavedKeyboard(savedKeyboardStr);
await this.config.deferForInitialization;

/*
Attempt to restore the user's last-used keyboard from their previous session.
The method auto-loads the default stub if one is available and the last-used keyboard
has no registered stub.

Note: any cloud stubs will probably not be available yet.
If we tracked cloud requests and awaited a Promise.all on pending queries,
we could handle that too.
*/
const loadingKbd: Promise<any> = this.contextManager.restoreSavedKeyboard(savedKeyboardStr);

// Wait for the initial keyboard to load before setting the OSK; this will avoid building an
// empty OSK that we'll instantly discard after.
try {
await loadingKbd;
} catch { /* in case of failed fetch due to network error or bad URI; we must still let the OSK init. */ };

const firstKbdConfig = {
keyboardToActivate: this.contextManager.activeKeyboard
};
const osk = device.touchable ? new views.AnchoredOSKView(this, firstKbdConfig) : new views.FloatingOSKView(this, firstKbdConfig);

await Promise.resolve();
setupOskListeners(this, osk, this.contextManager);
// And, now that we have our loaded active keyboard - or failed, thus must use that default...
// Now we set the OSK in place, an act which triggers VisualKeyboard construction.
this.osk = osk;
}

get register(): (x: CloudQueryResult) => void {
Expand Down
6 changes: 3 additions & 3 deletions web/src/app/browser/src/viewsAnchorpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function buildBaseOskConfiguration(engine: KeymanEngine) {
};

class PublishedAnchoredOSKView extends AnchoredOSKView {
constructor(engine: KeymanEngine, config?: ViewConfiguration) {
constructor(engine: KeymanEngine, config?: Partial<ViewConfiguration>) {
let finalConfig = {
...buildBaseOskConfiguration(engine),
...(config || {})
Expand All @@ -28,7 +28,7 @@ class PublishedAnchoredOSKView extends AnchoredOSKView {
}

class PublishedFloatingOSKView extends FloatingOSKView {
constructor(engine: KeymanEngine, config?: FloatingOSKViewConfiguration) {
constructor(engine: KeymanEngine, config?: Partial<FloatingOSKViewConfiguration>) {
let finalConfig: FloatingOSKViewConfiguration = {
...buildBaseOskConfiguration(engine),
...(config || {})
Expand All @@ -39,7 +39,7 @@ class PublishedFloatingOSKView extends FloatingOSKView {
}

class PublishedInlineOSKView extends InlinedOSKView {
constructor(engine: KeymanEngine, config?: ViewConfiguration) {
constructor(engine: KeymanEngine, config?: Partial<ViewConfiguration>) {
let finalConfig: ViewConfiguration = {
...buildBaseOskConfiguration(engine),
...(config || {})
Expand Down
37 changes: 31 additions & 6 deletions web/src/app/webview/src/keymanEngine.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DefaultRules, DeviceSpec, RuleBehavior, isEmptyTransform } from '@keymanapp/keyboard-processor'
import { DefaultRules, DeviceSpec, RuleBehavior, isEmptyTransform, Keyboard } from '@keymanapp/keyboard-processor'
import { KeymanEngine as KeymanEngineBase, KeyboardInterface } from 'keyman/engine/main';
import { AnchoredOSKView, ViewConfiguration, StaticActivator } from 'keyman/engine/osk';
import { getAbsoluteX, getAbsoluteY } from 'keyman/engine/dom-utils';
Expand Down Expand Up @@ -71,7 +71,32 @@ export default class KeymanEngine extends KeymanEngineBase<WebviewConfiguration,
});

this.contextManager.initialize();
this.config.finalizeInit();

// Is triggered by the previous call.
// Defer to allow any pending stubs to be registered
await this.config.deferForInitialization;

// Was at least one stub pre-registered?
const kbdCache = this.keyboardRequisitioner.cache;
const firstStub = kbdCache.defaultStub;
let firstKeyboard: Keyboard;
if(firstStub) {
// If so, the common engine will have automatically started fetching it.
// Wait for the keyboard to load in order to avoid the high cost of building
// a stand-in we'll automatically replace.
try {
// Re-uses pre-existing fetch requests and fetched keyboards.
firstKeyboard = await kbdCache.fetchKeyboardForStub(firstStub);
} catch { /* in case of failed fetch due to network error or bad URI; we must still let the OSK init. */ };
}

const keyboardConfig: ViewConfiguration['keyboardToActivate'] = firstKeyboard ? {
keyboard: firstKeyboard,
metadata: firstStub
} : null;

// We construct the OSK now to avoid layout reflow thrashing from building a stand-in keyboard if possible.
const oskConfig: ViewConfiguration = {
hostDevice: this.config.hostDevice,
pathConfig: this.config.paths,
Expand All @@ -82,13 +107,13 @@ export default class KeymanEngine extends KeymanEngineBase<WebviewConfiguration,
predictionContextManager: this.contextManager.predictionContext,
heightOverride: this.getOskHeight,
widthOverride: this.getOskWidth,
isEmbedded: true
isEmbedded: true,
keyboardToActivate: keyboardConfig
};

this.osk = new AnchoredOSKView(oskConfig);
setupEmbeddedListeners(this, this.osk);

this.config.finalizeInit();
const osk = new AnchoredOSKView(oskConfig);
setupEmbeddedListeners(this, osk);
this.osk = osk;
}

// Functions that the old 'app/webview' equivalent had always provided to the WebView
Expand Down
2 changes: 2 additions & 0 deletions web/src/engine/main/src/keymanEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export default class KeymanEngine<
// so we handle that here.
this.osk?.bannerController.selectBanner(state);
this.osk?.refreshLayout();

// If `this.osk` is not yet initialized, the OSK itself will check if the model is loaded
});

// The OSK does not possess a direct connection to the KeyboardProcessor's state-key
Expand Down
9 changes: 9 additions & 0 deletions web/src/engine/osk/src/config/viewConfiguration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { type PredictionContext } from "@keymanapp/input-processor";
import { Keyboard, KeyboardProperties } from "@keymanapp/keyboard-processor";
import type Activator from "../views/activator.js";
import CommonConfiguration from "./commonConfiguration.js";

Expand Down Expand Up @@ -40,4 +41,12 @@ export default interface Configuration extends CommonConfiguration {
* with the active context.
*/
predictionContextManager?: PredictionContext;

/**
* Can be set and specified during initialization to start with the specified keyboard activated.
*/
keyboardToActivate?: {
keyboard: Keyboard,
metadata: KeyboardProperties;
}
}
6 changes: 3 additions & 3 deletions web/src/engine/osk/src/keyboard-layout/oskKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,10 +318,10 @@ export default abstract class OSKKey {
* @param layoutParams
* @returns
*/
public detectStyles(layoutParams: KeyLayoutParams) {
public detectStyles(layoutParams: KeyLayoutParams): void {
// Avoid doing any font-size related calculations if there's no text to display.
if(this.spec.sp == ButtonClasses.spacer || this.spec.sp == ButtonClasses.blank) {
return null;
return;
}

// Attempt to detect static but key-specific style properties if they haven't yet
Expand All @@ -331,7 +331,7 @@ export default abstract class OSKKey {

// Abort if the element is not currently in the DOM; we can't get any info this way.
if(!lblStyle.fontFamily) {
return null;
return;
}
this._fontFamily = lblStyle.fontFamily;

Expand Down
11 changes: 10 additions & 1 deletion web/src/engine/osk/src/views/floatingOskView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default class FloatingOSKView extends OSKView {
dfltX: string;
dfltY: string;

layoutSerializer = new FloatingOSKCookieSerializer();
private layoutSerializer = new FloatingOSKCookieSerializer();

private titleBar: TitleBar;
private resizeBar: ResizeBar;
Expand Down Expand Up @@ -220,6 +220,15 @@ export default class FloatingOSKView extends OSKView {
* @return {boolean}
*/
private loadPersistedLayout(): void {
/*
If a keyboard is available during OSK construction, it is possible
for this field to be `undefined`. `loadPersistedLayout` will be called
later in construction, so it's safe to skip.
*/
if(!this.layoutSerializer) {
return;
}

let c = this.layoutSerializer.loadWithDefaults({
visible: 1,
userSet: 0,
Expand Down
21 changes: 20 additions & 1 deletion web/src/engine/osk/src/views/oskView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export default abstract class OSKView
private _baseFontSize: ParsedLengthStyle;

private needsLayout: boolean = true;
private initialized: boolean;

private _animatedHideTimeout: number;

Expand Down Expand Up @@ -238,7 +239,7 @@ export default abstract class OSKView

this._bannerController = new BannerController(this.bannerView, this.hostDevice, this.config.predictionContextManager);

this.keyboardView = this._GenerateKeyboardView(null, null);
this.keyboardView = new EmptyView();
this._Box.appendChild(this.keyboardView.element);

// Install the default OSK stylesheets - but don't have it managed by the keyboard-specific stylesheet manager.
Expand All @@ -251,10 +252,21 @@ export default abstract class OSKView
this.uiStyleSheetManager.linkExternalSheet(sheetHref);
}

this.activeKeyboard = this.config.keyboardToActivate;

// Ensure the appropriate banner mode is activated; it's possible for a model to have either
// partially or fully loaded, especially if the prior line has a full Keyboard instance.
const modelState = this.config.predictionContextManager?.modelState;
if(modelState) {
this.bannerController.selectBanner(modelState);
}

this.setBaseMouseEventListeners();
if(this.hostDevice.touchable) {
this.setBaseTouchEventListeners();
}

this.initialized = true;
}

protected get configuration(): Configuration {
Expand Down Expand Up @@ -540,6 +552,13 @@ export default abstract class OSKView
keyboard: Keyboard,
metadata: KeyboardProperties
}) {
// Is the keyboard already loaded? If so, ignore the change command.
//
// Note: ensures that the _instances_ are the same; it's possible to make new instances
// to force a refresh. Does not perform a deep-equals.
if(this.initialized && this.keyboardData?.keyboard == keyboardData?.keyboard && this.keyboardData?.metadata == keyboardData?.metadata) {
return;
}
this.keyboardData = keyboardData;
this.loadActiveKeyboard();

Expand Down
Loading