mirror of
https://github.com/keymanapp/keyman.git
synced 2026-11-03 09:37:47 +00:00
Merge pull request #16324 from keymanapp/auto/A19S34-merge-base-into-kmc-convert
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: A19S34 merge master into kmc-convert 😎
This commit is contained in:
commit
d3b07e5768
29 changed files with 520 additions and 143 deletions
19
HISTORY.md
19
HISTORY.md
|
|
@ -1,5 +1,24 @@
|
|||
# Keyman Version History
|
||||
|
||||
## 19.0.262 alpha 2026-08-04
|
||||
|
||||
* chore(web): fire `keyboardloaded` event only once per keyboard (#16297)
|
||||
|
||||
## 19.0.261 alpha 2026-07-30
|
||||
|
||||
* fix(core): clone state actions deep copy (#15961)
|
||||
* maint(android): update Android Target API to 36 (#16280)
|
||||
* refactor(web): adjust function visibility and variable names (#16286)
|
||||
|
||||
## 19.0.260 alpha 2026-07-27
|
||||
|
||||
* fix(web): ignore exception trying to access cookie (#16283)
|
||||
* chore(web): update help links to use latest version (#16285)
|
||||
|
||||
## 19.0.259 alpha 2026-07-20
|
||||
|
||||
* docs(web): fix typos following #16170 (#16225)
|
||||
|
||||
## 19.0.258 alpha 2026-07-15
|
||||
|
||||
* chore(android): remove doc for non-existing deprecated functions (#16244)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
19.0.259
|
||||
19.0.263
|
||||
|
|
@ -17,7 +17,7 @@ java {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdk 35
|
||||
compileSdk 36
|
||||
namespace="com.tavultesoft.kmapro"
|
||||
|
||||
// Don't compress kmp files so they can be copied via AssetManager
|
||||
|
|
@ -32,7 +32,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "com.tavultesoft.kmapro"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
targetSdkVersion 36
|
||||
|
||||
//println "===DUMPING PROPERTIES==="
|
||||
//dumpProperties(project) // Use this to dump all external properties for debugging TeamCity integration
|
||||
|
|
|
|||
|
|
@ -468,7 +468,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
|
|||
return true;
|
||||
} else if (item.getItemId() == R.id.action_update_keyboards) {
|
||||
KMManager.getUpdateTool().executeOpenUpdates();
|
||||
// Dismiss icon
|
||||
// Dismiss icon - we intentionally hide the icon, even if the user cancels the update
|
||||
updateUpdateCountIndicator(0);
|
||||
final MenuItem _keyboardupdate = menu.findItem(R.id.action_update_keyboards);
|
||||
if (_keyboardupdate != null && _keyboardupdate.isVisible()) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ java {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdk 35
|
||||
compileSdk 36
|
||||
namespace "com.keyman.engine"
|
||||
|
||||
buildFeatures {
|
||||
|
|
@ -29,7 +29,7 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
targetSdkVersion 36
|
||||
|
||||
// KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle but Gradle removes them for libraries
|
||||
buildConfigField "String", "KEYMAN_ENGINE_VERSION_NAME", "\""+KEYMAN_VERSION_NAME+"\""
|
||||
|
|
|
|||
|
|
@ -263,14 +263,14 @@ public final class KeyboardPickerActivity extends BaseActivity {
|
|||
|
||||
|
||||
// Determine the index to the current keyboard position to highlight as the selected keyboard
|
||||
int currentKeyboardIndex;
|
||||
int currentKeyboardIndex;
|
||||
String currentKeyboardKey = KMKeyboard.currentKeyboard();
|
||||
if (currentKeyboardKey == null) {
|
||||
SharedPreferences prefs = this.getSharedPreferences(this.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
|
||||
currentKeyboardIndex = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0);
|
||||
} else {
|
||||
currentKeyboardIndex = KeyboardController.getInstance().getKeyboardIndex(currentKeyboardKey);
|
||||
}
|
||||
}
|
||||
setSelection(currentKeyboardIndex);
|
||||
|
||||
imeList = getIMEList(this);
|
||||
|
|
@ -756,6 +756,32 @@ public final class KeyboardPickerActivity extends BaseActivity {
|
|||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array list with all lexical model hashmaps matching the lexical model ID.
|
||||
* @param context
|
||||
* @param lexicalModelId - ID of the lexical model
|
||||
* @return Array of lexical model hashmaps with lexicalModelId. Otherwise empty.
|
||||
*/
|
||||
public static ArrayList<HashMap<String, String>> getLexicalModelsMatchingId(Context context, String lexicalModelId) {
|
||||
ArrayList<HashMap<String, String>> lexicalModels = new ArrayList<>();
|
||||
|
||||
if (lexicalModelsList == null) {
|
||||
lexicalModelsList = getLexicalModelsList(context);
|
||||
}
|
||||
|
||||
if (lexicalModelsList != null) {
|
||||
int length = lexicalModelsList.size();
|
||||
for (int i=0; i < length; i++) {
|
||||
HashMap<String, String> lmInfo = lexicalModelsList.get(i);
|
||||
if (lexicalModelId.equals(lmInfo.get(KMManager.KMKey_LexicalModelID))) {
|
||||
lexicalModels.add(lmInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lexicalModels;
|
||||
}
|
||||
|
||||
public static HashMap<String, String> getLexicalModelInfo(Context context,int index) {
|
||||
if (index < 0) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.keyman.engine.KMManager;
|
|||
import com.keyman.engine.KeyboardPickerActivity;
|
||||
import com.keyman.engine.R;
|
||||
import com.keyman.engine.cloud.CloudApiTypes;
|
||||
import com.keyman.engine.data.CloudRepository;
|
||||
import com.keyman.engine.data.Keyboard;
|
||||
import com.keyman.engine.data.KeyboardController;
|
||||
import com.keyman.engine.data.LexicalModel;
|
||||
|
|
@ -188,7 +189,6 @@ public class CloudDataJsonUtil {
|
|||
}
|
||||
|
||||
public static void processLexicalModelPackageUpdateJSON(Context aContext, JSONObject pkgData, List<Bundle> updateBundles) {
|
||||
boolean saveModelsList = false;
|
||||
// Parse for lexical model package updates
|
||||
if (pkgData.has(CDKey_Models)) {
|
||||
try {
|
||||
|
|
@ -201,18 +201,11 @@ public class CloudDataJsonUtil {
|
|||
String cloudVersion = cloudModelObj.getString(CDKey_Version);
|
||||
String cloudKMP = cloudModelObj.getString(CDKey_KMP);
|
||||
// Valid lexical model package exists. See if lexical model list needs to be updated
|
||||
// Valid keyboard package exists. See if keyboard list needs to be updated
|
||||
int index = KeyboardPickerActivity.getLexicalModelIndex(aContext, lexicalModelID);
|
||||
if (index != -1) {
|
||||
HashMap<String, String> lmInfo = KeyboardPickerActivity.getLexicalModelInfo(aContext, index);
|
||||
String version = lmInfo.get(KMManager.KMKey_Version);
|
||||
ArrayList<HashMap<String, String>> lmInfos = KeyboardPickerActivity.getLexicalModelsMatchingId(aContext, lexicalModelID);
|
||||
for (HashMap<String, String> lmInfo : lmInfos) {
|
||||
String version = lmInfo.get(KMManager.KMKey_LexicalModelVersion);
|
||||
if (lexicalModelID.equalsIgnoreCase(lmInfo.get(KMManager.KMKey_LexicalModelID)) &&
|
||||
(FileUtils.compareVersions(cloudVersion, version) == FileUtils.VERSION_GREATER) &&
|
||||
(!MapCompat.getOrDefault(lmInfo, KMManager.KMKey_KMPLink, "").equalsIgnoreCase(cloudKMP))) {
|
||||
// Update keyboard with the latest KMP link
|
||||
lmInfo.put(KMManager.KMKey_KMPLink, cloudKMP);
|
||||
KeyboardPickerActivity.addLexicalModel(aContext, lmInfo);
|
||||
|
||||
(FileUtils.compareVersions(cloudVersion, version) == FileUtils.VERSION_GREATER)) {
|
||||
// Update bundle list
|
||||
LexicalModel lm = new LexicalModel(
|
||||
lmInfo.get(KMManager.KMKey_PackageID),
|
||||
|
|
@ -220,13 +213,25 @@ public class CloudDataJsonUtil {
|
|||
lmInfo.get(KMManager.KMKey_LexicalModelName),
|
||||
lmInfo.get(KMManager.KMKey_LanguageID),
|
||||
lmInfo.get(KMManager.KMKey_LanguageName),
|
||||
lmInfo.get(KMManager.KMKey_Version),
|
||||
lmInfo.get(KMManager.KMKey_LexicalModelVersion),
|
||||
lmInfo.get(KMManager.KMKey_CustomHelpLink),
|
||||
lmInfo.get(KMManager.KMKey_KMPLink));
|
||||
Bundle bundle = new Bundle(lm.buildDownloadBundle());
|
||||
updateBundles.add(bundle);
|
||||
String updateKMP = lm.getUpdateKMP();
|
||||
if (cloudLinkIsNewer(updateKMP, cloudKMP)) {
|
||||
// Update lexical model info with the latest KMP link after appending languageID
|
||||
String languageID = lm.getLanguageID();
|
||||
String link = String.format("%s&bcp47=%s", cloudKMP, languageID);
|
||||
lmInfo.put(KMManager.KMKey_KMPLink, link);
|
||||
KeyboardPickerActivity.addLexicalModel(aContext, lmInfo);
|
||||
lm.setUpdateKMP(link);
|
||||
updateKMP = link;
|
||||
}
|
||||
|
||||
saveModelsList = true;
|
||||
if (updateKMP != null && !updateKMP.isEmpty()) {
|
||||
// Update bundle list for update notifications
|
||||
Bundle bundle = new Bundle(lm.buildDownloadBundle());
|
||||
updateBundles.add(bundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -367,14 +372,13 @@ public class CloudDataJsonUtil {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (updateKMP == null || updateKMP.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
Uri cloudLink = Uri.parse(cloudKMP);
|
||||
|
||||
if (updateKMP == null || updateKMP.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Uri localLink = Uri.parse(updateKMP);
|
||||
Uri cloudLink = Uri.parse(cloudKMP);
|
||||
|
||||
boolean pathsMatch = localLink.getLastPathSegment().equalsIgnoreCase(cloudLink.getLastPathSegment());
|
||||
boolean cloudVersionNewer = FileUtils.compareVersions(
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ public class CloudCatalogDownloadCallback implements ICloudDownloadCallback<Data
|
|||
|
||||
// The actual update check
|
||||
CloudDataJsonUtil.processKeyboardPackageUpdateJSON(context, jsonTuple.packagesJSON, updateBundles);
|
||||
CloudDataJsonUtil.processLexicalModelPackageUpdateJSON(context, jsonTuple.packagesJSON, updateBundles);
|
||||
|
||||
// Only add installed kmp keyboards
|
||||
aDataSet.keyboards.clear();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.keyman.engine.cloud.CloudDownloadMgr;
|
|||
import com.keyman.engine.packages.JSONUtils;
|
||||
import com.keyman.engine.util.BCP47;
|
||||
import com.keyman.engine.util.DownloadFileUtils;
|
||||
import com.keyman.engine.util.FileUtils;
|
||||
import com.keyman.engine.util.KMLog;
|
||||
import com.keyman.engine.util.VersionUtils;
|
||||
|
||||
|
|
@ -153,8 +154,10 @@ public class CloudRepository {
|
|||
/**
|
||||
* Search the available lexical models list and see there's an associated model for a
|
||||
* given language ID. Available models are from the cloud catalog and locally installed models.
|
||||
* @param context Context
|
||||
* @param languageID String of the language ID to search
|
||||
* If more than one model are installed for a given language, the first one found will be
|
||||
* returned.
|
||||
* @param context Context
|
||||
* @param languageID String of the language ID to search
|
||||
* @return LexicalModel of an associated lexical model. Null if no match found
|
||||
*/
|
||||
public LexicalModel getAssociatedLexicalModel(@NonNull Context context, String languageID) {
|
||||
|
|
@ -253,6 +256,49 @@ public class CloudRepository {
|
|||
downloadMetaDataFromServer(context,updateHandler,onSuccess,onFailure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the new lexical models into the existing dataset models. If
|
||||
* a model already exists, it compares the versions and uses the newer one.
|
||||
* Otherwise the model gets added to the dataset. Models that exist
|
||||
* in the dataset but not in the new models list are preserved.
|
||||
* @param datasetModels Collection of lexical models in the dataset
|
||||
* @param newModels List of new lexical models to merge
|
||||
*/
|
||||
private void mergeLexicalModels(Dataset.LexicalModels datasetModels, List<LexicalModel> newModels) {
|
||||
if (newModels == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<LexicalModel> existingModels = new ArrayList<>(datasetModels.asList());
|
||||
List<LexicalModel> mergedModels = new ArrayList<>();
|
||||
|
||||
// Process all incoming models
|
||||
for (LexicalModel newModel : newModels) {
|
||||
LexicalModel existingMatch = null;
|
||||
for (int i = 0; i < existingModels.size(); i++) {
|
||||
if (newModel.equals(existingModels.get(i))) {
|
||||
existingMatch = existingModels.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingMatch != null) {
|
||||
if (FileUtils.compareVersions(existingMatch.getVersion(), newModel.getVersion()) != FileUtils.VERSION_LOWER) {
|
||||
mergedModels.add(existingMatch);
|
||||
} else {
|
||||
mergedModels.add(newModel);
|
||||
}
|
||||
} else {
|
||||
mergedModels.add(newModel);
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining existing models that weren't matched
|
||||
mergedModels.addAll(existingModels);
|
||||
|
||||
datasetModels.clear();
|
||||
datasetModels.addAll(mergedModels);
|
||||
}
|
||||
|
||||
/**
|
||||
* precache dataset and notify callbacks if no update from cloud api services is necessary.
|
||||
|
|
@ -286,11 +332,13 @@ public class CloudRepository {
|
|||
languageCodes.add(installedSet.getItem(i).code);
|
||||
}
|
||||
|
||||
// add all models that are installed locally
|
||||
memCachedDataset.lexicalModels.addAll(installedSet.lexicalModels.asList());
|
||||
|
||||
// Get kmp.json info from installed (adhoc and cloud) models.
|
||||
// Consolidate kmp.json info from packages/
|
||||
JSONObject kmpLanguagesArray = wrapKmpKeyboardJSON(JSONUtils.getLanguages());
|
||||
JSONArray kmpLexicalModelsArray = JSONUtils.getLexicalModels();
|
||||
final boolean fromKMP = true;
|
||||
|
||||
try {
|
||||
if (kmpLanguagesArray.getJSONObject(KMKeyboardDownloaderActivity.KMKey_Languages).
|
||||
|
|
@ -298,7 +346,10 @@ public class CloudRepository {
|
|||
memCachedDataset.keyboards.addAll(CloudDataJsonUtil.processKeyboardJSON(kmpLanguagesArray, true));
|
||||
}
|
||||
if (kmpLexicalModelsArray.length() > 0) {
|
||||
memCachedDataset.lexicalModels.addAll(CloudDataJsonUtil.processLexicalModelJSON(kmpLexicalModelsArray, fromKMP));
|
||||
// Add all models that exist locally, whether or not they are installed.
|
||||
// This set of models doesn't have the download url set, so instead of
|
||||
// replacing the models in `memCacheDataSet.lexicalModels` we merge them.
|
||||
mergeLexicalModels(memCachedDataset.lexicalModels, CloudDataJsonUtil.processLexicalModelJSON(kmpLexicalModelsArray, true));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
KMLog.LogException(TAG, "preCacheDataSet error ", e);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*/
|
||||
package com.keyman.engine.data;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class CloudRepositoryTests {
|
||||
|
||||
private CloudRepository repository;
|
||||
private Dataset dataset;
|
||||
private Context context;
|
||||
|
||||
private boolean containsModel(Dataset.LexicalModels models, String modelID) {
|
||||
for (int i = 0; i < models.getCount(); i++) {
|
||||
if (models.getItem(i).getLexicalModelID().equals(modelID)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setMemCachedDataset(Dataset dataset) throws Exception {
|
||||
Field field = CloudRepository.class.getDeclaredField("memCachedDataset");
|
||||
field.setAccessible(true);
|
||||
field.set(repository, dataset);
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
context = ApplicationProvider.getApplicationContext();
|
||||
repository = CloudRepository.shared;
|
||||
dataset = new Dataset(context);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
// Reset singleton state
|
||||
setMemCachedDataset(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeLexicalModels_PreservesUniqueExistingModels() throws Exception {
|
||||
// Setup
|
||||
dataset.lexicalModels.add(new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.0", "", ""));
|
||||
List<LexicalModel> newModels = new ArrayList<>();
|
||||
newModels.add(new LexicalModel("pkg2", "model2", "Model 2", "fr", "French", "1.0", "", ""));
|
||||
|
||||
// Execute
|
||||
Method mergeMethod = CloudRepository.class.getDeclaredMethod("mergeLexicalModels", Dataset.LexicalModels.class, List.class);
|
||||
mergeMethod.setAccessible(true);
|
||||
mergeMethod.invoke(repository, dataset.lexicalModels, newModels);
|
||||
|
||||
// Verify
|
||||
Assert.assertEquals(2, dataset.lexicalModels.getCount());
|
||||
Assert.assertTrue(containsModel(dataset.lexicalModels, "model1"));
|
||||
Assert.assertTrue(containsModel(dataset.lexicalModels, "model2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeLexicalModels_UpdatesVersion() throws Exception {
|
||||
// Setup
|
||||
dataset.lexicalModels.add(new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.0", "", ""));
|
||||
|
||||
List<LexicalModel> newModels = new ArrayList<>();
|
||||
// New model (same ID, newer version v1.1)
|
||||
newModels.add(new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.1", "", ""));
|
||||
|
||||
// Execute
|
||||
Method mergeMethod = CloudRepository.class.getDeclaredMethod("mergeLexicalModels", Dataset.LexicalModels.class, List.class);
|
||||
mergeMethod.setAccessible(true);
|
||||
mergeMethod.invoke(repository, dataset.lexicalModels, newModels);
|
||||
|
||||
// Verify
|
||||
Assert.assertEquals(1, dataset.lexicalModels.getCount());
|
||||
LexicalModel result = dataset.lexicalModels.getItem(0);
|
||||
Assert.assertEquals("1.1", result.getVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMergeLexicalModels_KeepsOlderIfNewerIsLowerVersion() throws Exception {
|
||||
// Setup
|
||||
dataset.lexicalModels.add(new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.2", "", ""));
|
||||
|
||||
List<LexicalModel> newModels = new ArrayList<>();
|
||||
// New model (same ID, older version v1.1)
|
||||
newModels.add(new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.1", "", ""));
|
||||
|
||||
// Execute
|
||||
Method mergeMethod = CloudRepository.class.getDeclaredMethod("mergeLexicalModels", Dataset.LexicalModels.class, List.class);
|
||||
mergeMethod.setAccessible(true);
|
||||
mergeMethod.invoke(repository, dataset.lexicalModels, newModels);
|
||||
|
||||
// Verify
|
||||
Assert.assertEquals(1, dataset.lexicalModels.getCount());
|
||||
LexicalModel result = dataset.lexicalModels.getItem(0);
|
||||
Assert.assertEquals("1.2", result.getVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAssociatedLexicalModel_ReturnsCorrectModel() throws Exception {
|
||||
// Setup
|
||||
LexicalModel model1 = new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.0", "", "");
|
||||
LexicalModel model2 = new LexicalModel("pkg2", "model2", "Model 2", "fr", "French", "1.0", "", "");
|
||||
dataset.lexicalModels.add(model1);
|
||||
dataset.lexicalModels.add(model2);
|
||||
setMemCachedDataset(dataset);
|
||||
|
||||
// Execute & Verify
|
||||
Assert.assertEquals(model1, repository.getAssociatedLexicalModel(context, "en"));
|
||||
Assert.assertEquals(model2, repository.getAssociatedLexicalModel(context, "fr"));
|
||||
Assert.assertEquals(model1, repository.getAssociatedLexicalModel(context, "EN")); // Case insensitivity
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAssociatedLexicalModel_ReturnsNullWhenDatasetIsNull() throws Exception {
|
||||
// Setup
|
||||
setMemCachedDataset(null);
|
||||
|
||||
// Execute & Verify
|
||||
Assert.assertNull(repository.getAssociatedLexicalModel(context, "en"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAssociatedLexicalModel_ReturnsFirstMatch() throws Exception {
|
||||
// Setup
|
||||
LexicalModel model1 = new LexicalModel("pkg1", "model1", "Model 1", "en", "English", "1.0", "", "");
|
||||
LexicalModel model2 = new LexicalModel("pkg2", "model2", "Model 2", "en", "English", "1.0", "", "");
|
||||
dataset.lexicalModels.add(model1);
|
||||
dataset.lexicalModels.add(model2);
|
||||
setMemCachedDataset(dataset);
|
||||
|
||||
// Execute & Verify
|
||||
Assert.assertEquals(model1, repository.getAssociatedLexicalModel(context, "en"));
|
||||
}
|
||||
}
|
||||
|
|
@ -79,8 +79,10 @@ analytics for Debug are associated with an App Bundle ID
|
|||
Replace `SERIAL` with the device serial number listed in step 2.
|
||||
|
||||
### Compiling the app's offline help
|
||||
|
||||
Keyman for Android help is maintained in the Markdown files in android/docs/help.
|
||||
The script `/resources/build/build-help.inc.sh` uses the `pandoc` tool to convert the Markdown files into html.
|
||||
The script `/resources/build/build-help.inc.sh` uses the `pandoc` tool to convert
|
||||
the Markdown files into html.
|
||||
|
||||
```bash
|
||||
# Convert markdown to html for offline help
|
||||
|
|
@ -95,7 +97,9 @@ There are two included sample projects that can be modified to test a keyboard.
|
|||
|
||||
**android/Samples/KMSample1** app runs a bare Keyman app for testing a keyboard.
|
||||
|
||||
**android/Samples/KMSample2** app provides prompts for setting KMSample2 as a system level keyboard.
|
||||
**android/Samples/KMSample2** app provides prompts for setting KMSample2 as a
|
||||
system level keyboard.
|
||||
|
||||
Both sample apps include a default Tamil keyboard and sample dictionary.
|
||||
|
||||
Building the debug versions of these projects follow the same steps as KMAPro:
|
||||
|
|
@ -106,12 +110,16 @@ Building the debug versions of these projects follow the same steps as KMAPro:
|
|||
|
||||
### Tests: KeyboardHarness
|
||||
|
||||
**android/Tests/KeyboardHarness** app is a test harness for developers to troubleshoot keyboards.
|
||||
**android/Tests/KeyboardHarness** app is a test harness for developers to
|
||||
troubleshoot keyboards.
|
||||
|
||||
1. Copy the keyboard js file and applicable ttf fonts to *android/Tests/KeyboardHarness/app/src/main/assets/*.
|
||||
2. In Keyman Developer
|
||||
* Open the `keyboardharness.kpj` project and add your keyboard files to the keyboard package.
|
||||
* Build the keyboardharness.kmp keyboard package
|
||||
|
||||
* Open the `keyboardharness.kpj` project and add your keyboard files to the
|
||||
keyboard package.
|
||||
* Build the keyboardharness.kmp keyboard package
|
||||
|
||||
3. Add the keyboard in *android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java*
|
||||
4. cd to android/Tests/KeyboardHarness/
|
||||
5. `./build.sh configure build`
|
||||
|
|
@ -120,62 +128,74 @@ Building the debug versions of these projects follow the same steps as KMAPro:
|
|||
--------------------------------------------------------------
|
||||
|
||||
## How to Build Keyman Engine for Android
|
||||
1. Open a terminal or Git Bash prompt and go to the Android project folder (e.g. `cd ~/keyman/android/`)
|
||||
|
||||
1. Open a terminal or Git Bash prompt and go to the Android project folder
|
||||
(e.g. `cd ~/keyman/android/`)
|
||||
2. Run `./build.sh build:engine`
|
||||
|
||||
Keyman Engine for Android library (**keyman-engine.aar**) is now ready to be imported in any project.
|
||||
Keyman Engine for Android library (**keyman-engine.aar**) is now ready to be
|
||||
imported in any project.
|
||||
|
||||
## How to Use Keyman Engine for Android Library
|
||||
|
||||
1. Add **keyman-engine.aar** into **[Your project folder]/app/libs/** folder.
|
||||
a. We recommend [downloading](https://keyman.com/downloads/#android-engine) the the latest stable release of Keyman Engine and extracting the .aar file.
|
||||
b. If you choose to use your own build of the Keyman Engine, get the library from **android/Samples/KMSample1/app/libs/keyman-engine.aar**
|
||||
|
||||
a. We recommend [downloading](https://keyman.com/downloads/#android-engine)
|
||||
the the latest stable release of Keyman Engine and extracting the .aar file.
|
||||
|
||||
b. If you choose to use your own build of the Keyman Engine, get the library
|
||||
from **android/Samples/KMSample1/app/libs/keyman-engine.aar**
|
||||
2. Open your project in Android Studio.
|
||||
3. Open **build.gradle** (Module: app) in "Gradle Scripts".
|
||||
4. Check that the `android{}` object, includes the following:
|
||||
```gradle
|
||||
android {
|
||||
compileSdkVersion 35
|
||||
|
||||
// Don't compress kmp files so they can be copied via AssetManager
|
||||
aaptOptions {
|
||||
noCompress "kmp"
|
||||
}
|
||||
```gradle
|
||||
android {
|
||||
compileSdkVersion 36
|
||||
|
||||
// Don't compress kmp files so they can be copied via AssetManager
|
||||
aaptOptions {
|
||||
noCompress "kmp"
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
```
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
```
|
||||
5. After the `android {}` object, include the following:
|
||||
````gradle
|
||||
repositories {
|
||||
flatDir {
|
||||
dirs 'libs'
|
||||
|
||||
````gradle
|
||||
repositories {
|
||||
flatDir {
|
||||
dirs 'libs'
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
api (name:'keyman-engine', ext:'aar')
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
api (name:'keyman-engine', ext:'aar')
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
|
||||
// Include this if you want to have QR Codes displayed on Keyboard Info
|
||||
implementation ('com.github.kenglxn.QRGen:android:3.0.1') {
|
||||
transitive = true
|
||||
// Include this if you want to have QR Codes displayed on Keyboard Info
|
||||
implementation ('com.github.kenglxn.QRGen:android:3.0.1') {
|
||||
transitive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
````
|
||||
5. include `import com.keyman.engine.*;` to use Keyman Engine in a class.
|
||||
6. include `import com.keyman.engine.*;` to use Keyman Engine in a class.
|
||||
|
||||
### Keyman Engine for Android help content
|
||||
|
||||
Keyman Engine for Android help is maintained in the Markdown files in android/docs/engine/.
|
||||
|
||||
## Design Documentation
|
||||
|
||||
Internal design documents about features pertaining to Keyman for Android and Keyman Engine for Android are maintained in the Markdown files in android/docs/internal/.
|
||||
Internal design documents about features pertaining to Keyman for Android and
|
||||
Keyman Engine for Android are maintained in the Markdown files in android/docs/internal/.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ java {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdk 35
|
||||
compileSdk 36
|
||||
namespace="com.keyman.kmsample1"
|
||||
|
||||
// Don't compress kmp files so they can be copied via AssetManager
|
||||
|
|
@ -27,7 +27,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "com.keyman.kmsample1"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
targetSdkVersion 36
|
||||
|
||||
// KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle
|
||||
versionCode KEYMAN_VERSION_CODE as Integer
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ java {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdk 35
|
||||
compileSdk 36
|
||||
namespace="com.keyman.kmsample2"
|
||||
|
||||
// Don't compress kmp files so they can be copied via AssetManager
|
||||
|
|
@ -27,7 +27,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "com.keyman.kmsample2"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
targetSdkVersion 36
|
||||
|
||||
// KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle
|
||||
versionCode KEYMAN_VERSION_CODE as Integer
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ java {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdk 35
|
||||
compileSdk 36
|
||||
namespace="com.keyman.android.tests.keyboardHarness"
|
||||
|
||||
// Don't compress kmp files so they can be copied via AssetManager
|
||||
|
|
@ -27,7 +27,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "com.keyman.android.tests.keyboardHarness"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
targetSdkVersion 36
|
||||
|
||||
// KEYMAN_VERSION_CODE, KEYMAN_VERSION_NAME, KEYMAN_VERSION_FOR_FILENAME from version.gradle
|
||||
versionCode KEYMAN_VERSION_CODE as Integer
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ ext.rootPath = '../../../'
|
|||
apply from: "$rootPath/version.gradle"
|
||||
|
||||
android {
|
||||
compileSdk 35
|
||||
compileSdk 36
|
||||
namespace="com.keyman.android.tests.keycode"
|
||||
|
||||
buildFeatures {
|
||||
|
|
@ -17,7 +17,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "com.keyman.android.tests.keycode"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 35
|
||||
targetSdkVersion 36
|
||||
|
||||
// KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle
|
||||
versionCode KEYMAN_VERSION_CODE as Integer
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ title: KMManager.getAssociatedLexicalModel()
|
|||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The **getAssociatedLexicalModel()** method searches the installed lexical models list and see if there's an associated model for a given language
|
||||
|
||||
## Syntax
|
||||
|
||||
```java
|
||||
HashMap<String, String> getAssociatedLexicalModel(String langId)
|
||||
HashMap<String, String> getAssociatedLexicalModel(String langId)
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
|
@ -16,17 +18,23 @@ HashMap<String, String> getAssociatedLexicalModel(String langId)
|
|||
: The language ID
|
||||
|
||||
### Returns
|
||||
A dictionary of the associated lexical model information with keys and values defined as
|
||||
|
||||
A dictionary of the associated lexical model information with keys and values defined as
|
||||
`HashMap<String key, String value)`.
|
||||
|
||||
`null` if there's no associated lexical model for a language `langId`.
|
||||
|
||||
If there are more than one lexical model installed for the given language,
|
||||
the first one found will be returned.
|
||||
|
||||
## Description
|
||||
|
||||
Use this method to search the installed lexical models list and see if there's an associated model for a given language ID.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example: Using `getAssociatedLexicalModel()`
|
||||
|
||||
The following script illustrates the use of `getAssociatedLexicalModel()`:
|
||||
|
||||
```java
|
||||
|
|
@ -35,5 +43,6 @@ The following script illustrates the use of `getAssociatedLexicalModel()`:
|
|||
```
|
||||
|
||||
## See also
|
||||
|
||||
* [addLexicalModel()](addLexicalModel)
|
||||
* [registerLexicalModel()](registerLexicalModel)
|
||||
|
|
|
|||
|
|
@ -125,8 +125,16 @@ namespace km {
|
|||
{
|
||||
assert(state);
|
||||
assert(action_item);
|
||||
if ((!state) || (!action_item))
|
||||
if ((!state) || (!action_item)) {
|
||||
return false;
|
||||
}
|
||||
// For this mock processor we only support queuing PERSIST_OPT action items.
|
||||
if (action_item->type == KM_CORE_IT_PERSIST_OPT && action_item->option) {
|
||||
state->actions().push_persist(update_option(static_cast<km_core_option_scope>(action_item->option->scope),
|
||||
action_item->option->key,
|
||||
action_item->option->value));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -166,6 +174,18 @@ namespace km {
|
|||
u"F2 pressed test save."));
|
||||
break;
|
||||
}
|
||||
case KM_CORE_VKEY_F3:
|
||||
{
|
||||
state->actions().push_persist(
|
||||
update_option(KM_CORE_OPT_KEYBOARD,
|
||||
u"__test_point_3",
|
||||
u"F3 pressed test save 1."));
|
||||
state->actions().push_persist(
|
||||
update_option(KM_CORE_OPT_KEYBOARD,
|
||||
u"__test_point_4",
|
||||
u"F3 pressed test save 2."));
|
||||
break;
|
||||
}
|
||||
|
||||
case KM_CORE_VKEY_F4:
|
||||
state->context().push_marker(KM_CORE_VKEY_QUOTE);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,23 @@ void actions::push_capslock(bool turnOn) {
|
|||
emplace_back(std::move(ai));
|
||||
}
|
||||
|
||||
actions::actions(actions const &other)
|
||||
: std::vector<action>(other)
|
||||
, _option_items_stack(other._option_items_stack)
|
||||
{
|
||||
// Update all option pointers to point to the new stack.
|
||||
size_t opt_index = 0;
|
||||
for (auto &item : *this) {
|
||||
if (item.type == KM_CORE_IT_PERSIST_OPT) {
|
||||
if (opt_index < _option_items_stack.size()) {
|
||||
item.option = &_option_items_stack[opt_index++];
|
||||
} else {
|
||||
// no matching item in the stack; clear pointer.
|
||||
item.option = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state::state(km::core::abstract_processor & ap, km_core_option_item const *env)
|
||||
: _processor(ap)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
|
||||
#include "keyman_core.h"
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ using action = km_core_action_item;
|
|||
|
||||
class actions : public std::vector<action>
|
||||
{
|
||||
std::vector<option> _option_items_stack;
|
||||
std::deque<option> _option_items_stack;
|
||||
|
||||
template<km_core_action_type V>
|
||||
void _push_vkey(km_core_virtual_key);
|
||||
|
|
@ -35,6 +36,10 @@ class actions : public std::vector<action>
|
|||
public:
|
||||
template<typename... Args>
|
||||
actions(Args&&... args);
|
||||
actions(actions const &other);
|
||||
// If the operator is needed in the future, it shall be implemented.
|
||||
// Currently blocking accidental use of it for now.
|
||||
actions &operator=(actions const &) = delete;
|
||||
|
||||
void push_character(km_core_usv usv);
|
||||
void push_marker(uint32_t marker);
|
||||
|
|
|
|||
|
|
@ -251,6 +251,24 @@ constexpr km_core_option_item const expected_persist_opt = {
|
|||
KM_CORE_OPT_KEYBOARD
|
||||
};
|
||||
|
||||
constexpr km_core_option_item const clone_persist_opt = {
|
||||
u"__test_clone",
|
||||
u"Not in original",
|
||||
KM_CORE_OPT_KEYBOARD
|
||||
};
|
||||
|
||||
constexpr km_core_option_item const test_point_3_opt = {
|
||||
u"__test_point_3",
|
||||
u"F3 pressed test save 1.",
|
||||
KM_CORE_OPT_KEYBOARD
|
||||
};
|
||||
|
||||
constexpr km_core_option_item const test_point_4_opt = {
|
||||
u"__test_point_4",
|
||||
u"F3 pressed test save 2.",
|
||||
KM_CORE_OPT_KEYBOARD
|
||||
};
|
||||
|
||||
extern "C"
|
||||
{
|
||||
uint8_t test_imx_callback(km_core_state *state, uint32_t imx_id, void *callback_object){
|
||||
|
|
@ -269,7 +287,8 @@ int main(int argc, char * argv[])
|
|||
|
||||
km_core_keyboard * test_kb = nullptr;
|
||||
km_core_state * test_state = nullptr,
|
||||
* test_clone = nullptr;
|
||||
* test_clone = nullptr,
|
||||
* test_clone_2 = nullptr;
|
||||
test_kb = (km_core_keyboard *)new km::core::mock_processor(km::core::path("dummy.mock"));
|
||||
|
||||
// Simple sanity tests.
|
||||
|
|
@ -394,9 +413,41 @@ int main(int argc, char * argv[])
|
|||
clone_state_deleted_text
|
||||
));
|
||||
|
||||
// Add two actions before cloning the state again
|
||||
try_status(km_core_process_event(test_state, KM_CORE_VKEY_F3, 0, 1, KM_CORE_EVENT_FLAG_DEFAULT));
|
||||
try_status(km_core_state_clone(test_state, &test_clone_2));
|
||||
|
||||
// Now put an option in the test_clone_2 state only
|
||||
km_core_action_item action_clone = {KM_CORE_IT_PERSIST_OPT, {0,}, };
|
||||
action_clone.option = &clone_persist_opt;
|
||||
if (test_clone_2->actions().back().type == KM_CORE_IT_END) {
|
||||
test_clone_2->actions().pop_back();
|
||||
}
|
||||
km_core_state_queue_action_items(test_clone_2, &action_clone);
|
||||
test_clone_2->actions().commit();
|
||||
|
||||
// Test debug dump
|
||||
auto doc3 = get_json_doc(*test_state), doc4 = get_json_doc(*test_clone_2);
|
||||
std::cout << "doc3:" << std::endl;
|
||||
std::cout << doc3 << std::endl;
|
||||
std::cout << "doc4:" << std::endl;
|
||||
std::cout << doc4 << std::endl;
|
||||
if (doc3 == doc4) return __LINE__;
|
||||
|
||||
km_core_action_item action_tp3 = {KM_CORE_IT_PERSIST_OPT, {0,}, };
|
||||
action_tp3.option = &test_point_3_opt;
|
||||
|
||||
km_core_action_item action_tp4 = {KM_CORE_IT_PERSIST_OPT, {0,}, };
|
||||
action_tp4.option = &test_point_4_opt;
|
||||
|
||||
test_assert(action_items(test_state, {action_tp3, action_tp4, {KM_CORE_IT_END}}));
|
||||
// Check that test_clone_2 has the same persisted options plus the extra queued option.
|
||||
test_assert(action_items(test_clone_2, {action_tp3, action_tp4, action_clone, {KM_CORE_IT_END}}));
|
||||
|
||||
// Destroy them
|
||||
km_core_state_dispose(test_state);
|
||||
km_core_state_dispose(test_clone);
|
||||
km_core_state_dispose(test_clone_2);
|
||||
km_core_keyboard_dispose(test_kb);
|
||||
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -88,14 +88,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache": {
|
||||
|
|
@ -642,9 +642,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
|
||||
"version": "7.5.22",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
|
||||
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
|
|
@ -769,9 +769,9 @@
|
|||
}
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"requires": {
|
||||
"balanced-match": "^4.0.2"
|
||||
}
|
||||
|
|
@ -1166,9 +1166,9 @@
|
|||
}
|
||||
},
|
||||
"tar": {
|
||||
"version": "7.5.11",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",
|
||||
"integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==",
|
||||
"version": "7.5.22",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
|
||||
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
|
||||
"requires": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { EventEmitter } from "eventemitter3";
|
|||
|
||||
/**
|
||||
* The return object documented for
|
||||
* https://help.keyman.com/developer/engine/web/16.0/reference/core/getUIState.
|
||||
* https://help.keyman.com/developer/engine/web/current-version/reference/core/getUIState.
|
||||
*
|
||||
* As it has long been documented in this format, property names should not be adjusted!
|
||||
*/
|
||||
|
|
@ -159,4 +159,4 @@ export class FocusAssistant extends EventEmitter<EventMap> {
|
|||
this.focusing=false;
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -360,14 +360,13 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
|||
*
|
||||
* This is based on the current `.activeTextStore` and its related attachment metadata.
|
||||
*/
|
||||
protected currentKeyboardSrcTextStore(): AbstractElementTextStore<any> {
|
||||
protected currentKeyboardSrcTextStore(): AbstractElementTextStore<any> | null {
|
||||
const textStore = this.currentTextStore || this.mostRecentTextStore;
|
||||
|
||||
if(this.isTextStoreKeyboardIndependent(textStore)) {
|
||||
return textStore;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private isTextStoreKeyboardIndependent(textStore: AbstractElementTextStore<any>): boolean {
|
||||
|
|
@ -403,12 +402,13 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Allows setting a control to a specific keyboard that does not change if the active keyboard changes when other
|
||||
* controls are active. Only activates the keyboard if the specified control represents the currently-active
|
||||
* context.
|
||||
* Allows setting a control to a specific keyboard that does not change if
|
||||
* the active keyboard changes when other controls are active. Only
|
||||
* activates the keyboard if the specified control represents the
|
||||
* currently-active context.
|
||||
*
|
||||
* This is the core method that backs
|
||||
* https://help.keyman.com/developer/engine/web/15.0/reference/core/setKeyboardForControl.
|
||||
* https://help.keyman.com/developer/engine/web/current-version/reference/core/setKeyboardForControl.
|
||||
* @param textStore
|
||||
* @param kbdId
|
||||
* @param langId
|
||||
|
|
@ -561,13 +561,13 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
|||
* Description Stores the last active element's keyboard settings. Should be called
|
||||
* whenever a KMW-enabled page element loses control.
|
||||
*/
|
||||
public _BlurKeyboardSettings(lastElem: HTMLElement, PInternalName?: string, PLgCode?: string): void {
|
||||
private _BlurKeyboardSettings(lastElem: HTMLElement, internalName?: string, lgCode?: string): void {
|
||||
let keyboardID = this.activeKeyboard ? this.activeKeyboard.keyboard.id : '';
|
||||
let langCode = this.activeKeyboard?.metadata.langId;
|
||||
|
||||
if(PInternalName !== undefined && PLgCode !== undefined) {
|
||||
keyboardID = PInternalName;
|
||||
langCode = PLgCode;
|
||||
if(internalName !== undefined && lgCode !== undefined) {
|
||||
keyboardID = internalName;
|
||||
langCode = lgCode;
|
||||
}
|
||||
|
||||
if(lastElem && lastElem._kmwAttachment.keyboard != null) {
|
||||
|
|
@ -585,7 +585,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
|||
* whenever a KMW-enabled page element gains control, but only once the prior
|
||||
* element's loss of control is guaranteed.
|
||||
*/
|
||||
public _FocusKeyboardSettings(lastElem: HTMLElement, blockGlobalChange: boolean): void {
|
||||
private _FocusKeyboardSettings(lastElem: HTMLElement, blockGlobalChange: boolean): void {
|
||||
// Important pre-condition: the newly-focused element must be set as active.
|
||||
const attachment = lastElem._kmwAttachment;
|
||||
const global = this.globalKeyboard;
|
||||
|
|
@ -606,7 +606,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
|||
* The return value indicates whether (true) or not (false) the calling event handler
|
||||
* should be terminated immediately after the call.
|
||||
*/
|
||||
public _CommonFocusHelper(textStore: AbstractElementTextStore<any>): boolean {
|
||||
private _CommonFocusHelper(textStore: AbstractElementTextStore<any>): boolean {
|
||||
const {focusAssistant} = this;
|
||||
|
||||
const activeKeyboard = this.activeKeyboard?.keyboard;
|
||||
|
|
@ -631,7 +631,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
|||
/**
|
||||
* Respond to KeymanWeb-aware input element receiving focus
|
||||
*/
|
||||
public _ControlFocus = (e: FocusEvent): boolean => {
|
||||
private _ControlFocus = (e: FocusEvent): boolean => {
|
||||
// Step 1: determine the corresponding TextStore instance.
|
||||
const textStore = textStoreForEvent(e);
|
||||
if(!textStore) {
|
||||
|
|
@ -656,7 +656,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
|||
/**
|
||||
* Respond to KMW losing focus on event
|
||||
*/
|
||||
public _ControlBlur = (e: FocusEvent): boolean => {
|
||||
private _ControlBlur = (e: FocusEvent): boolean => {
|
||||
// Step 0: if we're in a state where loss-of-focus should be outright-ignored, bypass the handler entirely.
|
||||
if(this.focusAssistant._IgnoreNextSelChange) {
|
||||
|
||||
|
|
@ -728,7 +728,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
|||
return true;
|
||||
}
|
||||
|
||||
doChangeEvent(textStore: AbstractElementTextStore<any>) {
|
||||
private doChangeEvent(textStore: AbstractElementTextStore<any>) {
|
||||
if(textStore.changed) {
|
||||
const event = new Event('change', {"bubbles": true, "cancelable": false});
|
||||
textStore.getElement().dispatchEvent(event);
|
||||
|
|
@ -737,7 +737,7 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
|||
textStore.changed = false;
|
||||
}
|
||||
|
||||
public _Click: (e: MouseEvent) => boolean = (e: MouseEvent) => {
|
||||
private _Click: (e: MouseEvent) => boolean = (e: MouseEvent) => {
|
||||
this.resetContext();
|
||||
return true;
|
||||
};
|
||||
|
|
@ -839,4 +839,4 @@ export class ContextManager extends ContextManagerBase<BrowserConfiguration> {
|
|||
this.page.shutdown();
|
||||
this.domEventTracker.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ export class ContextManager extends ContextManagerBase<WebviewConfiguration> {
|
|||
* Reflects the active 'textStore' upon which any `set activeKeyboard` operation will take place.
|
||||
* For app/webview... there's only one textStore, thus only a "global default" matters.
|
||||
*/
|
||||
protected currentKeyboardSrcTextStore(): SyntheticTextStore {
|
||||
protected currentKeyboardSrcTextStore(): SyntheticTextStore | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -223,4 +223,4 @@ export class ContextManager extends ContextManagerBase<WebviewConfiguration> {
|
|||
super.resetContext();
|
||||
this._hostTextStore.saveState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ export class AttachmentInfo {
|
|||
/**
|
||||
* Tracks the language code corresponding to the `keyboard` field.
|
||||
*/
|
||||
languageCode: string;
|
||||
public languageCode: string;
|
||||
|
||||
/**
|
||||
* Tracks the inputmode originally set by the webpage.
|
||||
*/
|
||||
inputMode?: string;
|
||||
public inputMode?: string;
|
||||
|
||||
/**
|
||||
* Constructor for AttachmentInfo.
|
||||
|
|
@ -18,4 +18,4 @@ export class AttachmentInfo {
|
|||
* @param keyboard - Provides the keyboard identifier.
|
||||
*/
|
||||
constructor(public readonly textStore: AbstractElementTextStore<any>, public keyboard: string) {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,16 +50,20 @@ export class CookieSerializer<Type extends Record<keyof Type, DecodedCookieField
|
|||
*/
|
||||
private _loadRawCookies(): Record<string, string> {
|
||||
const v: Record<string, string> = {};
|
||||
if(typeof(document.cookie) != 'undefined' && document.cookie != '') {
|
||||
const c = document.cookie.split(/;\s*/);
|
||||
for(let i = 0; i < c.length; i++) {
|
||||
const d = c[i].split('=');
|
||||
if(d.length == 2) {
|
||||
v[d[0]] = d[1];
|
||||
try {
|
||||
if(typeof(document.cookie) != 'undefined' && document.cookie != '') {
|
||||
const c = document.cookie.split(/;\s*/);
|
||||
for(let i = 0; i < c.length; i++) {
|
||||
const d = c[i].split('=');
|
||||
if(d.length == 2) {
|
||||
v[d[0]] = d[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore exceptions, e.g. if third-party cookies are blocked in
|
||||
// the browser (#r.keymanweb.com/9)
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +114,11 @@ export class CookieSerializer<Type extends Record<keyof Type, DecodedCookieField
|
|||
|
||||
const d = new Date(new Date().valueOf() + 1000 * 60 * 60 * 24 * 30).toUTCString();
|
||||
const cookieConfig = ' path=/; expires=' + d; //Fri, 31 Dec 2099 23:59:59 GMT;';
|
||||
document.cookie = `${cookieName}=${encodeURIComponent(serialization)}; ${cookieConfig}`;
|
||||
try {
|
||||
document.cookie = `${cookieName}=${encodeURIComponent(serialization)}; ${cookieConfig}`;
|
||||
} catch (e) {
|
||||
// Ignore exceptions, e.g. if third-party cookies are blocked in
|
||||
// the browser (#r.keymanweb.com/9)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
|
|||
* attached elements within the app/browser textStore. For `app/webview`, this should
|
||||
* always return a consistent value - likely, `null`.
|
||||
*/
|
||||
protected abstract currentKeyboardSrcTextStore(): TextStore;
|
||||
protected abstract currentKeyboardSrcTextStore(): TextStore | null;
|
||||
|
||||
/**
|
||||
* Ensures that newly activated keyboards are set correctly within managed context, possibly
|
||||
|
|
@ -405,4 +405,4 @@ export abstract class ContextManagerBase<MainConfig extends EngineConfiguration>
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -330,10 +330,6 @@ export class KeymanEngineBase<
|
|||
this.config.deferForInitialization.then(eventRaiser);
|
||||
}
|
||||
});
|
||||
|
||||
this.keyboardRequisitioner.cache.on('keyboardadded', (keyboard) => {
|
||||
this.legacyAPIEvents.callEvent('keyboardloaded', { keyboardName: keyboard.id });
|
||||
});
|
||||
//
|
||||
// #endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export type OSKRect = {
|
|||
|
||||
/**
|
||||
* Definition for OSK events documented at
|
||||
* https://help.keyman.com/developer/engine/web/16.0/reference/events/.
|
||||
* https://help.keyman.com/developer/engine/web/current-version/reference/events/.
|
||||
*/
|
||||
export interface LegacyOSKEventMap {
|
||||
'configclick'(obj: {}): void;
|
||||
|
|
@ -1331,4 +1331,4 @@ export abstract class OSKView
|
|||
): void {
|
||||
this.legacyEvents.removeEventListener(event, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue