Merge branch 'master' into chore/linux/cherry-pick/changelog

This commit is contained in:
Eberhard Beilharz 2025-03-03 08:10:17 +01:00 • committed by GitHub
commit 8d5fe8a66a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
252 changed files with 3084 additions and 1325 deletions

View file

@ -1,5 +1,9 @@
# Keyman Version History
## 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 +46,102 @@
* refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168)
* chore: increment to alpha 19.0 (#13187)
## 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.11

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;
@ -84,7 +85,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,16 +155,7 @@ 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;
KMManager.setMayPredictOverride(inputType);
@ -228,6 +225,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 +257,6 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
if (exText != null)
exText = null;
}
// Initialize keyboard options
KMManager.sendOptionsToKeyboard();
}
@Override

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

@ -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();
}
@ -475,8 +482,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
@Override
public void onKeyboardLoaded(KeyboardType keyboardType) {
// Initialize keyboard options
KMManager.sendOptionsToKeyboard();
// Do nothing
}
@Override
@ -770,39 +776,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

@ -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

@ -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

@ -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

@ -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);
}
@ -776,6 +800,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 +949,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 +2152,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 +2168,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));
}
@ -2301,9 +2331,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 +2365,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 +2384,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

@ -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

@ -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

@ -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

@ -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

@ -212,9 +212,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'

Binary file not shown.

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 \
@ -139,24 +144,21 @@ do_action clean
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 +179,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

@ -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,11 @@ 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',
]
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

@ -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

