Merge branch 'beta' into fix/windows/12660/preserve-report-usage

This commit is contained in:
rc-swag 2025-02-19 11:41:32 +10:00 • committed by GitHub
commit c39379bbe9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
101 changed files with 1027 additions and 498 deletions

View file

@ -1,5 +1,40 @@
# Keyman Version History
## 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 @@
18.0.192
18.0.196

View file

@ -67,6 +67,7 @@ android {
productFlavors {
}
String tier = new File('../../TIER.md').getText('UTF-8').trim();
applicationVariants.all { variant ->
// Append tag to app name to help differentiate versions for app testers:
// - stable builds have no suffix
@ -75,7 +76,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 +128,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 (String.valueOf(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
@ -250,8 +256,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

@ -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,32 @@ 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
// 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);
}
// TODO: Send user to pick another system keyboard
return;
}
SystemKeyboardWebViewClient = new KMKeyboardWebViewClient(appContext, keyboardType);
keyboard = SystemKeyboard;
webViewClient = SystemKeyboardWebViewClient;
@ -722,6 +744,7 @@ public final class KMManager {
}
keyboard.setBanner(KMManager.BannerType.HTML);
keyboard.showBanner(true);
sendLongpressDelay();
}
setEngineWebViewVersionStatus(appContext, keyboard);
}
@ -776,6 +799,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 +948,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 +2151,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 +2167,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 +2330,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 +2364,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 +2383,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,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

@ -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,34 @@
<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 title: System WebView not installed or enabled -->
<string name="alert_dialog_title_webview_missing">
Fatal Error: WebView Missing</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: Switch to another keyboard -->
<string name="body_switch_keyboard" comment="Direct user to select another keyboard">
Keyman requires WebView to be installed and enabled.\n\n
Switch to a different keyboard.
</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 +208,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 +256,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 +278,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 +305,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)

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

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

