Merge pull request #4624 from keymanapp/chore/beta-to-alpha-B14S7

chore: beta to alpha merge, B14S7
This commit is contained in:
Marc Durdin 2021-03-09 16:56:41 +11:00 committed by GitHub
commit fedbb1e025
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
215 changed files with 4186 additions and 1364 deletions

View file

@ -232,6 +232,88 @@
* chore: prepare 15.0 alpha (#4129)
## 14.0.253 beta 2021-03-05
* fix(android/app): Fix welcome.htm responsiveness (#4531)
* feat(developer): &CasedKeys system store (#4586)
* fix(android): Localize Toast notifications (#4588)
* fix(windows): PreservedKeyMap::MapUSCharToVK line order bug (#4595)
* fix(developer): tidy up expansions tests (#4592)
* fix(ios): adds i18n for some error alerts (#4577)
* fix(windows): incxstr could run over buffer with malformed data (#4596)
* chore(android/app): Update whatsnew with available display languages (#4610)
* chore(linux): Improve Sentry environment setting (#4589)
* fix(developer): Expand filenames before load (#4606)
## 14.0.252 beta 2021-03-04
* feat(ios): enables de, fr, and km localizations (#4585)
* fix(developer): improve CEF location search stability (#4571)
* fix(developer): Support all fonts in Keyboard Fonts dialog (#4574)
* feat(developer): Add different Open Containing Folder buttons (#4576)
* feat(developer): Range expansions (#4584)
* fix(web): fixes lack of respect for underlying-key display settings (#4572)
* fix(common/models): fixes application of suggestions immediately after a backspace (#4587)
* fix(linux): Improve version number (#4582)
* chore(linux): Don't report to Sentry in dev environment (#4581)
## 14.0.251 beta 2021-03-03
* fix(developer): Improve stability of named code constants (#4547)
* fix(developer): schema conformance for model package compiler (#4548)
* fix(developer): touch layout osk import handling of multiple modifiers (#4552)
* fix(developer): require language tag when compiling keyboard package (#4563)
* fix(developer): Avoid blank keys when importing KMX to KVKS (#4564)
* feat(developer): isRTL support for lexical model editor (#4559)
* fix(developer): track modified state in wordlist editor better (#4562)
* chore(ios): better visual feedback for keyboard search during poor internet connectivity (#4573)
* chore(common): Update crowdin files for `de` (#4578)
* chore(common/core/desktop): write debug output to console (#4569)
## 14.0.250 beta 2021-03-02
* fix(common/resources): Fix help.keyman.com path for CI (#4565)
## 14.0.249 beta 2021-03-01
* fix(web): mnemonic keystrokes w FF keymapping (#4540)
* chore(ios/app): Adjust help titles for installing custom dictionaries (#4550)
## 14.0.248 beta 2021-02-26
* fix(common/models): predictions after context reset / caret shift (#4411)
* change(oem/fv/ios): FV keyboards now package-based (#4471)
* fix(windows): Handle Caps Lock event correctly from TIP (#4536)
* fix(developer): run even if sentry unavailable (#4537)
* fix(developer): UTF-8 messages in LM compiler (#4539)
* feat(common/models): mid-context suggestions & reversions, fix(common/models): correction-search SMP issues (#4427)
* fix(ios): package installer completion requires welcome dismissal (#4543)
* fix(windows): Refresh settings on 64-bit apps (#4378)
* fix(windows): prevent re-registration of TIPs on 14.0 upgrade (#4535)
## 14.0.247 beta 2021-02-25
* fix(web): keyboard documentation patch-up (#4512)
* fix(web): removes package namespacing from kbd's CSS class (#4516)
## 14.0.246 beta 2021-02-24
* chore(common): allow forced version increment (#4522)
## 14.0.245 beta 2021-02-24
* fix(common/core/web): core key-processing now always returns RuleBehavior type. (#4508)
* fix(common/resources): Set help-keyman.com.sh executable (#4510)
* fix(developer/compilers): fixes error when "constructor" is in wordlist (#4504)
* fix(web): hides touch-alias caret when keystroke causes focus change (#4514)
## 14.0.244 beta 2021-02-22
* fix(common/models): merges identical suggestions after casing (#4502)
* fix(web): macOS 11 agent string parsing (#4497)
* fix(ios): app logging messages were transient, never stored (#4500)
* chore(ios): web-side sentry enablement try-catch (#4492)
## 14.0.243 beta 2021-02-12
* change(ios/app): Generate offline help from markdown (#4470)

View file

@ -1 +1 @@
15.0.19
15.0.19

View file

@ -10,6 +10,7 @@ import android.os.Handler;
import android.os.ResultReceiver;
import android.widget.Toast;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.util.FileUtils;
import java.io.File;
@ -27,8 +28,7 @@ public class DownloadResultReceiver extends ResultReceiver {
protected void onReceiveResult(int resultCode, Bundle resultData) {
switch(resultCode) {
case FileUtils.DOWNLOAD_ERROR :
Toast.makeText(context, "Download failed",
Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(context, R.string.download_failed, Toast.LENGTH_SHORT);
MainActivity.cleanupPackageInstall();
break;
case FileUtils.DOWNLOAD_SUCCESS :

View file

@ -1,5 +1,5 @@
/**
* Copyright (C) 2020 SIL International. All rights reserved.
* Copyright (C) 2020-2021 SIL International. All rights reserved.
*/
package com.tavultesoft.kmapro;
@ -13,6 +13,7 @@ import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
@ -22,6 +23,7 @@ import androidx.appcompat.app.AppCompatActivity;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.KMManager;
import com.tavultesoft.kmea.util.KMPLink;
import com.tavultesoft.kmea.util.WebViewUtil;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -162,6 +164,7 @@ public class KMPBrowserActivity extends BaseActivity {
@Override
protected void onDestroy() {
super.onDestroy();
WebViewUtil.cleanup(webView);
}
@Override

View file

@ -1,7 +1,9 @@
/**
* Copyright (C) SIL International. All rights reserved.
*/
package com.tavultesoft.kmapro;
import android.annotation.SuppressLint;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
@ -9,17 +11,8 @@ import androidx.fragment.app.Fragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
@ -36,9 +29,7 @@ import com.tavultesoft.kmea.util.KMLog;
import org.json.JSONObject;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -254,9 +245,6 @@ public class PackageActivity extends AppCompatActivity implements
languageList = new ArrayList<String>();
}
//Dataset kmpDataset = new Dataset(context);
//kmpDataset.keyboards.addAll(kbdsList);
List<Map<String, String>> installedPackageKeyboards =
kmpProcessor.processKMP(kmpFile, tempPackagePath, PackageProcessor.PP_KEYBOARDS_KEY, languageList);
// Do the notifications!
@ -270,8 +258,6 @@ public class PackageActivity extends AppCompatActivity implements
Toast.LENGTH_SHORT).show();
}
_cleanup = true;
notifyPackageInstallListeners(KeyboardEventHandler.EventType.PACKAGE_INSTALLED,
installedPackageKeyboards, 1);
if (installedPackageKeyboards != null) {
notifyPackageInstallListeners(KeyboardEventHandler.EventType.PACKAGE_INSTALLED,
installedPackageKeyboards, 1);
@ -294,8 +280,6 @@ public class PackageActivity extends AppCompatActivity implements
Toast.LENGTH_SHORT).show();
_cleanup = true;
notifyLexicalModelInstallListeners(KeyboardEventHandler.EventType.LEXICAL_MODEL_INSTALLED,
installedLexicalModels, 1);
if (installedLexicalModels != null) {
notifyLexicalModelInstallListeners(KeyboardEventHandler.EventType.LEXICAL_MODEL_INSTALLED,
installedLexicalModels, 1);

View file

@ -14,6 +14,7 @@ import com.stepstone.stepper.StepperLayout;
import com.stepstone.stepper.VerificationError;
import com.tavultesoft.kmea.packages.PackageProcessor;
import com.tavultesoft.kmea.util.FileUtils;
import com.tavultesoft.kmea.util.WebViewUtil;
import android.graphics.Bitmap;
import android.os.Bundle;
@ -135,16 +136,7 @@ public class WebViewFragment extends Fragment implements BlockingStep {
@Override
public void onPageFinished(WebView view, String url) {
// Inject a meta viewport tag into the head of the file if it doesn't exist
webView.loadUrl(
"javascript:(function() {" +
"if(!document.querySelectorAll('meta[name=viewport]').length) {"+
"let meta=document.createElement('meta');"+
"meta.name='viewport';"+
"meta.content='width=device-width, initial-scale=1';"+
"document.head.appendChild(meta);"+
"}"+
"})()"
);
WebViewUtil.injectViewport(view);
}
});

View file

@ -43,7 +43,7 @@
<!-- Context: Get Started menu -->
<string name="show_get_started" comment="Show the &quot;Get Started&quot; menu on startup">Zeige \"%1$s\" beim Start</string>
<!-- Context: Android Storage Permission -->
<string name="request_storage_permission" comment="Request storage permission to access keyboard packages"> Um Tastaturpakete zu installieren, braucht Keyman die Berechtigung zum Lesen/Schreiben.</string>
<string name="request_storage_permission" comment="Request storage permission to access keyboard packages"> Um Tastaturpakete zu installieren, braucht Keyman die Berechtigung zum Lesen von externem Speicher.</string>
<!-- Context: Android Storage Permission -->
<string name="storage_permission_denied" comment="Keyboard package installation may fail since Android storage permission not granted"> Zugriff auf den Speicher wurde abgelehnt. Möglicherweise funktioniert die Installation des Tastaturpakets nicht korrekt</string>
<!-- Context: Keyman Settings menu -->
@ -56,6 +56,8 @@
<!-- Context: Keyman Settings menu -->
<string name="install_keyboard_or_dictionary" comment="Menu item to install keyboard or dictionary">Tastatur oder Wörterbuch installieren</string>
<!-- Context: Keyman Settings menu -->
<string name="change_display_language" comment="Menu action to change interface language">Anzeigesprache ändern</string>
<!-- Context: Keyman Settings menu -->
<string name="show_banner" comment="text suggestions banner">Banner immer anzeigen</string>
<!-- Context: Keyman Settings menu -->
<string name="show_banner_on" comment="Description when toggle is on">Nicht implementiert</string>
@ -108,6 +110,8 @@
<!-- Context: KMP Package strings -->
<string name="install_predictive_text_package" comment="Title to install dictionary package">Wörterbuch installieren</string>
<!-- Context: KMP Package strings -->
<string name="not_valid_package_file" comment="Notification when invalid package file cannot be installed">%1$s ist keine gültige Keyman Paketdatei.\n%2$s\"</string>
<!-- Context: KMP Package strings -->
<string name="no_new_touch_keyboards_to_install" comment="Notification when no touch-optimized keyboards can be installed">Tastaturpaket enthält keine berührungsoptimierte Tastatur, die installiert werden könnte</string>
<!-- Context: KMP Package strings -->
<string name="no_new_predictive_text_to_install" comment="Notification when no dictionaries can be installed">Kein neues Wörterbuch zu installieren</string>

View file

@ -13,6 +13,7 @@
<activity
android:name="com.tavultesoft.kmea.KeyboardPickerActivity"
android:launchMode="singleTask"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.Dialog" >
@ -29,12 +30,6 @@
android:label=""
android:theme="@style/Theme.AppCompat.Light.Dialog">
</activity>
<activity
android:name="com.tavultesoft.kmea.KMHelpFileActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
</activity>
<activity
android:name="com.tavultesoft.kmea.ModelPickerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
@ -47,6 +42,18 @@
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.Dialog" >
</activity>
<!-- Put other WebViewActivities in a separate process so the Keyboard WebView doesn't lag.
Ref https://stackoverflow.com/questions/40650643/timed-out-waiting-on-iinputcontextcallback-with-custom-keyboard-on-android -->
<activity
android:name="com.tavultesoft.kmea.KMHelpFileActivity"
android:process=":KMHelpFileActivity"
android:launchMode="singleTask"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
</activity>
</application>
</manifest>

View file

@ -9,6 +9,8 @@ import android.content.ContextWrapper;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.LocaleList;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
@ -18,6 +20,21 @@ import com.tavultesoft.kmea.util.ContextUtils;
import java.util.Locale;
public class BaseActivity extends AppCompatActivity {
static ContextWrapper localeUpdatedContext;
/**
* Some classes aren't an AppCompatActivity and need this helper to localize Toast notifications
* in the updated locale.
* @param defaultContext - the context to fallback if localeUpdatedContext is null
* @param resID - resource ID of the string
* @param duration - length of the Toast notification (Toast.LENGTH_LONG or Toast.LENGTH_SHORT)
* @param args - optional format parameters for the string
*/
public static void makeToast(Context defaultContext, int resID, int duration, Object... args) {
Context context = (localeUpdatedContext != null) ? localeUpdatedContext : defaultContext;
String msg = context.getString(resID);
Toast.makeText(context, String.format(msg, args), duration).show();
}
@Override
protected void attachBaseContext(Context newBase) {
@ -36,7 +53,7 @@ public class BaseActivity extends AppCompatActivity {
} else {
localeToSwitchTo = Locale.forLanguageTag(languageTag);
}
ContextWrapper localeUpdatedContext = ContextUtils.updateLocale(newBase, localeToSwitchTo);
this.localeUpdatedContext = ContextUtils.updateLocale(newBase, localeToSwitchTo);
super.attachBaseContext(localeUpdatedContext);
}

View file

@ -11,8 +11,10 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.webkit.WebChromeClient;
@ -29,6 +31,7 @@ import com.tavultesoft.kmea.packages.PackageProcessor;
import com.tavultesoft.kmea.util.FileProviderUtils;
import com.tavultesoft.kmea.util.FileUtils;
import com.tavultesoft.kmea.util.HelpFile;
import com.tavultesoft.kmea.util.WebViewUtil;
import java.io.File;
@ -42,6 +45,7 @@ public class KMHelpFileActivity extends BaseActivity {
private WebView webView;
private Button finishButton;
private String pkgID;
private static boolean didSetDataDirectorySuffix = false;
@SuppressLint({"SetJavaScriptEnabled", "InflateParams"})
@Override
@ -49,6 +53,16 @@ public class KMHelpFileActivity extends BaseActivity {
super.onCreate(savedInstanceState);
final Context context = this;
// Different processes in the same application cannot directly share WebView-related data
// https://developer.android.com/reference/android/webkit/WebView.html#setDataDirectorySuffix(java.lang.String)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (!didSetDataDirectorySuffix) {
String processName = getProcessName();
WebView.setDataDirectorySuffix(processName);
didSetDataDirectorySuffix = true;
}
}
setContentView(R.layout.activity_help_file_layout);
Bundle bundle = getIntent().getExtras();
@ -63,7 +77,7 @@ public class KMHelpFileActivity extends BaseActivity {
finishButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
finish();
finishAfterTransition();
overridePendingTransition(0, android.R.anim.fade_out);
}
});
@ -130,16 +144,7 @@ public class KMHelpFileActivity extends BaseActivity {
@Override
public void onPageFinished(WebView view, String url) {
// Inject a meta viewport tag into the head of the file if it doesn't exist
webView.loadUrl(
"javascript:(function() {" +
"if(!document.querySelectorAll('meta[name=viewport]').length) {"+
"let meta=document.createElement('meta');"+
"meta.name='viewport';"+
"meta.content='width=device-width, initial-scale=1';"+
"document.head.appendChild(meta);"+
"}"+
"})()"
);
WebViewUtil.injectViewport(view);
}
});
@ -162,14 +167,22 @@ public class KMHelpFileActivity extends BaseActivity {
@Override
protected void onDestroy() {
super.onDestroy();
WebViewUtil.cleanup(webView);
}
@Override
public void onBackPressed() {
if (webView != null && webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
// Dismiss the help file
super.onBackPressed();
finishAndRemoveTask();
break;
}
}
return true;
}
}

View file

@ -13,6 +13,7 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.data.Keyboard;
import com.tavultesoft.kmea.data.KeyboardController;
import com.tavultesoft.kmea.KMManager.KeyboardType;
@ -154,10 +155,7 @@ final class KMKeyboard extends WebView {
// TODO: Fix base error rather than trying to ignore it "No keyboard stubs exist"
if ((cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) && (!cm.message().startsWith("No keyboard stubs exist"))) {
Toast.makeText(context, "Fatal Error with " + currentKeyboard +
". Loading default keyboard", Toast.LENGTH_LONG).show();
// Still send log about falling back to default keyboard (ignore language ID)
// Make Toast notification of error and send log about falling back to default keyboard (ignore language ID)
sendError(packageID, keyboardID, "");
Keyboard firstKeyboard = KeyboardController.getInstance().getKeyboardInfo(0);
if (firstKeyboard != null) {
@ -590,11 +588,13 @@ final class KMKeyboard extends WebView {
}
// Display Toast notification that keyboard selection failed, so loading default keyboard.
// Also sends a message to Sentry
// Display localized Toast notification that keyboard selection failed, so loading default keyboard.
// Also sends a message to Sentry (not localized)
private void sendError(String packageID, String keyboardID, String languageID) {
String msg = String.format("Can't set %s::%s for %s language. Loading default keyboard", packageID, keyboardID, languageID);
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
BaseActivity.makeToast(context, R.string.fatal_keyboard_error, Toast.LENGTH_LONG, packageID, keyboardID, languageID);
// Don't localize msg for Sentry
String msg = String.format(context.getString(R.string.fatal_keyboard_error), packageID, keyboardID, languageID);
Sentry.captureMessage(msg);
}

View file

@ -7,6 +7,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.R;
import com.tavultesoft.kmea.util.DownloadFileUtils;
@ -158,8 +159,7 @@ public class CloudApiTypes {
if (filename == null || filename.isEmpty() || cachedFile == null || !cachedFile.exists()) {
// failed to retrieve downloaded file
String message = context.getString(R.string.failed_to_retrieve_file);
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
BaseActivity.makeToast(context, R.string.failed_to_retrieve_file, Toast.LENGTH_LONG);
}
return cachedFile;

View file

@ -5,6 +5,7 @@ import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.KeyboardPickerActivity;
import com.tavultesoft.kmea.R;
import com.tavultesoft.kmea.cloud.CloudApiTypes;
@ -110,7 +111,7 @@ public class CloudCatalogDownloadCallback implements ICloudDownloadCallback<Data
private JSONArray ensureInit(Context aContext,Dataset aDataSet, JSONArray json) {
if (json == null && aDataSet.isEmpty()) {
Toast.makeText(context, "Failed to access Keyman server!", Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(context, R.string.cannot_connect, Toast.LENGTH_SHORT);
handleDownloadError();
return null;
}
@ -120,7 +121,7 @@ public class CloudCatalogDownloadCallback implements ICloudDownloadCallback<Data
private JSONObject ensureInit(Context aContext,Dataset aDataSet, JSONObject json) {
if (json == null && aDataSet.isEmpty()) {
Toast.makeText(context, "Failed to access Keyman server!", Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(context, R.string.cannot_connect, Toast.LENGTH_SHORT);
handleDownloadError();
return null;
}

View file

@ -4,6 +4,7 @@ import android.content.Context;
import android.net.Uri;
import android.widget.Toast;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.KMKeyboardDownloaderActivity;
import com.tavultesoft.kmea.KeyboardEventHandler;
import com.tavultesoft.kmea.R;
@ -107,9 +108,7 @@ public class CloudKeyboardPackageDownloadCallback implements ICloudDownloadCallb
@Override
public void applyCloudDownloadToModel(Context aContext, Void aModel, CloudKeyboardDownloadReturns aCloudResult)
{
Toast.makeText(aContext,
aContext.getString(R.string.keyboard_download_finished),
Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(aContext, R.string.keyboard_download_finished, Toast.LENGTH_SHORT);
if(aCloudResult.installedResource != null)
{

View file

@ -7,6 +7,7 @@ import android.content.Context;
import android.os.Bundle;
import android.widget.Toast;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.R;
import com.tavultesoft.kmea.cloud.CloudApiTypes;
import com.tavultesoft.kmea.cloud.CloudDataJsonUtil;
@ -80,8 +81,7 @@ public class CloudLexicalModelMetaDataDownloadCallback implements ICloudDownload
@Override
public void applyCloudDownloadToModel(Context aContext, Void aModel, List<CloudLexicalModelMetaDataDownloadCallback.MetaDataResult> aCloudResult) {
if (aCloudResult.isEmpty()) {
String msg = aContext.getString(R.string.catalog_unavailable);
Toast.makeText(aContext, msg, Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(aContext, R.string.catalog_unavailable, Toast.LENGTH_SHORT);
KMLog.LogError(TAG, "Could not reach server");
return;
}
@ -104,16 +104,12 @@ public class CloudLexicalModelMetaDataDownloadCallback implements ICloudDownload
if(_r.returnjson.target== CloudApiTypes.ApiTarget.KeyboardLexicalModels) {
if( CloudDownloadMgr.getInstance().alreadyDownloadingData(_r.additionalDownloadid))
{
Toast.makeText(aContext,
aContext.getString(R.string.dictionary_download_is_running_in_background),
Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(aContext, R.string.dictionary_download_is_running_in_background, Toast.LENGTH_SHORT);
continue;
}
CloudLexicalPackageDownloadCallback _callback = new CloudLexicalPackageDownloadCallback();
Toast.makeText(aContext,
aContext.getString(R.string.dictionary_download_start_in_background),
Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(aContext, R.string.dictionary_download_start_in_background, Toast.LENGTH_SHORT);
CloudDownloadMgr.getInstance().executeAsDownload(aContext,
_r.additionalDownloadid, null, _callback,
@ -152,8 +148,7 @@ public class CloudLexicalModelMetaDataDownloadCallback implements ICloudDownload
KMLog.LogException(TAG, "Error parsing lexical model from api.keyman.com. ", e);
}
} else {
String msg = aContext.getString(R.string.no_associated_model);
Toast.makeText(aContext, msg, Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(aContext, R.string.no_associated_model, Toast.LENGTH_SHORT);
}
}

View file

@ -4,6 +4,7 @@ import android.content.Context;
import android.net.Uri;
import android.widget.Toast;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.KMKeyboardDownloaderActivity;
import com.tavultesoft.kmea.KeyboardEventHandler;
import com.tavultesoft.kmea.R;
@ -98,9 +99,7 @@ public class CloudLexicalPackageDownloadCallback implements ICloudDownloadCallba
@Override
public void applyCloudDownloadToModel(Context aContext, Void aModel, CloudKeyboardDownloadReturns aCloudResult)
{
Toast.makeText(aContext,
aContext.getString(R.string.dictionary_download_finished),
Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(aContext, R.string.dictionary_download_finished, Toast.LENGTH_SHORT);
if(aCloudResult.installedResource != null)
{

View file

@ -6,6 +6,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.BuildConfig;
import com.tavultesoft.kmea.KMKeyboardDownloaderActivity;
import com.tavultesoft.kmea.KMManager;
@ -347,8 +348,7 @@ public class CloudRepository {
preCacheDataSet(context,null,null,null);
if(CloudDownloadMgr.getInstance().alreadyDownloadingData(DOWNLOAD_IDENTIFIER_CATALOGUE)) {
String msg = context.getString(R.string.catalog_download_is_running_in_background);
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(context, R.string.catalog_download_is_running_in_background, Toast.LENGTH_SHORT);
}
return memCachedDataset;
@ -394,12 +394,10 @@ public class CloudRepository {
cloudQueries.toArray(params);
if (CloudDownloadMgr.getInstance().alreadyDownloadingData(DOWNLOAD_IDENTIFIER_CATALOGUE)) {
String msg = context.getString(R.string.catalog_download_is_running_in_background);
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(context, R.string.catalog_download_is_running_in_background, Toast.LENGTH_SHORT);
} else {
updateIsRunning = true;
String msg = context.getString(R.string.catalog_download_start_in_background);
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(context, R.string.catalog_download_start_in_background, Toast.LENGTH_SHORT);
CloudDownloadMgr.getInstance().executeAsDownload(
context, DOWNLOAD_IDENTIFIER_CATALOGUE, memCachedDataset, _download_callback, params);
}

View file

@ -20,6 +20,7 @@ import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat.Builder;
import androidx.core.app.NotificationManagerCompat;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.KMKeyboardDownloaderActivity;
import com.tavultesoft.kmea.KeyboardPickerActivity;
import com.tavultesoft.kmea.KMManager;
@ -105,7 +106,7 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl
return;
}
Toast.makeText(currentContext, currentContext.getString(R.string.update_check_current), Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(currentContext, R.string.update_check_current, Toast.LENGTH_SHORT);
lastUpdateCheck = Calendar.getInstance();
SharedPreferences prefs = currentContext.getSharedPreferences(currentContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
@ -121,7 +122,7 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl
return;
}
Toast.makeText(currentContext, currentContext.getString(R.string.update_check_unavailable), Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(currentContext, R.string.update_check_unavailable, Toast.LENGTH_SHORT);
lastUpdateCheck = Calendar.getInstance();
updateCheckFailed = true;
checkingUpdates = false;
@ -193,7 +194,7 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl
currentContext.startActivity(intent);
}
} else {
Toast.makeText(currentContext, "No internet connection", Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(currentContext, R.string.cannot_connect, Toast.LENGTH_SHORT);
checkingUpdates = false;
}
}
@ -554,12 +555,12 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl
if (failedUpdateCount > 0) {
Toast.makeText(currentContext, "One or more resources failed to update!", Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(currentContext, R.string.update_failed, Toast.LENGTH_SHORT);
lastUpdateCheck = Calendar.getInstance();
updateFailed = true;
checkingUpdates = false;
} else {
Toast.makeText(currentContext, "Resources successfully updated!", Toast.LENGTH_SHORT).show();
BaseActivity.makeToast(currentContext, R.string.update_success, Toast.LENGTH_SHORT);
lastUpdateCheck = Calendar.getInstance();
SharedPreferences prefs = currentContext.getSharedPreferences(currentContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();

View file

@ -13,7 +13,9 @@ import android.widget.Toast;
import androidx.core.content.FileProvider;
import com.tavultesoft.kmea.BaseActivity;
import com.tavultesoft.kmea.KMManager;
import com.tavultesoft.kmea.R;
import com.tavultesoft.kmea.util.FileProviderUtils;
import com.tavultesoft.kmea.util.FileUtils;
@ -47,8 +49,11 @@ public final class HelpFile {
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
} catch (NullPointerException e) {
String message = "FileProvider undefined in app to load" + customHelp.toString();
Toast.makeText(context, message, Toast.LENGTH_LONG).show();
// Localize Toast error
BaseActivity.makeToast(context, R.string.fileprovider_undefined, Toast.LENGTH_LONG);
// Don't localize message to Sentry
String message = String.format(context.getString(R.string.fileprovider_undefined), customHelp.toString());
KMLog.LogException(TAG, message, e);
}
} else {

View file

@ -0,0 +1,41 @@
/**
* Copyright (C) 2021 SIL International. All rights reserved.
*/
package com.tavultesoft.kmea.util;
import android.view.ViewGroup;
import android.webkit.WebView;
public final class WebViewUtil {
// 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) {
webView.loadUrl(
"javascript:(function() {" +
"if(document.head && !document.querySelectorAll('meta[name=viewport]').length) {"+
"let meta=document.createElement('meta');"+
"meta.name='viewport';"+
"meta.content='width=device-width, initial-scale=1';"+
"document.head.appendChild(meta);"+
"}"+
"})()"
);
}
}
// Blank the webView and destroy completely
// Reference: https://stackoverflow.com/questions/17418503/destroy-webview-in-android/17458577#17458577
public static void cleanup(WebView webView) {
if (webView != null) {
webView.loadUrl("about:blank");
ViewGroup viewGroup = (ViewGroup) webView.getParent();
if (viewGroup != null) {
viewGroup.removeView(webView);
}
webView.removeAllViews();
webView.destroy();
webView = null;
}
}
}

View file

@ -63,7 +63,7 @@
<string name="keyboard_picker_new_keyboard" comment="Mark a language name that's newly installed in keyboard list">[neu] %1$s</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_qr_code" comment="QR Code description"> Scannen Sie diesen Code, um diese\nTastatur auf einem anderen Gerät zu laden</string>
<!-- Context: KMP Package welcome.htm title -->
<!-- Context: Keyboard Help welcome.htm title -->
<string name="welcome_package" comment="Title to welcome.htm page (name and version)">Willkommen bei %1$s</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Suche nach zugehörigem Wörterbuch zum Herunterladen</string>
@ -91,6 +91,8 @@
<string name="dictionary_download_is_running_in_background" comment="Notification that a dictionary download is still running">Das ausgewählte Wörterbuch wird bereits heruntergeladen. Bitte versuchen Sie es gleich nochmal!</string>
<!-- Context: Background download messages-->
<string name="dictionary_download_finished" comment="Notification that a dictionary download has finished">Download des Wörterbuchs ist abgeschlossen.</string>
<!-- Context: Background download messages -->
<string name="failed_to_retrieve_file" comment="Failed to retrieve downloaded file">Laden der heruntergeladenen Datei fehlgeschlagen</string>
<!-- Context: General Updates -->
<string name="update_check_unavailable" comment="Error message when a Keyman server can't be reached">Fehler beim Zugriff auf den Server!</string>
<!-- Context: General Updates -->
@ -123,6 +125,8 @@
<item quantity="one">(%1$d Tastatur)</item>
<item quantity="other">(%1$d Tastaturen)</item>
</plurals>
<!-- Context: "Change Display Language" menu -->
<string name="default_locale" comment="Use the device's current locale">Standardsprache</string>
<!-- Context: Content descriptions -->
<!-- Context: Content descriptions -->
<!-- Context: Popup menu labels -->

View file

@ -91,8 +91,6 @@
<string name="dictionary_download_is_running_in_background" comment="Notification that a dictionary download is still running">Le dictionnaire sélectionné est déjà en cours de téléchargement ; veuillez réessayer dans un instant !</string>
<!-- Context: Background download messages-->
<string name="dictionary_download_finished" comment="Notification that a dictionary download has finished">Téléchargement du dictionnaire terminé.</string>
<!-- Context: Background download messages-->
<string name="failed_to_access_downloaded_file" comment="Failed to access downloaded file">Impossible d\'accéder au fichier téléchargé</string>
<!-- Context: Background download messages -->
<string name="failed_to_retrieve_file" comment="Failed to retrieve downloaded file">Impossible de récupérer le fichier téléchargé</string>
<!-- Context: General Updates -->

View file

@ -58,7 +58,7 @@
<!-- Context: Keyboard Updates -->
<string name="cannot_connect" comment="Error message when network connection fails">\nCannot connect to Keyman server!\n</string>
<string name="cannot_connect" comment="Error message when network connection fails">Cannot connect to Keyman server!</string>
<!-- Context: Keyboard Updates -->
<string name="confirm_delete_keyboard" comment="Confirmation to delete a keyboard">Would you like to delete this keyboard?</string>
@ -106,6 +106,13 @@
<!-- Context: Keyboard Help welcome.htm title -->
<string name="welcome_package" comment="Title to welcome.htm page (name and version)">Welcome to %1$s</string>
<!-- Context: Keyboard app doesn't include FileProvider library needed to view help file -->
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">
FileProvider library needed to view help file: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">
Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard.</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>
@ -148,8 +155,8 @@
<!-- Context: Background download messages-->
<string name="dictionary_download_finished" comment="Notification that a dictionary download has finished">Dictionary download is finished.</string>
<!-- Context: Background download messages-->
<string name="failed_to_access_downloaded_file" comment="Failed to access downloaded file">Failed to access downloaded file</string>
<!-- Context: Background download messages -->
<string name="download_failed" comment="Notification that a download failed">Download failed</string>
<!-- Context: Background download messages -->
<string name="failed_to_retrieve_file" comment="Failed to retrieve downloaded file">Failed to retrieve downloaded file</string>
@ -160,6 +167,11 @@
<!-- Context: General Updates -->
<string name="update_check_current" comment="Notification that all resources are up to date">"All resources are up to date!"</string>
<!-- Context: General Updates -->
<string name="update_failed" comment="Notification that a resource update failed">One or more resources failed to update!</string>
<!-- Context: General Updates -->
<string name="update_success" comment="Notification that a resource update successfully updated">Resources successfully updated!</string>
<!-- Context: Model Info -->
<string name="model_version" comment="Title for a dictionary version">Dictionary version</string>

View file

@ -17,4 +17,8 @@ Here are some of the new features we have added to Keyman for Android 14.0:
* Match user input capital letters when offering suggestions (#3845)
* Update minimum Android SDK to 21 (Android 5.0 Lollipop) (#2993)
* Keyman now works more reliably with WeChat and Telegram (#4254)
* Added new menu to change the display language (#4261)
* Added new Settings menu to [Change Display Language](../basic/config/index#Change-Display-Language): (#4261)
* French
* German
* Khmer
* Obolo

View file

@ -1,2 +1,3 @@
.vs/
build*/
configure/

View file

@ -5,11 +5,12 @@ Maintainer: Debian Input Method Team <debian-input-method@lists.debian.org>
Uploaders:
Daniel Glassey <wdg@debian.org>,
Keyman team <support@keyman.com>,
Eberhard Beilharz <eb1@sil.org>,
Build-Depends:
debhelper (>= 11),
meson (>= 0.45),
ninja-build,
Standards-Version: 4.5.0
Standards-Version: 4.5.1
Vcs-Git: https://github.com/keymanapp/keyman.git
Vcs-Browser: https://github.com/keymanapp/keyman/tree/master/common/core/desktop
Homepage: https://www.keyman.com

View file

@ -0,0 +1,51 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=jcZL
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -1,6 +1,7 @@
# How to build the keyboard processor
## Prerequisites
- Python 3
- Meson build system.
- C++14 or later compiler (VC++ 2017 or later for Windows).
@ -8,46 +9,55 @@
- kmcomp (for tests) -- must be added to path
## Installing Python3
### Linux
You will be able to install a python3 package in any reputable recent version
of linux using its package manager if it's not already installed.
### Mac OS X
You can get the official installer from the official Python site:
[https://www.python.org/downloads/mac-osx/]()
<https://www.python.org/downloads/mac-osx/>
### Windows
You can get the official installer from the official Python site:
[https://www.python.org/downloads/windows/]()
You can get the official installer from the official Python site:
<https://www.python.org/downloads/windows/>
## Installing Meson
Ensure you have Python3 correctly installed and can run the command `pip3`.
```
```bash
python3 -m pip install meson
```
## Building
### Building on Windows
For Windows, use `build.bat` -- this handles environment and x86/x64
cross-compiles with Visual Studio 2017+.
You may need to set `SDKVER` environement variable to the current
Windows SDK version, if it cannot be automatically detected, e.g.:
```
```DOS
set SDKVER=10.0.19041.0
```
To build:
```
```DOS
build.bat all
```
### Building on Linux, macOS
For all other platforms, in your source directory do the following:
```
```bash
cd desktop
meson build --werror
cd build
@ -55,24 +65,29 @@ ninja
meson test
```
For a debug build, pass `--buildtype debug` to meson.
## Note on kmcomp
kmcomp is the command-line compiler from Keyman Developer, available from https://keyman.com/ or
in this repo in /windows/src/developer/kmcomp. The compiler is currently available as a Windows
kmcomp is the command-line compiler from Keyman Developer, available from <https://keyman.com/> or
in this repo in `/windows/src/developer/kmcomp`. The compiler is currently available as a Windows
PE executable only, but it does run under WINE.
## Additional configuration notes
### Windows
The search path can be edited through System settings / Advanced system settings /
Environment Variables / User environment variables.
If you have Keyman Developer installed, add %KeymanDeveloperPath% to your path. Otherwise, add
If you have Keyman Developer installed, add `%KeymanDeveloperPath%` to your path. Otherwise, add
the path where you extracted the kmcomp archive.
### Linux
You need a wrapper `kmcomp` shell script:
```
```bash
#!/bin/bash
wine `dirname "$0"`/kmcomp.exe "$@"
```
@ -81,4 +96,5 @@ Place this in the same folder as you extracted kmcomp.exe, and `chmod +x kmcomp`
to the path (e.g. `export PATH=/path/to/kmcomp:$PATH`, which you can add to `.bashrc`)
### macOS
TODO

View file

@ -1,6 +1,7 @@
# Keyman Keyboard Processor API
## Requirements
1. Cross platform.
2. Cross language.
3. Facilitate stateless operation of the Engine.
@ -9,8 +10,8 @@
6. Support querying Keyboard attributes.
7. Idempotent
## Design decisions in support of requirements:
- Use C or C99 types and calling convention for the interface, it has the
broadest language FFI support. [1,2]
- Have client (platform glue) code load keyboards, manage & pass state. [3,4,7]
@ -18,8 +19,8 @@
engine [5,6]
- Provide get/set calls for client accessible keyboard state information [3,4]
## Glossary
- __Platform layer:__
the code that consumes the Keyman Keyboard Processor API, and provides the
operating system-specific handling of keystroke events and integration with
@ -29,22 +30,32 @@ the application that has the focus and receives text events from the Platform
layer.
- __Context:__ Text preceding the insertion point
- __Marker:__ Positional state that can be placed in the Context.
- __Keyboard:__ A set of rules for execution my an Engine
- __Keyboard:__ A set of rules for execution by an Engine
- __Option:__ A variable in a dynamic or static key value store.
- __Processor:__
The component that implements this API and can parse and execute a particular
keyboard.
- __State:__ An object that hold internal state of the Processor for a given
- __State:__ An object that holds internal state of the Processor for a given
insertion point
- __Action:__
A directive output by the processor detailing how the Platform layer should
transform the Client Application's text buffer. There may be several items
produced by a single keyboard event.
- __Keyboard Event:__
A virtual key board event and modifier map recevied from the platform to be
A virtual key board event and modifier map recevied from the Platform layer to be
processed with the state object for this Client application.
- __Virtual Key:__
A code based on the US English layout, with values matching the Windows
virtual key codes. See `keyboardprocessor_vkeys.h` for definitions.
- __Modifier Key:__
The set of Control, Shift, Alt, Caps Lock keys. On some platforms these may
have other names (e.g. Alt is called Option on macOS); other platform-specific
modifiers such as Windows key are excluded from this set. Some modifiers are
transient, such as Control, and others have long-lasting state, such as
Caps Lock.
## API
### Namespace
All calls, types and enums are prefixed with the namespace identifier `km_kbp_`

View file

@ -41,23 +41,23 @@ The application that has the focus and receives text events from the Platform
layer.
- __Context:__ Text preceding the insertion point
- __Marker:__ Positional state that can be placed in the Context.
- __Keyboard:__ A set of rules for execution my an Engine
- __Keyboard:__ A set of rules for execution by an Engine
- __Option:__ A variable in a dynamic or static key value store.
- __Processor:__
The component that implements this API and can parse and execute a particular
keyboard.
- __State:__ An object that hold internal state of the Processor for a given
- __State:__ An object that holds internal state of the Processor for a given
insertion point
- __Action:__
A directive output by the processor detailing how the Platform layer should
transform the Client Application's text buffer. There may be several items
produced by a single keyboard event.
- __Keyboard Event:__
A virtual key event and modifier map received from the platform to be
A virtual key event and modifier map received from the Platform layer to be
processed with the state object for this Client application.
- __Virtual Key:__
A code based on the US English layout, with values matching the Windows
virtual key codes. See keyboardprocessor_vkeys.h for definitions.
virtual key codes. See `keyboardprocessor_vkeys.h` for definitions.
- __Modifier Key:__
The set of Control, Shift, Alt, Caps Lock keys. On some platforms these may
have other names (e.g. Alt is called Option on macOS); other platform-specific

View file

@ -54,25 +54,29 @@ int km::kbp::kmx::DebugLog_1(const char *file, int line, const char *function, c
fmtbuf[255] = 0;
va_end(vars);
if(g_debug_KeymanLog) {
if(g_debug_ToConsole) { // I3951
char windowinfo[1024];
sprintf(windowinfo,
"%ld" TAB //"TickCount" TAB
"%s:%d" TAB //"SourceFile" TAB
"%s" TAB //"Function"
"%s" NL, //"Message"
if (!g_debug_KeymanLog)
return 0;
GetTickCount(), //"TickCount" TAB
file, line, //"SourceFile" TAB
function, //"Function" TAB
fmtbuf); //"Message"
char windowinfo[1024];
sprintf(windowinfo,
"%ld" TAB //"TickCount" TAB
"%s:%d" TAB //"SourceFile" TAB
"%s" TAB //"Function"
"%s" NL, //"Message"
GetTickCount(), //"TickCount" TAB
file, line, //"SourceFile" TAB
function, //"Function" TAB
fmtbuf); //"Message"
if (g_debug_ToConsole) { // I3951
std::cout << windowinfo << std::endl; // OutputDebugStringA(windowinfo);
} else {
#ifdef _USE_WINDOWS
std::cout << windowinfo << std::endl; // OutputDebugStringA(windowinfo);
std::cout << windowinfo << std::endl; // OutputDebugStringA(windowinfo);
#else
syslog(LOG_DEBUG, "%s", windowinfo);
syslog(LOG_DEBUG, "%s", windowinfo);
#endif
}
}
return 0;
@ -125,7 +129,8 @@ const char *km::kbp::kmx::Debug_UnicodeString(PKMX_WCHAR s, int x) {
bufout[x][0] = 0;
for (p = s, q = bufout[x]; *p && (p - s < 128); p++)
{
sprintf(q, "U+%4.4X ", *p); q = strchr(q, 0);
sprintf(q, "U+%4.4X ", *p);
q = strchr(q, 0);
}
//WideCharToMultiByte(CP_ACP, 0, buf, -1, bufout, 128, NULL, NULL);
return bufout[x];

View file

@ -82,7 +82,7 @@ KMX_BOOL KMX_Processor::LoadKeyboard(km_kbp_path_name fileName, LPKEYBOARD *lpKe
LPKEYBOARD kbp;
PKMX_BYTE filebase;
DebugLog("Loading file %ws", fileName);
DebugLog("Loading file '%s'", fileName);
if(!fileName || !lpKeyboard)
{
DebugLog("Bad Filename");

View file

@ -10,7 +10,8 @@ using namespace kmx;
/* Globals */
KMX_BOOL km::kbp::kmx::g_debug_ToConsole = TRUE, km::kbp::kmx::g_debug_KeymanLog = TRUE;
KMX_BOOL km::kbp::kmx::g_debug_ToConsole = FALSE;
KMX_BOOL km::kbp::kmx::g_debug_KeymanLog = TRUE;
KMX_BOOL km::kbp::kmx::g_silent = FALSE;
/*
@ -120,7 +121,7 @@ KMX_BOOL KMX_Processor::ProcessGroup(LPGROUP gp, KMX_BOOL *pOutputKeystroke)
PKMX_WCHAR p;
int sdmfI;
/*
/*
If the number of nested groups goes higher than 50, then break out - this is
a limitation of stack size. This is basically a catch-all for freaky apps that
cause message loopbacks and nasty things like that. Okay, it's really a catch all

View file

@ -128,10 +128,8 @@ const char *Debug_UnicodeString(PKMX_WCHAR s, int x = 0);
const char *Debug_UnicodeString(std::u16string s, int x = 0);
const char *Debug_ModifierName(KMX_UINT modifiers);
//inline KMX_BOOL ShouldDebug();
inline KMX_BOOL ShouldDebug() {
return TRUE; // g_debug_KeymanLog;
return g_debug_KeymanLog;
}

View file

@ -121,7 +121,7 @@ PKMX_WCHAR km::kbp::kmx::incxstr(PKMX_WCHAR p)
case CODE_INDEX: return p+2;
case CODE_USE: return p+1;
case CODE_DEADKEY: return p+1;
case CODE_EXTENDED: p += 2; while(*p != UC_SENTINEL_EXTENDEDEND) p++; return p+1;
case CODE_EXTENDED: p += 2; while(*p && *p != UC_SENTINEL_EXTENDEDEND) p++; return p+1;
case CODE_CLEARCONTEXT: return p+1;
case CODE_CALL: return p+1;
case CODE_CONTEXTEX: return p+1;

View file

@ -0,0 +1,62 @@
# KMX unit tests
This directory contains unit tests that test the KMX processing.
The driver (`kmx.cpp`) loads and executes the tests. Each test consist of a
Keyman keyboard source file (`0*.kmn`) and the compiled KMX keyboard.
The source file contains rules and defines the setup, input and expected results.
## Defining tests in the `.kmn` file
The top of the file contains comments that define the setup, input and expected results.
Following the comments are the normal rules of a Keyman keyboard.
### Setup
- **description**: this is just for commenting what we test here
```text
c Description: Tests Caps Lock env set
```
- **option**: allows to put the system in a defined state. The example below turns
caps-lock off.
```text
c option: &capsLock=0
```
- **context**: allows to setup a string which serves as context
```text
c context:
```
### Input
- **keys**: defines one or more virtual keys that will be processed one after the
other
```text
c keys: [K_1][K_CAPS][K_2][SHIFT K_3][K_4][K_CAPS][K_5][K_CAPS][K_6]
```
### Expected Result
- **expected**: the resulting string of processing the input keys. In the example
below each key press is expected to output the string `'pass.'`.
```text
c expected: pass.pass.pass.pass.pass.pass.
```
## Running the tests
All tests can be run at once with `meson test`.
Alternatively it's possible to run a single test with:
```bash
cd common/core/desktop
build/tests/unit/kmx/kmx 'tests/unit/kmx/038 - punctkeys.kmn' 'tests/unit/kmx/038 - punctkeys.kmx'
```

View file

@ -510,5 +510,6 @@ int main(int argc, char *argv[])
return 1;
}
km::kbp::kmx::g_debug_ToConsole = TRUE;
return run_test(argv[1], argv[2]);
}

View file

@ -55,8 +55,10 @@ namespace com.keyman.text {
* Handles default output and keyboard processing for both OSK and physical keystrokes.
*
* @param {Object} e The abstracted KeyEvent to use for keystroke processing
* @returns {Object} A RuleBehavior object describing the cumulative effects of
* all matched keyboard rules.
*/
processKeyEvent(keyEvent: KeyEvent) {
processKeyEvent(keyEvent: KeyEvent): RuleBehavior {
let formFactor = keyEvent.device.formFactor;
// Determine the current target for text output and create a "mock" backup
@ -70,14 +72,14 @@ namespace com.keyman.text {
// If it's a desktop OSK style and this triggers a layer change,
// a modifier key was clicked. No output expected, so it's safe to instantly exit.
if(this.keyboardProcessor.selectLayer(keyEvent)) {
return true;
return new RuleBehavior();
}
}
// Will handle keystroke-based non-layer change modifier & state keys, mapping them through the physical keyboard's version
// of state management.
if(!fromOSK && this.keyboardProcessor.doModifierPress(keyEvent, !fromOSK)) {
return true;
return new RuleBehavior();
}
// If suggestions exist AND space is pressed, accept the suggestion and do not process the keystroke.
@ -88,10 +90,10 @@ namespace com.keyman.text {
// Can the suggestion UI revert a recent suggestion? If so, do that and swallow the backspace.
if((keyEvent.kName == "K_BKSP" || keyEvent.Lcode == Codes.keyCodes["K_BKSP"]) && this.languageProcessor.tryRevertSuggestion()) {
return;
return new RuleBehavior();
// Can the suggestion UI accept an existing suggestion? If so, do that and swallow the space character.
} else if((keyEvent.kName == "K_SPACE" || keyEvent.Lcode == Codes.keyCodes["K_SPACE"]) && this.languageProcessor.tryAcceptSuggestion('space')) {
return;
return new RuleBehavior();
}
}
@ -171,15 +173,6 @@ namespace com.keyman.text {
}
}
/* I732 END - 13/03/2007 MCD: End Positional Layout support in OSK */
// TODO: rework the return value to be `ruleBehavior` instead. Functions that call this one are
// the ones that should worry about event handler returns, etc. Not this one.
//
// They should also be the ones to handle the TODOs seen earlier in this function -
// once THOSE are properly relocated. (They're too DOM-heavy to remain in web-core.)
// Only return true (for the eventual event handler's return value) if we didn't match a rule.
return ruleBehavior;
}

View file

@ -255,7 +255,7 @@ namespace com.keyman.text.prediction {
// the input will be automatically rewound to the preInput state.
transform: original.transform,
// The ID part is critical; the reversion can't be applied without it.
transformId: original.token, // reversions use the additive inverse.
transformId: -original.token, // reversions use the additive inverse.
displayAs: reversion.displayAs, // The real reason we needed to call the LMLayer.
id: reversion.id,
tag: reversion.tag

View file

@ -37,15 +37,15 @@ namespace com.keyman.keyboards {
shiftKey?: LayoutKey,
capsKey?: LayoutKey,
numKey?: LayoutKey,
scrollKey?: LayoutKey
scrollKey?: LayoutKey,
aligned?: boolean
}
export type LayoutFormFactor = {
"displayUnderlying"?: boolean,
"font": string,
"layer": LayoutLayer[],
keyLabels?: boolean,
isDefault?: boolean;
isDefault?: boolean
}
export type LayoutSpec = {
@ -172,6 +172,11 @@ namespace com.keyman.keyboards {
}
}
// If there is no predefined layout, even touch layouts will follow the desktop's
// setting for the displayUnderlying flag. As the desktop layout uses a different
// format for its layout spec, that's found at the field referenced below.
layout["displayUnderlying"] = !!keyboard.scriptObject['KDU'];
// For desktop devices, we must create all layers, even if invalid.
if(formFactor == 'desktop') {
invalidIdList = Layouts.generateLayerIds(chiral);

View file

@ -71,11 +71,6 @@ namespace com.keyman.keyboards {
return this.scriptObject['KN'];
}
get displaysUnderlyingKeys(): boolean {
// Returns false if undefined or false-like (including 0), true otherwise.
return !!this.scriptObject['KDU'];
}
// TODO: Better typing.
private get _legacyLayoutSpec(): any {
return this.scriptObject['KV']; // used with buildDefaultLayout; layout must be constructed at runtime.

View file

@ -14,10 +14,12 @@ namespace com.keyman {
Opera: KeyMap = new KeyMap();
constructor() {
//ffie['k109'] = 189; // - // These two number-pad VK rules are *not* correct for more recent FF! JMD 8/11/12
//ffie['k107'] = 187; // = // FF 3.0 // I2062
// All three have been around since at least May 2014 / FF 29.
// It'd hard to find precise history, but at least that much has been confirmed.
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode, on Feb 26 2021.
this.FF['k61'] = 187; // = // FF 2.0
this.FF['k59'] = 186; // ;
this.FF['k173'] = 189; // -/_
}
}

View file

@ -116,7 +116,8 @@ namespace com.keyman.text {
let toCaret = this.getDeadkeyCaret();
// Step 1: Determine the number of left-deletions.
for(var newCaret=0; newCaret < fromCaret; newCaret++) {
let maxLeftMatch = fromCaret < toCaret ? fromCaret : toCaret;
for(var newCaret=0; newCaret < maxLeftMatch; newCaret++) {
if(from._kmwCharAt(newCaret) != to._kmwCharAt(newCaret)) {
break;
}
@ -132,8 +133,16 @@ namespace com.keyman.text {
let undeletedRight = to._kmwLength() - toCaret;
let originalRight = from._kmwLength() - fromCaret;
let deletedRight = originalRight - undeletedRight;
return new TextTransform(delta, deletedLeft, originalRight - undeletedRight);
// May occur when reverting a suggestion that had been applied mid-word.
if(deletedRight < 0) {
// Restores deleteRight characters.
delta = delta + to._kmwSubstr(toCaret, -deletedRight);
deletedRight = 0;
}
return new TextTransform(delta, deletedLeft, deletedRight);
}
buildTranscriptionFrom(original: OutputTarget, keyEvent: KeyEvent, alternates?: Alternate[]): Transcription {

View file

@ -4,9 +4,10 @@ namespace com.keyman.text {
*/
export class RuleBehavior {
/**
* The before-and-after Transform from matching a keyboard rule.
* The before-and-after Transform from matching a keyboard rule. May be `null`
* if no keyboard rules were matched for the keystroke.
*/
transcription: Transcription;
transcription: Transcription = null;
/**
* Indicates whether or not a BEEP command was issued by the matched keyboard rule.
@ -26,7 +27,7 @@ namespace com.keyman.text {
/**
* Denotes a non-output default behavior; this should be evaluated later, against the true keystroke.
*/
triggersDefaultCommand?: boolean;
triggersDefaultCommand: boolean = false;
/**
* Denotes error log messages generated when attempting to generate this behavior.
@ -44,6 +45,10 @@ namespace com.keyman.text {
predictionPromise?: Promise<Suggestion[]>;
finalize(processor: KeyboardProcessor) {
if(!this.transcription) {
throw "Cannot finalize a RuleBehavior with no transcription.";
}
let outputTarget = this.transcription.keystroke.Ltarg;
if(processor.beepHandler && this.beep) {

View file

@ -4,16 +4,16 @@ namespace models {
export function applyTransform(transform: Transform, context: Context): Context {
// First, get the current context
let fullLeftContext = context.left || '';
let lLen = fullLeftContext.length;
let lLen = fullLeftContext.kmwLength();
let lDel = lLen < transform.deleteLeft ? lLen : transform.deleteLeft;
let leftContext = fullLeftContext.substring(0, lLen - lDel) + (transform.insert || '');
let leftContext = fullLeftContext.kmwSubstr(0, lLen - lDel) + (transform.insert || '');
let fullRightContext = context.right || '';
let rLen = fullRightContext.length;
let rLen = fullRightContext.kmwLength();
let rDel = rLen < transform.deleteRight ? rLen : transform.deleteRight;
let rightContext = fullRightContext.substring(rDel);
let rightContext = fullRightContext.kmwSubstr(rDel);
return {
left: leftContext,

View file

@ -10,6 +10,71 @@ var ModelCompositor = require('../../build/intermediate').ModelCompositor;
describe('ModelCompositor', function() {
describe('Prediction with 14.0+ models', function() {
describe('Basic suggestion generation', function() {
var plainModel = new TrieModel(jsonFixture('tries/english-1000'),
{wordBreaker: wordBreakers.default}
);
it('generates suggestions with expected properties', function() {
let compositor = new ModelCompositor(plainModel);
let context = {
left: 'th', startOfBuffer: true, endOfBuffer: true,
};
let inputTransform = {
insert: 'e',
deleteLeft: 0
};
let suggestions = compositor.predict(inputTransform, context);
suggestions.forEach(function(suggestion) {
// Suggstions are built based on the context state BEFORE the triggering
// input, replacing the prediction's root with the complete word.
//
// This is necessary, in part, for proper display-string construction.
assert.equal(suggestion.transform.deleteLeft, 2);
});
let keep = suggestions.find(function(suggestion) {
return suggestion.tag == 'keep';
});
assert.isDefined(keep);
assert.equal(keep.transform.insert, 'the ');
// Expect an appended space.
let expectedEntries = ['they ', 'there ', 'their ', 'these ', 'themselves '];
expectedEntries.forEach(function(entry) {
assert.isDefined(suggestions.find(function(suggestion) {
return suggestion.transform.insert == entry;
}));
});
});
it('properly handles suggestions after a backspace', function() {
let compositor = new ModelCompositor(plainModel);
let context = {
left: 'the ', startOfBuffer: true, endOfBuffer: true,
};
let inputTransform = {
insert: '',
deleteLeft: 1
};
let suggestions = compositor.predict(inputTransform, context);
suggestions.forEach(function(suggestion) {
// Suggestions always delete the full root of the suggestion.
//
// After a backspace, that means the text 'the' - 3 chars.
// Char 4 is for the original backspace, as suggstions are built
// based on the context state BEFORE the triggering input -
// here, a backspace.
assert.equal(suggestion.transform.deleteLeft, 4);
});
});
});
describe('applySuggestionCasing', function() {
let plainApplyCasing = function(caseToApply, text) {
switch(caseToApply) {
@ -764,7 +829,7 @@ describe('ModelCompositor', function() {
let baseSuggestion = initialSuggestions[1];
let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform);
assert.equal(reversion.transformId, baseSuggestion.transformId);
assert.equal(reversion.transformId, -baseSuggestion.transformId);
assert.equal(reversion.id, -baseSuggestion.id);
let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext);
@ -802,7 +867,7 @@ describe('ModelCompositor', function() {
let baseSuggestion = initialSuggestions[1];
let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform);
assert.equal(reversion.transformId, baseSuggestion.transformId);
assert.equal(reversion.transformId, -baseSuggestion.transformId);
assert.equal(reversion.id, -baseSuggestion.id);
// Accepting the suggestion adds an extra context state.

View file

@ -175,11 +175,23 @@ class ModelCompositor {
finalInput = inputTransform; // A fallback measure. Greatly matters for empty contexts.
}
let deleteLeft = 0;
// remove actual token string. If new token, there should be nothing to delete.
if(!newEmptyToken) {
// If this is triggered from a backspace, make sure to use its results
// and also include its left-deletions! It's the one post-input context case.
if(allowBksp) {
deleteLeft = this.wordbreak(postContext).kmwLength() + inputTransform.deleteLeft;
} else {
// Normal case - use the pre-input context.
deleteLeft = this.wordbreak(context).kmwLength();
}
}
// Replace the existing context with the correction.
let correctionTransform: Transform = {
insert: correction, // insert correction string
// remove actual token string. If new token, there should be nothing to delete.
deleteLeft: newEmptyToken ? 0 : this.wordbreak(context).length,
deleteLeft: deleteLeft,
id: inputTransform.id // The correction should always be based on the most recent external transform/transcription ID.
}
@ -220,10 +232,15 @@ class ModelCompositor {
}
// Section 2 - post-analysis for our generated predictions, managing 'keep'.
// Assumption: Duplicated 'displayAs' properties indicate duplicated Suggestions.
// When true, we can use an 'associative array' to de-duplicate everything.
let suggestionDistribMap: {[key: string]: ProbabilityMass<Suggestion>} = {};
let currentCasing: CasingForm = null;
if(lexicalModel.languageUsesCasing) {
currentCasing = this.detectCurrentCasing(postContext);
}
let baseWord = this.wordbreak(context);
// Deduplicator + annotator of 'keep' suggestions.
for(let prediction of rawPredictions) {
@ -253,6 +270,16 @@ class ModelCompositor {
keepOption.p += prediction.p;
}
} else {
// Apply capitalization rules now; facilitates de-duplication of suggestions
// that may be caused as a result.
//
// Example: "apple" and "Apple" are separate when 'lower', but identical for 'initial' and 'upper'.
if(currentCasing && currentCasing != 'lower') {
this.applySuggestionCasing(prediction.sample, baseWord, currentCasing);
// update the mapping string, too.
displayText = prediction.sample.displayAs;
}
let existingSuggestion = suggestionDistribMap[displayText];
if(existingSuggestion) {
existingSuggestion.p += prediction.p;
@ -306,21 +333,28 @@ class ModelCompositor {
// Apply 'after word' punctuation and casing (when applicable). Also, set suggestion IDs.
// We delay until now so that utility functions relying on the unmodified Transform may execute properly.
let currentCasing: CasingForm = null;
if(lexicalModel.languageUsesCasing) {
currentCasing = this.detectCurrentCasing(postContext);
}
let compositor = this;
let baseWord = this.wordbreak(context);
suggestions.forEach(function(suggestion) {
if(currentCasing && currentCasing != 'lower') {
compositor.applySuggestionCasing(suggestion, baseWord, currentCasing);
}
// Valid 'keep' suggestions may have zero length; we still need to evaluate the following code
// for such cases.
suggestion.transform.insert += punctuation.insertAfterWord;
// Do we need to manipulate the suggestion's transform based on the current state of the context?
if(!context.right) {
// Only insert wordbreak characters if we're at the end of the context.
suggestion.transform.insert += punctuation.insertAfterWord;
} else {
// If we're mid-word, delete its original post-caret text.
const tokenization = compositor.tokenize(context);
if(tokenization && tokenization.caretSplitsToken) {
// While we wait on the ability to provide a more 'ideal' solution, let's at least
// go with a more stable, if slightly less ideal, solution for now.
//
// A predictive text default (on iOS, at least) - immediately wordbreak
// on suggestions accepted mid-word.
suggestion.transform.insert += punctuation.insertAfterWord;
}
}
// If this is a suggestion after wordbreak input, make sure we preserve the wordbreak transform!
if(prefixTransform) {
@ -430,7 +464,6 @@ class ModelCompositor {
// Step 1: generate and save the reversion's Transform.
let sourceTransform = suggestion.transform;
let deletedLeftChars = context.left.kmwSubstr(-sourceTransform.deleteLeft, sourceTransform.deleteLeft);
// right deletion is currently not implemented.
let insertedLength = sourceTransform.insert.kmwLength();
let reversionTransform: Transform = {
@ -450,7 +483,15 @@ class ModelCompositor {
postContext = models.applyTransform(postTransform, postContext);
}
let revertedPrefix = this.wordbreak(postContext);
let revertedPrefix: string;
let postContextTokenization = this.tokenize(postContext);
if(postContextTokenization) {
// Handles display string for reversions triggered by accepting a suggestion mid-token.
revertedPrefix = postContextTokenization.left[postContextTokenization.left.length-1];
revertedPrefix += postContextTokenization.caretSplitsToken ? postContextTokenization.right[0] : '';
} else {
revertedPrefix = this.wordbreak(postContext);
}
let firstConversion = models.transformToSuggestion(reversionTransform);
firstConversion.displayAs = revertedPrefix;
@ -460,7 +501,7 @@ class ModelCompositor {
// set the Reversion's ID directly.
let reversion = this.toAnnotatedSuggestion(firstConversion, 'revert');
if(suggestion.transformId != null) {
reversion.transformId = suggestion.transformId;
reversion.transformId = -suggestion.transformId;
}
if(suggestion.id != null) {
// Since a reversion inverts its source suggestion, we set its ID to be the
@ -493,7 +534,13 @@ class ModelCompositor {
let compositor = this;
let fallbackSuggestions = function() {
let revertedContext = models.applyTransform(reversion.transform, context);
return compositor.predict({ insert: '', deleteLeft: 0}, revertedContext);
let suggestions = compositor.predict({insert: '', deleteLeft: 0}, revertedContext);
suggestions.forEach(function(suggestion) {
// A reversion's transform ID is the additive inverse of its original suggestion;
// we revert to the state of said original suggestion.
suggestion.transformId = -reversion.transformId;
});
return suggestions;
}
if(!this.contextTracker) {
@ -525,9 +572,16 @@ class ModelCompositor {
// Will need to be modified a bit if/when phrase-level suggestions are implemented.
// Those will be tracked on the first token of the phrase, which won't be the tail
// if they cover multiple tokens.
return this.contextTracker.newest.tail.replacements.map(function(trackedSuggestion) {
let suggestions = this.contextTracker.newest.tail.replacements.map(function(trackedSuggestion) {
return trackedSuggestion.suggestion;
});
suggestions.forEach(function(suggestion) {
// A reversion's transform ID is the additive inverse of its original suggestion;
// we revert to the state of said original suggestion.
suggestion.transformId = -reversion.transformId;
});
return suggestions;
}
private wordbreak(context: Context): string {
@ -549,6 +603,16 @@ class ModelCompositor {
}
}
private tokenize(context: Context): models.Tokenization {
let model = this.lexicalModel;
if(model.wordbreaker) {
return models.tokenize(model.wordbreaker, context);
} else {
return null;
}
}
public resetContext(context: Context) {
// Force-resets the context, throwing out any previous fat-finger data, etc.
// Designed for use when the caret has been directly moved and/or the context sourced from a different control

View file

@ -68,7 +68,7 @@ files:
- source: /ios/engine/KMEI/KeymanEngine/Classes/en.lproj/ResourceInfoView.strings
dest: /ios/engine/ResourceInfoView.strings
translation: /ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/%osx_code%/%original_file_name%
translation: /ios/engine/KMEI/KeymanEngine/Classes/%osx_code%/%original_file_name%
- source: /ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings
dest: /ios/engine/Localizable.strings

View file

@ -27,11 +27,12 @@ display_usage ( ) {
echo "Usage: $0 [-test] [-publish-to-npm]"
echo " $0 -help"
echo
echo " -help displays this screen and exits"
echo " -test runs unit tests after building"
echo " -tdd runs unit tests WITHOUT building"
echo " -publish-to-npm publishes the current version to the npm package index"
echo " -dry-run do build, etc, but don't actually publish"
echo " -help displays this screen and exits"
echo " -test runs unit tests after building"
echo " -tdd runs unit tests WITHOUT building"
echo " -skip-package-install, -S skip package installation"
echo " -publish-to-npm publishes the current version to the npm package index"
echo " -dry-run do build, etc, but don't actually publish"
}
################################ Main script ################################

View file

@ -138,7 +138,7 @@ function _parseWordList(wordlist: WordList, source: WordListSource): void {
}
wordsSeenInThisFile.add(wordform);
wordlist[wordform] = (wordlist[wordform] || 0) + count;
wordlist[wordform] = (isNaN(wordlist[wordform]) ? 0 : wordlist[wordform] || 0) + count;
}
}
@ -459,15 +459,22 @@ namespace Trie {
* @param node The node to start summing weights.
*/
function sumWeights(node: Node): number {
let val: number;
if (node.type === 'leaf') {
return node.entries
val = node.entries
.map(entry => entry.weight)
//.map(entry => isNaN(entry.weight) ? 1 : entry.weight)
.reduce((acc, count) => acc + count, 0);
} else {
return Object.keys(node.children)
val = Object.keys(node.children)
.map((key) => sumWeights(node.children[key]))
.reduce((acc, count) => acc + count, 0);
}
if(isNaN(val)) {
console.error("Unexpected NaN has appeared!");
}
return val;
}
}

View file

@ -32,8 +32,9 @@ export default class KmpCompiler {
// 1. Unwrap arrays (and convert to array where single object)
// 2. Fix casing on `iD`
// 3. Rewrap info, keyboard.languages, lexicalModel.languages, startMenu.items elements
// 4. Convert options.followKeyboardVersion to a bool
// 5. Filenames need to be basenames (but this comes after processing)
// 4. Remove options.followKeyboardVersion, file.fileType
// 5. Convert file.copyLocation to a Number
// 6. Filenames need to be basenames (but this comes after processing)
//
// Helper functions
@ -66,9 +67,7 @@ export default class KmpCompiler {
let kmp: KmpJsonFile = {
system: kps.system,
options: {
followKeyboardVersion: kps.options.followKeyboardVersion === ''
}
options: {}
};
// Fill in additional fields
@ -76,10 +75,7 @@ export default class KmpCompiler {
let keys: (keyof KpsFileOptions & keyof KmpJsonFileOptions)[] = ['executeProgram', 'graphicFile', 'msiFilename', 'msiOptions', 'readmeFile'];
for (let element of keys) {
if (kps.options[element]) {
// TypeScript thinks it's possible to assign to followKeyboardVersion (a
// boolean), but in reality, all of the other keys are strings.
// Politely inform TypeScript that it's okay to do this assignment:
(<string> kmp.options[element]) = kps.options[element];
kmp.options[element] = kps.options[element];
}
}
@ -88,7 +84,14 @@ export default class KmpCompiler {
}
if(kps.files && kps.files.file) {
kmp.files = arrayWrap(kps.files.file);
kmp.files = arrayWrap(kps.files.file).map((file: KpsFileContentFile) => {
return {
name: file.name,
description: file.description,
copyLocation: parseInt(file.copyLocation, 10)
// note: we don't emit fileType as that is not permitted in kmp.json
}
});
}
if(kps.keyboards && kps.keyboards.keyboard) {

View file

@ -15,7 +15,6 @@ interface KmpJsonFileSystem {
}
interface KmpJsonFileOptions {
followKeyboardVersion: boolean;
readmeFile?: string;
graphicFile?: string;
executeProgram?: string;

View file

@ -3,9 +3,7 @@
"keymanDeveloperVersion": "12.0.1500.0",
"fileVersion": "12.0"
},
"options": {
"followKeyboardVersion": true
},
"options": {},
"info": {
"author": {
"description": "Eddie Antonio Santos",
@ -25,8 +23,7 @@
{
"name": "..\\build\\example.qaa.sencoten.model.js",
"description": "Lexical model example.qaa.sencoten.model.js",
"copyLocation": "0",
"fileType": ".model.js"
"copyLocation": 0
}
],
"lexicalModels": [

View file

@ -510,7 +510,7 @@
CE9E95CC24CE786900F6DD78 /* UniversalLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalLinks.swift; sourceTree = "<group>"; };
CE9E95CE24CE7A2C00F6DD78 /* UniversalLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalLinkTests.swift; sourceTree = "<group>"; };
CE9ECB1D24D3A8CC007C5718 /* PackageInstallView_iPad.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PackageInstallView_iPad.xib; sourceTree = "<group>"; };
CEA1486E2407808F00C6ECD2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
CEA1486E2407808F00C6ECD2 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
CEA14871240780E100C6ECD2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ResourceInfoView.xib; sourceTree = "<group>"; };
CEA14874240780EF00C6ECD2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ResourceInfoView.strings; sourceTree = "<group>"; };
CEA148772407869200C6ECD2 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/ResourceInfoView.strings; sourceTree = "<group>"; };
@ -520,6 +520,14 @@
CEA9670A24BEC8030035AACF /* EngineStateBundler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EngineStateBundler.swift; sourceTree = "<group>"; };
CEA9670C24BEEFF80035AACF /* khmer_angkor update-base.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "khmer_angkor update-base.bundle"; sourceTree = "<group>"; };
CEA9670E24BEF05A0035AACF /* Updates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updates.swift; sourceTree = "<group>"; };
CEACC90125F07C81006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/ResourceInfoView.strings; sourceTree = "<group>"; };
CEACC90325F07CAF006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
CEACC90425F07CB6006EAB45 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/Localizable.strings; sourceTree = "<group>"; };
CEACC90525F07CBA006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
CEACC90625F07CBC006EAB45 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = km; path = km.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
CEACC90725F07CDA006EAB45 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/ResourceInfoView.strings; sourceTree = "<group>"; };
CEACC90825F07CE9006EAB45 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
CEACC90925F07CED006EAB45 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
CEB8276624C6811800F3D39C /* KeymanHostTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanHostTests.swift; sourceTree = "<group>"; };
CEC0C66B2410AC9A003E1BCD /* Sentry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sentry.framework; path = ../../Carthage/Build/iOS/Sentry.framework; sourceTree = "<group>"; };
CEC0C6762410E049003E1BCD /* SentryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryManager.swift; sourceTree = "<group>"; };
@ -1293,6 +1301,8 @@
en,
Base,
km,
de,
fr,
);
mainGroup = F243887314BBD43000A3E055;
productRefGroup = F243887F14BBD43000A3E055 /* Products */;
@ -1672,6 +1682,9 @@
isa = PBXVariantGroup;
children = (
CE96E42C24D1229A005B8E5A /* en */,
CEACC90525F07CBA006EAB45 /* de */,
CEACC90625F07CBC006EAB45 /* km */,
CEACC90925F07CED006EAB45 /* fr */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@ -1680,6 +1693,9 @@
isa = PBXVariantGroup;
children = (
CEA1486E2407808F00C6ECD2 /* en */,
CEACC90325F07CAF006EAB45 /* de */,
CEACC90425F07CB6006EAB45 /* km */,
CEACC90825F07CE9006EAB45 /* fr */,
);
name = Localizable.strings;
sourceTree = "<group>";
@ -1690,6 +1706,8 @@
CEA14871240780E100C6ECD2 /* Base */,
CEA14874240780EF00C6ECD2 /* en */,
CEA148772407869200C6ECD2 /* km */,
CEACC90125F07C81006EAB45 /* de */,
CEACC90725F07CDA006EAB45 /* fr */,
);
name = ResourceInfoView.xib;
sourceTree = "<group>";

View file

@ -19,7 +19,8 @@ protocol HTTPDownloadDelegate: class {
class HTTPDownloader: NSObject {
var queue: [HTTPDownloadRequest] = []
// TODO: Make unowned
weak var handler: HTTPDownloadDelegate?
/*weak*/ var handler: HTTPDownloadDelegate? // 'weak' interferes with installs
// from the app's browser.
var currentRequest: HTTPDownloadRequest?
var downloadSession: URLSession!
public var userInfo: [String: Any] = [:]

View file

@ -386,7 +386,7 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
if(self.swallowBackspaceTextChange) {
// A single keyboard processing command should never trigger two of these in a row;
// only one output function will perform deletions.
// This should allow us to debug any failures of this assumption.
// So far, only occurs when debugging a breakpoint during a touch event on BKSP,
// so all seems good.

View file

@ -366,7 +366,9 @@ extension KeymanWebViewController {
* thereafter for settings updates.
*/
func setSentryState(enabled: Bool = SentryManager.enabled) {
webView?.evaluateJavaScript("sentryManager.enabled = \(enabled ? "true" : "false")")
// This may be called before the page - and thus the `sentryManager` variable -
// is ready. It's a known limitation, so why log error reports for it?
webView?.evaluateJavaScript("try { sentryManager.enabled = \(enabled ? "true" : "false") } catch(err) { }")
}
}

View file

@ -1,2 +0,0 @@
/* Class = "UILabel"; text = "Scan this code to load this keyboard on another device"; ObjectID = "z2O-MT-IoV"; */
"z2O-MT-IoV.text" = "ស្កេនកូដ​នេះដើម្បី​ផ្ទុក​ក្ដារចុចនេះ​នៅ​លើ​ឧបករណ៍​ផ្សេង";

View file

@ -8,4 +8,27 @@
import XCGLogger
public let log = XCGLogger(identifier: "KeymanEngine", includeDefaultDestinations: true)
// From XCGLogger docs:
// Note: This creates the log object lazily, which means it's not created until it's actually needed.
public let log: XCGLogger = {
// Default: the 'console', which is read by Xcode but doesn't reach the system logs.
let mainLog = XCGLogger(identifier: "KeymanEngine", includeDefaultDestinations: false)
// Ensures our log messages go out to the device's system log as well as the console.
let systemLogDest = AppleSystemLogDestination(identifier: "")
systemLogDest.showLogIdentifier = true
mainLog.add(destination: systemLogDest)
// Temporary logging level to ensure that app details are reported properly.
mainLog.outputLevel = .info
mainLog.logAppDetails()
#if DEBUG
mainLog.outputLevel = .debug
#else
mainLog.outputLevel = .warning
#endif
return mainLog
}()

View file

@ -121,7 +121,9 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
var associationQueryProgress: [Int: LanguagePickAssociator.Progress] = [:]
var installProgressMap: [KeymanPackage.Key: PackageInstallResult?] = [:]
let progressCallback: ProgressReceiver
let externalProgressCallback: ProgressReceiver
var promptProgressCallback: ProgressReceiver?
let downloadManager: ResourceDownloadManager
var isCancelled = false
@ -129,7 +131,7 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
init(with associationSpecs: [Associator], downloadManager: ResourceDownloadManager, receiver: @escaping ProgressReceiver) {
self.associationSpecs = associationSpecs
self.downloadManager = downloadManager
self.progressCallback = receiver
self.externalProgressCallback = receiver
}
// Since progress info is stored here, it makes the most sense to track progress-related
@ -139,12 +141,19 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
return !isCancelled && pickingCompleted
}
internal func notifyProgress(_ status: Progress) {
// Prompt gets first dibs - that way, if a prompt exists, its UI code
// executes before control transfers back to other modules.
self.promptProgressCallback?(status)
self.externalProgressCallback(status)
}
/**
* Computes the initial level of progress made toward the overall installation at the time that language selections were
* finalized.
*/
internal func initializeProgress() {
self.progressCallback(.starting)
notifyProgress(.starting)
}
/**
@ -153,7 +162,7 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
internal func reportProgress(complete: Bool = false) {
if reportsProgress {
if complete {
progressCallback(.complete)
notifyProgress(.complete)
} else {
//progressCallback(.inProgress)
}
@ -161,7 +170,7 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
}
internal func reportCancelled() {
progressCallback(.cancelled)
notifyProgress(.cancelled)
}
}
@ -388,8 +397,7 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
* May only be called once during the lifetime of its instance and is mutually exclusive with `pickLanguages`,
* the programmatic alternative.
*/
public func promptForLanguages(inNavigationVC navVC: UINavigationController,
uiCompletionHandler: @escaping (() -> Void)) {
public func promptForLanguages(inNavigationVC navVC: UINavigationController) {
guard self.associationQueriers == nil, !closureShared.pickingCompleted else {
fatalError("Invalid state - language picking has already been triggered.")
}
@ -397,12 +405,21 @@ public class AssociatingPackageInstaller<Resource: LanguageResource, Package: Ty
initializeSynchronizationGroups()
constructAssociationPickers()
closureShared.installGroup.enter()
let wrappedCompletionHandler = {
self.closureShared.installGroup.leave()
}
let pickerPrompt = PackageInstallViewController<Resource>(for: self.package,
defaultLanguageCode: defaultLgCode,
languageAssociators: associationQueriers!,
pickingCompletionHandler: coreInstallationClosure(),
uiCompletionHandler: uiCompletionHandler)
uiCompletionHandler: wrappedCompletionHandler)
closureShared.promptProgressCallback = { progress in
pickerPrompt.progressUpdate(progress)
}
navVC.pushViewController(pickerPrompt, animated: true)
}

View file

@ -69,6 +69,9 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat
private let languageCode: String?
private let session: URLSession
private var progressView: UIProgressView?
private var observation: NSKeyValueObservation? = nil
private static var ENDPOINT_ROOT: URL {
var baseURL = KeymanHosts.KEYMAN_COM
baseURL.appendPathComponent("go")
@ -93,6 +96,10 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat
fatalError("init(coder:) has not been implemented")
}
deinit {
observation = nil
}
public override func loadView() {
let webView = WKWebView()
webView.navigationDelegate = self
@ -104,6 +111,18 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat
webView.load(URLRequest(url: KeyboardSearchViewController.ENDPOINT_ROOT))
}
progressView = UIProgressView(progressViewStyle: .bar)
progressView!.translatesAutoresizingMaskIntoConstraints = false
observation = webView.observe(\.estimatedProgress) { _, _ in
if let progressView = self.progressView {
progressView.setProgress(Float(webView.estimatedProgress), animated: true)
progressView.isHidden = progressView.progress > 0.99
}
}
progressView!.setProgress(1, animated: false)
progressView!.isHidden = true
webView.addSubview(progressView!)
view = webView
}
@ -131,6 +150,25 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat
}
decisionHandler(.allow)
// Makes it clear that there IS a progress bar, in case of super-slow response.
progressView?.setProgress(0, animated: false)
// This way, if the load is instant, the 0.01 doesn't really stand out.
progressView?.setProgress(0.01, animated: true)
progressView?.isHidden = false
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let navVC = self.navigationController {
progressView?.topAnchor.constraint(equalTo: navVC.navigationBar.bottomAnchor).isActive = true
} else {
progressView?.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
}
progressView?.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true
progressView?.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
}
override public func viewWillDisappear(_ animated: Bool) {

View file

@ -59,6 +59,8 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
private var navMapping: [NavigationMode : UIBarButtonItem] = [:]
private var dismissalBlock: (() -> Void)? = nil
private weak var welcomeView: UIView?
private var mayPick: Bool = true
public init(for package: Resource.Package,
defaultLanguageCode: String? = nil,
@ -267,9 +269,11 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
}
set(mode) {
leftNavMode = mode
if mayPick {
leftNavMode = mode
navigationItem.leftBarButtonItem = navMapping[mode]
navigationItem.leftBarButtonItem = navMapping[mode]
}
}
}
@ -279,9 +283,11 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
}
set(mode) {
rightNavMode = mode
if mayPick {
rightNavMode = mode
navigationItem.rightBarButtonItem = navMapping[mode]
navigationItem.rightBarButtonItem = navMapping[mode]
}
}
}
@ -349,20 +355,29 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
Manager.shared.shouldReloadKeyboard = true
self.pickingCompletionHandler(selectedResources.map { $0.typedFullID })
let dismissalBlock = {
if let nvc = self.navigationController {
self.dismiss(animated: true)
nvc.popToRootViewController(animated: false)
} else { // Otherwise, if the root view of a navigation controller, dismiss it outright. (pop not available)
self.dismiss(animated: true)
}
// Prevent swipe dismissal.
if #available(iOSApplicationExtension 13.0, *) {
self.isModalInPresentation = true
}
// No more selection-manipulation allowed.
// This matters when there's no welcome page available.
languageTable.isUserInteractionEnabled = false
// Prevent extra 'install' commands and nav-bar related manipulation.
self.navigationItem.leftBarButtonItem?.isEnabled = false
self.rightNavigationMode = .none
self.mayPick = false
let dismissalBlock = {
self.associators.forEach { $0.pickerFinalized() }
}
// First, show the package's welcome - if it exists.
if let welcomeVC = PackageWebViewController(for: package, page: .welcome) {
self.dismissalBlock = dismissalBlock
// Prevent swipe dismissal.
if #available(iOSApplicationExtension 13.0, *) {
welcomeVC.isModalInPresentation = true
}
let subNavVC = UINavigationController(rootViewController: welcomeVC)
_ = subNavVC.view
@ -383,8 +398,16 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
subNavVC.presentationController?.delegate = self
self.present(subNavVC, animated: true, completion: nil)
self.welcomeView = welcomeVC.view
self.dismissalBlock = {
// Tells the user that we've received the 'done' command.
doneItem.isEnabled = false
dismissalBlock()
}
} else {
dismissalBlock()
self.dismissalBlock = dismissalBlock
onWelcomeDismissed()
}
}
@ -393,10 +416,33 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
}
@objc private func onWelcomeDismissed() {
self.dismissalBlock?()
self.dismissalBlock = nil
if let dismissalBlock = self.dismissalBlock {
dismissalBlock()
self.dismissalBlock = nil
self.uiCompletionHandler()
// Tell our owner (the AssociatingPackageInstaller) that all UI interactions are done.
// Triggers synchronization code, so make sure it only runs once!
self.uiCompletionHandler()
}
// Show a spinner forever.
// When installation is complete, this controller's view will be dismissed,
// removing said spinner.
let activitySpinner = Alerts.constructActivitySpinner()
// Determine the top-most view. If we're presenting the welcome page, THAT.
// If we aren't, our directly-owned view should be the top-most.
let activeView: UIView = self.welcomeView ?? view
activitySpinner.center = activeView.center
activitySpinner.startAnimating()
activeView.addSubview(activitySpinner)
activitySpinner.centerXAnchor.constraint(equalTo: activeView.centerXAnchor).isActive = true
activitySpinner.centerYAnchor.constraint(equalTo: activeView.centerYAnchor).isActive = true
// Note: we do NOT block user interaction; we merely bind them to the active view.
// Why prevent them from reading more on the welcome page while they wait?
}
public func tableView(_ tableView: UITableView, titleForHeaderInSection: Int) -> String? {
@ -515,4 +561,20 @@ public class PackageInstallViewController<Resource: LanguageResource>: UIViewCon
associators.forEach { $0.deselectLanguages( Set([languages[indexPath.row].id]) ) }
}
internal func progressUpdate<Package: TypedKeymanPackage<Resource>>(_ status: AssociatingPackageInstaller<Resource, Package>.Progress) where Resource.Package == Package {
switch(status) {
case .starting, .inProgress:
// nothing worth note
break
// All UI interactions have been completed AND installation is fully complete.
case .complete, .cancelled:
if let nvc = self.navigationController {
self.dismiss(animated: true)
nvc.popToRootViewController(animated: false)
} else { // Otherwise, if the root view of a navigation controller, dismiss it outright. (pop not available)
self.dismiss(animated: true)
}
}
}
}

View file

@ -682,4 +682,66 @@ public class ResourceDownloadManager {
value: PackageDownloadFailedNotification(packageKey: packageKey, error: error))
}
}
/**
* Designed for downloading KMP files when no metadata is available in advance.
*/
public func downloadRawKMP(from url: URL, handler: @escaping (URL?, Error?) -> Void) {
// First, we need something to handle the download.
class NoMetadataDelegate: HTTPDownloadDelegate {
private let closure: (URL?, Error?) -> Void
init(withHandler handler: @escaping (URL?, Error?) -> Void) {
self.closure = handler;
}
func downloadRequestStarted(_ request: HTTPDownloadRequest) {
// Not relevant
}
func downloadRequestFinished(_ request: HTTPDownloadRequest) {
if request.responseStatusCode != 200 {
// Possible request error (400 Bad Request, 404 Not Found, etc.)
let error = DownloadError.failed(.responseCode(request.responseStatusCode ?? 400,
request.responseStatusMessage ?? "",
request.url))
// Now that we've synthesized an appropriate error instance, use the same handler
// as for HTTPDownloader's 'failed' condition.
downloadRequestFailed(request, with: error)
} else {
self.closure(URL(fileURLWithPath: request.destinationFile!), nil)
}
}
func downloadRequestFailed(_ request: HTTPDownloadRequest, with error: Error?) {
self.closure(nil, error)
}
func downloadQueueFinished(_ queue: HTTPDownloader) {
// Not relevant
}
func downloadQueueCancelled(_ queue: HTTPDownloader) {
// Not relevant, but to be safe...
self.closure(nil, nil)
}
}
let delegate = NoMetadataDelegate(withHandler: handler)
let downloader = HTTPDownloader(delegate, session: self.session)
let request = HTTPDownloadRequest(url: url, downloadType: .downloadFile)
// Since we don't know the package key in advance, we'll download it to
// the app's cache directory, then figure everything out once we open it.
let cachesDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let tempFilename = url.lastPathComponent
let cachedDest = cachesDir.appendingPathComponent(tempFilename)
request.destinationFile = cachedDest.path
downloader.addRequest(request)
downloader.run()
}
}

View file

@ -228,27 +228,23 @@ public class ResourceFileManager {
activitySpinner.centerXAnchor.constraint(equalTo: rootVC.view.centerXAnchor).isActive = true
activitySpinner.centerYAnchor.constraint(equalTo: rootVC.view.centerYAnchor).isActive = true
rootVC.view.isUserInteractionEnabled = false
} else if status == .complete {
} else if status == .complete || status == .cancelled {
// Report completion!
activitySpinner.stopAnimating()
activitySpinner.removeFromSuperview()
rootVC.view.isUserInteractionEnabled = true
rootVC.dismiss(animated: true, completion: nil)
rootVC.dismiss(animated: true) {
Manager.shared.showKeyboard()
}
successHandler?(package)
}
}
if let navVC = rootVC as? UINavigationController {
packageInstaller.promptForLanguages(inNavigationVC: navVC) {
// The user will be on the main screen after this, so we should resummon the keyboard.
Manager.shared.showKeyboard()
}
packageInstaller.promptForLanguages(inNavigationVC: navVC)
} else {
let nvc = UINavigationController.init()
packageInstaller.promptForLanguages(inNavigationVC: nvc) {
// The user will be on the main screen after this, so we should resummon the keyboard.
Manager.shared.showKeyboard()
}
packageInstaller.promptForLanguages(inNavigationVC: nvc)
rootVC.present(nvc, animated: true, completion: nil)
}
}
@ -265,13 +261,14 @@ public class ResourceFileManager {
}
public func buildKMPError(_ error: KMPError) -> UIAlertController {
return buildSimpleAlert(title: "Error", message: error.localizedDescription)
return buildSimpleAlert(title: NSLocalizedString("alert-error-title", bundle: engineBundle, comment: ""),
message: error.localizedDescription)
}
public func buildSimpleAlert(title: String, message: String, completionHandler: (() -> Void)? = nil ) -> UIAlertController {
let alertController = UIAlertController(title: title, message: message,
preferredStyle: UIAlertController.Style.alert)
alertController.addAction(UIAlertAction(title: "OK",
alertController.addAction(UIAlertAction(title: NSLocalizedString("command-ok", bundle: engineBundle, comment: ""),
style: UIAlertAction.Style.default,
handler: { _ in
completionHandler?()

View file

@ -253,7 +253,7 @@ class LanguageSettingsViewController: UITableViewController {
}
default:
cell.textLabel?.text = "error"
cell.textLabel?.text = NSLocalizedString("alert-error-title", bundle: engineBundle, comment: "")
}
}
}

View file

@ -1,3 +1,2 @@
/* Class = "UILabel"; text = "Scan this code to load this keyboard on another device"; ObjectID = "z2O-MT-IoV"; */
"z2O-MT-IoV.text" = "Scan this code to load this keyboard on another device";
"z2O-MT-IoV.text" = "ស្កេនកូដ​នេះដើម្បី​ផ្ទុក​ក្ដារចុចនេះ​នៅ​លើ​ឧបករណ៍​ផ្សេង";

View file

@ -15,8 +15,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool {
KeymanEngine.log.outputLevel = .debug
KeymanEngine.log.logAppDetails()
Manager.applicationGroupIdentifier = "group.KMEI"
Manager.shared.canRemoveDefaultKeyboard = true

View file

@ -227,10 +227,11 @@ class MainViewController: UIViewController, UIAlertViewDelegate, TextViewDelegat
}
@objc func showAlert(_ message: String) {
let alertController = UIAlertController(title: "Keyboard Download Error",
let engineBundle = Bundle(for: Manager.self)
let alertController = UIAlertController(title: NSLocalizedString("alert-download-error-title", bundle: engineBundle, comment: ""),
message: message,
preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "OK",
alertController.addAction(UIAlertAction(title: NSLocalizedString("command-ok", bundle: engineBundle, comment: ""),
style: UIAlertActionStyle.default,
handler: nil))
self.present(alertController, animated: true, completion: nil)

View file

@ -10,8 +10,7 @@ import KeymanEngine
class KeyboardViewController: InputViewController {
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
KeymanEngine.log.outputLevel = .debug
KeymanEngine.log.logAppDetails()
_ = log // forces init of the log, which is useful in sys-kbd contexts.
Manager.applicationGroupIdentifier = "group.KMEI"
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}

View file

@ -4,7 +4,7 @@ title: Basic Help
* [Switching Between Keyboards](switching-between-keyboards)
* [Installing Custom Keyboards](installing-custom-keyboards)
* [Installing Custom Keyboards/Dictionaries](installing-custom-keyboards-dictionaries)
* [Sharing Keyboards](sharing-keyboards)
* [Using the Keyman Browser](using-keyman-browser)

View file

@ -1,10 +1,10 @@
---
title: Installing Custom Keyboards - Keyman for iPhone and iPad Help
title: Installing Custom Keyboards/Dictionaries - Keyman for iPhone and iPad Help
---
The following steps can also be used to install a dictionary package.
The following steps can be used to install either a keyboard package or a dictionary package.
###Download the File
### Download the File
If downloading a custom keyboard from the internet, click the link to your custom keyboard package file.
For this example, we'll install a custom keyboard from a link in Safari. Our example keyboard is for the GFF Amharic 7 keyboard.

View file

@ -45,4 +45,4 @@ Selecting the notification seen at the top will lead you to the following page:
Select the big **Install on iPhone** (or
**Install on iPad**) option will then download
the file for easy installation. (Refer to [Installing custom keyboards](installing-custom-keyboards) as necessary.)
the file for easy installation. (Refer to [Installing custom keyboards/dictionaries](installing-custom-keyboards-dictionaries) as necessary.)

View file

@ -24,7 +24,7 @@ title: Keyman for iPhone and iPad 14.0 Help
### [Using Keyman for iPhone and iPad](basic/)
* [Switching Between Keyboards](basic/switching-between-keyboards)
* [Installing Custom Keyboards](basic/installing-custom-keyboards)
* [Installing Custom Keyboards/Dictionaries](basic/installing-custom-keyboards-dictionaries)
* [Sharing Keyboards](basic/sharing-keyboards)
* [Using the Keyman Browser](basic/using-keyman-browser)
* [Removing Keyboards](basic/uninstalling-keyboards)

View file

@ -317,7 +317,7 @@
CE002CB42408B4A5002026CE /* Sentry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sentry.framework; path = ../../Carthage/Build/iOS/Sentry.framework; sourceTree = "<group>"; };
CE1F5ECD23331DA400141F3E /* OfflineHelp.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = OfflineHelp.bundle; sourceTree = "<group>"; };
CE2B1E4B21B6112B007D092E /* DeviceKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceKit.framework; path = ../../Carthage/Build/iOS/DeviceKit.framework; sourceTree = "<group>"; };
CE4C654524D0020900070856 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
CE4C654524D0020900070856 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
CE6137FF1FB99538009D0EF2 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CE6138031FB999C8009D0EF2 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CE79CDB42370111200010C06 /* Themes+Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Themes+Colors.xcassets"; sourceTree = "<group>"; };
@ -325,6 +325,9 @@
CE7C1AE1236925D800100C2C /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageBrowserViewController.swift; sourceTree = "<group>"; };
CE80AD32257F2B4A008D2150 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CEACC90E25F07D58006EAB45 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
CEACC90F25F07D5A006EAB45 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/Localizable.strings; sourceTree = "<group>"; };
CEACC91225F07D77006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ImageBanner.xib; sourceTree = "<group>"; };
CEF4E55823E967140065B9C7 /* ImageBannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBannerViewController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@ -772,6 +775,8 @@
en,
Base,
km,
fr,
de,
);
mainGroup = 98ABADA7176935E400B62590;
productRefGroup = 98ABADB1176935E400B62590 /* Products */;
@ -1090,6 +1095,9 @@
isa = PBXVariantGroup;
children = (
CE4C654524D0020900070856 /* en */,
CEACC90E25F07D58006EAB45 /* fr */,
CEACC90F25F07D5A006EAB45 /* km */,
CEACC91225F07D77006EAB45 /* de */,
);
name = Localizable.strings;
sourceTree = "<group>";

View file

@ -36,7 +36,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
vc.dismiss(animated: true, completion: nil)
if let package = rfm.prepareKMPInstall(from: destinationUrl, alertHost: vc) {
// We choose to prompt the user for comfirmation, rather
// First, explicitly hide the keyboard. Otherwise, the app may try to
// redisplay it before package installation is fully complete.
Manager.shared.hideKeyboard()
// We choose to prompt the user for confirmation, rather
// than automatically installing the package.
//
// Since we're operating at the root, we want to present in a separate UINavigationController.
@ -54,20 +58,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
SentryManager.start()
// Forces the logs to initialize, as their definitions result in lazy init.
// These references have been configured to also log app details.
_ = log
_ = KeymanEngine.log
UniversalLinks.externalLinkLauncher = { url in
UIApplication.shared.openURL(url)
}
#if DEBUG
KeymanEngine.log.outputLevel = .debug
log.outputLevel = .debug
KeymanEngine.log.logAppDetails()
#else
KeymanEngine.log.outputLevel = .warning
log.outputLevel = .warning
#endif
Manager.applicationGroupIdentifier = "group.KM4I"
// TODO: Assign a subclassed version of InputViewController that implements the image stuff.

View file

@ -204,6 +204,38 @@ class WebBrowserViewController: UIViewController, UIWebViewDelegate, UIAlertView
func webView(_ webView: UIWebView,
shouldStartLoadWith request: URLRequest,
navigationType: UIWebView.NavigationType) -> Bool {
if request.url?.lastPathComponent.hasSuffix(".kmp") ?? false {
// Can't have the browser auto-download with no way to select a different page.
// Can't just ignore it, either, as the .kmp may result from a redirect from
// the previous URL. (Like if using the keyman.com keyboard search!)
let userData = UserDefaults.standard
userData.set(nil as String?, forKey: webBrowserLastURLKey)
userData.synchronize()
// The user is trying to download a .kmp, but the standard
// UIWebView can't handle it properly.
ResourceDownloadManager.shared.downloadRawKMP(from: request.url!) { file, error in
// do something!
if let error = error {
let alertTitle = NSLocalizedString("alert-error-title", bundle: Bundle(for: Manager.self), comment: "")
let alert = ResourceFileManager.shared.buildSimpleAlert(title: alertTitle,
message: error.localizedDescription)
self.present(alert, animated: true, completion: nil)
return
}
// Re-use the standard 'open random file' code as when launching the
// app from a file. This will also auto-dismiss the browser, returning
// to the app's main screen.
if let file = file {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
_ = appDelegate.application(UIApplication.shared, open: file)
}
}
return false
}
updateAddress(request)
return true
}
@ -222,13 +254,27 @@ class WebBrowserViewController: UIViewController, UIWebViewDelegate, UIAlertView
func webView(_ webView: UIWebView, didFailLoadWithError error: Error) {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
updateButtons()
let alertController = UIAlertController(title: "Cannot Open Page",
message: error.localizedDescription,
preferredStyle: UIAlertController.Style.alert)
alertController.addAction(UIAlertAction(title: "OK",
style: UIAlertAction.Style.default,
handler: nil))
self.present(alertController, animated: true, completion: nil)
var signalError: Bool = true
// An error will likely result if the user attempts to download a KMP,
// despite the fact that we tell it not to attempt a load.
let nsError = error as NSError
if let url = nsError.userInfo["NSErrorFailingURLKey"] as? NSURL {
signalError = !(url.path?.hasSuffix(".kmp") ?? false)
}
if signalError {
let alertController = UIAlertController(title: NSLocalizedString("error-opening-page", comment: ""),
message: error.localizedDescription,
preferredStyle: UIAlertController.Style.alert)
alertController.addAction(UIAlertAction(title: NSLocalizedString("command-ok",
bundle: Bundle(for: Manager.self),
comment: ""),
style: UIAlertAction.Style.default,
handler: nil))
self.present(alertController, animated: true, completion: nil)
}
}
private func updateButtons() {

View file

@ -8,4 +8,27 @@
import XCGLogger
let log = XCGLogger(identifier: "Keyman", includeDefaultDestinations: true)
// From XCGLogger docs:
// Note: This creates the log object lazily, which means it's not created until it's actually needed.
public let log: XCGLogger = {
// Default: the 'console', which is read by Xcode but doesn't reach the system logs.
let mainLog = XCGLogger(identifier: "Keyman", includeDefaultDestinations: false)
// Ensures our log messages go out to the device's system log as well as the console.
let systemLogDest = AppleSystemLogDestination(identifier: "")
systemLogDest.showLogIdentifier = true
mainLog.add(destination: systemLogDest)
// Temporary logging level to ensure that app details are reported properly.
mainLog.outputLevel = .info
mainLog.logAppDetails()
#if DEBUG
mainLog.outputLevel = .debug
#else
mainLog.outputLevel = .warning
#endif
return mainLog
}()

View file

@ -116,9 +116,7 @@ class PackageBrowserViewController: UIDocumentPickerViewController, UIDocumentPi
}
if let navVC = self.navVC {
packageInstaller.promptForLanguages(inNavigationVC: navVC) {
// do nothing; the Settings menu dismissal will take care of displaying the keyboard
}
packageInstaller.promptForLanguages(inNavigationVC: navVC)
}
}
}

View file

@ -21,13 +21,9 @@ class KeyboardViewController: InputViewController {
// is enabled. They seem to get blocked otherwise, except in the Simulator.
SentryManager.start(sendingEnabled: true)
}
_ = log
_ = KeymanEngine.log
#if DEBUG
KeymanEngine.log.outputLevel = .debug
KeymanEngine.log.logAppDetails()
#else
KeymanEngine.log.outputLevel = .warning
#endif
Manager.applicationGroupIdentifier = "group.KM4I"
let bundle = Bundle(for: KeyboardViewController.self)

16
linux/.gitignore vendored
View file

@ -1,28 +1,35 @@
# linux
.deps/
autom4te.cache
*.o
*.orig
Makefile.in
configure
aclocal.m4
config.guess
config.sub
config.h
config.h.in
config.h.in~
config.log
config.rpath
config.status
compile
depcomp
install-sh
libtool
ltmain.sh
missing
test-driver
ylwrap
INSTALL
Makefile
ABOUT-NLS
ltoptions.m4
ltsugar.m4
ltversion.m4
lt~obsolete.m4
libtool.m4
stamp-h1
__pycache__
*.egg-info
build-*
@ -83,3 +90,10 @@ keyman-config/keyman_config/version.py
keyman-config/locale/*/LC_MESSAGES/
test.sh
debianpackage/
*.deb
*.build
*.buildinfo
*.changes
*.tar.xz
*.tar.gz
*.dsc

View file

@ -1,13 +1,21 @@
# LICENSE
The [kmflcomp](./kmflcomp), [libkmfl](./libkmfl), and [keyman_config](./keyman_config) projects
are covered by the [MIT license](./libkmfl/COPYING)
are covered by the [MIT license](./libkmfl/COPYING).
The [ibus-kmfl](./ibus-kmfl) project is licensed under GNU General Public License as published
by the Free Software Foundation; either [version 2 of the License](./ibus-kmfl/COPYING), or
(at your option) any later version.
The [ibus-kmfl](./ibus-kmfl) and [ibus-keyman](./ibus-keyman) projects are licensed under GNU
General Public License as published by the Free Software Foundation; either
[version 2 of the License](./ibus-kmfl/COPYING), or (at your option) any later version.
Two files in ibus-kmfl, [kmflutil.c](./ibus-kmfl/src/kmflutil.c) and
[kmflutil.h](./ibus-kmfl/src/kmflutil.h) are dual licensed by the MIT license and the
GNU General Public License which is described above. The MIT license alone may be chosen for
them for their use in other projects.
[kmflutil.h](./ibus-kmfl/src/kmflutil.h), as well as four files in ibus-keyman,
[keymanutil.c](./ibus-keyman/src/keymanutil.c), [keymanutil.h](./ibus-keyman/src/keymanutil.h),
[kmpdetails.c](./ibus-keyman/src/kmpdetails.c) and [kmpdetails.h](./ibus-keyman/src/kmpdetails.h),
are dual licensed by the MIT license and the GNU General Public License which is described above.
The MIT license alone may be chosen for them for their use in other projects.
Two files in ibus-keyman, [keyman-service.c](./ibus-keyman/src/keyman-service.c) and
[keyman-service.h](./ibus-keyman/src/keyman-service.h) are licensed under GNU
General Public License version 3 as published by the Free Software Foundation; either
[version 3 of the License](https://spdx.org/licenses/GPL-3.0-or-later.html), or (at your option)
any later version.

4
linux/ibus-keyman/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
po/POTFILES
src/keyman.xml
src/keyman.xml.in
src/ibus-engine-keyman

View file

@ -1 +1,3 @@
Keyman engine for IBus.
Keyman engine for IBus
This readme is required by autoconf. Otherwise see README.md.

View file

@ -0,0 +1,22 @@
# Keyman engine for IBus
## Requirements
You need autoconf, autopoint, gettext, automake and libtool to generate the build system.
Run `./autogen.sh` to run them.
## Building
```bash
./autogen.sh
./configure
make
sudo make install
```
For a debug build:
```bash
./configure CPPFLAGS=-DG_MESSAGES_DEBUG CFLAGS="-g -O0" CXXFLAGS="-g -O0"
```

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="inputmethod">
<id>com.keyman.ibus_keyman</id>
<metadata_license>FSFAP</metadata_license>
<project_license>MIT</project_license>
<metadata_license>GPL-2+</metadata_license>
<project_license>GPL-2+</project_license>
<project_group>Keyman</project_group>
<developer_name>SIL International</developer_name>
<icon type="remote" width="96" height="96">https://keyman.com/cdn/dev/img/keyman-logo.png</icon>

View file

@ -4,6 +4,7 @@ Priority: optional
Maintainer: Debian Input Method Team <debian-input-method@lists.debian.org>
Uploaders:
Keyman team <support@keyman.com>,
Eberhard Beilharz <eb1@sil.org>,
Build-Depends:
debhelper (>= 11),
libgtk-3-dev,
@ -11,7 +12,7 @@ Build-Depends:
libjson-glib-dev (>= 1.4.0),
libkmnkbp-dev (>=11.0.100),
pkg-config,
Standards-Version: 4.5.0
Standards-Version: 4.5.1
Vcs-Git: https://github.com/keymanapp/keyman.git
Vcs-Browser: https://github.com/keymanapp/keyman/tree/master/linux/ibus-keyman
Homepage: https://www.keyman.com

View file

@ -18,10 +18,6 @@ Files: src/keyman-service.c
Copyright: 2018-2021 SIL International
License: GPL-3+
Files: debian/*
Copyright: 2004-2021 SIL International
License: GPL-2+
License: GPL-2+
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View file

@ -0,0 +1,51 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=jcZL
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="inputmethod">
<id>com.keyman.ibus_kmfl</id>
<metadata_license>FSFAP</metadata_license>
<project_license>MIT</project_license>
<metadata_license>GPL-2+</metadata_license>
<project_license>GPL-2+</project_license>
<project_group>Keyman</project_group>
<developer_name>SIL International</developer_name>
<icon type="remote" width="96" height="96">https://keyman.com/cdn/dev/img/keyman-logo.png</icon>

View file

@ -4,6 +4,7 @@ Priority: optional
Maintainer: Debian Input Method Team <debian-input-method@lists.debian.org>
Uploaders:
Keyman team <support@keyman.com>,
Eberhard Beilharz <eb1@sil.org>,
Build-Depends:
debhelper (>= 11),
libibus-1.0-dev (>= 1.2),
@ -12,7 +13,7 @@ Build-Depends:
libx11-dev,
pkg-config,
x11proto-core-dev,
Standards-Version: 4.5.0
Standards-Version: 4.5.1
Vcs-Git: https://github.com/keymanapp/keyman.git
Vcs-Browser: https://github.com/keymanapp/keyman/tree/master/linux/ibus-kmfl
Homepage: https://www.keyman.com

View file

@ -0,0 +1,51 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=jcZL
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -2,7 +2,7 @@
<!-- Copyright (c) 2019 Daniel Glassey <wdg@debian.org> -->
<component type="desktop-application">
<id>com.keyman.config</id>
<metadata_license>FSFAP</metadata_license>
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<project_group>Keyman</project_group>
<developer_name>SIL International</developer_name>

View file

@ -4,6 +4,7 @@ Priority: optional
Maintainer: Debian Input Method Team <debian-input-method@lists.debian.org>
Uploaders:
Keyman team <support@keyman.com>,
Eberhard Beilharz <eb1@sil.org>,
Build-Depends:
bash-completion,
debhelper (>= 11),
@ -23,13 +24,12 @@ Build-Depends:
python3-requests,
python3-requests-cache,
python3-setuptools,
Standards-Version: 4.5.0
Standards-Version: 4.5.1
Vcs-Git: https://github.com/keymanapp/keyman.git
Vcs-Browser: https://github.com/keymanapp/keyman/tree/master/linux/keyman-config
Homepage: https://www.keyman.com
Package: keyman
Section: utils
Architecture: all
Depends:
python3-keyman-config,

View file

@ -1,5 +1,7 @@
#!/usr/bin/make -f
include /usr/share/dpkg/pkg-info.mk
#export DH_VERBOSE=1
export PYBUILD_NAME=keyman-config
export PYBUILD_INSTALL_ARGS=--install-scripts=/usr/share/keyman-config/
@ -9,6 +11,7 @@ export PYBUILD_INSTALL_ARGS=--install-scripts=/usr/share/keyman-config/
override_dh_auto_build:
make man
sed -i -e "s/^__pkgversion__ = \"\"/__pkgversion__ = \"$(DEB_VERSION)\"/g" keyman_config/version.py
make compile-po
dh_auto_build $@

View file

@ -0,0 +1,51 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=jcZL
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -9,6 +9,8 @@ from .version import __versionwithtag__
from .version import __majorversion__
from .version import __releaseversion__
from .version import __tier__
from .version import __pkgversion__
from .version import __environment__
def _(txt):
@ -36,6 +38,8 @@ KeymanDownloadsUrl = 'https://downloads.keyman.com'
if 'unittest' in sys.modules.keys():
print('Not reporting to Sentry')
elif os.environ.get('KEYMAN_NOSENTRY'):
print('Not reporting to Sentry because KEYMAN_NOSENTRY environment variable set')
else:
try:
# Try new sentry-sdk first
@ -46,13 +50,15 @@ else:
SentryUrl = "https://1d0edbf2d0dc411b87119b6e92e2c357@sentry.keyman.com/12"
sentry_sdk.init(
dsn=SentryUrl,
environment=__tier__,
environment=__environment__,
release=__version__,
)
with configure_scope() as scope:
scope.set_tag("app", os.path.basename(sys.argv[0]))
scope.set_tag("pkgversion", __pkgversion__)
scope.set_tag("platform", platform.platform())
scope.set_tag("system", platform.system())
scope.set_tag("tier", __tier__)
except ImportError:
try:
# sentry-sdk is not available, so use older raven
@ -61,11 +67,13 @@ else:
HaveSentryNewSdk = False
SentryUrl = "https://1d0edbf2d0dc411b87119b6e92e2c357:e6d5a81ee6944fc79bd9f0cbb1f2c2a4@sentry.keyman.com/12"
client = Client(SentryUrl, environment=__tier__, release=__version__)
client = Client(SentryUrl, environment=__environment__, release=__version__)
client.tags_context({
'app': os.path.basename(sys.argv[0]),
'pkgversion': __pkgversion__,
'platform': platform.platform(),
'system': platform.system(),
'tier': __tier__,
})
except ImportError:
# even raven is not available. This is the case on Ubuntu 16.04. Just ignore.

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