Merge pull request #4739 from keymanapp/chore/B14S8-beta-to-master

chore: B14S8 beta to alpha
This commit is contained in:
Marc Durdin 2021-03-22 08:47:21 +11:00 committed by GitHub
commit 6c9fe2c9fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
317 changed files with 7539 additions and 1372 deletions

View file

@ -290,6 +290,106 @@
* chore: prepare 15.0 alpha (#4129)
## 14.0.266 beta 2021-03-19
* chore(android/app): Migrate launcher icon to adaptive (#4720)
* fix(common/core/web): predictive banner activation logic (#4713)
* fix(web): allows refresh of currently-loaded model (#4728)
* fix(ios): kbd search now caches publishing data for result (#4729)
* fix(linux): Fix switching to keyboard in middle of line (#4678) (#4721)
* fix(windows): ensure profiles are reinstalled during setup (#4727)
* fix(windows): Avoid double start during setup (#4726)
## 14.0.265 beta 2021-03-18
* fix(common/core/web): disambiguation of keys sharing same base key ID (#4703)
* fix(windows): Download keyboard dialog TLS protocol support (#4714)
* fix(web): first-pass workaround for popup-key corrections (#4704)
* fix(developer): use var not let in definitions (#4718)
* chore(windows): finalize help (#4717)
## 14.0.264 beta 2021-03-17
* fix(mac): prevent duplicate keyboards in list (#4674)
* fix(android/app): Load system keyboard before checking overrides (#4696)
* fix(android): Update styling and set text to black (#4662)
## 14.0.263 beta 2021-03-16
* fix(ios): keyboard search did not alert for lack of net (#4693)
* chore(android/engine): Send KMW console errors to Sentry (#4692)
* fix(windows): start Keyman after setup only if not running (#4681)
* fix(developer): &CasedKeys and &MnemonicLayout are not compatible together (#4690)
* fix(android/engine): Fix crash when download file fails to copy to cache (#4697)
* fix(developer): validate keyboard_info should give info on non-canonical bcp47 (#4689)
* chore(developer): upgrade bcp 47 canonicalisation to warning (#4699)
* fix(developer): support named character codes at end of line (#4691)
## 14.0.262 beta 2021-03-16
* fix(android): Update styling and set text to black (#4662)
* fix(mac): prevent duplicate keyboards in list (#4674)
## 14.0.261 beta 2021-03-15
* fix(windows): Trigger language sync after changes (#4663)
* chore(linux): Ignore buildtools in keyman-config tarball (#4675)
## 14.0.260 beta 2021-03-15
* chore(windows): fully disable auto start task (#4658)
* fix(windows): keep online update in focus (#4661)
* fix(windows): setup must save install state for restarts (#4649)
* fix(ios): removal of default keyboard respected when updating app (#4651)
* feat(ios): auto-bundles the most recent version of default resources (#4656)
* fix(mac): register configuration window on load (#4667)
* chore(ios): Set Localizable.strings to UTF-8 (#4670)
* fix(android/app): Fix crash when clicking QR code (#4664)
* fix(android): fixes errors within lists used for UI (#4666)
* chore(common): Update crowdin for km (#4671)
* chore(android/app,ios/app): Update crowdin for de (#4672)
* fix(mac): release window resources on close (#4669)
* fix(mac): show local help in app (#4673)
* chore(windows): Update whatsnew.md (#4665)
## 14.0.259 beta 2021-03-12
* chore(developer): add B11 ISO code for ABNT2 keyboard (#4654)
* fix(web): better check for missing MutationObserver type (#4646)
* fix(android/engine): Reset in-app context when selection changes (#4636)
* chore(linux): Move signature files as well (#4653)
## 14.0.258 beta 2021-03-11
* fix(windows): context help links (#4626)
* fix(windows): hotkeys correctly ignore right modifier keys (#4628)
* fix(windows): Handle disabled profiles and invalid language ids (#4635)
* fix(web): event handling for TouchAliasElement's blinking caret (#4638)
* fix(common/models): prevents "undefined" reversion display string (#4648)
* docs(common): Update npm-packages readme (#4644)
## 14.0.257 beta 2021-03-10
* fix(common/core/web): Add keyboard check for scriptObject (#4640)
* docs(common): Update readme (#4642)
## 14.0.256 beta 2021-03-10
* chore(linux): Update readme (#4633)
## 14.0.255 beta 2021-03-09
* chore(common): Update readme (#4631)
## 14.0.254 beta 2021-03-09
* fix(web): use language code correctly in toolbar (#4620)
* chore(linux): Fix some lintian warnings for Debian package (#4621)
* docs(ios): adds supported l10ns to help/about/whatsnew (#4625)
* fix(windows): represerve keys on setfocus (#4622)
* fix(windows): show version selector for Keyman in installer (#4623)
* fix(windows): ensure valid base layout on install (#4627)
## 14.0.253 beta 2021-03-05
* fix(android/app): Fix welcome.htm responsiveness (#4531)

View file

@ -1 +1 @@
15.0.20
15.0.20

View file

@ -11,9 +11,8 @@
<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@drawable/ic_launcher"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@drawable/ic_launcher_round"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme"
android:supportsRtl="true">
@ -234,21 +233,21 @@
<activity
android:name=".InfoActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"></activity>
android:label="@string/app_name"/>
<activity
android:name=".BookmarksActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"></activity>
android:label="@string/app_name"/>
<activity
android:name=".PackageActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"></activity>
android:label="@string/app_name"/>
<activity
android:name=".GetStartedActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/Theme.AppCompat.Light.Dialog"></activity>
android:theme="@style/AppTheme.Light.Dialog"/>
<activity
android:name=".KeymanSettingsActivity"
android:label="@string/keyman_settings"
@ -282,7 +281,7 @@
android:name=".KeyboardSettingsActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.Dialog" />
android:theme="@style/AppTheme.Light.Dialog" />
<activity
android:name=".SelectPackageActivity"
android:label="@string/app_name"
@ -302,12 +301,12 @@
android:name=".WebBrowserActivity"
android:process=":WebBrowserActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"></activity>
android:label="@string/app_name"/>
<activity
android:name=".KMPBrowserActivity"
android:process=":KMPBrowserActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"></activity>
android:label="@string/app_name"/>
<provider

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View file

@ -136,7 +136,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
KMManager.setMayPredictOverride(inputType);
if (KMManager.getMayPredictOverride()) {
KMManager.setBannerOptions(false);
} else {
} else if (KMManager.isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)){
// Check if predictions needs to be re-enabled per Settings preference
Context appContext = getApplicationContext();
Keyboard kbInfo = KMManager.getCurrentKeyboardInfo(appContext);

View file

@ -149,7 +149,8 @@ public final class KeyboardSettingsActivity extends AppCompatActivity {
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
HashMap<String, String> hashMap = (HashMap<String, String>) parent.getItemAtPosition(position);
if (hashMap == null) {
KMLog.LogError(TAG, "map is null, position is " + position);
// Ignore null HashMap when clicking on QR code
return;
}
String itemTitle = MapCompat.getOrDefault(hashMap, titleKey, "");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View file

@ -13,16 +13,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_alignParentTop="true"
android:background="@color/keyman_orange" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_alignParentTop="true"
android:background="@color/keyman_orange" />
<LinearLayout
android:layout_width = "wrap_content"
android:layout_height="wrap_content"

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -43,7 +43,7 @@
<!-- Context: Get Started menu -->
<string name="show_get_started" comment="Show the &quot;Get Started&quot; menu on startup">បង្ហាញ \"%1$s\" ពេល​ចាប់​ផ្ដើម</string>
<!-- Context: Android Storage Permission -->
<string name="request_storage_permission" comment="Request storage permission to access keyboard packages">ដើម្បី​ដំឡើងកញ្ចប់​ក្ដារចុច សូម​អនុញ្ញាត​ឱ្យ Keyman មាន​សិទ្ធិ​អាន/កែ​នៅលើ​​ឧបករណ៍​ផ្ទុក។</string>
<string name="request_storage_permission" comment="Request storage permission to access keyboard packages"> ដើម្បី​ដំឡើងកញ្ចប់​ក្ដារចុច សូម​អនុញ្ញាត​ឱ្យ Keyman មាន​សិទ្ធិ​អាន​ឧបករណ៍​ផ្ទុក​ខាង​ក្រៅ</string>
<!-- Context: Android Storage Permission -->
<string name="storage_permission_denied" comment="Keyboard package installation may fail since Android storage permission not granted">សំណើ​សុំ​សិទ្ធិ​ចូល​ប្រើ​ឧបករណ៍​ផ្ទុក​ត្រូវ​បាន​បដិសេធ។ អាច​បរាជ័យ​ក្នុង​ការដំឡើង​កញ្ចប់​ក្ដារចុច</string>
<!-- Context: Keyman Settings menu -->
@ -55,6 +55,8 @@
<!-- Context: Keyman Settings menu -->
<string name="install_keyboard_or_dictionary" comment="Menu item to install keyboard or dictionary">ដំឡើង​ក្ដារ​ចុចឬ​បញ្ជី​ពាក្យ</string>
<!-- Context: Keyman Settings menu -->
<string name="change_display_language" comment="Menu action to change interface language">ប្ដូរ​ភាសានៃ​ការ​បង្ហាញ​</string>
<!-- Context: Keyman Settings menu -->
<string name="show_banner" comment="text suggestions banner">បង្ហាញ​បដា​ជានិច្ច</string>
<!-- Context: Keyman Settings menu -->
<string name="show_banner_on" comment="Description when toggle is on">ត្រូវ​អនុវត្ត</string>
@ -107,6 +109,8 @@
<!-- Context: KMP Package strings -->
<string name="install_predictive_text_package" comment="Title to install dictionary package">ដំឡើង​បញ្ជី​ពាក្យ</string>
<!-- Context: KMP Package strings -->
<string name="not_valid_package_file" comment="Notification when invalid package file cannot be installed">%1$s មិន​មែន​ជា​កញ្ចប់​ Keyman ដែល​ត្រឹមត្រូវ​ទេ។\n%2$s\"</string>
<!-- Context: KMP Package strings -->
<string name="no_new_touch_keyboards_to_install" comment="Notification when no touch-optimized keyboards can be installed">កញ្ចប់​នេះ​គ្មាន​ក្ដារចុច​សម្រាប់​ឧបករណ៍​ចល័ត​ទេ</string>
<!-- Context: KMP Package strings -->
<string name="no_new_predictive_text_to_install" comment="Notification when no dictionaries can be installed">គ្មាន​ការណែនាំពាក្យ​ស្វ័យប្រវត្តិថ្មីសម្រាប់​ដំឡើង​ទេ</string>

View file

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="AppTheme" parent="AppTheme.Base" />
<!--
Base application theme for API 21+. This theme completely replaces
AppTheme from res/values/styles.xml on API 21+ devices.
-->
<!-- Application theme. -->
<style name="AppTheme.Base" parent="@style/Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@android:color/white</item>
<!-- colorPrimaryDark is used for the status bar -->
<item name="colorPrimaryDark">@color/complementary_5</item>
<item name="android:textColor">@android:color/black</item>
<item name="android:textColorHint">@color/neutral_3</item>
<item name="android:textColorPrimary">@android:color/black</item>
<item name="dialogTheme">@style/AppTheme.Dialog</item>
<item name="alertDialogTheme">@style/AppTheme.Dialog.Alert</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.Toolbar" parent="Base.Widget.AppCompat.Toolbar">
<item name="android:colorPrimary">@android:color/white</item>
<item name="android:textColorPrimary">@android:color/black</item>
<item name="android:textColorSecondary">@android:color/black</item>
</style>
<style name="AppTheme.Dialog" parent="Theme.AppCompat.Light.Dialog">
<item name="android:colorAccent">@color/keyman_blue</item>
</style>
<style name="AppTheme.Dialog.Alert" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="android:colorAccent">@color/keyman_blue</item>
</style>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View file

@ -9,6 +9,8 @@
<style name="AppTheme.Base" parent="@style/Theme.AppCompat.Light.NoActionBar">
<!-- Customize the theme here -->
<item name="colorPrimary">@android:color/white</item>
<!-- colorPrimaryDark is used for the status bar -->
<item name="colorPrimaryDark">@color/complementary_5</item>
<item name="android:textColor">@android:color/black</item>
<item name="android:textColorHint">@color/neutral_3</item>
@ -30,6 +32,7 @@
</style>
<style name="AppTheme.Toolbar" parent="Base.Widget.AppCompat.Toolbar">
<item name="android:colorPrimary">@android:color/white</item>
<item name="android:background">@android:color/white</item>
<item name="android:textColorPrimary">@android:color/black</item>
<item name="android:textColorSecondary">@android:color/black</item>

View file

@ -16,31 +16,31 @@
android:launchMode="singleTask"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.Dialog" >
android:theme="@style/AppTheme.Light.Dialog" >
</activity>
<activity
android:name="com.tavultesoft.kmea.KeyboardInfoActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.Dialog" >
android:theme="@style/AppTheme.Light.Dialog" >
</activity>
<activity
android:name="com.tavultesoft.kmea.KMKeyboardDownloaderActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label=""
android:theme="@style/Theme.AppCompat.Light.Dialog">
android:theme="@style/AppTheme.Light.Dialog">
</activity>
<activity
android:name="com.tavultesoft.kmea.ModelPickerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.Dialog">
android:theme="@style/AppTheme.Light.Dialog">
</activity>
<activity
android:name="com.tavultesoft.kmea.ModelInfoActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.Dialog" >
android:theme="@style/AppTheme.Light.Dialog" >
</activity>
<!-- Put other WebViewActivities in a separate process so the Keyboard WebView doesn't lag.

View file

@ -149,13 +149,14 @@ final class KMKeyboard extends WebView {
}
}
// Not sending console errors to Sentry anymore since they should be handled by KMW sentryManager
// Send console errors to Sentry in case they're missed by KMW sentryManager
// (Ignoring spurious message "No keyboard stubs exist = ...")
// TODO: Analyze if this error warrants reverting to default keyboard
// TODO: Fix base error rather than trying to ignore it "No keyboard stubs exist"
if ((cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) && (!cm.message().startsWith("No keyboard stubs exist"))) {
// Make Toast notification of error and send log about falling back to default keyboard (ignore language ID)
sendKMWError(cm.lineNumber(), cm.sourceId(), cm.message());
sendError(packageID, keyboardID, "");
Keyboard firstKeyboard = KeyboardController.getInstance().getKeyboardInfo(0);
if (firstKeyboard != null) {
@ -623,6 +624,43 @@ final class KMKeyboard extends WebView {
return keyboardPath;
}
private void sendKMWError(int lineNumber, String sourceId, String message) {
if (Sentry.isEnabled()) {
Breadcrumb breadcrumb = new Breadcrumb();
breadcrumb.setMessage("KMKeyboard.sendKMWError");
breadcrumb.setCategory("KMWError");
breadcrumb.setLevel(SentryLevel.ERROR);
// Error info
breadcrumb.setData("cm_lineNumber", lineNumber);
breadcrumb.setData("cm_sourceID", sourceId);
breadcrumb.setData("cm_message", message);
// Keyboard info
if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP) {
breadcrumb.setData("keyboardType", "INAPP");
} else if (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
breadcrumb.setData("keyboardType", "SYSTEM");
} else {
breadcrumb.setData("keyboardType", "UNDEFINED");
}
if (this.packageID != null) {
breadcrumb.setData("packageID", this.packageID);
}
if (this.keyboardID != null) {
breadcrumb.setData("keyboardID", this.keyboardID);
}
if (this.keyboardName != null) {
breadcrumb.setData("keyboardName", this.keyboardName);
}
if (this.keyboardVersion != null) {
breadcrumb.setData("keyboardVersion", this.keyboardVersion);
}
Sentry.addBreadcrumb(breadcrumb);
Sentry.captureMessage("sendKMWError", SentryLevel.ERROR);
}
}
// Extract Unicode numbers (\\uxxxx) from a layer to character string.
// Ignores empty strings and layer names
// Returns: String

View file

@ -467,6 +467,18 @@ public final class KMManager {
}
}
public static boolean isKeyboardLoaded(KeyboardType type) {
if (type == KeyboardType.KEYBOARD_TYPE_INAPP) {
return InAppKeyboardLoaded;
} else if (type == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
return SystemKeyboardLoaded;
} else {
String msg = "Keyboard type undefined";
KMLog.LogError(TAG, msg);
return false;
}
}
@SuppressLint("InflateParams")
public static View createInputView(InputMethodService inputMethodService) {
//final Context context = appContext;
@ -2386,22 +2398,24 @@ public final class KMManager {
if (dn <= 0) {
if (start == end) {
if (!s.isEmpty() && s.charAt(0) == '\n') {
if (s.length() > 0 && s.charAt(0) == '\n') {
textView.keyDownUp(KeyEvent.KEYCODE_ENTER);
} else {
// *** TO DO: Try to find a solution to the bug on API < 17, insert overwrites on next line
InAppKeyboardShouldIgnoreTextChange = true;
InAppKeyboardShouldIgnoreSelectionChange = true;
textView.getText().insert(start, s);
if (s.length() > 0) {
InAppKeyboardShouldIgnoreTextChange = true;
InAppKeyboardShouldIgnoreSelectionChange = true;
textView.getText().insert(start, s);
}
}
} else {
if (!s.isEmpty() && s.charAt(0) == '\n') {
if (s.length() > 0 && s.charAt(0) == '\n') {
InAppKeyboardShouldIgnoreTextChange = true;
InAppKeyboardShouldIgnoreSelectionChange = true;
textView.getText().replace(start, end, "");
textView.keyDownUp(KeyEvent.KEYCODE_ENTER);
} else {
if (s.isEmpty()) {
if (s.length() == 0) {
textView.getText().delete(start, end);
} else {
InAppKeyboardShouldIgnoreTextChange = true;

View file

@ -65,8 +65,9 @@ public final class KMTextView extends AppCompatEditText {
int selStart = textView.getSelectionStart();
int selEnd = textView.getSelectionEnd();
KMManager.updateText(KeyboardType.KEYBOARD_TYPE_INAPP, textView.getText().toString());
KMManager.updateSelectionRange(KeyboardType.KEYBOARD_TYPE_INAPP, selStart, selEnd);
KMManager.resetContext(KeyboardType.KEYBOARD_TYPE_INAPP);
if (KMManager.updateSelectionRange(KeyboardType.KEYBOARD_TYPE_INAPP, selStart, selEnd)) {
KMManager.resetContext(KeyboardType.KEYBOARD_TYPE_INAPP);
}
}
private void init(final Context context) {
@ -165,7 +166,9 @@ public final class KMTextView extends AppCompatEditText {
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (activeView != null && activeView.equals(this)) {
KMManager.updateSelectionRange(KMManager.KeyboardType.KEYBOARD_TYPE_INAPP, selStart, selEnd);
if (KMManager.updateSelectionRange(KMManager.KeyboardType.KEYBOARD_TYPE_INAPP, selStart, selEnd)) {
KMManager.resetContext(KeyboardType.KEYBOARD_TYPE_INAPP);
}
}
}

View file

@ -151,13 +151,17 @@ public class CloudApiTypes {
* @return File - handle to the downloaded file in cache
*/
public File cacheAndOpenDestinationFile(Context context) {
File cachedFile = null;
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Uri data = downloadManager.getUriForDownloadedFile(downloadId);
DownloadFileUtils.Info info = DownloadFileUtils.cacheDownloadFile(context, data);
String filename = info.getFilename();
File cachedFile = info.getFile();
if (filename == null || filename.isEmpty() || cachedFile == null || !cachedFile.exists()) {
if (data != null) {
DownloadFileUtils.Info info = DownloadFileUtils.cacheDownloadFile(context, data);
String filename = info.getFilename();
cachedFile = info.getFile();
}
if (cachedFile == null) {
// failed to retrieve downloaded file
BaseActivity.makeToast(context, R.string.failed_to_retrieve_file, Toast.LENGTH_LONG);
}

View file

@ -94,10 +94,19 @@ public class NestedAdapter<Element, A extends ArrayAdapter<Element> & ListBacked
}
public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter<Element, A, FilterArg> filter, FilterArg filterArg) {
this(context, resource, adapter, filter, filterArg, filter.selectFrom(adapter, filterArg));
// Ensure that the list we pass through is writeable. Errors may result otherwise.
this(context, resource, adapter, filter, filterArg, new ArrayList<>(filter.selectFrom(adapter, filterArg)));
}
public NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter<Element, A, FilterArg> filter, FilterArg filterArg, List<Element> filteredList) {
// In order for the instance to be properly constructed, the `filteredList`
// parameter must be mutable. Unfortunately, there's no way to robustly
// check for that, and it illegal to construct a modifiable version
// before the super's constructor call b/c Java limitations.
// So... we'll keep it locked behind 'protected' for now.
//
// Could probably throw an extra parameter on for an 'internal' version
// and use the current signature as a 'helper' like with the constructor above?
protected NestedAdapter(@NonNull Context context, int resource, @NonNull A adapter, AdapterFilter<Element, A, FilterArg> filter, FilterArg filterArg, List<Element> filteredList) {
super(context, resource, filteredList);
this.wrappedAdapter = adapter;

View file

@ -3,6 +3,7 @@ package com.tavultesoft.kmea.util;
import android.os.Build;
import java.util.HashMap;
import java.lang.NullPointerException;
/**
* HashMap.getOrDefault is only available for Android API 24+, so this method is an alternative
@ -12,7 +13,15 @@ import java.util.HashMap;
public final class MapCompat {
private static final String TAG = "MapCompat";
public static <K, V> V getOrDefault(HashMap<K, V> map, K key, V defaultValue) {
/**
*
* @param map HashMap<String, String>
* @param key String
* @param defaultValue String
* @return String
* @throws NullPointerException
*/
public static <K, V> V getOrDefault(HashMap<K, V> map, K key, V defaultValue) throws NullPointerException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return map.getOrDefault(key, defaultValue);
}

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_activated="false" android:state_pressed="true" android:drawable="@color/activated_bg" />
<item android:state_activated="false" android:state_selected="true" android:drawable="@color/selected_bg" />
<item android:state_activated="true" android:drawable="@color/activated_bg" />
<item android:drawable="@android:color/white" />
<item android:state_activated="false" android:state_pressed="true" android:drawable="@color/activated_bg" />
<item android:state_activated="false" android:state_selected="true" android:drawable="@color/selected_bg" />
<item android:state_activated="true" android:drawable="@color/activated_bg" />
<item android:drawable="@android:color/transparent" />
</selector>

View file

@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/list_toolbar" />

View file

@ -5,6 +5,7 @@
android:layout_height="match_parent" >
<ListView
style="@style/AppTheme.Light.ListView"
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"

View file

@ -5,6 +5,7 @@
android:background="@drawable/list_item" >
<LinearLayout
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -15,6 +16,7 @@
<TextView
android:id="@+id/text1"
android:textColor="@android:color/black"
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"

View file

@ -5,6 +5,7 @@
android:background="@drawable/list_item" >
<LinearLayout
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -14,6 +15,7 @@
<TextView
android:id="@+id/text1"
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
@ -22,6 +24,7 @@
<TextView
android:id="@+id/text2"
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants"
@ -7,6 +8,7 @@
<LinearLayout
android:id="@+id/linearLayout1"
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -16,6 +18,7 @@
<TextView
android:id="@+id/text1"
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
@ -24,6 +27,7 @@
<TextView
android:id="@+id/text2"
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
@ -39,7 +43,7 @@
android:layout_centerVertical="true"
android:layout_marginEnd="8dp"
android:contentDescription="@string/image_button"
android:tint="@android:color/darker_gray"
android:src="@drawable/ic_light_action_info" />
android:src="@drawable/ic_light_action_info"
app:tint="@android:color/black" />
</RelativeLayout>

View file

@ -5,6 +5,7 @@
android:background="@drawable/list_item" >
<LinearLayout
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -15,6 +16,7 @@
<TextView
android:id="@+id/text1"
android:textColor="@drawable/textview_item"
android:background="@android:color/transparent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"

View file

@ -28,9 +28,9 @@
<!-- optional accent -->
<View
android:id="@+id/bar_accent"
style="@style/AppTheme.Divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_alignParentBottom="true"
android:background="?attr/colorAccent"/>
android:layout_alignParentBottom="true"/>
</com.google.android.material.appbar.AppBarLayout>

View file

@ -7,7 +7,8 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<View style="@style/Divider.horizontal" />
<View style="@style/AppTheme.Divider.horizontal"
android:background="@android:color/holo_red_dark"/>
<ImageView
android:id="@+id/qrCode"

View file

@ -36,7 +36,7 @@
<!-- Context: Button label -->
<string name="label_update" comment="Dialog button to update a resource">Aktualisieren</string>
<!-- Context: Keyboard Updates -->
<string name="cannot_connect" comment="Error message when network connection fails">\nKeine Verbindung zum Keyman Server!\n</string>
<string name="cannot_connect" comment="Error message when network connection fails">Kann keine Verbindung zum Keyman-Server herstellen!</string>
<!-- Context: Keyboard Updates -->
<string name="confirm_delete_keyboard" comment="Confirmation to delete a keyboard">Möchten Sie diese Tastatur löschen?</string>
<!-- Context: Keyboard Updates -->
@ -65,6 +65,10 @@
<string name="keyboard_qr_code" comment="QR Code description"> Scannen Sie diesen Code, um diese\nTastatur auf einem anderen Gerät zu laden</string>
<!-- Context: Keyboard Help welcome.htm title -->
<string name="welcome_package" comment="Title to welcome.htm page (name and version)">Willkommen bei %1$s</string>
<!-- Context: Keyboard app doesn't include FileProvider library needed to view help file -->
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> FileProvider-Bibliothek zum Anzeigen der Hilfe-Datei: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> Fataler Tastaturfehler mit %1$s:%2$s für %3$s Sprache. Standard-Tastatur wird geladen.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Suche nach zugehörigem Wörterbuch zum Herunterladen</string>
<!-- Context: Model Updates -->
@ -92,11 +96,17 @@
<!-- Context: Background download messages-->
<string name="dictionary_download_finished" comment="Notification that a dictionary download has finished">Download des Wörterbuchs ist abgeschlossen.</string>
<!-- Context: Background download messages -->
<string name="download_failed" comment="Notification that a download failed">Download fehlgeschlagen</string>
<!-- Context: Background download messages -->
<string name="failed_to_retrieve_file" comment="Failed to retrieve downloaded file">Laden der heruntergeladenen Datei fehlgeschlagen</string>
<!-- Context: General Updates -->
<string name="update_check_unavailable" comment="Error message when a Keyman server can't be reached">Fehler beim Zugriff auf den Server!</string>
<!-- Context: General Updates -->
<string name="update_check_current" comment="Notification that all resources are up to date">"Alle Ressourcen sind auf dem neuesten Stand!"</string>
<!-- Context: General Updates -->
<string name="update_failed" comment="Notification that a resource update failed">Eine oder mehrere Ressourcen konnten nicht aktualisiert werden!</string>
<!-- Context: General Updates -->
<string name="update_success" comment="Notification that a resource update successfully updated">Ressourcen erfolgreich aktualisiert!</string>
<!-- Context: Model Info -->
<string name="model_version" comment="Title for a dictionary version">Wörterbuchversion</string>
<!-- Context: Model Info -->

View file

@ -35,7 +35,7 @@
<!-- Context: Button label -->
<string name="label_update" comment="Dialog button to update a resource">អាប់ដេត</string>
<!-- Context: Keyboard Updates -->
<string name="cannot_connect" comment="Error message when network connection fails">\nមិនអាច​ភ្ជាប់​ទៅម៉ាស៊ីនមេ Keyman បានទេ!\n</string>
<string name="cannot_connect" comment="Error message when network connection fails">មិនអាច​ភ្ជាប់​ទៅម៉ាស៊ីនមេរបស់ Keyman បានទេ!</string>
<!-- Context: Keyboard Updates -->
<string name="confirm_delete_keyboard" comment="Confirmation to delete a keyboard">តើ​អ្នក​ចង់​លុប​ក្ដារចុច​នេះ​ឬ?</string>
<!-- Context: Keyboard Updates -->
@ -60,10 +60,14 @@
<string name="uninstall_keyboard" comment="Label for uninstalling keyboard">លុប​ក្ដារចុច</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_picker_new_keyboard" comment="Mark a language name that's newly installed in keyboard list">[ថ្មី] %1$s</string>
<!-- Context: Keyboard Help welcome.htm title -->
<string name="welcome_package" comment="Title to welcome.htm page (name and version)">ស្វាគមន៍​មកកាន់ %1$s</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_qr_code" comment="QR Code description"> ស្កេនកូដ​នេះដើម្បី​ផ្ទុក​ក្ដារចុចនេះ\nនៅលើឧបករណ៍ផ្សេង</string>
<!-- Context: Keyboard Help welcome.htm title -->
<string name="welcome_package" comment="Title to welcome.htm page (name and version)">ស្វាគមន៍​មកកាន់ %1$s</string>
<!-- Context: Keyboard app doesn't include FileProvider library needed to view help file -->
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file"> ត្រូវការបណ្ណាល័យ FileProvider ដើម្បី​មើលឯកសារជំនួយ៖ %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard"> មាន​បញ្ហាធ្ងន់ធ្ងរចំពោះក្ដារចុច %1$s:%2$s សម្រាប់​ភាសា %3$s។ កំពុង​ផ្ទុក​ក្ដារចុច​លំនាំដើម។</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">កំពុង​ពិនិត្យ​រក​បញ្ជី​ពាក្យ​ពាក់ព័ន្ធ​ដើម្បី​ទាញយក</string>
<!-- Context: Model Updates -->
@ -90,10 +94,18 @@
<string name="dictionary_download_is_running_in_background" comment="Notification that a dictionary download is still running">បញ្ជី​ពាក្យ​ដែល​បាន​ជ្រើសរើស​នេះ​កំពុង​ទាញយកហើយ សូម​ព្យាយាម​ម្ដងទៀត​ក្នុង​ពេល​ឆាប់ៗ នេះ!</string>
<!-- Context: Background download messages-->
<string name="dictionary_download_finished" comment="Notification that a dictionary download has finished">ការទាញយកបញ្ជី​ពាក្យបាន​ចប់​សព្វគ្រប់​ហើយ!</string>
<!-- Context: Background download messages -->
<string name="download_failed" comment="Notification that a download failed">ការទាញយកបានបរាជ័យ</string>
<!-- Context: Background download messages -->
<string name="failed_to_retrieve_file" comment="Failed to retrieve downloaded file">រក​មិន​ឃើញ​ឯកសារដែល​បាន​ទាញ​យក</string>
<!-- Context: General Updates -->
<string name="update_check_unavailable" comment="Error message when a Keyman server can't be reached">មិន​អាច​ចូលប្រើ​ម៉ាស៊ីនមេ​បាន​ទេ!</string>
<!-- Context: General Updates -->
<string name="update_check_current" comment="Notification that all resources are up to date">"ធនធានទាំង​អស់​គឺ​ថ្មី​បំផុត​ហើយ!"</string>
<!-- Context: General Updates -->
<string name="update_failed" comment="Notification that a resource update failed">ធនធាន​មួយ​ឬ​ច្រើន​បាន​បរាជ័យ​ក្នុង​ការអាប់ដេត!</string>
<!-- Context: General Updates -->
<string name="update_success" comment="Notification that a resource update successfully updated">បាន​អាប់ដេត​ធនធាននានា​ដោយ​ជោគជ័យ!</string>
<!-- Context: Model Info -->
<string name="model_version" comment="Title for a dictionary version">ជំនាន់​របស់បញ្ជី​ពាក្យ</string>
<!-- Context: Model Info -->
@ -121,6 +133,8 @@
<plurals name="keyboard_count">
<item quantity="other">(ក្ដារចុច %1$d)</item>
</plurals>
<!-- Context: "Change Display Language" menu -->
<string name="default_locale" comment="Use the device's current locale">ភាសាលំនាំដើម</string>
<!-- Context: Content descriptions -->
<!-- Context: Content descriptions -->
<!-- Context: Popup menu labels -->

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<!--
Base application theme for API 21+. This theme completely replaces
AppTheme from res/values/styles.xml on API 21+ devices.
-->
<style name="AppTheme.Base" parent="Theme.AppCompat.Light.NoActionBar">
</style>
</resources>

View file

@ -11,23 +11,39 @@
res/values-vXX/styles.xml, while customizations related to
backward-compatibility can go here.
-->
<item name="android:listViewStyle">@style/AppTheme.Light.ListView</item>
</style>
<style name="AppTheme.Dialog.Alert" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="android:textColor">@android:color/black</item>
<item name="android:textColorHint">@android:color/darker_gray</item>
<item name="android:textColorPrimary">@android:color/black</item>
<style name="AppTheme.Light.Dialog" parent="Theme.AppCompat.Light.Dialog">
<item name="android:background">@android:color/white</item>
<item name="android:textColor">@android:color/black</item>
<item name="android:textColorSecondary">@android:color/black</item>
<item name="android:textColorHint">@android:color/darker_gray</item>
<item name="android:textColorPrimary">@android:color/black</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="AppTheme.Dialog.Alert" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="android:textColor">@android:color/black</item>
<item name="android:textColorSecondary">@android:color/black</item>
<item name="android:textColorHint">@android:color/darker_gray</item>
<item name="android:textColorPrimary">@android:color/black</item>
</style>
<style name="AppTheme.Dialog.Progress" parent="AppTheme.Dialog.Alert">
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="Divider">
<item name="android:background">@android:color/holo_red_dark</item>
<style name="AppTheme.Light.ListView" parent="android:style/Widget.ListView">
<item name="android:listSelector">@drawable/list_item</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Divider.horizontal" parent="Divider">
<style name="AppTheme.Divider">
<item name="android:background">?attr/colorAccent</item>
</style>
<style name="AppTheme.Divider.horizontal" parent="AppTheme.Divider">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">1dp</item>
</style>

View file

@ -0,0 +1,63 @@
package com.tavultesoft.kmea.util;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.HashMap;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.N;
@RunWith(RobolectricTestRunner.class)
public class MapCompatTest {
private final HashMap<String, String> pets = new HashMap<String, String>();
// Each test sets up a HashMap
@Before
public void generatePets() {
pets.put("dog", "Spot");
pets.put("cat", "Mittens");
}
@Rule
public ExpectedException exceptionRule = ExpectedException.none();
@Test
public void test_getOrDefault_nullMapThrows() throws NullPointerException {
exceptionRule.expect(NullPointerException.class);
Assert.assertEquals("Peanut", MapCompat.getOrDefault(null, "elephant", "Peanut"));
}
@Config(sdk=LOLLIPOP)
@Test
public void test_getOrDefault_LollipopKeyExists() {
Assert.assertEquals("Spot", MapCompat.getOrDefault(pets, "dog", "none"));
}
@Config(sdk=N)
@Test
public void test_getOrDefault_NougatKeyExists() {
Assert.assertEquals("Spot", MapCompat.getOrDefault(pets, "dog", "none"));
}
@Config(sdk=LOLLIPOP)
@Test
public void test_getOrDefault_LollipopReturnDefault() {
Assert.assertEquals("Peanut", MapCompat.getOrDefault(pets, "elephant", "Peanut"));
}
@Config(sdk=N)
@Test
public void test_getOrDefault_NougatReturnDefault() {
Assert.assertEquals("Peanut", MapCompat.getOrDefault(pets, "elephant", "Peanut"));
}
}

4
android/build-help.sh Normal file → Executable file
View file

@ -71,8 +71,8 @@ displayInfo "" \
cd $KEYMAN_ROOT/android/help
MDLUA="$KEYMAN_ROOT/resources/build/htmlink.lua"
MD=`find -name "*.md"`
MDLUA="$KEYMAN_ROOT/resources/build/html-link.lua"
MD=`find . -name "*.md"`
DESTHTM="$THIS_DIR/KMAPro/kMAPro/src/main/assets/info"
if $DO_HTM; then

View file

@ -364,8 +364,6 @@ int run_test(const km::kbp::path & source, const km::kbp::path & compiled) {
}
std::string string_to_hex(const std::u16string& input) {
static const char hex_digits[] = "0123456789ABCDEF";
std::ostringstream result;
result << std::setfill('0') << std::hex << std::uppercase;

View file

@ -141,7 +141,9 @@ namespace com.keyman.text.prediction {
// Prevents an ugly "flash of unstyled content" layout issue during keyboard load
// on our mobile platforms when embedded.
lp.currentModel = model;
lp.emit('statechange', 'active');
if(this.mayPredict) {
lp.emit('statechange', 'active');
}
return this.lmEngine.loadModel(source, specType).then(function(config: Configuration) {
lp.configuration = config;
@ -173,7 +175,11 @@ namespace com.keyman.text.prediction {
return;
}
if(outputTarget) {
// Don't attempt predictions when disabled!
// invalidateContext otherwise bypasses .predict()'s check against this.
if(!this.isActive) {
return;
} else if(outputTarget) {
let transcription = outputTarget.buildTranscriptionFrom(outputTarget, null);
this.predict_internal(transcription, true);
} else {
@ -407,7 +413,13 @@ namespace com.keyman.text.prediction {
this._mayPredict = flag;
if(oldVal != flag) {
this.emit('statechange', flag ? 'active' : 'inactive');
// If there's no model to be activated and we've reached this point,
// the banner should remain inactive, as it already was.
// If it there was one and we've reached this point, we're globally
// deactivating, so we're fine.
if(this.activeModel) {
this.emit('statechange', flag ? 'active' : 'inactive');
}
}
}

View file

@ -16,7 +16,10 @@ namespace com.keyman.keyboards {
pad: ActiveKey.DEFAULT_PAD.toString()
};
/** WARNING - DO NOT USE DIRECTLY outside of @keymanapp/keyboard-processor! */
id?: string;
// These are fine.
width?: string;
pad?: string;
layer: string;
@ -29,12 +32,103 @@ namespace com.keyman.keyboards {
proportionalX: number;
proportionalWidth: number;
// Keeping things simple here, as this was added LATE in 14.0 beta.
// Could definitely extend in the future to instead return an object
// that denotes the 'nature' of the key.
// - isUnicode
// - isHardwareKey
// - etc.
// Reference for the terminology in the comments below:
// https://help.keyman.com/developer/current-version/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty
/**
* Matches the key code as set within Keyman Developer for the layout.
* For example, K_R or U_0020. Denotes either physical keys or virtual keys with custom output,
* with no additional metadata like layer or active modifiers.
*
* Is used to determine the keycode for input events, rule-matching, and keystroke processing.
*/
public get baseKeyID(): string {
if(typeof this.id === 'undefined') {
return undefined;
}
return this.id;
}
/**
* A unique identifier based on both the key ID & the 'desktop layer' to be used for the key.
*
* Allows diambiguation of scenarios where the same key ID is used twice within a layer, but
* with different innate modifiers. (Refer to https://github.com/keymanapp/keyman/issues/4617)
* The 'desktop layer' may be omitted if it matches the key's display layer.
*
* Examples, given a 'default' display layer, matching keys to Keyman keyboard language:
*
* ```
* "K_Q"
* + [K_Q]
* "K_Q+shift"
* + [K_Q SHIFT]
* ```
*
* Useful when the active layer of an input-event is already known.
*/
public get coreID(): string {
if(typeof this.id === 'undefined') {
return undefined;
}
let baseID = this.id || '';
if(this.displayLayer != this.layer) {
baseID = baseID + '+' + this.layer;
}
return baseID;
}
/**
* A keyboard-unique identifier to be used for any display elements representing this key
* in user interfaces and/or on-screen keyboards.
*
* Distinguishes between otherwise-identical keys on different layers of an OSK.
* Includes identifying information about the key's display layer.
*
* Examples, given a 'default' display layer, matching keys to Keyman keyboard language:
*
* ```
* "default-K_Q"
* + [K_Q]
* "default-K_Q+shift"
* + [K_Q SHIFT]
* ```
*
* Useful when only the active keyboard is known about an input event.
*/
public get elementID(): string {
if(typeof this.id === 'undefined') {
return undefined;
}
return this.displayLayer + '-' + this.coreID;
}
static polyfill(key: LayoutKey, layout: ActiveLayout, displayLayer: string) {
// Add class functions to the existing layout object, allowing it to act as an ActiveLayout.
let dummy = new ActiveKey();
let proto = Object.getPrototypeOf(dummy);
for(let prop in dummy) {
if(!key.hasOwnProperty(prop)) {
key[prop] = dummy[prop];
let descriptor = Object.getOwnPropertyDescriptor(proto, prop);
if(descriptor) {
// It's a computed property! Copy the descriptor onto the key's object.
Object.defineProperty(key, prop, descriptor);
} else {
key[prop] = dummy[prop];
}
}
}
@ -237,8 +331,8 @@ namespace com.keyman.keyboards {
populateKeyMap(map: {[keyId: string]: ActiveKey}) {
this.key.forEach(function(key: ActiveKey) {
if(key.id) {
map[key.id] = key;
if(key.coreID) {
map[key.coreID] = key;
}
});
}
@ -401,12 +495,12 @@ namespace com.keyman.keyboards {
this.row.forEach(function(row: ActiveRow): void {
row.key.forEach(function(key: ActiveKey): void {
// If the key lacks an ID, just skip it. Sometimes used for padding.
if(!key.id) {
if(!key.baseKeyID) {
return;
} else {
// Attempt to filter out known non-output keys.
// Results in a more optimized distribution.
switch(key.id) {
switch(key.baseKeyID) {
case 'K_SHIFT':
case 'K_LOPT':
case 'K_ROPT':
@ -418,7 +512,7 @@ namespace com.keyman.keyboards {
// Refer to text/codes.ts - these are Keyman-custom "keycodes" used for
// layer shifting keys. To be safe, we currently let K_TABBACK and
// K_TABFWD through, though we might be able to drop them too.
let code = com.keyman.text.Codes[key.id];
let code = com.keyman.text.Codes[key.baseKeyID];
if(code > 50000 && code < 50011) {
return;
}
@ -463,7 +557,7 @@ namespace com.keyman.keyboards {
distY += dy * layer.rowProportionalHeight;
let distance = distX * distX + distY * distY;
keyDists[key.id] = distance;
keyDists[key.coreID] = distance;
});
});

View file

@ -23,7 +23,7 @@ namespace com.keyman.keyboards {
"pad"?: string,
"sk"?: LayoutKey[]
}
export type LayoutRow = {
"id": string, // represents a number, though...
"key": LayoutKey[]
@ -53,7 +53,7 @@ namespace com.keyman.keyboards {
"phone"?: LayoutFormFactor,
"tablet"?: LayoutFormFactor
}
// This class manages default layout construction for consumption by OSKs without a specified layout.
export class Layouts {
static dfltCodes=[
@ -104,15 +104,14 @@ namespace com.keyman.keyboards {
'leftctrl-shift': '*LCtrlShift*',
'rightctrl-shift': '*RCtrlShift*'
};
/**
* Build a default layout for keyboards with no explicit layout
*
* @param {Object} PVK keyboard object (as loaded)
* @param {Object} kbdDevVersion object representing the version of Developer that compiled the keyboard
* @param {number} kbdBitmask keyboard modifier bitmask
* @param {string} formFactor
* @return {Object}
* @param {Object} PVK raw specifications
* @param {Keyboard} keyboard keyboard object (as loaded)
* @param {string} formFactor (really utils.FormFactor)
* @return {LayoutFormFactor}
*/
static buildDefaultLayout(PVK, keyboard: Keyboard, formFactor: string): LayoutFormFactor {
// Build a layout using the default for the device
@ -175,7 +174,7 @@ namespace com.keyman.keyboards {
// If there is no predefined layout, even touch layouts will follow the desktop's
// setting for the displayUnderlying flag. As the desktop layout uses a different
// format for its layout spec, that's found at the field referenced below.
layout["displayUnderlying"] = !!keyboard.scriptObject['KDU'];
layout["displayUnderlying"] = keyboard ? !!keyboard.scriptObject['KDU'] : false;
// For desktop devices, we must create all layers, even if invalid.
if(formFactor == 'desktop') {
@ -212,14 +211,14 @@ namespace com.keyman.keyboards {
// Erase the legacy shifted subkey array.
shiftKey['sk'] = [];
for(var layerID in keyLabels) {
for(var layerID in keyLabels) {
if(layerID == 'default' || layerID == 'shift') {
// These two are accessible from the layer without subkeys.
continue;
}
// Create a new subkey for the specified layer so that it will be accessible via OSK.
var specialChar = Layouts.modifierSpecials[layerID];
var specialChar = Layouts.modifierSpecials[layerID];
let subkey: LayoutKey = {
id: "K_" + specialChar,
text: specialChar,
@ -376,7 +375,7 @@ namespace com.keyman.keyboards {
/**
* Generates a list of potential layer ids for the specified chirality mode.
*
*
* @param {boolean} chiral // Does the keyboard use chiral modifiers or not?
*/
static generateLayerIds(chiral: boolean): string[] {
@ -401,7 +400,7 @@ namespace com.keyman.keyboards {
/**
* Sets a formatting property for the modifier keys when constructing a default layout for a keyboard.
*
*
* @param {Object} layer // One layer specification
* @param {boolean} chiral // Whether or not the keyboard uses chiral modifier information.
* @param {string} formFactor // The form factor of the device the layout is being constructed for.
@ -423,7 +422,7 @@ namespace com.keyman.keyboards {
case 'K_RSHIFT':
if(layerId.indexOf('shift') != -1) {
key['sp'] = buttonClasses['SHIFT-ON'];
}
}
if(formFactor != 'desktop') {
if(layerId != 'default') {
key['nextlayer']='default';
@ -439,7 +438,7 @@ namespace com.keyman.keyboards {
key['sp'] = buttonClasses['SHIFT-ON'];
}
break;
}
}
case 'K_RCTRL':
case 'K_RCONTROL':
if(chiral) {
@ -451,7 +450,7 @@ namespace com.keyman.keyboards {
case 'K_CONTROL':
if(layerId.indexOf('ctrl') != -1) {
if(!chiral || (layerId.indexOf('leftctrl') != -1 && layerId.indexOf('rightctrl') != -1)) {
key['sp'] = buttonClasses['SHIFT-ON'];
key['sp'] = buttonClasses['SHIFT-ON'];
}
}
break;
@ -461,18 +460,18 @@ namespace com.keyman.keyboards {
key['sp'] = buttonClasses['SHIFT-ON'];
}
break;
}
}
case 'K_RALT':
if(chiral) {
if(layerId.indexOf('rightalt') != -1) {
key['sp'] = buttonClasses['SHIFT-ON'];
}
break;
}
}
case 'K_ALT':
if(layerId.indexOf('alt') != -1) {
if(!chiral || (layerId.indexOf('leftalt') != -1 && layerId.indexOf('rightalt') != -1)) {
key['sp'] = buttonClasses['SHIFT-ON'];
key['sp'] = buttonClasses['SHIFT-ON'];
}
}
break;
@ -494,7 +493,7 @@ namespace com.keyman.keyboards {
/**
* Converts the legacy BK property from pre 10.0 into the KLS keyboard layer spec format,
* sparsifying it as possible to pre-emptively check invalid layers.
*
*
* @param {Array} BK keyboard object (as loaded)
* @return {Object}
*/
@ -525,7 +524,7 @@ namespace com.keyman.keyboards {
}
}
// There must always be at least a plain 'default' layer. Array(65).fill('') would be preferable but isn't supported on IE,
// There must always be at least a plain 'default' layer. Array(65).fill('') would be preferable but isn't supported on IE,
// but buildDefaultLayer will set the defaults for these layers if no entry exists for them in the array due to length.
if(typeof KLS['default'] == 'undefined' || ! KLS['default']) {
KLS['default'] = [''];
@ -840,4 +839,4 @@ namespace com.keyman.keyboards {
}
};
}
}
}

View file

@ -487,7 +487,11 @@ class ModelCompositor {
let postContextTokenization = this.tokenize(postContext);
if(postContextTokenization) {
// Handles display string for reversions triggered by accepting a suggestion mid-token.
revertedPrefix = postContextTokenization.left[postContextTokenization.left.length-1];
if(postContextTokenization.left.length > 0) {
revertedPrefix = postContextTokenization.left[postContextTokenization.left.length-1];
} else {
revertedPrefix = '';
}
revertedPrefix += postContextTokenization.caretSplitsToken ? postContextTokenization.right[0] : '';
} else {
revertedPrefix = this.wordbreak(postContext);

View file

@ -49,22 +49,22 @@ export function defaultSearchTermToKey(wordform: string): string {
*/
export function defaultCasedSearchTermToKey(wordform: string, applyCasing: CasingFunction): string {
// While this is a bit WET, as the basic `defaultSearchTermToKey` exists and performs some of
// the same functions, repetition is the easiest way to allow the function to be safely compiled
// the same functions, repetition is the easiest way to allow the function to be safely compiled
// with ease by use of `.toString()`.
return Array.from(wordform
.normalize('NFKD')
// Remove any combining diacritics (if input is in NFKD)
.replace(/[\u0300-\u036F]/g, '')
) // end of `Array.from`
.map(c => applyCasing('lower', c))
.map(function(c) { return applyCasing('lower', c)})
.join('');
}
/**
* Specifies default casing behavior for lexical models when `languageUsesCasing` is
* set to true.
* @param casing One of 'lower' (lowercased), 'upper' (uppercased), or 'initial'.
*
* @param casing One of 'lower' (lowercased), 'upper' (uppercased), or 'initial'.
*
* 'initial' is designed to cover cases like sentence-initial & proper noun capitalization in English.
* This may be overwritten as appropriate in model-specific implementations.
* @param text The text to be modified.
@ -76,15 +76,15 @@ export function defaultApplyCasing(casing: CasingForm, text: string): string {
case 'upper':
return text.toUpperCase();
case 'initial':
let headCode = text.charCodeAt(0);
var headCode = text.charCodeAt(0);
// The length of the first code unit, as measured in code points.
let headUnitLength = 1;
var headUnitLength = 1;
// Is the first character a high surrogate, indicating possible use of UTF-16
// Is the first character a high surrogate, indicating possible use of UTF-16
// surrogate pairs? Also, is the string long enough for there to BE a pair?
if(text.length > 1 && headCode >= 0xD800 && headCode <= 0xDBFF) {
// It's possible, so now we check for low surrogates.
let lowSurrogateCode = text.charCodeAt(1);
var lowSurrogateCode = text.charCodeAt(1);
if(lowSurrogateCode >= 0xDC00 && lowSurrogateCode <= 0xDFFF) {
// We have a surrogate pair; this pair is the 'first' character.

View file

@ -1,15 +1,15 @@
import { defaultApplyCasing,
defaultCasedSearchTermToKey,
import { defaultApplyCasing,
defaultCasedSearchTermToKey,
defaultSearchTermToKey
} from "./model-defaults";
/**
* Processes certain defined model behaviors in such a way that the needed closures
* may be safely compiled to a JS file and loaded within the LMLayer.
*
*
* This is accomplished by writing out a 'pseudoclosure' within the model's IIFE,
* then used to build _actual_ closures at LMLayer load time. This 'pseudoclosure'
* will very closely match the organizational patterns of this class in order to
* will very closely match the organizational patterns of this class in order to
* facilitate the maintenance of this approach.
*/
export class ModelDefinitions {
@ -18,10 +18,10 @@ export class ModelDefinitions {
* A closure fully implementing the model's defined `applyCasing` behavior with
* the function parameter preset to the version-appropriate default.
* `defaults.applyCasing` is captured as part of the closure.
*
*
* During compilation of some models (such as Trie-based wordlist templated models),
* this closure will be directly used as part of searchTermToKey.
*
*
* In compiled code, this will instead be defined in-line as an autogenerated closure
* using the other properties of the pseudoclosure.
*/
@ -34,7 +34,7 @@ export class ModelDefinitions {
*
* During compilation of some models (such as Trie-based wordlist templated models),
* this closure will be directly utilized when compiling the lexicon.
*
*
* In compiled code, this will instead be defined in-line as an autogenerated closure
* using the other properties of the pseudoclosure.
*/
@ -42,7 +42,7 @@ export class ModelDefinitions {
/**
* Contains embedded 'default' implementations that may be needed for
* closures in the compiled version, annotated with the current version
* closures in the compiled version, annotated with the current version
* of Developer.
*/
private defaults: {
@ -54,7 +54,7 @@ export class ModelDefinitions {
/**
* Contains the model-specific definitions specified in the model's source.
*
*
* These definitions may expect `defaults.applyCasing` as a parameter in
* their final closures.
*/
@ -95,10 +95,10 @@ export class ModelDefinitions {
} else if(modelSource.languageUsesCasing == false) {
this.model.searchTermToKey = defaultSearchTermToKey;
} else {
// If languageUsesCasing is not defined, then we use pre-14.0 behavior,
// If languageUsesCasing is not defined, then we use pre-14.0 behavior,
// which expects a lowercased default.
this.model.searchTermToKey = defaultCasedSearchTermToKey;
// Needed to provide pre-14.0 default lowercasing as part of the
// Needed to provide pre-14.0 default lowercasing as part of the
// search-term keying operation.
this.defaults.applyCasing = defaultApplyCasing;
// For compile-time use.
@ -120,18 +120,18 @@ export class ModelDefinitions {
/**
* Writes out a compiled JS version of the pseudoclosure, preserving all function
* implementations.
*
*
* This should be written to the file within the same IIFE as the model but BEFORE
* the model itself, as the model will need to refer to the definitions herein.
*/
compileDefinitions(): string {
let defn: string = '';
defn += `let ${PSEUDOCLOSURE} = {\n`
defn += `var ${PSEUDOCLOSURE} = {\n`
// ----------------------
// START - the 'defaults', which are common within the same Developer version.
defn += ` defaults: {\n version: "${this.defaults.version}"`;
// Only write out `applyCasing` if and when it is needed.
if(this.defaults.applyCasing) {
defn += `,\n applyCasing: ${this.defaults.applyCasing.toString()}`;
@ -153,7 +153,7 @@ export class ModelDefinitions {
// END - model-specific definitions
// ----------------------
// START - compiled closures. Given those definitions, write out the
// START - compiled closures. Given those definitions, write out the
// pseudoclosure-referencing closures for the needed methods.
// We should be able to define these closures in-line with the object's
@ -163,7 +163,7 @@ export class ModelDefinitions {
if(this.model.applyCasing) {
// A major potential issue: if the user wants to call extra custom functions that they've written.
//
// `applyCasing` recursion SHOULD be fine if they write `this.applyCasing() and forward all arguments
// `applyCasing` recursion SHOULD be fine if they write `this.applyCasing() and forward all arguments
// appropriately, as it will be known as `applyCasing` on the runtime `this` (`model`) object.
//
// Similarly, as long as any helper functions are similarly compiled and stored as part of `model`,

View file

@ -19,7 +19,7 @@ describe('Model definition pseudoclosures', function () {
// Note: not written the Turkish way. Turns out 'İ'.toLowerCase() decomposes the result,
// which would have made for a fairly yucky test.
['Istanbul', 'istanbul', 'istanbul'],
// The DEFAULT function is NOT responsible for understanding the Turkish
// case regarding the lowercasing of:
// 'I' U+0048 LATIN CAPITAL LETTER I to 'ı' U+0131 LATIN SMALL LETTER DOTLESS I
@ -29,14 +29,14 @@ describe('Model definition pseudoclosures', function () {
// full-width romaji has corresponding lowercased versions:
['', '', 'aesthetic'],
// "skýlos" is Greek for dog 🇬🇷🐶
// starts with an 's' and ends with an 's'
// which are DIFFERENT CHARACTERS in lowercased Greek!
['σκύλος', 'σκύλος', 'σκυλος'],
['ΣΚΥΛΟΣ', 'σκυλος', 'σκυλοσ'], // the keyed version after lowercasing doesn't know how
['ΣΚΥΛΟΣ', 'σκυλος', 'σκυλοσ'], // the keyed version after lowercasing doesn't know how
// to make the distinction. Both 'Σ's have the same char-code.
// Uncased syntax and numbers should pass through unscathed:
['1234.?!', '1234.?!', '1234.?!']
];
@ -67,19 +67,19 @@ describe('Model definition pseudoclosures', function () {
// Note: not written the Turkish way. Turns out 'İ'.toLowerCase() decomposes the result,
// which would have made for a fairly yucky test.
['Istanbul', 'Istanbul'],
['DİYARBAKIR', 'DIYARBAKIR'],
// full-width romaji has corresponding capitalized versions:
['', 'AESTHETIC'],
// "skýlos" is Greek for dog 🇬🇷🐶
// starts with an 's' and ends with an 's'
// which are DIFFERENT CHARACTERS in lowercased Greek!
['σκύλος', 'σκυλος'],
['ΣΚΥΛΟΣ', 'ΣΚΥΛΟΣ'], // the keyed version after lowercasing doesn't know how
['ΣΚΥΛΟΣ', 'ΣΚΥΛΟΣ'], // the keyed version after lowercasing doesn't know how
// to make the distinction. Both 'Σ's have the same char-code.
// Uncased syntax and numbers should pass through unscathed:
['1234.?!', '1234.?!']
];
@ -105,19 +105,19 @@ describe('Model definition pseudoclosures', function () {
// Note: not written the Turkish way. Turns out 'İ'.toLowerCase() decomposes the result,
// which would have made for a fairly yucky test.
['Istanbul', 'istanbul'],
['DİYARBAKIR', 'diyarbakir'],
// full-width romaji has corresponding capitalized versions:
['', 'aesthetic'],
// "skýlos" is Greek for dog 🇬🇷🐶
// starts with an 's' and ends with an 's'
// which are DIFFERENT CHARACTERS in lowercased Greek!
['σκύλος', 'σκυλος'],
['ΣΚΥΛΟΣ', 'σκυλοσ'], // the keyed version after lowercasing doesn't know how
['ΣΚΥΛΟΣ', 'σκυλοσ'], // the keyed version after lowercasing doesn't know how
// to make the distinction. Both 'Σ's have the same char-code.
// Uncased syntax and numbers should pass through unscathed:
['1234.?!', '1234.?!']
];
@ -143,7 +143,7 @@ describe('Model definition pseudoclosures', function () {
.replace(/ı/g, 'I')
.replace(/i/g, 'İ'));
case 'initial':
return turkishCasing('upper', text.charAt(0), defaultApplyCasing) + text.substr(1);
return turkishCasing('upper', text.charAt(0), defaultApplyCasing) + text.substr(1);
default:
return text;
}
@ -156,7 +156,7 @@ describe('Model definition pseudoclosures', function () {
return Array.from(wordform
.normalize('NFC') // Mostly to avoid decomposing 'İ'
) // end of `Array.from`
.map(c => applyCasing('lower', c)) // Will use custom `applyCasing` definition!
.map(function(c) { return applyCasing('lower', c)}) // Will use custom `applyCasing` definition!
.join('');
},
sources: [],
@ -167,14 +167,14 @@ describe('Model definition pseudoclosures', function () {
const testCases: [string, string, string][] = [
['İstanbul', 'istanbul', 'istanbul'],
// The DEFAULT function is NOT responsible for understanding the Turkish
// case regarding the lowercasing of:
// 'I' U+0048 LATIN CAPITAL LETTER I to 'ı' U+0131 LATIN SMALL LETTER DOTLESS I
// For Turkic languages, the recommendation is to make a
// custom applyCasing function:
['DİYARBAKIR', 'diyarbakır', 'diyarbakır'],
// Uncased syntax and numbers should pass through unscathed:
['1234.?!', '1234.?!', '1234.?!']
];

View file

@ -1,48 +1,48 @@
# History and Versioning
In Keyman 14.0, history and versioning is controlled as part of the build process,
In Keyman 14.0, history and versioning is controlled as part of the build process,
and unified. We no longer have a separate version or history for each project. The
controlling version number is in /VERSION.md and the master history is in /HISTORY.md.
controlling version number is in `/VERSION.md` and the master history is in `/HISTORY.md`.
While there may be multiple versions of HISTORY.md in different branches, the
While there may be multiple versions of `HISTORY.md` in different branches, the
**master** branch is source of truth and is the version that is published to the
website.
# VERSION.md
## VERSION.md
The version shown in VERSION.md is the upcoming release version. This means:
The version shown in `VERSION.md` is the upcoming release version. This means:
1. A snapshot of the repository will show the changes relevant to that version
2. The build system will simply use that version number when building.
3. If a release does not exist for the version shown, then you are at the
3. If a release does not exist for the version shown, then you are at the
bleeding edge.
# TIER.md
## TIER.md
The release tier is updated in TIER.md?
The release tier is updated in `TIER.md`?
# How history and version are updated
## How history and version are updated
For Alpha versions (**master**), the version number is incremented at the end of a
successful Nightly build. This means that during a single day, there may be multiple
For Alpha versions (**master**), the version number is incremented at the end of a
successful Nightly build. This means that during a single day, there may be multiple
builds with the same version number. Only the last one of these is kept.
A similar process will be followed for Beta versions.
For Stable versions, the version number should be manually incremented in a pull
For Stable versions, the version number should be manually incremented in a pull
request (these are much rarer).
## History format
### History format
```
## <yyyy-mm-dd> <version> <tier>
```text
## <yyyy-mm-dd> <version> <tier>
<type> [(<scope>[/<sub-scope>])]: <message> [(#<PR>)]
```
For example:
```
```text
## 2020-01-27 13.0.65 beta
feat(developer/compiler): Hotkeys defined in .kmn no longer need to be quoted (#2432)
chore: Release beta (#2555)

View file

@ -1,5 +1,4 @@
npm packages
============
# npm packages
This is a guide for how the various Keyman `npm` packages are developed
and published.
@ -9,14 +8,12 @@ Currently, we're using TeamCity for this.
Packages are **never** published from a developer's machine.
List of current npm packages
----------------------------
## List of current npm packages
* `@keymanapp/models-types` -- located at `common/models-types`
* `@keymanapp/lexical-model-compiler` -- located at `developer/js`
* `@keymanapp/models-types` -- located at `common/models-types`
* `@keymanapp/lexical-model-compiler` -- located at `developer/js`
For CI developers
-----------------
## For CI developers
### In general
@ -25,46 +22,53 @@ server.
This is typically done with:
./build.sh -test
```bash
./build.sh -test
```
Once the build succeeds and the tests pass, the version is set in
`package.json`:
./build.sh -version 12.0.${BUILD_NUMBER} -tier ${tier}
```bash
./build.sh -version 12.0.${BUILD_NUMBER} -tier ${tier}
```
Now you can publish! Use the following script for this (again, **only on the CI server**!)
./build.sh -publish-to-npm -tier ${tier}
```bash
./build.sh -publish-to-npm -tier ${tier}
```
It is then uploaded to the npm package directory. **Ensure that the
compiled sources are included in the tarball**. For example,
$ npm publish
npm notice 📦 @keymanapp/lexical-model-compiler@0.0.0
npm notice === Tarball Contents ===
npm notice 12.4kB dist/lexical-model-compiler/build-trie.js
npm notice 1.2kB dist/kmlmc.js
npm notice 3.0kB dist/kmlmi.js
npm notice 1.4kB dist/kmlmp.js
npm notice 5.0kB dist/package-compiler/kmp-compiler.js
npm notice 55B dist/package-compiler/kmp-json-file.js
npm notice 552B dist/package-compiler/kps-file.js
npm notice 4.6kB dist/lexical-model-compiler/lexical-model-compiler.js
npm notice 315B dist/lexical-model-compiler/lexical-model.js
npm notice 158B dist/util/min-keyman-version.js
npm notice 4.5kB dist/model-info-compiler/model-info-compiler.js
npm notice 57B dist/lexical-model-compiler/model-info-file.js
npm notice 341B dist/lexical-model-compiler/stub.js
npm notice 115B dist/util/sysexits.js
npm notice 2.1kB dist/util/util.js
npm notice 1.4kB package.json
npm notice 1.0kB README.md
```bash
$ npm publish
npm notice 📦 @keymanapp/lexical-model-compiler@0.0.0
npm notice === Tarball Contents ===
npm notice 12.4kB dist/lexical-model-compiler/build-trie.js
npm notice 1.2kB dist/kmlmc.js
npm notice 3.0kB dist/kmlmi.js
npm notice 1.4kB dist/kmlmp.js
npm notice 5.0kB dist/package-compiler/kmp-compiler.js
npm notice 55B dist/package-compiler/kmp-json-file.js
npm notice 552B dist/package-compiler/kps-file.js
npm notice 4.6kB dist/lexical-model-compiler/lexical-model-compiler.js
npm notice 315B dist/lexical-model-compiler/lexical-model.js
npm notice 158B dist/util/min-keyman-version.js
npm notice 4.5kB dist/model-info-compiler/model-info-compiler.js
npm notice 57B dist/lexical-model-compiler/model-info-file.js
npm notice 341B dist/lexical-model-compiler/stub.js
npm notice 115B dist/util/sysexits.js
npm notice 2.1kB dist/util/util.js
npm notice 1.4kB package.json
npm notice 1.0kB README.md
```
For every release of **Keyman Developer**, the CI publishes a release of
`@keymanapp/lexical-model-compiler`. The version number is locked with
the particular version of Keyman Developer.
### `set_npm_version`
This helper function sets the version in the `package.json` for the

3
ios/.gitignore vendored
View file

@ -17,6 +17,9 @@ engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyman-sentr
Keyman 2*
Carthage
# Default resource packages (which may be downloaded automatically via build-script
engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/*.kmp
# Generated offline help
keyman/Keyman/resources/OfflineHelp.bundle/Contents/Resources

View file

@ -71,7 +71,7 @@ displayInfo "" \
cd $KEYMAN_ROOT/ios/help
MDLUA="$KEYMAN_ROOT/resources/build/htmlink.lua"
MDLUA="$KEYMAN_ROOT/resources/build/html-link.lua"
MD=`find . -name "*.md"`
DESTHTM="$THIS_DIR/keyman/Keyman/resources/OfflineHelp.bundle/Contents/Resources"

View file

@ -213,6 +213,8 @@
CEA1486C2407808F00C6ECD2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CEA1486F2407808F00C6ECD2 /* Localizable.strings */; };
CEA1486D2407808F00C6ECD2 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CEA1486F2407808F00C6ECD2 /* Localizable.strings */; };
CEA14870240780E100C6ECD2 /* ResourceInfoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEA14872240780E100C6ECD2 /* ResourceInfoView.xib */; };
CEA31E5825FB51D500534484 /* No-defaults early 14.0.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CEA31E5725FB51D400534484 /* No-defaults early 14.0.bundle */; };
CEA31E6025FB5BC200534484 /* Early 14.0 with defaults.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CEA31E5F25FB5BC200534484 /* Early 14.0 with defaults.bundle */; };
CEA6BA9D249872E8002D44CE /* Simple 13.0 Migration.bundle in Resources */ = {isa = PBXBuildFile; fileRef = CEA6BA9C249872E7002D44CE /* Simple 13.0 Migration.bundle */; };
CEA70CCF24CAC3AE001C12E6 /* Obsoletions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA70CCE24CAC3AE001C12E6 /* Obsoletions.swift */; };
CEA9670924BEC4030035AACF /* ResourceUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA9670824BEC4030035AACF /* ResourceUpdateTests.swift */; };
@ -514,6 +516,8 @@
CEA14871240780E100C6ECD2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ResourceInfoView.xib; sourceTree = "<group>"; };
CEA14874240780EF00C6ECD2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ResourceInfoView.strings; sourceTree = "<group>"; };
CEA148772407869200C6ECD2 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/ResourceInfoView.strings; sourceTree = "<group>"; };
CEA31E5725FB51D400534484 /* No-defaults early 14.0.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "No-defaults early 14.0.bundle"; sourceTree = "<group>"; };
CEA31E5F25FB5BC200534484 /* Early 14.0 with defaults.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "Early 14.0 with defaults.bundle"; sourceTree = "<group>"; };
CEA6BA9C249872E7002D44CE /* Simple 13.0 Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "Simple 13.0 Migration.bundle"; sourceTree = "<group>"; };
CEA70CCE24CAC3AE001C12E6 /* Obsoletions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Obsoletions.swift; sourceTree = "<group>"; };
CEA9670824BEC4030035AACF /* ResourceUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceUpdateTests.swift; sourceTree = "<group>"; };
@ -887,6 +891,8 @@
CE37C38923FCC9AF007031C6 /* resources */ = {
isa = PBXGroup;
children = (
CEA31E5F25FB5BC200534484 /* Early 14.0 with defaults.bundle */,
CEA31E5725FB51D400534484 /* No-defaults early 14.0.bundle */,
CEA9670C24BEEFF80035AACF /* khmer_angkor update-base.bundle */,
CE02EE0B24A5863100D58F92 /* Queries.bundle */,
CEA6BA9C249872E7002D44CE /* Simple 13.0 Migration.bundle */,
@ -1338,6 +1344,7 @@
9ADC45A022E1895D004C78C6 /* LanguageLMDetailViewController.xib in Resources */,
CE973D832484A5DC00F66045 /* PackageJSON.bundle in Resources */,
CEF888D223FE6ADF00667693 /* No-defaults 10.0 Migration.bundle in Resources */,
CEA31E5825FB51D500534484 /* No-defaults early 14.0.bundle in Resources */,
CE9B440F23FE4E7500499CAB /* 12.0 Ad-hoc Migration.bundle in Resources */,
CE8B5BB22491DA540075CCB0 /* 13.0 Cloud to Package Migration.bundle in Resources */,
9A0FCA0922D7C58B00D33F86 /* Keyman.bundle in Resources */,
@ -1345,6 +1352,7 @@
CE02EE0C24A5863100D58F92 /* Queries.bundle in Resources */,
CEA6BA9D249872E8002D44CE /* Simple 13.0 Migration.bundle in Resources */,
CEF888D323FE6ADF00667693 /* Simple 10.0 Migration.bundle in Resources */,
CEA31E6025FB5BC200534484 /* Early 14.0 with defaults.bundle in Resources */,
CE37C38F23FCD52F007031C6 /* Lexical Models.bundle in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;

View file

@ -69,20 +69,28 @@ public enum Key {
public enum Defaults {
private static let font = Font(family: "LatinWeb", source: ["DejaVuSans.ttf"], size: nil)
public static let keyboard = InstallableKeyboard(id: "sil_euro_latin",
name: "EuroLatin (SIL)",
languageID: "en",
languageName: "English",
version: "1.9.1",
isRTL: false,
font: font,
oskFont: nil,
isCustom: false)
public static let lexicalModel = InstallableLexicalModel(id: "nrc.en.mtnt",
name: "English dictionary (MTNT)",
languageID: "en",
version: "0.1.4",
isCustom: false)
public static let keyboardID = FullKeyboardID(keyboardID: "sil_euro_latin", languageID: "en")
public static let lexicalModelID = FullLexicalModelID(lexicalModelID: "nrc.en.mtnt", languageID: "en")
public static var keyboardPackage: KeyboardKeymanPackage = {
let bundledKMP = Resources.bundle.url(forResource: keyboardID.keyboardID, withExtension: ".kmp")!
return try! ResourceFileManager.shared.prepareKMPInstall(from: bundledKMP) as! KeyboardKeymanPackage
}()
public static let lexicalModelPackage: LexicalModelKeymanPackage = {
let bundledKMP = Resources.bundle.url(forResource: lexicalModelID.lexicalModelID, withExtension: ".model.kmp")!
return try! ResourceFileManager.shared.prepareKMPInstall(from: bundledKMP) as! LexicalModelKeymanPackage
}()
// Must be retrieved from their packages!
public static let keyboard: InstallableKeyboard = {
return keyboardPackage.findResource(withID: keyboardID)!
}()
public static let lexicalModel: InstallableLexicalModel = {
return lexicalModelPackage.findResource(withID: lexicalModelID)!
}()
}
public enum Resources {

View file

@ -190,6 +190,16 @@ public class KeymanPackage {
distributionMethod = downloadURL != nil ? .cloud : .unknown
self.downloadURL = downloadURL
}
// Used to store metadata for newly-downloaded results from the keyboard search.
// To save a version, we require also having a known URL.
internal init(downloadURL: URL, version: Version) {
self.timestampForLastQuery = NSDate().timeIntervalSince1970
self.latestVersion = version.fullString
self.downloadURL = downloadURL
distributionMethod = .cloud
}
}
static private let kmpFile = "kmp.json"

View file

@ -16,10 +16,10 @@ private enum MigrationLevel {
struct VersionResourceSet {
var version: Version
var resources: [AnyLanguageResource]
var resources: [AnyLanguageResourceFullID]
// but how to handle deprecation?
init(version: Version, resources: [AnyLanguageResource]) {
init(version: Version, resources: [AnyLanguageResourceFullID]) {
self.version = version
self.resources = resources
}
@ -29,54 +29,14 @@ public enum Migrations {
static let resourceHistory: [VersionResourceSet] = {
let font = Font(family: "LatinWeb", source: ["DejaVuSans.ttf"], size: nil)
let european = InstallableKeyboard(id: "european",
name: "EuroLatin Keyboard",
languageID: "en",
languageName: "English",
version: "1.7",
isRTL: false,
font: font,
oskFont: nil,
isCustom: false)
let european = FullKeyboardID(keyboardID: "european", languageID: "en")
// Default keyboard in version 10.0 (and likely before)
let european2 = InstallableKeyboard(id: "european2",
name: "EuroLatin2 Keyboard",
languageID: "en",
languageName: "English",
version: "1.6",
isRTL: false,
font: font,
oskFont: nil,
isCustom: false)
let european2 = FullKeyboardID(keyboardID: "european2", languageID: "en")
let sil_euro_latin_1_8_1 = InstallableKeyboard(id: "sil_euro_latin",
name: "EuroLatin (SIL)",
languageID: "en",
languageName: "English",
version: "1.8.1",
isRTL: false,
font: font,
oskFont: nil,
isCustom: false)
let sil_euro_latin = FullKeyboardID(keyboardID: "sil_euro_latin", languageID: "en")
let sil_euro_latin_1_9_1 = InstallableKeyboard(id: "sil_euro_latin",
name: "EuroLatin (SIL)",
languageID: "en",
languageName: "English",
version: "1.9.1",
isRTL: false,
font: font,
oskFont: nil,
isCustom: false)
let nrc_en_mtnt_0_1_2 = InstallableLexicalModel(id: "nrc.en.mtnt",
name: "English dictionary (MTNT)",
languageID: "en",
version: "0.1.2",
isCustom: false)
let nrc_en_mtnt = Defaults.lexicalModel
let nrc_en_mtnt = FullLexicalModelID(lexicalModelID: "nrc.en.mtnt", languageID: "en")
// Unknown transition point: european
// Before v11: european2
@ -86,17 +46,13 @@ public enum Migrations {
let legacy_resources = VersionResourceSet(version: Version.fallback, resources: [european])
let v10_resources = VersionResourceSet(version: Version("10.0")!, resources: [european2])
let v11_resources = VersionResourceSet(version: Version("11.0")!, resources: [sil_euro_latin_1_8_1])
let v12_resources = VersionResourceSet(version: Version("12.0")!, resources: [sil_euro_latin_1_8_1, nrc_en_mtnt_0_1_2])
let v13_resources = VersionResourceSet(version: Version("13.0.65")!, resources: [sil_euro_latin_1_8_1, nrc_en_mtnt])
let v14_resources = VersionResourceSet(version: Version("14.0.0")!, resources: [sil_euro_latin_1_9_1, nrc_en_mtnt])
let v11_resources = VersionResourceSet(version: Version("11.0")!, resources: [sil_euro_latin])
let v12_resources = VersionResourceSet(version: Version("12.0")!, resources: [sil_euro_latin, nrc_en_mtnt])
timeline.append(legacy_resources)
timeline.append(v10_resources)
timeline.append(v11_resources)
timeline.append(v12_resources)
timeline.append(v13_resources)
timeline.append(v14_resources)
return timeline
}()
@ -190,9 +146,16 @@ public enum Migrations {
static func updateResources(storage: Storage) {
var lastVersion = engineVersion
if (lastVersion ?? Version.fallback) >= Version.latestFeature {
// We're either current or have just been downgraded; no need to do modify resources.
// If it's a downgrade, it's near-certainly a testing environment.
// Will always seek to update default resources on version upgrades,
// even if only due to the build component of the version.
//
// This may make intentional downgrading of our default resources tedious,
// as there's (currently) no way to detect if a user intentionally did so before
// the app upgrade.
if lastVersion != nil, lastVersion! > Version.currentTagged {
// We've just been downgraded; no need to modify resources.
// If it's a downgrade, it's near-certainly a testing environment
return
}
@ -219,8 +182,21 @@ public enum Migrations {
}
}
var userKeyboards = Storage.active.userDefaults.userKeyboards ?? []
var userModels = Storage.active.userDefaults.userLexicalModels ?? []
var hasVersionDefaults = true
var oldDefaultKbd: InstallableKeyboard? = nil
var oldDefaultLex: InstallableLexicalModel? = nil
// Only assigned a non-nil value if the old default's ID matches the current default's.
// Used for version comparisons to ensure we don't unnecessarily downgrade.
var currentDefaultKbdVersion: Version? = nil
var currentDefaultLexVersion: Version? = nil
if lastVersion != nil && lastVersion != Version.freshInstall {
// Time to deinstall the old version's resources.
// Time to check on the old version's default resources.
// The user may have updated them, and possibly even beyond the currently-packaged version.
// First, find the most recent version with a listed history.
let possibleHistories: [VersionResourceSet] = resourceHistory.compactMap { set in
if set.version <= lastVersion! {
@ -236,44 +212,46 @@ public enum Migrations {
let resources = possibleHistories[possibleHistories.count-1].resources
resources.forEach { res in
if let kbd = res as? InstallableKeyboard {
var userKeyboards = Storage.active.userDefaults.userKeyboards
if let kbd = res as? FullKeyboardID {
// Does not remove the deprecated keyboard's files - just the registration.
userKeyboards?.removeAll(where: { kbd2 in
return kbd.id == kbd2.id && kbd.languageID == kbd2.languageID
})
Storage.active.userDefaults.userKeyboards = userKeyboards
} else if let lex = res as? InstallableLexicalModel {
var userModels = Storage.active.userDefaults.userLexicalModels
if let match = userKeyboards.first(where: { $0.fullID == kbd }) {
if match.fullID == Defaults.keyboard.fullID {
currentDefaultKbdVersion = Version(match.version)
}
oldDefaultKbd = match
} else {
hasVersionDefaults = false
}
} else if let lex = res as? FullLexicalModelID {
// Parallels the issue with deprecated files for keyboards.
userModels?.removeAll(where: { lex2 in
return lex.id == lex2.id && lex.languageID == lex2.languageID
})
Storage.active.userDefaults.userLexicalModels = userModels
if let match = userModels.first(where: { $0.fullID == lex }) {
if match.fullID == Defaults.lexicalModel.fullID {
currentDefaultLexVersion = Version(match.version)
}
oldDefaultLex = match
} else {
hasVersionDefaults = false
}
} // else Not yet implemented
}
// The user has customized their default resources; Keyman will refrain from
// changing the user's customizations.
if !hasVersionDefaults {
return
}
}
// Now to install the new version's resources.
var userKeyboards = Storage.active.userDefaults.userKeyboards ?? []
var userModels = Storage.active.userDefaults.userLexicalModels ?? []
let defaultsNeedBackup = (lastVersion ?? Version.fallback) < Version.defaultsNeedBackup
var installKbd = false
var installLex = false
// Don't add the keyboard a second time if it's already installed and backed up. Can happen
// if multiple Keyman versions match.
if !userKeyboards.contains(where: { kbd in
kbd.id == Defaults.keyboard.id && kbd.languageID == Defaults.keyboard.languageID
}) {
// First the keyboard. If it needs an update:
if currentDefaultKbdVersion ?? Version("0.0")! <= Version(Defaults.keyboard.version)! || defaultsNeedBackup {
// Remove old installation.
userKeyboards.removeAll(where: { $0.fullID == oldDefaultKbd?.fullID })
userKeyboards = [Defaults.keyboard] + userKeyboards // Make sure the default goes in the first slot!
Storage.active.userDefaults.userKeyboards = userKeyboards
installKbd = true
}
if(defaultsNeedBackup || installKbd) {
do {
try Storage.active.installDefaultKeyboard(from: Resources.bundle)
} catch {
@ -281,15 +259,12 @@ public enum Migrations {
}
}
if !userModels.contains(where: { lex in
lex.id == Defaults.lexicalModel.id && lex.languageID == Defaults.lexicalModel.languageID
}) {
if currentDefaultLexVersion ?? Version("0.0")! < Version(Defaults.lexicalModel.version)! || defaultsNeedBackup {
// Remove old installation
userModels.removeAll(where: { $0.fullID == oldDefaultLex?.fullID} )
userModels = [Defaults.lexicalModel] + userModels
Storage.active.userDefaults.userLexicalModels = userModels
installLex = true
}
if(defaultsNeedBackup || installLex) {
do {
try Storage.active.installDefaultLexicalModel(from: Resources.bundle)
} catch {
@ -298,7 +273,7 @@ public enum Migrations {
}
// Store the version we just upgraded to.
storage.userDefaults.lastEngineVersion = Version.latestFeature
storage.userDefaults.lastEngineVersion = Version.currentTagged
}
static func migrateUserDefaultsToStructs(storage: Storage) {

View file

@ -14,6 +14,11 @@ public class Version: NSObject, Comparable {
case alpha
case beta
case stable
// In case we wish to do any tier-based comparisons.
public static func < (lhs: Version.Tier, rhs: Version.Tier) -> Bool {
return lhs.rawValue < rhs.rawValue
}
}
public static let fallback = Version("1.0")!

View file

@ -157,6 +157,28 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat
progressView?.isHidden = false
}
private func failWithAlert(for error: Error) {
let errorTitle = NSLocalizedString("alert-no-connection-title", bundle: engineBundle, comment: "")
let alert = ResourceFileManager.shared.buildSimpleAlert(title: errorTitle, message: error.localizedDescription) {
// If we are in a UINavigationViewController and are not its root view...
if let navVC = self.navigationController, navVC.viewControllers.first != self {
navVC.popViewController(animated: true)
} else {
self.dismiss(animated: true)
}
}
self.present(alert, animated: true)
}
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
failWithAlert(for: error)
}
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
failWithAlert(for: error)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
@ -232,6 +254,15 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat
// Illegal state - we already checked this.
fatalError()
}
// We've likely installed the package, and we already know
// the package's current publishing state - we literally just
// downloaded it from the cloud as part of the keyboard search.
//
// Why query later when we know the query's answer NOW?
let publishState = KeymanPackage.DistributionStateMetadata(downloadURL: packageURL, version: package.version)
Storage.active.userDefaults.cachedPackageQueryMetadata[package.key] = publishState
}
downloadManager.downloadPackage(withKey: packageKey, from: packageURL, withNotifications: true, completionBlock: downloadClosure)

View file

@ -137,7 +137,7 @@ public class ResourceFileManager {
*/
public func prepareKMPInstall(from url: URL) throws -> KeymanPackage {
// Once selected, start the standard install process.
log.info("Installing KMP from \(url)")
log.info("Opening KMP from \(url)")
// Step 1: Copy it to a temporary location, making it a .zip in the process
let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]

View file

@ -15,6 +15,105 @@ class MigrationTests: XCTestCase {
TestUtils.standardTearDown()
}
// // Uncomment this and set up whatever resources are needed for your test bundle in order to build one!
// func testForMigrationBundleConstruction() {
// do {
// if let sil_euro_latin = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.silEuroLatinKMP) as? KeyboardKeymanPackage {
// try ResourceFileManager.shared.install(resourceWithID: TestUtils.Keyboards.sil_euro_latin.fullID, from: sil_euro_latin)
// } else {
// XCTFail()
// }
//
// if let sencoten_kbd_kmp = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.Keyboards.fvSencotenKMP) as? KeyboardKeymanPackage {
// try ResourceFileManager.shared.install(resourceWithID: TestUtils.Keyboards.fv_sencoten.fullID, from: sencoten_kbd_kmp)
// } else {
// XCTFail()
// }
//
// if let mtntKMP = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.LexicalModels.mtntKMP) as? LexicalModelKeymanPackage {
// try ResourceFileManager.shared.install(resourceWithID: TestUtils.LexicalModels.mtnt.fullID,
// from: mtntKMP)
// } else {
// XCTFail()
// }
//
// if let sencoten_lm_kmp = try ResourceFileManager.shared.prepareKMPInstall(from: TestUtils.LexicalModels.sencotenKMP) as? LexicalModelKeymanPackage {
// try ResourceFileManager.shared.install(resourceWithID: TestUtils.LexicalModels.sencoten.fullID,
// from: sencoten_lm_kmp)
// } else {
// XCTFail()
// }
//
// } catch {
// XCTFail("File system commands failed.")
// }
//
// // Set the desired version entry for your format, if different from the actual local version.
// // Only use if you're sure the specified version has a matching file format.
// Storage.active.userDefaults.lastEngineVersion = Version.packageBasedFileReorg
// Storage.active.userDefaults.migrationLevel = 20 // migratedForKMP
//
// // Note: you may need to manually pause at a certain (marked) point within this method
// // for everything to successfully write out!
// if let saveState = try? TestUtils.EngineStateBundler.createBundle(withName: "Early 14.0 with defaults") {
// self.add(saveState)
// } else {
// XCTFail()
// }
// }
func testSimpleEarly14Migration() {
// A case where the user has both SENCOTEN installed for both keyboard & lexical model.
// Mirrors `testNoDefaultsEarly14Migration`, but where sil_euro_latin
// was not removed by the user; in contrast, the base resources
// should be auto-updated.
TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.simple_14)
Migrations.migrate(storage: Storage.active)
Migrations.updateResources(storage: Storage.active)
let userDefaults = Storage.active.userDefaults
let userKeyboards = userDefaults.userKeyboards ?? []
let userLexicalModels = userDefaults.userLexicalModels ?? []
XCTAssertEqual(userKeyboards.count, 2)
// Because there's a lexical model update, it installs the whole package.
// Not exactly ideal, but correct for pre-existing behavior, which
// installs the whole default package instead of a single language-code pairing.
XCTAssertEqual(userLexicalModels.count, 4)
//[s]il_[e]uro_[l]atin
let kbdSEL = userKeyboards.first(where: { $0.fullID == TestUtils.Keyboards.sil_euro_latin.fullID })
XCTAssertNotNil(kbdSEL)
// Because there's a keyboard update (1.9.3 vs 1.9.1, at the time of writing).
// we expect a more recent version than the testing version.
XCTAssertGreaterThan(Version(kbdSEL!.version)!, Version(TestUtils.Keyboards.sil_euro_latin.version)!)
XCTAssertTrue(userLexicalModels.contains(where: { $0.fullID == TestUtils.LexicalModels.mtnt.fullID }))
XCTAssertTrue(userKeyboards.contains(where: { $0.fullID == TestUtils.Keyboards.fv_sencoten.fullID }))
XCTAssertTrue(userLexicalModels.contains(where: { $0.fullID == TestUtils.LexicalModels.sencoten.fullID }))
}
func testNoDefaultsEarly14Migration() {
// A case where the user only has SENCOTEN installed for both keyboard & lexical model.
// sil_euro_latin was explicitly removed by the user.
TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.noDefault_14)
Migrations.migrate(storage: Storage.active)
Migrations.updateResources(storage: Storage.active)
let userDefaults = Storage.active.userDefaults
let userKeyboards = userDefaults.userKeyboards ?? []
let userLexicalModels = userDefaults.userLexicalModels ?? []
XCTAssertEqual(userKeyboards.count, 1)
XCTAssertEqual(userLexicalModels.count, 1)
XCTAssertFalse(userKeyboards.contains(where: { $0.fullID == TestUtils.Keyboards.sil_euro_latin.fullID }))
XCTAssertFalse(userLexicalModels.contains(where: { $0.fullID == TestUtils.LexicalModels.mtnt.fullID }))
XCTAssertTrue(userKeyboards.contains(where: { $0.fullID == TestUtils.Keyboards.fv_sencoten.fullID }))
XCTAssertTrue(userLexicalModels.contains(where: { $0.fullID == TestUtils.LexicalModels.sencoten.fullID }))
}
func testComplexVersion13Migration() {
TestUtils.Migrations.applyBundleToFileSystem(TestUtils.Migrations.cloud_to_kmp_13)
Migrations.migrate(storage: Storage.active)
@ -30,7 +129,7 @@ class MigrationTests: XCTestCase {
sil_euro_latin_kbds.forEach {
XCTAssertEqual($0.packageID, "sil_euro_latin")
XCTAssertEqual($0.version, TestUtils.Keyboards.sil_euro_latin.version)
XCTAssertEqual($0.version, Defaults.keyboard.version)
}
let sil_euro_latin_package = ResourceFileManager.shared.installedPackages.first(where: { $0.id == "sil_euro_latin" }) as! KeyboardKeymanPackage
@ -52,7 +151,7 @@ class MigrationTests: XCTestCase {
let userDefaults = Storage.active.userDefaults
// SIL EuroLatin should be updated to 1.9.1. The lexical model version should be unchanged.
// SIL EuroLatin should be updated to the currently-bundled version. The lexical model version should be unchanged.
let defaultKbd = userDefaults.userKeyboards![0]
XCTAssertEqual(defaultKbd.id, Defaults.keyboard.id)

View file

@ -47,14 +47,21 @@ extension TestUtils {
try FileManager.default.copyItem(at: storageDirectory, to: bundleConstructionURL.appendingPathComponent("Library").appendingPathComponent("keyman"))
log.info("Preferences directory source: \(preferencesDirectory)")
let pListPath = bundleConstructionURL.appendingPathComponent("Library").appendingPathComponent("Preferences")
let testEngineFilename = "com.keyman.testing.KeymanEngineTestHost.plist"
let appGroupFilename = "group.KM4I.plist"
// If trying to ensure that UserDefaults is properly synchronized for this copy operation,
// go to the folder for the link above in Finder and watch the file to ensure it's properly
// fleshed out.
//
// Set a breakpoint to the following line, only resuming when the plist is fully flushed to
// the file system. You may need to rename it in the resulting bundle.
try FileManager.default.copyItem(at: preferencesDirectory, to:
bundleConstructionURL.appendingPathComponent("Library").appendingPathComponent("Preferences"))
try FileManager.default.copyItem(at: preferencesDirectory, to: pListPath)
// sleep(1000)
try FileManager.default.moveItem(at: pListPath.appendingPathComponent(testEngineFilename), to: pListPath.appendingPathComponent(appGroupFilename))
}
let attachmentFile = try Zip.quickZipFiles([bundleConstructionURL], fileName: "bundleArchive")

View file

@ -17,5 +17,13 @@ extension TestUtils {
languageID: "en",
version: "0.1.4",
isCustom: false)
static let sencotenKMP = TestUtils.lexicalModelsBundle.url(forResource: "nrc.str.sencoten.model",
withExtension: "kmp")!
static let sencoten = InstallableLexicalModel(id: "nrc.str.sencoten",
name: "SENĆOŦEN (Saanich Dialect) Lexical Model",
languageID: "str",
version: "1.0.5",
isCustom: false)
}
}

View file

@ -29,6 +29,8 @@ extension TestUtils {
static let adhoc_12 = TestUtils.findSubBundle(forResource: "12.0 Ad-hoc Migration", ofType: ".bundle")
static let simple_10 = TestUtils.findSubBundle(forResource: "Simple 10.0 Migration", ofType: ".bundle")
static let noDefault_10 = TestUtils.findSubBundle(forResource: "No-defaults 10.0 Migration", ofType: ".bundle")
static let noDefault_14 = TestUtils.findSubBundle(forResource: "No-defaults early 14.0", ofType: "bundle")
static let simple_14 = TestUtils.findSubBundle(forResource: "Early 14.0 with defaults", ofType: "bundle")
// The default keyboard in version 10.0.
static let european2_font = Font(family: "LatinWeb", source: ["DejaVuSans.ttf"], size: nil)
@ -81,10 +83,10 @@ extension TestUtils {
}
}
static func getVersionHistory(for version: Version) -> [AnyLanguageResource] {
static func getVersionHistory(for version: Version) -> [AnyLanguageResourceFullID] {
let resourceHistory = KeymanEngine.Migrations.resourceHistory
let match: [[AnyLanguageResource]] = resourceHistory.compactMap() { entry in
let match: [[AnyLanguageResourceFullID]] = resourceHistory.compactMap() { entry in
if entry.version == version {
return entry.resources
} else {

Some files were not shown because too many files have changed in this diff Show more