@ -40,7 +40,7 @@
"valuesFormatting": "parseText"
},
{
// start: gdbserver 10.3.0.58:2345 /media/sf_Develop/keyman/keyman/linux/build/x86_64/debug/tests/ibus-keyman-tests
// start: linux/ibus-keyman/tests/scripts/run-tests.sh --remote-debug --no-surrounding-text --no-wayland -- k_001___basic_input_unicodei
// then attach debugger in vscode
"type": "gdb",
"request": "attach",

View file

@ -3,6 +3,6 @@ title: What's New in Keyman 18.0 for iPhone and iPad
---
Here are some significant changes to Keyman for iPhone and iPad 18.0:
* Minimum supported version of iOS is 13.0.
* Minimum supported version of iOS is 15.6.
* Predictive text and on screen keyboard startup performance improvements (#11784, #11264, #11265)
* Keyboard size can now be adjusted to your preference (#12571)

View file

@ -1711,7 +1711,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SystemKeyboard/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1757,7 +1757,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SystemKeyboard/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1805,7 +1805,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = KeymanEngineTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1849,7 +1849,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = KeymanEngineTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1900,7 +1900,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = KeymanEngine/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1955,7 +1955,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = KeymanEngine/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1979,7 +1979,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = F27FCAF6157FD95E00FBBA20 /* Keyman-lib.xcconfig */;
buildSettings = {
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
PRODUCT_NAME = "$(TARGET_NAME)";
STRIP_INSTALLED_PRODUCT = NO;
STRIP_STYLE = all;
@ -1992,7 +1992,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = F27FCAF6157FD95E00FBBA20 /* Keyman-lib.xcconfig */;
buildSettings = {
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = NO;
};
@ -2021,7 +2021,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = "KeymanEngine/resources/Keyman.bundle/Contents/Resources/KeymanEngine-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2058,7 +2058,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = "KeymanEngine/resources/Keyman.bundle/Contents/Resources/KeymanEngine-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2110,7 +2110,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
KEYMAN_ROOT = "$(SRCROOT)/../../..";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
@ -2151,7 +2151,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = KeymanEngine/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2175,7 +2175,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = F27FCAF6157FD95E00FBBA20 /* Keyman-lib.xcconfig */;
buildSettings = {
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTS_MACCATALYST = NO;
};
@ -2194,7 +2194,7 @@
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 3YE4W86L3G;
ENABLE_BITCODE = NO;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2244,7 +2244,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SystemKeyboard/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2292,7 +2292,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = KeymanEngineTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2333,7 +2333,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = "KeymanEngine/resources/Keyman.bundle/Contents/Resources/KeymanEngine-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2385,7 +2385,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
KEYMAN_ROOT = "$(SRCROOT)/../../..";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG NO_SENTRY";
@ -2428,7 +2428,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
KEYMAN_ROOT = "$(SRCROOT)/../../..";
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
@ -2449,7 +2449,7 @@
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 3YE4W86L3G;
ENABLE_BITCODE = NO;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -2482,7 +2482,7 @@
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 3YE4W86L3G;
ENABLE_BITCODE = NO;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View file

@ -13,11 +13,7 @@ import UIKit
extension Colors {
public static var systemBackground: UIColor {
get {
if #available(iOS 13.0, *) {
return UIColor.systemBackground
} else {
return UIColor.white
}
return UIColor.systemBackground
}
}

View file

@ -208,13 +208,6 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
edgesForExtendedLayout = []
rightNavigationMode = .next
if #available(*, iOS 13.4) {
// No icon issues here.
} else {
tabVC.tabBar.items![0].titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -16)
tabVC.tabBar.items![1].titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -16)
}
}
// If there's only one language in the package, hide the language picker.
@ -357,9 +350,7 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
self.pickingCompletionHandler(selectedResources.map { $0.typedFullID })
// Prevent swipe dismissal.
if #available(iOS 13.0, *) {
self.isModalInPresentation = true
}
self.isModalInPresentation = true
// No more selection-manipulation allowed.
// This matters when there's no welcome page available.
@ -376,9 +367,7 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
// First, show the package's welcome - if it exists.
if let welcomeVC = PackageWebViewController(for: package, page: .welcome) {
// Prevent swipe dismissal.
if #available(iOS 13.0, *) {
welcomeVC.isModalInPresentation = true
}
welcomeVC.isModalInPresentation = true
let subNavVC = UINavigationController(rootViewController: welcomeVC)
_ = subNavVC.view
@ -502,11 +491,7 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
private func setCellStyle(_ cell: UITableViewCell, style: CellStyle) {
var textColor: UIColor
if #available(*, iOS 13.0) {
textColor = .label
} else {
textColor = .black
}
textColor = .label
switch style {
case .none:

View file

@ -41,6 +41,16 @@ private class CustomInputView: UIInputView, UIInputViewAudioFeedback {
super.init(coder: coder)
}
public func destroy() {
// In app-extension mode, there are scenarios in which this class does not properly
// deallocate! We need to help that process along. In particular, doing this allows
// us to guarantee that the WebView is allowed to be GC'd, even when Apple fails to GC
// this (`CustomInputView`) instance - which actually happens. (Refer to #12216.)
keymanWeb.removeFromParent()
keymanWeb.destroy()
keymanWeb = nil
}
public var enableInputClicksWhenVisible: Bool {
get {
// Implemented as noted by https://developer.apple.com/documentation/uikit/uidevice/1620050-playinputclick.
@ -98,37 +108,37 @@ private class CustomInputView: UIInputView, UIInputViewAudioFeedback {
*/
func keyboardHeightChanged() {
os_log("CustomInputView keyboardHeightChanged", log: KeymanEngineLogger.ui, type: .info)
// deactivate constraints for both orientations (though one should already be inactive)
landscapeConstraint?.isActive = false
portraitConstraint?.isActive = false
// rebuild both portrait and landscape constraints
self.buildKeyboardHeightConstraints(bannerHeight: InputViewController.topBarHeight)
// activate constraints for the current orientation
if InputViewController.isPortrait {
portraitConstraint?.isActive = true
} else {
landscapeConstraint?.isActive = true
}
self.setNeedsLayout()
}
private func buildKeyboardHeightConstraints(bannerHeight: CGFloat) {
os_log("CustomInputView buildKeyboardHeightConstraints", log: KeymanEngineLogger.ui, type: .info)
let innerView = keymanWeb.view!
// Cannot be met by the in-app keyboard, but helps to 'force' height for the system keyboard.
let portraitHeightConstraint = innerView.heightAnchor.constraint(equalToConstant: bannerHeight + keymanWeb.readKeyboardHeight(isPortrait: true)!)
portraitHeightConstraint.identifier = "Height constraint for portrait mode"
portraitHeightConstraint.priority = .defaultHigh
let landscapeHeightConstraint = innerView.heightAnchor.constraint(equalToConstant: bannerHeight + keymanWeb.readKeyboardHeight(isPortrait: false)!)
landscapeHeightConstraint.identifier = "Height constraint for landscape mode"
landscapeHeightConstraint.priority = .defaultHigh
portraitConstraint = portraitHeightConstraint
landscapeConstraint = landscapeHeightConstraint
// .isActive will be set according to the current portrait/landscape perspective.
@ -166,6 +176,8 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
var portraitConstraint: NSLayoutConstraint?
var landscapeConstraint: NSLayoutConstraint?
var outerWidthConstraint: NSLayoutConstraint?
private var keymanWeb: KeymanWebViewController
private var swallowBackspaceTextChange: Bool = false
@ -206,7 +218,7 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
keymanWeb = KeymanWebViewController(storage: Storage.active)
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
var message = self.hasFullAccess ? "hasFullAccess: true" : "hasFullAccess: false"
let message = self.hasFullAccess ? "hasFullAccess: true" : "hasFullAccess: false"
os_log("%{public}s", log: KeymanEngineLogger.settings, type: .default, message)
SentryManager.breadcrumb(message)
@ -217,6 +229,12 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
fatalError("init(coder:) has not been implemented")
}
deinit {
inputView?.removeFromSuperview()
(inputView as? CustomInputView)?.destroy()
inputView = nil
}
open override func updateViewConstraints() {
resetKeyboardState()
@ -301,6 +319,16 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
}
}
open override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if outerWidthConstraint != nil {
outerWidthConstraint?.isActive = false
self.inputView?.removeConstraint(self.outerWidthConstraint!)
outerWidthConstraint = nil
}
}
open override func textDidChange(_ textInput: UITextInput?) {
// Swallows self-triggered calls from emptying the context due to keyboard rules
if self.swallowBackspaceTextChange && textDocumentProxy.documentContextBeforeInput == nil {
@ -368,7 +396,7 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
}
}
}
func deleteSelection() -> Bool {
if let selected = textDocumentProxy.selectedText, selected.count > 0 {
/*
@ -381,23 +409,23 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
/*
We have a problem to resolve here: we cannot simply delete the selection
with either .deleteBackward() or .insertText(""):
- If there is selected text immediately following a space (U+0020),
.deleteBackward() will delete that space IN ADDITION to the selected text.
- Unlike .insertText("-any-string-here"), .insertText("") does nothing;
it does not replace the selection with the new string.
Our policy (#9073) on handling the backspace key when there is a
text selection is to just delete the selection. We have to override
the special case of space being deleted by .deleteBackward() ourselves.
Additionally, the internal Web engine cannot anticipate the special
case and requires precise and consistent backspace handling in line
with our policy in order to keep the context on both sides synchronized.
As .insertText() does not delete the selection if the string to
be inserted is empty, we insert something that won't combine,
like a ZWNJ, and then delete it.
iOS does not allow users to select text in a way that splits
character clusters. This implies that it's impossible for an
inserted ZWNJ to combine with existing context, making this
@ -405,14 +433,14 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
*/
textDocumentProxy.insertText("\u{200c}")
textDocumentProxy.deleteBackward()
let afterManipulation = textDocumentProxy.documentContextBeforeInput ?? ""
// And now to finish our 'canary' check.
if beforeManipulation != afterManipulation {
os_log(.error, log: KeymanEngineLogger.engine, "Could not cleanly execute backspace for selected text")
}
return true
}
return false
@ -527,11 +555,15 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
}
// These require the view to appear - parent and our relationship with it must exist!
// ... wait, is THIS possibly a leak source?
private func setOuterConstraints() {
var baseWidthConstraint: NSLayoutConstraint
baseWidthConstraint = self.inputView!.widthAnchor.constraint(equalTo: parent!.view.safeAreaLayoutGuide.widthAnchor)
baseWidthConstraint.priority = UILayoutPriority(rawValue: 999)
baseWidthConstraint.isActive = true
guard outerWidthConstraint == nil else {
outerWidthConstraint!.isActive = true
return
}
outerWidthConstraint = self.inputView!.widthAnchor.constraint(equalTo: parent!.view.safeAreaLayoutGuide.widthAnchor)
outerWidthConstraint!.priority = UILayoutPriority(rawValue: 999)
outerWidthConstraint!.isActive = true
}
public var kmwHeight: CGFloat {
@ -560,7 +592,7 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
customInputView.keyboardHeightChanged()
}
}
func fixLayout() {
view.setNeedsLayout()
view.layoutIfNeeded()
@ -589,7 +621,7 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
}
// KeymanWebViewController maintenance methods
func reload() {
public func reload() {
keymanWeb.reloadKeyboard()
}
@ -624,10 +656,18 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
keymanWeb.showHelpBubble()
}
func dismissHelpBubble() {
keymanWeb.dismissHelpBubble()
}
func showHelpBubble(afterDelay delay: TimeInterval) {
keymanWeb.showHelpBubble(afterDelay: delay)
}
internal func enforceKeyboardSize() {
keymanWeb.resizeKeyboard()
}
func clearText() {
setContextState(text: nil, range: NSRange(location: 0, length: 0))
keymanWeb.resetContext()

View file

@ -26,7 +26,9 @@ class KeyboardMenuView: UIView, UITableViewDelegate, UITableViewDataSource, UIGe
private var tableView: UITableView?
private let closeButtonTitle: String?
private var _inputViewController: InputViewController?
// Is populated during init from a weak ref by KeymanWebViewController,
// and instances of this class are owned by the same.
private weak var _inputViewController: InputViewController?
override var inputViewController: InputViewController? {
return _inputViewController
}

View file

@ -72,14 +72,6 @@ class KeymanWebViewController: UIViewController {
super.init(nibName: nil, bundle: nil)
_ = view
NotificationCenter.default.addObserver(
forName: UIApplication.willEnterForegroundNotification,
object: nil,
queue: OperationQueue.main
) { _ in
self.reloadKeyboard()
}
}
required init?(coder aDecoder: NSCoder) {
@ -154,19 +146,12 @@ class KeymanWebViewController: UIViewController {
view = webView
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow),
name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide),
name: UIResponder.keyboardWillHideNotification, object: nil)
reloadKeyboard()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if #available(iOSApplicationExtension 14.0, *) {
self.userContentController.removeAllScriptMessageHandlers()
}
self.userContentController.removeAllScriptMessageHandlers()
self.userContentController.add(self, name: keymanWebViewName)
}
@ -186,6 +171,18 @@ class KeymanWebViewController: UIViewController {
// MARK: - JavaScript functions
extension KeymanWebViewController {
// In app-extension mode, there are scenarios in which this class does not properly
// deallocate! We need to help that process along.
func destroy() {
// Message handlers can maintain references.
if #available(iOSApplicationExtension 14.0, *) {
self.userContentController.removeAllScriptMessageHandlers()
} else {
self.userContentController.removeScriptMessageHandler(forName: keymanWebViewName)
}
view = nil
}
func languageMenuPosition(_ completion: @escaping (CGRect) -> Void) {
webView!.evaluateJavaScript("langMenuPos();") { result, _ in
guard let result = result as? String, !result.isEmpty else {
@ -990,18 +987,3 @@ extension KeymanWebViewController {
return keyboardMenuView != nil
}
}
// MARK: - Keyboard Notifications
extension KeymanWebViewController {
@objc func keyboardWillShow(_ notification: Notification) {
resizeKeyboard()
if Manager.shared.isKeymanHelpOn {
showHelpBubble(afterDelay: 1.5)
}
}
@objc func keyboardWillHide(_ notification: Notification) {
dismissHelpBubble()
}
}

View file

@ -355,14 +355,8 @@ class KeyboardHeightViewController: UIViewController {
var arrowSymbolImage: UIImage? = nil
keyboardResizer.isUserInteractionEnabled = true
// if using iOS earlier than 13.0, use bitmap image of arrow and non-dynamic gray
if #available(iOSApplicationExtension 13.0, *) {
arrowSymbolImage = UIImage(systemName: "arrow.up.and.down.square.fill")
keyboardResizer.backgroundColor = UIColor.systemGray3 // adapts for light/dark mode
} else {
arrowSymbolImage = UIImage(named:"arrow.up.and.down", in:engineBundle, compatibleWith:nil)
keyboardResizer.backgroundColor = UIColor.systemGray
}
arrowSymbolImage = UIImage(systemName: "arrow.up.and.down.square.fill")
keyboardResizer.backgroundColor = UIColor.systemGray3 // adapts for light/dark mode
keyboardResizer.image = arrowSymbolImage
keyboardResizer.layer.cornerRadius = 5
contentView.addSubview(keyboardResizer)

View file

@ -165,6 +165,7 @@ public class Manager: NSObject, UIGestureRecognizerDelegate {
}
set(value) {
// The system keyboard will set itself here when its `viewDidAppear` triggers.
_inputViewController = value
}
}
@ -239,10 +240,35 @@ public class Manager: NSObject, UIGestureRecognizerDelegate {
SentryManager.capture(error, message:message)
}
}
// Keep these within our singleton Manager instance. As we never remove these,
// setting them within something prone to replacement (like the keyboard when in
// app-extension mode) can lead to memory leaks. See #12216.
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow), //
name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide), //
name: UIResponder.keyboardWillHideNotification, object: nil)
// We used to preload the old KeymanWebViewController, but now that it's embedded within the
// InputViewController, that's not exactly viable.
}
// MARK: - Keyboard Notifications
// Do NOT place these anywhere within InputViewController or its children.
// So far as we can tell, the OS causes the app extension to sometimes throw
// instances away, and maintaining references via notification to them can
// cause memory leaks. Refer to #12216.
@objc func keyboardWillShow(_ notification: Notification) {
inputViewController?.enforceKeyboardSize()
if Manager.shared.isKeymanHelpOn {
inputViewController?.showHelpBubble(afterDelay: 1.5)
}
}
@objc func keyboardWillHide(_ notification: Notification) {
inputViewController?.dismissHelpBubble()
}
// MARK: - Keyboard management