@ -79,6 +79,18 @@ async function doCopy(callbacks: NodeCompilerCallbacks, sources: string | string
return false;
}
const dest = options.outPath;
callbacks.reportMessage(InfrastructureMessages.Info_CopyingProject({source, dest}));
const result = runCopier(callbacks, source, options);
if(result) {
callbacks.reportMessage(InfrastructureMessages.Info_ProjectCopiedSuccessfully({source, dest}));
} else {
callbacks.reportMessage(InfrastructureMessages.Info_ProjectNotCopiedSuccessfully({source, dest}));
}
return result;
}
async function runCopier(callbacks: NodeCompilerCallbacks, source: string, options: CopierOptions) {
const copier = new KeymanProjectCopier();
try {
if(!await copier.init(callbacks, options)) {

View file

@ -113,9 +113,16 @@ async function generate(generator: KeymanCompiler, ids: string | string[], comma
}
const options = commanderOptionsToGeneratorOptions(id, commanderOptions);
const outPath = options.outPath;
const callbacks = new NodeCompilerCallbacks(options);
if(!await doGenerate(callbacks, generator, options)) {
callbacks.reportMessage(InfrastructureMessages.Info_GeneratingProject({id, outPath}));
const result = await doGenerate(callbacks, generator, options);
if(result) {
callbacks.reportMessage(InfrastructureMessages.Info_ProjectGeneratedSuccessfully({id}));
} else {
callbacks.reportMessage(InfrastructureMessages.Info_ProjectNotGeneratedSuccessfully({id}));
return await exitProcess(1);
}
}

View file

@ -158,5 +158,48 @@ export class InfrastructureMessages {
this.ERROR_CopyRequiresOutPath,
`The copy command requires the --out-path, -o parameter`
);
}
// For this message, we override the filename with the passed-in file. A bit of a hack but does the job
static INFO_CopyingProject = SevInfo | 0x0023;
static Info_CopyingProject = (o:{source:string,dest:string}) => ({filename:o.source, ...m(
this.INFO_CopyingProject,
`Copying project '${def(o.source)}' to '${def(o.dest)}'`,
)});
// For this message, we override the filename with the passed-in file. A bit of a hack but does the job
static INFO_ProjectCopiedSuccessfully = SevInfo | 0x0024;
static Info_ProjectCopiedSuccessfully = (o:{source:string, dest:string}) => ({filename:o.source, ...m(
this.INFO_ProjectCopiedSuccessfully,
`'${def(o.source)}' copied to '${def(o.dest)}' successfully.`,
)});
// For this message, we override the filename with the passed-in file. A bit of a hack but does the job
static INFO_ProjectNotCopiedSuccessfully = SevInfo | 0x0025;
static Info_ProjectNotCopiedSuccessfully = (o:{source:string, dest:string}) => ({filename:o.source, ...m(
this.INFO_ProjectNotCopiedSuccessfully,
`Failed to copy '${def(o.source)}' to '${def(o.dest)}'.`,
)});
// For this message, we override the filename with the passed-in file. A bit of a hack but does the job
static INFO_GeneratingProject = SevInfo | 0x0026;
static Info_GeneratingProject = (o:{id:string, outPath: string}) => ({filename:o.id, ...m(
this.INFO_GeneratingProject,
`Generating new project '${def(o.id)}' in '${def(o.outPath)}'.`,
)});
// For this message, we override the filename with the passed-in file. A bit of a hack but does the job
static INFO_ProjectGeneratedSuccessfully = SevInfo | 0x0027;
static Info_ProjectGeneratedSuccessfully = (o:{id:string}) => ({filename:o.id, ...m(
this.INFO_ProjectGeneratedSuccessfully,
`New project '${def(o.id)}' generated successfully.`,
)});
// For this message, we override the filename with the passed-in file. A bit of a hack but does the job
static INFO_ProjectNotGeneratedSuccessfully = SevInfo | 0x0028;
static Info_ProjectNotGeneratedSuccessfully = (o:{id:string}) => ({filename:o.id, ...m(
this.INFO_ProjectNotGeneratedSuccessfully,
`Failed to generate new project '${def(o.id)}'.`,
)});
}

View file

@ -242,12 +242,18 @@ export class NodeCompilerCallbacks implements CompilerCallbacks {
messageSpecialColor(event: CompilerEvent) {
switch(event.code) {
case InfrastructureMessages.INFO_BuildingFile:
case InfrastructureMessages.INFO_CopyingProject:
case InfrastructureMessages.INFO_GeneratingProject:
return color.whiteBright;
case InfrastructureMessages.INFO_FileNotBuiltSuccessfully:
case InfrastructureMessages.INFO_ProjectNotBuiltSuccessfully:
case InfrastructureMessages.INFO_ProjectNotCopiedSuccessfully:
case InfrastructureMessages.INFO_ProjectNotGeneratedSuccessfully:
return color.red;
case InfrastructureMessages.INFO_FileBuiltSuccessfully:
case InfrastructureMessages.INFO_ProjectBuiltSuccessfully:
case InfrastructureMessages.INFO_ProjectCopiedSuccessfully:
case InfrastructureMessages.INFO_ProjectGeneratedSuccessfully:
return color.green;
}
return null;

View file

@ -1458,13 +1458,23 @@ KMX_DWORD CheckStatementOffsets(PFILE_KEYBOARD fk, PFILE_GROUP gp, PKMX_WCHAR co
}
} else if (*(p + 1) == CODE_CONTEXTEX) {
int contextOffset = *(p + 2);
if (contextOffset > xstrlen(context))
if (contextOffset > xstrlen(context)) {
return KmnCompilerMessages::ERROR_ContextExHasInvalidOffset;
}
// contextOffset <= 0 is tested in parse stage
for (q = context, i = 1; *q && i < contextOffset; q = incxstr(q), i++);
if(*q == UC_SENTINEL && (*(q + 1) == CODE_IFOPT || *(q + 1) == CODE_IFSYSTEMSTORE)) {
return KmnCompilerMessages::ERROR_ContextExCannotReferenceIf;
}
if(*q == UC_SENTINEL && *(q + 1) == CODE_NUL) {
return KmnCompilerMessages::ERROR_ContextExCannotReferenceNul;
}
// Due to a limitation in earlier versions of KeymanWeb, the minimum version
// for context() referring to notany() is 14.0. See #917 for details.
if (kmcmp::CompileTarget == CKF_KEYMANWEB) {
for (q = context, i = 1; *q && i < contextOffset; q = incxstr(q), i++);
if (*q == UC_SENTINEL && *(q + 1) == CODE_NOTANY) {
if(!VerifyKeyboardVersion(fk, VERSION_140)) {
return KmnCompilerMessages::ERROR_140FeatureOnlyContextAndNotAnyWeb;
@ -2267,7 +2277,9 @@ KMX_DWORD GetXStringImpl(PKMX_WCHAR tstr, PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX
}
int n1b;
n1b = atoiW(q);
if (n1b < 1 || n1b >= 0xF000) return KmnCompilerMessages::ERROR_InvalidToken;
if (n1b < 1 || n1b >= 0xF000) {
return KmnCompilerMessages::ERROR_ContextExHasInvalidOffset;
}
tstr[mx++] = UC_SENTINEL;
tstr[mx++] = CODE_CONTEXTEX;
tstr[mx++] = n1b;

View file

@ -1281,10 +1281,10 @@ TEST_F(CompilerTest, GetXStringImpl_type_c_test) {
const KMX_WCHAR tstr_context_offset_valid[] = { UC_SENTINEL, CODE_CONTEXTEX, 1, 0 };
EXPECT_EQ(0, u16cmp(tstr_context_offset_valid, tstr));
// context, CERR_InvalidToke, offset < 1
// context, ERROR_ContextExHasInvalidOffset, offset < 1
fileKeyboard.version = VERSION_60;
u16cpy(str, u"context(0)");
EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE));
EXPECT_EQ(KmnCompilerMessages::ERROR_ContextExHasInvalidOffset, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE));
// context, large offset < 0xF000, valid
fileKeyboard.version = VERSION_60;
@ -1293,10 +1293,10 @@ TEST_F(CompilerTest, GetXStringImpl_type_c_test) {
const KMX_WCHAR tstr_context_large_offset_valid[] = { UC_SENTINEL, CODE_CONTEXTEX, 61439, 0 };
EXPECT_EQ(0, u16cmp(tstr_context_large_offset_valid, tstr));
// context, KmnCompilerMessages::ERROR_InvalidToken, too large offset == 0xF000
// context, KmnCompilerMessages::ERROR_ContextExHasInvalidOffset, too large offset == 0xF000
fileKeyboard.version = VERSION_60;
u16cpy(str, u"context(61440)"); //0xF000
EXPECT_EQ(KmnCompilerMessages::ERROR_InvalidToken, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE));
EXPECT_EQ(KmnCompilerMessages::ERROR_ContextExHasInvalidOffset, GetXStringImpl(tstr, &fileKeyboard, str, u"", output, 80, 0, &newp, FALSE));
// context, KmnCompilerMessages::ERROR_60FeatureOnly_Contextn
fileKeyboard.version = VERSION_50;

View file

@ -73,7 +73,12 @@ tests = [
'k_045___deadkey_and_context',
'k_046___deadkey_and_contextex',
'k_047___caps_always_off_initially_on',
'k_048___modifier_keys_keep_context'
'k_048___modifier_keys_keep_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',
]
foreach kbd : tests

View file

@ -8,7 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
builder_describe "Keyboard project generation and conversion tool" \
@/common/include \
@/common/windows/delphi \
clean configure build test publish install
clean configure build test publish install edit
builder_parse "$@"
@ -62,3 +62,4 @@ builder_run_action build:project do_build
builder_run_action test:project do_test
builder_run_action publish:project do_publish
builder_run_action install:project cp "$DEVELOPER_PROGRAM/kmconvert.exe" "$INSTALLPATH_KEYMANDEVELOPER/kmconvert.exe"
builder_run_action edit:project start kmconvert.dproj

View file

@ -8,7 +8,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
builder_describe "Chromium browser host process for Keyman Developer" \
@/common/include \
@/common/windows/delphi \
clean configure build test publish install
clean configure build test publish install edit
builder_parse "$@"
@ -52,3 +52,5 @@ builder_run_action build:project do_build
# builder_run_action test:project do_test
builder_run_action publish:project do_publish
builder_run_action install:project cp "$DEVELOPER_PROGRAM/kmdbrowserhost.exe" "$INSTALLPATH_KEYMANDEVELOPER/kmdbrowserhost.exe"
builder_run_action edit:project start kmdbrowserhost.dproj

View file

@ -9,7 +9,7 @@ builder_describe \
"Keyman Developer Setup bootstrap executable" \
@/common/include \
@/common/windows/delphi \
clean configure build test publish
clean configure build test publish edit
builder_parse "$@"
@ -55,3 +55,4 @@ builder_run_action configure:project configure_windows_build_environment
builder_run_action build:project do_build
builder_run_action publish:project do_publish
# builder_run_action test:project do_test
builder_run_action edit:project start setup.dproj

View file

@ -9,7 +9,7 @@ builder_describe "Build Keyman Developer IDE" \
@/common/include \
@/core:x86 \
@/common/windows/delphi \
clean configure build test publish install
clean configure build test publish install edit
builder_parse "$@"
@ -113,6 +113,7 @@ builder_run_action build:project do_build
# builder_run_action test:project do_test
builder_run_action publish:project do_publish
builder_run_action install:project do_install
builder_run_action edit:project start tike.dproj
# Note: generating monaco installer:
# @echo *******************************************************************************************

View file

@ -31,6 +31,7 @@ inherited frmLdmlKeyboardDebug: TfrmLdmlKeyboardDebug
Font.Name = 'Arial'
Font.Style = []
ParentFont = False
PlainText = True
PopupMenu = mnuPopup
ScrollBars = ssVertical
TabOrder = 0

View file

@ -412,7 +412,7 @@ procedure TfrmLdmlKeyboardDebug.Run(vkey: Word);
if dk.SavedPosition < 0 then
begin
dk.Position := L + dk.SavedPosition;
if (dk.Position < 0) or (dk.Position >= L) or (s[dk.Position + 1] <> #$FFFC) then
if (dk.Position < 0) or (dk.Position >= L) or (s[dk.Position + 1] <> DeadKey_Marker) then
// This can happen if the deadkey was in a replaced selection
dk.Delete;
end;
@ -481,7 +481,7 @@ procedure TfrmLdmlKeyboardDebug.Run(vkey: Word);
// Returns the length of the context items in UTF16, treating markers as a
// single UTF16 code unit (U+FFFC)
// single UTF16 code unit (DeadKey_Marker)
function ContextItemsLengthUTF16(pc: pkm_core_context_item): Integer;
begin
Result := 0;
@ -519,7 +519,7 @@ procedure TfrmLdmlKeyboardDebug.Run(vkey: Word);
dk.Position := Result.Length + offset;
dk.SavedPosition := Result.Length + offset;
FDeadkeys.Add(dk);
Result := Result + #$FFFC;
Result := Result + DeadKey_Marker;
end;
Inc(pc);
end;

View file

@ -31,6 +31,7 @@ inherited frmDebug: TfrmDebug
Font.Name = 'Arial'
Font.Style = []
ParentFont = False
PlainText = True
PopupMenu = mnuPopup
ScrollBars = ssVertical
TabOrder = 0

View file

@ -679,7 +679,7 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
dk: TDeadKeyInfo;
begin
Result.Selection := memo.Selection;
Result.Length := Length(memo.Text);
Result.Length := Length(memo.GetTextCR);
for dk in FDeadkeys do
begin
if dk.Position >= Result.Selection.Start
@ -694,13 +694,13 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
L: Integer;
s: string;
begin
s := memo.Text;
s := memo.GetTextCR;
L := Length(s);
for dk in FDeadkeys do
if dk.SavedPosition < 0 then
begin
dk.Position := L + dk.SavedPosition;
if (dk.Position < 0) or (dk.Position >= L) or (s[dk.Position + 1] <> #$FFFC) then
if (dk.Position < 0) or (dk.Position >= L) or (s[dk.Position + 1] <> DeadKey_Marker) then
// This can happen if the deadkey was in a replaced selection
dk.Delete;
end;
@ -710,6 +710,7 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
procedure DoBackspace(BackspaceType: km_core_backspace_type; ExpectedValue: NativeUInt);
var
t: string;
m, n: Integer;
dk: TDeadKeyInfo;
state: TMemoSelectionState;
@ -721,7 +722,7 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
'ExpectedValue=U+'+IntToHex(ExpectedValue, 4)+'; '+
'memo.SelStart='+IntToStr(memo.SelStart)+'; '+
'memo.SelLength='+IntToStr(memo.SelLength)+'; '+
'memo.Text='+Copy(memo.Text, 1, 256);
'memo.Text='+Copy(memo.GetTextCR, 1, 256);
end;
begin
// Offset is zero-based, but string is 1-based. Beware!
@ -740,11 +741,14 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
Exit;
end;
t := memo.GetTextCR;
case BackspaceType of
KM_CORE_BT_MARKER:
begin
Assert(m >= 1, AssertionMessage);
Assert(memo.Text[m] = #$FFFC, AssertionMessage);
Assert(m <= t.Length, AssertionMessage);
Assert(t[m] = DeadKey_Marker, AssertionMessage);
dk := FDeadkeys.GetFromPosition(m-1);
Assert(Assigned(dk));
dk.Delete;
@ -753,23 +757,19 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
KM_CORE_BT_CHAR:
begin
Assert(m >= 1, AssertionMessage);
Assert(memo.Text[m] <> #$FFFC, AssertionMessage);
Assert(m <= t.Length, AssertionMessage);
Assert(t[m] <> DeadKey_Marker, AssertionMessage);
// Delete surrogate pairs
if (m > 1) and
Uni_IsSurrogate2(memo.Text[m]) and
Uni_IsSurrogate1(memo.Text[m-1]) then
Dec(m, 2)
// Delete \r\n line breaks
else if (m > 1) and
(memo.Text[m] = #$0A) and
(memo.Text[m-1] = #$0D) then
Uni_IsSurrogate2(t[m]) and
Uni_IsSurrogate1(t[m-1]) then
Dec(m, 2)
else
Dec(m);
end;
KM_CORE_BT_UNKNOWN:
begin
while (m >= 1) and (memo.Text[m] = #$FFFC) do
while (m >= 1) and (t[m] = DeadKey_Marker) do
begin
dk := FDeadkeys.GetFromPosition(m-1);
Assert(Assigned(dk), AssertionMessage);
@ -779,14 +779,14 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
// Delete character
if (m > 1) and
Uni_IsSurrogate2(memo.Text[m]) and
Uni_IsSurrogate1(memo.Text[m-1]) then
Uni_IsSurrogate2(t[m]) and
Uni_IsSurrogate1(t[m-1]) then
Dec(m, 2)
else
Dec(m);
// Also delete deadkeys to left of current character
while (m >= 1) and (memo.Text[m] = #$FFFC) do
while (m >= 1) and (t[m] = DeadKey_Marker) do
begin
dk := FDeadkeys.GetFromPosition(m-1);
Assert(Assigned(dk), AssertionMessage);
@ -800,7 +800,7 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
memo.Lines.BeginUpdate;
try
memo.Text := Copy(memo.Text, 1, m) + Copy(memo.Text, n+1, MaxInt);
memo.Text := Copy(t, 1, m) + Copy(t, n+1, MaxInt);
memo.SelStart := m;
finally
memo.Lines.EndUpdate;
@ -830,7 +830,7 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
begin
state := SaveMemoSelectionState;
memo.SelText := WideChar($FFFC);
memo.SelText := DeadKey_Marker;
memo.SelStart := memo.SelStart + memo.SelLength; // I1603
memo.SelLength := 0;
dk.Position := memo.SelStart - 1;
@ -843,27 +843,6 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
end;
end;
procedure DoHandleShortcut(vk: UINT);
begin
// Because we disable shortcuts in the debug memo, there are a small set of
// editor Ctrl+Key shortcuts we need to rehandle here. Note that Ctrl+Ins,
// Shift+Ins, Shift+Del appear to be handled natively by the debug memo
// control (Windows code?), as are all cursor movement / selection keys,
// apart from Ctrl+A
if GetKeyState(VK_CONTROL) >= 0 then
Exit;
case vk of
Ord('A'): memo.SelectAll;
Ord('C'): memo.CopyToClipboard;
Ord('V'): memo.PasteFromClipboard;
Ord('X'): memo.CutToClipboard;
Ord('Y'): ; // redo not supported in edit
Ord('Z'): memo.Undo;
end;
end;
procedure DoEmitKeystroke(dwData: DWord);
var
flag: Integer;
@ -889,8 +868,6 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
else msg.Msg := WM_KEYUP;
memo.Dispatch(msg);
DoHandleShortcut(LOBYTE(dwData));
RealignMemoSelectionState(state);
end;
end;
@ -898,11 +875,13 @@ procedure TfrmDebug.ExecuteEventAction(n: Integer);
procedure DoChar(const text: string);
var
state: TMemoSelectionState;
selStart: Integer;
begin
state := SaveMemoSelectionState;
// Line breaks: replace \r (0x0D) with \r\n (0x0D 0x0A) so line breaks work
memo.SelText := ReplaceStr(Text, #$0D, #$0D#$0A);
memo.SelStart := memo.SelStart + memo.SelLength; // I1603
selStart := memo.SelStart;
memo.SelText := text;
memo.SelStart := selStart + text.Length;
memo.SelLength := 0;
RealignMemoSelectionState(state);
end;
@ -1357,20 +1336,22 @@ end;
procedure TfrmDebug.UpdateCharacterGrid; // I4808
var
start, len: Integer;
t: string;
begin
if csDestroying in ComponentState then
Exit;
start := memo.SelStart;
len := memo.SelLength;
if start + len > Length(memo.Text) then
t := memo.GetTextCR;
if start + len > t.Length then
begin
// RichEdit has a virtual final character, which is selected when
// pressing Ctrl+A, etc.
len := Length(memo.Text) - start;
len := t.Length - start;
end;
TCharacterGridRenderer.Fill(sgChars, memo.Text, FDeadkeys, start, len, memo.Selection.Anchor);
TCharacterGridRenderer.Fill(sgChars, t, FDeadkeys, start, len, memo.Selection.Anchor);
TCharacterGridRenderer.Size(sgChars, memo.Font);
end;

View file

@ -3000,7 +3000,7 @@ begin
else kbdparser.KeyboardText := frameSource.EditorText;
kbdparser.AddRequiredLines;
kbdparser.SetSystemStoreValue(ssTargets, 'windows native');
kbdparser.SetSystemStoreValue(ssTargets, 'desktop');
kbdparser.SetSystemStoreValue(ssVersion, SKeymanVersion90);
kbdparser.DeleteSystemStore(ssVisualKeyboard);
kbdparser.DeleteSystemStore(ssBitmap);

View file

@ -236,7 +236,7 @@ inherited frmPackageEditor: TfrmPackageEditor
Width = 546
Height = 32
AutoSize = False
Caption =
Caption =
'A typical package will need keyboards, fonts, and documentation.' +
' You shouldn'#39't typically add source files. Also, don'#39't add any s' +
'tandard Keyman files (such as keyman.exe) here.'
@ -1342,7 +1342,7 @@ inherited frmPackageEditor: TfrmPackageEditor
Top = 48
Width = 551
Height = 13
Caption =
Caption =
'Compiling the package takes all the files you have selected and ' +
'compresses them into a single package file.'
end
@ -1617,11 +1617,4 @@ inherited frmPackageEditor: TfrmPackageEditor
Left = 32
Top = 492
end
object dlgNewCustomisation: TSaveDialog
DefaultExt = 'kct'
Filter = 'Customisation file (*.kct)|*.kct|All files (*.*)|*.*'
Options = [ofOverwritePrompt, ofHideReadOnly, ofPathMustExist, ofEnableSizing]
Left = 32
Top = 532
end
end

View file

@ -68,7 +68,6 @@ uses
type
TfrmPackageEditor = class(TfrmTikeEditor) // I4689
dlgFiles: TOpenDialog;
dlgNewCustomisation: TSaveDialog;
pages: TLeftTabbedPageControl;
pageFiles: TTabSheet;
pageDetails: TTabSheet;
@ -628,6 +627,7 @@ begin
end;
end;
dlgFiles.InitialDir := ExtractFileDir(FileName);
UpdateData;
Result := True;
end;
@ -806,8 +806,10 @@ begin
if Found then Continue; // Don't add the file again
AddFile(Files[i]);
dlgFiles.InitialDir := ExtractFileDir(Files[i]);
end;
if lbFiles.CanFocus then lbFiles.SetFocus; // I3100 // I3506
dlgFiles.FileName := '';
end;
procedure TfrmPackageEditor.cmdRemoveFileClick(Sender: TObject);

View file

@ -12,6 +12,8 @@ function GetContextFromMemo(Memo: TKeymanDeveloperDebuggerMemo; DeadKeys: TDebug
implementation
uses
System.SysUtils,
Unicode;
function GetContextFromMemo(Memo: TKeymanDeveloperDebuggerMemo; DeadKeys: TDebugDeadkeyInfoList; IncludeMarkers: Boolean): TArray<km_core_context_item>;
@ -19,21 +21,23 @@ var
n, i: Integer;
ch: Char;
dk: TDeadKeyInfo;
t: string;
begin
n := 0;
SetLength(Result, Length(Memo.Text)+1);
t := Memo.GetTextCR;
SetLength(Result, t.Length+1);
i := 1;
while i <= Memo.SelStart + Memo.SelLength do
begin
ch := Memo.Text[i];
if Uni_IsSurrogate1(ch) and (i < Length(Memo.Text)) and
Uni_IsSurrogate2(Memo.Text[i+1]) then
ch := t[i];
if Uni_IsSurrogate1(ch) and (i < t.Length) and
Uni_IsSurrogate2(t[i+1]) then
begin
Result[n]._type := KM_CORE_CT_CHAR;
Result[n].character := Uni_SurrogateToUTF32(ch, Memo.Text[i+1]);
Result[n].character := Uni_SurrogateToUTF32(ch, t[i+1]);
Inc(i);
end
else if Ord(ch) = $FFFC then
else if ch = DeadKey_Marker then
begin
if IncludeMarkers then
begin

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