mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-09 23:47:27 +00:00
Merge pull request #16194 from keymanapp/auto/A19S32-merge-base-into-autocorrect
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled
auto: A19S32 merge master into autocorrect 🚂
This commit is contained in:
commit
a6747c8ac9
61 changed files with 1358 additions and 542 deletions
4
.github/workflows/api-verification.yml
vendored
4
.github/workflows/api-verification.yml
vendored
|
|
@ -71,7 +71,7 @@ jobs:
|
|||
- name: "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }})"
|
||||
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true'
|
||||
run: |
|
||||
echo "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }}):" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }}):" | tee -a ${GITHUB_STEP_SUMMARY}
|
||||
|
||||
BIN_PACKAGE=$(ls "${GITHUB_WORKSPACE}/artifacts/" | grep "${PKG_NAME}[0-9]*_${{ steps.environment_step.outputs.KEYMAN_VERSION }}-1${{ steps.environment_step.outputs.PRERELEASE_TAG }}+$(lsb_release -c -s)1_amd64.deb")
|
||||
cd ${{ github.workspace }}/keyman/linux
|
||||
|
|
@ -80,7 +80,7 @@ jobs:
|
|||
--bin-pkg "${GITHUB_WORKSPACE}/artifacts/${BIN_PACKAGE}" \
|
||||
--git-sha "${{ steps.environment_step.outputs.GIT_SHA }}" \
|
||||
--git-base "${{ steps.environment_step.outputs.GIT_BASE }}" \
|
||||
verify 2>> $GITHUB_STEP_SUMMARY
|
||||
verify 2> >(tee -a ${GITHUB_STEP_SUMMARY} >&2)
|
||||
|
||||
- name: Archive .symbols file
|
||||
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true' && always()
|
||||
|
|
|
|||
29
HISTORY.md
29
HISTORY.md
|
|
@ -1,5 +1,33 @@
|
|||
# Keyman Version History
|
||||
|
||||
## 19.0.251 alpha 2026-07-02
|
||||
|
||||
* fix(android): skip test on Windows that triggers androidx WebKit bug (#16184)
|
||||
|
||||
## 19.0.250 alpha 2026-07-01
|
||||
|
||||
* fix(android): use `WebViewAssetLoader` for loading assets on Android (address regression in 19.0.242) (#16146)
|
||||
* fix(ios): bundle in KMW's globe-hint.css (#16142)
|
||||
* fix(ios): load host page, keyboards through a consistent WKURLSchemeHandler (#16136)
|
||||
|
||||
## 19.0.249 alpha 2026-06-30
|
||||
|
||||
* fix(windows): add update property to remote check (#16126)
|
||||
* chore(android): allow to build FV app in docker container (#16163)
|
||||
* fix(developer): warn only on race when destroying TAppSourceHttpResponder (#16140)
|
||||
* fix(developer): map shift key nextlayer property when importing OSK (#16110)
|
||||
|
||||
## 19.0.248 alpha 2026-06-29
|
||||
|
||||
* chore: add missing line to history (#16154)
|
||||
* maint(resources): DRY out `PRInformation` interface (#16158)
|
||||
* fix(developer): prevent clone of legacy keyboards with no source (#16111)
|
||||
* fix(developer): consolidate user options in TypeScript code (#16134)
|
||||
* fix(developer): fixup references to layer after deleting in Touch Layout Editor (#16129)
|
||||
* fix(web): revert regression in setting `activeKeyboard` in `set osk` (#16156)
|
||||
* test(web): add e2e tests for examples from guide (#16108)
|
||||
* maint(linux): show output of API check also in log file (#16153)
|
||||
|
||||
## 19.0.247 alpha 2026-06-16
|
||||
|
||||
* chore: update multi-labeler to 5.0.0 (#16100)
|
||||
|
|
@ -39,6 +67,7 @@
|
|||
|
||||
## 19.0.241 alpha 2026-06-02
|
||||
|
||||
* chore: web-core preflight (#16015)
|
||||
* chore(web): web-core preflight - strip core references (#16040)
|
||||
* docs: add note on how to use composer on dockerized websites (#16029)
|
||||
* fix(web): fix race displaying active keyboard in menu (#16042)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
19.0.248
|
||||
19.0.252
|
||||
|
|
@ -173,10 +173,11 @@ dependencies {
|
|||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'com.stepstone.stepper:material-stepper:4.3.1'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
implementation files('libs/keyman-engine.aar')
|
||||
implementation 'io.sentry:sentry-android:8.19.1'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation "com.android.installreferrer:installreferrer:2.2"
|
||||
implementation 'com.android.installreferrer:installreferrer:2.2'
|
||||
|
||||
// Add dependency for generating QR Codes
|
||||
// (Even though it's embedded in KMEA, because we're manually copying keyman-engine.aar,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ dependencies {
|
|||
implementation 'commons-io:commons-io:2.16.1'
|
||||
implementation 'io.sentry:sentry-android:8.19.1'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
|
||||
// Robolectric
|
||||
testImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (C) 2017-2018 SIL International. All rights reserved.
|
||||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*/
|
||||
|
||||
package com.keyman.engine;
|
||||
|
|
@ -23,6 +23,7 @@ import com.keyman.engine.util.DependencyUtil.LibraryType;
|
|||
import com.keyman.engine.util.FileUtils;
|
||||
import com.keyman.engine.util.KMLog;
|
||||
import com.keyman.engine.util.KMString;
|
||||
import com.keyman.engine.util.WebViewUtils;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
|
@ -85,7 +86,8 @@ final class KMKeyboard extends WebView {
|
|||
|
||||
private static String txtFont = "";
|
||||
private static String oskFont = null;
|
||||
private static String keyboardRoot = "";
|
||||
private static String dataRoot = "";
|
||||
private static String packageRoot = "";
|
||||
private final String fontUndefined = "undefined";
|
||||
private GestureDetector gestureDetector;
|
||||
private static ArrayList<OnKeyboardEventListener> kbEventListeners = null;
|
||||
|
|
@ -366,8 +368,7 @@ final class KMKeyboard extends WebView {
|
|||
} else {
|
||||
KMManager.SystemKeyboardWebViewClient.setKeyboardLoaded(false);
|
||||
}
|
||||
|
||||
String htmlPath = "file://" + getContext().getDir("data", Context.MODE_PRIVATE) + "/" + KMManager.KMFilename_KeyboardHtml;
|
||||
String htmlPath = WebViewUtils.buildAssetUrl(KMManager.KMFilename_KeyboardHtml);
|
||||
loadUrl(htmlPath);
|
||||
setBackgroundColor(0);
|
||||
}
|
||||
|
|
@ -561,6 +562,7 @@ final class KMKeyboard extends WebView {
|
|||
return oskFont;
|
||||
}
|
||||
|
||||
// REVIEW: this method seems to be unused
|
||||
/**
|
||||
* Return the full path to the special OSK font,
|
||||
* which is with all the keyboard assets at the root app_data folder
|
||||
|
|
@ -646,7 +648,7 @@ final class KMKeyboard extends WebView {
|
|||
}
|
||||
String kbKey = KMString.format("%s_%s", languageID, keyboardID);
|
||||
|
||||
setKeyboardRoot(packageID);
|
||||
setPackageRoot(packageID);
|
||||
|
||||
// Escape single-quoted names for javascript call
|
||||
keyboardName = keyboardName.replaceAll("\'", "\\\\'"); // Double-escaped-backslash b/c regex.
|
||||
|
|
@ -702,20 +704,20 @@ final class KMKeyboard extends WebView {
|
|||
KMManager.getLatestKeyboardFileVersion(getContext(), packageID, keyboardID) : null;
|
||||
}
|
||||
|
||||
setKeyboardRoot(packageID);
|
||||
setPackageRoot(packageID);
|
||||
|
||||
if(kOskFont == null || kOskFont.isEmpty())
|
||||
kOskFont = kFont;
|
||||
|
||||
JSONObject jDisplayFont = makeFontPaths(kFont);
|
||||
JSONObject jOskFont = makeFontPaths(kOskFont);
|
||||
JSONObject jDisplayFont = makeFontObject(kFont);
|
||||
JSONObject jOskFont = makeFontObject(kOskFont);
|
||||
|
||||
txtFont = getFontFilename(jDisplayFont);
|
||||
oskFont = getFontFilename(jOskFont);
|
||||
|
||||
String kbKey = KMString.format("%s_%s", languageID, keyboardID);
|
||||
|
||||
String keyboardPath = makeKeyboardPath(packageID, keyboardID, keyboardVersion);
|
||||
String keyboardUrl = makeKeyboardUrl(packageID, keyboardID, keyboardVersion);
|
||||
|
||||
JSONObject reg = new JSONObject();
|
||||
try {
|
||||
|
|
@ -723,7 +725,7 @@ final class KMKeyboard extends WebView {
|
|||
reg.put("KI", "Keyboard_" + keyboardID);
|
||||
reg.put("KLC", languageID);
|
||||
reg.put("KL", languageName);
|
||||
reg.put("KF", keyboardPath);
|
||||
reg.put("KF", keyboardUrl);
|
||||
reg.put("KP", packageID);
|
||||
|
||||
if (jDisplayFont != null) reg.put("KFont", jDisplayFont);
|
||||
|
|
@ -809,28 +811,31 @@ final class KMKeyboard extends WebView {
|
|||
}
|
||||
|
||||
// Set the base path of the keyboard depending on the package ID
|
||||
private void setKeyboardRoot(String packageID) {
|
||||
private void setPackageRoot(String packageID) {
|
||||
this.dataRoot = WebViewUtils.buildAssetUrl("");
|
||||
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
|
||||
this.keyboardRoot = (context.getDir("data", Context.MODE_PRIVATE).toString() +
|
||||
File.separator + KMManager.KMDefault_UndefinedPackageID + File.separator);
|
||||
this.packageRoot = this.dataRoot + KMManager.KMDefault_UndefinedPackageID + "/";
|
||||
} else {
|
||||
this.keyboardRoot = (context.getDir("data", Context.MODE_PRIVATE).toString() +
|
||||
File.separator + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator);
|
||||
this.packageRoot = this.dataRoot + KMManager.KMDefault_AssetPackages + "/" + packageID + "/";
|
||||
}
|
||||
}
|
||||
|
||||
public String getKeyboardRoot() {
|
||||
return this.keyboardRoot;
|
||||
private String getDataRoot() {
|
||||
return this.dataRoot;
|
||||
}
|
||||
|
||||
private String makeKeyboardPath(String packageID, String keyboardID, String keyboardVersion) {
|
||||
String keyboardPath;
|
||||
private String getPackageRoot() {
|
||||
return this.packageRoot;
|
||||
}
|
||||
|
||||
private String makeKeyboardUrl(String packageID, String keyboardID, String keyboardVersion) {
|
||||
String keyboardUrl = getPackageRoot();
|
||||
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
|
||||
keyboardPath = getKeyboardRoot() + keyboardID + "-" + keyboardVersion + ".js";
|
||||
keyboardUrl += keyboardID + "-" + keyboardVersion + ".js";
|
||||
} else {
|
||||
keyboardPath = getKeyboardRoot() + keyboardID + ".js";
|
||||
keyboardUrl += keyboardID + ".js";
|
||||
}
|
||||
return keyboardPath;
|
||||
return keyboardUrl;
|
||||
}
|
||||
|
||||
private void sendKMWError(int lineNumber, String sourceId, String message) {
|
||||
|
|
@ -1042,13 +1047,26 @@ final class KMKeyboard extends WebView {
|
|||
}
|
||||
|
||||
/**
|
||||
* Take a font JSON object and adjust to pass to JS
|
||||
* 1. Replace "source" keys for "files" keys
|
||||
* 2. Create full font paths for .ttf or .svg
|
||||
* @param font String font JSON object as a string
|
||||
* @return JSONObject of modified font information with full paths. If font is invalid, return `null`
|
||||
* Create a JSON object consisting of the font family and the URLs of the
|
||||
* font files on the local device.
|
||||
*
|
||||
* The `font` parameter can either be the filename of the font (with an
|
||||
* extension recognized as font), or a Font object or JSON string.
|
||||
* In the former case a new JSON object is created with the font family
|
||||
* derived from the filename, and the font filename prefixed with path
|
||||
* to the fonts.
|
||||
* In the latter case the legacy `sources` key is renamed to `files`.
|
||||
* If `files` is a single string it will be prefixed with the path to the
|
||||
* fonts. If `files` is an array, the array is iterated until finding
|
||||
* the first file with a font extension which is then prefixed with the
|
||||
* path to the fonts.
|
||||
*
|
||||
* @param font A string containing either the font filename or a font JSON
|
||||
* object as a string
|
||||
* @return JSONObject of modified font information with full paths. If font
|
||||
* is invalid, return `null`.
|
||||
*/
|
||||
private JSONObject makeFontPaths(String font) {
|
||||
private JSONObject makeFontObject(String font) {
|
||||
|
||||
if(font == null || font.equals("")) {
|
||||
return null;
|
||||
|
|
@ -1059,14 +1077,13 @@ final class KMKeyboard extends WebView {
|
|||
JSONObject jfont = new JSONObject();
|
||||
jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length()-4));
|
||||
JSONArray jfiles = new JSONArray();
|
||||
jfiles.put(keyboardRoot + font);
|
||||
String fontRoot = KMManager.isDefaultFont(font) ? getDataRoot() : getPackageRoot();
|
||||
jfiles.put(fontRoot + font);
|
||||
jfont.put(KMManager.KMKey_FontFiles, jfiles);
|
||||
return jfont;
|
||||
}
|
||||
|
||||
JSONObject fontObj = new JSONObject(font);
|
||||
JSONArray sourceArray;
|
||||
String fontFile;
|
||||
|
||||
// Replace "sources" key with "files"
|
||||
if (fontObj.has(KMManager.KMKey_FontSource)) {
|
||||
|
|
@ -1076,16 +1093,18 @@ final class KMKeyboard extends WebView {
|
|||
|
||||
Object obj = fontObj.get(KMManager.KMKey_FontFiles);
|
||||
if (obj instanceof String) {
|
||||
fontFile = fontObj.getString(KMManager.KMKey_FontFiles);
|
||||
fontObj.put(KMManager.KMKey_FontFiles, keyboardRoot + obj);
|
||||
String fontFile = fontObj.getString(KMManager.KMKey_FontFiles);
|
||||
String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRoot() : getPackageRoot();
|
||||
fontObj.put(KMManager.KMKey_FontFiles, fontRoot + obj);
|
||||
return fontObj;
|
||||
} else if (obj instanceof JSONArray) {
|
||||
sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles);
|
||||
JSONArray sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles);
|
||||
if (sourceArray != null) {
|
||||
for (int i = 0; i < sourceArray.length(); i++) {
|
||||
fontFile = sourceArray.getString(i);
|
||||
String fontFile = sourceArray.getString(i);
|
||||
if (FileUtils.hasFontExtension(fontFile)) {
|
||||
fontObj.put(KMManager.KMKey_FontFiles, keyboardRoot + fontFile);
|
||||
String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRoot() : getPackageRoot();
|
||||
fontObj.put(KMManager.KMKey_FontFiles, fontRoot + fontFile);
|
||||
fontObj.remove(KMManager.KMKey_FontSource);
|
||||
return fontObj;
|
||||
}
|
||||
|
|
@ -1094,7 +1113,6 @@ final class KMKeyboard extends WebView {
|
|||
}
|
||||
} catch (JSONException e) {
|
||||
KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (C) 2023 SIL International. All rights reserved.
|
||||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*/
|
||||
package com.keyman.engine;
|
||||
|
||||
|
|
@ -9,9 +9,13 @@ import android.graphics.Bitmap;
|
|||
import android.graphics.RectF;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.RelativeLayout;
|
||||
import androidx.webkit.WebViewAssetLoader;
|
||||
import androidx.webkit.WebViewAssetLoader.InternalStoragePathHandler;
|
||||
|
||||
import com.keyman.engine.KeyboardEventHandler.EventType;
|
||||
import com.keyman.engine.KMManager;
|
||||
|
|
@ -19,6 +23,7 @@ import com.keyman.engine.KMManager.KeyboardType;
|
|||
import com.keyman.engine.KMManager.SuggestionType;
|
||||
import com.keyman.engine.util.KMLog;
|
||||
import com.keyman.engine.data.Keyboard;
|
||||
import com.keyman.engine.util.WebViewUtils;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
|
@ -31,11 +36,16 @@ public final class KMKeyboardWebViewClient extends WebViewClient {
|
|||
public Context context;
|
||||
private KeyboardType keyboardType;
|
||||
private boolean keyboardLoaded;
|
||||
private WebViewAssetLoader assetLoader;
|
||||
|
||||
KMKeyboardWebViewClient(Context context, KeyboardType keyboardType) {
|
||||
this.context = context;
|
||||
this.keyboardType = keyboardType;
|
||||
this.keyboardLoaded = false;
|
||||
this.assetLoader = new WebViewAssetLoader.Builder()
|
||||
.addPathHandler(WebViewUtils.ASSET_DATA_PATH,
|
||||
new InternalStoragePathHandler(context, context.getDir("data", Context.MODE_PRIVATE)))
|
||||
.build();
|
||||
|
||||
if (keyboardType != KeyboardType.KEYBOARD_TYPE_INAPP && keyboardType != KeyboardType.KEYBOARD_TYPE_SYSTEM) {
|
||||
KMLog.LogError(TAG, String.format("Cannot initialize: Invalid keyboard type: %s", keyboardType.toString()));
|
||||
|
|
@ -58,6 +68,11 @@ public final class KMKeyboardWebViewClient extends WebViewClient {
|
|||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
return this.assetLoader.shouldInterceptRequest(request.getUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
Log.d("KMEA", String.format("onPageFinished: [%s] %s", keyboardType.toString(), url));
|
||||
|
|
@ -74,46 +89,44 @@ public final class KMKeyboardWebViewClient extends WebViewClient {
|
|||
kmKeyboard.keyboardSet = false;
|
||||
KMManager.currentLexicalModel = null;
|
||||
|
||||
if (url.startsWith("file")) { // TODO: is this test necessary?
|
||||
this.keyboardLoaded = true;
|
||||
this.keyboardLoaded = true;
|
||||
|
||||
SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
|
||||
int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0);
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
|
||||
int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0);
|
||||
Keyboard keyboardInfo = null;
|
||||
if (index >= 0) {
|
||||
keyboardInfo = KMManager.getKeyboardInfo(context, index);
|
||||
}
|
||||
String langId = null;
|
||||
if (keyboardInfo != null) {
|
||||
langId = keyboardInfo.getLanguageID();
|
||||
kmKeyboard.setKeyboard(keyboardInfo);
|
||||
} else {
|
||||
// Revert to default (index 0) or fallback keyboard
|
||||
keyboardInfo = KMManager.getKeyboardInfo(context, 0);
|
||||
if (keyboardInfo == null) {
|
||||
// Don't log to Sentry because some keyboard apps like FV don't install keyboards until the user chooses
|
||||
keyboardInfo = KMManager.getDefaultKeyboard(context);
|
||||
}
|
||||
Keyboard keyboardInfo = KMManager.getKeyboardInfo(context, index);
|
||||
String langId = null;
|
||||
if (keyboardInfo != null) {
|
||||
langId = keyboardInfo.getLanguageID();
|
||||
kmKeyboard.setKeyboard(keyboardInfo);
|
||||
} else {
|
||||
// Revert to default (index 0) or fallback keyboard
|
||||
keyboardInfo = KMManager.getKeyboardInfo(context, 0);
|
||||
if (keyboardInfo == null) {
|
||||
// Don't log to Sentry because some keyboard apps like FV don't install keyboards until the user chooses
|
||||
keyboardInfo = KMManager.getDefaultKeyboard(context);
|
||||
}
|
||||
if (keyboardInfo != null) {
|
||||
langId = keyboardInfo.getLanguageID();
|
||||
kmKeyboard.setKeyboard(keyboardInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KMManager.registerAssociatedLexicalModel(langId);
|
||||
KMManager.registerAssociatedLexicalModel(langId);
|
||||
|
||||
kmKeyboard.showHelpBubbleAfterDelay(2000, true); // check if it should be shown at that time!
|
||||
kmKeyboard.showHelpBubbleAfterDelay(2000, true); // check if it should be shown at that time!
|
||||
|
||||
kmKeyboard.callJavascriptAfterLoad();
|
||||
kmKeyboard.setSpacebarText(KMManager.getSpacebarText());
|
||||
kmKeyboard.callJavascriptAfterLoad();
|
||||
kmKeyboard.setSpacebarText(KMManager.getSpacebarText());
|
||||
|
||||
KeyboardEventHandler.notifyListeners(KMTextView.kbEventListeners, keyboardType, EventType.KEYBOARD_LOADED, null);
|
||||
KeyboardEventHandler.notifyListeners(KMTextView.kbEventListeners, keyboardType, EventType.KEYBOARD_LOADED, null);
|
||||
|
||||
// Special handling for in-app TextView context keymanapp/keyman#3809
|
||||
if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP &&
|
||||
KMTextView.activeView != null && KMTextView.activeView.getClass() == KMTextView.class) {
|
||||
KMTextView.updateTextContext();
|
||||
}
|
||||
// Special handling for in-app TextView context keymanapp/keyman#3809
|
||||
if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP &&
|
||||
KMTextView.activeView != null && KMTextView.activeView.getClass() == KMTextView.class) {
|
||||
KMTextView.updateTextContext();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (C) 2017 SIL International. All rights reserved.
|
||||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*/
|
||||
|
||||
package com.keyman.engine;
|
||||
|
|
@ -406,6 +406,10 @@ public final class KMManager {
|
|||
return getResourceRoot() + KMDefault_LexicalModelPackages + File.separator;
|
||||
}
|
||||
|
||||
public static String getLexicalModelsUrl() {
|
||||
return WebViewUtils.buildAssetUrl(KMDefault_LexicalModelPackages + "/");
|
||||
}
|
||||
|
||||
public static String getCloudDir() {
|
||||
return getResourceRoot() + KMDefault_UndefinedPackageID + File.separator;
|
||||
}
|
||||
|
|
@ -496,6 +500,19 @@ public final class KMManager {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given font is the default font or the default OSK font.
|
||||
* @param fontName String - The font name and extension
|
||||
* @return boolean true if `fontName` is DejaVuSans.ttf or keymanweb-osk.ttf
|
||||
* or null or empty string, false otherwise
|
||||
*/
|
||||
public static boolean isDefaultFont(String fontName) {
|
||||
if (fontName == null || fontName.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return fontName.equals(KMDefault_KeyboardFont) || fontName.equals(KMFilename_Osk_Ttf_Font);
|
||||
}
|
||||
|
||||
public static void initialize(final Context context, KeyboardType keyboardType) {
|
||||
appContext = context.getApplicationContext();
|
||||
|
||||
|
|
@ -1650,8 +1667,9 @@ public final class KMManager {
|
|||
String modelID = lexicalModelInfo.get(KMKey_LexicalModelID);
|
||||
String languageID = lexicalModelInfo.get(KMKey_LanguageID);
|
||||
boolean modelFileExists = true;
|
||||
File modelFile = new File(getLexicalModelsDir(), pkgID + File.separator + modelID + ".model.js");
|
||||
String path = "file://" + modelFile.getAbsolutePath();
|
||||
String modelFilename = pkgID + File.separator + modelID + ".model.js";
|
||||
File modelFile = new File(getLexicalModelsDir(), modelFilename);
|
||||
String url = getLexicalModelsUrl() + modelFilename;
|
||||
|
||||
// Disable sugestions if lexical-model file doesn't exist
|
||||
if (!modelFile.exists()) {
|
||||
|
|
@ -1666,7 +1684,7 @@ public final class KMManager {
|
|||
modelObj.put("id", modelID);
|
||||
languageJSONArray.put(languageID);
|
||||
modelObj.put("languages", languageJSONArray);
|
||||
modelObj.put("path", path);
|
||||
modelObj.put("path", url);
|
||||
modelObj.put("CustomHelpLink", lexicalModelInfo.get(KMKey_CustomHelpLink));
|
||||
} catch (JSONException e) {
|
||||
KMLog.LogException(TAG, "Invalid lexical model to register", e);
|
||||
|
|
@ -2225,7 +2243,12 @@ public final class KMManager {
|
|||
File kmpJSONFile = new File(path);
|
||||
if (!kmpJSONFile.exists()) {
|
||||
if (!KMManager.isTestMode()) {
|
||||
KMLog.LogError(TAG, path + " not found. Returning version 1.0");
|
||||
if (packageID.equals(KMDefault_PackageID)) {
|
||||
// Suppress error log for the default fallback keyboard if it's not installed
|
||||
Log.d(TAG, path + " not found. Returning version 1.0");
|
||||
} else {
|
||||
KMLog.LogError(TAG, path + " not found. Returning version 1.0");
|
||||
}
|
||||
}
|
||||
return "1.0";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/**
|
||||
* Copyright (C) 2020 SIL International. All rights reserved.
|
||||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*/
|
||||
package com.keyman.engine.data;
|
||||
|
||||
|
|
@ -15,6 +15,7 @@ import com.keyman.engine.util.BCP47;
|
|||
import com.keyman.engine.util.FileUtils;
|
||||
import com.keyman.engine.util.KMLog;
|
||||
import com.keyman.engine.util.KMString;
|
||||
import com.keyman.engine.util.WebViewUtils;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
|
@ -173,24 +174,24 @@ public class Keyboard extends LanguageResource implements Serializable {
|
|||
return o;
|
||||
}
|
||||
|
||||
private String getKeyboardRoot(Context context) {
|
||||
String keyboardRoot = context.getDir("data", Context.MODE_PRIVATE).toString() +
|
||||
File.separator;
|
||||
|
||||
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
|
||||
return keyboardRoot + KMManager.KMDefault_UndefinedPackageID + File.separator;
|
||||
} else {
|
||||
return keyboardRoot + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator;
|
||||
}
|
||||
private String getDataRoot() {
|
||||
return WebViewUtils.buildAssetUrl("");
|
||||
}
|
||||
|
||||
public String getKeyboardPath(Context context) {
|
||||
private String getPackageRoot() {
|
||||
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
|
||||
return getDataRoot() + KMManager.KMDefault_UndefinedPackageID + "/";
|
||||
}
|
||||
return getDataRoot() + KMManager.KMDefault_AssetPackages + "/" + packageID + "/";
|
||||
}
|
||||
|
||||
private String getKeyboardUrl() {
|
||||
String keyboardID = this.getKeyboardID();
|
||||
String keyboardVersion = this.getVersion();
|
||||
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
|
||||
return getKeyboardRoot(context) + keyboardID + "-" + keyboardVersion + ".js";
|
||||
return getPackageRoot() + keyboardID + "-" + keyboardVersion + ".js";
|
||||
} else {
|
||||
return getKeyboardRoot(context) + keyboardID + ".js";
|
||||
return getPackageRoot() + keyboardID + ".js";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,17 +203,17 @@ public class Keyboard extends LanguageResource implements Serializable {
|
|||
stubObj.put("KI", "Keyboard_" + this.getKeyboardID());
|
||||
stubObj.put("KLC", this.getLanguageID());
|
||||
stubObj.put("KL", this.getLanguageName());
|
||||
stubObj.put("KF", this.getKeyboardPath(context));
|
||||
stubObj.put("KF", this.getKeyboardUrl());
|
||||
stubObj.put("KP", this.getPackageID());
|
||||
|
||||
String displayFont = this.getFont();
|
||||
if(displayFont != null) {
|
||||
stubObj.put("KFont", this.buildDisplayFontObject(displayFont, context));
|
||||
stubObj.put("KFont", this.buildDisplayFontObject(displayFont));
|
||||
}
|
||||
|
||||
String oskFont = this.getOSKFont();
|
||||
if(oskFont != null) {
|
||||
stubObj.put("KOskFont", this.buildDisplayFontObject(oskFont, context));
|
||||
stubObj.put("KOskFont", this.buildDisplayFontObject(oskFont));
|
||||
}
|
||||
|
||||
String displayName = this.getDisplayName();
|
||||
|
|
@ -228,34 +229,33 @@ public class Keyboard extends LanguageResource implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Take a font JSON object and adjust to pass to JS
|
||||
* 1. Replace "source" keys for "files" keys
|
||||
* 2. Create full font paths for .ttf or .svg
|
||||
* @param font String font JSON object as a string
|
||||
* @return JSONObject of modified font information with full paths. If font is invalid, return `null`
|
||||
* Create a JSON object consisting of the font family and the font
|
||||
* file URL.
|
||||
*
|
||||
* @param font A string containing the font filename
|
||||
* @return JSONObject of modified font information with full URL. If font
|
||||
* is invalid, return `null`.
|
||||
*/
|
||||
private JSONObject buildDisplayFontObject(String font, Context context) {
|
||||
private JSONObject buildDisplayFontObject(String font) {
|
||||
if(font == null || font.equals("")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String keyboardRoot = this.getKeyboardRoot(context);
|
||||
String fontRoot = KMManager.isDefaultFont(font) ? this.getDataRoot() : this.getPackageRoot();
|
||||
|
||||
try {
|
||||
if (FileUtils.hasFontExtension(font)) {
|
||||
JSONObject jfont = new JSONObject();
|
||||
jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length() - 4));
|
||||
JSONArray jfiles = new JSONArray();
|
||||
jfiles.put(keyboardRoot + font);
|
||||
jfiles.put(fontRoot + font);
|
||||
jfont.put(KMManager.KMKey_FontFiles, jfiles);
|
||||
return jfont;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -42,6 +42,28 @@ public final class WebViewUtils {
|
|||
private static final String CHROME_INSTALL_PATTERN_FORMATSTR = "^.*Chrome/([\\d.]+).*$";
|
||||
private static final Pattern installPattern = Pattern.compile(CHROME_INSTALL_PATTERN_FORMATSTR);
|
||||
|
||||
/**
|
||||
* Reserved magic domain for loading files from the local device. At runtime
|
||||
* the WebViewAssetLoader will replace the protocol and domain with the
|
||||
* internal storage path.
|
||||
* See https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader
|
||||
*/
|
||||
private static final String MAGIC_DEFAULT_DOMAIN = "https://appassets.androidplatform.net";
|
||||
|
||||
/**
|
||||
* Path under the asset domain where all assets live
|
||||
*/
|
||||
public static final String ASSET_DATA_PATH = "/data/";
|
||||
|
||||
/**
|
||||
* Build a full URL to the provided asset
|
||||
*/
|
||||
public static String buildAssetUrl(String assetPath) {
|
||||
String appendAsset = assetPath == null ? "" :
|
||||
(assetPath.startsWith("/") ? assetPath.substring(1) : assetPath);
|
||||
return WebViewUtils.MAGIC_DEFAULT_DOMAIN + WebViewUtils.ASSET_DATA_PATH + appendAsset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Keyman Engine mode based on the Chrome version.
|
||||
* @param context - The context
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*/
|
||||
package com.keyman.engine;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
|
|
@ -32,7 +36,11 @@ public class KMManagerTest {
|
|||
// For some keyboard list tests, load an existing keyboard list.
|
||||
// Can't use @Before because context is null before running tests.
|
||||
public void loadOldKeyboardsList() {
|
||||
KMManager.initialize(ApplicationProvider.getApplicationContext(), KMManager.KeyboardType.KEYBOARD_TYPE_INAPP);
|
||||
Context context = ApplicationProvider.getApplicationContext();
|
||||
// Create appData directory accessed in KMKeyboardWebViewClient
|
||||
File dataDir = context.getDir("data", Context.MODE_PRIVATE);
|
||||
dataDir.mkdirs();
|
||||
KMManager.initialize(context, KMManager.KeyboardType.KEYBOARD_TYPE_INAPP);
|
||||
|
||||
File keyboards_dat = new File(TEST_RESOURCE_ROOT, OLD_KEYBOARDS_LIST);
|
||||
if (keyboards_dat == null || !keyboards_dat.exists()) {
|
||||
|
|
@ -111,6 +119,11 @@ public class KMManagerTest {
|
|||
*/
|
||||
@Test
|
||||
public void create_newKeyboardsList() {
|
||||
if(File.separatorChar != '/') {
|
||||
Log.w(TAG, "create_newKeyboardsList skipped due to #16183");
|
||||
return;
|
||||
}
|
||||
|
||||
loadOldKeyboardsList();
|
||||
dat_list = new ArrayList<HashMap<String, String>>();
|
||||
|
||||
|
|
@ -182,6 +195,12 @@ public class KMManagerTest {
|
|||
|
||||
@Test
|
||||
public void test_updateOldKeyboardsList() {
|
||||
// #16183
|
||||
if(File.separatorChar != '/') {
|
||||
Log.w(TAG, "test_updateOldKeyboardsList skipped due to #16183");
|
||||
return;
|
||||
}
|
||||
|
||||
loadOldKeyboardsList();
|
||||
Assert.assertNotNull(dat_list);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,37 +26,48 @@ public class WebViewUtilsTest {
|
|||
@Test
|
||||
public void test_ChromeEmpty_EngineWebViewVersionStatusDisabled() {
|
||||
String chromeVersion = "";
|
||||
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
|
||||
WebViewUtils.EngineWebViewVersionStatus.DISABLED);
|
||||
Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DISABLED,
|
||||
WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Chrome36_EngineWebViewVersionStatusDisabled() {
|
||||
double chromeVersionFloat = Float.parseFloat(WebViewUtils.KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME) - 1.0;
|
||||
String chromeVersion = String.valueOf(chromeVersionFloat);
|
||||
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
|
||||
WebViewUtils.EngineWebViewVersionStatus.DISABLED);
|
||||
Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DISABLED,
|
||||
WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Chrome37_EngineWebViewVersionStatusDegraded() {
|
||||
String chromeVersion = WebViewUtils.KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME;
|
||||
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
|
||||
WebViewUtils.EngineWebViewVersionStatus.DEGRADED);
|
||||
Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DEGRADED,
|
||||
WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Chrome94_EngineWebViewVersionStatusDegraded() {
|
||||
double chromeVersionFloat = Float.parseFloat(WebViewUtils.KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME) - 1.0;
|
||||
String chromeVersion = String.valueOf(chromeVersionFloat);
|
||||
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
|
||||
WebViewUtils.EngineWebViewVersionStatus.DEGRADED);
|
||||
Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.DEGRADED,
|
||||
WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_Chrome95_EngineWebViewVersionStatusFull() {
|
||||
String chromeVersion = WebViewUtils.KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME;
|
||||
Assert.assertEquals(WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion),
|
||||
WebViewUtils.EngineWebViewVersionStatus.FULL);
|
||||
Assert.assertEquals(WebViewUtils.EngineWebViewVersionStatus.FULL,
|
||||
WebViewUtils.getEngineWebViewVersionStatus(context, null, chromeVersion));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_buildAssetUrl() {
|
||||
Assert.assertEquals("https://appassets.androidplatform.net/data/", WebViewUtils.buildAssetUrl(""));
|
||||
Assert.assertEquals("https://appassets.androidplatform.net/data/", WebViewUtils.buildAssetUrl(null));
|
||||
Assert.assertEquals("https://appassets.androidplatform.net/data/foo", WebViewUtils.buildAssetUrl("foo"));
|
||||
Assert.assertEquals("https://appassets.androidplatform.net/data/foo/", WebViewUtils.buildAssetUrl("foo/"));
|
||||
Assert.assertEquals("https://appassets.androidplatform.net/data/foo/", WebViewUtils.buildAssetUrl("/foo/"));
|
||||
Assert.assertEquals("https://appassets.androidplatform.net/data/foo/bar.html", WebViewUtils.buildAssetUrl("foo/bar.html"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,4 +50,5 @@ dependencies {
|
|||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation files('libs/keyman-engine.aar')
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,4 +49,5 @@ dependencies {
|
|||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation files('libs/keyman-engine.aar')
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,4 +60,5 @@ dependencies {
|
|||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation files('libs/keyman-engine.aar')
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
|
|||
KMManager.KMDefault_KeyboardFont,
|
||||
KMManager.KMDefault_KeyboardFont);
|
||||
KMManager.addKeyboard(this, chiralityKBInfo);
|
||||
Keyboard.setDefaultKeyboard(chiralityKBInfo);
|
||||
|
||||
// Longpress test keyboard
|
||||
Keyboard longpressKBbInfo = new Keyboard(
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@ function API_UserAgent: string; // = 'Keyman for Windows/<ver>...'
|
|||
function API_UserAgent_Developer: string; // = 'Keyman Developer/<ver>...'
|
||||
function API_UserAgent_Diagnostics: string;
|
||||
|
||||
function API_Path_Keyboard(const id: string): string;
|
||||
|
||||
function KeymanCom_Protocol_Server: string; // = 'https://keyman.com';
|
||||
|
||||
function MakeAPIURL(path: string): string;
|
||||
|
|
@ -121,6 +123,8 @@ const
|
|||
S_KeymanCom_Staging = 'https://keyman.com'; // #7227 disabling: 'https://keyman-staging.com';
|
||||
S_APIServer_Staging = 'api.keyman.com'; // #7227 disabling: 'api.keyman-staging.com';
|
||||
|
||||
S_API_Path_Keyboard = '/keyboard/%0:s';
|
||||
|
||||
const
|
||||
URLPath_PackageDownload_Format = '/go/package/download/%0:s?platform=windows&tier=%1:s&bcp47=%2:s&update=%3:d';
|
||||
URL_KeymanDeveloper_HelpKmcMessage_Format = S_Host_KmnSh+'/%0:s';
|
||||
|
|
@ -187,4 +191,9 @@ begin
|
|||
Result := Format(URL_KeymanDeveloper_HelpKmcMessage_Format, [id.ToLower]);
|
||||
end;
|
||||
|
||||
function API_Path_Keyboard(const id: string): string;
|
||||
begin
|
||||
Result := Format(S_API_Path_Keyboard, [id]);
|
||||
end;
|
||||
|
||||
end.
|
||||
|
|
|
|||
|
|
@ -78,3 +78,5 @@ export { getFontFamily, getFontFamilySync } from './font-family.js';
|
|||
export * as ValidIds from './valid-ids.js';
|
||||
|
||||
export * as ProjectLoader from './project-loader.js';
|
||||
|
||||
export { optionsManager, KeymanDeveloperOption, KeymanDeveloperOptions, KeymanDeveloperOptionsPath } from './keyman-developer-options.js';
|
||||
|
|
|
|||
128
developer/src/common/web/utils/src/keyman-developer-options.ts
Normal file
128
developer/src/common/web/utils/src/keyman-developer-options.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* User options for Keyman Developer. These are stored in options.json in the
|
||||
* user profile; the location varies by operating system or may be stored in
|
||||
* browser storage on web sites.
|
||||
*
|
||||
* The node-based loader is implemented in both kmc and Keyman Developer Server,
|
||||
* in order to keep node dependencies out of the developer-utils module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The standard path under the user profile where options.json is stored; use
|
||||
* `path.join(os.homedir(), ...KeymanDeveloperOptionsPath)` or similar
|
||||
*/
|
||||
export const KeymanDeveloperOptionsPath = [/* '~', */ '.keymandeveloper', 'options.json'];
|
||||
|
||||
/**
|
||||
* The set of standard user options for Keyman Developer. Corresponds to
|
||||
* TKeymanDeveloperOptions in `developer/src/tike/main/KeymanDeveloperOptions.pas`
|
||||
*/
|
||||
export interface KeymanDeveloperOptions {
|
||||
"use tab char": boolean;
|
||||
"link font sizes": boolean;
|
||||
"indent size": number;
|
||||
"use old debugger": boolean;
|
||||
"editor theme": string;
|
||||
"debugger break when exiting line": boolean;
|
||||
"debugger single step after break": boolean;
|
||||
"debugger show store offset": boolean;
|
||||
"debugger recompile with debug info": boolean;
|
||||
"debugger auto reset before compilng": boolean;
|
||||
"auto save before compiling": boolean;
|
||||
"osk auto save before importing": boolean;
|
||||
"web host port": number;
|
||||
"server keep alive": boolean;
|
||||
"server use local addresses": boolean;
|
||||
"server ngrok token": string;
|
||||
"server ngrok region": string;
|
||||
"server use ngrok": boolean;
|
||||
"server show console window": boolean;
|
||||
"char map disable database lookups": boolean;
|
||||
"char map auto lookup": boolean;
|
||||
"open keyboard files in source view": boolean;
|
||||
"display theme": string;
|
||||
"external editor path": string;
|
||||
"smtp server": string;
|
||||
"test email addresses": string;
|
||||
"web ladder length": number;
|
||||
"default project path": string;
|
||||
"automatically report errors": boolean;
|
||||
"automatically report usage": boolean;
|
||||
"toolbar visible": boolean;
|
||||
"active project": string;
|
||||
"prompt to upgrade projects": boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A single Keyman Developer user option.
|
||||
*/
|
||||
export type KeymanDeveloperOption = keyof KeymanDeveloperOptions;
|
||||
|
||||
const DEFAULT_OPTIONS: KeymanDeveloperOptions = {
|
||||
// Corresponds to TKeymanDeveloperOptions.Read in KeymanDeveloperOptions.pas
|
||||
"use tab char": false,
|
||||
"link font sizes": true,
|
||||
"indent size": 4,
|
||||
"use old debugger": false,
|
||||
"editor theme": '',
|
||||
"debugger break when exiting line": true,
|
||||
"debugger single step after break": false,
|
||||
"debugger show store offset": false,
|
||||
"debugger recompile with debug info": false,
|
||||
"debugger auto reset before compilng": false,
|
||||
"auto save before compiling": false,
|
||||
"osk auto save before importing": false,
|
||||
"web host port": 8008,
|
||||
"server keep alive": false,
|
||||
"server use local addresses": true,
|
||||
"server ngrok token": '',
|
||||
"server ngrok region": '',
|
||||
"server use ngrok": false,
|
||||
"server show console window": false,
|
||||
"char map disable database lookups": false,
|
||||
"char map auto lookup": true,
|
||||
"open keyboard files in source view": false,
|
||||
"display theme": 'Windows10',
|
||||
"external editor path": '',
|
||||
"smtp server": '',
|
||||
"test email addresses": '',
|
||||
"web ladder length": 100,
|
||||
"default project path": '', // Note: this diverges from Delphi code, which uses CSIDL_PERSONAL on Windows, but it is not used in Server
|
||||
"automatically report errors": true,
|
||||
"automatically report usage": true,
|
||||
"toolbar visible": true,
|
||||
"active project": '',
|
||||
"prompt to upgrade projects": true,
|
||||
}
|
||||
|
||||
|
||||
class KeymanDeveloperOptionsManager {
|
||||
private options: KeymanDeveloperOptions = {...DEFAULT_OPTIONS};
|
||||
constructor() {}
|
||||
|
||||
public load(blob: Uint8Array | null) {
|
||||
this.options = {...DEFAULT_OPTIONS};
|
||||
if(blob !== null && blob !== undefined) {
|
||||
const data = JSON.parse(new TextDecoder('utf-8').decode(blob));
|
||||
if(typeof data == 'object') {
|
||||
// TODO: verify fields in options
|
||||
this.options = {...DEFAULT_OPTIONS, ...data};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public get<T extends KeymanDeveloperOption>(valueName: T): KeymanDeveloperOptions[T] {
|
||||
return this.options[valueName];
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.options = {...DEFAULT_OPTIONS};
|
||||
}
|
||||
}
|
||||
|
||||
export const optionsManager = new KeymanDeveloperOptionsManager();
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ class ProjectBuilder {
|
|||
|
||||
// Give a hint if the project is v1.0
|
||||
if(this.project.options.version != '2.0') {
|
||||
if(getOption("prompt to upgrade projects", true)) {
|
||||
if(getOption("prompt to upgrade projects")) {
|
||||
this.callbacks.reportMessage(InfrastructureMessages.Hint_ProjectIsVersion10());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class KeymanSentry {
|
|||
return true;
|
||||
}
|
||||
|
||||
return getOption('automatically report errors', true);
|
||||
return getOption('automatically report errors');
|
||||
}
|
||||
|
||||
static init(options?: SentryNodeOptions) {
|
||||
|
|
|
|||
|
|
@ -1,69 +1,32 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Load Keyman Developer's options from the standard Options location. This
|
||||
* small loader is duplicated in Keyman Developer Server, because we do not have
|
||||
* a shared node-aware module at this time.
|
||||
*/
|
||||
|
||||
import * as os from 'node:os';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { KeymanDeveloperOption, KeymanDeveloperOptions, KeymanDeveloperOptionsPath, optionsManager } from '@keymanapp/developer-utils';
|
||||
|
||||
export interface KeymanDeveloperOptions {
|
||||
"use tab char"?: boolean;
|
||||
"link font sizes"?: boolean;
|
||||
"indent size"?: number;
|
||||
"use old debugger"?: boolean;
|
||||
"editor theme"?: string;
|
||||
"debugger break when exiting line"?: boolean;
|
||||
"debugger single step after break"?: boolean;
|
||||
"debugger show store offset"?: boolean;
|
||||
"debugger recompile with debug info"?: boolean;
|
||||
"debugger auto reset before compilng"?: boolean;
|
||||
"auto save before compiling"?: boolean;
|
||||
"osk auto save before importing"?: boolean;
|
||||
"web host port"?: number;
|
||||
"server keep alive"?: boolean;
|
||||
"server use local addresses"?: boolean;
|
||||
"server ngrok token"?: string;
|
||||
"server ngrok region"?: string;
|
||||
"server use ngrok"?: boolean;
|
||||
"server show console window"?: boolean;
|
||||
"char map disable database lookups"?: boolean;
|
||||
"char map auto lookup"?: boolean;
|
||||
"open keyboard files in source view"?: boolean;
|
||||
"display theme"?: string;
|
||||
"external editor path"?: string;
|
||||
"smtp server"?: string;
|
||||
"test email addresses"?: string;
|
||||
"web ladder length"?: number;
|
||||
"default project path"?: string;
|
||||
"automatically report errors"?: boolean;
|
||||
"automatically report usage"?: boolean;
|
||||
"toolbar visible"?: boolean;
|
||||
"active project"?: string;
|
||||
"prompt to upgrade projects"?: boolean;
|
||||
};
|
||||
let optionsLoaded: boolean = false;
|
||||
|
||||
type KeymanDeveloperOption = keyof KeymanDeveloperOptions;
|
||||
|
||||
// Default has no options set, and unit tests will use the defaults (won't call
|
||||
// `loadOptions()`)
|
||||
let options: KeymanDeveloperOptions = {};
|
||||
|
||||
// We only load the options from disk once on first use
|
||||
let optionsLoaded = false;
|
||||
|
||||
export async function loadOptions(): Promise<KeymanDeveloperOptions> {
|
||||
export async function loadOptions(): Promise<boolean> {
|
||||
if(optionsLoaded) {
|
||||
return options;
|
||||
return true;
|
||||
}
|
||||
optionsLoaded = true;
|
||||
|
||||
options = {};
|
||||
try {
|
||||
const optionsFile = path.join(os.homedir(), '.keymandeveloper', 'options.json');
|
||||
const optionsFile = path.join(os.homedir(), ...KeymanDeveloperOptionsPath);
|
||||
if(fs.existsSync(optionsFile)) {
|
||||
for(let i = 0; i < 5; i++) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(optionsFile, 'utf-8'));
|
||||
if(typeof data == 'object') {
|
||||
options = data;
|
||||
}
|
||||
break;
|
||||
} catch(e) {
|
||||
const data = fs.readFileSync(optionsFile) as Uint8Array;
|
||||
return optionsManager.load(data);
|
||||
} catch(e: any) {
|
||||
if(e?.code == 'EBUSY') {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
} else {
|
||||
|
|
@ -76,20 +39,20 @@ export async function loadOptions(): Promise<KeymanDeveloperOptions> {
|
|||
} catch(e) {
|
||||
// Nothing to report here, sadly -- because we cannot rely on Sentry at this
|
||||
// low level.
|
||||
options = {};
|
||||
}
|
||||
optionsLoaded = true;
|
||||
return options;
|
||||
|
||||
optionsManager.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getOption<T extends KeymanDeveloperOption>(valueName: T, defaultValue: KeymanDeveloperOptions[T]): KeymanDeveloperOptions[T] {
|
||||
return options[valueName] ?? defaultValue;
|
||||
export function getOption<T extends KeymanDeveloperOption>(valueName: T): KeymanDeveloperOptions[T] {
|
||||
return optionsManager.get(valueName);
|
||||
}
|
||||
|
||||
/**
|
||||
* unit tests will clear options before running, for consistency
|
||||
*/
|
||||
export function clearOptions() {
|
||||
options = {};
|
||||
optionsLoaded = true;
|
||||
optionsLoaded = false;
|
||||
return optionsManager.clear();
|
||||
}
|
||||
|
|
@ -319,6 +319,23 @@ function TVisualKeyboardToTouchLayoutConverter.SetupModifierKeysForImportedLayou
|
|||
|
||||
for l in p.Layers do
|
||||
begin
|
||||
// Find the Shift key and assign the next layer
|
||||
k := l.FindKeyById('K_SHIFT');
|
||||
if Assigned(k) then
|
||||
begin
|
||||
if l.id = 'default' then
|
||||
begin
|
||||
k.NextLayer := 'shift';
|
||||
end
|
||||
else
|
||||
begin
|
||||
// All layers other than default will return to default layer
|
||||
// when shift is pressed, because we do not currently map
|
||||
// shift+other mod layers with use of the Shift key in the import
|
||||
k.NextLayer := 'default';
|
||||
end;
|
||||
end;
|
||||
|
||||
// Find the Ctrl key for the layer
|
||||
k := l.FindKeyById('K_LCONTROL');
|
||||
if not Assigned(k) then
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class KeymanSentry {
|
|||
return true;
|
||||
}
|
||||
|
||||
return getOption('automatically report errors', true);
|
||||
return getOption('automatically report errors');
|
||||
}
|
||||
|
||||
static init(options?: SentryNodeOptions) {
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import { mkdirSync } from 'fs';
|
||||
import { loadJsonFile } from './load-json-file.js';
|
||||
|
||||
export class Configuration {
|
||||
public readonly appDataPath: string;
|
||||
public readonly cachePath: string;
|
||||
public readonly cacheStateFilename: string;
|
||||
public readonly lockFilename: string;
|
||||
public readonly pidFilename: string;
|
||||
public readonly configFilename: string;
|
||||
|
||||
/* Configuration values - set in config.json by TIKE */
|
||||
|
||||
public readonly port: number;
|
||||
|
||||
/* ngrok Configuration */
|
||||
|
||||
public readonly useNgrok: boolean;
|
||||
public readonly ngrokToken: string;
|
||||
public readonly ngrokVisible: boolean;
|
||||
|
||||
public ngrokEndpoint: string = '';
|
||||
|
||||
constructor() {
|
||||
|
||||
this.appDataPath = (process.env.APPDATA ||
|
||||
(process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share")) +
|
||||
'/Keyman/Keyman Developer/Server/';
|
||||
this.cachePath = this.appDataPath + 'cache/';
|
||||
this.cacheStateFilename = this.appDataPath + 'cache.json';
|
||||
this.lockFilename = this.appDataPath + 'lock.json';
|
||||
this.pidFilename = this.appDataPath + 'pid.json';
|
||||
this.configFilename = this.appDataPath + 'config.json';
|
||||
|
||||
mkdirSync(this.cachePath, { recursive: true});
|
||||
|
||||
const cfg = loadJsonFile(this.configFilename);
|
||||
|
||||
this.port = cfg?.port ?? 8008;
|
||||
|
||||
// ngrok configuration
|
||||
this.useNgrok = cfg?.useNgrok ?? false;
|
||||
this.ngrokToken = cfg?.ngrokToken ?? '';
|
||||
this.ngrokVisible = cfg?.ngrokVisible ?? false;
|
||||
}
|
||||
};
|
||||
|
||||
export const configuration = new Configuration();
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { writeFileSync } from 'fs';
|
||||
import { configuration } from './config.js';
|
||||
import { standardPaths } from './standardPaths.js';
|
||||
import { loadJsonFile } from './load-json-file.js';
|
||||
|
||||
export interface DebugObject {
|
||||
|
|
@ -97,7 +97,7 @@ export class SiteData {
|
|||
}
|
||||
|
||||
private loadState() {
|
||||
const state = loadJsonFile(configuration.cacheStateFilename);
|
||||
const state = loadJsonFile(standardPaths.cacheStateFilename);
|
||||
this.loadDebugObject(DebugKeyboard, state?.keyboards, this.keyboards);
|
||||
this.loadDebugObject(DebugModel, state?.models, this.models);
|
||||
this.loadDebugObject(DebugFont, state?.fonts, this.fonts);
|
||||
|
|
@ -106,7 +106,7 @@ export class SiteData {
|
|||
}
|
||||
|
||||
public saveState() {
|
||||
writeFileSync(configuration.cacheStateFilename, JSON.stringify(this, null, 2), 'utf-8');
|
||||
writeFileSync(standardPaths.cacheStateFilename, JSON.stringify(this, null, 2), 'utf-8');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as express from 'express';
|
|||
import { DebugObject, isValidId, simplifyId } from "../../../data.js";
|
||||
import * as fs from 'fs';
|
||||
import * as crypto from 'crypto';
|
||||
import { configuration } from '../../../config.js';
|
||||
import { standardPaths } from '../../../standardPaths.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
// We allow only 12 objects of each type in the cache
|
||||
|
|
@ -41,7 +41,7 @@ export function apiRegisterFile<O extends DebugObject> (intf: new () => O, root:
|
|||
|
||||
o.lastUse = new Date();
|
||||
o.id = id;
|
||||
o.filename = configuration.cachePath + o.filenameFromId(id);
|
||||
o.filename = standardPaths.cachePath + o.filenameFromId(id);
|
||||
fs.writeFileSync(o.filename, file);
|
||||
o.sha256 = crypto.createHash('sha256').update(file).digest('hex');
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import express from 'express';
|
|||
import multer from 'multer';
|
||||
import * as ws from 'ws';
|
||||
import { KeymanSentry } from './KeymanSentry.js';
|
||||
import { configuration } from './config.js';
|
||||
import { standardPaths } from './standardPaths.js';
|
||||
import { environment } from './environment.js';
|
||||
import setupRoutes from './routes.js';
|
||||
import { shutdown } from './shutdown.js';
|
||||
import { initTray } from './tray.js';
|
||||
import { loadOptions } from './options.js';
|
||||
import { getOption, loadOptions } from './options.js';
|
||||
|
||||
const options = {
|
||||
ngrokLog: false, // Set this to true if you need to see ngrok logs in the console
|
||||
|
|
@ -18,13 +18,13 @@ const options = {
|
|||
|
||||
/* Lock file - report on PID and prevent multiple instances cleanly */
|
||||
|
||||
console.log(`Starting Keyman Developer Server ${environment.versionWithTag}, listening on port ${configuration.port}.`);
|
||||
|
||||
// We need to load the Keyman Developer options before attempting to initialize
|
||||
// Sentry. `loadOptions` silently suppresses exceptions and returns a default
|
||||
// set of options if an error occurs.
|
||||
await loadOptions();
|
||||
|
||||
console.log(`Starting Keyman Developer Server ${environment.versionWithTag}, listening on port ${getOption('web host port')}.`);
|
||||
|
||||
KeymanSentry.init();
|
||||
try {
|
||||
await run();
|
||||
|
|
@ -80,10 +80,11 @@ export async function run() {
|
|||
|
||||
let server = null;
|
||||
try {
|
||||
server = app.listen(configuration.port);
|
||||
server = app.listen(getOption("web host port"));
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
// TODO handle and cleanup EADDRINUSE, throw anything else
|
||||
return;
|
||||
}
|
||||
|
||||
/* Attach the web socket server */
|
||||
|
|
@ -96,14 +97,14 @@ export async function run() {
|
|||
|
||||
/* Launch ngrok if enabled */
|
||||
|
||||
configuration.ngrokEndpoint = '';
|
||||
if(configuration.useNgrok) {
|
||||
standardPaths.ngrokEndpoint = '';
|
||||
if(getOption("server use ngrok")) {
|
||||
await startNGrok();
|
||||
}
|
||||
|
||||
/* Load the tray icon */
|
||||
|
||||
tray.start(configuration.port, configuration.ngrokEndpoint);
|
||||
tray.start(getOption("web host port"), standardPaths.ngrokEndpoint);
|
||||
}
|
||||
|
||||
async function loadNGrok() {
|
||||
|
|
@ -129,8 +130,8 @@ async function startNGrok() {
|
|||
let started = false;
|
||||
const listener = await ngrok.forward({
|
||||
proto: 'http',
|
||||
addr: configuration.port,
|
||||
authtoken: configuration.ngrokToken,
|
||||
addr: getOption("web host port"),
|
||||
authtoken: getOption("server ngrok token"),
|
||||
onLogEvent: (msg: string) => {
|
||||
if(options.ngrokLog) {
|
||||
console.log(chalk.cyan(('\n'+msg).split('\n').join('\n[ngrok] ').trim()));
|
||||
|
|
@ -139,19 +140,19 @@ async function startNGrok() {
|
|||
onStatusChange: (state: string) => {
|
||||
if(state == 'connected' && started) {
|
||||
// We only announce reconnection after initial start
|
||||
configuration.ngrokEndpoint = listener.url() ?? '';
|
||||
console.log(chalk.blueBright('ngrok tunnel reconnected at %s'), configuration.ngrokEndpoint);
|
||||
standardPaths.ngrokEndpoint = listener.url() ?? '';
|
||||
console.log(chalk.blueBright('ngrok tunnel reconnected at %s'), standardPaths.ngrokEndpoint);
|
||||
} else if(state == 'closed') {
|
||||
configuration.ngrokEndpoint = '';
|
||||
standardPaths.ngrokEndpoint = '';
|
||||
console.log(chalk.blueBright('ngrok tunnel closed'));
|
||||
}
|
||||
}
|
||||
});
|
||||
started = true;
|
||||
configuration.ngrokEndpoint = listener.url();
|
||||
console.log(chalk.blueBright('ngrok tunnel established at %s'), configuration.ngrokEndpoint);
|
||||
standardPaths.ngrokEndpoint = listener.url() ?? "";
|
||||
console.log(chalk.blueBright('ngrok tunnel established at %s'), standardPaths.ngrokEndpoint);
|
||||
} catch(e) {
|
||||
configuration.ngrokEndpoint = '';
|
||||
standardPaths.ngrokEndpoint = '';
|
||||
console.error(chalk.red('ngrok tunnel failed to connect with an error: %s'), e);
|
||||
return false;
|
||||
}
|
||||
|
|
@ -168,8 +169,8 @@ function getRunningInstancePid(pidFilename: string) {
|
|||
}
|
||||
|
||||
function writeLockFile() {
|
||||
const lockFilename = configuration.lockFilename.replaceAll(/[\\\/]/g, path.sep);
|
||||
const pidFilename = configuration.pidFilename.replaceAll(/[\\\/]/g, path.sep);
|
||||
const lockFilename = standardPaths.lockFilename.replaceAll(/[\\\/]/g, path.sep);
|
||||
const pidFilename = standardPaths.pidFilename.replaceAll(/[\\\/]/g, path.sep);
|
||||
|
||||
// console.debug(`Testing existence of ${lockFilename}`);
|
||||
if(fs.existsSync(lockFilename)) {
|
||||
|
|
|
|||
|
|
@ -1,70 +1,32 @@
|
|||
// TODO: this is duplicated in kmc
|
||||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Load Keyman Developer's options from the standard Options location. This
|
||||
* small loader is duplicated in kmc, because we do not have a shared node-aware
|
||||
* module at this time.
|
||||
*/
|
||||
|
||||
import * as os from 'node:os';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { KeymanDeveloperOption, KeymanDeveloperOptions, KeymanDeveloperOptionsPath, optionsManager } from '@keymanapp/developer-utils';
|
||||
|
||||
export interface KeymanDeveloperOptions {
|
||||
"use tab char"?: boolean;
|
||||
"link font sizes"?: boolean;
|
||||
"indent size"?: number;
|
||||
"use old debugger"?: boolean;
|
||||
"editor theme"?: string;
|
||||
"debugger break when exiting line"?: boolean;
|
||||
"debugger single step after break"?: boolean;
|
||||
"debugger show store offset"?: boolean;
|
||||
"debugger recompile with debug info"?: boolean;
|
||||
"debugger auto reset before compilng"?: boolean;
|
||||
"auto save before compiling"?: boolean;
|
||||
"osk auto save before importing"?: boolean;
|
||||
"web host port"?: number;
|
||||
"server keep alive"?: boolean;
|
||||
"server use local addresses"?: boolean;
|
||||
"server ngrok token"?: string;
|
||||
"server ngrok region"?: string;
|
||||
"server use ngrok"?: boolean;
|
||||
"server show console window"?: boolean;
|
||||
"char map disable database lookups"?: boolean;
|
||||
"char map auto lookup"?: boolean;
|
||||
"open keyboard files in source view"?: boolean;
|
||||
"display theme"?: string;
|
||||
"external editor path"?: string;
|
||||
"smtp server"?: string;
|
||||
"test email addresses"?: string;
|
||||
"web ladder length"?: number;
|
||||
"default project path"?: string;
|
||||
"automatically report errors"?: boolean;
|
||||
"automatically report usage"?: boolean;
|
||||
"toolbar visible"?: boolean;
|
||||
"active project"?: string;
|
||||
"prompt to upgrade projects"?: boolean;
|
||||
};
|
||||
let optionsLoaded: boolean = false;
|
||||
|
||||
type KeymanDeveloperOption = keyof KeymanDeveloperOptions;
|
||||
|
||||
// Default has no options set, and unit tests will use the defaults (won't call
|
||||
// `loadOptions()`)
|
||||
let options: KeymanDeveloperOptions = {};
|
||||
|
||||
// We only load the options from disk once on first use
|
||||
let optionsLoaded = false;
|
||||
|
||||
export async function loadOptions(): Promise<KeymanDeveloperOptions> {
|
||||
export async function loadOptions(): Promise<boolean> {
|
||||
if(optionsLoaded) {
|
||||
return options;
|
||||
return true;
|
||||
}
|
||||
optionsLoaded = true;
|
||||
|
||||
options = {};
|
||||
try {
|
||||
const optionsFile = path.join(os.homedir(), '.keymandeveloper', 'options.json');
|
||||
const optionsFile = path.join(os.homedir(), ...KeymanDeveloperOptionsPath);
|
||||
if(fs.existsSync(optionsFile)) {
|
||||
for(let i = 0; i < 5; i++) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(optionsFile, 'utf-8'));
|
||||
if(typeof data == 'object') {
|
||||
options = data;
|
||||
}
|
||||
break;
|
||||
} catch(e) {
|
||||
const data = fs.readFileSync(optionsFile) as Uint8Array;
|
||||
return optionsManager.load(data);
|
||||
} catch(e: any) {
|
||||
if(e?.code == 'EBUSY') {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
} else {
|
||||
|
|
@ -77,20 +39,20 @@ export async function loadOptions(): Promise<KeymanDeveloperOptions> {
|
|||
} catch(e) {
|
||||
// Nothing to report here, sadly -- because we cannot rely on Sentry at this
|
||||
// low level.
|
||||
options = {};
|
||||
}
|
||||
optionsLoaded = true;
|
||||
return options;
|
||||
|
||||
optionsManager.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getOption<T extends KeymanDeveloperOption>(valueName: T, defaultValue: KeymanDeveloperOptions[T]): KeymanDeveloperOptions[T] {
|
||||
return options[valueName] ?? defaultValue;
|
||||
export function getOption<T extends KeymanDeveloperOption>(valueName: T): KeymanDeveloperOptions[T] {
|
||||
return optionsManager.get(valueName);
|
||||
}
|
||||
|
||||
/**
|
||||
* unit tests will clear options before running, for consistency
|
||||
*/
|
||||
export function clearOptions() {
|
||||
options = {};
|
||||
optionsLoaded = true;
|
||||
optionsLoaded = false;
|
||||
return optionsManager.clear();
|
||||
}
|
||||
|
|
@ -12,9 +12,10 @@ import handleIncPackagesJson from './handlers/inc/packages-json.js';
|
|||
import apiPackageRegister from './handlers/api/package/register.js';
|
||||
import handleIncKeyboardsCss from './handlers/inc/keyboards-css.js';
|
||||
import { Environment } from './version-data.js';
|
||||
import { configuration } from './config.js';
|
||||
import { standardPaths } from './standardPaths.js';
|
||||
import chalk from 'chalk';
|
||||
import { shutdown } from './shutdown.js';
|
||||
import { getOption } from './options.js';
|
||||
|
||||
export default function setupRoutes(app: express.Express, upload: multer.Multer, wsServer: ws.WebSocketServer, environment: Environment ) {
|
||||
|
||||
|
|
@ -163,7 +164,7 @@ export default function setupRoutes(app: express.Express, upload: multer.Multer,
|
|||
/* ngrok data */
|
||||
|
||||
app.get('/api/status', (_req,res,next) => {
|
||||
const response = { ngrokEnabled: configuration.useNgrok, ngrokEndpoint: configuration.ngrokEndpoint };
|
||||
const response = { ngrokEnabled: getOption("server use ngrok"), ngrokEndpoint: standardPaths.ngrokEndpoint };
|
||||
res.send(response);
|
||||
next();
|
||||
});
|
||||
|
|
|
|||
33
developer/src/server/src/standardPaths.ts
Normal file
33
developer/src/server/src/standardPaths.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Path and URL constants (in some cases calculated)
|
||||
*/
|
||||
import { mkdirSync } from 'node:fs';
|
||||
|
||||
class StandardPaths {
|
||||
public readonly appDataPath: string;
|
||||
public readonly cachePath: string;
|
||||
public readonly cacheStateFilename: string;
|
||||
public readonly lockFilename: string;
|
||||
public readonly pidFilename: string;
|
||||
|
||||
/* ngrok Configuration */
|
||||
|
||||
public ngrokEndpoint: string = '';
|
||||
|
||||
constructor() {
|
||||
|
||||
this.appDataPath = (process.env.APPDATA ||
|
||||
(process.platform == 'darwin' ? process.env.HOME + '/Library/Preferences' : process.env.HOME + "/.local/share")) +
|
||||
'/Keyman/Keyman Developer/Server/';
|
||||
this.cachePath = this.appDataPath + 'cache/';
|
||||
this.cacheStateFilename = this.appDataPath + 'cache.json';
|
||||
this.lockFilename = this.appDataPath + 'lock.json';
|
||||
this.pidFilename = this.appDataPath + 'pid.json';
|
||||
|
||||
mkdirSync(this.cachePath, {recursive: true});
|
||||
}
|
||||
};
|
||||
|
||||
export const standardPaths = new StandardPaths();
|
||||
|
|
@ -76,11 +76,22 @@ var
|
|||
begin
|
||||
T := FSources.LockList;
|
||||
try
|
||||
// Note: Unlike regular functions, Assert has short-circuit evaluation
|
||||
// intrinsics on the first param which makes it safe to dereference T[0] in
|
||||
// the second parameter.
|
||||
Assert(T.Count = 0, 'TAppSourceHttpResponder.Sources should be empty at destruction '+
|
||||
'(T.Count='+IntToStr(T.Count)+', T[0].Filename='+T[0].Filename+')');
|
||||
// There is a race where RegisterSource is called on the server side
|
||||
// where a request is started in the form but the server does not respond
|
||||
// before the form is destroyed:
|
||||
// 1. http request starts on form
|
||||
// 2. Form destroyed, calls UnregisterSource
|
||||
// 3. http request received in TAppSourceHttpResponder,
|
||||
// RespondTouchEditorState calls RegisterSource
|
||||
// 4. Ooops
|
||||
|
||||
// There is another race somewhere with unsaved text editors, or else a
|
||||
// resource leak. For now, we'll report this as a message rather than crash.
|
||||
if T.Count > 0 then
|
||||
begin
|
||||
TKeymanSentryClient.Instance.ReportMessage('TAppSourceHttpResponder.Sources should be empty at destruction '+
|
||||
'(T.Count='+IntToStr(T.Count)+', T[0].Filename='+T[0].Filename+')', True);
|
||||
end;
|
||||
finally
|
||||
FSources.UnlockList;
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ type
|
|||
const S_Kmc = 'kmc.cmd';
|
||||
class function KmcPath: string; static;
|
||||
|
||||
const S_ServerConfigJson = 'config.json';
|
||||
class function ServerDataPath: string; static;
|
||||
class function ServerPath: string; static;
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ type
|
|||
procedure optWriteString(const nm, value: string);
|
||||
procedure optWriteBool(const nm: string; value: Boolean);
|
||||
procedure optWriteInt(const nm: string; value: Integer);
|
||||
procedure WriteServerConfigurationJson;
|
||||
class function Get_Initial_DefaultProjectPath: string; static;
|
||||
function BackOffAndSaveJson(const Filename: string; const JSON: TJSONObject): Boolean;
|
||||
public
|
||||
|
|
@ -476,8 +475,6 @@ begin
|
|||
finally
|
||||
FreeAndNil(json);
|
||||
end;
|
||||
|
||||
WriteServerConfigurationJson;
|
||||
end;
|
||||
|
||||
function TKeymanDeveloperOptions.BackOffAndSaveJson(const Filename: string; const JSON: TJSONObject): Boolean;
|
||||
|
|
@ -513,23 +510,6 @@ begin
|
|||
Result := False;
|
||||
end;
|
||||
|
||||
procedure TKeymanDeveloperOptions.WriteServerConfigurationJson;
|
||||
var
|
||||
o: TJSONObject;
|
||||
begin
|
||||
o := TJSONObject.Create;
|
||||
try
|
||||
o.AddPair('port', TJSONNumber.Create(FServerDefaultPort));
|
||||
o.AddPair('ngrokToken', FServerNgrokToken);
|
||||
o.AddPair('useNgrok', TJSONBool.Create(FServerUseNgrok));
|
||||
o.AddPair('ngrokVisible', TJSONBool.Create(FServerServerShowConsoleWindow));
|
||||
ForceDirectories(TKeymanDeveloperPaths.ServerDataPath);
|
||||
BackOffAndSaveJSON(TKeymanDeveloperPaths.ServerDataPath + TKeymanDeveloperPaths.S_ServerConfigJson, o);
|
||||
finally
|
||||
o.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TKeymanDeveloperOptions.optWriteBool(const nm: string; value: Boolean);
|
||||
begin
|
||||
json.AddPair(nm, TJSONBool.Create(value));
|
||||
|
|
|
|||
|
|
@ -486,7 +486,12 @@ procedure TframeTextEditor.LoadFileInBrowser(const AData: string);
|
|||
function GenerateNewFilename: string;
|
||||
begin
|
||||
Inc(FInitialFilenameIndex);
|
||||
Result := '*texteditor*'+IntToStr(FInitialFilenameIndex);
|
||||
Result := '*texteditor';
|
||||
if Owner <> nil then
|
||||
Result := Result + '*' + Owner.ClassName;
|
||||
if Parent <> nil then
|
||||
Result := Result + '*' + Parent.Name;
|
||||
Result := Result + '*'+IntToStr(FInitialFilenameIndex);
|
||||
end;
|
||||
function EncodeFont(const prefix: string; f: TFont): string;
|
||||
begin
|
||||
|
|
|
|||
|
|
@ -1,119 +1,127 @@
|
|||
inherited frmCloneKeymanCloudProjectParameters: TfrmCloneKeymanCloudProjectParameters
|
||||
BorderIcons = [biSystemMenu, biMaximize]
|
||||
Caption = 'Clone Project from Keyman Cloud'
|
||||
ClientHeight = 506
|
||||
ClientWidth = 840
|
||||
Position = poScreenCenter
|
||||
ExplicitWidth = 856
|
||||
ExplicitHeight = 545
|
||||
PixelsPerInch = 96
|
||||
TextHeight = 13
|
||||
object cmdOK: TButton
|
||||
Left = 680
|
||||
Top = 473
|
||||
Width = 73
|
||||
Height = 25
|
||||
Anchors = [akRight, akBottom]
|
||||
Caption = 'OK'
|
||||
Default = True
|
||||
TabOrder = 2
|
||||
OnClick = cmdOKClick
|
||||
end
|
||||
object cmdCancel: TButton
|
||||
Left = 759
|
||||
Top = 473
|
||||
Width = 73
|
||||
Height = 25
|
||||
Anchors = [akRight, akBottom]
|
||||
Cancel = True
|
||||
Caption = 'Cancel'
|
||||
ModalResult = 2
|
||||
TabOrder = 3
|
||||
end
|
||||
object panWebHost: TPanel
|
||||
Left = 0
|
||||
Top = 0
|
||||
Width = 842
|
||||
Height = 334
|
||||
Anchors = [akLeft, akTop, akRight, akBottom]
|
||||
BevelOuter = bvNone
|
||||
TabOrder = 0
|
||||
end
|
||||
object gbNewProjectDetails: TGroupBox
|
||||
Left = 8
|
||||
Top = 340
|
||||
Width = 823
|
||||
Height = 129
|
||||
Anchors = [akLeft, akRight, akBottom]
|
||||
Caption = 'New Project Details'
|
||||
TabOrder = 1
|
||||
object lblFileName: TLabel
|
||||
Left = 16
|
||||
Top = 24
|
||||
Width = 76
|
||||
Height = 13
|
||||
Caption = '&New project ID:'
|
||||
FocusControl = editKeyboardID
|
||||
end
|
||||
object lblProjectFilename: TLabel
|
||||
Left = 16
|
||||
Top = 102
|
||||
Width = 101
|
||||
Height = 13
|
||||
Caption = 'New project &filename'
|
||||
FocusControl = editProjectFilename
|
||||
end
|
||||
object lblPath: TLabel
|
||||
Left = 16
|
||||
Top = 75
|
||||
Width = 83
|
||||
Height = 13
|
||||
Caption = 'Destination &path:'
|
||||
FocusControl = editPath
|
||||
end
|
||||
object editKeyboardID: TEdit
|
||||
Left = 148
|
||||
Top = 21
|
||||
Width = 205
|
||||
Height = 21
|
||||
TabOrder = 0
|
||||
OnChange = editKeyboardIDChange
|
||||
end
|
||||
object editProjectFilename: TEdit
|
||||
Left = 148
|
||||
Top = 99
|
||||
Width = 669
|
||||
Height = 21
|
||||
TabStop = False
|
||||
ParentColor = True
|
||||
ReadOnly = True
|
||||
TabOrder = 4
|
||||
OnChange = editKeyboardIDChange
|
||||
end
|
||||
object cmdBrowse: TButton
|
||||
Left = 744
|
||||
Top = 72
|
||||
Width = 73
|
||||
Height = 21
|
||||
Caption = '&Browse...'
|
||||
TabOrder = 3
|
||||
OnClick = cmdBrowseClick
|
||||
end
|
||||
object editPath: TEdit
|
||||
Left = 148
|
||||
Top = 72
|
||||
Width = 590
|
||||
Height = 21
|
||||
TabOrder = 2
|
||||
OnChange = editPathChange
|
||||
end
|
||||
object chkRelocateExternal: TCheckBox
|
||||
Left = 148
|
||||
Top = 48
|
||||
Width = 257
|
||||
Height = 17
|
||||
Caption = 'Relocate &external files into new project folder'
|
||||
TabOrder = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
inherited frmCloneKeymanCloudProjectParameters: TfrmCloneKeymanCloudProjectParameters
|
||||
BorderIcons = [biSystemMenu, biMaximize]
|
||||
Caption = 'Clone Project from Keyman Cloud'
|
||||
ClientHeight = 506
|
||||
ClientWidth = 840
|
||||
Position = poScreenCenter
|
||||
ExplicitWidth = 856
|
||||
ExplicitHeight = 545
|
||||
PixelsPerInch = 96
|
||||
TextHeight = 13
|
||||
object lblMessage: TLabel
|
||||
Left = 8
|
||||
Top = 478
|
||||
Width = 314
|
||||
Height = 13
|
||||
Caption = 'The keyboard %0:s has no source available. It cannot be cloned.'
|
||||
FocusControl = editPath
|
||||
end
|
||||
object cmdOK: TButton
|
||||
Left = 680
|
||||
Top = 473
|
||||
Width = 73
|
||||
Height = 25
|
||||
Anchors = [akRight, akBottom]
|
||||
Caption = 'OK'
|
||||
Default = True
|
||||
TabOrder = 2
|
||||
OnClick = cmdOKClick
|
||||
end
|
||||
object cmdCancel: TButton
|
||||
Left = 759
|
||||
Top = 473
|
||||
Width = 73
|
||||
Height = 25
|
||||
Anchors = [akRight, akBottom]
|
||||
Cancel = True
|
||||
Caption = 'Cancel'
|
||||
ModalResult = 2
|
||||
TabOrder = 3
|
||||
end
|
||||
object panWebHost: TPanel
|
||||
Left = 0
|
||||
Top = 0
|
||||
Width = 842
|
||||
Height = 334
|
||||
Anchors = [akLeft, akTop, akRight, akBottom]
|
||||
BevelOuter = bvNone
|
||||
TabOrder = 0
|
||||
end
|
||||
object gbNewProjectDetails: TGroupBox
|
||||
Left = 8
|
||||
Top = 340
|
||||
Width = 823
|
||||
Height = 129
|
||||
Anchors = [akLeft, akRight, akBottom]
|
||||
Caption = 'New Project Details'
|
||||
TabOrder = 1
|
||||
object lblFileName: TLabel
|
||||
Left = 16
|
||||
Top = 24
|
||||
Width = 76
|
||||
Height = 13
|
||||
Caption = '&New project ID:'
|
||||
FocusControl = editKeyboardID
|
||||
end
|
||||
object lblProjectFilename: TLabel
|
||||
Left = 16
|
||||
Top = 102
|
||||
Width = 101
|
||||
Height = 13
|
||||
Caption = 'New project &filename'
|
||||
FocusControl = editProjectFilename
|
||||
end
|
||||
object lblPath: TLabel
|
||||
Left = 16
|
||||
Top = 75
|
||||
Width = 83
|
||||
Height = 13
|
||||
Caption = 'Destination &path:'
|
||||
FocusControl = editPath
|
||||
end
|
||||
object editKeyboardID: TEdit
|
||||
Left = 148
|
||||
Top = 21
|
||||
Width = 205
|
||||
Height = 21
|
||||
TabOrder = 0
|
||||
OnChange = editKeyboardIDChange
|
||||
end
|
||||
object editProjectFilename: TEdit
|
||||
Left = 148
|
||||
Top = 99
|
||||
Width = 669
|
||||
Height = 21
|
||||
TabStop = False
|
||||
ParentColor = True
|
||||
ReadOnly = True
|
||||
TabOrder = 4
|
||||
OnChange = editKeyboardIDChange
|
||||
end
|
||||
object cmdBrowse: TButton
|
||||
Left = 744
|
||||
Top = 72
|
||||
Width = 73
|
||||
Height = 21
|
||||
Caption = '&Browse...'
|
||||
TabOrder = 3
|
||||
OnClick = cmdBrowseClick
|
||||
end
|
||||
object editPath: TEdit
|
||||
Left = 148
|
||||
Top = 72
|
||||
Width = 590
|
||||
Height = 21
|
||||
TabOrder = 2
|
||||
OnChange = editPathChange
|
||||
end
|
||||
object chkRelocateExternal: TCheckBox
|
||||
Left = 148
|
||||
Top = 48
|
||||
Width = 257
|
||||
Height = 17
|
||||
Caption = 'Relocate &external files into new project folder'
|
||||
TabOrder = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ type
|
|||
cmdBrowse: TButton;
|
||||
editPath: TEdit;
|
||||
chkRelocateExternal: TCheckBox;
|
||||
lblMessage: TLabel;
|
||||
procedure cmdOKClick(Sender: TObject);
|
||||
procedure editSourceProjectFilenameChange(Sender: TObject);
|
||||
procedure FormCreate(Sender: TObject);
|
||||
|
|
@ -57,6 +58,7 @@ type
|
|||
cef: TframeCEFHost;
|
||||
dlgBrowse: TBrowse4Folder;
|
||||
FKeymanID: string;
|
||||
FSourceAvailable: Boolean;
|
||||
frmDownloadProgress: TfrmDownloadProgress;
|
||||
function GetBasePath: string;
|
||||
function GetKeyboardID: string;
|
||||
|
|
@ -64,6 +66,7 @@ type
|
|||
procedure EnableControls;
|
||||
procedure SetKeyboardID(const Value: string);
|
||||
procedure UpdateProjectFilename;
|
||||
procedure UpdateMessage;
|
||||
function GetProjectFilename: string;
|
||||
function GetSourceProjectFilename: string;
|
||||
function GetRelocateExternal: Boolean;
|
||||
|
|
@ -71,6 +74,7 @@ type
|
|||
procedure cefLoadEnd(Sender: TObject);
|
||||
procedure DownloadCallback(Owner: TfrmDownloadProgress; var Result: Boolean);
|
||||
procedure DownloadWrapperCallback(var Cancelled: Boolean);
|
||||
function IsKeyboardSourceAvailable(const id: string): Boolean;
|
||||
protected
|
||||
function GetHelpTopic: string; override;
|
||||
public
|
||||
|
|
@ -86,9 +90,11 @@ function ShowCloneKeymanCloudProjectParameters(Owner: TComponent): Boolean;
|
|||
implementation
|
||||
|
||||
uses
|
||||
System.JSON,
|
||||
System.Net.UrlClient,
|
||||
Vcl.ComCtrls,
|
||||
|
||||
HttpUploader,
|
||||
KeymanDeveloperOptions,
|
||||
Keyman.Developer.System.KmcWrapper,
|
||||
Keyman.Developer.System.HelpTopics,
|
||||
|
|
@ -182,6 +188,7 @@ begin
|
|||
dlgBrowse.Root := Desktop;
|
||||
dlgBrowse.Title := 'Select folder to save project to';
|
||||
|
||||
UpdateMessage;
|
||||
EnableControls;
|
||||
end;
|
||||
|
||||
|
|
@ -201,9 +208,100 @@ begin
|
|||
not u.Path.StartsWith(URLSubPath_KeymanDeveloper_Clone_Keyboards_Custom)
|
||||
then FKeymanID := u.Path.Substring(URLSubPath_KeymanDeveloper_Clone_Keyboards.Length)
|
||||
else FKeymanID := '';
|
||||
|
||||
FSourceAvailable := IsKeyboardSourceAvailable(FKeymanID);
|
||||
UpdateMessage;
|
||||
EnableControls;
|
||||
end;
|
||||
|
||||
function TfrmCloneKeymanCloudProjectParameters.IsKeyboardSourceAvailable(const id: string): Boolean;
|
||||
|
||||
function GetKeyboardDataFromApiServer(const id: string): string;
|
||||
var
|
||||
http: THTTPUploader;
|
||||
begin
|
||||
http := THTTPUploader.Create(nil);
|
||||
try
|
||||
http.Request.HostName := API_Server;
|
||||
http.Request.Protocol := API_Protocol;
|
||||
http.Request.UrlPath := API_Path_Keyboard(id);
|
||||
try
|
||||
http.Upload;
|
||||
except
|
||||
// Silently swallow network errors
|
||||
on E:Exception do Exit('');
|
||||
end;
|
||||
|
||||
if (http.Response.StatusCode < 200) or (http.Response.StatusCode > 299) then
|
||||
begin
|
||||
// Keyboard not found or invalid response
|
||||
Exit('');
|
||||
end;
|
||||
|
||||
Result := UTF8ToString(PAnsiChar(http.Response.MessageBodyAsString));
|
||||
finally
|
||||
FreeAndNil(http);
|
||||
end;
|
||||
end;
|
||||
|
||||
function GetSourcePathFromBody(const body: string): string;
|
||||
var
|
||||
val: TJSONValue;
|
||||
obj: TJSONObject;
|
||||
begin
|
||||
try
|
||||
val := TJSONObject.ParseJSONValue(body);
|
||||
except
|
||||
// Not a valid response
|
||||
Exit('');
|
||||
end;
|
||||
try
|
||||
if not (val is TJSONObject) then
|
||||
begin
|
||||
// Not a valid response
|
||||
Exit('');
|
||||
end;
|
||||
|
||||
obj := val as TJSONObject;
|
||||
val := obj.Values['sourcePath'];
|
||||
if not Assigned(val) or not (val is TJSONString) then
|
||||
begin
|
||||
// no sourcePath property
|
||||
Exit('');
|
||||
end;
|
||||
|
||||
Result := (val as TJSONString).Value;
|
||||
finally
|
||||
val.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
var
|
||||
body, sourcePath: string;
|
||||
begin
|
||||
if id = '' then
|
||||
begin
|
||||
Exit(False);
|
||||
end;
|
||||
|
||||
body := GetKeyboardDataFromApiServer(id);
|
||||
if body = '' then
|
||||
begin
|
||||
Exit(False);
|
||||
end;
|
||||
|
||||
sourcePath := GetSourcePathFromBody(body);
|
||||
if sourcePath = '' then
|
||||
begin
|
||||
Exit(False);
|
||||
end;
|
||||
|
||||
// Keyboards in legacy/ do not have source available. Keyboards in
|
||||
// release/ and experimental/ have source, and other new categories will
|
||||
// also have source in future.
|
||||
Result := not sourcePath.startsWith('legacy');
|
||||
end;
|
||||
|
||||
procedure TfrmCloneKeymanCloudProjectParameters.cmdBrowseClick(Sender: TObject);
|
||||
begin
|
||||
dlgBrowse.InitialDir := editPath.Text;
|
||||
|
|
@ -221,22 +319,26 @@ end;
|
|||
procedure TfrmCloneKeymanCloudProjectParameters.editKeyboardIDChange(Sender: TObject);
|
||||
begin
|
||||
UpdateProjectFilename;
|
||||
UpdateMessage;
|
||||
EnableControls;
|
||||
end;
|
||||
|
||||
procedure TfrmCloneKeymanCloudProjectParameters.editSourceProjectFilenameChange(Sender: TObject);
|
||||
begin
|
||||
UpdateMessage;
|
||||
EnableControls;
|
||||
end;
|
||||
|
||||
procedure TfrmCloneKeymanCloudProjectParameters.editPathChange(Sender: TObject);
|
||||
begin
|
||||
UpdateProjectFilename;
|
||||
UpdateMessage;
|
||||
EnableControls;
|
||||
end;
|
||||
|
||||
procedure TfrmCloneKeymanCloudProjectParameters.editVersionChange(Sender: TObject);
|
||||
begin
|
||||
UpdateMessage;
|
||||
EnableControls;
|
||||
end;
|
||||
|
||||
|
|
@ -248,7 +350,8 @@ begin
|
|||
(FKeymanID <> '') and
|
||||
(Trim(editPath.Text) <> '') and
|
||||
(Trim(editKeyboardID.Text) <> '') and
|
||||
TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True);
|
||||
TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True) and
|
||||
FSourceAvailable;
|
||||
|
||||
cmdOK.Enabled := e;
|
||||
end;
|
||||
|
|
@ -292,9 +395,41 @@ end;
|
|||
procedure TfrmCloneKeymanCloudProjectParameters.SetKeyboardID(const Value: string);
|
||||
begin
|
||||
editKeyboardID.Text := Value;
|
||||
UpdateMessage;
|
||||
EnableControls;
|
||||
end;
|
||||
|
||||
procedure TfrmCloneKeymanCloudProjectParameters.UpdateMessage;
|
||||
var
|
||||
msg: string;
|
||||
begin
|
||||
if FKeymanID = '' then
|
||||
begin
|
||||
msg := 'Please choose a keyboard from the search form above.';
|
||||
end
|
||||
else if not FSourceAvailable then
|
||||
begin
|
||||
msg := Format('The keyboard %0:s has no source available. It cannot be cloned.', [FKeymanID]);
|
||||
end
|
||||
else if Trim(editPath.Text) = '' then
|
||||
begin
|
||||
msg := 'A valid destination path must be selected.';
|
||||
end
|
||||
else if Trim(editKeyboardID.Text) = '' then
|
||||
begin
|
||||
msg := 'Please enter a valid new project identifier.';
|
||||
end
|
||||
else if not TKeyboardUtils.IsValidKeyboardID(Trim(editKeyboardID.Text), True) then
|
||||
begin
|
||||
msg := 'Please enter a valid new project identifier.';
|
||||
end
|
||||
else
|
||||
begin
|
||||
msg := '';
|
||||
end;
|
||||
lblMessage.Caption := msg;
|
||||
end;
|
||||
|
||||
procedure TfrmCloneKeymanCloudProjectParameters.UpdateProjectFilename;
|
||||
begin
|
||||
editProjectFilename.Text :=
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ $(function() {
|
|||
$('#btnDelLayer').click(function () {
|
||||
if ($('#selLayer option').length == 1) return;
|
||||
builder.saveUndo();
|
||||
KVKL[builder.lastPlatform].layer.splice(builder.lastLayerIndex, 1);
|
||||
const deletedLayer = KVKL[builder.lastPlatform].layer.splice(builder.lastLayerIndex, 1)[0];
|
||||
updateNextLayerReferences(KVKL[builder.lastPlatform], deletedLayer.id, '');
|
||||
builder.selectPlatform();
|
||||
builder.generate(false,true);
|
||||
});
|
||||
|
|
@ -45,29 +46,8 @@ $(function() {
|
|||
// Layer dialogs
|
||||
//
|
||||
|
||||
function submitLayerProperties() {
|
||||
var newLayerName = $('#layerName').val();
|
||||
if (!newLayerName.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||
alert('Layer name must contain only alphanumerics, underscore and hyphen.');
|
||||
return false;
|
||||
}
|
||||
for(var i = 0; i < KVKL[builder.lastPlatform].layer.length; i++) {
|
||||
if(i != builder.lastLayerIndex && KVKL[builder.lastPlatform].layer[i].id == newLayerName) {
|
||||
alert('Layer name must not already be in use for the current platform.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
builder.saveUndo();
|
||||
builder.generate();
|
||||
|
||||
var platform = KVKL[builder.lastPlatform];
|
||||
var oldLayerName = platform.layer[builder.lastLayerIndex].id;
|
||||
|
||||
let fixup = function(key) {
|
||||
if (key.layer == oldLayerName) {
|
||||
key.layer = newLayerName;
|
||||
}
|
||||
function updateNextLayerReferences(platform, oldLayerName, newLayerName) {
|
||||
const fixup = function(key) {
|
||||
if (key.nextlayer == oldLayerName) {
|
||||
key.nextlayer = newLayerName;
|
||||
}
|
||||
|
|
@ -94,6 +74,28 @@ $(function() {
|
|||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitLayerProperties() {
|
||||
const newLayerName = $('#layerName').val();
|
||||
if (!newLayerName.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||
alert('Layer name must contain only alphanumerics, underscore and hyphen.');
|
||||
return false;
|
||||
}
|
||||
for(let i = 0; i < KVKL[builder.lastPlatform].layer.length; i++) {
|
||||
if(i != builder.lastLayerIndex && KVKL[builder.lastPlatform].layer[i].id == newLayerName) {
|
||||
alert('Layer name must not already be in use for the current platform.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
builder.saveUndo();
|
||||
builder.generate();
|
||||
|
||||
const platform = KVKL[builder.lastPlatform];
|
||||
const oldLayerName = platform.layer[builder.lastLayerIndex].id;
|
||||
|
||||
updateNextLayerReferences(platform, oldLayerName, newLayerName);
|
||||
|
||||
platform.layer[builder.lastLayerIndex].id = newLayerName;
|
||||
builder.prepareLayers();
|
||||
|
|
|
|||
1
ios/.gitignore
vendored
1
ios/.gitignore
vendored
|
|
@ -13,6 +13,7 @@ samples/KMSample2/KeymanEngine.xcframework
|
|||
samples/KMSample2/build
|
||||
engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanios.js
|
||||
engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanweb-osk.ttf
|
||||
engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/globe-hint.css
|
||||
engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/kmwosk.css
|
||||
engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyman.js.map
|
||||
engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanweb-webview.js
|
||||
|
|
|
|||
|
|
@ -145,6 +145,7 @@
|
|||
CE8B0BBD248734240045EB2E /* KeymanPackageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8B0BBC248734240045EB2E /* KeymanPackageTests.swift */; };
|
||||
CE8B0BBF248764ED0045EB2E /* KMPResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8B0BBE248764ED0045EB2E /* KMPResource.swift */; };
|
||||
CE8B5BB22491DA540075CCB0 /* 13.0 Cloud to Package Migration.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CE8B5BB12491DA530075CCB0 /* 13.0 Cloud to Package Migration.bundle */; };
|
||||
CE8E6B1E2FEC17B100F5E731 /* WebViewSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8E6B1D2FEC17A900F5E731 /* WebViewSchemeHandler.swift */; };
|
||||
CE8EDEB123F53D1A009E1FF6 /* FileManagementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A079DD1223194B100581263 /* FileManagementTests.swift */; };
|
||||
CE8EDEB323F53F96009E1FF6 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */; };
|
||||
CE969BE8251AD8B500376D6A /* PackageWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE969BE7251AD8B500376D6A /* PackageWebViewController.swift */; };
|
||||
|
|
@ -459,6 +460,7 @@
|
|||
CE8B0BBC248734240045EB2E /* KeymanPackageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanPackageTests.swift; sourceTree = "<group>"; };
|
||||
CE8B0BBE248764ED0045EB2E /* KMPResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KMPResource.swift; sourceTree = "<group>"; };
|
||||
CE8B5BB12491DA530075CCB0 /* 13.0 Cloud to Package Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "13.0 Cloud to Package Migration.bundle"; sourceTree = "<group>"; };
|
||||
CE8E6B1D2FEC17A900F5E731 /* WebViewSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewSchemeHandler.swift; sourceTree = "<group>"; };
|
||||
CE8EDEB223F53F96009E1FF6 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = "<group>"; };
|
||||
CE969BE7251AD8B500376D6A /* PackageWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageWebViewController.swift; sourceTree = "<group>"; };
|
||||
CE96E42C24D1229A005B8E5A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
|
|
@ -804,6 +806,7 @@
|
|||
CE79B24823C711FF007E72AE /* KeyboardScaleMap.swift */,
|
||||
C0EF3E7A1F95B65300CE9BD4 /* KeymanWebDelegate.swift */,
|
||||
C0C16A881FA8146300F090BA /* KeymanWebViewController.swift */,
|
||||
CE8E6B1D2FEC17A900F5E731 /* WebViewSchemeHandler.swift */,
|
||||
C0A5FF361F6682EB00BE740C /* PopoverView.swift */,
|
||||
);
|
||||
path = Keyboard;
|
||||
|
|
@ -1522,6 +1525,7 @@
|
|||
9A079DCA222E050E00581263 /* LexicalModelKeymanPackage.swift in Sources */,
|
||||
9A60764422893A4E003BCFBA /* SettingsViewController.swift in Sources */,
|
||||
C06085B41F9485E40057E5B9 /* UIButton+Helpers.swift in Sources */,
|
||||
CE8E6B1E2FEC17B100F5E731 /* WebViewSchemeHandler.swift in Sources */,
|
||||
C0959CD41F99C44E00B616BC /* Constants.swift in Sources */,
|
||||
C0452BAD1F9F21270064431A /* Keyboard.swift in Sources */,
|
||||
29B30C232B564F9900C342A4 /* KeymanEngineLogger.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ class KeymanWebViewController: UIViewController {
|
|||
let storage: Storage
|
||||
weak var delegate: KeymanWebDelegate?
|
||||
private var useSpecialFont = false
|
||||
private var userContentController = WKUserContentController()
|
||||
private let userContentController = WKUserContentController()
|
||||
private let schemeHandler: WebViewSchemeHandler
|
||||
private let keymanWebViewName: String = "keyman"
|
||||
|
||||
// Views
|
||||
|
|
@ -69,8 +70,9 @@ class KeymanWebViewController: UIViewController {
|
|||
|
||||
init(storage: Storage) {
|
||||
self.storage = storage
|
||||
self.schemeHandler = WebViewSchemeHandler(storage: storage)
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
_ = view
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +124,7 @@ class KeymanWebViewController: UIViewController {
|
|||
config.preferences = prefs
|
||||
config.suppressesIncrementalRendering = false
|
||||
config.userContentController = self.userContentController
|
||||
config.setURLSchemeHandler(schemeHandler, forURLScheme: schemeHandler.scheme)
|
||||
|
||||
webView = KeymanWebView(frame: CGRect(origin: .zero, size: keyboardSize), configuration: config)
|
||||
webView!.isOpaque = false
|
||||
|
|
@ -182,7 +185,7 @@ extension KeymanWebViewController {
|
|||
}
|
||||
view = nil
|
||||
}
|
||||
|
||||
|
||||
func languageMenuPosition(_ completion: @escaping (CGRect) -> Void) {
|
||||
webView!.evaluateJavaScript("langMenuPos();") { result, _ in
|
||||
guard let result = result as? String, !result.isEmpty else {
|
||||
|
|
@ -279,20 +282,28 @@ extension KeymanWebViewController {
|
|||
// family does not have to match the name in the font file. It only has to be unique.
|
||||
return [
|
||||
"family": "\(keyboard.id)__\(isOsk ? "osk" : "display")",
|
||||
"files": font.source.map { storage.fontURL(forResource: keyboard, filename: $0)!.absoluteString }
|
||||
"files": font.source.map {
|
||||
schemeHandler.buildUrlForFile(
|
||||
fileURL: storage.fontURL(forResource: keyboard, filename: $0)!
|
||||
).absoluteString
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
func setKeyboard(_ keyboard: InstallableKeyboard) throws {
|
||||
let fileURL = storage.keyboardURL(for: keyboard)
|
||||
let loadingURL = schemeHandler.buildUrlForFile(
|
||||
fileURL: storage.keyboardURL(for: keyboard)
|
||||
)
|
||||
|
||||
var stub: [String: Any] = [
|
||||
"KI": "Keyboard_\(keyboard.id)",
|
||||
"KN": keyboard.name,
|
||||
"KLC": keyboard.languageID,
|
||||
"KL": keyboard.languageName,
|
||||
"KF": fileURL.absoluteString
|
||||
"KF": loadingURL.absoluteString
|
||||
]
|
||||
|
||||
|
||||
if let packageID = keyboard.packageID {
|
||||
stub["KP"] = packageID
|
||||
}
|
||||
|
|
@ -367,7 +378,7 @@ extension KeymanWebViewController {
|
|||
let stub: [String: Any] = [
|
||||
"id": lexicalModel.id,
|
||||
"languages": [lexicalModel.languageID], // Change when InstallableLexicalModel is updated to store an array
|
||||
"path": fileURL.absoluteString
|
||||
"path": schemeHandler.buildUrlForFile(fileURL: fileURL).absoluteString
|
||||
]
|
||||
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
||||
|
|
@ -861,7 +872,9 @@ extension KeymanWebViewController {
|
|||
|
||||
// MARK: - Show/hide views
|
||||
func reloadKeyboard() {
|
||||
webView!.loadFileURL(Storage.active.kmwURL, allowingReadAccessTo: Storage.active.baseDir)
|
||||
let hostPageFileUrl = URL(fileURLWithPath: Resources.kmwFilename, relativeTo: storage.baseDir)
|
||||
let hostPageUrl = schemeHandler.buildUrlForFile(fileURL: hostPageFileUrl)
|
||||
webView!.load(URLRequest(url: hostPageUrl))
|
||||
isLoading = true
|
||||
|
||||
updateSpacebarText()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Created by Joshua Horton on 2026-06-24.
|
||||
*
|
||||
* WebViewKeyboardLoader implements a URLSchemeHandler that allows
|
||||
* the hosted Keyman Engine for Web to access all files, consistently,
|
||||
* via a http-like protocol, preventing CORS access issues for files
|
||||
* loaded dynamically.
|
||||
*/
|
||||
|
||||
import WebKit
|
||||
import UniformTypeIdentifiers
|
||||
import os.log
|
||||
|
||||
func getMimeType(forExtension ext: String) -> String {
|
||||
// Find the UTType associated with the file extension
|
||||
if let utType = UTType(filenameExtension: ext) {
|
||||
// Return the preferred MIME type if it exists
|
||||
return utType.preferredMIMEType ?? "application/octet-stream"
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
class WebViewSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
let storage: Storage
|
||||
let scheme = "keyman-engine"
|
||||
|
||||
init(storage: Storage) {
|
||||
self.storage = storage
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) {
|
||||
guard let url = urlSchemeTask.request.url else {
|
||||
return
|
||||
}
|
||||
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
|
||||
components.scheme = "file"
|
||||
|
||||
let fileUrl = components.url
|
||||
|
||||
let doError = { () -> Void in
|
||||
let message = "Could not load url via WKURLSchemeHandler: \(url)"
|
||||
let errorInfo = [
|
||||
NSLocalizedDescriptionKey: message
|
||||
]
|
||||
let error = NSError(domain: "WebViewKeyboardLoader", code: 500, userInfo: errorInfo)
|
||||
|
||||
os_log("%{public}s", log:KeymanEngineLogger.settings, type: .error, message)
|
||||
SentryManager.capture(error, message: message)
|
||||
|
||||
urlSchemeTask.didFailWithError(error)
|
||||
}
|
||||
|
||||
guard fileUrl != nil else {
|
||||
doError()
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let fileContents = try Data(contentsOf: fileUrl!)
|
||||
let fileExtension = fileUrl!.pathExtension
|
||||
|
||||
let mimeType: String = getMimeType(forExtension: fileExtension)
|
||||
let charset: String = (mimeType.hasPrefix("text/") || mimeType == "application/json")
|
||||
? "; charset=utf-8"
|
||||
: ""
|
||||
|
||||
let response = HTTPURLResponse(
|
||||
url: url,
|
||||
statusCode: 200,
|
||||
httpVersion: "HTTP/1.1",
|
||||
headerFields: [
|
||||
"Content-Type": "\(mimeType)\(charset)",
|
||||
]
|
||||
)!
|
||||
|
||||
urlSchemeTask.didReceive(response)
|
||||
urlSchemeTask.didReceive(fileContents)
|
||||
urlSchemeTask.didFinish()
|
||||
} catch {
|
||||
doError()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, stop urlSchemeTask: any WKURLSchemeTask) {
|
||||
}
|
||||
|
||||
func buildUrlForFile(fileURL: URL) -> URL {
|
||||
var loadingURLBuilder = URLComponents()
|
||||
loadingURLBuilder.scheme = scheme
|
||||
loadingURLBuilder.path = fileURL.path
|
||||
return loadingURLBuilder.url!
|
||||
}
|
||||
}
|
||||
|
|
@ -245,6 +245,10 @@ extension Storage {
|
|||
resourceName: "kmwosk.css",
|
||||
dstDir: baseDir,
|
||||
excludeFromBackup: true)
|
||||
try Storage.copy(from: bundle,
|
||||
resourceName: "globe-hint.css",
|
||||
dstDir: baseDir,
|
||||
excludeFromBackup: true)
|
||||
try Storage.copy(from: bundle,
|
||||
resourceName: "keymanweb-osk.ttf",
|
||||
dstDir: baseDir,
|
||||
|
|
|
|||
|
|
@ -102,7 +102,8 @@ function update_bundle ( ) {
|
|||
KMW_PRODUCT="$KEYMAN_ROOT/web/build/app/webview/$CONFIG"
|
||||
KMW_RESOURCES="$KEYMAN_ROOT/web/build/app/resources"
|
||||
|
||||
#Copy over the relevant resources! It's easiest to do if we navigate to the resulting folder.
|
||||
# Copy relevant KeymanWeb resources; this list is also in Storage.swift
|
||||
cp "$KMW_RESOURCES/osk/globe-hint.css" "$BUNDLE_PATH/globe-hint.css"
|
||||
cp "$KMW_RESOURCES/osk/kmwosk.css" "$BUNDLE_PATH/kmwosk.css"
|
||||
cp "$KMW_RESOURCES/osk/keymanweb-osk.ttf" "$BUNDLE_PATH/keymanweb-osk.ttf"
|
||||
cp "$KMW_PRODUCT/keymanweb-webview.js" "$BUNDLE_PATH/keymanweb-webview.js"
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ dependencies {
|
|||
implementation files('libs/keyman-engine.aar')
|
||||
implementation 'io.sentry:sentry-android:8.19.1'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.14.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
|
|
|||
|
|
@ -7,13 +7,9 @@ type GitHub = ReturnType<typeof getOctokit>;
|
|||
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { gt } from 'semver';
|
||||
import { reportHistory } from './reportHistory.js';
|
||||
import { reportHistory, PRInformation } from './reportHistory.js';
|
||||
import { spawnChild } from './util/spawnAwait.js';
|
||||
|
||||
interface PRInformation {
|
||||
title: string;
|
||||
number: number;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// splitPullsIntoHistory
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const getPullRequestInformation = async (
|
|||
return commit_id;
|
||||
};
|
||||
|
||||
interface PRInformation {
|
||||
export interface PRInformation {
|
||||
title: string;
|
||||
number: number;
|
||||
version?: string;
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ else
|
|||
fi
|
||||
|
||||
run_android() {
|
||||
if [[ -n ${RELEASE_OEM:-} ]] && [[ -n ${RELEASE_OEM_FIRSTVOICES:-} ]]; then
|
||||
DOCKER_RUN_ARGS+=(-e "RELEASE_OEM=${RELEASE_OEM:-}" -e "RELEASE_OEM_FIRSTVOICES=${RELEASE_OEM_FIRSTVOICES:-}")
|
||||
fi
|
||||
docker_wrapper run "${DOCKER_RUN_ARGS[@]}" -i --rm -v "${KEYMAN_ROOT}":/home/build/build \
|
||||
-v "${KEYMAN_ROOT}/core/build/docker-core/${build_dir}":/home/build/build/core/build \
|
||||
"${registry_slash}keymanapp/keyman-android-ci:${image_version}" \
|
||||
|
|
|
|||
19
web/build.sh
19
web/build.sh
|
|
@ -114,6 +114,25 @@ build_tests_action() {
|
|||
|
||||
cp "${KEYMAN_ROOT}/web/src/test/auto/dom/cases/attachment/textStoreForElement.tests.html" \
|
||||
"${KEYMAN_ROOT}/web/build/test/dom/cases/attachment/"
|
||||
|
||||
# Copy and update guide examples - for local, PR, and test builds we
|
||||
# replace the CDN URL with the local build path, so that we can test
|
||||
# against the current build
|
||||
mkdir -p "${KEYMAN_ROOT}/web/build/docs/engine/guide"
|
||||
cp -r "${KEYMAN_ROOT}/web/docs/engine/guide/examples" \
|
||||
"${KEYMAN_ROOT}/web/build/docs/engine/guide/"
|
||||
|
||||
# shellcheck disable=SC2310
|
||||
if ! builder_is_ci_release_build; then
|
||||
for f in "${KEYMAN_ROOT}/web/build/docs/engine/guide/examples"/*.html; do
|
||||
# Replace CDN URL (https://s.keyman.com/kwm/engine/18.0.123) with
|
||||
# local local build path (/build/publish/debug). We write to a temp
|
||||
# file and then replace the original instead of modifying in-place.
|
||||
# This is safer and more portable.
|
||||
sed "s|https://s\.keyman\.com/kmw/engine/[0-9]*\.[0-9]*\.[0-9]*/|/build/publish/${config}/|g" \
|
||||
"${f}" > "${f}.tmp" && mv "${f}.tmp" "${f}"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
coverage_action() {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@
|
|||
<body>
|
||||
<h1>Automatic Mode Example</h1>
|
||||
<form action='.' method='post'>
|
||||
<p><input type='text' id='multilingual' name='multilingual' size="40" /></p>
|
||||
<p><textarea cols='40' rows='5'></textarea></p>
|
||||
<p><input type='text' id='multilingual' name='multilingual' size="40" data-testid='multilingual'/></p>
|
||||
<p><textarea cols='40' rows='5' data-testid='textarea'></textarea></p>
|
||||
</form>
|
||||
|
||||
<a href="automatic-control">Back to Document</a>
|
||||
|
|
|
|||
|
|
@ -24,13 +24,13 @@ keyman.init().then(async function() {
|
|||
}
|
||||
document.f.multilingual.focus();
|
||||
|
||||
keyman.setActiveKeyboard('', '');
|
||||
await keyman.setActiveKeyboard('', '');
|
||||
});
|
||||
|
||||
function KWControlChange() {
|
||||
async function KWControlChange() {
|
||||
var name = KWControl.value.substr(0, KWControl.value.indexOf("$$"));
|
||||
var languageCode = KWControl.value.substr(KWControl.value.indexOf("$$") + 2);
|
||||
keyman.setActiveKeyboard(name, languageCode);
|
||||
await keyman.setActiveKeyboard(name, languageCode);
|
||||
document.f.multilingual.focus();
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ function KWControlChange() {
|
|||
<h1>Manual Control - Custom Interface</h1>
|
||||
<form name='f' action='.' method='post'>
|
||||
|
||||
<p>Keyboard: <select id='KWControl' onchange='KWControlChange()'><option value=''>English</option></select></p>
|
||||
<p><label for='KWControl'>Keyboard:</label> <select id='KWControl' onchange='KWControlChange()'><option value=''>English</option></select></p>
|
||||
|
||||
<p><input type='text' id='multilingual' name='multilingual' size="40" placeholder="id='multilingual'"/></p>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
languages: { id: 'lo', name: 'Lao' },
|
||||
filename: "./js/laokeys.js"
|
||||
});
|
||||
keyman.setActiveKeyboard('laokeys');
|
||||
await keyman.setActiveKeyboard('laokeys');
|
||||
keyman.osk.hide();
|
||||
});
|
||||
|
||||
|
|
@ -30,8 +30,8 @@
|
|||
|
||||
<p><img style="border: solid 1px black; padding: 2px 2px 3px 2px" src='js/kmicon.png' alt='KeymanWeb' onclick='KWControlClick()' id='KWControl' /></p>
|
||||
|
||||
<p><input type='text' id='multilingual' name='multilingual' size="40" /></p>
|
||||
<p><textarea cols='40' rows='5'></textarea></p>
|
||||
<p><input type='text' id='multilingual' name='multilingual' size="40" data-testid='multilingual'/></p>
|
||||
<p><textarea cols='40' rows='5' data-testid='textarea'></textarea></p>
|
||||
|
||||
<a href="manual-control">Back to Document</a>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -28,15 +28,15 @@ Include the following script in the HEAD of your page:
|
|||
}
|
||||
document.f.multilingual.focus();
|
||||
|
||||
keyman.setActiveKeyboard('', '');
|
||||
await keyman.setActiveKeyboard('', '');
|
||||
});
|
||||
|
||||
/* KWControlChange: Called when user selects an item in the KWControl SELECT */
|
||||
function KWControlChange() {
|
||||
async function KWControlChange() {
|
||||
/* Select the keyboard in KeymanWeb */
|
||||
var name = KWControl.value.substr(0, KWControl.value.indexOf("$$"));
|
||||
var languageCode = KWControl.value.substr(KWControl.value.indexOf("$$"+2));
|
||||
keyman.setActiveKeyboard(name, languageCode);
|
||||
var languageCode = KWControl.value.substr(KWControl.value.indexOf("$$") + 2);
|
||||
await keyman.setActiveKeyboard(name, languageCode);
|
||||
/* Focus onto the multilingual field in the form */
|
||||
document.f.multilingual.focus();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@
|
|||
title: Manual Mode Example
|
||||
---
|
||||
|
||||
In this example, the web page designer specifies when KeymanWeb's on-screen keyboard may be displayed on non-mobile devices. They have also specified that the LaoKeys keyboard should be activated by default. This example continues to use the KeymanWeb default interface. Please click [this link](__manual-control.html) to open the test page.
|
||||
In this example, the web page designer specifies when KeymanWeb's on-screen keyboard may be
|
||||
displayed on non-mobile devices. They have also specified that the LaoKeys keyboard should be
|
||||
activated by default. This example continues to use the KeymanWeb default interface. Please click
|
||||
[this link](__manual-control.html) to open the test page.
|
||||
|
||||
## Code Walkthrough
|
||||
|
||||
|
|
@ -17,7 +20,7 @@ Include the following script in the HEAD of your page:
|
|||
languages:{id:'lo',name:'Lao'},
|
||||
filename: "./js/laokeys.js"
|
||||
});
|
||||
keyman.setActiveKeyboard('laokeys');
|
||||
await keyman.setActiveKeyboard('laokeys');
|
||||
keyman.osk.hide();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -21,24 +21,12 @@ export class PathConfiguration implements OSKResourcePathConfiguration {
|
|||
private _fonts: string;
|
||||
readonly protocol: string;
|
||||
|
||||
/*
|
||||
* Pre-modularization code corresponding to `sourcePath`:
|
||||
```
|
||||
// Determine path and protocol of executing script, setting them as
|
||||
// construction defaults.
|
||||
//
|
||||
// This can only be done during load when the active script will be the
|
||||
// last script loaded. Otherwise the script must be identified by name.
|
||||
|
||||
var scripts = document.getElementsByTagName('script');
|
||||
var ss = scripts[scripts.length-1].src;
|
||||
var sPath = ss.substr(0,ss.lastIndexOf('/')+1);
|
||||
```
|
||||
*/
|
||||
constructor(pathSpec: Required<PathOptionSpec>, sourcePath: string) {
|
||||
const sourceURL = new URL(sourcePath);
|
||||
|
||||
sourcePath = addDelimiter(sourcePath);
|
||||
this.sourcePath = sourcePath;
|
||||
this.protocol = sourcePath.replace(/(.{3,5}:)(.*)/,'$1');
|
||||
this.protocol = sourceURL.protocol;
|
||||
|
||||
this.updateFromOptions(pathSpec);
|
||||
}
|
||||
|
|
@ -75,8 +63,16 @@ export class PathConfiguration implements OSKResourcePathConfiguration {
|
|||
|
||||
p = addDelimiter(p);
|
||||
|
||||
// Absolute
|
||||
if((p.replace(/^(http)s?:.*/,'$1') == 'http') || (p.replace(/^(file):.*/,'$1') == 'file')) {
|
||||
// Absolute - with protocol specified
|
||||
const protocolList = [
|
||||
'http:',
|
||||
'https:',
|
||||
'file:',
|
||||
// If using a custom origin (say, hosted in an iOS WebView via WKURLSchemeHandler)
|
||||
this.protocol
|
||||
];
|
||||
|
||||
if(protocolList.find((protocol) => p.startsWith(protocol))) {
|
||||
return p;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ProcessorInitOptions } from 'keyman/engine/js-processor';
|
|||
import { DOMKeyboardLoader } from "keyman/engine/keyboard";
|
||||
import { WorkerFactory } from "@keymanapp/lexical-model-layer/web"
|
||||
import { InputProcessor } from './headless/inputProcessor.js';
|
||||
import { OSKView, KeyboardData } from "keyman/engine/osk";
|
||||
import { OSKView } from "keyman/engine/osk";
|
||||
import { KeyboardRequisitioner, ModelCache, toUnprefixedKeyboardId, DOMCloudRequester } from "keyman/engine/keyboard-storage";
|
||||
import { ModelSpec, PredictionContext } from "keyman/engine/interfaces";
|
||||
|
||||
|
|
@ -388,7 +388,7 @@ export class KeymanEngineBase<
|
|||
this.core.keyboardProcessor.contextDevice = value?.targetDevice ?? this.config.softDevice;
|
||||
if(value) {
|
||||
// Don't build an OSK if no keyboard is available yet; avoid the extra flash.
|
||||
if (this.contextManager.activeKeyboard && this.contextManager.activeKeyboard instanceof KeyboardData) { // TODO-embed-osk-in-kmx: add support for OSK for KMX keyboards
|
||||
if (this.contextManager.activeKeyboard) {
|
||||
value.activeKeyboard = this.contextManager.activeKeyboard;
|
||||
}
|
||||
value.on('keyevent', this.keyEventListener);
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ export function getResourcePath(config: ViewConfiguration) {
|
|||
if(config.isEmbedded) {
|
||||
resourcePathExt = '';
|
||||
}
|
||||
return `${config.pathConfig.resources}/${resourcePathExt}`
|
||||
return `${config.pathConfig.resources}${resourcePathExt}`
|
||||
}
|
||||
|
||||
export abstract class OSKView
|
||||
|
|
@ -267,6 +267,7 @@ export abstract class OSKView
|
|||
|
||||
for(const sheetFile of OSKView.STYLESHEET_FILES) {
|
||||
const sheetHref = `${resourcePath}${sheetFile}`;
|
||||
|
||||
this.uiStyleSheetManager.linkExternalSheet(sheetHref);
|
||||
}
|
||||
|
||||
|
|
@ -856,7 +857,7 @@ export abstract class OSKView
|
|||
isEmbedded: this.config.isEmbedded,
|
||||
specialFont: {
|
||||
family: 'SpecialOSK',
|
||||
files: [`${resourcePath}/keymanweb-osk.ttf`],
|
||||
files: [`${resourcePath}keymanweb-osk.ttf`],
|
||||
path: '' // Not actually used.
|
||||
},
|
||||
gestureParams: this.config.gestureParams
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import { KeyTip } from './keytip.interface.js';
|
|||
import { OSKKey } from './keyboard-layout/oskKey.js';
|
||||
import { OSKLayer, LayerLayoutParams } from './keyboard-layout/oskLayer.js';
|
||||
import { OSKLayerGroup } from './keyboard-layout/oskLayerGroup.js';
|
||||
import { OSKView } from './views/oskView.js';
|
||||
import { getResourcePath, OSKView } from './views/oskView.js';
|
||||
import { ParsedLengthStyle } from './lengthStyle.js';
|
||||
import { defaultFontSize } from './fontSizeUtils.js';
|
||||
import { PhoneKeyTip } from './input/gestures/browser/phoneKeytip.js';
|
||||
|
|
@ -1372,7 +1372,8 @@ export class VisualKeyboard extends EventEmitter<EventMap> implements KeyboardVi
|
|||
device: {
|
||||
formFactor?: DeviceSpec.FormFactor,
|
||||
OS?: DeviceSpec.OperatingSystem,
|
||||
touchable?: boolean
|
||||
touchable?: boolean,
|
||||
browser?: DeviceSpec.Browser
|
||||
} = {};
|
||||
|
||||
// Device emulation for target documentation.
|
||||
|
|
@ -1380,9 +1381,11 @@ export class VisualKeyboard extends EventEmitter<EventMap> implements KeyboardVi
|
|||
if (formFactor != 'desktop') {
|
||||
device.OS = DeviceSpec.OperatingSystem.iOS;
|
||||
device.touchable = true;
|
||||
device.browser = DeviceSpec.Browser.Safari;
|
||||
} else {
|
||||
device.OS = DeviceSpec.OperatingSystem.Windows;
|
||||
device.touchable = false;
|
||||
device.browser = DeviceSpec.Browser.Chrome;
|
||||
}
|
||||
|
||||
const layout = PKbd.layout(formFactor);
|
||||
|
|
@ -1398,7 +1401,11 @@ export class VisualKeyboard extends EventEmitter<EventMap> implements KeyboardVi
|
|||
styleSheetManager: null,
|
||||
specialFont: {
|
||||
family: 'SpecialOSK',
|
||||
files: [`${pathConfig.resources}/osk/keymanweb-osk.ttf`],
|
||||
files: [`${getResourcePath({
|
||||
// Not actually leveraged.
|
||||
hostDevice: device as DeviceSpec,
|
||||
pathConfig
|
||||
})}keymanweb-osk.ttf`],
|
||||
path: '' // Not actually used.
|
||||
}
|
||||
});
|
||||
|
|
|
|||
64
web/src/test/auto/e2e/e2eUtils.ts
Normal file
64
web/src/test/auto/e2e/e2eUtils.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*/
|
||||
|
||||
import { type Locator, type Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Expands the keyboard selection menu and returns the text content of the
|
||||
* currently selected keyboard.
|
||||
*/
|
||||
export async function getSelectedKeyboardMenuText(page: Page): Promise<string | undefined> {
|
||||
const watchDog = page.waitForFunction(() => !!document.getElementById('KeymanWeb_KbdList'));
|
||||
await page.getByRole('img', { name: 'Use Web Keyboard' }).click();
|
||||
await watchDog;
|
||||
return page.evaluate(() => {
|
||||
const selectedKbd = document.querySelector('#kmwico .selected');
|
||||
return selectedKbd?.textContent;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Expands the keyboard selection menu and returns the menu items as an array
|
||||
*/
|
||||
export async function getAllKeyboardMenuText(page: Page): Promise<(string|undefined)[]> {
|
||||
const watchDog = page.waitForFunction(() => !!document.getElementById('KeymanWeb_KbdList'));
|
||||
await page.getByRole('img', { name: 'Use Web Keyboard' }).hover();
|
||||
await watchDog;
|
||||
return page.evaluate(() => {
|
||||
const menuItems = [];
|
||||
const menuDiv = document.querySelector('#kmwico');
|
||||
const kbdList = menuDiv?.lastElementChild;
|
||||
for (let i = 0; i < (kbdList ? kbdList.children.length : 0); i++) {
|
||||
const item = kbdList?.children[i];
|
||||
menuItems.push(item?.textContent);
|
||||
}
|
||||
return menuItems;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the specified URL and waits for the page load event.
|
||||
*/
|
||||
export async function loadPage(page: Page, url: string): Promise<Page> {
|
||||
const loadPromise = page.waitForEvent('load');
|
||||
await page.goto(url);
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the specified field and waits for the OSK to be shown, returning a
|
||||
* locator for the OSK title bar.
|
||||
*/
|
||||
export async function clickFieldAndWaitForOSK(page: Page, fieldLocator: Locator): Promise<Locator> {
|
||||
const keyboardchangePromise = page.evaluate(async () => {
|
||||
return new Promise((resolve) => {
|
||||
keyman.addEventListener('keyboardchange', function (kbd) {
|
||||
resolve(kbd);
|
||||
});
|
||||
});
|
||||
});
|
||||
await fieldLocator.click();
|
||||
await keyboardchangePromise;
|
||||
return page.locator('#keymanweb_title_bar');
|
||||
}
|
||||
256
web/src/test/auto/e2e/guide-examples.tests.ts
Normal file
256
web/src/test/auto/e2e/guide-examples.tests.ts
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*/
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { clickFieldAndWaitForOSK, getAllKeyboardMenuText, getSelectedKeyboardMenuText, loadPage } from './e2eUtils';
|
||||
|
||||
async function setTimeoutAndLoadPage(page: Page, url: string): Promise<void> {
|
||||
test.setTimeout(5000);
|
||||
await loadPage(page, url);
|
||||
}
|
||||
|
||||
test.describe('First example from the guide', function () {
|
||||
const beforeEach = async (page: Page) => {
|
||||
await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__first-example.html');
|
||||
}
|
||||
|
||||
test('Input field shows US keyboard', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
const oskTitleBar = await clickFieldAndWaitForOSK(page, page.getByPlaceholder('Hello World'));
|
||||
|
||||
// Verify OSK shows US keyboard
|
||||
await expect(page.getByRole('img', { name: 'Use Web Keyboard' })).toBeVisible();
|
||||
await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).toBeVisible();
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy();
|
||||
await expect(oskTitleBar).toContainText('US');
|
||||
|
||||
await expect(await getSelectedKeyboardMenuText(page)).toBe('English - US');
|
||||
});
|
||||
|
||||
test('Keyman menu has expected keyboards', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
await clickFieldAndWaitForOSK(page, page.getByPlaceholder('Hello World'));
|
||||
|
||||
// Verify OSK menu has expected entries
|
||||
await expect(page.getByRole('img', { name: 'Use Web Keyboard' })).toBeVisible();
|
||||
await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).toBeVisible();
|
||||
await expect(await getAllKeyboardMenuText(page)).toEqual(['(System keyboard)', 'English - US', 'Thai - Thai Kedmanee Basic'])
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Auto-control example from the guide', function () {
|
||||
const beforeEach = async (page: Page) => {
|
||||
await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__auto-control.html');
|
||||
}
|
||||
|
||||
test('Input field shows Lao keyboard', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
await page.getByTestId('multilingual' ).click();
|
||||
|
||||
// Verify OSK is shown
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy();
|
||||
await expect(page.locator('#keymanweb_title_bar')).toContainText('Lao (Phonetic)');
|
||||
});
|
||||
|
||||
test('Textarea shows Lao keyboard', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
await page.getByTestId('textarea').click();
|
||||
|
||||
// Verify OSK is shown
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy();
|
||||
await expect(page.locator('#keymanweb_title_bar')).toContainText('Lao (Phonetic)');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Control-by-control example from the guide', function () {
|
||||
const beforeEach = async (page: Page) => {
|
||||
await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__control-by-control.html');
|
||||
}
|
||||
|
||||
test('address field does not have KeymanWeb enabled', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
await page.getByPlaceholder('id = address').click();
|
||||
|
||||
// Verify OSK is not shown
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeFalsy();
|
||||
await expect(page.getByRole('img', { name: 'Use Web Keyboard' })).not.toBeVisible();
|
||||
await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).not.toBeVisible();
|
||||
});
|
||||
|
||||
// TODO: #16080
|
||||
test.skip('subject field does not show keyboard and defaults to system keyboard', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
await page.getByPlaceholder('id = subject').click();
|
||||
|
||||
// Verify OSK is shown
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy();
|
||||
await expect(page.getByRole('img', { name: 'Use Web Keyboard' })).toBeVisible();
|
||||
await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).not.toBeVisible();
|
||||
|
||||
await expect(await getSelectedKeyboardMenuText(page)).toBe('(System keyboard)');
|
||||
});
|
||||
|
||||
test('message body field shows Lao keyboard', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
await page.getByPlaceholder('id = text').click();
|
||||
|
||||
// Verify OSK is shown
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy();
|
||||
await expect(page.getByRole('img', { name: 'Use Web Keyboard' })).toBeVisible();
|
||||
await expect(page.getByRole('img', { name: 'Show On Screen Keyboard' })).toBeVisible();
|
||||
|
||||
// Verify Lao (Phonetic) keyboard is active
|
||||
await expect(page.locator('#keymanweb_title_bar')).toContainText('Lao (Phonetic)');
|
||||
// Verify "Lao - Lao (Phonetic)" is selected (bold) in the menu
|
||||
await expect(await getSelectedKeyboardMenuText(page)).toBe('Lao - Lao (Phonetic)');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Full manual control example from the guide', function () {
|
||||
const beforeEach = async (page: Page) => {
|
||||
await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__full-manual-control.html');
|
||||
}
|
||||
|
||||
test('Shows English and no OSK after loading page', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
|
||||
// Verify 'English' selected (which has the value '') and no OSK showing
|
||||
await expect(page.getByLabel('Keyboard')).toHaveValue('');
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).not.toBeTruthy();
|
||||
});
|
||||
|
||||
test('Selecting English keyboard shows no OSK', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
// first switch to Hebrew
|
||||
let keyboardchangePromise = page.evaluate(async () => {
|
||||
return new Promise((resolve) => {
|
||||
keyman.addEventListener('keyboardchange', function (kbd) {
|
||||
resolve(kbd);
|
||||
});
|
||||
});
|
||||
});
|
||||
await page.getByLabel('Keyboard').selectOption('Hebrew');
|
||||
await keyboardchangePromise;
|
||||
|
||||
// then back to English
|
||||
keyboardchangePromise = page.evaluate(async () => {
|
||||
return new Promise((resolve) => {
|
||||
keyman.addEventListener('keyboardchange', function (kbd) {
|
||||
resolve(kbd);
|
||||
});
|
||||
});
|
||||
});
|
||||
await page.getByLabel('Keyboard').selectOption('English');
|
||||
await keyboardchangePromise;
|
||||
|
||||
// Verify no OSK showing
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).not.toBeTruthy();
|
||||
});
|
||||
|
||||
test('Selecting Devanagari keyboard shows Devanagari OSK', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
const keyboardchangePromise = page.evaluate(async () => {
|
||||
return new Promise((resolve) => {
|
||||
keyman.addEventListener('keyboardchange', function (kbd) {
|
||||
resolve(kbd);
|
||||
});
|
||||
});
|
||||
});
|
||||
await page.getByLabel('Keyboard').selectOption('Devanagari (INSCRIPT)');
|
||||
await keyboardchangePromise;
|
||||
|
||||
// Verify Devanagari OSK showing
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy();
|
||||
await expect(page.locator('#keymanweb_title_bar')).toContainText('Devanagari (INSCRIPT)');
|
||||
});
|
||||
|
||||
test('Selecting Hebrew shows Hebrew OSK', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
const keyboardchangePromise = page.evaluate(async () => {
|
||||
return new Promise((resolve) => {
|
||||
keyman.addEventListener('keyboardchange', function (kbd) {
|
||||
resolve(kbd);
|
||||
});
|
||||
});
|
||||
});
|
||||
await page.getByLabel('Keyboard').selectOption('Hebrew');
|
||||
await keyboardchangePromise;
|
||||
|
||||
// Verify Hebrew OSK showing
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy();
|
||||
await expect(page.locator('#keymanweb_title_bar')).toContainText('Hebrew');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Manual control example from the guide', function () {
|
||||
const beforeEach = async (page: Page) => {
|
||||
await setTimeoutAndLoadPage(page, 'http://localhost:3000/build/docs/engine/guide/examples/__manual-control.html');
|
||||
}
|
||||
|
||||
test('Does not show OSK after loading', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
await page.getByTestId('multilingual').click();
|
||||
|
||||
// Verify no OSK showing
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).not.toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).not.toBeTruthy();
|
||||
});
|
||||
|
||||
test('Shows Lao OSK after clicking button', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
await page.getByAltText('KeymanWeb').click();
|
||||
await page.getByTestId('multilingual').click();
|
||||
|
||||
// Verify Lao OSK showing
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy();
|
||||
await expect(page.locator('#keymanweb_title_bar')).toContainText('Lao');
|
||||
});
|
||||
|
||||
test('Hides Lao OSK after clicking button', async ({ page }) => {
|
||||
// Setup
|
||||
await beforeEach(page);
|
||||
|
||||
// click button
|
||||
await page.getByAltText('KeymanWeb').click();
|
||||
|
||||
// Verify Lao OSK showing
|
||||
await page.getByTestId('multilingual').click();
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).toBeTruthy();
|
||||
await expect(page.locator('#keymanweb_title_bar')).toContainText('Lao');
|
||||
|
||||
// Click button again to hide OSK
|
||||
await page.getByAltText('KeymanWeb').click();
|
||||
|
||||
// Verify Lao OSK not showing
|
||||
await page.getByTestId('multilingual').click();
|
||||
await expect(await page.evaluate(() => keyman.osk.isEnabled())).not.toBeTruthy();
|
||||
await expect(await page.evaluate(() => keyman.osk.isVisible())).not.toBeTruthy();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -158,6 +158,7 @@ begin
|
|||
try
|
||||
http.Fields.Add('version', ansistring(CKeymanVersionInfo.Version));
|
||||
http.Fields.Add('tier', ansistring(CKeymanVersionInfo.Tier));
|
||||
http.Fields.Add('update', '1'); // This is checking for an update
|
||||
if FForce then
|
||||
http.Fields.Add('manual', '1')
|
||||
else
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue