Merge pull request #13510 from keymanapp/chore/merge-master-into-web-core

chore: merge master into web-core 🎼
This commit is contained in:
Marc Durdin 2025-03-16 06:07:50 +07:00 • committed by GitHub
commit a9e002e5ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
341 changed files with 7727 additions and 2898 deletions

View file

@ -18,6 +18,9 @@ body:
You may also wish to discuss your bug report with the Keyman community at:
https://community.software.sil.org/c/keyman
Are you using Linux and fcitx5? If so, please create issues at:
https://github.com/fcitx/fcitx5-keyman/issues/new/choose
- type: textarea
id: description
attributes:

View file

@ -1,5 +1,22 @@
# Keyman Version History
## 19.0.13 alpha 2025-03-12
* feat(windows): hard code key cap colour (#13444)
## 19.0.12 alpha 2025-03-04
* chore: remove legacy IIS setup notes (#13403)
## 19.0.11 alpha 2025-03-03
* chore(linux): Update debian changelog (#13347)
* fix(common): improve resilience of build script trigger (#13402)
## 19.0.10 alpha 2025-03-01
* chore: merge beta into master B18S2 (#13391)
## 19.0.9 alpha 2025-02-27
* docs: move node.js before Emscripten (#13366)
@ -42,6 +59,166 @@
* refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168)
* chore: increment to alpha 19.0 (#13187)
## 18.0.214 beta 2025-03-13
* fix(mac): change text in privacy alert for macOS 13.0 and later (#13480)
* docs(mac): update troubleshooting page for macOS security (#13476)
* fix(core): unalign `usetCount` and `rangeCount` (#13450)
* fix(developer): strip markers when generating KVK from LDML (#13479)
## 18.0.213 beta 2025-03-12
* docs(developer): fix typos in readme and add link (#13414)
* fix(developer): add folder text to Error_FileDoesNotExist message (#13457)
* fix(developer): load Keyman Developer options in Server before use (#13459)
* chore(core): add `clean` actions for `:mac` and `:win` targets (#13460)
* chore(common): update langtags.json to 2025-02-18 release (#13461)
* fix(developer): use '-' as placeholder for temp project name in cmdline (#13462)
* chore: handle failure to retrieve GitHub PR status in build triggers (#13474)
## 18.0.212 beta 2025-03-11
* fix(oem/fv/android): Avoid resetting keyboard index (#13449)
* fix(android/engine): Ignore key errors if user switched to another IME (#13466)
* fix(android): Cache inputType to determine external keyboard handling (#13475)
## 18.0.211 beta 2025-03-10
* feat(windows): hide update header if no updates are available (#13442)
* fix(ios): suppress Get Started dialog when closing help page (#13422)
* docs(ios): update minimum supported ios version (#13452)
## 18.0.210 beta 2025-03-07
* chore(common): Add crowdin localization for Vietnamese (#13322)
* chore(linux): add link to fcitx5 issues to bug issue template (#13451)
## 18.0.209 beta 2025-03-06
* fix(common): fail builds with low test coverage for all typescript projects (#13418)
* fix(windows): Check for updates registry value stored under the one Key (#13413)
* fix(windows): remove online update deadcode (#13421)
## 18.0.208 beta 2025-03-05
* fix(windows): Check registry report usage value in setup process (#13215)
* fix(android): Don't report missing kmp.json (#13411)
## 18.0.207 beta 2025-03-04
* fix(common): improve resilience of build script trigger (#13407)
* fix(mac): improve compliance checking (#13337)
* chore(common): add unit test for nul and context() in context part of rule (#13412)
## 18.0.206 beta 2025-03-03
* fix(android): Control when system keyboard Keyboard Picker clears activity stack (#13313)
* docs(android): document control flag for system keyboard picker and task (#13373)
## 18.0.205 beta 2025-03-02
* chore(linux): Update debian changelog (#13346)
## 18.0.204 beta 2025-03-01
* chore(windows): ensure dependencies for keyman32 exclude core:wasm (#13379)
## 18.0.203 beta 2025-02-28
* feat(windows): keyman configuration icon refresh (#13294)
* fix(windows): if kmshell is "silent" don't start a Keyman update (#13330)
* chore(windows): Update crowdin strings for Khmer (#13364)
* docs: clean up links in builder.md (#13328)
* chore: add build.sh edit action for Delphi projects (#13327)
* fix(developer): handle line breaks in debugger correctly (#13334)
* fix(developer): use U+0001 for deadkey markers in the debugger (#13336)
* feat(developer): use .kps folder as initial dir for Add Files (#13324)
* fix(developer): verify `context(n)` offsets are valid in kmcmplib (#13308)
* chore(core): add unit tests to validate `nul` with `index` and `context` (#13311)
* fix(core): include `nul` in context offset calculations in kmx processor (#13314)
* fix(developer): include `nul` in offset calculations for kmw v10 compiler (#13326)
## 18.0.202 beta 2025-02-27
* fix(android): Direct user to switch system IME when WebView fails (#13352)
* fix(android): Filter out SocketTimeoutExceptions (#13353)
* refactor(linux): extract method for setting up mocks in tests (#13355)
* chore(linux): update to current Debian policy version (#13362)
* fix(developer): prevent double action of editor hotkeys in debugger (#13345)
* fix(linux): ignore DBusException during install and uninstall (#13356)
* fix(linux): ignore exception when fcitx5-configtool is not found (#13363)
* fix(developer): prevent simultaneous opening of the same project by two processes (#13341)
* chore: add a workflow to automatically close linked issues (#13369)
## 18.0.201 beta 2025-02-26
* fix(web): prevent cookie-default behavior from mangling engine init (#13331)
* chore(linux): adjust to renamed xdg module (#13354)
## 18.0.200 beta 2025-02-25
* fix(web): properly support workerless use of Web engine (#13290)
* fix(web): reconnect, patch up engine/main headless unit tests (#13291)
* change(ios): re-order host page script load so Sentry can report script load failures (#13332)
* chore(linux): Update to new Debian policy version (#13325)
## 18.0.199 beta 2025-02-24
* fix(windows): Update Greek Polytonic for keyboard help (#13323)
## 18.0.198 beta 2025-02-21
* fix(android): Update system keyboard height consistently (#13273)
## 18.0.197 beta 2025-02-20
* fix(linux): address compiling with ICU 76 (#13285)
## 18.0.196 beta 2025-02-19
* fix(windows): remove sentry report on firstrun in idle state (#13266)
* fix(android): Correct the scope of `tier` in build.gradle (#13268)
* chore(common): Update crowdin strings for Polytonic Greek (#13277)
* fix(web): provide informative error + message when loading UI without the engine (#13269)
* fix(developer): Use 'desktop' for target in OSK fill temp keyboard (#13271)
* fix(linux): add missing dependency for uploading to llso (#13279)
* refactor(linux): re-enable integration tests (#13234)
## 18.0.195 beta 2025-02-18
* docs(web): use promise-chaining in manual-control example (#13272)
## 18.0.194 beta 2025-02-17
* fix(android): Use TIER.md to determine Play Store tier (#13259)
* fix(android): prevent update-checks from crashing system keyboard and app if `DownloadManager` is disabled (#13218)
* feat(linux): add `--remote-debug` flag to `run-tests.sh` script (#13233)
* chore(linux): improve output when running tests (#13247)
* refactor(linux): cleanup code of test fixture (#13248)
## 18.0.193 beta 2025-02-14
* chore(developer): add INFO messages for `kmc copy` and `kmc generate` (#13200)
* fix(android): Remove sendOptionsToKeyboard function from Engine API (#13219)
* docs(android): Document removing sendOptionsToKeyboard API (#13220)
* chore(ios): increase minimum ios version to 15 (#13201)
* fix(android): Catch WebView exceptions and prompt user to install WebView (#13140)
* chore(common): Update crowdin strings for Mon (#13241)
* chore(ios,windows): Update Crowdin strings for Czech (#13242)
* change(web): drop 'dom' unit test concurrency when run in CI mode (#13141)
* chore(web): ensure /web/build.sh clean triggers all predictive-text child actions (#13158)
* chore(web): fix coverage-reporting artifact (#13196)
## 18.0.192 beta 2025-02-14
* chore(common): Fix 17.0.335 tier in HISTORY.md (#13213)
* docs(linux): update what's new for 18.0 (#13207)
* chore(linux): additional code cleanup after Focal removal (#13206)
* docs(windows): update keyman for windows 18.0 whats-new (#13211)
* chore: undo master merge (#13238)
* change(ios): prevent memory leaks from keyboard-related notification observers (#13129)
* change(ios): mitigate keyboard input-view memory leak by force-discarding WebView (#13169)
## 18.0.191 beta 2025-02-12
* feat(windows): handle a hard windows reset occurring while downloading updated keyman files (#13128)

View file

@ -1 +1 @@
19.0.10
19.0.14

View file

@ -9,6 +9,8 @@ plugins {
ext.rootPath = '../../'
apply from: "$rootPath/version.gradle"
String tier = new File('../../TIER.md').getText('UTF-8').trim();
android {
compileSdk 34
namespace="com.tavultesoft.kmapro"
@ -75,7 +77,7 @@ android {
// - PR builds also have "-test-1234" suffix (where 1234 is PR#)
// Gradle sync will nullify these values, so have a fallback
String appSuffix = ''
String tag = VERSION_TAG ? VERSION_TAG : '-' + new File('../../TIER.md').getText('UTF-8');
String tag = VERSION_TAG ? VERSION_TAG : '-' + tier;
if (tag.startsWithAny("-beta", "beta")) {
appSuffix = ' Beta';
} else if (tag.startsWithAny("-alpha", "alpha")) {
@ -127,7 +129,7 @@ if (env_keys_json_file != null && file(env_keys_json_file).exists()) {
// Deactivate lower conflicting version APKs
resolutionStrategy.set(com.github.triplet.gradle.androidpublisher.ResolutionStrategy.IGNORE)
switch (System.env.TIER) {
switch (tier) {
case 'beta':
track.set("beta")
println "TIER set to beta"

View file

@ -4,6 +4,7 @@
package com.keyman.android;
import com.keyman.engine.util.DownloadFileUtils;
import com.tavultesoft.kmapro.AdjustLongpressDelayActivity;
import com.tavultesoft.kmapro.BuildConfig;
import com.tavultesoft.kmapro.DefaultLanguageResource;
@ -44,6 +45,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
private static View inputView = null;
private static ExtractedText exText = null;
private KMHardwareKeyboardInterpreter interpreter = null;
private int inputType = InputType.TYPE_NULL;
private int lastOrientation = Configuration.ORIENTATION_UNDEFINED;
private static final String TAG = "SystemKeyboard";
@ -84,7 +86,12 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
boolean mayHaveHapticFeedback = prefs.getBoolean(KeymanSettingsActivity.hapticFeedbackKey, false);
KMManager.setHapticFeedback(mayHaveHapticFeedback);
KMManager.executeResourceUpdate(this);
// Checking for updates should never be allowed to crash the keyboard.
// Just silently fail if this occurs.
if(DownloadFileUtils.getDownloadManager(this) != null) {
// Will try to emit a toast if it fails - i.e., is not silent.
KMManager.executeResourceUpdate(this);
}
}
@Override
@ -149,18 +156,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
KMManager.onStartInput(attribute, restarting);
KMManager.resetContext(KeyboardType.KEYBOARD_TYPE_SYSTEM);
// This method (likely) includes the IME equivalent to `onResume` for `Activity`-based classes,
// making it an important time to detect orientation changes.
Context appContext = getApplicationContext();
int newOrientation = KMManager.getOrientation(appContext);
if(newOrientation != lastOrientation) {
lastOrientation = newOrientation;
Configuration newConfig = this.getResources().getConfiguration();
KMManager.onConfigurationChanged(newConfig);
}
// Temporarily disable predictions on certain fields (e.g. hidden password field or numeric)
int inputType = attribute.inputType;
inputType = attribute.inputType;
KMManager.setMayPredictOverride(inputType);
if (KMManager.getMayPredictOverride()) {
KMManager.setBannerOptions(false);
@ -207,6 +205,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
public void onStartInputView(EditorInfo attribute, boolean restarting) {
super.onStartInputView(attribute, restarting);
setInputView(onCreateInputView());
// Update the input type
inputType = attribute.inputType;
}
@Override
@ -228,6 +229,16 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
public void onComputeInsets(InputMethodService.Insets outInsets) {
super.onComputeInsets(outInsets);
// This method (likely) includes the IME equivalent to `onResume` for `Activity`-based classes,
// making it an important time to detect orientation changes.
Context appContext = getApplicationContext();
int newOrientation = KMManager.getOrientation(appContext);
if(newOrientation != lastOrientation) {
lastOrientation = newOrientation;
Configuration newConfig = this.getResources().getConfiguration();
KMManager.onConfigurationChanged(newConfig);
}
// We should extend the touchable region so that Keyman sub keys menu can receive touch events outside the keyboard frame
Point size = KMManager.getWindowSize(getApplicationContext());
@ -250,8 +261,6 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
if (exText != null)
exText = null;
}
// Initialize keyboard options
KMManager.sendOptionsToKeyboard();
}
@Override
@ -271,6 +280,11 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// Determine if Physical keystroke should be passed off
if (inputType == InputType.TYPE_NULL) {
return false; // Revert to default handling
}
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:

View file

@ -133,7 +133,6 @@ public class AdjustLongpressDelayActivity extends BaseActivity {
// Store the longpress delay as a reference
// and then update KeymanWeb with the longpress delay
KMManager.setLongpressDelay(currentDelayTimeMS);
KMManager.sendOptionsToKeyboard();
super.onBackPressed();
}

View file

@ -11,6 +11,7 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.AndroidRuntimeException;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@ -70,7 +71,13 @@ public class KMPBrowserActivity extends BaseActivity {
}
}
setContentView(R.layout.activity_kmp_browser);
try {
setContentView(R.layout.activity_kmp_browser);
} catch (AndroidRuntimeException e) {
// WebView not enabled
finish();
return;
}
webView = (WebView) findViewById(R.id.kmpBrowserWebView);
webView.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);

View file

@ -3,6 +3,7 @@ package com.tavultesoft.kmapro;
import android.os.Bundle;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.KMManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
@ -32,6 +33,9 @@ public class KeymanSettingsActivity extends BaseActivity {
}
innerFragment = (KeymanSettingsFragment) getSupportFragmentManager().findFragmentById(R.id.keyman_settings_fragment);
// For Keyman sites, disable keyboard picker task flag so keyboard picker doesn't dismiss Keyman app
KMManager.dontCloseParentAppOnShowKeyboardPicker();
}
@Override

View file

@ -43,7 +43,9 @@ import com.keyman.android.DownloadIntentService;
import com.keyman.engine.util.KMLog;
import com.keyman.engine.util.KMPLink;
import com.keyman.engine.util.KMString;
import com.keyman.engine.util.WebViewUtils;
import com.keyman.engine.util.WebViewUtils.EngineWebViewVersionStatus;
import com.keyman.engine.util.WebViewUtils.SystemWebViewStatus;
import android.Manifest;
import android.app.ProgressDialog;
@ -153,7 +155,9 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
KMManager.setDebugMode(true);
}
// Verify WebView installed and enabled before attempting to initialize KMManager
KMManager.initialize(getApplicationContext(), KeyboardType.KEYBOARD_TYPE_INAPP);
KMManager.executeResourceUpdate(this);
DefaultLanguageResource.install(context);
@ -181,8 +185,11 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
textView.setTextSize((float) textSize);
textView.setSelection(textView.getText().length());
// Use in-app keyboard WebView to check the Chrome version
checkChromeVersion(KMManager.getKMKeyboard(KeyboardType.KEYBOARD_TYPE_INAPP));
// Check WebView enabled and Chrome version
SystemWebViewStatus webViewStatus = WebViewUtils.getSystemWebViewStatus(context);
EngineWebViewVersionStatus chromeStatus = KMManager.getEngineWebViewVersionStatus();
updateWebViewIfNeeded(chromeStatus, webViewStatus);
CheckInstallReferrer.checkGooglePlayInstallReferrer(this, context);
checkGetStarted();
}
@ -240,6 +247,9 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
KMManager.onResume();
KMManager.hideSystemKeyboard();
// Reset keyboard picker Activity Task flag
KMManager.closeParentAppOnShowKeyboardPicker();
// onConfigurationChanged() only triggers when device is rotated while app is in foreground
// This handles when device is rotated while app is in background
// using KMManager.getOrientation() since getConfiguration().orientation is unreliable #10241
@ -475,8 +485,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
@Override
public void onKeyboardLoaded(KeyboardType keyboardType) {
// Initialize keyboard options
KMManager.sendOptionsToKeyboard();
// Do nothing
}
@Override
@ -770,39 +779,114 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
KMManager.setHapticFeedback(mayHaveHapticFeedback);
}
private void checkChromeVersion(WebView webView) {
if (KMManager.getEngineWebViewVersionStatus() != EngineWebViewVersionStatus.FULL) {
LinearLayout updateChromeLayout = (LinearLayout) findViewById(R.id.updateChromeLayout);
updateChromeLayout.setVisibility(View.VISIBLE);
Button updateChromeButton = (Button)findViewById(R.id.updateChromeButton);
updateChromeButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Launch PlayStore to update Chrome
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.android.chrome"));
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
} else {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=com.android.chrome"));
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
} else {
Toast.makeText(getApplicationContext(), getString(R.string.unable_to_open_browser), Toast.LENGTH_SHORT).show();
}
}
} catch (android.content.ActivityNotFoundException e) {
// Link to Chrome if user is not signed in to Play Store
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=com.android.chrome"));
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
} else {
Toast.makeText(getApplicationContext(), getString(R.string.unable_to_open_browser), Toast.LENGTH_SHORT).show();
}
}
}
});
/**
* Based on whether Chrome and WebView are installed & enabled,
* display the corresponding update panel
* @param chromeStatus
* @param webViewStatus
*/
private void updateWebViewIfNeeded(EngineWebViewVersionStatus chromeStatus, SystemWebViewStatus webViewStatus) {
if (chromeStatus == EngineWebViewVersionStatus.FULL && webViewStatus == SystemWebViewStatus.FULL) {
return;
}
if (webViewStatus == SystemWebViewStatus.NOT_INSTALLED) {
displayInstallWebView();
} else if (webViewStatus == SystemWebViewStatus.DISABLED) {
displayEnableWebView();
} else {
// Otherwise, display update Chrome
displayUpdateChrome();
}
}
private void displayInstallWebView() {
TextView textView = (TextView)findViewById((R.id.kmWebViewChromeTextView));
textView.setText(getString(R.string.body_install_webview));
Button button = (Button)findViewById(R.id.webViewChromeButton);
button.setText(getString(R.string.label_install_webview));
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Play Store link to install WebView
Intent intent = new Intent(Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=com.google.android.webview"));
// Launch intent
try {
PackageManager packageManager = context.getPackageManager();
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent);
}
} catch (android.content.ActivityNotFoundException e) {
Toast.makeText(getApplicationContext(), getString(R.string.unable_to_open_browser), Toast.LENGTH_SHORT).show();
}
}
});
LinearLayout webViewChromeLayout = (LinearLayout) findViewById(R.id.checkWebViewChromeLayout);
webViewChromeLayout.setVisibility(View.VISIBLE);
}
private void displayEnableWebView() {
TextView textView = (TextView)findViewById((R.id.kmWebViewChromeTextView));
textView.setText(getString(R.string.body_enable_webview));
Button button = (Button)findViewById(R.id.webViewChromeButton);
button.setText(getString(R.string.label_enable_webview));
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Application settings to enable WebView
Intent intent = new Intent("android.settings.APPLICATION_DETAILS_SETTINGS",
Uri.parse("package:com.google.android.webview"));
// Launch intent
try {
PackageManager packageManager = context.getPackageManager();
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent);
}
} catch (android.content.ActivityNotFoundException e) {
Toast.makeText(getApplicationContext(), getString(R.string.unable_to_open_browser), Toast.LENGTH_SHORT).show();
}
}
});
LinearLayout webViewChromeLayout = (LinearLayout) findViewById(R.id.checkWebViewChromeLayout);
webViewChromeLayout.setVisibility(View.VISIBLE);
}
private void displayUpdateChrome() {
// TextView's default string is to update Chrome
Button button = (Button)findViewById(R.id.webViewChromeButton);
button.setText(getString(R.string.button_update_chrome));
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Primary Play Store link to install/update Chrome
Intent intent = new Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=com.android.chrome"));
try {
PackageManager packageManager = context.getPackageManager();
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent);
} else {
// (alternate Play Store link to install/update Chrome)
intent = new Intent(Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=com.android.chrome"));
intent.resolveActivity(packageManager);
startActivity(intent);
}
} catch (android.content.ActivityNotFoundException e) {
Toast.makeText(getApplicationContext(), getString(R.string.unable_to_open_browser), Toast.LENGTH_SHORT).show();
}
}
});
LinearLayout webViewChromeLayout = (LinearLayout) findViewById(R.id.checkWebViewChromeLayout);
webViewChromeLayout.setVisibility(View.VISIBLE);
}
private Uri requestPermissionIntentUri;

View file

@ -11,7 +11,7 @@
<include layout="@layout/titlebar" />
<include layout="@layout/update_chrome_layout"
<include layout="@layout/check_chrome_webview_layout"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -22,7 +22,7 @@
android:background="@drawable/textview_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/updateChromeLayout"
android:layout_below="@id/checkWebViewChromeLayout"
android:ems="10"
android:inputType="textMultiLine|textNoSuggestions"
android:hint="@string/textview_hint"

View file

@ -1,14 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/updateChromeLayout"
android:id="@+id/checkWebViewChromeLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:orientation="horizontal">
<!-- Banner to display if old version of Chrome installed -->
<!-- Banner to display if WebView is not installed/enabled or old version of Chrome installed -->
<TextView
android:id="@+id/kmUpgradeChromeTextView"
android:id="@+id/kmWebViewChromeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
@ -20,7 +20,7 @@
android:textSize="@dimen/update_chrome_label_textsize" />
<Button
android:id="@+id/updateChromeButton"
android:id="@+id/webViewChromeButton"
style="@style/Widget.AppCompat.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -16,7 +16,7 @@
<!-- Context: Menu Action -->
<string name="action_settings" comment="Menu action to open Settings menu">Ρυθμίσεις</string>
<!-- Context: Menu Action -->
<string name="action_install_updates" comment="Menu notification that keyboard or dictionary updates available">Εγκατάσταση Ενημερώσεων</string>
<string name="action_install_updates" comment="Menu notification that keyboard or dictionary updates available">Ἐγκατάσταση Ἐνημερώσεων</string>
<!-- Context: Title -->
<string name="title_version" comment="Title of Keyman for Android version">Ἔκδοση %1$s</string>
<!-- Context: Text label when old Chrome version installed -->
@ -38,11 +38,11 @@
<!-- Context: Clear Text dialog -->
<string name="all_text_will_be_cleared" comment="Erase all the text">\nΤὸ κείμενο θὰ ἐκκαθαρισθεῖ πλήρως\n</string>
<!-- Context: Get Started menu -->
<string name="get_started" comment="Menu for getting started">Μικρύνετε τὸ κείμενο</string>
<string name="get_started" comment="Menu for getting started">Ξεκινῆστε</string>
<!-- Context: Get Started menu -->
<string name="add_a_keyboard" comment="Menu item to add a keyboard">Προσθέστε πληκτρολόγιο γιὰ τὴν γλῶσσα σας</string>
<!-- Context: Get Started menu -->
<string name="enable_system_keyboard" comment="Menu item to enable Keyman as system keyboard">Ὁρίστε τὸ Κῆμαν ὡς παν-συστημικὸ πληκτρολόγιο</string>
<string name="enable_system_keyboard" comment="Menu item to enable Keyman as system keyboard">Ὁρίστε τὸ Keyman ὡς πληκτρολόγιο συστήματος</string>
<!-- Context: Get Started menu -->
<string name="set_keyman_as_default" comment="Menu item to set Keyman as default system keyboard">Ὁρίστε τὸ Κῆμαν ὡς προεπιλεγμένο πληκτρολόγιο</string>
<!-- Context: Get Started menu -->
@ -68,6 +68,8 @@
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">Μεταβολὴ ὕψους πληκτρολογίου</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_longpress_delay" comment="Menu action to adjust longpress delay duration">Καθυστέρηση παρατεταμένου πατήματος</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">Λεζάντα πλήκτρου διαστήματος</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_keyboard" comment="Spacebar caption option - keyboard">Πληκτρολόγιο</string>
@ -87,11 +89,11 @@
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">Καμμία λεζάντα στὸ πλῆκτρο διαστήματος</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">Δὸνηση κατὰ τὴν πληκτρολόγηση</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner" comment="text suggestions banner">Νὰ ἐμφανίζεται πάντα banner</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_on" comment="Description when toggle is on">Πρὸς ὑλοποίησιν</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_off" comment="Description when toggle is off">OFF: ἐμφάνιση μόνον ὅταν ἔχει ἐνεργοποιηθεῖ ἡ πρόβλεψη κειμένου</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report" comment="permission for sending crash reports">Ἐπιτρέψτε τὴν ἀποστολὴ ἀναφορῶν κατάρρευσης μέσῳ δικτύου</string>
@ -123,6 +125,14 @@
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Περιστρέψτε τὴν συσκευὴ γιὰ προσαρμογὴ τοῦ ὕψους σὲ λειτουργία πορτραίτου καὶ τοπίου</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Ἐπαναφέρετε τὶς προεπιλεγμένες ρυθμίσεις</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Χρόνος καθυστέρησης: %1$.1f δευτ</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_up" comment="Make longpress delay longer">Μεγαλύτερος χρόνος καθυστέρησης</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_down" comment="Make longpress delay shorter">Μικρότερος χρόνος καθυστέρησης</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_longpress_delay_slider" comment="Adjust longpress delay time">Ρυθμιστής καθυστέρησης πατήματος</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_text" comment="Prompt to type in the browser search bar">Ἀναζητῆστε ἢ πληκτρολογῆστε URL</string>
<!-- Context: Keyman Web Browser -->

View file

@ -148,7 +148,7 @@
<!-- Context: KMP Package strings -->
<string name="no_new_predictive_text_to_install" comment="Notification when no dictionaries can be installed">No new predictive text to install</string>
<!-- Context: KMP Package strings -->
<string name="no_targets_to_install" comment="Notification when a package doesn't contain keyboards or dictionaries">No keyboards or predictive text to install</string>
<string name="no_targets_to_install" comment="Notification when a package doesn't contain keyboards or dictionaries">This is not a valid Keyman keyboard or predictive text package.</string>
<!-- Context: KMP Package strings -->
<string name="no_associated_languages" comment="Notification when a keyboard package doesn't contain languages">Keyboard package has no associated languages to install</string>
<!-- Context: KMP Package strings -->

View file

@ -67,6 +67,8 @@
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">ချိၚ်ဆသမၠုၚ်ကဳဗုဒ်ညိ</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_longpress_delay" comment="Menu action to adjust longpress delay duration">ချိၚ်အခိၚ် ဍဵုလအ်အ်</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">က္တိုပ်လိက် Spacebar</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_keyboard" comment="Spacebar caption option - keyboard">ကဳဗုဒ်</string>
@ -87,11 +89,11 @@
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">လ္ပဓမံက်ထ္ၜးမလိက်ပ္ဍဲspacebarညိ</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">ပ္ဍဲကာလမတက်လိက် ချဳဒတဝ်ညိ</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner" comment="text suggestions banner">ဓမံက်ထ္ၜးbannerလၟိုန်ညိ</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_on" comment="Description when toggle is on">သွက်ဂွံဓမံက်ရုပ်ရဴ</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_off" comment="Description when toggle is off">ကၟာတ်လဝ်မ္ဂး မလိက်ချိၚ်ဆလဝ်တံဂှ် ထ္ၜးအဃောပံက်လဝ်ဟေၚ်</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report" comment="permission for sending crash reports">ကဵုအခေါၚ် ပၠံၚ်လိက်စဳရေၚ်လ္တူ အေန်တာနက်ညိ</string>
@ -123,6 +125,14 @@
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">ဂွံချိၚ် လံက်သတိက် လံက်ဇမၠိၚ်ဂှ် ဗဂေတ်ကဵုစက်ပိုဲညိ။</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">ကလေၚ်သ္ပနူပကတိ</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">အခိၚ်ဖအိၚ်: %1$.1f စက္က</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_up" comment="Make longpress delay longer">ဖအိၚ်အခိၚ်လအ်အ်</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_down" comment="Make longpress delay shorter">ဖအိၚ်အခိၚ်အောန်န်</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_longpress_delay_slider" comment="Adjust longpress delay time">ဍဵုလအ်အ်သွက် မသရေတ််ဖအိၚ်အခိၚ်</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_text" comment="Prompt to type in the browser search bar">ဂၠာဲ ဟွံသေၚ် တက် URL</string>
<!-- Context: Keyman Web Browser -->

View file

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- app_name removed in 16.0 beta. Set in build.gradle -->
<!-- Context: Menu Action -->
<string name="action_share" comment="Menu action to send text content to another app">Chia sẻ</string>
<!-- Context: Menu Action -->
<string name="action_web" comment="Menu action to open Keyman browser">Trình duyệt</string>
<!-- Context: Menu Action -->
<string name="action_text_size" comment="Menu action to adjust text size">Cỡ chữ</string>
<!-- Context: Menu Action -->
<string name="action_overflow" comment="Show additional menu items">Thêm</string>
<!-- Context: Menu Action -->
<string name="action_clear_text" comment="Menu action to erase text">Xóa văn bản</string>
<!-- Context: Menu Action -->
<string name="action_info" comment="Menu action to display Keyman help page">Thông tin</string>
<!-- Context: Menu Action -->
<string name="action_settings" comment="Menu action to open Settings menu">Cài đặt</string>
<!-- Context: Menu Action -->
<string name="action_install_updates" comment="Menu notification that keyboard or dictionary updates available">Cài đặt Cập nhật</string>
<!-- Context: Title -->
<string name="title_version" comment="Title of Keyman for Android version">Phiên bản: %1$s</string>
<!-- Context: Text label when old Chrome version installed -->
<string name="text_require_chrome_57" comment="Banner text when old version of Chrome is installed">Keyman cần trình duyệt Chrome phiên bản 57 hoặc mới hơn.</string>
<!-- Context: Button label -->
<string name="button_update_chrome" comment="Prompt to upgrade Chrome in the Play Store">Cập nhật Chrome</string>
<!-- Context: Textfield prompt (&#8230; is the ellipsis character "...") -->
<string name="textview_hint" comment="Prompt to start typing">Bắt đầu nhập tại đây&#8230;</string>
<!-- Context: Splash screen (version/copyright info currently disabled -->
<!-- Context: Splash screen (version/copyright info currently disabled -->
<!-- Context: Text Size dialog -->
<string name="text_size" comment="Current text size (number)">Cỡ chữ: %1$d</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_up" comment="Make text bigger">Tăng cỡ chữ</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_down" comment="Make text smaller">Giảm cỡ chữ</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_slider" comment="Adjust text size">Thanh trượt kích thước văn bản</string>
<!-- Context: Clear Text dialog -->
<string name="all_text_will_be_cleared" comment="Erase all the text">\nTất cả văn bản sẽ bị xóa\n</string>
<!-- Context: Get Started menu -->
<string name="get_started" comment="Menu for getting started">Bắt đầu</string>
<!-- Context: Get Started menu -->
<string name="add_a_keyboard" comment="Menu item to add a keyboard">Thêm bàn phím cho ngôn ngữ của bạn</string>
<!-- Context: Get Started menu -->
<string name="enable_system_keyboard" comment="Menu item to enable Keyman as system keyboard">Đặt Keyman làm bàn phím hệ thống</string>
<!-- Context: Get Started menu -->
<string name="set_keyman_as_default" comment="Menu item to set Keyman as default system keyboard">Đặt Keyman làm bàn phím mặc định</string>
<!-- Context: Get Started menu -->
<string name="more_info" comment="Open the Keyman for Android help page">Thêm thông tin</string>
<!-- Context: Get Started menu -->
<string name="show_get_started" comment="Show the &quot;Get Started&quot; menu on startup">Hiển thị \"%1$s\" khi khởi động</string>
<!-- Context: Android Storage Permission -->
<string name="request_storage_permission" comment="Request storage permission to access keyboard packages">Để cài đặt bàn phím, hãy cho phép Keyman đọc bộ nhớ ngoài</string>
<!-- Context: Android Storage Permission -->
<string name="storage_permission_denied" comment="Keyboard package installation may fail since Android storage permission not granted">Yêu cầu cho phép bộ nhớ đã bị từ chối. Có khả năng không thể cài đặt bàn phím</string>
<string name="storage_permission_denied2" comment="Keyboard package installation failed. Recommend install from local file"> Yêu cầu cấp quyền bộ nhớ không thành công. Hãy vào cài đặt trong Keyman - Cài đặt từ tệp cục bộ</string>
<!-- Context: Keyman Settings menu -->
<string name="keyman_settings" comment="Settings menu title">Cài đặt</string>
<!-- Context: Keyman Settings menu -->
<plurals name="installed_languages">
<item quantity="other">Ngôn ngữ đã cài đặt (%1$d)</item>
</plurals>
<!-- Context: Keyman Settings menu -->
<string name="install_keyboard_or_dictionary" comment="Menu item to install keyboard or dictionary">Cài đặt Bàn phím hay Từ điển</string>
<!-- Context: Keyman Settings menu -->
<string name="change_display_language" comment="Menu action to change interface language">Ngôn ngữ hiển thị</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">Điều chỉnh độ cao bàn phím</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_longpress_delay" comment="Menu action to adjust longpress delay duration">Điều chỉnh độ trễ khi nhấn lâu</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">Tên phím khoảng cách</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_keyboard" comment="Spacebar caption option - keyboard">Bàn phím</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_language" comment="Spacebar caption option - language">Ngôn ngữ</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_language_keyboard" comment="Spacebar caption option - language + keyboard">Ngôn ngữ + Bàn phím</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_blank" comment="Spacebar caption option - blank">Trống</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_keyboard" comment="Spacebar caption hint - keyboard">Hiển thị tên bàn phím trên phím cách</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_language" comment="Spacebar caption hint - language">Hiển thị tên ngôn ngữ trên phím cách</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_language_keyboard" comment="Spacebar caption hint - language + keyboard">Ngôn ngữ và bàn phím trên phím cách</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">Không hiển thị gì trên phím cách</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">Rung khi đánh máy</string>
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner" comment="text suggestions banner">Luôn hiển thị dòng biểu ngữ</string>
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_on" comment="Description when toggle is on">Sắp được cài đặt</string>
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_off" comment="Description when toggle is off">Khi tắt, chỉ hiển thị khi bật tính năng dự đoán chữ</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report" comment="permission for sending crash reports">Cho phép gửi báo cáo vấn đề qua mạng</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report_on" comment="Description when toggle is on">Khi mở, báo cáo vấn đề sẽ được gửi đi</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report_off" comment="Description when toggle is off">Khi tắt, báo cáo vấn đề sẽ không được gửi đi</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="install_from_keyman_dot_com" comment="Install package from Keyman server">Cài đặt từ keyman.com</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="install_from_local_file" comment="Install package file from local device">Cài đặt từ tệp cục bộ</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="install_from_other_device" comment="Scan QR Code">Cài đặt từ thiết bị khác</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="add_languages_to_installed_keyboard" comment="Install additional languages from a keyboard package">Thêm ngôn ngữ cho bàn phím đã cài đặt</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="add_language_subtext" comment="Additional information for adding another language">(từ gói bàn phím)</string>
<!-- Context: Select Package and Select Languages menu -->
<string name="title_select_keyboard_package_list" comment="Select a keyboard package to add languages">Chọn Gói bàn phím</string>
<!-- Context: Select Package and Select Languages menu -->
<string name="title_select_languages_for_package" comment="Select language names from the keyboard package">Chọn ngôn ngữ cho %1$s</string>
<!-- Context: Select Package and Select Languages menu -->
<string name="added_language_to_keyboard" comment="Notification that a language name is added to a keyboard">Đã thêm ngôn ngữ \'%1$s\' vào %2$s</string>
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Tất cả ngôn ngữ đã được cài đặt rồi</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Kéo bàn phím để điều chỉnh độ cao</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Xoay thiết bị để điều chỉnh độ ngang độ dọc</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Khôi phục mặc định</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Thời gian trễ: %1$.1f giây</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_up" comment="Make longpress delay longer">Thời gian trễ lâu hơn</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_down" comment="Make longpress delay shorter">Thời gian trễ thấp hơn</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_longpress_delay_slider" comment="Adjust longpress delay time">Thanh trượt thời gian trễ nhấn lâu</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_text" comment="Prompt to type in the browser search bar">Tìm kiếm hoặc nhập URL</string>
<!-- Context: Keyman Web Browser -->
<string name="title_bookmarks" comment="Title for bookmarks">Dấu trang</string>
<!-- Context: Keyman Web Browser -->
<string name="no_bookmarks" comment="Text when bookmark list is empty">Không có dấu trang</string>
<!-- Context: Keyman Web Browser -->
<string name="add_bookmark" comment="Confirmation to add bookmark">Thêm dấu trang</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_title" comment="Page title for saved bookmark">Tiêu đề</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_url" comment="Page URL for saved bookmark">url</string>
<!-- Context: KMP Package strings -->
<string name="title_package_failed_to_install" comment="Error dialog title when package failed to install">Cài đặt gói %1$s không thành công</string>
<!-- Context: KMP Package strings -->
<string name="downloading_keyboard_package" comment="Confirmation to download keyboard package filename">Đang tải xuống gói bàn phím \n%1$s&#8230;</string>
<!-- Context: KMP Package strings -->
<string name="failed_to_extract" comment="Notification that keyboard package failed to unzip">Giải nén thất bại</string>
<!-- Context: KMP Package strings -->
<string name="install_keyboard_package" comment="Title to install keyboard package">Cài đặt bàn phím</string>
<!-- Context: KMP Package strings -->
<string name="install_predictive_text_package" comment="Title to install dictionary package">Cài đặt từ điển</string>
<!-- Context: KMP Package strings -->
<string name="not_valid_package_file" comment="Notification when invalid package file cannot be installed">%1$s không phải tệp gói bàn phím Keyman hợp lệ.\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">Gói bàn phím không có bàn phím được tối ưu hóa để cài đặt</string>
<!-- Context: KMP Package strings -->
<string name="no_new_predictive_text_to_install" comment="Notification when no dictionaries can be installed">Không có văn bản dự đoán mới để cài đặt</string>
<!-- Context: KMP Package strings -->
<string name="no_targets_to_install" comment="Notification when a package doesn't contain keyboards or dictionaries">Không có bàn phím hay văn bản dự đoán để cài đặt</string>
<!-- Context: KMP Package strings -->
<string name="no_associated_languages" comment="Notification when a keyboard package doesn't contain languages">Gói bàn phím không có ngôn ngữ liên quan để cài đặt</string>
<!-- Context: KMP Package strings -->
<string name="invalid_metadata" comment="kmp.json metadata file is invalid or missing in package">Siêu dữ liệu trong gói không hợp lệ/bị thiếu</string>
<!-- Context: KMP Package strings -->
<string name="minimum_keyboard_version_not_supported" comment="Notification when keyboard has functionality not supported by current Keyman">Bàn phím cần phiên bản mới hơn của ứng dụng Keyman</string>
<!-- Context: anywhere -->
<string name="unable_to_open_browser" comment="Notification when a browser activity cannot be launched">Không thể mở trình duyệt</string>
</resources>

View file

@ -27,7 +27,6 @@
<!-- Context: Menu Action -->
<string name="action_install_updates" comment="Menu notification that keyboard or dictionary updates available">Install Updates</string>
<!-- Context: Title -->
<string name="title_version" comment="Title of Keyman for Android version">Version: %1$s</string>

View file

@ -7,6 +7,11 @@
</intent>
</queries>
<queries>
<package android:name="com.android.webkit" />
<package android:name="com.google.android.ims" />
</queries>
<!-- https://developer.android.com/develop/ui/views/notifications/notification-permission#declare -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />

View file

@ -13,6 +13,7 @@ var bannerImagePath = '';
var bannerHTMLContents = '';
var fragmentToggle = 0;
var deferredBannerCall;
var longpressDelay = null;
var sentryManager = new KeymanSentryManager({
hostPlatform: "android"
@ -71,6 +72,10 @@ function init() {
keyman.refreshOskLayout();
}
// Initialize the longpress delay
if(longpressDelay !== null) {
setLongpressDelay(longpressDelay);
}
});
keyman.addEventListener('keyboardloaded', setIsChiral);
@ -128,11 +133,10 @@ function notifyHost(event, params) {
// Update the KeymanWeb longpress delay
// delay is in milliseconds
function setLongpressDelay(delay) {
if (keyman.osk) {
longpressDelay = delay;
if (keyman && keyman.osk) {
keyman.osk.gestureParams.longpress.waitLength = delay;
console_debug('setLongpressDelay('+delay+')');
} else {
window.console.log('setLongpressDelay error: keyman.osk undefined');
}
}

View file

@ -69,6 +69,7 @@ public class DisplayLanguages {
new DisplayLanguageType("ru-RU", "Pyccĸий (Russian)"),
new DisplayLanguageType("shu-latn", "Shuwa (Latin)"),
new DisplayLanguageType("sv-SE", "svenska (Swedish)"),
new DisplayLanguageType("vi-VN", "Tiếng Việt (Vietnamese)"),
new DisplayLanguageType("uk-UA", "Українська (Ukrainian)"),
new DisplayLanguageType("hia-NG", "Waha"),
new DisplayLanguageType("zh-CN", "中文(简体) (Simplified Chinese)")

View file

@ -32,6 +32,7 @@ import android.content.res.Configuration;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.AndroidRuntimeException;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.GestureDetector;
@ -110,14 +111,29 @@ final class KMKeyboard extends WebView {
public String specialOskFont = "";
public KMKeyboard(Context context) {
/**
* Constructor for KMKeyboard.
* If System WebView not installed/enabled, will throw AndroidRuntimeException.
* Can't catch the exception here because super(context) must be the first line.
* @param context
* @throws AndroidRuntimeException
*/
public KMKeyboard(Context context) throws AndroidRuntimeException {
super(context);
this.context = context;
this.keyboardType = KeyboardType.KEYBOARD_TYPE_INAPP;
initKMKeyboard(context);
}
public KMKeyboard(Context context, KeyboardType keyboardType) {
/**
* Constructor for KMKeyboard.
* If System WebView not installed/enabled, will throw AndroidRuntimeException.
* Can't catch the exception here because super(context) must be the first line.
* @param context {Context}
* @param keyboardType {KeyboardType}
* @throws AndroidRuntimeException
*/
public KMKeyboard(Context context, KeyboardType keyboardType) throws AndroidRuntimeException {
super(context);
this.context = context;
this.keyboardType = keyboardType;

View file

@ -72,7 +72,7 @@ public class KMKeyboardDownloaderActivity extends BaseActivity {
//TODO: move to keyboard manager class
private static ArrayList<KeyboardEventHandler.OnKeyboardDownloadEventListener> kbDownloadEventListeners = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

View file

@ -119,7 +119,9 @@ public class KMKeyboardJSHandler {
InputConnection ic = KMManager.getInputConnection(k.keyboardType);
if (ic == null) {
KMLog.LogError(TAG, "insertText failed: InputConnection is null");
// current active InputConnection is no longer bound to the Keyman Engine input method
// (user switched to another IME) so disregard the keystroke.
// https://developer.android.com/reference/android/inputmethodservice/InputMethodService#getCurrentInputConnection()
return;
}
@ -273,7 +275,9 @@ public class KMKeyboardJSHandler {
InputConnection ic = KMManager.getInputConnection(k.keyboardType);
if (ic == null) {
KMLog.LogError(TAG, "dispatchKey failed: InputConnection is null");
// current active InputConnection is no longer bound to the Keyman Engine input method
// (user switched to another IME) so disregard the keystroke.
// https://developer.android.com/reference/android/inputmethodservice/InputMethodService#getCurrentInputConnection()
return;
}

View file

@ -33,6 +33,7 @@ import android.net.NetworkInfo;
import android.os.Build;
import android.os.IBinder;
import android.text.InputType;
import android.util.AndroidRuntimeException;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
@ -73,6 +74,7 @@ import com.keyman.engine.util.KMLog;
import com.keyman.engine.util.KMString;
import com.keyman.engine.util.MapCompat;
import com.keyman.engine.util.WebViewUtils;
import com.keyman.engine.util.WebViewUtils.SystemWebViewStatus;
import org.json.JSONArray;
import org.json.JSONException;
@ -687,12 +689,33 @@ public final class KMManager {
KMKeyboardWebViewClient webViewClient = null;
if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard == null) {
InAppKeyboard = new KMKeyboard(appContext, KeyboardType.KEYBOARD_TYPE_INAPP);
try {
InAppKeyboard = new KMKeyboard(appContext, KeyboardType.KEYBOARD_TYPE_INAPP);
} catch (AndroidRuntimeException e) {
// Catch fatal error when WebView not installed/enabled
// Only log exceptions unrelated to WebView
if (e != null && !e.getMessage().contains("MissingWebViewPackageException")) {
KMLog.LogException(TAG, "initKeyboard for INAPP" , e);
}
return;
}
InAppKeyboardWebViewClient = new KMKeyboardWebViewClient(appContext, keyboardType);
keyboard = InAppKeyboard;
webViewClient = InAppKeyboardWebViewClient;
} else if (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard == null) {
SystemKeyboard = new KMKeyboard(appContext, KeyboardType.KEYBOARD_TYPE_SYSTEM);
try {
SystemKeyboard = new KMKeyboard(appContext, KeyboardType.KEYBOARD_TYPE_SYSTEM);
} catch (AndroidRuntimeException e) {
// Catch fatal error when WebView not installed/enabled
// Only log exceptions unrelated to WebView
if (e != null && !e.getMessage().contains("MissingWebViewPackageException")) {
KMLog.LogException(TAG, "initKeyboard for SYSTEM", e);
}
// Direct user to pick another system keyboard
InputMethodManager imManager = (InputMethodManager) appContext.getSystemService(Context.INPUT_METHOD_SERVICE);
imManager.showInputMethodPicker();
return;
}
SystemKeyboardWebViewClient = new KMKeyboardWebViewClient(appContext, keyboardType);
keyboard = SystemKeyboard;
webViewClient = SystemKeyboardWebViewClient;
@ -722,6 +745,7 @@ public final class KMManager {
}
keyboard.setBanner(KMManager.BannerType.HTML);
keyboard.showBanner(true);
sendLongpressDelay();
}
setEngineWebViewVersionStatus(appContext, keyboard);
}
@ -768,6 +792,24 @@ public final class KMManager {
}
}
/**
* Normally, launching the KeyboardPickerActivity from a system keyboard closes the parent app
* (The keyboard picker becomes the root of the activity stack)
* Calling this function keeps the parent app running in the background
*/
public static void dontCloseParentAppOnShowKeyboardPicker() {
KeyboardPickerActivity.dontCloseParentAppOnShowKeyboardPicker();
}
/**
* Normally, launching the KeyboardPickerActivity from a system keyboard closes the parent app
* (The keyboard picker becomes the root of the activity stack)
* Calling this function restores this default behavior
*/
public static void closeParentAppOnShowKeyboardPicker() {
KeyboardPickerActivity.closeParentAppOnShowKeyboardPicker();
}
@SuppressLint("InflateParams")
public static View createInputView(InputMethodService inputMethodService) {
//final Context context = appContext;
@ -776,6 +818,10 @@ public final class KMManager {
final FrameLayout mainLayout = new FrameLayout(appContext);
mainLayout.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
if (SystemKeyboard == null) {
return mainLayout;
}
RelativeLayout keyboardLayout = new RelativeLayout(appContext);
keyboardLayout.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
ViewGroup parent = (ViewGroup) SystemKeyboard.getParent();
@ -921,7 +967,7 @@ public final class KMManager {
copyAsset(context, KMFilename_KmwCss, "", true);
copyAsset(context, KMFilename_KmwGlobeHintCss, "", true);
copyAsset(context, KMFilename_Osk_Ttf_Font, "", true);
// Needed until our minimum version of Chrome is 61.0+.
copyAsset(context, KMFilename_JSPolyfill2, "", true);
@ -2124,7 +2170,7 @@ public final class KMManager {
}
/**
* Set the longpress delay (in milliseconds) as a stored preference.
* Set the longpress delay (in milliseconds) as a stored preference and sends to KeymanWeb.
* Valid range is 300 ms to 1500 ms. Returns true if the preference is successfully stored.
* @param longpressDelay - int longpress delay in milliseconds
* @return boolean
@ -2140,17 +2186,19 @@ public final class KMManager {
editor.putInt(KMKey_LongpressDelay, longpressDelay);
editor.commit();
// Send longpress delay to KeymanWeb
sendLongpressDelay();
return true;
}
/**
* Sends options to the KeymanWeb keyboard.
* Sends longpress delay to the KeymanWeb keyboard.
* 1. number of milliseconds to trigger a longpress gesture.
* This method requires a keyboard to be loaded for the value to take effect.
*/
public static void sendOptionsToKeyboard() {
private static void sendLongpressDelay() {
int longpressDelay = getLongpressDelay();
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) {
if (InAppKeyboard != null) {
InAppKeyboard.loadJavascript(KMString.format("setLongpressDelay(%d)", longpressDelay));
}
@ -2270,7 +2318,10 @@ public final class KMManager {
if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
if (KeyboardPickerActivity.getCanCloseParentAppOnShowKeyboardPicker()) {
// Keyboard Picker Activity becomes root activity and clears Keyman app
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
}
i.putExtra(KMKey_DisplayKeyboardSwitcher, kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM);
@ -2301,9 +2352,9 @@ public final class KMManager {
public static boolean updateText(KeyboardType kbType, String text) {
boolean result = false;
if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) {
if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) {
return InAppKeyboard.updateText(text);
} else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
} else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) {
return SystemKeyboard.updateText(text);
}
@ -2335,13 +2386,13 @@ public final class KMManager {
public static boolean updateSelectionRange(KeyboardType kbType) {
boolean result = false;
if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) {
if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) {
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP) && !InAppKeyboard.shouldIgnoreSelectionChange()) {
result = InAppKeyboard.updateSelectionRange();
}
InAppKeyboard.setShouldIgnoreSelectionChange(false);
} else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
} else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) {
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && !SystemKeyboard.shouldIgnoreSelectionChange()) {
result = SystemKeyboard.updateSelectionRange();
}
@ -2354,11 +2405,11 @@ public final class KMManager {
public static void resetContext(KeyboardType kbType) {
if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) {
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) {
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP) && InAppKeyboard != null) {
InAppKeyboard.loadJavascript("resetContext()");
}
} else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)) {
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && SystemKeyboard != null) {
SystemKeyboard.loadJavascript("resetContext()");
}
}

View file

@ -119,7 +119,7 @@ public final class KMTextView extends AppCompatEditText {
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
activeView = v;
if (KMManager.InAppKeyboardWebViewClient.getKeyboardLoaded()) {
if (KMManager.InAppKeyboard != null && KMManager.InAppKeyboardWebViewClient.getKeyboardLoaded()) {
updateTextContext();
}
showKeyboard();
@ -165,7 +165,7 @@ public final class KMTextView extends AppCompatEditText {
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
if (activeView != null && activeView.equals(this)) {
if (KMManager.updateSelectionRange(KMManager.KeyboardType.KEYBOARD_TYPE_INAPP)) {
if (KMManager.InAppKeyboard != null && KMManager.updateSelectionRange(KMManager.KeyboardType.KEYBOARD_TYPE_INAPP)) {
KMManager.resetContext(KeyboardType.KEYBOARD_TYPE_INAPP);
}
}
@ -176,7 +176,7 @@ public final class KMTextView extends AppCompatEditText {
AppCompatActivity activity = (AppCompatActivity)context;
Window mainWindow = activity.getWindow();
if (hasWindowFocus) {
if (hasWindowFocus && KMManager.InAppKeyboard != null) {
KMManager.InAppKeyboardWebViewClient.setContext(context);
activeView = mainWindow.getCurrentFocus();

View file

@ -81,6 +81,14 @@ public final class KeyboardPickerActivity extends BaseActivity {
}
private boolean dismissOnSelect = true;
/*
// When the System Keyboard launches the keyboard picker while this flag is set,
// the intent includes Intent.FLAG_ACTIVITY_CLEAR_TASK which clears the activity stack.
// Disable this to keep the keyboard picker from clearing the parent app.
*/
private static boolean canCloseParentAppOnShowKeyboardPicker = true;
protected static boolean canAddNewKeyboard = true;
protected static boolean canRemoveKeyboard = true;
protected static boolean shouldCheckKeyboardUpdates = true;
@ -474,6 +482,32 @@ public final class KeyboardPickerActivity extends BaseActivity {
notifyKeyboardsUpdate(context);
}
/**
* Returns canCloseParentAppOnShowKeyboardPicker
* @return boolean
*/
public static boolean getCanCloseParentAppOnShowKeyboardPicker() {
return canCloseParentAppOnShowKeyboardPicker;
}
/**
* Normally, launching the KeyboardPickerActivity from a system keyboard closes the parent app
* (The keyboard picker becomes the root of the activity stack)
* Calling this function keeps the parent app running in the background
*/
protected static void dontCloseParentAppOnShowKeyboardPicker() {
canCloseParentAppOnShowKeyboardPicker = false;
}
/**
* Normally, launching the KeyboardPickerActivity from a system keyboard closes the parent app
* (The keyboard picker becomes the root of the activity stack)
* Calling this function restores this default behavior
*/
protected static void closeParentAppOnShowKeyboardPicker() {
canCloseParentAppOnShowKeyboardPicker = true;
}
/**
* getModelIDFromPosition - Get the lexical model ID at a given position
* @param context

View file

@ -5,11 +5,15 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;
import com.keyman.engine.R;
import com.keyman.engine.util.KMLog;
import com.keyman.engine.util.DownloadFileUtils;
import java.io.File;
import java.util.HashMap;
@ -206,12 +210,34 @@ public class CloudDownloadMgr{
* @param params the cloud api params for download
* @param <ModelType> the target models type
* @param <ResultType> the cloud requests result type
* @return `false` if the download request cannot be executed; `true` otherwise.
*/
public <ModelType,ResultType> void executeAsDownload(Context aContext, String aDownloadIdentifier,
public <ModelType,ResultType> boolean executeAsDownload(Context aContext, String aDownloadIdentifier,
ModelType aTargetModel,
ICloudDownloadCallback<ModelType,ResultType> aCallback,
CloudApiTypes.CloudApiParam... params)
{
CloudApiTypes.CloudApiParam... params) {
try {
executeAsDownloadInternal(aContext, aDownloadIdentifier, aTargetModel, aCallback, params);
} catch (DownloadManagerDisabledException e) {
Toast.makeText(aContext,
aContext.getString(R.string.update_check_download_manager_disabled),
Toast.LENGTH_SHORT).show();
return false;
} catch (Exception e) {
Toast.makeText(aContext,
aContext.getString(R.string.update_check_unavailable),
Toast.LENGTH_SHORT).show();
KMLog.LogException(TAG, "Unexpected exception occurred during download/query attempt", e);
return false;
}
return true;
}
private <ModelType,ResultType> void executeAsDownloadInternal(Context aContext, String aDownloadIdentifier,
ModelType aTargetModel,
ICloudDownloadCallback<ModelType,ResultType> aCallback,
CloudApiTypes.CloudApiParam... params) throws DownloadManagerDisabledException {
if(!isInitialized) {
Log.w(TAG, "DownloadManager not initialized. Initializing CloudDownloadMgr.");
initialize(aContext);
@ -219,16 +245,22 @@ public class CloudDownloadMgr{
KMLog.LogBreadcrumb("CloudDownloadMgr", "CloudDownloadMgr.executeAsDownload() called; already initialized", true);
}
DownloadManager downloadManager = DownloadFileUtils.getDownloadManager(aContext);
if(downloadManager == null) {
// The callback object provided to us provides no way to directly signal a
// failure. That said, we can also immediately detect that we WILL fail and
// corresponding error _now_, rather than later.
//
// Unique custom error so it's easy to explicitly filter.
throw new DownloadManagerDisabledException();
}
synchronized (downloadSetByDownloadIdentifier) {
if (alreadyDownloadingData(aDownloadIdentifier) || params == null) {
return;
}
DownloadManager downloadManager = (DownloadManager) aContext.getSystemService(Context.DOWNLOAD_SERVICE);
if(downloadManager==null)
throw new IllegalStateException("DownloadManager is not available");
aCallback.initializeContext(aContext);
CloudApiTypes.CloudDownloadSet<ModelType,ResultType> _downloadSet =

View file

@ -0,0 +1,7 @@
package com.keyman.engine.cloud;
public class DownloadManagerDisabledException extends RuntimeException {
DownloadManagerDisabledException() {
super("System service DownloadManager is not available and cannot facilitate downloads or queries.");
}
}

View file

@ -111,8 +111,8 @@ public class CloudLexicalModelMetaDataDownloadCallback implements ICloudDownload
BaseActivity.makeToast(aContext, R.string.dictionary_download_start_in_background, Toast.LENGTH_SHORT);
CloudDownloadMgr.getInstance().executeAsDownload(aContext,
_r.additionalDownloadid, null, _callback,
CloudDownloadMgr.getInstance().executeAsDownload(
aContext, _r.additionalDownloadid, null, _callback,
_r.additionalDownloads.toArray(new CloudApiTypes.CloudApiParam[0]));
}
}

View file

@ -13,12 +13,14 @@ import com.keyman.engine.KMManager;
import com.keyman.engine.KeyboardPickerActivity;
import com.keyman.engine.R;
import com.keyman.engine.cloud.CloudApiTypes;
import com.keyman.engine.cloud.DownloadManagerDisabledException;
import com.keyman.engine.cloud.impl.CloudCatalogDownloadCallback;
import com.keyman.engine.cloud.impl.CloudCatalogDownloadReturns;
import com.keyman.engine.cloud.CloudDataJsonUtil;
import com.keyman.engine.cloud.CloudDownloadMgr;
import com.keyman.engine.packages.JSONUtils;
import com.keyman.engine.util.BCP47;
import com.keyman.engine.util.DownloadFileUtils;
import com.keyman.engine.util.KMLog;
import com.keyman.engine.util.VersionUtils;
@ -366,6 +368,10 @@ public class CloudRepository {
* @param onFailure A callback to be triggered upon failure of a query.
*/
private void downloadMetaDataFromServer(@NonNull Context context, UpdateHandler updateHandler, Runnable onSuccess, Runnable onFailure) {
if(DownloadFileUtils.getDownloadManager(context) == null) {
onFailure.run();
return;
}
boolean cacheValid = getCacheValidity(context);
// For local and PR test builds, force download of metadata
@ -405,8 +411,13 @@ public class CloudRepository {
BaseActivity.makeToast(context, R.string.catalog_download_is_running_in_background, Toast.LENGTH_SHORT);
} else {
updateIsRunning = true;
CloudDownloadMgr.getInstance().executeAsDownload(
boolean executionStarted = CloudDownloadMgr.getInstance().executeAsDownload(
context, DOWNLOAD_IDENTIFIER_CATALOGUE, memCachedDataset, _download_callback, params);
if(!executionStarted) {
// Since we couldn't initiate the execution's async components,
// we need to immediately clear the update-running flag.
updateIsRunning = false;
}
}
}
}

View file

@ -122,6 +122,8 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl
return;
}
// Warning: can be run by our system keyboard, which will attempt to
// display the toast if it or the app is visible!
BaseActivity.makeToast(currentContext, R.string.update_check_unavailable, Toast.LENGTH_SHORT);
lastUpdateCheck = Calendar.getInstance();
updateCheckFailed = true;

View file

@ -122,8 +122,8 @@ public class PackageProcessor {
*/
public File unzipKMP(File path) throws IOException {
File tempKeyboardPath = constructPath(path, true);
if (!tempKeyboardPath.exists()) {
tempKeyboardPath.mkdir();
if (!tempKeyboardPath.exists() && !tempKeyboardPath.mkdir()) {
KMLog.LogBreadcrumb(TAG, "Unable to mkdir for: " + tempKeyboardPath.toString(), true);
}
ZipUtils.unzip(path, tempKeyboardPath);
@ -165,7 +165,6 @@ public class PackageProcessor {
JSONParser parser = new JSONParser();
return parser.getJSONObjectFromFile(infoFile);
} else {
KMLog.LogError(TAG, "kmp.json does not exist");
return null;
}
}
@ -421,7 +420,6 @@ public class PackageProcessor {
*/
public String getPackageTarget(JSONObject json) {
if (json == null) {
KMLog.LogError(TAG, "kmp.json is null");
return PP_TARGET_INVALID;
}
try {

View file

@ -3,6 +3,7 @@ package com.keyman.engine.util;
import java.io.InputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
public final class Connection {
@ -99,6 +100,9 @@ public final class Connection {
attempt++;
}
}
} catch (SocketTimeoutException e) {
KMLog.LogBreadcrumb(TAG, "Connection.initialize timeout for: " + originalUrl, true);
KMLog.LogInfo(TAG, "SocketTimeoutException");
} catch (Exception e) {
KMLog.LogException(TAG, "Initialization failed:", e);
}

View file

@ -3,7 +3,9 @@
*/
package com.keyman.engine.util;
import android.app.DownloadManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
@ -21,6 +23,37 @@ import java.io.InputStream;
public final class DownloadFileUtils {
private static final String TAG = "DownloadFileUtils";
private static final String DOWNLOAD_MANAGER_PACKAGE_NAME = "com.android.providers.downloads";
/**
* Determines whether or not Android's `DownloadManager` service is active, only
* returning an instance of DownloadManager when it is currently accessible and enabled.
* Downloads and cloud queries are impossible when it's disabled.
*
* This solution is based heavily on
* https://gist.github.com/Folyd/b9412bb6e2b06eb511f7.
* @return A valid and enabled reference to the system's DownloadManager service. May be null
* if it is not accessible or is disabled.
*/
public static DownloadManager getDownloadManager(Context context) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
if(downloadManager==null) {
return null;
}
int state = context.getPackageManager().getApplicationEnabledSetting(DOWNLOAD_MANAGER_PACKAGE_NAME);
if(
state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED ||
state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER ||
state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
) {
return null;
};
return downloadManager;
}
/**
* Small class for returning information about a file downloaded via DownloadManager.
*/

View file

@ -4,9 +4,11 @@
package com.keyman.engine.util;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.AndroidRuntimeException;
import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.WebView;
import com.keyman.engine.util.KMLog;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -22,6 +24,14 @@ public final class WebViewUtils {
FULL; // WebView supports touch keyboards and LDML keyboards
}
// System WebView status
public enum SystemWebViewStatus {
UNDETERMINED, // Functionality not determined yet
NOT_INSTALLED, // WebView not installed
DISABLED, // WebView installed, but disabled
FULL; // WebView installed and enabled
}
private static final String CHROME_INSTALL_PATTERN_FORMATSTR = "^.*Chrome/([\\d.]+).*$";
private static final Pattern installPattern = Pattern.compile(CHROME_INSTALL_PATTERN_FORMATSTR);
@ -51,6 +61,34 @@ public final class WebViewUtils {
return EngineWebViewVersionStatus.FULL;
}
/**
* Determine the state of the system WebView (required for Keyman)
* @param context
* @return {SystemWebViewStatus}
*/
public static SystemWebViewStatus getSystemWebViewStatus(
Context context) {
// Check whether WebView is installed
// If the package is installed but disabled, hasSystemFeature() still returns true
PackageManager packageManager = context.getPackageManager();
if (!packageManager.hasSystemFeature("android.software.webview")) {
return SystemWebViewStatus.NOT_INSTALLED;
}
// Use CookieManager to check if WebView is really enabled
// https://stackoverflow.com/a/70354583
try {
CookieManager.getInstance();
} catch (AndroidRuntimeException e) {
// WebView installed but not enabled
return SystemWebViewStatus.DISABLED;
} catch (Exception e) {
return SystemWebViewStatus.DISABLED;
}
return SystemWebViewStatus.FULL;
}
// Inject a meta viewport tag into the head of the file if it doesn't exist
public static void injectViewport(WebView webView) {
if (webView != null) {

View file

@ -133,10 +133,15 @@
<string name="keyboard_install_toast" comment="Notification when a keyboard is installed">Τὸ πληκτρολόγιο %1$s ἐγκατεστάθη</string>
<!-- Context: Language Settings Keyboard uninstall strings -->
<string name="keyboard_deleted_toast" comment="Notification when a keyboard is deleted">Τὸ πληκτρολόγιο διεγράφη</string>
<!-- Context: Language Settings menu strings -->
<!-- Context: Language Settings menu strings. Removed in Keyman 18 -->
<string name="enable_corrections" comment="Enable corrections from the suggestion banner">Ἐνεργοποιῆστε τὶς διορθώσεις</string>
<!-- Context: Language Settings menu strings -->
<!-- Context: Language Settings menu strings. Removed in Keyman 18 -->
<string name="enable_predictions" comment="Enable predictions from the suggestion banner">Ἐνεργοποιῆστε προβλέψεις κειμένου</string>
<!-- Context: Language Settings menu strings - radio buttons -->
<string name="suggestions_radio_0" comment="Predictions and corrections disabled">Ἀπενεργοποίηση προτάσεων</string>
<string name="suggestions_radio_1" comment="Suggestions Enabled: Predictions with no corrections">Μόνο προβλέψεις</string>
<string name="suggestions_radio_2" comment="Suggestions Enabled: Predictions with corrections">Προβλέψεις καὶ διορθώσεις</string>
<string name="suggestions_radio_3" comment="Suggestions Enabled: Predictions with auto-corrections">Προβλέψεις μὲ αὐτόματες διορθώσεις</string>
<!-- Context: Language Settings menu strings -->
<string name="model_label">Λεξικό</string>
<plurals name="model_count">

View file

@ -131,10 +131,15 @@
<string name="keyboard_install_toast" comment="Notification when a keyboard is installed">ကဳဗုဒ် %1$s စုတ်လဝ်တုဲ</string>
<!-- Context: Language Settings Keyboard uninstall strings -->
<string name="keyboard_deleted_toast" comment="Notification when a keyboard is deleted">ဇိုတ် ကဳဗုဒ်တုဲ</string>
<!-- Context: Language Settings menu strings -->
<!-- Context: Language Settings menu strings. Removed in Keyman 18 -->
<string name="enable_corrections" comment="Enable corrections from the suggestion banner">ပံက် ပလေဝ်ဒါန်</string>
<!-- Context: Language Settings menu strings -->
<!-- Context: Language Settings menu strings. Removed in Keyman 18 -->
<string name="enable_predictions" comment="Enable predictions from the suggestion banner">ပံက် တၚ်ချိၚ်ခယျဂမၠိုၚ်</string>
<!-- Context: Language Settings menu strings - radio buttons -->
<string name="suggestions_radio_0" comment="Predictions and corrections disabled">Disable suggestions</string>
<string name="suggestions_radio_1" comment="Suggestions Enabled: Predictions with no corrections">တၚ်ချိၚ်ခယျဂမၠိုၚ်ဟေၚ်</string>
<string name="suggestions_radio_2" comment="Suggestions Enabled: Predictions with corrections">တၚ်ချၚ်ခယျ ကေုာံ တၚ်ပလေဝ်ဂမၠိုၚ်</string>
<string name="suggestions_radio_3" comment="Suggestions Enabled: Predictions with auto-corrections">တၚ်ချိၚ်ခယျနကဵု တၚ်ပလေဝ်အဝ်တဵု</string>
<!-- Context: Language Settings menu strings -->
<string name="model_label">အဘိဓာန်</string>
<plurals name="model_count">

View file

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Device type -->
<!-- Application name (Keyman Engine for Android) -->
<!-- Context: Title for list -->
<plurals name="title_keyboards">
<item quantity="other">Bàn phím</item>
</plurals>
<!-- Context: Title for list -->
<plurals name="title_other_input_methods">
<item quantity="other">Bộ gõ khác</item>
</plurals>
<!-- Context: Title for list -->
<string name="title_add_keyboard" comment="Add a new keyboard">Thêm bàn phím mới</string>
<!-- Context: Title for list -->
<string name="title_languages_settings" comment="List of installed languages">Ngôn ngữ đã cài đặt</string>
<!-- Context: Title for list -->
<string name="title_language_settings" comment="Keyman Settings for a language">Cài đặt %1$s</string>
<!-- Context: Button label -->
<string name="label_add" comment="Button to add an item">Thêm</string>
<!-- Context: Button label -->
<string name="label_back" comment="Button to go back">Quay lại</string>
<!-- Context: Button label -->
<string name="label_cancel" comment="Button to cancel an action">Hủy</string>
<!-- Context: Button label -->
<string name="label_close" comment="Close a dialog">Đóng</string>
<!-- Context: Button label. Removed in Keyman 15 -->
<string name="label_close_keyman" comment="Close the Keyman application">Đóng ứng dụng Keyman</string>
<!-- Context: Button label -->
<string name="label_forward" comment="Button to go forward">Chuyển tiếp</string>
<!-- Context: Button label -->
<string name="label_next_input_method" comment="Switch to next input method (replaces label_close_keyman)">Phương thức nhập kế tiếp</string>
<!-- Context: Button label -->
<string name="label_download" comment="Button to confirm downloading an item">Tải về</string>
<!-- Context: Button label -->
<string name="label_install" comment="Confirmation to install a package">Cài đặt</string>
<!-- Context: Button label -->
<string name="label_later" comment="Do an action at a later time">Để sau</string>
<!-- Context: Button label -->
<string name="label_next" comment="Go to the next screen">Tiếp theo</string>
<!-- Context: Button label -->
<string name="label_ok" comment="Button to acknowledge a dialog">Vâng</string>
<!-- Context: Button label -->
<string name="label_update" comment="Dialog button to update a resource">Cập nhật</string>
<!-- Context: No internet for updates -->
<string name="no_internet_connection" comment="Internet connection missing">Không có kết nối mạng</string>
<!-- Context: Keyboard Updates -->
<string name="cannot_connect" comment="Error message when network connection fails">Không thể kết nối tới máy chủ Keyman!</string>
<!-- Context: Keyboard Updates -->
<string name="confirm_delete_keyboard" comment="Confirmation to delete a keyboard">Bạn có muốn xóa bàn phím này không?</string>
<!-- Context: Keyboard Updates -->
<string name="confirm_download_keyboard" comment="Confirmation to download a keyboard update">Bạn có muốn tải xuống phiên bản mới nhất của bàn phím này?</string>
<!-- Context: Keyboard Updates -->
<string name="confirm_update" comment="Confirmation to update several resources">Bạn có muốn cập nhật bàn phím lẫn từ điển bây giờ không?</string>
<!-- Context: Keyboard Updates -->
<string name="keyboard_updates_channel">Cập nhật về Nguồn dữ liệu</string>
<!-- Context: Keyboard Updates -->
<string name="keyboard_updates_available" comment="Notification when resource updates are available">Có sẵn cập nhật về Nguồn dữ liệu</string>
<!-- Context: Keyboard Updates -->
<string name="update_available" comment="Currently installed version of a resource with an available update">%1$s (Đã có bản cập nhật)</string>
<!-- Context: Keyboard Updates -->
<string name="keyboard_update_message" comment="language name: keyboard name">Cập nhật có sẵn cho bàn phím %1$s: %2$s </string>
<!-- Context: Keyboard Updates -->
<string name="dictionary_update_message">Cập nhật có sẵn cho từ điển%1$s:%2$s </string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_version" comment="Label for keyboard version">Phiên bản bàn phím</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="help_link" comment="Label for help link">Link trợ giúp</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="uninstall_keyboard" comment="Label for uninstalling keyboard">Gỡ cài đặt bàn phím</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">[new] %1$s</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_qr_code" comment="QR Code description">Quét mã này để tải bàn phím này \n trên thiết bị khác</string>
<!-- Context: Keyboard Help welcome.htm title -->
<string name="welcome_package" comment="Title to welcome.htm page (name and version)">Chào mừng đến với %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">Cần có Thư viện FileProvider để xem phần trợ giúp file: %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">Lỗi bàn phím nghiêm trọng với %1$s: %2$s cho ngôn ngữ %3$s. Đang tải bàn phím mặc định.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">Lỗi trong bàn phím %1$s:%2$s cho ngôn ngữ %3$s.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Kiểm tra từ điển liên quan để tải về</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download">Không thể kết nối với server của Keyman để kiểm tra từ điển được liên kết để tải xuống</string>
<!-- Context: Model Updates -->
<string name="confirm_download_model" comment="Confirmation to download a dictionary update">Bạn có muốn tải xuống phiên bản mới nhất của từ điển này?</string>
<!-- Context: No associated dictionary found -->
<string name="no_associated_model" comment="Confirmation there's no available dictionary to download">Không có từ điển để tải xuống</string>
<!-- Context: Model Updates -->
<string name="catalog_unavailable" comment="Notification that the cloud resource catalog can't be updated">Catalog kho dữ liệu hiện không có sẵn</string>
<!-- Context: Background download messages-->
<string name="catalog_download_start_in_background" comment="Notification that the resource catalog update will begin">Cập nhật catalog đã được bắt đầu ngầm</string>
<!-- Context: Background download messages-->
<string name="catalog_download_is_running_in_background" comment="Notification that the resource catalog update is still downloading">Catalog vẫn đang tải về; xin vui lòng thử lại sau một lát!</string>
<!-- Context: Background download messages-->
<string name="progress_message_checking_resource_is_running" comment="Progress notification that the check for a resource is still running ">Đang kiểm tra kho dữ liệu</string>
<!-- Context: Background download messages-->
<string name="keyboard_download_start_in_background" comment="Notification that a keyboard download has started">Bàn phím đã bắt đầu tải về ngầm</string>
<!-- Context: Background download messages-->
<string name="keyboard_download_is_running_in_background" comment="Notification that a keyboard download is still running">Bàn phím được chọn đang được tải về; vui lòng thử lại sau một lát!</string>
<!-- Context: Background download messages-->
<string name="keyboard_download_finished" comment="Notification that a keyboard download has finished">Bàn phím đã được tải xong!</string>
<!-- Context: Background download messages-->
<string name="dictionary_download_start_in_background" comment="Notification that a dictionary download has started">Từ điển đã bắt đầu tải về ngầm</string>
<!-- Context: Background download messages-->
<string name="dictionary_download_is_running_in_background" comment="Notification that a dictionary download is still running">Từ điển được chọn đang được tải về; vui lòng thử lại sau một lát!</string>
<!-- Context: Background download messages. Removed in Keyman 17-->
<string name="dictionary_download_finished" comment="Notification that a dictionary download has finished">Tải từ điển hoàn tất.</string>
<!-- Context: Background download messages -->
<string name="download_failed" comment="Notification that a download failed">Tải xuống thất bại</string>
<!-- Context: Background download messages -->
<string name="failed_to_retrieve_file" comment="Failed to retrieve downloaded file">Không thể thu hồi file đã tải về</string>
<!-- Context: General Updates -->
<string name="update_check_unavailable" comment="Error message when a Keyman server can't be reached">Không thể kết nối với máy chủ!</string>
<!-- Context: General Updates -->
<string name="update_check_current" comment="Notification that all resources are up to date">"Toàn bộ kho dữ liệu đã được cập nhật!"</string>
<!-- Context: General Updates -->
<string name="update_failed" comment="Notification that a resource update failed">Một hay nhiều dữ liệu không thể cập nhật!</string>
<!-- Context: General Updates. Removed in Keyman 17 -->
<string name="update_success" comment="Notification that a resource update successfully updated">Kho dữ liệu đã được cập nhật thành công!</string>
<!-- Context: Model Info -->
<string name="model_version" comment="Title for a dictionary version">Phiên bản từ điển</string>
<!-- Context: Model Info -->
<string name="uninstall_model" comment="Label to uninstall a dictionary">Gỡ cài đặt từ điển</string>
<!-- Context: Model Info -->
<string name="confirm_delete_model" comment="Confirmation to delete a dictionary">Bạn có muốn xóa từ điển này không?</string>
<!-- Context: Model Deleted -->
<string name="model_deleted" comment="Notification when a dictionary is deleted">Từ điển đã bị xóa</string>
<!-- Context: Language Settings Keyboard install strings -->
<string name="keyboard_install_toast" comment="Notification when a keyboard is installed">%1$s bàn phím đã được cài đặt</string>
<!-- Context: Language Settings Keyboard uninstall strings -->
<string name="keyboard_deleted_toast" comment="Notification when a keyboard is deleted">Bàn phím đã bị xóa</string>
<!-- Context: Language Settings menu strings. Removed in Keyman 18 -->
<string name="enable_corrections" comment="Enable corrections from the suggestion banner">Bật hiệu chỉnh</string>
<!-- Context: Language Settings menu strings. Removed in Keyman 18 -->
<string name="enable_predictions" comment="Enable predictions from the suggestion banner">Bật tiên đoán</string>
<!-- Context: Language Settings menu strings - radio buttons -->
<string name="suggestions_radio_0" comment="Predictions and corrections disabled">Tắt các đề xuất</string>
<string name="suggestions_radio_1" comment="Suggestions Enabled: Predictions with no corrections">Chỉ dự đoán</string>
<string name="suggestions_radio_2" comment="Suggestions Enabled: Predictions with corrections">Dự đoán có sửa chữa</string>
<string name="suggestions_radio_3" comment="Suggestions Enabled: Predictions with auto-corrections">Dự đoán có sửa chữa tự động</string>
<!-- Context: Language Settings menu strings -->
<string name="model_label">Từ điển</string>
<plurals name="model_count">
<item quantity="other">Từ điển</item>
</plurals>
<!-- Context: Language Settings menu substring - Check for available dictionary -->
<string name="check_available_model" comment="Check for available dictionary">Kiểm tra từ điển có sẵn</string>
<string name="check_model_online" comment="Check for dictionaries online">Kiểm tra từ điển trên mạng</string>
<!-- Context: Language Settings menu strings -->
<string name="model_info_header" comment="Dictionary name">Từ điển: %1$s</string>
<!-- Context: Language Settings menu strings -->
<string name="model_picker_header" comment="Dictionaries for a specified language">%1$s Từ điển</string>
<!-- Context: Language Settings menu strings -->
<!-- Context: Language Settings menu strings -->
<string name="model_install_toast" comment="Notification when a dictionary is installed">Từ điển đã được cài đặt</string>
<!-- Context: Language Settings menu strings -->
<plurals name="keyboard_count">
<item quantity="other">(%1$d bàn phím)</item>
</plurals>
<!-- Context: "Change Display Language" menu -->
<string name="default_locale" comment="Use the device's current locale">Vị trí mặc định</string>
<!-- Context: Content descriptions -->
<!-- Context: Content descriptions -->
<!-- Context: Popup menu labels -->
<string name="label_delete" comment="Confirmation to delete an item">Xóa</string>
<!-- Context: Shared preferences name -->
<!-- Context: Other strings -->
<string name="help_bubble_text">Chạm ở đây để đổi bàn phím</string>
<!-- Context: anywhere -->
<string name="unable_to_open_browser" comment="Notification when a browser activity cannot be launched">Không thể mở trình duyệt</string>
</resources>

View file

@ -9,14 +9,14 @@
<!-- Context: Title for list -->
<plurals name="title_keyboards">
<item quantity="one">Keyboard</item>
<item quantity="other">Keyboards</item>
<item quantity="one">Keyboard</item>
<item quantity="other">Keyboards</item>
</plurals>
<!-- Context: Title for list -->
<plurals name="title_other_input_methods">
<item quantity="one">Other Input Method</item>
<item quantity="other">Other Input Methods</item>
<item quantity="one">Other Input Method</item>
<item quantity="other">Other Input Methods</item>
</plurals>
<!-- Context: Title for list -->
@ -127,10 +127,24 @@
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">
Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">
Error in keyboard %1$s:%2$s for %3$s language.</string>
<!-- Context: Alert Dialog body: System WebView not installed -->
<string name="body_install_webview" comment="Notification when WebView not installed">
Keyman requires WebView to be installed."</string>
<!-- Context: Alert Dialog body: System WebView installed but not enabled -->
<string name="body_enable_webview" comment="Notification when WebView installed but not enabled">
Keyman requires WebView to be enabled."</string>
<!-- Context Button label: Install WebView -->
<string name="label_install_webview" comment="Button label to install webview">Install WebView</string>
<!-- Context Button label: Enable WebView -->
<string name="label_enable_webview" comment="Button label to enable webview">Enable WebView</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Checking for associated dictionary to download</string>
@ -184,6 +198,9 @@
<!-- Context: General Updates -->
<string name="update_check_unavailable" comment="Error message when a Keyman server can't be reached">Failed to access server!</string>
<!-- Context: Update-check failure notification -->
<string name="update_check_download_manager_disabled" comment="Notification that a system service needed for updates is disabled">DownloadManager disabled - cannot check for updates</string>
<!-- Context: General Updates -->
<string name="update_check_current" comment="Notification that all resources are up to date">"All resources are up to date!"</string>
@ -229,8 +246,8 @@
<!-- Context: Language Settings menu strings -->
<string name="model_label">Dictionary</string>
<plurals name="model_count">
<item quantity="one">Dictionary</item>
<item quantity="other">Dictionaries</item>
<item quantity="one">Dictionary</item>
<item quantity="other">Dictionaries</item>
</plurals>
<!-- Context: Language Settings menu substring - Check for available dictionary -->
@ -251,8 +268,8 @@
<!-- Context: Language Settings menu strings -->
<plurals name="keyboard_count">
<item quantity="one">(%1$d keyboard)</item>
<item quantity="other">(%1$d keyboards)</item>
<item quantity="one">(%1$d keyboard)</item>
<item quantity="other">(%1$d keyboards)</item>
</plurals>
@ -278,4 +295,5 @@
<!-- Context: anywhere -->
<string name="unable_to_open_browser" comment="Notification when a browser activity cannot be launched">Unable to launch web browser</string>
</resources>

View file

@ -22,6 +22,9 @@
<item name="android:textColorPrimary">@android:color/black</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="AppTheme.NoTitleDialog" parent="AppTheme.Light.Dialog">
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.Dialog.Alert" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="android:textColor">@android:color/black</item>

View file

@ -0,0 +1,39 @@
---
title: KMManager.closeParentAppOnShowKeyboardPicker()
---
## Summary
The **closeParentAppOnShowKeyboardPicker()** method restores the default behavior of the keyboard picker menu closing the parent app.
## Syntax
```java
KMManager.closeParentAppOnShowKeyboardPicker()
```
## Description
Normally, launching the KeyboardPickerActivity from a system keyboard closes the parent app.
(The keyboard picker becomes the root of the activity stack.)
Calling this function restores this default behavior.
## Examples
### Example: Using `closeParentAppOnShowKeyboardPicker()`
The following script illustrates the use of `closeParentAppOnShowKeyboardPicker()`:
```java
@Override
protected void onResume() {
super.onResume();
KMManager.onResume();
KMManager.hideSystemKeyboard();
// Reset keyboard picker Activity Task flag
KMManager.closeParentAppOnShowKeyboardPicker();
```
## See also
* [dontCloseParentAppOnShowKeyboardPicker()](dontCloseParentAppOnShowKeyboardPicker)
* [showKeyboardPicker()](showKeyboardPicker)

View file

@ -0,0 +1,38 @@
---
title: KMManager.dontCloseParentAppOnShowKeyboardPicker()
---
## Summary
The **dontCloseParentAppOnShowKeyboardPicker()** method disables the keyboard picker task flag so keyboard picker doesn't dismiss the parent app.
## Syntax
```javascript
KMManager.dontCloseParentAppOnShowKeyboardPicker()
```
## Description
Normally, launching the KeyboardPickerActivity from a system keyboard closes the parent app.
(The keyboard picker becomes the root of the activity stack)
Calling this function keeps the parent app running in the background.
## Examples
### Example: Using `dontCloseParentAppOnShowKeyboardPicker()`
The following script illustrates the use of `dontCloseParentAppOnShowKeyboardPicker()`:
```java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// When using Keyman system keyboard within the Keyman app,
// disable keyboard picker task flag so keyboard picker doesn't dismiss Keyman app
KMManager.dontCloseParentAppOnShowKeyboardPicker();
```
## See also
* [closeParentAppOnShowKeyboardPicker()](closeParentAppOnShowKeyboardPicker)
* [showKeyboardPicker()](showKeyboardPicker)

View file

@ -34,5 +34,4 @@ The following code illustrates the use of `getLongpressDelay()`:
Keyman Engine for Android 18.0: New function.
## See also
* [sendOptionsToKeyboard](sendOptionsToKeyboard)
* [setLongpressDelay](setLongpressDelay)

View file

@ -50,6 +50,9 @@ The KMManager is the core class which provides most of the methods and constants
[`canRemoveKeyboard()`](canRemoveKeyboard)
: returns whether removing a keyboard is enabled, like in the keyboard picker menu
[`closeParentAppOnShowKeyboardPicker()`](closeParentAppOnShowKeyboardPicker)
: restores keyboard picker task flag so keyboard picker dismisses the parent app
[`copyHTMLBannerAssets()`](copyHTMLBannerAssets)
: copies a folder of HTML banner assets so it's available for your keyboard app's resources
@ -59,6 +62,9 @@ The KMManager is the core class which provides most of the methods and constants
[`deregisterLexicalModel()`](deregisterLexicalModel)
: deregisters the specified lexical model from the LMLayer so it isn't used
[`dontCloseParentAppOnShowKeyboardPicker()`](dontCloseParentAppOnShowKeyboardPicker)
: disables keyboard picker task flag so keyboard picker doesn't dismiss the parent app
[`executeHardwareKeystroke()`](executeHardwareKeystroke)
: process the keystroke generated from a physical keyboard
@ -212,9 +218,6 @@ The KMManager is the core class which provides most of the methods and constants
[`removeKeyboardEventListener()`](removeKeyboardEventListener)
: removes the specified listener from the list of keyboard event listeners
[`sendOptionsToKeyboard()`](sendOptionsToKeyboard)
: sends options like longpress delay to the KeymanWeb keyboard
[`setCanAddNewKeyboard()`](setCanAddNewKeyboard)
: sets whether adding a new keyboard is allowed

View file

@ -1,41 +0,0 @@
---
title: KMManager.sendOptionsToKeyboard()
---
## Summary
The `sendOptionsToKeyboard()` method sends options like longpress delay to the KeymanWeb keyboard.
## Syntax
```java
KMManager.sendOptionsToKeyboard()
```
## Description
Use this method to update options in the KeymanWeb keyboard.
* Number of milliseconds to trigger a longpress gesture
This method requires a keyboard to be loaded for the values to take effect.
## Examples
### Example: Using `sendOptionsToKeyboard()`
The following code illustrates the use of `sendOptionsToKeyboard()`:
```java
int currentDelayTimeMS = 250;
// Store currentDelayTimeMS
KMManager.setLongpressDelay(currentDelayTimeMS);
// Apply the keyboard options
KMManager.sendOptionsToKeyboard();
```
## History
Keyman Engine for Android 18.0: New function.
## See also
* [getLongpressDelay](getLongpressDelay)
* [setLongpressDelay](setLongpressDelay)

View file

@ -4,7 +4,7 @@ title: KMManager.setLongpressDelay()
## Summary
The `setLongpressDelay()` method sets the number of milliseconds to trigger a longpress gesture as a stored preference.
The `setLongpressDelay()` method sets the number of milliseconds to trigger a longpress gesture, applies to the keyboard, and stores preference.
## Syntax
@ -27,11 +27,8 @@ The following code illustrates the use of `setLongpressDelay()`:
```java
int currentDelayTimeMS = 300;
// Store currentDelayTimeMS
// Store currentDelayTimeMS and applies
KMManager.setLongpressDelay(currentDelayTimeMS);
// Apply the keyboard options
KMManager.sendOptionsToKeyboard();
```
## History
@ -39,4 +36,3 @@ Keyman Engine for Android 18.0: New function.
## See also
* [getLongpressDelay](getLongpressDelay)
* [sendOptionsToKeyboard](sendOptionsToKeyboard)

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View file

@ -0,0 +1 @@
kmcomp/

View file

@ -0,0 +1,36 @@
#!/usr/bin/env bash
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
builder_describe "Baseline keyboards tests -- built with 16.0 compiler" \
clean \
configure \
build
builder_describe_platform \
:project win
builder_parse "$@"
builder_describe_outputs \
configure kmcomp/kmcomp.exe \
build k_000___null_keyboard.kmx
do_configure() {
mkdir -p kmcomp
unzip kmcomp-16.0.138.zip -d kmcomp/
}
do_build() {
local name
for name in *.kmn; do
./kmcomp/kmcomp.exe -no-compiler-version -d "$name"
done
}
builder_run_action clean rm -rf *.kmx ./kmcomp/
builder_run_action configure do_configure
builder_run_action build do_build

View file

@ -0,0 +1,15 @@
store(&NAME) '050 - nul_and_context'
c Description: Tests that context has the right offset when used with nul
c keys: [K_T][K_B]
c expected: 2t3
c context:
store(&version) '10.0'
begin Unicode > use(Main)
store(cons) 'mnpqrstv'
group(Main) using keys
nul any(cons) + 'b' > '2' context(2) '3'

View file

@ -0,0 +1,16 @@
store(&NAME) '051 - if_and_context'
c Description: Tests that context has the right offset when used with if
c keys: [K_T][K_D]
c expected: 4t5
c context:
store(&version) '10.0'
begin Unicode > use(Main)
store(cons) 'mnpqrstv'
store(ifx) '1'
group(Main) using keys
if(ifx = '1') any(cons) + 'd' > '4' context(2) '5'

View file

@ -0,0 +1,16 @@
store(&NAME) '052 - nul_and_index'
c Description: Tests that context has the right offset when used with nul
c keys: [K_T][K_B]
c expected: 2T3
c context:
store(&version) '10.0'
begin Unicode > use(Main)
store(cons) 'mnpqrstv'
store(outs) 'MNPQRSTV'
group(Main) using keys
nul any(cons) + 'b' > '2' index(outs, 2) '3'

View file

@ -0,0 +1,18 @@
store(&NAME) '053 - if_and_index'
c Description: Tests that index() has the right offset when used with if
c keys: [K_T][K_D]
c expected: 4T5
c context:
store(&version) '10.0'
begin Unicode > use(Main)
store(cons) 'mnpqrstv'
store(outs) 'MNPQRSTV'
store(ifx) '1'
group(Main) using keys
if(ifx = '1') any(cons) + 'd' > '4' index(outs, 2) '5'

View file

@ -0,0 +1,15 @@
store(&NAME) '054 - nul_and_contextex'
c Description: Tests that context in context part of rule has the right offset when used with nul
c keys: [K_T][K_T][K_B]
c expected: 2tt3
c context:
store(&version) '10.0'
begin Unicode > use(Main)
store(cons) 'mnpqrstv'
group(Main) using keys
nul any(cons) context(2) + 'b' > '2' context(2) context(3) '3'

Binary file not shown.

View file

@ -1418,7 +1418,7 @@ var
// but this requires a file format change, so a much bigger scope
if not Enabled then Font.Color := $808080
else Font.Color := clWindowText;
else Font.Color := KeyFontColor_Cap;
FKeyboard.FDrawChar.DisplayQuality := ctCleartype;
FKeyboard.FDrawChar.Color := Font.Color;

View file

@ -5,7 +5,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
builder_describe "Common Delphi components" clean configure build test
builder_describe "Common Delphi components" clean configure build test edit
builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------
@ -28,3 +28,4 @@ builder_run_action clean:project clean_windows_project_files
builder_run_action configure:project configure_windows_build_environment
builder_run_action build:project do_build
# builder_run_action test:project do_test
builder_run_action edit:project start common_components.dproj

View file

@ -19,6 +19,9 @@ type
const S_CustomisationFilename = 'desktop_pro.pxx';
const S_KeymanAppData_UpdateCache = 'Keyman\UpdateCache\';
class var FHaveCachedKeymanRoot: Boolean;
class var FCachedKeymanRoot: string;
public
const S_KMShell = 'kmshell.exe';
const S_TSysInfoExe = 'tsysinfo.exe';
@ -434,12 +437,21 @@ end;
class function TKeymanPaths.RunningFromSource(var keyman_root: string): Boolean;
begin
if FHaveCachedKeymanRoot then
begin
keyman_root := FCachedKeymanRoot;
Exit(keyman_root <> '');
end;
// On developer machines, if we are running within the source repo, then use
// those paths
keyman_root := GetEnvironmentVariable('KEYMAN_ROOT');
keyman_root := GetEnvironmentVariable('KEYMAN_ROOT').Replace('/', '\', [rfReplaceAll]);
if keyman_root <> '' then
keyman_root := IncludeTrailingPathDelimiter(keyman_root);
Result := (keyman_root <> '') and SameText(keyman_root, ParamStr(0).Substring(0, keyman_root.Length));
FHaveCachedKeymanRoot := True;
if Result then
FCachedKeymanRoot := keyman_root;
end;
class function TKeymanPaths.KeymanCoreLibraryPath(const Filename: string): string;

View file

@ -5,7 +5,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
builder_describe "Build build_standards_data tool" clean configure build test
builder_describe "Build build_standards_data tool" clean configure build test edit
builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------
@ -49,3 +49,4 @@ builder_run_action clean:project clean_windows_project_files
builder_run_action configure:project configure_windows_build_environment
builder_run_action build:project do_build
# builder_run_action test:project do_test
builder_run_action edit:project start build_standards_data.dproj

View file

@ -7,7 +7,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
builder_describe "Unicode character database build tool" \
@/common/windows/delphi:keymanversion \
clean configure build test
clean configure build test edit
builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------
@ -30,3 +30,4 @@ builder_run_action clean:project clean_windows_project_files
builder_run_action configure:project configure_windows_build_environment
builder_run_action build:project do_build
# builder_run_action test:project do_test
builder_run_action edit:project start buildunidata.dproj

View file

@ -8,7 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
builder_describe \
"Development and build utilities for Delphi" \
@/common/windows/delphi:keymanversion \
clean configure build test prepublish
clean configure build test prepublish edit
builder_parse "$@"
@ -30,3 +30,4 @@ builder_run_action configure:project configure_windows_build_environment
builder_run_action build:project delphi_msbuild devtools.dproj "//p:Platform=Win32"
# builder_run_action test:project do_test
builder_run_action prepublish:project "$DEVTOOLS" -rt
builder_run_action edit:project start devtools.dproj

View file

@ -5,7 +5,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
builder_describe "Sentrytool for converting Delphi symbols into sentry-readable format" clean configure build test
builder_describe "Sentrytool for converting Delphi symbols into sentry-readable format" \
clean configure build test edit
builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------
@ -30,3 +31,4 @@ builder_run_action clean:project clean_windows_project_files
builder_run_action configure:project configure_windows_build_environment
builder_run_action build:project delphi_msbuild sentrytool.dproj "//p:Platform=Win32"
# builder_run_action test:project do_test
builder_run_action edit:project start sentrytool.dproj

View file

@ -5,7 +5,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
builder_describe "Tool for validating klog is disabled for release builds" clean configure build test prepublish
builder_describe "Tool for validating klog is disabled for release builds" \
clean configure build test prepublish edit
builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------
@ -33,3 +34,4 @@ builder_run_action configure:project configure_windows_build_environment
builder_run_action build:project do_build
# builder_run_action test:project do_test
builder_run_action prepublish:project "$WIN32_TARGET_PATH/test_klog.exe"
builder_run_action edit:project start test_klog.dproj

View file

@ -5,7 +5,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
builder_describe "Tool to verify all Windows executable signatures and manifests" clean configure build test verify
builder_describe "Tool to verify all Windows executable signatures and manifests" \
clean configure build test verify edit
builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------
@ -27,3 +28,4 @@ builder_run_action clean:project clean_windows_project_files
builder_run_action configure:project configure_windows_build_environment
builder_run_action build:project do_build
# builder_run_action test:project do_test
builder_run_action edit:project start verify_signatures.dproj

View file

@ -29,6 +29,7 @@ archtargets=(":wasm WASM build")
case $BUILDER_OS in
win)
archtargets+=(
":win Both x86 and x64"
":x86 32-bit Windows (x86) build"
":x64 64-bit Windows (x64) build"
)
@ -92,9 +93,12 @@ fi
# generates a 'fat' library from them.
builder_describe_internal_dependency \
build:mac build:mac-x86_64 \
build:mac build:mac-arm64
build:mac build:mac-arm64 \
build:win build:x86 \
build:win build:x64
builder_describe_outputs \
configure:win /core/build/win/$BUILDER_CONFIGURATION/ \
configure:x86 /core/build/x86/$BUILDER_CONFIGURATION/build.ninja \
configure:x64 /core/build/x64/$BUILDER_CONFIGURATION/build.ninja \
configure:mac /core/build/mac/$BUILDER_CONFIGURATION/ \
@ -102,6 +106,7 @@ builder_describe_outputs \
configure:mac-arm64 /core/build/mac-arm64/$BUILDER_CONFIGURATION/build.ninja \
configure:arch /core/build/arch/$BUILDER_CONFIGURATION/build.ninja \
configure:wasm /core/build/wasm/$BUILDER_CONFIGURATION/build.ninja \
build:win /core/build/win/$BUILDER_CONFIGURATION/BUILT \
build:x86 /core/build/x86/$BUILDER_CONFIGURATION/src/libkeymancore.a \
build:x64 /core/build/x64/$BUILDER_CONFIGURATION/src/libkeymancore.a \
build:mac /core/build/mac/$BUILDER_CONFIGURATION/libkeymancore.a \
@ -134,29 +139,28 @@ do_action() {
# -------------------------------------------------------------------------------
do_action clean
builder_run_action clean:mac rm -rf "$KEYMAN_ROOT/core/build/mac/$BUILDER_CONFIGURATION"
builder_run_action clean:win rm -rf "$KEYMAN_ROOT/core/build/win/$BUILDER_CONFIGURATION"
# -------------------------------------------------------------------------------
do_action configure
# After we have built the necessary internal dependencies, then we can go
# ahead and build a fat library for external consumption
if builder_start_action configure:mac; then
mkdir -p "$KEYMAN_ROOT/core/build/mac/$BUILDER_CONFIGURATION"
builder_finish_action success configure:mac
fi
# After we have built the necessary internal dependencies, then we can go ahead
# and build a fat library for external consumption for mac, and no-op for win
builder_run_action configure:mac mkdir -p "$KEYMAN_ROOT/core/build/mac/$BUILDER_CONFIGURATION"
builder_run_action configure:win mkdir -p "$KEYMAN_ROOT/core/build/win/$BUILDER_CONFIGURATION"
# -------------------------------------------------------------------------------
do_action build
if builder_start_action build:mac; then
lipo -create \
"$KEYMAN_ROOT/core/build/mac-x86_64/$BUILDER_CONFIGURATION/src/libkeymancore.a" \
"$KEYMAN_ROOT/core/build/mac-arm64/$BUILDER_CONFIGURATION/src/libkeymancore.a" \
-output "$KEYMAN_ROOT/core/build/mac/$BUILDER_CONFIGURATION/libkeymancore.a"
builder_finish_action success build:mac
fi
builder_run_action build:mac lipo -create \
"$KEYMAN_ROOT/core/build/mac-x86_64/$BUILDER_CONFIGURATION/src/libkeymancore.a" \
"$KEYMAN_ROOT/core/build/mac-arm64/$BUILDER_CONFIGURATION/src/libkeymancore.a" \
-output "$KEYMAN_ROOT/core/build/mac/$BUILDER_CONFIGURATION/libkeymancore.a"
builder_run_action build:win touch "$KEYMAN_ROOT/core/build/win/$BUILDER_CONFIGURATION/BUILT"
# -------------------------------------------------------------------------------
@ -177,6 +181,15 @@ if builder_start_action test:mac; then
builder_finish_action success test:mac
fi
if builder_start_action test:win; then
# We can assume that build:win has run so both architectures will be available
MESON_PATH="$KEYMAN_ROOT/core/build/x86/$BUILDER_CONFIGURATION"
meson test -C "$MESON_PATH" $testparams
MESON_PATH="$KEYMAN_ROOT/core/build/x64/$BUILDER_CONFIGURATION"
meson test -C "$MESON_PATH" $testparams
builder_finish_action success test:win
fi
# -------------------------------------------------------------------------------
do_action install

View file

@ -648,8 +648,8 @@ typedef KMX_DWORD KMXPLUS_USET;
struct COMP_KMXPLUS_USET {
static const KMX_DWORD IDENT = LDML_SECTIONID_USET;
COMP_KMXPLUS_HEADER header;
KMX_DWORD usetCount;
KMX_DWORD rangeCount;
KMX_DWORD_unaligned usetCount;
KMX_DWORD_unaligned rangeCount;
// see helper for: usets sub-table
// see helper for: ranges sub-table

View file

@ -23,6 +23,7 @@ KMX_ProcessEvent::KMX_ProcessEvent() : m_actions(&m_context), m_options(&m_keybo
m_indexStack = new KMX_WORD[GLOBAL_ContextStackSize];
m_miniContext = new KMX_WCHAR[GLOBAL_ContextStackSize];
m_miniContextIfLen = 0;
m_miniContextStartsWithNul = FALSE;
m_debug_items = nullptr;
}
@ -338,10 +339,14 @@ KMX_BOOL KMX_ProcessEvent::ProcessGroup(LPGROUP gp, KMX_BOOL *pOutputKeystroke)
m_miniContextIfLen = xstrlen(kkp->dpContext) - xstrlen_ignoreifopt(kkp->dpContext);
// 11 Aug 2003 - I25(v6) - mcdurdin - CODE_NUL context support
if(*kkp->dpContext == UC_SENTINEL && *(kkp->dpContext+1) == CODE_NUL)
if(*kkp->dpContext == UC_SENTINEL && *(kkp->dpContext+1) == CODE_NUL) {
m_miniContextStartsWithNul = TRUE;
u16cpy(m_miniContext, /*GLOBAL_ContextStackSize,*/ m_context.Buf(xstrlen_ignoreifopt(kkp->dpContext)-1) /*, GLOBAL_ContextStackSize*/); // I3162 // I3536
else
}
else {
m_miniContextStartsWithNul = FALSE;
u16cpy(m_miniContext, /*GLOBAL_ContextStackSize,*/ m_context.Buf(xstrlen_ignoreifopt(kkp->dpContext)) /*, GLOBAL_ContextStackSize*/); // I3162 // I3536
}
m_miniContext[GLOBAL_ContextStackSize-1] = 0;
@ -458,7 +463,12 @@ int KMX_ProcessEvent::PostString(PKMX_WCHAR str, LPKEYBOARD lpkb, PKMX_WCHAR end
}
case CODE_CONTEXTEX:
p++;
for (q = m_miniContext, i = m_miniContextIfLen; *q && i < *p - 1; i++, q = incxstr(q));
// We have to exclude nul and if() statements from our offset calculation
i = m_miniContextIfLen;
if(m_miniContextStartsWithNul) {
i++;
}
for (q = m_miniContext; *q && i < *p - 1; i++, q = incxstr(q));
if (*q) {
KMX_BOOL ignoreOutputKeystroke;
temp = incxstr(q);
@ -600,6 +610,7 @@ KMX_BOOL KMX_ProcessEvent::ContextMatch(LPKEY kkp)
memset(m_indexStack, 0, GLOBAL_ContextStackSize*sizeof(KMX_WORD));
p = kkp->dpContext;
indexp = m_indexStack;
if(*p == 0)
{
@ -611,6 +622,7 @@ KMX_BOOL KMX_ProcessEvent::ContextMatch(LPKEY kkp)
// If context buf is longer than the context, then obviously not start of doc.
if(m_context.Buf(xstrlen_ignoreifopt(p))) return FALSE;
p = incxstr(p);
indexp++;
if(*p == 0) return TRUE;
}
@ -660,7 +672,6 @@ KMX_BOOL KMX_ProcessEvent::ContextMatch(LPKEY kkp)
{
return FALSE; // context buf is too short!
}
indexp = m_indexStack;
for(; *p && *q; p = incxstr(p))
{

View file

@ -38,7 +38,10 @@ class KMX_ProcessEvent {
private:
PKMX_WORD m_indexStack;
PKMX_WCHAR m_miniContext;
int m_miniContextIfLen; // number of if() statements excluded from start of m_miniContext
/** number of `if()` statements excluded from start of `m_miniContext` for offset calculations */
int m_miniContextIfLen;
/** flag if we need to account for `nul` in offset calculations */
bool m_miniContextStartsWithNul;
KMSTATE m_state;
km_core_state *m_core_state;

View file

@ -80,7 +80,12 @@ tests = [
'k_046___deadkey_and_contextex',
'k_047___caps_always_off_initially_on',
'k_048___modifier_keys_keep_context',
'k_049___enter_invalidates_context'
'k_049___enter_invalidates_context',
'k_050___nul_and_context',
'k_051___if_and_context',
'k_052___nul_and_index',
'k_053___if_and_index',
'k_054___nul_and_contextex',
]
node = find_program('node', required: true)

View file

@ -8,7 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
builder_describe \
"Keyman Developer Delphi components" \
@/common/windows/delphi \
clean configure build test
clean configure build test edit
builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------
@ -33,3 +33,4 @@ builder_run_action clean:project clean_windows_project_files
builder_run_action configure:project configure_windows_build_environment
builder_run_action build:project do_build
# builder_run_action test:project do_test
builder_run_action edit:project start delphi_components.dproj

View file

@ -53,6 +53,8 @@ type
procedure WndProc(var Message: TMessage); override;
public
constructor Create(AOwner: TComponent); override;
function GetTextCR: string;
function GetSelTextCR: string;
published
property AllowUnicode: Boolean read FAllowUnicodeInput write SetAllowUnicode default True;
property OnMessage: TKeymanDeveloperDebuggerMessageEvent read FOnMessage write FOnMessage;
@ -65,6 +67,9 @@ procedure Register;
implementation
uses
System.SysUtils;
{ TKeymanDeveloperDebuggerMemo }
procedure TKeymanDeveloperDebuggerMemo.CNKeyDown(var Message: TWMKeyDown);
@ -123,6 +128,16 @@ begin
end;
end;
function TKeymanDeveloperDebuggerMemo.GetSelTextCR: string;
begin
Result := SelText.Replace(#$0D#$0A, #$0D, [rfReplaceAll]);
end;
function TKeymanDeveloperDebuggerMemo.GetTextCR: string;
begin
Result := string(Text).Replace(#$0D#$0A, #$0D, [rfReplaceAll]);
end;
procedure TKeymanDeveloperDebuggerMemo.SetAllowUnicode(const Value: Boolean);
begin
if Value <> fAllowUnicodeInput then

View file

@ -272,6 +272,9 @@ namespace KmnCompilerMessages {
HINT_IndexStoreLong = SevHint | 0x0B0,
ERROR_ContextExCannotReferenceIf = SevError | 0x0B1,
ERROR_ContextExCannotReferenceNul = SevError | 0x0B2,
FATAL_BufferOverflow = SevFatal | 0x0C0
// FATAL_Break = SevFatal | 0x0C1, unused
};

View file

@ -49,5 +49,5 @@ function do_build() {
builder_run_action clean rm -rf ./build/
builder_run_action configure verify_npm_setup
builder_run_action build do_build
builder_run_action test builder_do_typescript_tests 50
builder_run_action test builder_do_typescript_tests 45
builder_run_action publish builder_publish_npm

View file

@ -1,6 +1,6 @@
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'node:os';
import * as fs from 'node:fs';
import * as path from 'node:path';
export interface KeymanDeveloperOptions {
"use tab char"?: boolean;

View file

@ -25,11 +25,7 @@ export class CopierMessages {
o.e ?? 'unknown error'
);
static INFO_CopyingProject = SevInfo | 0x0002;
static Info_CopyingProject = (o:{type: string, id: string}) => m(
this.INFO_CopyingProject,
`Copying project of type ${def(o.type)} with id ${def(o.id)}`
);
// unused 0x0002 (was INFO_CopyingProject, now in InfrastructureMessages)
static ERROR_CannotCreateFolder = SevError | 0x0003;
static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException(

View file

@ -7,7 +7,7 @@
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils";
const Namespace = CompilerErrorNamespace.Generator;
const SevInfo = CompilerErrorSeverity.Info | Namespace;
// const SevInfo = CompilerErrorSeverity.Info | Namespace;
// const SevHint = CompilerErrorSeverity.Hint | Namespace;
const SevWarn = CompilerErrorSeverity.Warn | Namespace;
const SevError = CompilerErrorSeverity.Error | Namespace;
@ -23,11 +23,7 @@ export class GeneratorMessages {
o.e ?? 'unknown error',
);
static INFO_GeneratingProject = SevInfo | 0x0002;
static Info_GeneratingProject = (o:{type: string, id: string}) => m(
this.INFO_GeneratingProject,
`Generating project of type ${def(o.type)} with id ${def(o.id)}`
);
// unused 0x0002 (was INFO_GeneratingProject, now in InfrastructureMessages)
static ERROR_CannotCreateFolder = SevError | 0x0003;
static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException(

View file

@ -737,7 +737,19 @@ export class KmnCompilerMessages {
static HINT_IndexStoreLong = SevHint | 0x0B0;
static Hint_IndexStoreLong = () => m(
this.HINT_IndexStoreLong,
`The store referenced in index() is longer than the store referenced in any()`
`The store referenced in index() is longer than the store referenced in any()`,
);
static ERROR_ContextExCannotReferenceIf = SevError | 0x0B1;
static Error_ContextExCannotReferenceIf = () => m(
this.ERROR_ContextExCannotReferenceIf,
`The offset in context() points to a non-character if() statement in the context`,
);
static ERROR_ContextExCannotReferenceNul = SevError | 0x0B2;
static Error_ContextExCannotReferenceNul = () => m(
this.ERROR_ContextExCannotReferenceNul,
`The offset in context() points to a non-character nul statement in the context`,
);
static FATAL_BufferOverflow = SevFatal | 0x0C0;

View file

@ -643,7 +643,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp
let recContext = ExpandSentinel(fk, pwszContext, x);
if(isKeyboardVersion10OrLater()) {
if(recContext.IsSentinel && [KMX.KMXFile.CODE_NUL, KMX.KMXFile.CODE_IFOPT, KMX.KMXFile.CODE_IFSYSTEMSTORE].includes(recContext.Code)) {
if(recContext.IsSentinel && [KMX.KMXFile.CODE_IFOPT, KMX.KMXFile.CODE_IFSYSTEMSTORE].includes(recContext.Code)) {
Result--;
}
}

View file

@ -0,0 +1,11 @@
store(&TARGETS) 'any'
begin Unicode > use(main)
store(cons) 'bcd'
store(ifx) '1'
group(main) using keys
c ERROR_ContextExCannotReferenceIf:
if(ifx = '1') any(cons) + 'd' > context(1)

View file

@ -0,0 +1,10 @@
store(&TARGETS) 'any'
begin Unicode > use(main)
store(cons) 'bcd'
group(main) using keys
c ERROR_ContextExCannotReferenceNul:
nul any(cons) + 'a' > 'x' context(1)

View file

@ -0,0 +1,12 @@
store(&TARGETS) 'any'
begin Unicode > use(main)
store(cons) 'bcd'
store(ifx) '1'
group(main) using keys
c ERROR_ContextExHasInvalidOffset: `context(3)` should be `context(2)`
if(ifx='1') any(cons) + 'a' > 'x' context(3) 'z'

View file

@ -0,0 +1,11 @@
store(&TARGETS) 'any'
begin Unicode > use(main)
store(cons) 'bcd'
group(main) using keys
c ERROR_ContextExHasInvalidOffset: `context(2)` is longer than range
any(cons) + 'a' > 'x' context(2) 'z'

View file

@ -0,0 +1,12 @@
store(&TARGETS) 'any'
begin Unicode > use(main)
store(cons) 'bcd'
store(ifx) '1'
group(main) using keys
c ERROR_ContextExHasInvalidOffset: `context(3)` should be `context(2)`
nul any(cons) + 'a' > 'x' context(3) 'z'

View file

@ -0,0 +1,11 @@
store(&TARGETS) 'any'
begin Unicode > use(main)
store(cons) 'bcd'
group(main) using keys
c ERROR_ContextExHasInvalidOffset: `context(0)` is lower than 1
any(cons) + 'a' > 'x' context(0) 'z'

View file

@ -1,8 +1,9 @@
c test_index_12980 generated from template at 2025-01-22 13:25:35
c Validates index() offsets when used with if() - see #12980
c Test with v10+ kmw compiler
store(&VERSION) '14.0'
store(&NAME) 'test_index_12980'
store(&NAME) 'test_index_12980_v10'
store(&TARGETS) 'web'

View file

@ -0,0 +1,22 @@
c test_index_12980 generated from template at 2025-01-22 13:25:35
c Validates index() offsets when used with if() - see #12980
c Test with v9 KMW compiler (does not support notany)
store(&VERSION) '9.0'
store(&NAME) 'test_index_12980_v9'
store(&TARGETS) 'web'
begin Unicode > use(main)
group(main) using keys
store(a) '1'
store(b) '2'
store(e) '5'
store(f) '6'
if(&platform = 'web') any(a) + 'a' > context 'A'
if(&platform = 'web') any(b) + 'b' > context(2) 'B'
nul any(e) + 'e' > context 'E'
nul any(f) + 'f' > context(2) 'F'

View file

@ -218,29 +218,56 @@ describe('KeymanWeb Compiler', function() {
assert.lengthOf(callbacks.messages, 2);
});
it('should generate correct index offsets (#12980) for context and context(n) when building a KMW keyboard', async function() {
const filenames = generateTestFilenames('test_index_12980');
function doMatchKIO(data: string, s: string, offset: number) {
const regex = new RegExp('k\\.KIO\\(-1,this\\.'+s+',(.+?),t\\);');
const m = regex.exec(data);
assert.isNotNull(m, `Could not find match for ${s}`);
assert.equal(m[1], offset.toString(), `Expected '${s}' usage to have an index parameter == 1`);
}
it('should generate correct index offsets (#12980) for context and context(n) when building a KMW keyboard with v9 target', async function() {
const filenames = generateTestFilenames('test_index_12980_v9');
let result = await kmnCompiler.run(filenames.source, null);
assert.isNotNull(result);
const data = new TextDecoder('utf-8').decode(result.artifacts.js.data);
function doMatchKIO(s: string) {
const regex = new RegExp('k\\.KIO\\(-1,this\\.'+s+',(.+?),t\\);');
const m = regex.exec(data);
assert.isNotNull(m, `Could not find match for ${s}`);
assert.equal(m[1], '1', `Expected '${s}' usage to have an index parameter == 1`);
if(debug) {
console.log(result.artifacts.js.filename);
await kmnCompiler.write(result.artifacts);
}
doMatchKIO('s_a_6');
doMatchKIO('s_b_7');
// v9 targets should match
assert.match(data, /k\.KO\(-1,t,"A"\);/);
assert.match(data, /k\.KIO\(1,this.s_b_7,1,t\);/);
assert.match(data, /k\.KO\(-1,t,"E"\);/);
assert.match(data, /k\.KIO\(1,this.s_f_9,1,t\);/);
});
// matches the 4 notany() rules
const matches = [...data.matchAll(/k\.KCXO\(-1,t,1,1\);/g)];
assert.lengthOf(matches, 4);
it('should generate correct index offsets (#12980) for context and context(n) when building a KMW keyboard with v10+ target', async function() {
const filenames = generateTestFilenames('test_index_12980_v10');
doMatchKIO('s_e_10');
doMatchKIO('s_f_11');
let result = await kmnCompiler.run(filenames.source, null);
assert.isNotNull(result);
const data = new TextDecoder('utf-8').decode(result.artifacts.js.data);
if(debug) {
console.log(result.artifacts.js.filename);
await kmnCompiler.write(result.artifacts);
}
doMatchKIO(data, 's_a_6', 1); //if() has index 1
doMatchKIO(data, 's_b_7', 1); //if() has index 1
// matches the 2 notany() rules for if() and 2 for nul
const matches_if = [...data.matchAll(/k\.KCXO\(-1,t,1,1\);/g)];
assert.lengthOf(matches_if, 2);
const matches_nul = [...data.matchAll(/k\.KCXO\(-1,t,2,2\);/g)];
assert.lengthOf(matches_nul, 2);
doMatchKIO(data, 's_e_10', 2);
doMatchKIO(data, 's_f_11', 2);
});
});

View file

@ -185,7 +185,6 @@ describe('KmnCompilerMessages', function () {
it('should generate WARN_IndexStoreShort if a store referenced in index() is shorter than the corresponding any() store', async function() {
await testForMessage(this, ['keyboards', 'warn_index_store_short.kmn'], KmnCompilerMessages.WARN_IndexStoreShort);
callbacks.clear();
await testForMessage(this, ['keyboards', 'warn_index_store_short_key.kmn'], KmnCompilerMessages.WARN_IndexStoreShort);
});
@ -193,8 +192,28 @@ describe('KmnCompilerMessages', function () {
it('should generate HINT_IndexStoreLong if a store referenced in index() is longer than the corresponding any() store', async function() {
await testForMessage(this, ['keyboards', 'hint_index_store_long.kmn'], KmnCompilerMessages.HINT_IndexStoreLong);
callbacks.clear();
await testForMessage(this, ['keyboards', 'hint_index_store_long_key.kmn'], KmnCompilerMessages.HINT_IndexStoreLong);
});
// ERROR_ContextExHasInvalidOffset
it('should generate ERROR_ContextExHasInvalidOffset if the offset is lower than 1 or greater than the context length', async function() {
await testForMessage(this, ['invalid-keyboards', 'error_contextex_has_invalid_offset_if.kmn'], KmnCompilerMessages.ERROR_ContextExHasInvalidOffset);
await testForMessage(this, ['invalid-keyboards', 'error_contextex_has_invalid_offset_long.kmn'], KmnCompilerMessages.ERROR_ContextExHasInvalidOffset);
await testForMessage(this, ['invalid-keyboards', 'error_contextex_has_invalid_offset_nul.kmn'], KmnCompilerMessages.ERROR_ContextExHasInvalidOffset);
await testForMessage(this, ['invalid-keyboards', 'error_contextex_has_invalid_offset_zero.kmn'], KmnCompilerMessages.ERROR_ContextExHasInvalidOffset);
});
// ERROR_ContextExCannotReferenceIf
it('should generate ERROR_ContextExCannotReferenceIf if the offset points to an if() statement', async function() {
await testForMessage(this, ['invalid-keyboards', 'error_contextex_cannot_reference_if.kmn'], KmnCompilerMessages.ERROR_ContextExCannotReferenceIf);
});
// ERROR_ContextExCannotReferenceNul
it('should generate ERROR_ContextExCannotReferenceNul if the offset points to a nul statement', async function() {
await testForMessage(this, ['invalid-keyboards', 'error_contextex_cannot_reference_nul.kmn'], KmnCompilerMessages.ERROR_ContextExCannotReferenceNul);
});
});

View file

@ -47,7 +47,7 @@ function do_build() {
}
function do_build_abnf() {
# we convert over all abnf files found.
# we convert over all abnf files found.
local file
for file in "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards"/*/abnf/*.abnf; do
local cldrver="$(basename $(dirname $(dirname "$file")))"
@ -82,7 +82,7 @@ builder_run_action configure do_configure
builder_run_action build do_build
builder_run_action build-fixtures do_build_fixtures
builder_run_action api api-extractor run --local --verbose
builder_run_action test builder_do_typescript_tests
builder_run_action test builder_do_typescript_tests 75
#-------------------------------------------------------------------------------------------------------------------

View file

@ -135,7 +135,9 @@ export class LdmlKeyboardVisualKeyboardCompiler {
private getDisplayFromKey(keydef: KMXPlus.KeysKeys, source: KMXPlus.KMXPlusData) {
const display = source.disp?.disps?.find(d => d.id.value == keydef.id.value || d.to.value == keydef.to.value);
return display?.display.value ?? keydef.to.value;
const value = display?.display.value ?? keydef.to.value;
// strip markers from the output (these are valid in keydef.to, but not in display.display, nor in kvk)
return value.replaceAll(/\uffff\u0008./g, '');
}
private translateLayerModifiersToVisualKeyboardShift(modifiers: number): VisualKeyboard.VisualKeyboardShiftState {

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