View file

@ -43,15 +43,13 @@ class PackageWebViewController: UIViewController, WKNavigationDelegate {
let prefs = WKPreferences()
prefs.javaScriptEnabled = true
if #available(iOS 13.0, *) {
/**
In iPadOS 16 and above WKWebView defaults to lying about its user-agent,
telling the web server that it is a mac. We can avoid this with .mobile:
*/
let pref = WKWebpagePreferences.init()
pref.preferredContentMode = .mobile
config.defaultWebpagePreferences = pref
}
/**
In iPadOS 16 and above WKWebView defaults to lying about its user-agent,
telling the web server that it is a mac. We can avoid this with .mobile:
*/
let pref = WKWebpagePreferences.init()
pref.preferredContentMode = .mobile
config.defaultWebpagePreferences = pref
// Inject a meta viewport tag into the head of the file if it doesn't exist
let metaViewportInjection = """

View file

@ -104,15 +104,13 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat
let config = WKWebViewConfiguration()
if #available(iOS 13.0, *) {
/**
In iPadOS 16 and above WKWebView defaults to lying about its user-agent,
telling the web server that it is a mac. We can avoid this with .mobile:
*/
let pref = WKWebpagePreferences.init()
pref.preferredContentMode = .mobile
config.defaultWebpagePreferences = pref
}
/**
In iPadOS 16 and above WKWebView defaults to lying about its user-agent,
telling the web server that it is a mac. We can avoid this with .mobile:
*/
let pref = WKWebpagePreferences.init()
pref.preferredContentMode = .mobile
config.defaultWebpagePreferences = pref
let webView = WKWebView.init(frame: CGRect.zero, configuration: config)
webView.navigationDelegate = self

View file

@ -11,4 +11,4 @@ PRODUCT_NAME = $(TARGET_NAME)
GCC_PRECOMPILE_PREFIX_HEADER = YES
GCC_PREFIX_HEADER = KME-iphoneos/KME-iphoneos-Prefix.pch
IPHONEOS_DEPLOYMENT_TARGET = 4.0
IPHONEOS_DEPLOYMENT_TARGET = 15.6

View file

@ -14,4 +14,4 @@ GCC_PREFIX_HEADER = KME-iphonesimulator/KME-iphonesimulator-Prefix.pch
ARCHS = i386
VALID_ARCHS = i386
IPHONEOS_DEPLOYMENT_TARGET = 4.0
IPHONEOS_DEPLOYMENT_TARGET = 15.6

View file

@ -11,4 +11,4 @@ PRODUCT_NAME = $(TARGET_NAME)
GCC_PRECOMPILE_PREFIX_HEADER = YES
GCC_PREFIX_HEADER = KeymanEngine/KeymanEngine-Prefix.pch
IPHONEOS_DEPLOYMENT_TARGET = 4.0
IPHONEOS_DEPLOYMENT_TARGET = 15.6

View file

@ -1,21 +0,0 @@
{
"images" : [
{
"filename" : "arrow.up.and.down.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -226,6 +226,24 @@
/* Text showing name of spacebar caption - language + keyboard */
"menu-settings-spacebar-item-languageKeyboard" = "Jazyk a klávesnice";
/* Label for the "Adjust Keyboard Height" item on the main settings screen */
"menu-settings-adjust-keyboard-height" = "Upravit výšku klávesnice";
/* Title for the "Adjust Keyboard Height" settings child screen */
"adjust-keyboard-height-title" = "Upravit klávesnici";
/* Label for "Reset to Default Keyboard Height" button on the adjust height screen */
"button-label-reset-default-keyboard-height" = "Obnovit výchozí výšku klávesnice";
/* Instruction text to drag keyboard to resize */
"keyboard-drag-instructions" = "Tažením šipky nastavíte výšku klávesnice.";
/* Instruction to rotate to adjust landscape keyboard height (displayed when device is portrait) */
"portrait-keyboard-rotate-instructions" = "Otočte zařízení pro nastavení na šířku.";
/* Instruction to rotate to adjust portrait keyboard height (displayed when device is landscape) */
"landscape-keyboard-rotate-instructions" = "Otočte zařízení pro nastavení na výšku.";
/* Short text for notification: download failure for keyboard */
"notification-download-failure-keyboard" = "Stahování klávesnice se nezdařilo";

View file

@ -226,6 +226,24 @@
/* Text showing name of spacebar caption - language + keyboard */
"menu-settings-spacebar-item-languageKeyboard" = "ဘာသာ ကေုာံ ကဳဗုဒ်";
/* Label for the "Adjust Keyboard Height" item on the main settings screen */
"menu-settings-adjust-keyboard-height" = "ချိၚ်ဆသမၠုၚ်ကဳဗုဒ်ညိ";
/* Title for the "Adjust Keyboard Height" settings child screen */
"adjust-keyboard-height-title" = "ချိၚ်ကဳဗုဒ်";
/* Label for "Reset to Default Keyboard Height" button on the adjust height screen */
"button-label-reset-default-keyboard-height" = "ကလေၚ်ကေတ် သမၠုၚ်ကဳဗုဒ် ပကတိ";
/* Instruction text to drag keyboard to resize */
"keyboard-drag-instructions" = "သ္ဂောံချိၚ်သမၠုၚ်ကဳဗုဒ်ဂှ် ဓဇက်မလဴဂှ်ညိ";
/* Instruction to rotate to adjust landscape keyboard height (displayed when device is portrait) */
"portrait-keyboard-rotate-instructions" = "ကလေၚ်ကေတ်ဗီုပြၚ်ပကတိ";
/* Instruction to rotate to adjust portrait keyboard height (displayed when device is landscape) */
"landscape-keyboard-rotate-instructions" = "သ္ဂောံစွံလ္ပါ်ပဇူဂှ် ဗဂေတ်ကဵုစက်ညိ";
/* Short text for notification: download failure for keyboard */
"notification-download-failure-keyboard" = "ဂြဲဖျေံကဳဗုဒ်ဟွံအံၚ်ဇၞး";

View file

@ -1131,7 +1131,7 @@
GCC_PREFIX_HEADER = "Keyman/Keyman-Prefix.pch";
GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)";
INFOPLIST_FILE = Keyman/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1166,7 +1166,7 @@
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = "Keyman/Keyman-Prefix.pch";
INFOPLIST_FILE = Keyman/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1232,7 +1232,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
KEYMAN_ROOT = "$(SRCROOT)/../../..";
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = "";
@ -1284,7 +1284,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
KEYMAN_ROOT = "$(SRCROOT)/../../..";
ONLY_ACTIVE_ARCH = NO;
OTHER_CFLAGS = "-DNS_BLOCK_ASSERTIONS=1";
@ -1319,7 +1319,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
INFOPLIST_FILE = SWKeyboard/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1362,7 +1362,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
INFOPLIST_FILE = SWKeyboard/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1431,7 +1431,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
KEYMAN_ROOT = "$(SRCROOT)/../../..";
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = "";
@ -1456,7 +1456,7 @@
GCC_PREFIX_HEADER = "Keyman/Keyman-Prefix.pch";
GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)";
INFOPLIST_FILE = Keyman/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -1500,7 +1500,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
INFOPLIST_FILE = SWKeyboard/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View file

@ -67,9 +67,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// In iOS 15, navigation bars become transparent by default when the edge
// of the scrollable content aligns with the edge of the navigation bar.
// Force the appearance back to the pre-iOS 15 default behavior.
if #available(iOS 15, *) {
UINavigationBar.appearance().scrollEdgeAppearance = UINavigationBarAppearance()
}
UINavigationBar.appearance().scrollEdgeAppearance = UINavigationBarAppearance()
UniversalLinks.externalLinkLauncher = { url in
UIApplication.shared.open(url)
}

View file

@ -54,13 +54,9 @@ class WebBrowserViewController: UIViewController, WKNavigationDelegate, UIAlertV
webView.navigationDelegate = self
if #available(iOS 13.0, *) {
// Dark mode settings must be applied through this new property,
// its class, and others like it.
navBar.standardAppearance.configureWithOpaqueBackground()
} else {
// Fallback on earlier versions
}
// Dark mode settings must be applied through this new property,
// its class, and others like it.
navBar.standardAppearance.configureWithOpaqueBackground()
// Setup NavigationBar
navbarBackground = KMNavigationBarBackgroundView()
@ -119,12 +115,6 @@ class WebBrowserViewController: UIViewController, WKNavigationDelegate, UIAlertV
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if #available(iOS 13.0, *) {
// 13.0 auto-adjusts the top to avoid the status bar.
} else {
navBarTopConstraint.constant = AppDelegate.statusBarHeight()
}
}
override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {

View file

@ -112,6 +112,14 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg
forName: Notifications.keyboardRemoved,
observer: self,
function: MainViewController.keyboardRemoved)
NotificationCenter.default.addObserver(
forName: UIApplication.willEnterForegroundNotification,
object: nil,
queue: OperationQueue.main
) { _ in
Manager.shared.inputViewController.reload()
}
// Unfortunately, it's the main app with the file definitions.
// We have to gerry-rig this so that the framework-based SettingsViewController
@ -278,13 +286,7 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg
// Setup NavigationBar
if let navbar = navigationController?.navigationBar {
if #available(iOS 13.0, *) {
// Dark mode settings must be applied through this new property,
// its class, and others like it.
navbar.standardAppearance.configureWithOpaqueBackground()
} else {
// Fallback on earlier versions
}
navbar.standardAppearance.configureWithOpaqueBackground()
navbarBackground = KMNavigationBarBackgroundView()
navbarBackground.addToNavbar(navbar)
navbarBackground.setOrientation(UIApplication.shared.statusBarOrientation)
@ -695,12 +697,7 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg
let lframe = CGRect(x: 0, y: bframe.origin.y - 1, width: sframe.size.width, height: 1)
let borderLine = UIView(frame: lframe)
if #available(iOS 13.0, *) {
borderLine.backgroundColor = UIColor.separator
} else {
let c: CGFloat = 230.0 / 255.0
borderLine.backgroundColor = UIColor(red: c, green: c, blue: c, alpha: 1.0)
}
borderLine.backgroundColor = UIColor.separator
containerView.addSubview(borderLine)
let tframe = CGRect(x: 0, y: 0, width: bframe.size.width, height: 40)
@ -709,11 +706,7 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg
textSizeTitle.backgroundColor = UIColor.clear
textSizeTitle.text = String.init(format: NSLocalizedString("text-size-label", comment: ""), Int(textSize))
textSizeTitle.textAlignment = .center
if #available(iOS 13.0, *) {
textSizeTitle.textColor = UIColor.secondaryLabel
} else {
textSizeTitle.textColor = UIColor.gray
}
textSizeTitle.textColor = UIColor.secondaryLabel
textSizeTitle.font = textSizeTitle.font.withSize(14.0)
containerView.addSubview(textSizeTitle)

View file

@ -33,13 +33,6 @@ class PackageBrowserViewController: UIDocumentPickerViewController, UIDocumentPi
bundle: Bundle(for: Manager.self),
comment: "")
if #available(iOS 13.0, *) {
// Easily dismissable without the extra button.
} else {
self.navigationItem.setLeftBarButton(
UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(self.doCancel)),
animated: true)
}
self.modalPresentationStyle = .fullScreen
}

View file

@ -81,28 +81,26 @@ class KeyboardViewController: InputViewController {
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard let previousTraitCollection = previousTraitCollection else {return}
if #available(iOS 13.0, *) {
if previousTraitCollection.hasDifferentColorAppearance(comparedTo: traitCollection) {
/* Ensure that the keyboard banner image transitions! Noting that the backing view isn't in the view hierarchy,
* and that manipulating said hierarchy is key for triggering the appearance change
* (see https://developer.apple.com/documentation/uikit/uiappearance)...
*
* We have to reload the banner's backing view to trigger the change. Thanks, Apple.
* The only other alternative - creating a new instance of the view & its controller.
*
* With loadView, at least we can reuse the old instance, which would serve far better
* for engine API calls if we decide to let the base InputViewController perform UIView
* render functionality (as a KMEI offering) instead of having it only in our app.
* That said, note https://developer.apple.com/documentation/uikit/uiviewcontroller/1621454-loadview:
*
* > You should never call this method directly.
*
* Well... I wouldn't, if it weren't the only way to trigger this without requiring a new instance.
* We should be fine since we never place the banner's backing view into the actual hierarchy.
*/
topBarImageSource.loadView()
setupTopBarImage(size: view.frame.size)
}
if previousTraitCollection.hasDifferentColorAppearance(comparedTo: traitCollection) {
/* Ensure that the keyboard banner image transitions! Noting that the backing view isn't in the view hierarchy,
* and that manipulating said hierarchy is key for triggering the appearance change
* (see https://developer.apple.com/documentation/uikit/uiappearance)...
*
* We have to reload the banner's backing view to trigger the change. Thanks, Apple.
* The only other alternative - creating a new instance of the view & its controller.
*
* With loadView, at least we can reuse the old instance, which would serve far better
* for engine API calls if we decide to let the base InputViewController perform UIView
* render functionality (as a KMEI offering) instead of having it only in our app.
* That said, note https://developer.apple.com/documentation/uikit/uiviewcontroller/1621454-loadview:
*
* > You should never call this method directly.
*
* Well... I wouldn't, if it weren't the only way to trigger this without requiring a new instance.
* We should be fine since we never place the banner's backing view into the actual hierarchy.
*/
topBarImageSource.loadView()
setupTopBarImage(size: view.frame.size)
}
}
}

View file

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -296,7 +296,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = "";
@ -342,7 +342,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = "";
SDKROOT = iphoneos;
@ -365,7 +365,7 @@
"$(SRCROOT)/../../Carthage/Build/",
);
INFOPLIST_FILE = KMSample1/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -390,7 +390,7 @@
"$(SRCROOT)/../../Carthage/Build/",
);
INFOPLIST_FILE = KMSample1/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View file

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -467,7 +467,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = "";
@ -518,7 +518,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = "";
SDKROOT = iphoneos;
@ -540,7 +540,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = KMSample2/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -566,7 +566,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = KMSample2/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -593,7 +593,7 @@
"$(inherited)",
);
INFOPLIST_FILE = SWKeyboard/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -619,7 +619,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = SWKeyboard/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.1;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View file

@ -390,7 +390,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = WHE552KZN5;
INFOPLIST_FILE = CalibrationKbd/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -411,7 +411,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = WHE552KZN5;
INFOPLIST_FILE = CalibrationKbd/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -546,9 +546,9 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = KeyboardCalibrator/KeyboardCalibrator.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = WHE552KZN5;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = KeyboardCalibrator/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -567,9 +567,9 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = KeyboardCalibrator/KeyboardCalibrator.entitlements;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = WHE552KZN5;
DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = KeyboardCalibrator/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -589,7 +589,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = WHE552KZN5;
INFOPLIST_FILE = DefaultKeyboardHostTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -611,7 +611,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = WHE552KZN5;
INFOPLIST_FILE = DefaultKeyboardHostTests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -632,6 +632,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = WHE552KZN5;
INFOPLIST_FILE = DefaultKeyboardHostUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -652,6 +653,7 @@
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = WHE552KZN5;
INFOPLIST_FILE = DefaultKeyboardHostUITests/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View file

@ -2,4 +2,12 @@
title: What's New in Keyman 18.0 for Linux
---
Here are some of the new features we have added to Keyman for Linux 18.0:
Here are some significant changes to Keyman for Linux 18.0:
- Minimum supported Ubuntu version is now 22.04 Jammy LTS.
- Other supported versions are Ubuntu 24.04 Noble LTS and
Ubuntu 24.10 Oracular. Users that are running an older version of
Ubuntu will have to continue using an older Keyman version.
- Keyman no longer requires a patched version of ibus as Keyman now uses
a system service to manage keystroke order (#11535).
- Added support for simulation of AltGr (right Alt) with Ctrl+Alt (#11852)

View file

@ -15,6 +15,7 @@ fi
if ! which Xvfb > /dev/null || ! which Xephyr > /dev/null || ! which metacity > /dev/null; then
echo "# Please install Xvfb, Xephyr and metacity before running these tests!"
echo "# sudo apt install xvfb xserver-xephyr metacity"
exit 1
fi

View file

@ -24,3 +24,26 @@ To run a single test you pass the testname (as found in
```bash
scripts/run-tests.sh -- k_000___null_keyboard k_005___nul_with_initial_context
```
## Debugging tests
This is most easily done by running the tests in a VM and then remote debugging
into the VM. The VM should be the same Linux version as the host, and the
Keyman repo should be shared with the VM through a shared folder.
Then you can start the `run-tests.sh` script on the VM with the
`--remote-debug` option. This will start the background services and then
start a GDB session waiting for the debugger to connect.
For example:
```bash
linux/ibus-keyman/tests/scripts/run-tests.sh --remote-debug \
--no-surrounding-text --no-wayland -- k_001___basic_input_unicodei
```
On the host, you can then attach to the VM. See the
`Attach to gdbserver (ibus-keyman integration tests)` configuration
in `docs/settings/linux/launch.json` for a sample configuration in
vscode. You'll have to adjust the IP address to match the VM which the
`run-tests.sh` script will output.

View file

@ -16,6 +16,7 @@ fi
if ! which Xvfb > /dev/null || ! which Xephyr > /dev/null || ! which metacity > /dev/null || ! which mutter > /dev/null; then
echo "Please install Xvfb, Xephyr, metacity and mutter before running these tests!"
echo "sudo apt install xvfb xserver-xephyr metacity mutter"
exit 1
fi
@ -43,7 +44,7 @@ function help() {
function run_tests() {
# Output these lines to stderr - the first line on stdout has to be the TAP version number
# which running ${TESTFILE} outputs
echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log and /tmp/ibus-daemon.log!" >&2
echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log, /tmp/ibus-daemon.log and /tmp/km-test-server.log!" >&2
echo "" >&2
echo "# Starting tests..." >&2

View file

@ -25,6 +25,7 @@ fi
if ! which Xvfb > /dev/null || ! which Xephyr > /dev/null || ! which metacity > /dev/null || ! which mutter > /dev/null; then
echo "Please install Xvfb, Xephyr, metacity and mutter before running these tests!"
echo "sudo apt install xvfb xserver-xephyr metacity mutter"
exit 1
fi
@ -36,6 +37,7 @@ function help() {
echo " --help, -h, -? Display this help"
echo " --verbose, -v Run tests verbosely"
echo " --debug debug test logging output"
echo " --remote-debug start gdbserver after setup but prior to running the tests"
echo " -k passed to GLib testing framework"
echo " --tap output in TAP format. Passed to GLib testing framework"
echo " --surrounding-text run tests with surrounding texts enabled"
@ -86,10 +88,14 @@ function run_tests() {
source "$ENV_FILE"
echo "DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS"
if [[ -n ${REMOTE_DEBUG:-} ]]; then
echo "===> Now attach debugger to ${REMOTE_HOST}"
fi
# Note: -k and --tap are consumed by the GLib testing framework
#shellcheck disable=SC2068 # we want to split array elements!
#shellcheck disable=SC2086
"${G_TEST_BUILDDIR:-../../build/$(arch)/${CONFIG}/tests}/ibus-keyman-tests" ${ARG_K-} ${ARG_TAP-} \
${REMOTE_DEBUG:-} "${G_TEST_BUILDDIR:-../../build/$(arch)/${CONFIG}/tests}/ibus-keyman-tests" ${ARG_K-} ${ARG_TAP-} \
${ARG_VERBOSE-} ${ARG_DEBUG-} ${ARG_SURROUNDING_TEXT-} ${ARG_NO_SURROUNDING_TEXT-} \
--directory "$TESTDIR" "${DISPLAY_SERVER}" ${TESTFILES[@]}
echo "# Finished tests."
@ -111,6 +117,7 @@ while (( $# )); do
--no-x11) USE_X11=0;;
--verbose|-v) ARG_VERBOSE=--verbose;;
--debug) ARG_DEBUG=--debug-log;;
--remote-debug) REMOTE_HOST="$(ip route get 8.8.8.8 | sed -E 's/.*src (\S+) .*/\1/;t;d'):2345" && REMOTE_DEBUG="gdbserver ${REMOTE_HOST}";;
--) shift && break ;;
*) echo "Error: Unexpected argument \"$1\". Exiting." ; exit 4 ;;
esac

View file

@ -256,7 +256,7 @@ function _setup_ibus() {
echo "if kill -9 ${PID}; then ibus restart || ibus start; fi # ibus-daemon" >> "${CLEANUP_FILE}"
else
# test run as part of the build
echo "kill -9 ${PID} || true" >> "${CLEANUP_FILE}"
echo "kill -9 ${PID} || true # ibus-daemon" >> "${CLEANUP_FILE}"
fi
echo "${PID} ibus-daemon" >> "${PID_FILE}"
sleep 1s

View file

@ -88,16 +88,6 @@ ibus_keyman_tests_fixture_tear_down(IBusKeymanTestsFixture *fixture, gconstpoint
delete data;
}
static void
set_caps_lock_state(IBusKeymanTestsFixture *fixture, bool caps_lock_on) {
set_capslock_indicator((guint32)caps_lock_on);
}
static bool
get_caps_lock_state(IBusKeymanTestsFixture *fixture) {
return get_capslock_indicator();
}
static void
switch_keyboard(IBusKeymanTestsFixture *fixture, const gchar *keyboard) {
ibus_bus_set_global_engine(fixture->bus, keyboard);
@ -185,9 +175,9 @@ is_modifier(guint16 keycode) {
}
static std::list<gdk_key_event>
get_involved_keys(IBusKeymanTestsFixture *fixture, km::tests::key_event test_event) {
get_involved_keys(km::tests::key_event test_event) {
guint modifiers = 0;
if (get_caps_lock_state(fixture)) {
if (get_capslock_indicator()) {
modifiers = IBUS_LOCK_MASK;
}
@ -250,13 +240,13 @@ get_context_keys(std::u16string context) {
}
static void
press_keys(IBusKeymanTestsFixture *fixture, km::tests::KmxTestSource & test_source,km::tests::key_event key_event) {
auto involved_keys = get_involved_keys(fixture, key_event);
press_keys(IBusKeymanTestsFixture *fixture, km::tests::key_event key_event) {
auto involved_keys = get_involved_keys(key_event);
// press and hold the individual modifier keys, finally the actual key
std::stack<GdkEventKey> keys_to_release;
for (auto key = involved_keys.begin(); key != involved_keys.end(); key++) {
bool old_caps_lock = get_caps_lock_state(fixture);
bool old_caps_lock = get_capslock_indicator();
guint lock_modifier = 0;
// KEY_CAPSLOCK behaves a bit strange:
@ -267,7 +257,7 @@ press_keys(IBusKeymanTestsFixture *fixture, km::tests::KmxTestSource & test_sour
// - the LOCK_MASK modifier is set on both keypress and release
// - capslock gets turned off on release
if (key->keycode == KEY_CAPSLOCK && !old_caps_lock) {
set_caps_lock_state(fixture, true);
set_capslock_indicator((guint32)true);
lock_modifier = IBUS_LOCK_MASK;
}
GdkEventKey keyEvent = {
@ -289,7 +279,7 @@ press_keys(IBusKeymanTestsFixture *fixture, km::tests::KmxTestSource & test_sour
keys_to_release.push(keyEvent);
if (key->keycode == KEY_CAPSLOCK && old_caps_lock) {
set_caps_lock_state(fixture, false);
set_capslock_indicator((guint32) false);
}
}
@ -326,19 +316,19 @@ static void test_source(IBusKeymanTestsFixture *fixture, gconstpointer user_data
g_settings_sync();
switch_keyboard(fixture, kmxfile.c_str());
if (test_source.caps_lock_state() != get_caps_lock_state(fixture)) {
set_caps_lock_state(fixture, test_source.caps_lock_state());
if (test_source.caps_lock_state() != get_capslock_indicator()) {
set_capslock_indicator((guint32)test_source.caps_lock_state());
}
auto contextKeys = get_context_keys(context);
for (auto k = contextKeys.begin(); k != contextKeys.end(); k++) {
press_keys(fixture, test_source, *k);
press_keys(fixture, *k);
}
gtk_im_context_focus_in(fixture->context);
for (auto p = test_source.next_key(keys); p.vk != 0; p = test_source.next_key(keys)) {
press_keys(fixture, test_source, p);
press_keys(fixture, p);
}
if (expected.length() == 0) {

View file

@ -54,6 +54,7 @@ android {
productFlavors {
}
String tier = new File('../../../TIER.md').getText('UTF-8').trim();
applicationVariants.all { variant ->
// Adjust output name to "firstvoices-${VERSION_MD}.apk"
variant.outputs.all {
@ -95,7 +96,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 (String.valueOf(tier)) {
case 'beta':
track.set("beta")
println "TIER set to beta"

View file

@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@ -581,7 +581,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = "";
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -633,7 +633,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = "";
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@ -653,7 +653,7 @@
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = D7TR486TEH;
INFOPLIST_FILE = FirstVoices/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
KEYMAN_ROOT = "$(SRCROOT)/../../..";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -682,7 +682,7 @@
CODE_SIGN_STYLE = Manual;
DEVELOPMENT_TEAM = D7TR486TEH;
INFOPLIST_FILE = FirstVoices/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
KEYMAN_ROOT = "$(SRCROOT)/../../..";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -709,7 +709,7 @@
DEVELOPMENT_TEAM = D7TR486TEH;
HEADER_SEARCH_PATHS = "";
INFOPLIST_FILE = SWKeyboard/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
KEYMAN_ROOT = "$(SRCROOT)/../../..";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@ -740,7 +740,7 @@
DEVELOPMENT_TEAM = D7TR486TEH;
HEADER_SEARCH_PATHS = "";
INFOPLIST_FILE = SWKeyboard/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
KEYMAN_ROOT = "$(SRCROOT)/../../..";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View file

@ -7,7 +7,17 @@
"reporter": ["text", "text-summary"],
"reports-dir": "build/coverage",
"src": [
"**/src/",
"src/**",
"../common/**/src"
],
"include": [
"src/**"
],
"exclude": [
"src/test/**",
"**/src/tests/**",
"**/build/**",
"src/tools/**",
"../node_modules/*"
]
}

View file

@ -35,7 +35,7 @@ builder_describe "Builds engine modules for Keyman Engine for Web (KMW)." \
":engine/keyboard-storage Subset used to collate keyboards and request them from the cloud" \
":engine/main Builds all common code used by KMW's app/-level targets" \
":engine/osk Builds the Web OSK module" \
":engine/predictive-text=src/engine/predictive-text/worker-main Builds KMW's predictive text module" \
":engine/predictive-text Builds KMW's predictive text module" \
":help Online documentation" \
":samples Builds all needed resources for the KMW sample-page set" \
":tools Builds engine-related development resources" \
@ -141,12 +141,26 @@ test_action() {
coverage_action() {
builder_echo "Creating coverage report..."
cd "$KEYMAN_ROOT"
mkdir -p web/build/coverage/tmp
find . -type f -name coverage-\*.json -print0 | xargs -0 cp -t web/build/coverage/tmp
c8 report --config web/.c8rc.json ---reporter html --clean=false --reports-dir=web/build/coverage
rm -rf web/build/coverage/tmp
cd web
# Always ensure our collation folder is clean, just in case something went wrong on a prior run.
rm -rf build/coverage/tmp
# Also clean out the sub-report directories + main index.html; don't keep old ones if we fail!
rm -rf build/coverage/app
rm -rf build/coverage/engine
rm -f build/coverage/index.html
# Collate the results into a single folder
mkdir build/coverage/tmp
find . -type f -name coverage-\*.json -print0 | xargs -0 cp -t build/coverage/tmp
# Note for maintainers: the .c8rc.json config needs to be written from the perspective of
# the working directory - including its `include` and `exclude` entries. All ignores and
# such must be redefined in whatever that top-level .c8rc.json is.
c8 report --reporter html
# No need to keep the collation folder around afterward; we can always redo that work.
rm -rf build/coverage/tmp
}
builder_run_child_actions build:engine/dom-utils

View file

@ -99,20 +99,11 @@ function test-headless() {
fi
TEST_OPTS=
TEST_CD_REQD=false
if builder_has_option --ci; then
TEST_OPTS="--reporter mocha-teamcity-reporter"
fi
if [[ -n "$TEST_EXTENSIONS" ]]; then
TEST_OPTS="$TEST_OPTS --extension $TEST_EXTENSIONS"
TEST_CD_REQD=true
fi
if [ $TEST_CD_REQD ]; then
# The mocha config needed to live-compile TS-based tests only applies
# if the command is started within the appropriate subfolder.
pushd "${TEST_BASE}" > /dev/null
TEST_BASE=
fi
if [[ -e .c8rc.json ]]; then
@ -120,10 +111,6 @@ function test-headless() {
else
mocha --recursive "${TEST_BASE}${TEST_FOLDER}" $TEST_OPTS
fi
if [ $TEST_CD_REQD ]; then
popd > /dev/null
fi
}
# Runs all headless tests (written in typescript) corresponding to the

View file

@ -6,21 +6,16 @@
<script type='text/javascript' src='js/laokeys_load.js'></script>
<script type="text/javascript">
var KWControl = null;
function SetupDocument()
{
keyman.init();
KWControl = document.getElementById('KWControl');
keyman.setActiveKeyboard('laokeys');
keyman.osk.hide();
keyman.init().then(function() {
keyman.setActiveKeyboard('laokeys');
keyman.osk.hide();
});
}
function KWControlClick()
{
var KWControl = document.getElementById('KWControl');
if(keyman.osk.isEnabled()) {
keyman.osk.hide();
} else {

View file

@ -14,13 +14,12 @@ Include the following script in the HEAD of your page:
function SetupDocument()
{
/* Make sure that Keyman is initialized (we can't guarantee initialization order) */
keyman.init();
KWControl = document.getElementById('KWControl');
/* Prevents automatic display of the onscreen keyboard. (default automatic) */
keyman.osk.hide();
/* Select the LaoKeys keyboard */
keyman.setActiveKeyboard('laokeys');
keyman.init().then(function () {
/* Prevents automatic display of the onscreen keyboard. (default automatic) */
keyman.osk.hide();
/* Select the LaoKeys keyboard */
keyman.setActiveKeyboard('laokeys');
});
}
/* KWControlClick: Called when user clicks on the KWControl IMG */
@ -56,7 +55,7 @@ And finally, include the control img for KeymanWeb:
```html
<!-- Display the KeymanWeb icon for the user to click on -->
<img style="border: solid 1px black; padding: 2px 2px 3px 2px" src='kmicon.png' alt='KeymanWeb' onclick='KWControlClick()' id='KWControl' />
<img style="border: solid 1px black; padding: 2px 2px 3px 2px" src='kmicon.png' alt='KeymanWeb' onclick='KWControlClick()' />
```
## API References

View file

@ -0,0 +1,10 @@
{
"check-coverage": false,
"clean": true,
"exclude-after-remap": true,
"reporter": ["text", "text-summary"],
"reports-dir": "../../../build/coverage/raw/app/browser",
"src": [
"src/"
]
}

View file

@ -3,7 +3,7 @@
"clean": true,
"exclude-after-remap": true,
"reporter": ["text", "text-summary"],
"reports-dir": "build/coverage",
"reports-dir": "../../../build/coverage/raw/engine/attachment",
"src": [
"src/"
]

View file

@ -10,7 +10,7 @@
],
"exclude-after-remap": true,
"reporter": ["text", "text-summary"],
"reports-dir": "build/coverage",
"reports-dir": "../../../build/coverage/raw/raw/engine/common/web-utils",
"src": [
"src/"
]

View file

@ -3,7 +3,7 @@
"clean": true,
"exclude-after-remap": true,
"reporter": ["text", "text-summary"],
"reports-dir": "build/coverage",
"reports-dir": "../../../build/coverage/raw/engine/events",
"src": [
"src/"
]

View file

@ -3,7 +3,7 @@
"clean": true,
"exclude-after-remap": true,
"reporter": ["text", "text-summary"],
"reports-dir": "../../../build/engine/interfaces/coverage",
"reports-dir": "../../../build/coverage/raw/engine/interfaces",
"src": [
"src/"
]

View file

@ -3,7 +3,7 @@
"clean": true,
"exclude-after-remap": true,
"reporter": ["text", "text-summary"],
"reports-dir": "build/coverage",
"reports-dir": "../../../build/coverage/raw/engine/keyboard-storage",
"src": [
"src/"
]

View file

@ -8,7 +8,7 @@
],
"exclude-after-remap": true,
"reporter": ["text", "text-summary"],
"reports-dir": "build/coverage",
"reports-dir": "../../../build/coverage/raw/engine/keyboard",
"src": [
"src/"
]

View file

@ -11,7 +11,7 @@
"src/engine/headless/*"
],
"reporter": ["text", "text-summary"],
"reports-dir": "build/coverage",
"reports-dir": "../../../../build/coverage/raw/engine/osk/gesture-processor",
"src": [
"src/engine/headless/*"
]

View file

@ -53,7 +53,6 @@ builder_run_child_actions configure
## Build actions
builder_run_child_actions build:wordbreakers
builder_run_child_actions build:templates
builder_run_child_actions build:worker-thread
builder_run_child_actions build:worker-main

View file

@ -7,7 +7,7 @@
],
"exclude-after-remap": true,
"reporter": ["text", "text-summary"],
"reports-dir": "build/coverage",
"reports-dir": "../../../../build/coverage/raw/engine/predictive-text/templates",
"src": [
"src/"
]

View file

@ -7,7 +7,7 @@
],
"exclude-after-remap": true,
"reporter": ["text", "text-summary"],
"reports-dir": "build/coverage",
"reports-dir": "../../../../build/coverage/raw/engine/predictive-text/wordbreakers",
"src": [
"src/"
]

View file

@ -6,6 +6,10 @@ import { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from "@keymanapp/lm-
export default class Worker {
static constructInstance(): Worker {
let scriptStr = unwrap(LMLayerWorkerCode);
const srcMapUrlIndex = scriptStr.indexOf("//# sourceMappingURL")
if(srcMapUrlIndex > 0) {
scriptStr = scriptStr.substring(0, srcMapUrlIndex);
}
scriptStr += '\n' + LMLayerWorkerSourcemapComment;
let worker = new MappedWorker(scriptStr);

View file

@ -7,7 +7,7 @@
],
"exclude-after-remap": true,
"reporter": ["text", "text-summary"],
"reports-dir": "build/coverage",
"reports-dir": "../../../../build/coverage/raw/engine/predictive-text/worker-thread",
"src": [
"src/main"
]

View file

@ -9,5 +9,6 @@ export default {
reporters: [
teamcityReporter(), /* custom-written, for CI-friendly reports */
sessionStabilityReporter({ciMode: true})
]
],
concurrency: 1
}

View file

@ -1,6 +1,7 @@
import { assert } from 'chai';
import fs from 'fs';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
@ -9,6 +10,9 @@ import { KeyboardInterface } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
import { KeyboardTest, NodeProctor } from '@keymanapp/recorder-core';
import { env } from 'node:process';
const KEYMAN_ROOT = env.KEYMAN_ROOT;
describe('Engine - Basic Simulation', function() {
let testJSONtext = fs.readFileSync(require.resolve('@keymanapp/common-test-resources/json/engine_tests/basic_lao_simulation.json'));
// Common test suite setup.
@ -25,7 +29,7 @@ describe('Engine - Basic Simulation', function() {
before(async function() {
// -- START: Standard Recorder-based unit test loading boilerplate --
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
let keyboard = await keyboardLoader.loadKeyboardFromPath('../../../../../common/test/' + testSuite.keyboard.filename);
let keyboard = await keyboardLoader.loadKeyboardFromPath(KEYMAN_ROOT + '/common/test/' + testSuite.keyboard.filename);
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;

View file

@ -10,6 +10,9 @@ import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader'
import { KeyboardTest, NodeProctor } from '@keymanapp/recorder-core';
import { ModifierKeyConstants } from '@keymanapp/common-types';
import { env } from 'node:process';
const KEYMAN_ROOT = env.KEYMAN_ROOT;
describe('Engine - Chirality', function() {
let testJSONtext = fs.readFileSync(require.resolve('@keymanapp/common-test-resources/json/engine_tests/chirality.json'));
// Common test suite setup.
@ -26,7 +29,7 @@ describe('Engine - Chirality', function() {
before(async function() {
// -- START: Standard Recorder-based unit test loading boilerplate --
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
let keyboard = await keyboardLoader.loadKeyboardFromPath('../../../../../common/test/' + testSuite.keyboard.filename);
let keyboard = await keyboardLoader.loadKeyboardFromPath(KEYMAN_ROOT + '/common/test/' + testSuite.keyboard.filename);
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;

View file

@ -9,6 +9,9 @@ import { KeyboardInterface } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
import { KeyboardTest, NodeProctor } from '@keymanapp/recorder-core';
import { env } from 'node:process';
const KEYMAN_ROOT = env.KEYMAN_ROOT;
describe('Engine - Deadkeys', function() {
let testJSONtext = fs.readFileSync(require.resolve('@keymanapp/common-test-resources/json/engine_tests/deadkeys.json'));
// Common test suite setup.
@ -25,7 +28,7 @@ describe('Engine - Deadkeys', function() {
before(async function() {
// -- START: Standard Recorder-based unit test loading boilerplate --
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
let keyboard = await keyboardLoader.loadKeyboardFromPath('../../../../../common/test/' + testSuite.keyboard.filename);
let keyboard = await keyboardLoader.loadKeyboardFromPath(KEYMAN_ROOT + '/common/test/' + testSuite.keyboard.filename);
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;

View file

@ -9,6 +9,9 @@ import { KeyboardInterface } from 'keyman/engine/js-processor';
import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader';
import { KeyboardTest, NodeProctor } from '@keymanapp/recorder-core';
import { env } from 'node:process';
const KEYMAN_ROOT = env.KEYMAN_ROOT;
describe('Engine - Unmatched Final Groups', function() {
let testJSONtext = fs.readFileSync(require.resolve('@keymanapp/common-test-resources/json/engine_tests/ghp_enter.json'));
// Common test suite setup.
@ -24,7 +27,7 @@ describe('Engine - Unmatched Final Groups', function() {
before(async function() {
// -- START: Standard Recorder-based unit test loading boilerplate --
let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal));
const keyboard = await keyboardLoader.loadKeyboardFromPath('../../../../../common/test/' + testSuite.keyboard.filename);
const keyboard = await keyboardLoader.loadKeyboardFromPath(KEYMAN_ROOT + '/common/test/' + testSuite.keyboard.filename);
keyboardWithHarness = keyboardLoader.harness;
keyboardWithHarness.activeKeyboard = keyboard;

View file

@ -4,6 +4,11 @@ title: What's New in Keyman 18.0 for Windows
Here are some of the new features we have added to Keyman 18.0 for Windows:
- Minimum supported version of Windows is 10.0
- Updates to Keyman are now applied before Keyman starts for the first time in a session, so Windows no longer needs to be restarted (#10041)
- Keyman no longer adds a desktop shortcut when it is installed (#11401)
- Added an option to make Right Alt and Right Control also work for keyboard switching hotkeys if preferred (#11471)
## Related Topics
- [Version History](history)

View file

@ -327,6 +327,10 @@
<string name="koShowWelcome" comment="Startup options - show/hide welcome screen">Zobrazit úvodní obrazovku</string>
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->
<!-- Modified: 18.0.96.0 -->
<string name="koCheckForUpdates" comment="Automatically check for updates and download">Automatically check for updates and download</string>
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->
<!-- Introduced: 7.0.236.0 -->
<string name="koTestKeymanFunctioning" comment="Startup options - test for conflicting apps when Product starts">Test pro protichůdné aplikace při spuštění klíče</string>
<!-- Context: Configuration Dialog - Options tab -->
@ -454,6 +458,22 @@
<!-- String Type: PlainText -->
<!-- Introduced: 7.0.230.0 -->
<string name="S_Menu_Diagnostics_Diagnostics" comment="Support button submenu - performs a diagnostic report">Diagnostika</string>
<!-- Context: Configuration Dialog - Update tab -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.168 -->
<string name="S_No_Updates_Available" comment="Update tab - no updates are available">Žádné dostupné aktualizace.</string>
<!-- Context: Configuration Dialog - Update tab -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.168 -->
<string name="S_Updates_Available" comment="Update tab - informs user updates are available ">K dispozici jsou aktualizace, které budou aplikovány při příštím restartu Windows.</string>
<!-- Context: Configuration Dialog - Update tab pop up -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.168 -->
<string name="S_Update_Restart_Req" comment="Update tab pop up - warns user restart will require restart ">Aktualizace nyní vyžaduje restart počítače.</string>
<!-- Context: Configuration Dialog - Update on boot pop up -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.168 -->
<string name="S_Ready_To_Install" comment="Update on boot pop up - inform user update about to install">Aktualizace Keyman je připravena k instalaci.</string>
<!-- Context: Download Keyboard Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 7.0.230.0 -->
@ -546,6 +566,10 @@ klávesnici, kterou používáte v systému Windows. Klávesnice se automaticky
<string name="SKHotkeyConflicts_Interface" comment="Hotkey dialog - warning of hotkey conflict with hotkey for another interface hotkey">Klávesa %1$s je v konfliktu s jiným klávesovým zkratkou. Pokud budete pokračovat, ostatní klávesová zkratka bude vymazána. Pokračovat?</string>
<!-- Context: Online Update Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.230.0 -->
<string name="S_Update" comment="Update tab name">Aktualizovat</string>
<!-- Context: Online Update Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 7.0.230.0 -->
<string name="S_Update_Title" comment="Update dialog title, where Keyman = product name">K dispozici jsou aktualizované komponenty klíče</string>
<!-- Context: Online Update Dialog -->
@ -615,6 +639,14 @@ klávesnici, kterou používáte v systému Windows. Klávesnice se automaticky
<!-- String Type: FormatString -->
<!-- Introduced: 8.0.288.0 -->
<string name="SKUpdate_IconMenuExit" comment="Update icon - exit menu item">E&amp;xit online update check</string>
<!-- Context: Update Dialog -->
<!-- String Type: FormatString -->
<!-- Introduced: 17.0233.0 -->
<string name="S_Button_Update_ApplyNow" comment="Apply update now button - update dialog">Aplikovat aktualizaci nyní</string>
<!-- Context: Update Dialog -->
<!-- String Type: FormatString -->
<!-- Introduced: 17.0233.0 -->
<string name="S_Button_Update_CheckNow" comment="Check for updates now button - update dialog">Zkontrolovat aktualizace</string>
<!-- Context: Splash Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 8.0.288.0 -->

View file

@ -295,6 +295,10 @@
<string name="koAltGrCtrlAlt" comment="General options - Ctrl+Alt simulates AltGr on computers without AltGr">ပချဳ AltGr နကဵု Ctrl+Alt</string>
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->
<!-- Introduced: 18.0.95 -->
<string name="koRightModifierHK" comment="General options - Right Ctrl Alt and Shift trigger hotkey">ကဵုအခေါၚ်ကု မဝ်ဒဳဝှာၚ်ယျာ လ္ပါ်ပုၚ် သွက် မဍဵုတၟေၚ်</string>
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->
<!-- Introduced: 9.0.480.0 -->
<string name="koDeadkeyConversion" comment="Advanced options - Base keyboard deadkeys are treated as normal keys">စွံ မဍဵုချိုတ်တ် နကဵု မဍဵု အလောတ်</string>
<!-- Context: Configuration Dialog - Options tab -->
@ -323,6 +327,10 @@
<string name="koShowWelcome" comment="Startup options - show/hide welcome screen">ထ္ၜးမုက်လိက် ဒုၚ်တၠုၚ်</string>
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->
<!-- Modified: 18.0.96.0 -->
<string name="koCheckForUpdates" comment="Automatically check for updates and download">Automatically check for updates and download</string>
<!-- Context: Configuration Dialog - Options tab -->
<!-- String Type: FormatString -->
<!-- Introduced: 7.0.236.0 -->
<string name="koTestKeymanFunctioning" comment="Startup options - test for conflicting apps when Product starts">စၟဳစၟတ် အပလဳကေယှေန် ပဠပထုဲမာန် အဃောစ ကဳမာန်</string>
<!-- Context: Configuration Dialog - Options tab -->
@ -416,6 +424,14 @@
<string name="S_Support_Copyright" comment="Support - Product Copyright">တၠမူ © SIL International. အခေါၚ်အဝဵုနွံပေၚ်ၚ်။</string>
<!-- Context: Configuration Dialog - Support tab -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.94.0 -->
<string name="S_Support_CreatedBySILGlobal" comment="Created by {SIL Global} ">ညးကၠောန်ဗဒှ်%1$s</string>
<!-- Context: Configuration Dialog - Support tab -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.94.0 -->
<string name="S_Support_CopyrightSILGlobal" comment="Copyright © {SIL Global}. All Rights Reserved.">တၠမူ © %1$s။ အခေါၚ်အရာဖအိုတ်ဂှ် ညးပိုၚ်ရောၚ်။</string>
<!-- Context: Configuration Dialog - Support tab -->
<!-- String Type: PlainText -->
<!-- Introduced: 7.0.230.0 -->
<string name="S_Support_Version" comment="Support - version">ပါယှေန်</string>
<!-- Context: Configuration Dialog - Support tab -->
@ -442,6 +458,22 @@
<!-- String Type: PlainText -->
<!-- Introduced: 7.0.230.0 -->
<string name="S_Menu_Diagnostics_Diagnostics" comment="Support button submenu - performs a diagnostic report">ဂၠိုၚ်ဂၠါဲပြဿနာ</string>
<!-- Context: Configuration Dialog - Update tab -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.168 -->
<string name="S_No_Updates_Available" comment="Update tab - no updates are available">တၚ်သမၠုၚ်က္ဆံၚ်ဟွံမွဲဏီ။</string>
<!-- Context: Configuration Dialog - Update tab -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.168 -->
<string name="S_Updates_Available" comment="Update tab - informs user updates are available ">သမၠုၚ်ပ္တိုန်တံဂှ် ယဝ်ကၟာတ်ပံက်ထောံ မုက်လိက်တုဲမ္ဂး ဒှ်ဒတန်ရ။</string>
<!-- Context: Configuration Dialog - Update tab pop up -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.168 -->
<string name="S_Update_Restart_Req" comment="Update tab pop up - warns user restart will require restart ">သမၠုၚ်ပ္တိုန်လၟုဟ်မ္ဂး ဒးကၟာတ်ပံက်စက်ဏောၚ်။ </string>
<!-- Context: Configuration Dialog - Update on boot pop up -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.168 -->
<string name="S_Ready_To_Install" comment="Update on boot pop up - inform user update about to install">တၚ်သမၠုၚ်က္ဆံၚ်သွက်ကဳမာန် စုတ်မာန်ရ။</string>
<!-- Context: Download Keyboard Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 7.0.230.0 -->
@ -533,6 +565,10 @@
<string name="SKHotkeyConflicts_Interface" comment="Hotkey dialog - warning of hotkey conflict with hotkey for another interface hotkey">မဍဵုတၟေၚ် %1$s ဒှ်ပြဿနာကု မဍဵုတၟေၚ်တၞဟ်ဟ်ရ။ ယဝ်ဆက်အာမ္ဂး မဍဵုတၟေၚ်တၞဟ်ဟ်ဂှ် သ္အးထောံရောၚ်။ ဆက်အာဟာ\?</string>
<!-- Context: Online Update Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0.230.0 -->
<string name="S_Update" comment="Update tab name">တၚ်သမၠုၚ်ပ္တိုန်</string>
<!-- Context: Online Update Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 7.0.230.0 -->
<string name="S_Update_Title" comment="Update dialog title, where Keyman = product name">ကပေါတ်တၟိသွက်ကဳမာန်တံကလိဂွံမာန်ယျ</string>
<!-- Context: Online Update Dialog -->
@ -602,16 +638,27 @@
<!-- String Type: FormatString -->
<!-- Introduced: 8.0.288.0 -->
<string name="SKUpdate_IconMenuExit" comment="Update icon - exit menu item">ဗ&amp;ဒေါံ စၟဳစၟတ်သမၠုၚ်က္ဆံၚ်အောန်လာၚ်</string>
<!-- Context: Update Dialog -->
<!-- String Type: FormatString -->
<!-- Introduced: 17.0233.0 -->
<string name="S_Button_Update_ApplyNow" comment="Apply update now button - update dialog">ပ္တိုန်တၚ်သမၠုၚ်စက်လၟုဟ်</string>
<!-- Context: Update Dialog -->
<!-- String Type: FormatString -->
<!-- Introduced: 17.0233.0 -->
<string name="S_Button_Update_CheckNow" comment="Check for updates now button - update dialog">စၟဳစၟတ်သွက် တၚ်သမၠုၚ်ပ္တိုန်တၟိတၟိ</string>
<!-- Context: Splash Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 8.0.288.0 -->
<string name="S_Splash_Name" comment="Splash major title">ကဳမာန်</string>
<!-- Context: Splash Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 8.0.294.0 -->
<string name="S_Splash_Start" comment="Start Keyman button">စ ကဳမာန်</string>
<!-- Context: Splash Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 18.0 -->
<string name="S_Splash_Start_2" comment="Start {Keyman} button">စ %1$s</string>
<!-- Context: Splash Dialog -->
<!-- String Type: PlainText -->
<!-- Introduced: 8.0.294.0 -->
<string name="S_Splash_Exit" comment="Exit button">တိတ်</string>
<!-- Context: Splash Dialog -->
@ -653,7 +700,6 @@
<!-- Context: Formatted Messages -->
<!-- String Type: FormatString -->
<!-- Introduced: 7.0.230.0 -->
<string name="SKShortApplicationTitle" comment="Product name">ကဳမာအ်</string>
<!-- Context: Hints -->
<!-- String Type: PlainText -->
<!-- Introduced: 7.0.244.0 -->
@ -777,7 +823,6 @@
<!-- Context: Formatted Messages -->
<!-- String Type: FormatString -->
<!-- Introduced: 7.0.230.0 -->
<string name="SKApplicationTitle" comment="Product name used for the title of message boxes and message dialogs">ကဳမာန်</string>
<!-- Parameters: %1$s = Version number string -->
<!-- Context: Formatted Messages -->
<!-- Introduced: 7.0.230.0 -->
@ -900,6 +945,10 @@
<!-- String Type: FormatString -->
<!-- Introduced: 8.0.294.0 -->
<string name="S_OSK_FontHelper_ChooseKeyboard" comment="OSK Font helper choose keyboard">သ္ဂောံဆဵု မလိက်ကောန်ဍောၚ် မဆေၚ်စပ်တံဂှ် ရုဲကဵု ကဳဗုဒ် ကဳမာန်တံညိ။</string>
<!-- Context: OSK Font Helper -->
<!-- String Type: FormatString -->
<!-- Introduced: 17.0.312.0 -->
<string name="S_OSK_FontHelper_NoFonts" comment="OSK Font helper no fonts found for keyboard">မလိက်ဝှတ် မကဵုဖျုန်ကဵု သွက်ကဳဗုက် %1$s ဟွံဆဵုရ.</string>
<!-- Context: Text Editor -->
<!-- String Type: FormatString -->
<!-- Introduced: 10.0.836.0 -->

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