diff --git a/HISTORY.md b/HISTORY.md index 78352f95e5..1815302cac 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,98 @@ # Keyman Version History +## 18.0.180 alpha 2025-01-29 + +* chore(common): Update crowdin strings for Portuguese (#13056) +* chore(common): Update crowdin strings for Spanish (Latin America) (#13044) +* chore(linux): Update debian changelog (#13063) +* chore(linux): improve compatibility with Gentoo Linux (#13070) + +## 18.0.179 alpha 2025-01-28 + +* fix(android): improve resource-update tool handling of host Activity's closure (#13004) +* fix(windows): bool return value of KeymanIsTextSelected (#12841) +* chore(mac): add breadcrumbs for Sentry (#12939) +* fix(mac): show and hide OSK consistently for all keyboards (#12988) +* fix(linux): remove unnecessary files from source tarball (#13053) +* fix(windows): handle keyboard package not downloaded (#12948) + +## 18.0.178 alpha 2025-01-27 + +* fix(web): fix handling of data: url fonts, fonts with single quotes in filename (#13032) +* fix(android): disable early keyboard-loading when not feasible (#13001) +* fix(web): add null guard for focusing next element via keystroke (#13013) +* chore: adjust one bookmark link in windows doc (#13050) +* chore: adjust bookmark links in developer docs (#13049) + +## 18.0.177 alpha 2025-01-24 + +* fix(android): restores DOMRect polyfill for old Chrome support (#13012) +* fix(developer): produce correct index parameter value for context in kmw compiler (#13003) +* chore: add platform to what's new titles (#12989) +* chore: add platforms to welcome titles (#12990) +* chore: add platforms to version history titles (#13016) +* chore: add platforms to troubleshooting titles and windows to help homepage (#13019) +* chore: add platforms to system requirements and download-and-install docs (#13024) +* chore: add mac platform to titles (#13027) +* chore: add linux platform to titles (#13028) +* chore: add ios platform to titles (#13031) +* fix(developer): escape font facename in touch layout editor (#13020) +* fix: use correct path for resources/build/version (#13029) +* fix(developer): don't list .js keyboards for multi-keyboard packages in kmc-keyboard-info (#13034) +* fix(web): do not simplify nul-prefixed contexts when rule-matching (#12998) +* feat(android): add breadcrumbing during package install about package file, source (#13021) +* fix(linux): correct permissions and paths in Linux docker image (#13008) + +## 18.0.176 alpha 2025-01-23 + +* chore(developer): stub out icon support in kmc-generate (#12971) +* chore(developer): remove '--rename' option from kmc-copy (#12973) +* feat(developer): add 'verbose' and 'debug' log levels to kmc (#12976) +* chore(developer): validate output folder name in kmc-copy (#12979) +* chore(developer): report on filename collisions in kmc-copy (#12982) +* feat(developer): support copying disorganized projects in kmc-copy (#12984) +* chore(linux): docker build: update package index before installing packages in Dockerfiles (#12992) +* feat(web): allow main manual-test page to load from KMP (#12977) +* fix(android): Remove access to MEDIA storage permissions (#12999) + +## 18.0.175 alpha 2025-01-22 + +* feat(windows): Fixes check now to use SM and LastUpdateCheckTime key on first run. (#12922) +* chore(developer): validate keyboard and model IDs in kmc-generate (#12958) +* fix(developer): use correct terms in kmc-generate lexical model help (#12960) +* fix(developer): use 1.0.0 as default for ldml keyboards in kmc-generate (#12962) +* chore: adds h1 tags (#12967) +* fix(web): prevent warning that occurs when multitapping longpressable modifier keys (#12964) +* chore(developer): add test for basic transform to kmc-generate (#12966) +* fix(android/engine): Revert how keyboard picker menu launches (#12986) + +## 18.0.174 alpha 2025-01-21 + +* chore(developer): validate targets parameter in kmc-generate (#12955) +* fix(developer): support `&displayMap` font in web debugger (#12929) + +## 18.0.173 alpha 2025-01-20 + +* feat(developer): rewrite font data in .kvk from package metadata (#12949) +* docs(ios): document gestures within iOS app help (#12900) +* fix(developer): include developer-utils deps in server build (#12951) + +## 18.0.172 alpha 2025-01-19 + +* chore(common): Add 17.0.333 to version history (#12926) + +## 18.0.171 alpha 2025-01-18 + +* fix(common/web): add StrsItem.isEqual() method (#12868) +* fix(common/web): handle invalid order and tertiary arguments to ElementString.fromString() (#12882) + +## 18.0.170 alpha 2025-01-17 + +* chore(linux): update copyright year (#12918) +* fix(web): patch unit test with execution dependent on autocorrect state (#12940) +* test(common/web): unit tests for element-string (#12811) +* chore(linux): Update debian changelog (#12917) + ## 18.0.169 alpha 2025-01-17 * chore(windows): remove `postinstall` state from mermaid diagram (#12923) @@ -953,7 +1046,7 @@ ## 18.0.41 alpha 2024-05-22 * fix(developer): handle `KM_CORE_IT_INVALIDATE_CONTEXT` in debugger (#11488) -* chore(linux): Trigger GHA packaging for stable builds :cherries: (#11495) +* chore(linux): Trigger GHA packaging for stable builds (#11495) * chore(android,mac,windows): Update crowdin strings for DE (#11497) * feat(web): custom infrastructure for @web/test-runner use (#11403) * chore(web): conversion of lm-worker browser-test for @web/test-runner use (#11404) @@ -1149,6 +1242,31 @@ * chore(common): move to 18.0 alpha (#10713) * chore: move to 18.0 alpha +## 17.0.334 stable 2025-01-27 + +* chore(linux): update copyright year (#12919) +* chore(linux): Update debian changelog (#12916) +* fix(android): Remove access to MEDIA storage permissions (#13005) +* fix(android): restores DOMRect polyfill for old Chrome support (#13014) +* fix(web): add null guard for focusing next element via keystroke (#13035) +* fix: use correct path for resources/build/version (#13030) +* fix(web): fix handling of fonts with single quotes in filename (#13041) +* fix(android): disable early keyboard-loading when not feasible (#13039) +* fix(web): do not simplify nul-prefixed contexts when rule-matching (#13040) +* feat(android): add breadcrumbing during package install about package file, source (#13038) +* fix(web): prevent multitap warning that occurs multitapping longpressable modifier keys (#12974) + +## 17.0.333 stable 2025-01-16 + +* fix(core): permanently disable logging (#12674) +* fix(linux): pushing of updated changelog branch (#12819) +* fix(linux): work around Lintian errors (#12817) +* fix(core): implement ldml_processor::get_key_list() (#12816) +* chore(linux): Update debian changelog (#12022) +* fix(android): use main looper to dispatch key events when OSK is hidden (#12875) +* chore: use GitHub PR titles when writing HISTORY.md (#12908) +* fix(developer): filter incorrect fonts out of .keyboard_info (#12913) + ## 17.0.332 stable 2024-11-06 * fix(developer): create Server config directory before options save (#12609) diff --git a/VERSION.md b/VERSION.md index df7920cb67..3428a04a07 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.170 \ No newline at end of file +18.0.181 \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml index 3ba7eaf9fc..bdb1d18a43 100644 --- a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml +++ b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml @@ -7,10 +7,6 @@ - - - - diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java index 888d430b0b..431d97e353 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java @@ -35,8 +35,8 @@ public class CheckPermissions { checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE); } else { // API 33+ - //https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions - // Manifest.permission.READ_MEDIA_AUDIO doesn't seem to be needed + // We had to remove these MEDIA permissions from AndroidManifest.xml so these will end up failing + // https://support.google.com/googleplay/android-developer/answer/14115180?hl=en permissionsOK = permissionsOK && checkPermission(activity, Manifest.permission.READ_MEDIA_IMAGES); permissionsOK = permissionsOK && checkPermission(activity, Manifest.permission.READ_MEDIA_VIDEO); } diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java index bb770ef5b2..4410761e6d 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java @@ -23,6 +23,7 @@ import androidx.appcompat.app.AppCompatActivity; import com.keyman.engine.BaseActivity; import com.keyman.engine.KMManager; +import com.keyman.engine.util.KMLog; import com.keyman.engine.util.KMPLink; import com.keyman.engine.util.KMString; import com.keyman.engine.util.WebViewUtils; @@ -105,6 +106,7 @@ public class KMPBrowserActivity extends BaseActivity { // Create intent with keyboard download link for KMAPro main activity to handle Intent intent = new Intent(context, MainActivity.class); intent.setData(downloadURI); + KMLog.LogBreadcrumb(TAG, "Successful keyboard search now triggering package install", true); startActivity(intent); // Finish activity diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 2ae689364c..908d2f4538 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -262,6 +262,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene if (loadingIntentUri != null) { String scheme = loadingIntentUri.getScheme().toLowerCase(); + final String link = loadingIntentUri.toString(); switch (scheme) { // content:// Android DownloadManager // file:// Chrome downloads and Filebrowsers @@ -269,6 +270,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene case "file": requestPermissionIntentUri = loadingIntentUri; if (CheckPermissions.isPermissionOK(this)) { + KMLog.LogBreadcrumb(TAG, "Installing local KMP: " + link, true); useLocalKMP(context, loadingIntentUri); } else { CheckPermissions.requestPermission(this, context); @@ -277,14 +279,16 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene case "http" : case "https" : // Might need to modify link like KMPBrowserActivity - String link = loadingIntentUri.toString(); if (KMPLink.isKeymanInstallLink(link)) { loadingIntentUri = KMPLink.getKeyboardDownloadLink(link); } + // Is usually triggered by KMPBrowserActivity + KMLog.LogBreadcrumb(TAG, "Installing via HTTPS intent: " + link, true); downloadKMP(loadingIntentUri, KmpInstallMode.Full); break; case "keyman" : // TODO: Only accept download links from Keyman browser activities when universal links work + KMLog.LogBreadcrumb(TAG, "Installing via keyman-link intent: " + link, true); if (KMPLink.isKeymanDownloadLink(loadingIntentUri.toString())) { // Convert opaque URI to hierarchical URI so the query parameters can be parsed @@ -296,7 +300,6 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene loadingIntentUri = Uri.parse(builder.build().toString()); downloadKMP(loadingIntentUri, KmpInstallMode.Full); } else if (KMPLink.isLegacyKeymanDownloadLink(loadingIntentUri.toString())) { - link = loadingIntentUri.toString(); loadingIntentUri = KMPLink.getLegacyKeyboardDownloadLink(link); downloadKMP(loadingIntentUri, KmpInstallMode.Full); } else { diff --git a/android/KMAPro/kMAPro/src/main/res/values-b+es+419/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-b+es+419/strings.xml index bc75863cb5..856c14c922 100644 --- a/android/KMAPro/kMAPro/src/main/res/values-b+es+419/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values-b+es+419/strings.xml @@ -53,6 +53,7 @@ Para instalar los paquetes del teclado, permite que Keyman acceda al almacenamiento externo. Se ha denegado la solicitud de permiso de almacenamiento. La instalación del paquete de teclado podría fallar + Falló la solicitud de permiso de almacenamiento. Seleccione Configuración de Keyman - Instalar desde archivo local Configuración @@ -67,6 +68,8 @@ Ajustar la altura del teclado + Adjust longpress delay + Leyenda de barra espaciadora Teclado @@ -86,11 +89,11 @@ No mostrar leyenda en la barra espaciadora Vibrar al escribir - + Mostrar siempre el banner - + A implementar - + Cuando está desactivado, solo se muestra cuando el texto predictivo está habilitado Permitir el envío de informes de fallas a través de la red @@ -122,6 +125,14 @@ Rota el dispositivo para ajustar el modo vertical y horizontal Restaurar valores predeterminados + + Delay Time: %1$.1f seconds + + Delay time longer + + Delay time shorter + + Longpress delay time slider Buscar o escribir una dirección web diff --git a/android/KMAPro/kMAPro/src/main/res/values-pt-rPT/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-pt-rPT/strings.xml index 02f1d9291d..bef479b7ca 100644 --- a/android/KMAPro/kMAPro/src/main/res/values-pt-rPT/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values-pt-rPT/strings.xml @@ -68,6 +68,8 @@ Ajustar altura do teclado + Ajustar atraso do toque longo + Barra de espaço Teclado @@ -87,11 +89,11 @@ Não mostrar legenda na barra de espaço Vibrar ao tocar - + Sempre mostrar banner - + A ser implementado - + Quando desativado, apenas mostrado quando o texto preditivo é ativado Permitir o envio de relatórios de erro pela rede @@ -123,6 +125,14 @@ Gire o dispositivo para ajustar o retrato e a paisagem Restaurar padrões + + Tempo de atraso: %1$.1f segundos + + Tempo de atraso maior + + Tempo de atraso menor + + Atraso do tempo com toque longo Pesquise ou digite um endereço diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 5a25367223..0bd199b4d6 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -37,7 +37,20 @@ function init() { keyman.beepKeyboard = beepKeyboard; // Readies the keyboard stub for instant loading during the init process. - KeymanWeb.registerStub(JSON.parse(jsInterface.initialKeyboard())); + try { + let starterStubStr = jsInterface.initialKeyboard(); + if(starterStubStr) { + KeymanWeb.registerStub(JSON.parse(starterStubStr)); + } + } catch (err) { + // If a Java exception triggers during this time... okay, we can't + // load a keyboard during startup. Rely on older loading techniques. + // + // Note: no information about the Java-side error is available here. + if(err.message.indexOf("Java exception") < 0) { + throw err; + } + } keyman.init({ 'embeddingApp':device, diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java b/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java index 4783e401b8..15c0e35827 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java @@ -64,7 +64,7 @@ public class DisplayLanguages { new DisplayLanguageType("ann", "Obolo"), new DisplayLanguageType("pl-PL", "Polski (Polish)"), new DisplayLanguageType("el", "Polytonic Greek"), - new DisplayLanguageType("pt-PT", "Português do Portugal"), + new DisplayLanguageType("pt-PT", "Português de Portugal"), new DisplayLanguageType("ff-ZA", "Pulaar-Fulfulde"), // or Fulah new DisplayLanguageType("ru-RU", "Pyccĸий (Russian)"), new DisplayLanguageType("shu-latn", "Shuwa (Latin)"), diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java index d403a29235..ecd55c2bc9 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java @@ -75,7 +75,10 @@ public class KMKeyboardJSHandler { Keyboard kbd = Keyboard.getDefaultKeyboard(context); int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, -1); if (index >= 0) { - kbd = KMManager.getKeyboardInfo(this.context, index); + // It is possible for this method to return `null` - especially + // if the KeyboardController instance hasn't yet been initialized. + Keyboard lastKbd = KMManager.getKeyboardInfo(this.context, index); + kbd = lastKbd != null ? lastKbd : kbd; } return kbd.toStub(context); } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index cc12b36dca..0e4c261232 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -921,6 +921,9 @@ public final class KMManager { copyAsset(context, KMFilename_KmwCss, "", true); copyAsset(context, KMFilename_KmwGlobeHintCss, "", true); copyAsset(context, KMFilename_Osk_Ttf_Font, "", true); + + // Needed until our minimum version of Chrome is 61.0+. + copyAsset(context, KMFilename_JSPolyfill2, "", true); // Copy default keyboard font copyAsset(context, KMDefault_KeyboardFont, "", true); @@ -2267,6 +2270,7 @@ public final class KMManager { if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) { i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); } i.putExtra(KMKey_DisplayKeyboardSwitcher, kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java b/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java index 18b29ceb76..9f02480bf2 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java @@ -520,10 +520,6 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl @Override public void onPackageInstalled(List> keyboardsInstalled) { - if (!isContextAvailable()) { - KMLog.LogError(TAG, "onPackageInstalled where context is null."); - } - if (! openUpdates.isEmpty() && keyboardsInstalled != null) { for (Map k : keyboardsInstalled) { String _langid = k.get(KMManager.KMKey_LanguageID); @@ -537,10 +533,6 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl @Override public void onLexicalModelInstalled(List> lexicalModelsInstalled) { - if (!isContextAvailable()) { - KMLog.LogError(TAG, "onLexicalModelInstalled where context is null."); - } - if (! openUpdates.isEmpty()) { for(Map _lm:lexicalModelsInstalled) { String _langid = _lm.get(KMManager.KMKey_LanguageID); @@ -566,8 +558,9 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl // TODO: make it smoother. Documented as #11097. KMManager.clearKeyboardCache(); + Context appContext = currentContext.getApplicationContext(); if (failedUpdateCount > 0) { - BaseActivity.makeToast(currentContext, R.string.update_failed, Toast.LENGTH_SHORT); + BaseActivity.makeToast(appContext, R.string.update_failed, Toast.LENGTH_SHORT); lastUpdateCheck = Calendar.getInstance(); updateFailed = true; checkingUpdates = false; diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/packages/PackageProcessor.java b/android/KMEA/app/src/main/java/com/keyman/engine/packages/PackageProcessor.java index 1e3bff4a76..65252e63bb 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/packages/PackageProcessor.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/packages/PackageProcessor.java @@ -447,6 +447,9 @@ public class PackageProcessor { public String getPackageTarget(File kmpPath) { try { File tempPath = unzipKMP(kmpPath); + String kmpFilename = kmpPath.getName(); + + KMLog.LogBreadcrumb(TAG, "Determining package type for " + kmpFilename, false); String target = getPackageTarget(loadPackageInfo(tempPath)); FileUtils.deleteDirectory(tempPath); return target; diff --git a/android/KMEA/app/src/main/res/values-b+es+419/strings.xml b/android/KMEA/app/src/main/res/values-b+es+419/strings.xml index 8cefeeeefd..c0bd0412ed 100644 --- a/android/KMEA/app/src/main/res/values-b+es+419/strings.xml +++ b/android/KMEA/app/src/main/res/values-b+es+419/strings.xml @@ -107,7 +107,7 @@ La descarga del diccionario inició en segundo plano El diccionario seleccionado ya se está descargando; inténtalo de nuevo en un momento. - + La descarga del diccionario ha finalizado. Error al descargar @@ -119,7 +119,7 @@ "¡Todos los recursos están actualizados!" ¡Uno o más recursos no se han actualizado! - + ¡Recursos actualizados correctamente! Versión del diccionario @@ -133,10 +133,15 @@ Teclado %1$s instalado Teclado borrado - + Habilitar correcciones - + Habilitar predicciones + + Desactivar sugerencias + Sólo predicciones + Predicciones con correcciones + Predicciones con autocorrecciones Diccionario diff --git a/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml b/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml index f58f48dd2d..ceb0b82e0e 100644 --- a/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml +++ b/android/KMEA/app/src/main/res/values-pt-rPT/strings.xml @@ -133,10 +133,15 @@ Teclado %1$s instalado Teclado eliminado - + Habilitar correções - + Habilitar previsões + + Disable suggestions + Predictions only + Predictions with corrections + Predictions with auto-corrections Dicionário diff --git a/android/docs/engine/whatsnew.md b/android/docs/engine/whatsnew.md index e8c2a3d694..af3f8932d6 100644 --- a/android/docs/engine/whatsnew.md +++ b/android/docs/engine/whatsnew.md @@ -1,5 +1,5 @@ --- -title: What's New +title: What's New in Keyman Engine 18.0 for Android --- ## Keyman Engine for Android Breaking Changes: ## diff --git a/android/docs/help/about/history.md b/android/docs/help/about/history.md index 3a0b9af3a6..2774029615 100644 --- a/android/docs/help/about/history.md +++ b/android/docs/help/about/history.md @@ -1,5 +1,5 @@ --- -title: Version History +title: Version History of Keyman for Android --- A frequent updated changelog for Keyman is kept at [help.keyman.com/version-history](https://help.keyman.com/version-history/) on the Keyman Help website. diff --git a/android/docs/help/about/welcome.md b/android/docs/help/about/welcome.md index 9c2ff5652a..f21aabeeba 100644 --- a/android/docs/help/about/welcome.md +++ b/android/docs/help/about/welcome.md @@ -1,5 +1,5 @@ --- -title: Welcome to Keyman +title: Welcome to Keyman for Android --- Thank you for installing Keyman for Android. Whether you're a new or returning user, we believe you will appreciate the significant enhancements in this latest version of Keyman for Android. diff --git a/android/docs/help/about/whatsnew.md b/android/docs/help/about/whatsnew.md index 500d0a9593..0f0e891e42 100644 --- a/android/docs/help/about/whatsnew.md +++ b/android/docs/help/about/whatsnew.md @@ -1,5 +1,5 @@ --- -title: What's New +title: What's New in Keyman 18.0 for Android --- Here are some of the new features we have added to Keyman 18.0 for Android: diff --git a/android/docs/help/start/installing-keyboards.md b/android/docs/help/start/installing-keyboards.md index 8e4df5f880..248bc74ebe 100644 --- a/android/docs/help/start/installing-keyboards.md +++ b/android/docs/help/start/installing-keyboards.md @@ -1,5 +1,5 @@ --- -title: How To - Download and Install a Keyman Keyboard +title: How To - Download and Install a Keyman Keyboard on Android --- ## Adding New Keyboards diff --git a/android/docs/help/troubleshooting/grant-storage-permission.md b/android/docs/help/troubleshooting/grant-storage-permission.md index 1b8554d7f0..7b47cdf4ea 100644 --- a/android/docs/help/troubleshooting/grant-storage-permission.md +++ b/android/docs/help/troubleshooting/grant-storage-permission.md @@ -19,13 +19,7 @@ permission to access storage / "file and media". The screenshot below is from An ### Android 13.0+ Devices -Step 1) -Go to Android Settings. +Due to Google Play Store's "Photo and Video Permissions policy", +Keyman for Android must use a system picker to browse for .kmp files from within the Keyman app. -Step 2) -Depending on your device, click "Apps" and grant Keyman permission to the following: - * Photos and videos --> Always allow all - -The screenshot below is from Android 14.0. - -![](../android_images/keyman-storage-permission-34b.png) +Follow the steps in [Installing Custom Keyboards/Dictionaries](../basic/installing-custom-packages). diff --git a/android/docs/help/troubleshooting/index.md b/android/docs/help/troubleshooting/index.md index 474f4f8119..eb62977439 100644 --- a/android/docs/help/troubleshooting/index.md +++ b/android/docs/help/troubleshooting/index.md @@ -1,5 +1,5 @@ --- -title: Troubleshooting +title: Keyman for Android Troubleshooting --- * [How To - Getting Keyboard Help](keyboard-help) diff --git a/common/web/types/src/kmx/kmx-plus/element-string.ts b/common/web/types/src/kmx/kmx-plus/element-string.ts index b389107cce..d14c4a9011 100644 --- a/common/web/types/src/kmx/kmx-plus/element-string.ts +++ b/common/web/types/src/kmx/kmx-plus/element-string.ts @@ -19,8 +19,8 @@ export class ElemElement { order: number; // -128 to +127; used only by reorder element values tertiary: number; // -128 to +127; used only by reorder element values flags: ElemElementFlags; - isEqual(a: ElemElement) { - return a.value === this.value && + isEqual(a: ElemElement): boolean { + return a.value.isEqual(this.value) && a.order === this.order && a.tertiary === this.tertiary && a.flags === this.flags; @@ -104,8 +104,8 @@ export class ElementString extends Array { typeFlag |= constants.elem_flags_type_str; } } - elem.order = orders.length ? parseInt(orders[i], 10) : 0; - elem.tertiary = tertiaries.length ? parseInt(tertiaries[i], 10) : 0; + elem.order = orders.length ? this.parseIntOrZero(orders[i]) : 0; + elem.tertiary = tertiaries.length ? this.parseIntOrZero(tertiaries[i]) : 0; elem.flags = ElemElementFlags.none | (ElemElementFlags.type & typeFlag) | (tertiary_bases?.[i] == '1' /* TODO-LDML: or 'true'? */ ? ElemElementFlags.tertiary_base : 0) | @@ -125,5 +125,9 @@ export class ElementString extends Array { } return true; } + private static parseIntOrZero(str: string): number { + const num = parseInt(str, 10); + return !Number.isNaN(num) ? num : 0; + } } ; diff --git a/common/web/types/src/kmx/kmx-plus/kmx-plus.ts b/common/web/types/src/kmx/kmx-plus/kmx-plus.ts index a88688f7ab..d0de2989d0 100644 --- a/common/web/types/src/kmx/kmx-plus/kmx-plus.ts +++ b/common/web/types/src/kmx/kmx-plus/kmx-plus.ts @@ -131,6 +131,10 @@ export class StrsItem { get isOneChar() { return this.char !== undefined; } + + isEqual(a: StrsItem): boolean { + return a.value === this.value && a.char === this.char; + } }; /** diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts new file mode 100644 index 0000000000..49a4c2c954 --- /dev/null +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -0,0 +1,498 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Dr Mark C. Sinclair on 2024-12-09 + * + * Test code for element-string.ts + */ + +import 'mocha'; +import { assert } from 'chai'; +import { constants } from '@keymanapp/ldml-keyboard-constants'; +import { ElemElementFlags, ElemElement, ElementString } from '../../../src/kmx/kmx-plus/element-string.js'; +import { StrsItem, UsetItem, Strs, StrsOptions, DependencySections, CharStrsItem, Uset } from '../../../src/kmx/kmx-plus/kmx-plus.js'; +import { UnicodeSet, UnicodeSetParser } from '../../../src/ldml-keyboard/unicodeset-parser-api.js'; +import { ElementParser, ElementSegment, ElementType } from '../../../src/ldml-keyboard/pattern-parser.js'; + +const GOTHIC_A = new StrsItem("𐌰", 0x10330); +const GOTHIC_B = new StrsItem("𐌱", 0x10331); +const GOTHIC_C = new StrsItem("𐌲", 0x10332); +const GOTHIC_D = new StrsItem("𐌳", 0x10333); +const HI_GOTHIC_A = new StrsItem('\ud800', 0xd800); +const LO_GOTHIC_A = new StrsItem('\udf30', 0xdf30); +const GOTHIC_PATTERN = "[𐌰-𐍊]"; +const GOTHIC_STRSITEM = new StrsItem(GOTHIC_PATTERN); +const GOTHIC_SET = new UnicodeSet(GOTHIC_PATTERN, [[0x10330,0x1034A]]); +const GOTHIC_USETITEM = new UsetItem(GOTHIC_SET, GOTHIC_STRSITEM); +const UGARITIC_PATTERN = "[𐎀-𐎟]"; +const UGARITIC_STRSITEM = new StrsItem(UGARITIC_PATTERN); +const UGARITIC_SET = new UnicodeSet(UGARITIC_PATTERN, [[0x10380,0x1039F]]); +const UGARITIC_USETITEM = new UsetItem(UGARITIC_SET, UGARITIC_STRSITEM); + +class TestUnicodeSetParser implements UnicodeSetParser { + parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => GOTHIC_SET; + sizeUnicodeSet = (pattern: string) : number => 1; +}; + +const origElementParserSegment = ElementParser.segment; +let sections: DependencySections = null; + +describe('Test of ElementString file', () => { + describe('Test of ElemElement', () => { + describe('Test of isEqual()', () => { + it('returns true when elems identical', () => { + const one = initElemElement(); + const two = initElemElement(); + assert.isTrue(one.isEqual(two)); + }); + it('returns true when elems are clones', () => { + const one = initElemElement(new StrsItem("𐌰", 0x10330)); + const two = initElemElement(new StrsItem("𐌰", 0x10330)); + assert.isTrue(one.isEqual(two)); + }); + it('returns false when value differs', () => { + const one = initElemElement(GOTHIC_A); + const two = initElemElement(GOTHIC_B); + assert.isFalse(one.isEqual(two)); + }); + it('returns false when order differs', () => { + const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0); + const two = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 1); + assert.isFalse(one.isEqual(two)); + }); + it('returns false when tertiary differs', () => { + const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 0); + const two = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 1); + assert.isFalse(one.isEqual(two)); + }); + it('returns false when flags differs', () => { + const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 0, ElemElementFlags.none); + const two = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 0, ElemElementFlags.type); + assert.isFalse(one.isEqual(two)); + }); + it('returns true even though uset differs', () => { + const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM); + const two = initElemElement(GOTHIC_A, UGARITIC_USETITEM); + assert.isTrue(one.isEqual(two)); + }); + }); + }); + describe('Test of ElementString class', () => { + beforeEach(() => { + sections = { + strs: new Strs(), + uset: new Uset(), + usetparser: new TestUnicodeSetParser(), + }; + ElementParser.segment = stubElementParserSegment_CodePoint; + }); + afterEach(() => { + ElementParser.segment = origElementParserSegment; + }); + describe('Test of fromStrings()', () => { + it('returns an empty ElementString if source is null', () => { + const es = ElementString.fromStrings({}, null); + assert.deepEqual(es, new ElementString()); + }); + it('can create an ElementString from a string array', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + const expected = [ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_C), + ]; + assert.deepEqual(actual, expected); + }); + it('can create an ElementString from a string', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings(sections, "𐌰𐌱𐌲"); + const expected = [ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_C), + ]; + assert.deepEqual(actual, expected); + }); + it('can apply order string', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + "1 2 3", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 1), + initElemElement(GOTHIC_B, undefined, 2), + initElemElement(GOTHIC_C, undefined, 3), + ]; + assert.deepEqual(actual, expected); + }); + it('can apply single order to all', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + "1", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 1), + initElemElement(GOTHIC_B, undefined, 1), + initElemElement(GOTHIC_C, undefined, 1), + ]; + assert.deepEqual(actual, expected); + }); + it('can handle order string that is too short', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + "1 2", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 1), + initElemElement(GOTHIC_B, undefined, 2), + initElemElement(GOTHIC_C, undefined, 0), + ]; + assert.deepEqual(actual, expected); + }); + it('can handle non-number in order string', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + "1 A 3", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 1), + initElemElement(GOTHIC_B, undefined, 0), + initElemElement(GOTHIC_C, undefined, 3), + ]; + assert.deepEqual(actual, expected); + }); + it('can apply tertiary string', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + null, + "1 2 3", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 0, 1), + initElemElement(GOTHIC_B, undefined, 0, 2), + initElemElement(GOTHIC_C, undefined, 0, 3), + ]; + assert.deepEqual(actual, expected); + }); + it('can apply single tertiary to all', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + null, + "1", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 0, 1), + initElemElement(GOTHIC_B, undefined, 0, 1), + initElemElement(GOTHIC_C, undefined, 0, 1), + ]; + assert.deepEqual(actual, expected); + }); + it('can handle tertiary string that is too short', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + null, + "1 2", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 0, 1), + initElemElement(GOTHIC_B, undefined, 0, 2), + initElemElement(GOTHIC_C, undefined, 0, 0), + ]; + assert.deepEqual(actual, expected); + }); + it('can handle non-number in tertiary string', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + null, + "1 A 3", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 0, 1), + initElemElement(GOTHIC_B, undefined, 0, 0), + initElemElement(GOTHIC_C, undefined, 0, 3), + ]; + assert.deepEqual(actual, expected); + }); + it('can apply tertiary_base string', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + null, + null, + "1 0 1", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.tertiary_base), + initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none), + initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.tertiary_base), + ]; + assert.deepEqual(actual, expected); + }); + it('can apply single tertiary_base to all', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + null, + null, + "1", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.tertiary_base), + initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.tertiary_base), + initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.tertiary_base), + ]; + assert.deepEqual(actual, expected); + }); + it('can handle tertiary_base string that is too short', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + null, + null, + "1 0", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.tertiary_base), + initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none), + initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.none), + ]; + assert.deepEqual(actual, expected); + }); + it('can apply prebase string', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + null, + null, + null, + "1 0 1", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.prebase), + initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none), + initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.prebase), + ]; + assert.deepEqual(actual, expected); + }); + it('can apply single prebase to all', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + null, + null, + null, + "1", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.prebase), + initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.prebase), + initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.prebase), + ]; + assert.deepEqual(actual, expected); + }); + it('can handle prebase string that is too short', () => { + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings( + sections, + "𐌰𐌱𐌲", + null, + null, + null, + "1 0", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.prebase), + initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none), + initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.none), + ]; + assert.deepEqual(actual, expected); + }); + it('can create an ElementString from a uset string', () => { + ElementParser.segment = stubElementParserSegment_Uset; + sections.strs.allocString = stubStrsAllocString_Str; + sections.uset.allocUset = stubUsetAllocUset; + const actual = ElementString.fromStrings(sections, "[𐌰-𐍊]"); + const expected = [ + initElemElement( + new StrsItem(''), + GOTHIC_USETITEM, + 0, + 0, + constants.elem_flags_type_uset, + ), + ]; + assert.deepEqual(actual, expected); + }); + it('returns null for an invalid unicode set size', () => { + ElementParser.segment = stubElementParserSegment_Uset; + sections.usetparser.sizeUnicodeSet = (pattern: string) : number => -1; + assert.isNull(ElementString.fromStrings(sections, "[𐌰-𐍊]")); + }); + it('returns null if it cannot parse the unicode set', () => { + ElementParser.segment = stubElementParserSegment_Uset; + sections.usetparser.parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => null; + assert.isNull(ElementString.fromStrings(sections, "[𐌰-𐍊]")); + }); + it('can handle quad strings', () => { + sections.strs.allocString = stubStrsAllocString_Char; + ElementParser.segment = stubElementParserSegment_Escaped; + const actual = ElementString.fromStrings(sections, "\\ud800\\udf30"); + const expected = [ + initElemElement(HI_GOTHIC_A), + initElemElement(LO_GOTHIC_A), + ]; + assert.deepEqual(actual, expected); + }); + it('can handle ElemElement of string type', () => { + sections.strs.allocString = stubStrsAllocString_Str; + const actual = ElementString.fromStrings(sections, ["𐌰𐌱𐌲",]); + const expected = [ + initElemElement( + new StrsItem("𐌰𐌱𐌲"), + undefined, + 0, + 0, + constants.elem_flags_type_str, + ), + ]; + assert.deepEqual(actual, expected); + }); + }); + describe('Test of isEqual()', () => { + it('returns true when ElementStrings are identical', () => { + const es = initElementString([ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_C), + ]); + assert.isTrue(es.isEqual(es)); + }); + it('returns true when ElementStrings are clones', () => { + const one = initElementString([ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_C), + ]); + const two = initElementString([ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_C), + ]); + assert.isTrue(one.isEqual(two)); + }); + it('returns false when ElementStrings are different lengths', () => { + const one = initElementString([ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_C), + ]); + const two = initElementString([ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + ]); + assert.isFalse(one.isEqual(two)); + }); + it('returns false when ElementStrings have different ElemElements', () => { + const one = initElementString([ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_C), + ]); + const two = initElementString([ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_D), + ]); + assert.isFalse(one.isEqual(two)); + }); + }); + describe('Test of parseIntOrZero()', () => { + it('returns a number for a valid string', () => { + const num = ElementString['parseIntOrZero']('1'); + assert.equal(num, 1); + }); + it('returns zero for an invalid string', () => { + const num = ElementString['parseIntOrZero']('A'); + assert.equal(num, 0); + }); + it('returns zero for undefined', () => { + const num = ElementString['parseIntOrZero'](undefined); + assert.equal(num, 0); + }); + it('returns zero for a null string', () => { + const num = ElementString['parseIntOrZero'](null); + assert.equal(num, 0); + }); + }); + }); +}); + +function initElemElement( + value: StrsItem = GOTHIC_A, + uset: UsetItem = undefined, + order: number = 0, + tertiary: number = 0, + flags: ElemElementFlags = ElemElementFlags.none, +): ElemElement { + const ee = new ElemElement(); + ee.value = value; + ee.uset = uset; + ee.order = order; + ee.tertiary = tertiary; + ee.flags = flags; + return ee; +}; + +function initElementString(elemElements: ElemElement[]): ElementString { + const es: ElementString = new ElementString(); + elemElements.forEach((ee) => {es.push(ee)}); + return es; +}; + +function stubStrsAllocString_Char(s?: string, opts?: StrsOptions, sections?: DependencySections): StrsItem { + return new CharStrsItem(s); +}; + +function stubStrsAllocString_Str(s?: string, opts?: StrsOptions, sections?: DependencySections): StrsItem { + return new StrsItem(s); +}; + +function stubElementParserSegment_CodePoint(str: string): ElementSegment[] { + return [...str].map(s => new ElementSegment(s, ElementType.codepoint)); +}; + +function stubElementParserSegment_Uset(str: string): ElementSegment[] { + return [new ElementSegment(str, ElementType.uset)]; +}; + +function stubElementParserSegment_Escaped(str: string): ElementSegment[] { + const strs = str.match(/\\u[0-9a-fA-F]{4}/g); + return strs.map((s) => new ElementSegment(s, ElementType.escaped)); +}; + +function stubUsetAllocUset(set: UnicodeSet, sections: DependencySections) : UsetItem { + return new UsetItem(set, new StrsItem(set.pattern)); +}; + diff --git a/common/windows/delphi/keyboards/kmxfileconsts.pas b/common/windows/delphi/keyboards/kmxfileconsts.pas index e8f0e0d740..8e1955aaa6 100644 --- a/common/windows/delphi/keyboards/kmxfileconsts.pas +++ b/common/windows/delphi/keyboards/kmxfileconsts.pas @@ -230,7 +230,8 @@ type ssPlatform = 31, ssBaseLayout = 32, ssLayer = 33, ssVKDictionary = 34, ssLayoutFile = 35, // I3438 // I3483 ssKeyboardVersion = 36, ssKMW_EmbedCSS = 37, ssTargets = 38, ssCasedKeys = 39, - ssBegin_NewContext = 40, ssBegin_PostKeystroke = 41, ssNewLayer = 42, ssOldLayer = 43); // I4140 // I4368 // I4504 + ssBegin_NewContext = 40, ssBegin_PostKeystroke = 41, ssNewLayer = 42, ssOldLayer = 43, // I4140 // I4368 // I4504 + ssDisplayMap = 44); const SystemStoreNames: array[TSystemStore] of WideString = ( @@ -245,7 +246,8 @@ const '', '', '', '', 'LAYOUTFILE', 'KEYBOARDVERSION', 'KMW_EMBEDCSS', 'TARGETS', //9.0 // I3483 // I4140 // I4368 // I4504 'CASEDKEYS', //14.0, - '', '', 'NEWLAYER', 'OLDLAYER'); //15.0 + '', '', 'NEWLAYER', 'OLDLAYER', //15.0 + 'DISPLAYMAP'); //17.0 implementation diff --git a/developer/docs/help/guides/lexical-models/advanced/unicode-breaker-extension.md b/developer/docs/help/guides/lexical-models/advanced/unicode-breaker-extension.md index a9e4d3776c..100f523acf 100644 --- a/developer/docs/help/guides/lexical-models/advanced/unicode-breaker-extension.md +++ b/developer/docs/help/guides/lexical-models/advanced/unicode-breaker-extension.md @@ -23,16 +23,16 @@ the specification itself. There are three ways - all of them optional - to extend and customize the word-breaking rules themselves: -* If you need to prevent splits in very specific scenarios and/or add splits in other specific scenarios, you may specify [context-based rules](#rules) to obtain the desired behavior. +* If you need to prevent splits in very specific scenarios and/or add splits in other specific scenarios, you may specify [context-based rules](#toc-custom-word-breaking-rules) to obtain the desired behavior. * If certain characters are not handled appropriately for their role in - your language, you may [map characters](#map) to different + your language, you may [map characters](#toc-character-property-remapping) to different word-breaking character classes - including custom ones. This will override the default property they are assigned by the default implementation, with the new property applying for all word-breaking rules. * If the default word-breaking classes from the specification are too general for certain aspects of your language, it is possible to - [define custom character classes](#define) for use in custom + [define custom character classes](#toc-defining-and-using-new-word-breaking-properties) for use in custom rules. ## A first example @@ -183,7 +183,7 @@ the potential boundary. ### Word-breaking property names The names used in each array must be defined in one of the following places: * https://unicode.org/reports/tr29/#Table_Word_Break_Property_Values -* `customProperties` - your [declaration of any custom property types](#define) +* `customProperties` - your [declaration of any custom property types](#toc-defining-and-using-new-word-breaking-properties) * One of the special property types `"Other"`, `"sot"`, or `"eot"`: * `Other`: a character without an associated word-breaking property value * `sot`: "start of text" - a marker indicating the beginning of the string being word-broken @@ -277,7 +277,7 @@ As noted at the top of the file: ### Redefining character properties -Of note from [our first example](#example): +Of note from [our first example](#toc-a-first-example): ```typescript /*** Character class overrides for specific characters ***/ @@ -324,7 +324,7 @@ words (and/or names) in some languages. Default word-breaking behavior will spl hyphenated words and names apart, but by changing the property of hyphens, it is possible to disable this behavior. -Noting [rule WB6](#WB6) and WB7, the `MidLetter` class is designed to prevent +Noting [rule WB6](#toc-custom-word-breaking-rules) and WB7, the `MidLetter` class is designed to prevent word-breaks from occurring when its characters lie directly between letters - hence the property name. Assigning hyphens to this class can provide the desired behavior. @@ -364,7 +364,7 @@ the second example above. (After all, `can'` could be the end of a quoted phrase in English - `'sure you can'` - in which case we might want the split to occur.) -Revisiting [an earlier example](#example) and simplifying a little bit: +Revisiting [an earlier example](#toc-redefining-character-properties) and simplifying a little bit: ```typescript /*** Definition of extra word-breaking rules ***/ @@ -440,7 +440,7 @@ included a couple of extra rules: } ``` -By replicating [WB6](#WB6) and WB7's structure and allowing `Hyphen` to match in the same +By replicating [WB6](#toc-custom-word-breaking-rules) and WB7's structure and allowing `Hyphen` to match in the same position as `MidLetter` in the original rules, we can prevent word-breaking splits after additional text has been typed after a `Hyphen`-property character. This does not _replace_ the behavior of WB6 and WB7 - it merely _extends_ it to include the new property. diff --git a/developer/docs/help/guides/lexical-models/advanced/word-breaker.md b/developer/docs/help/guides/lexical-models/advanced/word-breaker.md index f8f321327a..1b5881f823 100644 --- a/developer/docs/help/guides/lexical-models/advanced/word-breaker.md +++ b/developer/docs/help/guides/lexical-models/advanced/word-breaker.md @@ -13,9 +13,9 @@ However, in languages written in other scripts — especially East Asian scripts like Chinese, Japanese, Khmer, Lao, and Thai — there are no obvious break in between words. For these languages, there must be special rules for determining when words start and stop. This is what a _word breaker function_ is responsible for. It is a little bit of code that looks at some text to determine where the words are. You can customize the word breaker in three ways: -- If your language uses its writing system in an unconventional way (e.g., use spaces to separate words in Thai, Lao, Burmese, or Khmer), you can [override the script's default behaviour](#overrides) -- If the default word breaker creates **too many splits**, you can [choose which strings join words together](#join). -- If the default word breaker creates **not enough splits**, you must [create your own word breaker function](#custom). +- If your language uses its writing system in an unconventional way (e.g., use spaces to separate words in Thai, Lao, Burmese, or Khmer), you can [override the script's default behaviour](#toc-overriding-script-defaults) +- If the default word breaker creates **too many splits**, you can [choose which strings join words together](#toc-customize-joining-rules). +- If the default word breaker creates **not enough splits**, you must [create your own word breaker function](#toc-writing-a-custom-word-breaker-function). - Alternatively, you may choose to [customize and extend the wordbreaker's behavior](./unicode-breaker-extension) by adding extra rules and changing how it treats specific characters. ## Overriding script defaults diff --git a/developer/docs/help/reference/file-types/metadata.md b/developer/docs/help/reference/file-types/metadata.md index 1a0d631820..59d30375dc 100644 --- a/developer/docs/help/reference/file-types/metadata.md +++ b/developer/docs/help/reference/file-types/metadata.md @@ -38,25 +38,25 @@ The kmp.json file is a base object described as : `Object` - [`System`](#obj-system) object. + [`System`](#toc-the-system-object) object. `options` : `Object` - An [`Options`](#obj-options) object. + An [`Options`](#toc-the-options-object) object. `startMenu` : `Object` - [`Start Menu`](#obj-startMenu) object. + [`Start Menu`](#toc-the-start-menu-object) object. `info` : `Object` - [`Info`](#obj-info) object. + [`Info`](#toc-the-info-object) object. `files` @@ -69,13 +69,13 @@ The kmp.json file is a base object described as : `Object` - Array of [`Keyboard`](#obj-keyboard) objects. + Array of [`Keyboard`](#toc-the-keyboard-object) objects. `lexicalModels` : `Object` - Array of [`LexicalModel`](#obj-lexicalModel) objects. + Array of [`LexicalModel`](#toc-the-lexicalmodel-object) objects. ### The System object @@ -139,7 +139,7 @@ The `StartMenu` object is used by Keyman Desktop to install windows : `Array` - An array of [Item](#obj-item) objects + An array of [Item](#toc-the-item-object) objects ### The Item Object @@ -229,7 +229,7 @@ The `Keyboard` object describes an individual keyboard in the Keyman package. A : `Array` - An array of [`Language`](#obj-language) objects linked to the keyboard. + An array of [`Language`](#toc-the-language-object) objects linked to the keyboard. `displayFont` @@ -247,7 +247,7 @@ The `Keyboard` object describes an individual keyboard in the Keyman package. A : `Array` - An array of [`Example`](#obj-example) objects linked to the keyboard. + An array of [`Example`](#toc-the-example-object) objects linked to the keyboard. ### The Language object @@ -342,4 +342,4 @@ The `LexicalModel` object describes an individual model in the Keyman package. A : `Array` - An array of [`Language`](#obj-language) objects linked to the model. + An array of [`Language`](#toc-the-language-object) objects linked to the model. diff --git a/developer/docs/help/reference/file-types/tsv.md b/developer/docs/help/reference/file-types/tsv.md index b926ed9572..1e82d62981 100644 --- a/developer/docs/help/reference/file-types/tsv.md +++ b/developer/docs/help/reference/file-types/tsv.md @@ -16,7 +16,7 @@ Details: Spreadsheet programs such as Microsoft Excel and Google Sheets can export into TSV format. TSVs can also be programmatically generated from other data sources. For advanced users, see [File - Format](#file-format) for more details. + Format](#toc-file-format) for more details. Distributed with lexical model: : No. This is a development file and should not be distributed. diff --git a/developer/docs/help/reference/kmc/cli/reference.md b/developer/docs/help/reference/kmc/cli/reference.md index 763d5ad71f..fdaed58faa 100644 --- a/developer/docs/help/reference/kmc/cli/reference.md +++ b/developer/docs/help/reference/kmc/cli/reference.md @@ -367,10 +367,13 @@ and the files in that folder will follow the : The name of keyboard author (default: blank) + `-d, --description ` diff --git a/developer/src/common/delphi/general/KeyboardFonts.pas b/developer/src/common/delphi/general/KeyboardFonts.pas index 5c80392521..f1a2bac2b9 100644 --- a/developer/src/common/delphi/general/KeyboardFonts.pas +++ b/developer/src/common/delphi/general/KeyboardFonts.pas @@ -21,7 +21,8 @@ unit KeyboardFonts; interface type - TKeyboardFont = (kfontCode, kfontChar, kfontOSK, kfontTouchLayoutPhone, kfontTouchLayoutTablet, kfontTouchLayoutDesktop); + TKeyboardFont = (kfontCode, kfontChar, kfontOSK, kfontTouchLayoutPhone, kfontTouchLayoutTablet, kfontTouchLayoutDesktop, + kfontDisplayMap); TKeyboardFontArray = array[TKeyboardFont] of string; // I4409 @@ -34,7 +35,7 @@ type const KeyboardFontId: array[TKeyboardFont] of string = ( - 'Code', 'Char', 'Osk', 'TouchLayoutPhone', 'TouchLayoutTablet', 'TouchLayoutDesktop' + 'Code', 'Char', 'Osk', 'TouchLayoutPhone', 'TouchLayoutTablet', 'TouchLayoutDesktop', 'DisplayMap' ); implementation diff --git a/developer/src/common/include/kmn_compiler_errors.h b/developer/src/common/include/kmn_compiler_errors.h index f2410f03b0..5ca5c6acfd 100644 --- a/developer/src/common/include/kmn_compiler_errors.h +++ b/developer/src/common/include/kmn_compiler_errors.h @@ -28,11 +28,13 @@ namespace CompilerErrorSeverity { // We use a namespace to stop the enum names leaking out into global namespace scope enum { - Info = 0x000000, // Informational, not necessarily a problem - Hint = 0x100000, // Something the user might want to be aware of - Warn = 0x200000, // Warning: Not great, but we can keep going. - Error = 0x300000, // Severe error where we can't continue - Fatal = 0x400000, // OOM or should-not-happen internal problem + Debug = 0x000000, // log everything including internal debug + Verbose = 0x100000, // log everything, except debug + Info = 0x200000, // Informational, not necessarily a problem + Hint = 0x300000, // Something the user might want to be aware of + Warn = 0x400000, // Warning: Not great, but we can keep going. + Error = 0x500000, // Severe error where we can't continue + Fatal = 0x600000, // OOM or should-not-happen internal problem }; }; diff --git a/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts b/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts index a0b0464e21..fad1dd646f 100644 --- a/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts +++ b/developer/src/common/web/test-helpers/TestCompilerCallbacks.ts @@ -2,7 +2,8 @@ import * as fs from 'fs'; import * as path from 'path'; import { CompilerEvent, CompilerCallbacks, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerError, CompilerNetAsyncCallbacks, DefaultCompilerFileSystemAsyncCallbacks, - CompilerFileSystemAsyncCallbacks } from '@keymanapp/developer-utils'; + CompilerFileSystemAsyncCallbacks, + CompilerErrorSeverity} from '@keymanapp/developer-utils'; import { fileURLToPath } from 'url'; const { TEST_SAVE_FIXTURES } = process.env; @@ -27,6 +28,10 @@ export class TestCompilerCallbacks implements CompilerCallbacks { this.messages = []; } + filteredMessages(severity: CompilerErrorSeverity = CompilerErrorSeverity.Info) { + return this.messages.filter(m => CompilerError.severity(m.code) >= severity); + } + printMessages() { if(this.messages.length) { process.stdout.write(CompilerError.formatEvent(this.messages)); diff --git a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts index 7f0d9aee76..8a16de6bf1 100644 --- a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts +++ b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts @@ -31,7 +31,7 @@ export function verifyCompilerMessagesObject(source: Record, namespa // Verify each object member matches the pattern we expect if(typeof m[key] == 'function') { - const o = /^(Info|Hint|Warn|Error|Fatal)_([A-Za-z0-9_]+)$/.exec(key); + const o = /^(Debug|Verbose|Info|Hint|Warn|Error|Fatal)_([A-Za-z0-9_]+)$/.exec(key); expect(o).to.be.instanceOf(Array, `Expected member ${key} to be a valid message function name`); const c = o[1].toUpperCase() + '_' + o[2]; @@ -41,7 +41,7 @@ export function verifyCompilerMessagesObject(source: Record, namespa expect(v.code).to.equal(m[c], `Function ${key} returns the wrong code`); } else if(typeof m[key] == 'number') { - const o = /^(INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key); + const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key); expect(o).to.be.instanceOf(Array); const f = toTitleCase(o[1]) + '_' + o[2]; @@ -53,7 +53,7 @@ export function verifyCompilerMessagesObject(source: Record, namespa // Verify severify masks if(typeof m[key] == 'number') { - const o = /^(INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key); + const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key); expect(o).to.be.instanceOf(Array); const mask = CompilerError.formatSeverity(m[key]).toUpperCase(); diff --git a/developer/src/common/web/utils/src/compiler-interfaces.ts b/developer/src/common/web/utils/src/compiler-interfaces.ts index ec6d719b30..788457cc02 100644 --- a/developer/src/common/web/utils/src/compiler-interfaces.ts +++ b/developer/src/common/web/utils/src/compiler-interfaces.ts @@ -21,14 +21,18 @@ export interface CompilerEvent { }; export enum CompilerErrorSeverity { - Info = 0x000000, // Informational, not necessarily a problem - Hint = 0x100000, // Something the user might want to be aware of - Warn = 0x200000, // Warning: Not great, but we can keep going. - Error = 0x300000, // Severe error where we can't continue - Fatal = 0x400000, // OOM or should-not-happen internal problem + Debug = 0x000000, // log everything including internal debug + Verbose = 0x100000, // log everything, except debug + Info = 0x200000, // Informational, not necessarily a problem + Hint = 0x300000, // Something the user might want to be aware of + Warn = 0x400000, // Warning: Not great, but we can keep going. + Error = 0x500000, // Severe error where we can't continue + Fatal = 0x600000, // OOM or should-not-happen internal problem }; export const CompilerErrorSeverityValues = [ + CompilerErrorSeverity.Debug, + CompilerErrorSeverity.Verbose, CompilerErrorSeverity.Info, CompilerErrorSeverity.Hint, CompilerErrorSeverity.Warn, @@ -48,6 +52,8 @@ export enum CompilerErrorMask { }; const errorSeverityName = { + [CompilerErrorSeverity.Debug]: 'debug', + [CompilerErrorSeverity.Verbose]: 'verbose', [CompilerErrorSeverity.Info]: 'info', [CompilerErrorSeverity.Hint]: 'hint', [CompilerErrorSeverity.Warn]: 'warn', @@ -419,8 +425,9 @@ export const ALL_COMPILER_LOG_LEVELS = [ 'error', /// Only errors emitted 'warn', /// Errors + warnings 'hint', /// Errors + warnings + hints - 'info', /// All messages: errors + warnings + hints + info - 'debug' /// All messages: errors + warnings + hints + info, plus debug logs + 'info', /// All normal messages: errors + warnings + hints + info + 'verbose', /// All messages + verbose logging + 'debug', /// All messages + verbose + internal debug ] as const; type CompilerLogLevelTuple = typeof ALL_COMPILER_LOG_LEVELS; @@ -432,7 +439,8 @@ export const compilerLogLevelToSeverity: {[index in CompilerLogLevel]: number} = 'warn': CompilerErrorSeverity.Warn, 'hint': CompilerErrorSeverity.Hint, 'info': CompilerErrorSeverity.Info, - 'debug': CompilerErrorSeverity.Info + 'verbose': CompilerErrorSeverity.Verbose, + 'debug': CompilerErrorSeverity.Debug, }; export const ALL_COMPILER_LOG_FORMATS = [ diff --git a/developer/src/kmc-keyboard-info/src/font-family.ts b/developer/src/common/web/utils/src/font-family.ts similarity index 51% rename from developer/src/kmc-keyboard-info/src/font-family.ts rename to developer/src/common/web/utils/src/font-family.ts index a78e341de7..d8fad53e37 100644 --- a/developer/src/kmc-keyboard-info/src/font-family.ts +++ b/developer/src/common/web/utils/src/font-family.ts @@ -24,5 +24,32 @@ export async function getFontFamily(source: Uint8Array) { return null; } + return font.meta.property.find(prop => prop.name == 'font-family')?.text ?? null; +} + +/** + * Extracts the font-family from an in-memory TTF or WOFF blob in `source` + * parameter. + * + * @param source In-memory TTF or WOFF font blob + * + * @throws Uncaught exceptions from ttfMeta.promise if the font file is invalid. + * + * @returns If the file is invalid or cannot be parsed, returns `null`, + * otherwise returns the font family as a string. + */ +export function getFontFamilySync(source: Uint8Array) { + /* c8 ignore next 3 */ + if(!source) { + return null; + } + + const buffer = Buffer.from(source); + const font = ttfMeta.ttfInfoSync(buffer); + /* c8 ignore next 3 */ + if(!font) { + return null; + } + return font.meta.property.find(prop => prop.name == 'font-family')?.text ?? null; } \ No newline at end of file diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index 290efd5fc7..f8ee6246da 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -68,4 +68,8 @@ export * as SourceFilenamePatterns from './source-filename-patterns.js'; export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js'; export * as GitHubUrls from './github-urls.js'; -export * as CloudUrls from './cloud-urls.js'; \ No newline at end of file +export * as CloudUrls from './cloud-urls.js'; + +export { getFontFamily, getFontFamilySync } from './font-family.js'; + +export * as ValidIds from './valid-ids.js'; diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/LICENSE b/developer/src/common/web/utils/src/ttfmeta/LICENSE similarity index 100% rename from developer/src/kmc-keyboard-info/src/ttfmeta/LICENSE rename to developer/src/common/web/utils/src/ttfmeta/LICENSE diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/README.md b/developer/src/common/web/utils/src/ttfmeta/README.md similarity index 100% rename from developer/src/kmc-keyboard-info/src/ttfmeta/README.md rename to developer/src/common/web/utils/src/ttfmeta/README.md diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/index.js b/developer/src/common/web/utils/src/ttfmeta/lib/index.js similarity index 73% rename from developer/src/kmc-keyboard-info/src/ttfmeta/lib/index.js rename to developer/src/common/web/utils/src/ttfmeta/lib/index.js index eb94d53c1f..53972297f1 100644 --- a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/index.js +++ b/developer/src/common/web/utils/src/ttfmeta/lib/index.js @@ -31,6 +31,20 @@ export function ttfInfo(pathOrData, callback) { } } +/** + * @namespace + * @param {string | number | Buffer | URL} pathOrData + * @returns {typeof ttf.result} + */ +export function ttfInfoSync(pathOrData) { + if (pathOrData instanceof Buffer) { + return ttf.ttfInfoSync(ttf.view(pathOrData)); + } else { + const data = fs.readFileSync(pathOrData); + return ttf.ttfInfoSync(ttf.view(data)); + } +} + /** * @param {string | number | Buffer | URL} pathOrData * @returns {Promise} @@ -48,5 +62,5 @@ export function promise(pathOrData) { } /** @namespace */ -export const ttfMeta = { ttfInfo, promise }; +export const ttfMeta = { ttfInfo, ttfInfoSync, promise }; export default ttfMeta; diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/main.js b/developer/src/common/web/utils/src/ttfmeta/lib/main.js similarity index 97% rename from developer/src/kmc-keyboard-info/src/ttfmeta/lib/main.js rename to developer/src/common/web/utils/src/ttfmeta/lib/main.js index 239a6244dc..a952512f26 100644 --- a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/main.js +++ b/developer/src/common/web/utils/src/ttfmeta/lib/main.js @@ -300,6 +300,16 @@ export function ttfInfo(data, callback) { } } +/** + * @namespace + * @param {*} data + * @returns {typeof result} + */ +export function ttfInfoSync(data) { + resultTables(data); + return result; +} + /** * @param {string | number | Buffer | URL | DataView} pathOrData * @returns {Promise} diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/meta.js b/developer/src/common/web/utils/src/ttfmeta/lib/meta.js similarity index 100% rename from developer/src/kmc-keyboard-info/src/ttfmeta/lib/meta.js rename to developer/src/common/web/utils/src/ttfmeta/lib/meta.js diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/test/local.mjs b/developer/src/common/web/utils/src/ttfmeta/test/local.mjs similarity index 100% rename from developer/src/kmc-keyboard-info/src/ttfmeta/test/local.mjs rename to developer/src/common/web/utils/src/ttfmeta/test/local.mjs diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/test/mocha-index.mjs b/developer/src/common/web/utils/src/ttfmeta/test/mocha-index.mjs similarity index 100% rename from developer/src/kmc-keyboard-info/src/ttfmeta/test/mocha-index.mjs rename to developer/src/common/web/utils/src/ttfmeta/test/mocha-index.mjs diff --git a/developer/src/common/web/utils/src/valid-ids.ts b/developer/src/common/web/utils/src/valid-ids.ts new file mode 100644 index 0000000000..717a173f5d --- /dev/null +++ b/developer/src/common/web/utils/src/valid-ids.ts @@ -0,0 +1,38 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + + +/** + * Verifies that an id passed in is valid as an identifier for a Keyman .kmn + * keyboard: starts with alpha or underscore and remainder is alphanumeric + * @param id + * @returns + */ +export function isValidKeymanKeyboardId(id: string) { + return /^[a-z_][a-z0-9_]*$/.test(id); +} + +/** + * Verifies that an id passed in is valid as an identifier for an LDML xml + * keyboard: starts with alpha or underscore and remainder is alphanumeric. + * Note that this test is not the same as the CLDR test for valid LDML xml + * filename, as this test has a narrower validity policy to match the Keyman + * implementation requirements. + * @param id + * @returns + */ +export function isValidLdmlKeyboardId(id: string) { + return /^[a-z_][a-z0-9_]*$/.test(id); +} + +/** + * Verifies that an id passed in is valid as an identifier for a Keyman + * lexical model: starts with alpha or underscore and remainder is alphanumeric, + * in three components -- author.bcp47.uniq + * @param id + * @returns + */ +export function isValidLexicalModelId(id: string) { + return /^[a-z_][a-z0-9_]*\.[a-z][a-z0-9_]+\.[a-z_][a-z0-9_]*$/.test(id); +} diff --git a/developer/src/common/web/utils/test/valid-ids.tests.ts b/developer/src/common/web/utils/test/valid-ids.tests.ts new file mode 100644 index 0000000000..c285332b66 --- /dev/null +++ b/developer/src/common/web/utils/test/valid-ids.tests.ts @@ -0,0 +1,76 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + */ + +import { assert } from 'chai'; +import 'mocha'; +import { isValidKeymanKeyboardId, isValidLdmlKeyboardId, isValidLexicalModelId } from '../src/valid-ids.js'; + +describe('valid-ids', function () { + it('should accept a valid Keyman keyboard ID', function() { + [ + 'khmer_angkor', + 'm', + 'fancypants1_2', + 'keyboard___', + '_100', + ].forEach(id => assert.isTrue(isValidKeymanKeyboardId(id), `expected '${id}' to be valid`)); + }); + + it('should reject invalid Keyman keyboard IDs', function() { + [ + 'khmer_angkor@example.com', + 'keyboard ', + 'Khmer', + ' keyboard', + '111', + '?', + 'keyboard.version', + ].forEach(id => assert.isFalse(isValidKeymanKeyboardId(id), `expected '${id}' to be invalid`)); + }); + + it('should accept a valid LDML keyboard ID', function() { + [ + 'khmer_angkor', + 'm', + 'fancypants1_2', + 'keyboard___', + '_100', + ].forEach(id => assert.isTrue(isValidLdmlKeyboardId(id), `expected '${id}' to be valid`)); + }); + + it('should reject invalid LDML keyboard IDs', function() { + [ + 'khmer_angkor@example.com', + 'keyboard ', + ' keyboard', + 'Khmer', + '111', + '?', + 'keyboard.version', + ].forEach(id => assert.isFalse(isValidLdmlKeyboardId(id), `expected '${id}' to be invalid`)); + }); + + it('should accept a valid lexical model ID', function() { + [ + 'khmer.km.khmer_angkor', + 'nrc.en.mtnt', + ].forEach(id => assert.isTrue(isValidLexicalModelId(id), `expected '${id}' to be valid`)); + }); + + it('should reject invalid lexical model IDs', function() { + [ + 'khmer', + 'en-us', + 'khmer.km-KM.angkor', + 'Khmer.km_km.angkor', + 'model ', + ' model', + '111', + '?', + 'lexical.model', + 'lexical..model', + '.en.model', + ].forEach(id => assert.isFalse(isValidLexicalModelId(id), `expected '${id}' to be invalid`)); + }); +}); diff --git a/developer/src/common/web/utils/tsconfig.json b/developer/src/common/web/utils/tsconfig.json index 6f96f75ee7..362ec3a972 100644 --- a/developer/src/common/web/utils/tsconfig.json +++ b/developer/src/common/web/utils/tsconfig.json @@ -5,8 +5,12 @@ "rootDir": ".", "outDir": "./build/", "baseUrl": ".", + + /** for ttfmeta */ + "allowJs": true, }, "include": [ "src/**/*.ts", + "src/ttfmeta/lib/*.js", ], } diff --git a/developer/src/kmc-copy/build.sh b/developer/src/kmc-copy/build.sh index ee8615fc44..b681757be2 100755 --- a/developer/src/kmc-copy/build.sh +++ b/developer/src/kmc-copy/build.sh @@ -39,6 +39,6 @@ builder_run_action api api-extractor run --local --verbose # note: `export TEST_SAVE_ARTIFACTS=1` to save a copy of artifacts to temp path # note: `export TEST_SAVE_FIXTURES=1` to get a copy of cloud-based fixtures saved to online/ # TODO: -skip-full -builder_run_action test builder_do_typescript_tests 70 +builder_run_action test builder_do_typescript_tests 75 builder_run_action publish builder_publish_npm diff --git a/developer/src/kmc-copy/src/KeymanProjectCopier.ts b/developer/src/kmc-copy/src/KeymanProjectCopier.ts index 40f68fc1fc..9e8d175991 100644 --- a/developer/src/kmc-copy/src/KeymanProjectCopier.ts +++ b/developer/src/kmc-copy/src/KeymanProjectCopier.ts @@ -4,7 +4,7 @@ * Copy a keyboard or lexical model project */ -import { CloudUrls, GitHubUrls, CompilerCallbacks, CompilerLogLevel, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter } from "@keymanapp/developer-utils"; +import { CloudUrls, GitHubUrls, CompilerCallbacks, CompilerLogLevel, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KeymanDeveloperProject, KeymanDeveloperProjectOptions, KPJFileReader, KPJFileWriter, KpsFileReader, KpsFileWriter, ValidIds } from "@keymanapp/developer-utils"; import { KeymanFileTypes } from "@keymanapp/common-types"; import { CopierMessages } from "./copier-messages.js"; @@ -29,11 +29,6 @@ export interface CopierOptions /* not inheriting from CompilerBaseOptions */ { * output path where project folder will be created */ outPath: string; - /* - * TODO-COPY rename: rename existing files rather than copy - - rename: boolean; - */ /** * dryRun: show what would happen */ @@ -125,7 +120,6 @@ export class KeymanProjectCopier implements KeymanCompiler { this.asyncCallbacks = new CopierAsyncCallbacks(this.callbacks, this.githubRef); - // TODO-COPY: validate outputId is valid for the project type? this.outputId = this.callbacks.path.basename(this.outPath); this.sourceId = this.callbacks.path.basename(projectSource, KeymanFileTypes.Source.Project); @@ -136,6 +130,20 @@ export class KeymanProjectCopier implements KeymanCompiler { return null; } + if(project.isKeyboardProject()) { + // LDML and .kmn keyboards have the same id validity requirements, so + // we'll just test against .kmn + if(!ValidIds.isValidKeymanKeyboardId(this.outputId)) { + this.callbacks.reportMessage(CopierMessages.Error_InvalidKeyboardId({id: this.outputId})); + return null; + } + } else if(project.isLexicalModelProject()) { + if(!ValidIds.isValidLexicalModelId(this.outputId)) { + this.callbacks.reportMessage(CopierMessages.Error_InvalidLexicalModelId({id: this.outputId})); + return null; + } + } + const result: CopierResult = { artifacts: { 'kmc-copy:outputPath': { data: null, @@ -302,7 +310,6 @@ export class KeymanProjectCopier implements KeymanCompiler { }; private async copyFolder(inputPath: string, outputPath: string, ignorePatterns: (string | RegExp)[], result: CopierResult) { - // TODO-COPY: watch out for file collisions when copying project files -- after renames const files = await this.asyncCallbacks.fsAsync.readdir(inputPath); for(const {filename,type} of files) { const fullPath = this.normalizePath(this.callbacks.path.join(inputPath, filename)); @@ -312,9 +319,17 @@ export class KeymanProjectCopier implements KeymanCompiler { if(type == 'dir') { await this.copyFolder(fullPath, this.callbacks.path.join(outputPath, filename), ignorePatterns, result); } else if(!result.artifacts[fullPath]) { + const filename = this.generateNewFilename(fullPath, outputPath); + + for(const artifact of Object.keys(result.artifacts)) { + if(result.artifacts[artifact].filename == filename) { + this.callbacks.reportMessage(CopierMessages.Warn_FilenameCollides({filename})); + } + } + result.artifacts[fullPath] = { data: await this.asyncCallbacks.fsAsync.readFile(fullPath), - filename: this.generateNewFilename(fullPath, outputPath) + filename } } } @@ -365,7 +380,7 @@ export class KeymanProjectCopier implements KeymanCompiler { return true; } - private async copyGenericFile(_project: KeymanDeveloperProject, filename: string, outputPath: string, _source: string, result: CopierResult): Promise { + private async copyGenericFile(project: KeymanDeveloperProject, filename: string, outputPath: string, _source: string, result: CopierResult): Promise { if(result.artifacts[filename]) { return true; } @@ -388,6 +403,10 @@ export class KeymanProjectCopier implements KeymanCompiler { return false; } + if(KeymanFileTypes.binaryTypeFromFilename(filename) != null) { + outputPath = this.callbacks.path.join(this.outPath, 'build'); + } + result.artifacts[filename] = { data, filename: this.generateNewFilename(filename, outputPath) @@ -578,6 +597,7 @@ export class KeymanProjectCopier implements KeymanCompiler { */ private generateNewFilename(filename: string, outputPath: string): string { const {ext, base} = this.splitFilename(filename); + const newFilename = base == this.sourceId ? this.callbacks.path.join(outputPath, this.outputId + ext) : this.callbacks.path.join(outputPath, base + ext); diff --git a/developer/src/kmc-copy/src/cloud.ts b/developer/src/kmc-copy/src/cloud.ts index 7b3ef1ff11..a65284933e 100644 --- a/developer/src/kmc-copy/src/cloud.ts +++ b/developer/src/kmc-copy/src/cloud.ts @@ -63,9 +63,10 @@ export class KeymanCloudSource { } public async downloadFolderFromGitHub(ref: GitHubRef): Promise<{filename:string,type:'dir'|'file'}[]> { - // this.callbacks.reportMessage(CopierMessages.Info_DownloadingFile({ref})); // TODO-COPY: verbose mode support const url = `https://api.github.com/repos/${ref.owner}/${ref.repo}/contents/${ref.path}?ref=${ref.branch}`; + this.callbacks.reportMessage(CopierMessages.Verbose_DownloadingFolder({path:ref.path, url})); + let folder: any; try { folder = await this.callbacks.net.fetchJSON(url); @@ -94,6 +95,7 @@ export class KeymanCloudSource { } const url = `https://raw.githubusercontent.com/${ref.owner}/${ref.repo}/refs/heads/${ref.branch}${ref.path}`; + this.callbacks.reportMessage(CopierMessages.Verbose_DownloadingFile({filename:this.callbacks.path.basename(ref.path), url})); try { return await this.callbacks.net.fetchBlob(url); diff --git a/developer/src/kmc-copy/src/copier-messages.ts b/developer/src/kmc-copy/src/copier-messages.ts index 78a948ec79..8afff79149 100644 --- a/developer/src/kmc-copy/src/copier-messages.ts +++ b/developer/src/kmc-copy/src/copier-messages.ts @@ -7,6 +7,7 @@ import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException } from "@keymanapp/developer-utils"; const Namespace = CompilerErrorNamespace.Copier; +const SevVerbose = CompilerErrorSeverity.Verbose | Namespace; const SevInfo = CompilerErrorSeverity.Info | Namespace; // const SevHint = CompilerErrorSeverity.Hint | Namespace; const SevWarn = CompilerErrorSeverity.Warn | Namespace; @@ -198,4 +199,36 @@ export class CopierMessages { the provided error details for more details.` ); + static VERBOSE_DownloadingFile = SevVerbose | 0x001C; + static Verbose_DownloadingFile = (o:{filename: string, url: string}) => m( + this.VERBOSE_DownloadingFile, + `Downloading '${def(o.filename)}' from '${def(o.url)}'`, + ); + + static VERBOSE_DownloadingFolder = SevVerbose | 0x001D; + static Verbose_DownloadingFolder = (o:{path: string, url: string}) => m( + this.VERBOSE_DownloadingFolder, + `Downloading folder '${def(o.path)}' from '${def(o.url)}'`, + ); + + // See also generator-messages.ERROR_InvalidKeymanKeyboardId + static ERROR_InvalidKeyboardId = SevError | 0x001E; + static Error_InvalidKeyboardId = (o:{id: string}) => m( + this.ERROR_InvalidKeyboardId, + `The specified keyboard id '${def(o.id)}' contains characters that are not permitted for a keyboard id or filename.`, + ); + + // See also generator-messages.ERROR_InvalidLexicalModelId + static ERROR_InvalidLexicalModelId = SevError | 0x001F; + static Error_InvalidLexicalModelId = (o:{id:string}) => m( + this.ERROR_InvalidLexicalModelId, + `The specified lexical model id '${def(o.id)}' contains characters that are not permitted or does not match the required pattern of 'author.bcp47.uniq'.`, + ); + + static WARN_FilenameCollides = SevWarn | 0x0020; + static Warn_FilenameCollides = (o:{filename:string}) => m( + this.WARN_FilenameCollides, + `The output file '${def(o.filename)}' has two different possible source files.`, + ); + }; diff --git a/developer/src/kmc-copy/test/copier-messages.tests.ts b/developer/src/kmc-copy/test/copier-messages.tests.ts new file mode 100644 index 0000000000..0312910860 --- /dev/null +++ b/developer/src/kmc-copy/test/copier-messages.tests.ts @@ -0,0 +1,54 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + */ + +import 'mocha'; +// import * as path from 'path'; +// import * as fs from 'fs'; +// import { fileURLToPath } from 'url'; +import { assert } from 'chai'; +import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; +import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; +import { CopierMessages } from '../src/copier-messages.js'; +import { KeymanProjectCopier } from '../src/KeymanProjectCopier.js'; +import { makePathToFixture } from './helpers/index.js'; + +describe('CopierMessages', function () { + const callbacks = new TestCompilerCallbacks(makePathToFixture('online')); + + this.beforeEach(function() { + callbacks.clear(); + }); + + this.afterEach(function() { + if(this.currentTest?.isFailed()) { + callbacks.printMessages(); + } + }); + + it('should have a valid CopierMessages object', function() { + return verifyCompilerMessagesObject(CopierMessages, CompilerErrorNamespace.Copier); + }); + + it('should generate ERROR_InvalidKeyboardId if the output keyboard id is invalid', async function() { + const copier = new KeymanProjectCopier(); + assert.isTrue(await copier.init(callbacks, {dryRun: true, outPath: '0invalid'})) + assert.isNull(await copier.run('cloud:jawa')); + assert.isTrue(callbacks.hasMessage(CopierMessages.ERROR_InvalidKeyboardId)); + }); + + it('should generate ERROR_InvalidLexicalModelId if the output lexical model id is invalid', async function() { + const copier = new KeymanProjectCopier(); + assert.isTrue(await copier.init(callbacks, {dryRun: true, outPath: 'nrc.str-x-sencoten.sencoten'})) + assert.isNull(await copier.run('cloud:nrc.str.sencoten')); + assert.isTrue(callbacks.hasMessage(CopierMessages.ERROR_InvalidLexicalModelId)); + }); + + it('should generate WARN_FilenameCollision if a copied file will collide', async function() { + const copier = new KeymanProjectCopier(); + assert.isTrue(await copier.init(callbacks, {dryRun: true, outPath: 'collided'})) + const result = await copier.run(makePathToFixture('projects/collision/collision.kpj')); + assert.isNotNull(result); + assert.isTrue(callbacks.hasMessage(CopierMessages.WARN_FilenameCollides)); + }); +}); diff --git a/developer/src/kmc-copy/test/copier.tests.ts b/developer/src/kmc-copy/test/copier.tests.ts index 6d583862ae..6a2a8cbb1e 100644 --- a/developer/src/kmc-copy/test/copier.tests.ts +++ b/developer/src/kmc-copy/test/copier.tests.ts @@ -306,7 +306,6 @@ describe('KeymanProjectCopier', function() { assert.isTrue(await copier.init(callbacks, { dryRun: false, outPath, - // TODO-COPY rename: false, })); const result = await copier.run(fullSource); @@ -316,7 +315,7 @@ describe('KeymanProjectCopier', function() { // assert.isOk(result); - assert.isEmpty(callbacks.messages); + assert.isEmpty(callbacks.filteredMessages()); if(TEST_SAVE_ARTIFACTS) { assert.isTrue(await copier.write(result.artifacts)); @@ -370,7 +369,6 @@ describe('KeymanProjectCopier', function() { assert.isTrue(await copier.init(callbacks, { dryRun: false, outPath, - // TODO-COPY rename: false, })); // armenian_mnemonic selected because (a) small, and (b) has v2.0 project, so @@ -379,7 +377,7 @@ describe('KeymanProjectCopier', function() { // We should have no messages and a successful result assert.isOk(result); - assert.isEmpty(callbacks.messages); + assert.isEmpty(callbacks.filteredMessages()); // TODO-COPY: verify outcome using pattern above @@ -400,14 +398,13 @@ describe('KeymanProjectCopier', function() { assert.isTrue(await copier.init(callbacks, { dryRun: false, outPath, - // TODO-COPY rename: false, })); const result = await copier.run('cloud:'+id); // We should have no messages and a successful result assert.isOk(result); - assert.isEmpty(callbacks.messages); + assert.isEmpty(callbacks.filteredMessages()); // TODO-COPY: verify outcome using pattern above @@ -492,8 +489,41 @@ describe('KeymanProjectCopier', function() { }); })); + it('should copy a disorganized project into current structure', async function() { + const copier = new KeymanProjectCopier(); + assert.isTrue(await copier.init(callbacks, { + dryRun: false, + outPath: 'organized', + })); + + const result = await copier.run(makePathToFixture('projects','disorganized','alephwithbeth.kpj')); + assert.sameMembers(Object.keys(result.artifacts).map(k => result.artifacts[k].filename), [ + 'organized', // kmc-copy:outputPath + 'organized/organized.kpj', + 'organized/HISTORY.md', + 'organized/LICENSE.md', + 'organized/README.md', + // primary source files get moved to 'source/' + 'organized/source/organized.kps', + 'organized/source/organized.kmn', + 'organized/source/organized.ico', + 'organized/source/organized.kvks', + // files in 'other/' get moved to 'source/other/' + 'organized/source/other/organized.keyman-touch-layout', + 'organized/source/other/readme.htm', + 'organized/source/other/welcome/welcome.htm', + 'organized/source/other/welcome/KeyboardLayout.png', + 'organized/source/other/welcome/KeyboardLayoutAlt.png', + 'organized/source/other/welcome/KeyboardLayoutShift.png', + // build artifact files get moved to build/ + 'organized/build/organized.js', + 'organized/build/organized.kvk', + 'organized/build/organized.kmx', + // 'organized/build/organized.kmp', // TODO-COPY: do we need to copy this file as well? + ]); + }); + // TODO-COPY: additional tests - it.skip('should copy a disorganized project into current structure', async function() {}); it.skip('should copy a standalone .kmn into a new project', async function() {}); it.skip('should copy a standalone .kmn and .kps into a new project', async function() {}); }); diff --git a/developer/src/kmc-copy/test/fixtures/projects/collision/collision.kpj b/developer/src/kmc-copy/test/fixtures/projects/collision/collision.kpj new file mode 100644 index 0000000000..fffdca635e --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/collision/collision.kpj @@ -0,0 +1,7 @@ + + + + keyboard + 2.0 + + diff --git a/developer/src/kmc-copy/test/fixtures/projects/collision/source/collided.ico b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collided.ico new file mode 100644 index 0000000000..84b57c5aeb Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collided.ico differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.ico b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.ico new file mode 100644 index 0000000000..84b57c5aeb Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.ico differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.kmn b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.kmn new file mode 100644 index 0000000000..121e224d89 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/collision/source/collision.kmn @@ -0,0 +1,7 @@ +store(&TARGETS) 'any' +store(&NAME) 'Collision' +store(&BITMAP) 'collision.ico' + +begin Unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/HISTORY.md b/developer/src/kmc-copy/test/fixtures/projects/disorganized/HISTORY.md new file mode 100644 index 0000000000..65dd7e13c7 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/HISTORY.md @@ -0,0 +1,35 @@ +alephwithbeth Change History +==================== + +1.0.6 (2024-06-11) +------------------ +* Remove source files from package + +1.0.5 (2023-02-28) +------------------ +* replaced hyphen with maqef +* added a touch-optimized keyboard layout for phone + +1.0.4 (2023-02-13) +------------------ +* Removed unnecessary version numbers. +* added description + +1.0.3 (2023-02-11) +------------------ +* Removed the faulty auto correct feature. +* Captialized all words of the title. +* Added title to .kmn + +1.0.2 (2023-02-09) +------------------ +* Renamed keyboard to "Aleph with Beth" + +1.0.1 (2023-02-08) +---------------- +* Updated author and website (in kps) +* Added copyrights where needed. + +1.0 (2023-01-28) +---------------- +* Created by Cameron Briggs diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/LICENSE.md b/developer/src/kmc-copy/test/fixtures/projects/disorganized/LICENSE.md new file mode 100644 index 0000000000..7f84a306c0 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2023-2024 Cameron Briggs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/README.md b/developer/src/kmc-copy/test/fixtures/projects/disorganized/README.md new file mode 100644 index 0000000000..16edd8db20 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/README.md @@ -0,0 +1,25 @@ +alephwithbeth keyboard +============== + +Description +----------- +A Hebrew keyboard designed to intuitively match the standard QWERTY layout. + +Copyright +--------- +See [LICENSE.md](LICENSE.md) + +Supported Platforms +------------------- + * Windows + * macOS + * Linux + * Web + * iPhone + * iPad + * Android phone + * Android tablet + * Mobile devices + * Desktop devices + * Tablet devices + diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.ico b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.ico new file mode 100644 index 0000000000..84b57c5aeb Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.ico differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.js b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.js new file mode 100644 index 0000000000..9466d76b85 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.js @@ -0,0 +1 @@ +KeymanWeb.KR(new Keyboard_alephwithbeth());function Keyboard_alephwithbeth(){this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9;this.KI="Keyboard_alephwithbeth";this.KN="Aleph With Beth";this.KMINVER="14.0";this.KV={F:' 1em "Arial"',K102:0};this.KV.KLS={"shift": ["","!","֘","֨","֜","֞","֔","֬","֝","(",")","_","+","","","","קּ","וּ","ֵ","רּ","תּ","יּ","ֻ","","וֹ","פּ","{","}","|","","","","ָ","סּ","דּ","טּ","גּ","הּ","וַיּ","כּ","לּ","״","\"","","","","","","","זּ","צּ","שּׂ","שּׁ","בּ","נּ","מּ","","ּ","?","","","","","",""],"default": ["֑","1","2","3","4","5","6","7","8","9","0","־","=","","","","ק","ו","ֶ","ר","ת","י","ע","ִ","ֹ","פ","[","]","\\","","","","ַ","ס","ד","ט","ג","ה","ח","כ","ל","ְ","'","","","","","","","ז","צ","שׂ","שׁ","ב","נ","מ","א",".","/","","","","","",""],"alt": ["","֗","֮","֙","֠","֩","֟","֡","֕","֓","֯","ֿ","◌","","","","","","ֱ","","","","","ֽ","ֳ","ף","֔","֒","֫","","","","ֲ","","","","•","̊","","ך","","׃","ׄ","","","","","","","","ץ","","","","ן","ם","֫","̈","״","","","","","",""]};this.KV.BK=(function(x){var e=Array.apply(null,Array(65)).map(String.prototype.valueOf,""),r=[],v,i,m=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt'];for(i=m.length-1;i>=0;i--)if((v=x[m[i]])||r.length)r=(v?v:e).slice().concat(r);return r})(this.KV.KLS);this.KDU=0;this.KH="Aleph With Beth";this.KM=0;this.KBVER="1.0.6";this.KMBM=0x70;this.KRTL=1;this.KVKL={"tablet":{"displayUnderlying":false,"layer":[{"id":"default","row":[{"id":"1","key":[{"id":"K_1","text":"1"},{"id":"K_2","text":"2"},{"id":"K_3","text":"3"},{"id":"K_4","text":"4"},{"id":"K_5","text":"5"},{"id":"K_6","text":"6"},{"id":"K_7","text":"7"},{"id":"K_8","text":"8"},{"id":"K_9","text":"9"},{"id":"K_0","text":"0"},{"id":"K_HYPHEN","text":"־"},{"id":"K_EQUAL","text":"="},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"ק","pad":"75"},{"id":"K_W","text":"ו"},{"id":"K_E","text":"ֶ"},{"id":"K_R","text":"ר"},{"id":"K_T","text":"ת"},{"id":"K_Y","text":"י"},{"id":"K_U","text":"ע"},{"id":"K_I","text":"ִ"},{"id":"K_O","text":"ֹ"},{"id":"K_P","text":"פ"},{"id":"K_LBRKT","text":"["},{"id":"K_RBRKT","text":"]"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE","text":"֑"},{"id":"K_A","text":"ַ"},{"id":"K_S","text":"ס"},{"id":"K_D","text":"ד"},{"id":"K_F","text":"ט"},{"id":"K_G","text":"ג"},{"id":"K_H","text":"ה"},{"id":"K_J","text":"ח"},{"id":"K_K","text":"כ"},{"id":"K_L","text":"ל"},{"id":"K_COLON","text":"ְ"},{"id":"K_QUOTE","text":"'"},{"id":"K_BKSLASH","text":"\\"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift"},{"id":"K_oE2"},{"id":"K_Z","text":"ז"},{"id":"K_X","text":"צ"},{"id":"K_C","text":"שׂ"},{"id":"K_V","text":"שׁ"},{"id":"K_B","text":"ב"},{"id":"K_N","text":"נ"},{"id":"K_M","text":"מ"},{"id":"K_COMMA","text":"א"},{"id":"K_PERIOD","text":"."},{"id":"K_SLASH","text":"/"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"shift","row":[{"id":"1","key":[{"id":"K_1","text":"!"},{"id":"K_2","text":"֘"},{"id":"K_3","text":"֨"},{"id":"K_4","text":"֜"},{"id":"K_5","text":"֞"},{"id":"K_6","text":"֔"},{"id":"K_7","text":"֬"},{"id":"K_8","text":"֝"},{"id":"K_9","text":"("},{"id":"K_0","text":")"},{"id":"K_HYPHEN","text":"_"},{"id":"K_EQUAL","text":"+"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"קּ","pad":"75"},{"id":"K_W","text":"וּ"},{"id":"K_E","text":"ֵ"},{"id":"K_R","text":"רּ"},{"id":"K_T","text":"תּ"},{"id":"K_Y","text":"יּ"},{"id":"K_U","text":"ֻ"},{"id":"K_I"},{"id":"K_O","text":"וֹ"},{"id":"K_P","text":"פּ"},{"id":"K_LBRKT","text":"{"},{"id":"K_RBRKT","text":"}"},{"id":"T_new_362","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ָ"},{"id":"K_S","text":"סּ"},{"id":"K_D","text":"דּ"},{"id":"K_F","text":"טּ"},{"id":"K_G","text":"גּ"},{"id":"K_H","text":"הּ"},{"id":"K_J","text":"וַיּ"},{"id":"K_K","text":"כּ"},{"id":"K_L","text":"לּ"},{"id":"K_COLON","text":"״"},{"id":"K_QUOTE","text":"\""},{"id":"K_BKSLASH","text":"|"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"default"},{"id":"K_oE2"},{"id":"K_Z","text":"זּ"},{"id":"K_X","text":"צּ"},{"id":"K_C","text":"שּׂ"},{"id":"K_V","text":"שּׁ"},{"id":"K_B","text":"בּ"},{"id":"K_N","text":"נּ"},{"id":"K_M","text":"מּ"},{"id":"K_COMMA"},{"id":"K_PERIOD","text":"ּ"},{"id":"K_SLASH","text":"?"},{"id":"T_new_388","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"alt","row":[{"id":"1","key":[{"id":"K_1","text":"֗"},{"id":"K_2","text":"֮"},{"id":"K_3","text":"֙"},{"id":"K_4","text":"֠"},{"id":"K_5","text":"֩"},{"id":"K_6","text":"֟"},{"id":"K_7","text":"֡"},{"id":"K_8","text":"֕"},{"id":"K_9","text":"֓"},{"id":"K_0","text":"֯"},{"id":"K_HYPHEN","text":"ֿ"},{"id":"K_EQUAL","text":"◌"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","pad":"75"},{"id":"K_W"},{"id":"K_E","text":"ֱ"},{"id":"K_R"},{"id":"K_T"},{"id":"K_Y"},{"id":"K_U"},{"id":"K_I","text":"ֽ"},{"id":"K_O","text":"ֳ"},{"id":"K_P","text":"ף"},{"id":"K_LBRKT","text":"֔"},{"id":"K_RBRKT","text":"֒"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ֲ"},{"id":"K_S"},{"id":"K_D"},{"id":"K_F"},{"id":"K_G","text":"•"},{"id":"K_H","text":"̊"},{"id":"K_J"},{"id":"K_K","text":"ך"},{"id":"K_L"},{"id":"K_COLON","text":"׃"},{"id":"K_QUOTE","text":"ׄ"},{"id":"K_BKSLASH","text":"֫"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift","layer":"shift"},{"id":"K_oE2"},{"id":"K_Z"},{"id":"K_X","text":"ץ"},{"id":"K_C"},{"id":"K_V"},{"id":"K_B"},{"id":"K_N","text":"ן"},{"id":"K_M","text":"ם"},{"id":"K_COMMA","text":"֫"},{"id":"K_PERIOD","text":"̈"},{"id":"K_SLASH","text":"״"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"default","width":"130","sp":"1","nextlayer":"default"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]}]},"phone":{"displayUnderlying":false,"layer":[{"id":"default","row":[{"id":"1","key":[{"id":"K_1","text":"1"},{"id":"K_2","text":"2"},{"id":"K_3","text":"3"},{"id":"K_4","text":"4"},{"id":"K_5","text":"5"},{"id":"K_6","text":"6"},{"id":"K_7","text":"7"},{"id":"K_8","text":"8"},{"id":"K_9","text":"9"},{"id":"K_0","text":"0"},{"id":"K_HYPHEN","text":"־"},{"id":"K_EQUAL","text":"="},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"ק","pad":"75"},{"id":"K_W","text":"ו"},{"id":"K_E","text":"ֶ"},{"id":"K_R","text":"ר"},{"id":"K_T","text":"ת"},{"id":"K_Y","text":"י"},{"id":"K_U","text":"ע"},{"id":"K_I","text":"ִ"},{"id":"K_O","text":"ֹ"},{"id":"K_P","text":"פ"},{"id":"K_LBRKT","text":"["},{"id":"K_RBRKT","text":"]"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE","text":"֑"},{"id":"K_A","text":"ַ"},{"id":"K_S","text":"ס"},{"id":"K_D","text":"ד"},{"id":"K_F","text":"ט"},{"id":"K_G","text":"ג"},{"id":"K_H","text":"ה"},{"id":"K_J","text":"ח"},{"id":"K_K","text":"כ"},{"id":"K_L","text":"ל"},{"id":"K_COLON","text":"ְ"},{"id":"K_QUOTE","text":"'"},{"id":"K_BKSLASH","text":"\\"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift"},{"id":"K_oE2"},{"id":"K_Z","text":"ז"},{"id":"K_X","text":"צ"},{"id":"K_C","text":"שׂ"},{"id":"K_V","text":"שׁ"},{"id":"K_B","text":"ב"},{"id":"K_N","text":"נ"},{"id":"K_M","text":"מ"},{"id":"K_COMMA","text":"א"},{"id":"K_PERIOD","text":"."},{"id":"K_SLASH","text":"/"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"shift","row":[{"id":"1","key":[{"id":"K_1","text":"!"},{"id":"K_2","text":"֘"},{"id":"K_3","text":"֨"},{"id":"K_4","text":"֜"},{"id":"K_5","text":"֞"},{"id":"K_6","text":"֔"},{"id":"K_7","text":"֬"},{"id":"K_8","text":"֝"},{"id":"K_9","text":"("},{"id":"K_0","text":")"},{"id":"K_HYPHEN","text":"_"},{"id":"K_EQUAL","text":"+"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"קּ","pad":"75"},{"id":"K_W","text":"וּ"},{"id":"K_E","text":"ֵ"},{"id":"K_R","text":"רּ"},{"id":"K_T","text":"תּ"},{"id":"K_Y","text":"יּ"},{"id":"K_U","text":"ֻ"},{"id":"K_I"},{"id":"K_O","text":"וֹ"},{"id":"K_P","text":"פּ"},{"id":"K_LBRKT","text":"{"},{"id":"K_RBRKT","text":"}"},{"id":"T_new_362","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ָ"},{"id":"K_S","text":"סּ"},{"id":"K_D","text":"דּ"},{"id":"K_F","text":"טּ"},{"id":"K_G","text":"גּ"},{"id":"K_H","text":"הּ"},{"id":"K_J","text":"וַיּ"},{"id":"K_K","text":"כּ"},{"id":"K_L","text":"לּ"},{"id":"K_COLON","text":"״"},{"id":"K_QUOTE","text":"\""},{"id":"K_BKSLASH","text":"|"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"default"},{"id":"K_oE2"},{"id":"K_Z","text":"זּ"},{"id":"K_X","text":"צּ"},{"id":"K_C","text":"שּׂ"},{"id":"K_V","text":"שּׁ"},{"id":"K_B","text":"בּ"},{"id":"K_N","text":"נּ"},{"id":"K_M","text":"מּ"},{"id":"K_COMMA"},{"id":"K_PERIOD","text":"ּ"},{"id":"K_SLASH","text":"?"},{"id":"T_new_388","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"alt","width":"130","sp":"1","nextlayer":"alt"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]},{"id":"alt","row":[{"id":"1","key":[{"id":"K_1","text":"֗"},{"id":"K_2","text":"֮"},{"id":"K_3","text":"֙"},{"id":"K_4","text":"֠"},{"id":"K_5","text":"֩"},{"id":"K_6","text":"֟"},{"id":"K_7","text":"֡"},{"id":"K_8","text":"֕"},{"id":"K_9","text":"֓"},{"id":"K_0","text":"֯"},{"id":"K_HYPHEN","text":"ֿ"},{"id":"K_EQUAL","text":"◌"},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","pad":"75"},{"id":"K_W"},{"id":"K_E","text":"ֱ"},{"id":"K_R"},{"id":"K_T"},{"id":"K_Y"},{"id":"K_U"},{"id":"K_I","text":"ֽ"},{"id":"K_O","text":"ֳ"},{"id":"K_P","text":"ף"},{"id":"K_LBRKT","text":"֔"},{"id":"K_RBRKT","text":"֒"},{"id":"T_new_138","width":"10","sp":"10"}]},{"id":"3","key":[{"id":"K_BKQUOTE"},{"id":"K_A","text":"ֲ"},{"id":"K_S"},{"id":"K_D"},{"id":"K_F"},{"id":"K_G","text":"•"},{"id":"K_H","text":"̊"},{"id":"K_J"},{"id":"K_K","text":"ך"},{"id":"K_L"},{"id":"K_COLON","text":"׃"},{"id":"K_QUOTE","text":"ׄ"},{"id":"K_BKSLASH","text":"֫"}]},{"id":"4","key":[{"id":"K_SHIFT","text":"*Shift*","width":"160","sp":"1","nextlayer":"shift","layer":"shift"},{"id":"K_oE2"},{"id":"K_Z"},{"id":"K_X","text":"ץ"},{"id":"K_C"},{"id":"K_V"},{"id":"K_B"},{"id":"K_N","text":"ן"},{"id":"K_M","text":"ם"},{"id":"K_COMMA","text":"֫"},{"id":"K_PERIOD","text":"̈"},{"id":"K_SLASH","text":"״"},{"id":"T_new_164","width":"10","sp":"10"}]},{"id":"5","key":[{"id":"K_LCONTROL","text":"default","width":"130","sp":"1","nextlayer":"default"},{"id":"K_LOPT","text":"*Menu*","width":"140","sp":"1"},{"id":"K_SPACE","width":"930"},{"id":"K_ENTER","text":"*Enter*","width":"145","sp":"1"}]}]}]}};this.s7="כמנפצ";this.s8="ךםןףץ";this.s9="כּפּ";this.s10="ךּףּ";this.s11="ּ";this.s12="מּנּצּ";this.s13="םןץ";this.s14="ֽ֑֖֛֢֣֤֥֦֧֪֚֭֮֒֓֔֕֗֘֙֜֝֞֟֠֡֨֩֫֬֯־ֿ׀ׄ";this.s15="...";this.KVER="18.0.170.0";this.KVS=[];this.gs=function(t,e) {return this.g0(t,e);};this.gs=function(t,e) {return this.g0(t,e);};this.g0=function(t,e) {var k=KeymanWeb,r=0,m=0;if(k.KKM(e,16384,9)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}}else if(k.KKM(e,16384,13)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}}else if(k.KKM(e,16384,32)) {if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KA(1,k.KC(2,1,t),this.s14)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(3,this.s8,1,t);k.KIO(-1,this.s14,2,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(3,1,t),this.s7)&&k.KCM(2,t,"ּ",1)&&k.KA(2,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(3,this.s8,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,3,t);}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KCM(1,t,"ּ",1)){r=m=1;k.KIO(2,this.s8,1,t);k.KO(-1,t,"ּ");}else if(k.KA(0,k.KC(2,1,t),this.s7)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s8,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s9)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s10,1,t);k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(2,1,t),this.s12)&&k.KA(1,k.KC(1,1,t),this.s14)){r=m=1;k.KIO(2,this.s13,1,t);k.KO(-1,t,"ּ");k.KIO(-1,this.s14,2,t);}else if(k.KA(0,k.KC(1,1,t),this.s7)){r=m=1;k.KIO(1,this.s8,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s9)){r=m=1;k.KIO(1,this.s10,1,t);}else if(k.KA(0,k.KC(1,1,t),this.s12)){r=m=1;k.KIO(1,this.s13,1,t);k.KO(-1,t,"ּ");}else if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16432,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16400,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16448,32)) {if(1){r=m=1;k.KO(0,t," ");}}else if(k.KKM(e,16400,49)) {if(1){r=m=1;k.KO(0,t,"!");}}else if(k.KKM(e,16400,222)) {if(1){r=m=1;k.KO(0,t,"\"");}}else if(k.KKM(e,16400,51)) {if(1){r=m=1;k.KO(0,t,"֨");}}else if(k.KKM(e,16400,52)) {if(1){r=m=1;k.KO(0,t,"֜");}}else if(k.KKM(e,16400,53)) {if(1){r=m=1;k.KO(0,t,"֞");}}else if(k.KKM(e,16400,55)) {if(1){r=m=1;k.KO(0,t,"֬");}}else if(k.KKM(e,16384,222)) {if(1){r=m=1;k.KO(0,t,"'");}}else if(k.KKM(e,16448,222)) {if(1){r=m=1;k.KO(0,t,"ׄ");}}else if(k.KKM(e,16400,57)) {if(1){r=m=1;k.KO(0,t,"(");}}else if(k.KKM(e,16400,48)) {if(1){r=m=1;k.KO(0,t,")");}}else if(k.KKM(e,16400,56)) {if(1){r=m=1;k.KO(0,t,"֝");}}else if(k.KKM(e,16400,187)) {if(1){r=m=1;k.KO(0,t,"+");}}else if(k.KKM(e,16448,188)) {if(1){r=m=1;k.KO(0,t,"֫");}}else if(k.KKM(e,16384,188)) {if(1){r=m=1;k.KO(0,t,"א");}}else if(k.KKM(e,16384,189)) {if(1){r=m=1;k.KO(0,t,"־");}}else if(k.KKM(e,16448,189)) {if(1){r=m=1;k.KO(0,t,"ֿ");}}else if(k.KKM(e,16384,190)) {if(1){r=m=1;k.KO(0,t,".");}}else if(k.KKM(e,16448,190)) {if(1){r=m=1;k.KO(0,t,"̈");}}else if(k.KKM(e,16384,191)) {if(1){r=m=1;k.KO(0,t,"/");}}else if(k.KKM(e,16448,191)) {if(1){r=m=1;k.KO(0,t,"״");}}else if(k.KKM(e,16384,48)) {if(1){r=m=1;k.KO(0,t,"0");}}else if(k.KKM(e,16448,48)) {if(1){r=m=1;k.KO(0,t,"֯");}}else if(k.KKM(e,16384,49)) {if(1){r=m=1;k.KO(0,t,"1");}}else if(k.KKM(e,16448,49)) {if(1){r=m=1;k.KO(0,t,"֗");}}else if(k.KKM(e,16384,50)) {if(1){r=m=1;k.KO(0,t,"2");}}else if(k.KKM(e,16448,50)) {if(1){r=m=1;k.KO(0,t,"֮");}}else if(k.KKM(e,16384,51)) {if(1){r=m=1;k.KO(0,t,"3");}}else if(k.KKM(e,16448,51)) {if(1){r=m=1;k.KO(0,t,"֙");}}else if(k.KKM(e,16384,52)) {if(1){r=m=1;k.KO(0,t,"4");}}else if(k.KKM(e,16448,52)) {if(1){r=m=1;k.KO(0,t,"֠");}}else if(k.KKM(e,16384,53)) {if(1){r=m=1;k.KO(0,t,"5");}}else if(k.KKM(e,16448,53)) {if(1){r=m=1;k.KO(0,t,"֩");}}else if(k.KKM(e,16384,54)) {if(1){r=m=1;k.KO(0,t,"6");}}else if(k.KKM(e,16448,54)) {if(1){r=m=1;k.KO(0,t,"֟");}}else if(k.KKM(e,16384,55)) {if(1){r=m=1;k.KO(0,t,"7");}}else if(k.KKM(e,16448,55)) {if(1){r=m=1;k.KO(0,t,"֡");}}else if(k.KKM(e,16384,56)) {if(1){r=m=1;k.KO(0,t,"8");}}else if(k.KKM(e,16448,56)) {if(1){r=m=1;k.KO(0,t,"֕");}}else if(k.KKM(e,16384,57)) {if(1){r=m=1;k.KO(0,t,"9");}}else if(k.KKM(e,16448,57)) {if(1){r=m=1;k.KO(0,t,"֓");}}else if(k.KKM(e,16400,186)) {if(1){r=m=1;k.KO(0,t,"״");}}else if(k.KKM(e,16448,186)) {if(1){r=m=1;k.KO(0,t,"׃");}}else if(k.KKM(e,16384,186)) {if(1){r=m=1;k.KO(0,t,"ְ");}}else if(k.KKM(e,16400,188)) {if(1){r=m=1;}}else if(k.KKM(e,16384,187)) {if(1){r=m=1;k.KO(0,t,"=");}}else if(k.KKM(e,16448,187)) {if(1){r=m=1;k.KO(0,t,"◌");}}else if(k.KKM(e,16400,190)) {if(1){r=m=1;k.KO(0,t,"ּ");}}else if(k.KKM(e,16400,191)) {if(1){r=m=1;k.KO(0,t,"?");}}else if(k.KKM(e,16400,50)) {if(1){r=m=1;k.KO(0,t,"֘");}}else if(k.KKM(e,16400,65)) {if(1){r=m=1;k.KO(0,t,"ָ");}}else if(k.KKM(e,16400,66)) {if(1){r=m=1;k.KO(0,t,"בּ");}}else if(k.KKM(e,16400,67)) {if(1){r=m=1;k.KO(0,t,"שּׂ");}}else if(k.KKM(e,16400,68)) {if(1){r=m=1;k.KO(0,t,"דּ");}}else if(k.KKM(e,16400,69)) {if(1){r=m=1;k.KO(0,t,"ֵ");}}else if(k.KKM(e,16400,70)) {if(1){r=m=1;k.KO(0,t,"טּ");}}else if(k.KKM(e,16400,71)) {if(1){r=m=1;k.KO(0,t,"גּ");}}else if(k.KKM(e,16400,72)) {if(1){r=m=1;k.KO(0,t,"הּ");}}else if(k.KKM(e,16400,73)) {if(1){r=m=1;}}else if(k.KKM(e,16400,74)) {if(1){r=m=1;k.KO(0,t,"וַיּ");}}else if(k.KKM(e,16400,75)) {if(1){r=m=1;k.KO(0,t,"כּ");}}else if(k.KKM(e,16400,76)) {if(1){r=m=1;k.KO(0,t,"לּ");}}else if(k.KKM(e,16400,77)) {if(1){r=m=1;k.KO(0,t,"מּ");}}else if(k.KKM(e,16400,78)) {if(1){r=m=1;k.KO(0,t,"נּ");}}else if(k.KKM(e,16400,79)) {if(1){r=m=1;k.KO(0,t,"וֹ");}}else if(k.KKM(e,16400,80)) {if(1){r=m=1;k.KO(0,t,"פּ");}}else if(k.KKM(e,16400,81)) {if(1){r=m=1;k.KO(0,t,"קּ");}}else if(k.KKM(e,16400,82)) {if(1){r=m=1;k.KO(0,t,"רּ");}}else if(k.KKM(e,16400,83)) {if(1){r=m=1;k.KO(0,t,"סּ");}}else if(k.KKM(e,16400,84)) {if(1){r=m=1;k.KO(0,t,"תּ");}}else if(k.KKM(e,16400,85)) {if(1){r=m=1;k.KO(0,t,"ֻ");}}else if(k.KKM(e,16400,86)) {if(1){r=m=1;k.KO(0,t,"שּׁ");}}else if(k.KKM(e,16400,87)) {if(1){r=m=1;k.KO(0,t,"וּ");}}else if(k.KKM(e,16400,88)) {if(1){r=m=1;k.KO(0,t,"צּ");}}else if(k.KKM(e,16400,89)) {if(1){r=m=1;k.KO(0,t,"יּ");}}else if(k.KKM(e,16400,90)) {if(1){r=m=1;k.KO(0,t,"זּ");}}else if(k.KKM(e,16384,219)) {if(1){r=m=1;k.KO(0,t,"[");}}else if(k.KKM(e,16448,219)) {if(1){r=m=1;k.KO(0,t,"֔");}}else if(k.KKM(e,16384,220)) {if(1){r=m=1;k.KO(0,t,"\\");}}else if(k.KKM(e,16448,220)) {if(1){r=m=1;k.KO(0,t,"֫");}}else if(k.KKM(e,16384,221)) {if(1){r=m=1;k.KO(0,t,"]");}}else if(k.KKM(e,16448,221)) {if(1){r=m=1;k.KO(0,t,"֒");}}else if(k.KKM(e,16400,54)) {if(1){r=m=1;k.KO(0,t,"֔");}}else if(k.KKM(e,16400,189)) {if(1){r=m=1;k.KO(0,t,"_");}}else if(k.KKM(e,16448,192)) {if(1){r=m=1;}}else if(k.KKM(e,16384,192)) {if(1){r=m=1;k.KO(0,t,"֑");}}else if(k.KKM(e,16448,65)) {if(1){r=m=1;k.KO(0,t,"ֲ");}}else if(k.KKM(e,16384,65)) {if(1){r=m=1;k.KO(0,t,"ַ");}}else if(k.KKM(e,16448,66)) {if(1){r=m=1;}}else if(k.KKM(e,16384,66)) {if(1){r=m=1;k.KO(0,t,"ב");}}else if(k.KKM(e,16448,67)) {if(1){r=m=1;}}else if(k.KKM(e,16384,67)) {if(1){r=m=1;k.KO(0,t,"שׂ");}}else if(k.KKM(e,16448,68)) {if(1){r=m=1;}}else if(k.KKM(e,16384,68)) {if(1){r=m=1;k.KO(0,t,"ד");}}else if(k.KKM(e,16448,69)) {if(1){r=m=1;k.KO(0,t,"ֱ");}}if(m) {}else if(k.KKM(e,16384,69)) {if(1){r=m=1;k.KO(0,t,"ֶ");}}else if(k.KKM(e,16448,70)) {if(1){r=m=1;}}else if(k.KKM(e,16384,70)) {if(1){r=m=1;k.KO(0,t,"ט");}}else if(k.KKM(e,16448,71)) {if(1){r=m=1;k.KO(0,t,"•");}}else if(k.KKM(e,16384,71)) {if(1){r=m=1;k.KO(0,t,"ג");}}else if(k.KKM(e,16448,72)) {if(1){r=m=1;k.KO(0,t,"̊");}}else if(k.KKM(e,16384,72)) {if(1){r=m=1;k.KO(0,t,"ה");}}else if(k.KKM(e,16448,73)) {if(1){r=m=1;k.KO(0,t,"ֽ");}}else if(k.KKM(e,16384,73)) {if(1){r=m=1;k.KO(0,t,"ִ");}}else if(k.KKM(e,16448,74)) {if(1){r=m=1;}}else if(k.KKM(e,16384,74)) {if(1){r=m=1;k.KO(0,t,"ח");}}else if(k.KKM(e,16448,75)) {if(1){r=m=1;k.KO(0,t,"ך");}}else if(k.KKM(e,16384,75)) {if(1){r=m=1;k.KO(0,t,"כ");}}else if(k.KKM(e,16448,76)) {if(1){r=m=1;}}else if(k.KKM(e,16384,76)) {if(1){r=m=1;k.KO(0,t,"ל");}}else if(k.KKM(e,16448,77)) {if(1){r=m=1;k.KO(0,t,"ם");}}else if(k.KKM(e,16384,77)) {if(1){r=m=1;k.KO(0,t,"מ");}}else if(k.KKM(e,16448,78)) {if(1){r=m=1;k.KO(0,t,"ן");}}else if(k.KKM(e,16384,78)) {if(1){r=m=1;k.KO(0,t,"נ");}}else if(k.KKM(e,16448,79)) {if(1){r=m=1;k.KO(0,t,"ֳ");}}else if(k.KKM(e,16384,79)) {if(1){r=m=1;k.KO(0,t,"ֹ");}}else if(k.KKM(e,16448,80)) {if(1){r=m=1;k.KO(0,t,"ף");}}else if(k.KKM(e,16384,80)) {if(1){r=m=1;k.KO(0,t,"פ");}}else if(k.KKM(e,16448,81)) {if(1){r=m=1;}}else if(k.KKM(e,16384,81)) {if(1){r=m=1;k.KO(0,t,"ק");}}else if(k.KKM(e,16448,82)) {if(1){r=m=1;}}else if(k.KKM(e,16384,82)) {if(1){r=m=1;k.KO(0,t,"ר");}}else if(k.KKM(e,16448,83)) {if(1){r=m=1;}}else if(k.KKM(e,16384,83)) {if(1){r=m=1;k.KO(0,t,"ס");}}else if(k.KKM(e,16448,84)) {if(1){r=m=1;}}else if(k.KKM(e,16384,84)) {if(1){r=m=1;k.KO(0,t,"ת");}}else if(k.KKM(e,16448,85)) {if(1){r=m=1;}}else if(k.KKM(e,16384,85)) {if(1){r=m=1;k.KO(0,t,"ע");}}else if(k.KKM(e,16448,86)) {if(1){r=m=1;}}else if(k.KKM(e,16384,86)) {if(1){r=m=1;k.KO(0,t,"שׁ");}}else if(k.KKM(e,16448,87)) {if(1){r=m=1;}}else if(k.KKM(e,16384,87)) {if(1){r=m=1;k.KO(0,t,"ו");}}else if(k.KKM(e,16448,88)) {if(1){r=m=1;k.KO(0,t,"ץ");}}else if(k.KKM(e,16384,88)) {if(1){r=m=1;k.KO(0,t,"צ");}}else if(k.KKM(e,16448,89)) {if(1){r=m=1;}}else if(k.KKM(e,16384,89)) {if(1){r=m=1;k.KO(0,t,"י");}}else if(k.KKM(e,16448,90)) {if(1){r=m=1;}}else if(k.KKM(e,16384,90)) {if(1){r=m=1;k.KO(0,t,"ז");}}else if(k.KKM(e,16400,219)) {if(1){r=m=1;k.KO(0,t,"{");}}else if(k.KKM(e,16400,220)) {if(1){r=m=1;k.KO(0,t,"|");}}else if(k.KKM(e,16400,221)) {if(1){r=m=1;k.KO(0,t,"}");}}else if(k.KKM(e,16400,192)) {if(1){r=m=1;}}return r;};} \ No newline at end of file diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmn b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmn new file mode 100644 index 0000000000..8ab7ab2901 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmn @@ -0,0 +1,215 @@ +store(&TARGETS) 'any' +store(&NAME) 'Aleph With Beth' +store(&BITMAP) 'alephwithbeth.ico' +store(&VISUALKEYBOARD) 'alephwithbeth.kvks' + +store(letters-with-final-forms) "כמנפצ" +store(sofit-forms-of-letters) "ךםןףץ" +store(presentation-dageshed-letters-with-final-forms) "כּפּ" +store(presentation-dageshed-sofit-forms-of-letters) "ךּףּ" +store(dagesh) 'ּ' +c store(vowels) "ְֱֲֳִֵֶַׇׇָֹֻׅ" + +store(normal-dageshed-letters-with-final-forms) "מּנּצּ" +store(normal-undageshed-sofit-forms-of-letters) "םןץ" + +store(punctuation) U+0591 .. U+05AF U+05BD .. U+05BF U+05C0 U+05C4 +store(whitespace) [K_SPACE] [K_ENTER] [K_TAB] + +store(&KMW_RTL) '1' +store(&LAYOUTFILE) 'other\alephwithbeth.keyman-touch-layout' +store(©RIGHT) '© Cameron Briggs' +store(&KEYBOARDVERSION) '1.0.6' +store(&KMW_HELPTEXT) 'Aleph With Beth' + +begin Unicode > use(main) + +group(main) using keys ++ [SHIFT K_I] > nul ++ [ALT K_BKQUOTE] > nul ++ [ALT K_B] > nul ++ [ALT K_V] > nul ++ [ALT K_C] > nul ++ [ALT K_Z] > nul ++ [ALT K_L] > nul ++ [ALT K_J] > nul ++ [ALT K_F] > nul ++ [ALT K_D] > nul ++ [ALT K_S] > nul ++ [ALT K_U] > nul ++ [ALT K_Y] > nul ++ [ALT K_T] > nul ++ [ALT K_R] > nul ++ [ALT K_W] > nul ++ [ALT K_Q] > nul ++ [SHIFT K_BKSLASH] > '|' ++ [SHIFT K_EQUAL] > '+' ++ [SHIFT K_HYPHEN] > '_' ++ [SHIFT K_0] > ')' ++ [SHIFT K_9] > '(' ++ [SHIFT K_SLASH] > '?' ++ [SHIFT K_QUOTE] > '"' ++ [SHIFT K_BKQUOTE] > nul ++ [K_EQUAL] > '=' ++ [K_HYPHEN] > '־' ++ [K_0] > '0' ++ [K_9] > '9' ++ [K_8] > '8' ++ [K_7] > '7' ++ [K_6] > '6' ++ [K_5] > '5' ++ [K_4] > '4' ++ [K_3] > '3' ++ [K_2] > '2' ++ [K_1] > '1' ++ [K_PERIOD] > '.' ++ [K_SLASH] > '/' ++ [K_QUOTE] > U+0027 ++ [SHIFT K_COLON] > '״' ++ [ALT K_A] > 'ֲ' ++ [SHIFT K_RBRKT] > '}' ++ [SHIFT K_LBRKT] > '{' ++ [K_BKSLASH] > '\' ++ [K_RBRKT] > ']' ++ [K_LBRKT] > '[' ++ [SHIFT K_R] > 'רּ' ++ [SHIFT CTRL K_SPACE] > ' ' ++ [SHIFT K_SPACE] > ' ' ++ [K_SPACE] > ' ' ++ [ALT K_SPACE] > ' ' ++ [SHIFT K_8] > '֝' ++ [SHIFT K_7] > '֬' ++ [SHIFT K_6] > '֔' ++ [SHIFT K_5] > '֞' ++ [SHIFT K_4] > '֜' ++ [SHIFT K_3] > '֨' ++ [SHIFT K_2] > '֘' ++ [SHIFT K_1] > '!' ++ [ALT K_SLASH] > '״' ++ [ALT K_PERIOD] > '̈' ++ [ALT K_COMMA] > '֫' ++ [ALT K_QUOTE] > 'ׄ' ++ [ALT K_COLON] > '׃' ++ [ALT K_H] > '̊' ++ [ALT K_G] > '•' ++ [ALT K_BKSLASH] > '֫' ++ [ALT K_RBRKT] > '֒' ++ [ALT K_LBRKT] > '֔' ++ [ALT K_I] > 'ֽ' ++ [ALT K_O] > 'ֳ' ++ [ALT K_E] > 'ֱ' ++ [ALT K_EQUAL] > '◌' ++ [ALT K_HYPHEN] > 'ֿ' ++ [ALT K_0] > '֯' ++ [ALT K_9] > '֓' ++ [ALT K_8] > '֕' ++ [ALT K_7] > '֡' ++ [ALT K_6] > '֟' ++ [ALT K_5] > '֩' ++ [ALT K_4] > '֠' ++ [ALT K_3] > '֙' ++ [ALT K_2] > '֮' ++ [ALT K_1] > '֗' ++ [ALT K_N] > 'ן' ++ [ALT K_P] > 'ף' ++ [ALT K_K] > 'ך' ++ [ALT K_M] > 'ם' ++ [ALT K_X] > 'ץ' ++ [SHIFT K_PERIOD] > 'ּ' ++ [SHIFT K_COMMA] > nul ++ [SHIFT K_M] > 'מּ' ++ [SHIFT K_N] > 'נּ' ++ [SHIFT K_B] > 'בּ' ++ [SHIFT K_V] > 'שּׁ' ++ [SHIFT K_C] > 'שּׂ' ++ [SHIFT K_X] > 'צּ' ++ [SHIFT K_Z] > 'זּ' ++ [SHIFT K_L] > 'לּ' ++ [SHIFT K_K] > 'כּ' ++ [SHIFT K_J] > 'וַיּ' ++ [SHIFT K_H] > 'הּ' ++ [SHIFT K_G] > 'גּ' ++ [SHIFT K_F] > 'טּ' ++ [SHIFT K_D] > 'דּ' ++ [SHIFT K_S] > 'סּ' ++ [SHIFT K_A] > 'ָ' ++ [SHIFT K_P] > 'פּ' ++ [SHIFT K_O] > 'וֹ' ++ [SHIFT K_U] > 'ֻ' ++ [SHIFT K_Y] > 'יּ' ++ [SHIFT K_T] > 'תּ' ++ [SHIFT K_E] > 'ֵ' ++ [SHIFT K_W] > 'וּ' ++ [SHIFT K_Q] > 'קּ' ++ [K_BKQUOTE] > '֑' ++ [K_COMMA] > 'א' ++ [K_M] > 'מ' ++ [K_N] > 'נ' ++ [K_B] > 'ב' ++ [K_V] > 'שׁ' ++ [K_C] > 'שׂ' ++ [K_X] > 'צ' ++ [K_Z] > 'ז' ++ [K_COLON] > 'ְ' ++ [K_L] > 'ל' ++ [K_K] > 'כ' ++ [K_J] > 'ח' ++ [K_H] > 'ה' ++ [K_G] > 'ג' ++ [K_F] > 'ט' ++ [K_D] > 'ד' ++ [K_S] > 'ס' ++ [K_A] > 'ַ' ++ [K_P] > 'פ' ++ [K_O] > 'ֹ' ++ [K_I] > 'ִ' ++ [K_U] > 'ע' ++ [K_Y] > 'י' ++ [K_T] > 'ת' ++ [K_R] > 'ר' ++ [K_E] > 'ֶ' ++ [K_W] > 'ו' ++ [K_Q] > 'ק' +c Transform final letters +any(letters-with-final-forms) + any(whitespace) > index(sofit-forms-of-letters,1) + +c Transform final letters with a dagesh +any(letters-with-final-forms) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh +any(presentation-dageshed-letters-with-final-forms) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) +any(normal-dageshed-letters-with-final-forms) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh + +c Transform final letters with a vowel +c any(letters-with-final-forms) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) + +c Transform final letters with a vowel and a dagesh +c any(letters-with-final-forms) any(vowels) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) $dagesh +c any(letters-with-final-forms) $dagesh any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(vowels,3) +c any(presentation-dageshed-letters-with-final-forms) any(vowels) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(vowels,2) +c any(normal-dageshed-letters-with-final-forms) any(vowels) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(vowels,2) + +c Transform final letters with punctuation +any(letters-with-final-forms) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) + +c Transform final letters with punctuation and a dagesh +any(letters-with-final-forms) any(punctuation) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) $dagesh +any(letters-with-final-forms) $dagesh any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(punctuation,3) +any(presentation-dageshed-letters-with-final-forms) any(punctuation) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(punctuation,2) +any(normal-dageshed-letters-with-final-forms) any(punctuation) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(punctuation,2) + +c Transfrom final letters with a punctuation and a vowel +c any(letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3) +c any(letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3) + +c Transform final letters with punctuation, a vowel, and a dagesh (phew!) +c any(letters-with-final-forms) any(punctuation) any(vowels) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3) $dagesh +c any(letters-with-final-forms) any(punctuation) $dagesh any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) index(punctuation,2) $dagesh index(vowels,4) +c any(letters-with-final-forms) any(vowels) any(punctuation) $dagesh + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3) $dagesh +c any(letters-with-final-forms) any(vowels) $dagesh any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) index(vowels,2) $dagesh index(punctuation,4) +c any(letters-with-final-forms) $dagesh any(punctuation) any(vowels) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(punctuation,3) index(vowels,4) +c any(letters-with-final-forms) $dagesh any(vowels) any(punctuation) + any(whitespace) > index(sofit-forms-of-letters,1) $dagesh index(vowels,3) index(punctuation,4) + + +c any(presentation-dageshed-letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(punctuation,2) index(vowels,3) +c any(presentation-dageshed-letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(presentation-dageshed-sofit-forms-of-letters,1) index(vowels,2) index(punctuation,3) +c any(normal-dageshed-letters-with-final-forms) any(punctuation) any(vowels) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(punctuation,2) index(vowels,3) +c any(normal-dageshed-letters-with-final-forms) any(vowels) any(punctuation) + any(whitespace) > index(normal-undageshed-sofit-forms-of-letters,1) $dagesh index(vowels,2) index(punctuation,3) diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmp b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmp new file mode 100644 index 0000000000..d44bf1edca Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmp differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmx b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmx new file mode 100644 index 0000000000..867213cedd Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kmx differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kpj b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kpj new file mode 100644 index 0000000000..03565f5141 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kpj @@ -0,0 +1,133 @@ + + + + $PROJECTPATH + True + True + + + + id_ede98e4633e239f933cbfd1f4e1b766c + HISTORY.md + HISTORY.md + + .md + + + id_53e892b8b41cc4caece1cfd5ef21d6e7 + LICENSE.md + LICENSE.md + + .md + + + id_0730bb7c2e8f9ea2438b52e419dd86c9 + README.md + README.md + + .md + + + id_195752eb7ec06cfb1f90ed508d2c5b54 + alephwithbeth.kps + alephwithbeth.kps + + .kps +
+ Aleph With Beth + © Cameron Briggs +
+
+ + id_e72bb7d9956f6973ce3de0362e47b0d3 + alephwithbeth.kmn + alephwithbeth.kmn + 1.0.6 + .kmn +
+ Aleph With Beth + © Cameron Briggs +
+
+ + id_dc799c273ed24ff1ca06a2429e5af8b4 + alephwithbeth.ico + alephwithbeth.ico + + .ico + id_e72bb7d9956f6973ce3de0362e47b0d3 + + + id_8da344c4cea6f467013357fe099006f5 + readme.htm + other\readme.htm + + .htm + id_195752eb7ec06cfb1f90ed508d2c5b54 + + + id_724e5b4c63f10bc0abf7077f7c3172fc + welcome.htm + other\welcome\welcome.htm + + .htm + id_195752eb7ec06cfb1f90ed508d2c5b54 + + + id_101067b24e270db6fdd8b52ac6c77297 + alephwithbeth.js + alephwithbeth.js + + .js + id_195752eb7ec06cfb1f90ed508d2c5b54 + + + id_cf57b9fb0e29a5e6b4ff4b9a55fc7ef3 + alephwithbeth.kvk + alephwithbeth.kvk + + .kvk + id_195752eb7ec06cfb1f90ed508d2c5b54 + + + id_dfa1b7bb444da6106e7ffcce1b6ae59d + alephwithbeth.kmx + alephwithbeth.kmx + + .kmx + id_195752eb7ec06cfb1f90ed508d2c5b54 + + + id_92d25e21c1bb82cab07440032ea15ca1 + KeyboardLayout.png + other\welcome\KeyboardLayout.png + + .png + id_195752eb7ec06cfb1f90ed508d2c5b54 + + + id_a751525f488287b36c872b48e3c385b8 + KeyboardLayoutAlt.png + other\welcome\KeyboardLayoutAlt.png + + .png + id_195752eb7ec06cfb1f90ed508d2c5b54 + + + id_a260e1bca7e99f6955c706b3697f842c + KeyboardLayoutShift.png + other\welcome\KeyboardLayoutShift.png + + .png + id_195752eb7ec06cfb1f90ed508d2c5b54 + + + id_c296b5df3dc126e219e0173fcbc1baec + LICENSE.md + LICENSE.md + + .md + id_195752eb7ec06cfb1f90ed508d2c5b54 + +
+
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kps b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kps new file mode 100644 index 0000000000..93a49e397e --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kps @@ -0,0 +1,95 @@ + + + + 17.0.325.0 + 7.0 + + + + other\readme.htm + LICENSE.md + other\welcome\welcome.htm + + + + + + + + + + + Aleph With Beth + © Cameron Briggs + Cameron Briggs + https://freehebrew.online + alephwithbeth keyboard + + + + other\readme.htm + File readme.htm + 0 + .htm + + + other\welcome\welcome.htm + File welcome.htm + 0 + .htm + + + alephwithbeth.js + File alephwithbeth.js + 0 + .js + + + alephwithbeth.kvk + File alephwithbeth.kvk + 0 + .kvk + + + alephwithbeth.kmx + Keyboard alephwithbeth + 0 + .kmx + + + other\welcome\KeyboardLayout.png + File KeyboardLayout.png + 0 + .png + + + other\welcome\KeyboardLayoutAlt.png + File KeyboardLayoutAlt.png + 0 + .png + + + other\welcome\KeyboardLayoutShift.png + File KeyboardLayoutShift.png + 0 + .png + + + LICENSE.md + + 0 + .md + + + + + Aleph With Beth + alephwithbeth + 1.0.4 + + Hebrew + + + + + diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvk b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvk new file mode 100644 index 0000000000..7772d84d9e Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvk differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvks b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvks new file mode 100644 index 0000000000..6b3cf3eeb9 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/alephwithbeth.kvks @@ -0,0 +1,138 @@ + + +
+ 10.0 + alephwithbeth + +
+ + + ! + " + ֨ + ֜ + ֞ + ֬ + ( + ) + ֝ + + + ״ + ּ + ? + ֘ + ָ + בּ + שּׂ + דּ + ֵ + טּ + גּ + הּ + וַיּ + כּ + לּ + מּ + נּ + וֹ + פּ + קּ + רּ + סּ + תּ + ֻ + שּׁ + וּ + צּ + יּ + זּ + ֔ + _ + { + | + } + + + ' + א + . + / + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + ְ + = + [ + \ + ] + ֑ + ַ + ב + שׂ + ד + ֶ + ט + ג + ה + ִ + ח + כ + ל + מ + נ + ֹ + פ + ק + ר + ס + ת + ע + שׁ + ו + צ + י + ז + ־ + 0 + + + ׄ + ֫ + ֿ + ̈ + ״ + ֯ + ֗ + ֮ + ֙ + ֠ + ֩ + ֟ + ֡ + ֕ + ֓ + ׃ + ◌ + ֔ + ֫ + ֒ + ֲ + ֱ + • + ̊ + ֽ + ך + ם + ן + ֳ + ף + ץ + + +
diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/alephwithbeth.keyman-touch-layout b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/alephwithbeth.keyman-touch-layout new file mode 100644 index 0000000000..05db57c7f9 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/alephwithbeth.keyman-touch-layout @@ -0,0 +1,1646 @@ +{ + "tablet": { + "displayUnderlying": false, + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + }, + { + "id": "K_HYPHEN", + "text": "־" + }, + { + "id": "K_EQUAL", + "text": "=" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "ק", + "pad": "75" + }, + { + "id": "K_W", + "text": "ו" + }, + { + "id": "K_E", + "text": "ֶ" + }, + { + "id": "K_R", + "text": "ר" + }, + { + "id": "K_T", + "text": "ת" + }, + { + "id": "K_Y", + "text": "י" + }, + { + "id": "K_U", + "text": "ע" + }, + { + "id": "K_I", + "text": "ִ" + }, + { + "id": "K_O", + "text": "ֹ" + }, + { + "id": "K_P", + "text": "פ" + }, + { + "id": "K_LBRKT", + "text": "[" + }, + { + "id": "K_RBRKT", + "text": "]" + }, + { + "id": "T_new_138", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "֑" + }, + { + "id": "K_A", + "text": "ַ" + }, + { + "id": "K_S", + "text": "ס" + }, + { + "id": "K_D", + "text": "ד" + }, + { + "id": "K_F", + "text": "ט" + }, + { + "id": "K_G", + "text": "ג" + }, + { + "id": "K_H", + "text": "ה" + }, + { + "id": "K_J", + "text": "ח" + }, + { + "id": "K_K", + "text": "כ" + }, + { + "id": "K_L", + "text": "ל" + }, + { + "id": "K_COLON", + "text": "ְ" + }, + { + "id": "K_QUOTE", + "text": "'" + }, + { + "id": "K_BKSLASH", + "text": "\\" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "ז" + }, + { + "id": "K_X", + "text": "צ" + }, + { + "id": "K_C", + "text": "שׂ" + }, + { + "id": "K_V", + "text": "שׁ" + }, + { + "id": "K_B", + "text": "ב" + }, + { + "id": "K_N", + "text": "נ" + }, + { + "id": "K_M", + "text": "מ" + }, + { + "id": "K_COMMA", + "text": "א" + }, + { + "id": "K_PERIOD", + "text": "." + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "id": "T_new_164", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "alt", + "width": "130", + "sp": "1", + "nextlayer": "alt" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "!" + }, + { + "id": "K_2", + "text": "֘" + }, + { + "id": "K_3", + "text": "֨" + }, + { + "id": "K_4", + "text": "֜" + }, + { + "id": "K_5", + "text": "֞" + }, + { + "id": "K_6", + "text": "֔" + }, + { + "id": "K_7", + "text": "֬" + }, + { + "id": "K_8", + "text": "֝" + }, + { + "id": "K_9", + "text": "(" + }, + { + "id": "K_0", + "text": ")" + }, + { + "id": "K_HYPHEN", + "text": "_" + }, + { + "id": "K_EQUAL", + "text": "+" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "קּ", + "pad": "75" + }, + { + "id": "K_W", + "text": "וּ" + }, + { + "id": "K_E", + "text": "ֵ" + }, + { + "id": "K_R", + "text": "רּ" + }, + { + "id": "K_T", + "text": "תּ" + }, + { + "id": "K_Y", + "text": "יּ" + }, + { + "id": "K_U", + "text": "ֻ" + }, + { + "id": "K_I", + "text": "" + }, + { + "id": "K_O", + "text": "וֹ" + }, + { + "id": "K_P", + "text": "פּ" + }, + { + "id": "K_LBRKT", + "text": "{" + }, + { + "id": "K_RBRKT", + "text": "}" + }, + { + "id": "T_new_362", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "" + }, + { + "id": "K_A", + "text": "ָ" + }, + { + "id": "K_S", + "text": "סּ" + }, + { + "id": "K_D", + "text": "דּ" + }, + { + "id": "K_F", + "text": "טּ" + }, + { + "id": "K_G", + "text": "גּ" + }, + { + "id": "K_H", + "text": "הּ" + }, + { + "id": "K_J", + "text": "וַיּ" + }, + { + "id": "K_K", + "text": "כּ" + }, + { + "id": "K_L", + "text": "לּ" + }, + { + "id": "K_COLON", + "text": "״" + }, + { + "id": "K_QUOTE", + "text": "\"" + }, + { + "id": "K_BKSLASH", + "text": "|" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "זּ" + }, + { + "id": "K_X", + "text": "צּ" + }, + { + "id": "K_C", + "text": "שּׂ" + }, + { + "id": "K_V", + "text": "שּׁ" + }, + { + "id": "K_B", + "text": "בּ" + }, + { + "id": "K_N", + "text": "נּ" + }, + { + "id": "K_M", + "text": "מּ" + }, + { + "id": "K_COMMA", + "text": "" + }, + { + "id": "K_PERIOD", + "text": "ּ" + }, + { + "id": "K_SLASH", + "text": "?" + }, + { + "id": "T_new_388", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "alt", + "width": "130", + "sp": "1", + "nextlayer": "alt" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + }, + { + "id": "alt", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "֗" + }, + { + "id": "K_2", + "text": "֮" + }, + { + "id": "K_3", + "text": "֙" + }, + { + "id": "K_4", + "text": "֠" + }, + { + "id": "K_5", + "text": "֩" + }, + { + "id": "K_6", + "text": "֟" + }, + { + "id": "K_7", + "text": "֡" + }, + { + "id": "K_8", + "text": "֕" + }, + { + "id": "K_9", + "text": "֓" + }, + { + "id": "K_0", + "text": "֯" + }, + { + "id": "K_HYPHEN", + "text": "ֿ" + }, + { + "id": "K_EQUAL", + "text": "◌" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "", + "pad": "75" + }, + { + "id": "K_W", + "text": "" + }, + { + "id": "K_E", + "text": "ֱ" + }, + { + "id": "K_R", + "text": "" + }, + { + "id": "K_T", + "text": "" + }, + { + "id": "K_Y", + "text": "" + }, + { + "id": "K_U", + "text": "" + }, + { + "id": "K_I", + "text": "ֽ" + }, + { + "id": "K_O", + "text": "ֳ" + }, + { + "id": "K_P", + "text": "ף" + }, + { + "id": "K_LBRKT", + "text": "֔" + }, + { + "id": "K_RBRKT", + "text": "֒" + }, + { + "id": "T_new_138", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "" + }, + { + "id": "K_A", + "text": "ֲ" + }, + { + "id": "K_S", + "text": "" + }, + { + "id": "K_D", + "text": "" + }, + { + "id": "K_F", + "text": "" + }, + { + "id": "K_G", + "text": "•" + }, + { + "id": "K_H", + "text": "̊" + }, + { + "id": "K_J", + "text": "" + }, + { + "id": "K_K", + "text": "ך" + }, + { + "id": "K_L", + "text": "" + }, + { + "id": "K_COLON", + "text": "׃" + }, + { + "id": "K_QUOTE", + "text": "ׄ" + }, + { + "id": "K_BKSLASH", + "text": "֫" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift", + "layer": "shift" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "" + }, + { + "id": "K_X", + "text": "ץ" + }, + { + "id": "K_C", + "text": "" + }, + { + "id": "K_V", + "text": "" + }, + { + "id": "K_B", + "text": "" + }, + { + "id": "K_N", + "text": "ן" + }, + { + "id": "K_M", + "text": "ם" + }, + { + "id": "K_COMMA", + "text": "֫" + }, + { + "id": "K_PERIOD", + "text": "̈" + }, + { + "id": "K_SLASH", + "text": "״" + }, + { + "id": "T_new_164", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "default", + "width": "130", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + } + ] + }, + "phone": { + "displayUnderlying": false, + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + }, + { + "id": "K_HYPHEN", + "text": "־" + }, + { + "id": "K_EQUAL", + "text": "=" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "ק", + "pad": "75" + }, + { + "id": "K_W", + "text": "ו" + }, + { + "id": "K_E", + "text": "ֶ" + }, + { + "id": "K_R", + "text": "ר" + }, + { + "id": "K_T", + "text": "ת" + }, + { + "id": "K_Y", + "text": "י" + }, + { + "id": "K_U", + "text": "ע" + }, + { + "id": "K_I", + "text": "ִ" + }, + { + "id": "K_O", + "text": "ֹ" + }, + { + "id": "K_P", + "text": "פ" + }, + { + "id": "K_LBRKT", + "text": "[" + }, + { + "id": "K_RBRKT", + "text": "]" + }, + { + "id": "T_new_138", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "֑" + }, + { + "id": "K_A", + "text": "ַ" + }, + { + "id": "K_S", + "text": "ס" + }, + { + "id": "K_D", + "text": "ד" + }, + { + "id": "K_F", + "text": "ט" + }, + { + "id": "K_G", + "text": "ג" + }, + { + "id": "K_H", + "text": "ה" + }, + { + "id": "K_J", + "text": "ח" + }, + { + "id": "K_K", + "text": "כ" + }, + { + "id": "K_L", + "text": "ל" + }, + { + "id": "K_COLON", + "text": "ְ" + }, + { + "id": "K_QUOTE", + "text": "'" + }, + { + "id": "K_BKSLASH", + "text": "\\" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "ז" + }, + { + "id": "K_X", + "text": "צ" + }, + { + "id": "K_C", + "text": "שׂ" + }, + { + "id": "K_V", + "text": "שׁ" + }, + { + "id": "K_B", + "text": "ב" + }, + { + "id": "K_N", + "text": "נ" + }, + { + "id": "K_M", + "text": "מ" + }, + { + "id": "K_COMMA", + "text": "א" + }, + { + "id": "K_PERIOD", + "text": "." + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "id": "T_new_164", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "alt", + "width": "130", + "sp": "1", + "nextlayer": "alt" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "!" + }, + { + "id": "K_2", + "text": "֘" + }, + { + "id": "K_3", + "text": "֨" + }, + { + "id": "K_4", + "text": "֜" + }, + { + "id": "K_5", + "text": "֞" + }, + { + "id": "K_6", + "text": "֔" + }, + { + "id": "K_7", + "text": "֬" + }, + { + "id": "K_8", + "text": "֝" + }, + { + "id": "K_9", + "text": "(" + }, + { + "id": "K_0", + "text": ")" + }, + { + "id": "K_HYPHEN", + "text": "_" + }, + { + "id": "K_EQUAL", + "text": "+" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "קּ", + "pad": "75" + }, + { + "id": "K_W", + "text": "וּ" + }, + { + "id": "K_E", + "text": "ֵ" + }, + { + "id": "K_R", + "text": "רּ" + }, + { + "id": "K_T", + "text": "תּ" + }, + { + "id": "K_Y", + "text": "יּ" + }, + { + "id": "K_U", + "text": "ֻ" + }, + { + "id": "K_I", + "text": "" + }, + { + "id": "K_O", + "text": "וֹ" + }, + { + "id": "K_P", + "text": "פּ" + }, + { + "id": "K_LBRKT", + "text": "{" + }, + { + "id": "K_RBRKT", + "text": "}" + }, + { + "id": "T_new_362", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "" + }, + { + "id": "K_A", + "text": "ָ" + }, + { + "id": "K_S", + "text": "סּ" + }, + { + "id": "K_D", + "text": "דּ" + }, + { + "id": "K_F", + "text": "טּ" + }, + { + "id": "K_G", + "text": "גּ" + }, + { + "id": "K_H", + "text": "הּ" + }, + { + "id": "K_J", + "text": "וַיּ" + }, + { + "id": "K_K", + "text": "כּ" + }, + { + "id": "K_L", + "text": "לּ" + }, + { + "id": "K_COLON", + "text": "״" + }, + { + "id": "K_QUOTE", + "text": "\"" + }, + { + "id": "K_BKSLASH", + "text": "|" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "זּ" + }, + { + "id": "K_X", + "text": "צּ" + }, + { + "id": "K_C", + "text": "שּׂ" + }, + { + "id": "K_V", + "text": "שּׁ" + }, + { + "id": "K_B", + "text": "בּ" + }, + { + "id": "K_N", + "text": "נּ" + }, + { + "id": "K_M", + "text": "מּ" + }, + { + "id": "K_COMMA", + "text": "" + }, + { + "id": "K_PERIOD", + "text": "ּ" + }, + { + "id": "K_SLASH", + "text": "?" + }, + { + "id": "T_new_388", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "alt", + "width": "130", + "sp": "1", + "nextlayer": "alt" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + }, + { + "id": "alt", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "֗" + }, + { + "id": "K_2", + "text": "֮" + }, + { + "id": "K_3", + "text": "֙" + }, + { + "id": "K_4", + "text": "֠" + }, + { + "id": "K_5", + "text": "֩" + }, + { + "id": "K_6", + "text": "֟" + }, + { + "id": "K_7", + "text": "֡" + }, + { + "id": "K_8", + "text": "֕" + }, + { + "id": "K_9", + "text": "֓" + }, + { + "id": "K_0", + "text": "֯" + }, + { + "id": "K_HYPHEN", + "text": "ֿ" + }, + { + "id": "K_EQUAL", + "text": "◌" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "", + "pad": "75" + }, + { + "id": "K_W", + "text": "" + }, + { + "id": "K_E", + "text": "ֱ" + }, + { + "id": "K_R", + "text": "" + }, + { + "id": "K_T", + "text": "" + }, + { + "id": "K_Y", + "text": "" + }, + { + "id": "K_U", + "text": "" + }, + { + "id": "K_I", + "text": "ֽ" + }, + { + "id": "K_O", + "text": "ֳ" + }, + { + "id": "K_P", + "text": "ף" + }, + { + "id": "K_LBRKT", + "text": "֔" + }, + { + "id": "K_RBRKT", + "text": "֒" + }, + { + "id": "T_new_138", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "" + }, + { + "id": "K_A", + "text": "ֲ" + }, + { + "id": "K_S", + "text": "" + }, + { + "id": "K_D", + "text": "" + }, + { + "id": "K_F", + "text": "" + }, + { + "id": "K_G", + "text": "•" + }, + { + "id": "K_H", + "text": "̊" + }, + { + "id": "K_J", + "text": "" + }, + { + "id": "K_K", + "text": "ך" + }, + { + "id": "K_L", + "text": "" + }, + { + "id": "K_COLON", + "text": "׃" + }, + { + "id": "K_QUOTE", + "text": "ׄ" + }, + { + "id": "K_BKSLASH", + "text": "֫" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "160", + "sp": "1", + "nextlayer": "shift", + "layer": "shift" + }, + { + "id": "K_oE2", + "text": "" + }, + { + "id": "K_Z", + "text": "" + }, + { + "id": "K_X", + "text": "ץ" + }, + { + "id": "K_C", + "text": "" + }, + { + "id": "K_V", + "text": "" + }, + { + "id": "K_B", + "text": "" + }, + { + "id": "K_N", + "text": "ן" + }, + { + "id": "K_M", + "text": "ם" + }, + { + "id": "K_COMMA", + "text": "֫" + }, + { + "id": "K_PERIOD", + "text": "̈" + }, + { + "id": "K_SLASH", + "text": "״" + }, + { + "id": "T_new_164", + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LCONTROL", + "text": "default", + "width": "130", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "140", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "930" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "145", + "sp": "1" + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayout.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayout.png new file mode 100644 index 0000000000..d0998e288b Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayout.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutAlt.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutAlt.png new file mode 100644 index 0000000000..f740eec1e3 Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutAlt.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutShift.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutShift.png new file mode 100644 index 0000000000..9b243d4dd2 Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/KeyboardLayoutShift.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/alephwithbeth.php b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/alephwithbeth.php new file mode 100644 index 0000000000..48a2c2e7c8 --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/help/alephwithbeth.php @@ -0,0 +1,22 @@ + + + +

Start Using Aleph with Beth

+ +

Desktop Layout

+ +

Base layout

+Keyboard layout +

Layout when shift is pressed

+Keyboard layout (when shift key is pressed) +

Layout when alt is pressed

+Keyboard layout (when alt key is pressed) + diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/readme.htm b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/readme.htm new file mode 100644 index 0000000000..f187388c1e --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/readme.htm @@ -0,0 +1,22 @@ + + + + + + alephwithbeth + + + + +

Aleph With Beth

+ +

A Hebrew keyboard designed to intuitively match the standard QWERTY layout.

+ +

© Cameron Briggs

+ + + diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayout.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayout.png new file mode 100644 index 0000000000..d0998e288b Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayout.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutAlt.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutAlt.png new file mode 100644 index 0000000000..f740eec1e3 Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutAlt.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutShift.png b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutShift.png new file mode 100644 index 0000000000..9b243d4dd2 Binary files /dev/null and b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/KeyboardLayoutShift.png differ diff --git a/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/welcome.htm b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/welcome.htm new file mode 100644 index 0000000000..717e7bedeb --- /dev/null +++ b/developer/src/kmc-copy/test/fixtures/projects/disorganized/other/welcome/welcome.htm @@ -0,0 +1,28 @@ + + + + + + Start Using alephwithbeth + + + + +

Start Using Aleph With Beth

+ +

Keyboard Layouts

+ +

Base layout

+Keyboard layout +

Layout when shift is pressed

+Keyboard layout (when shift key is pressed) +

Layout when alt is pressed

+Keyboard layout (when alt key is pressed) + + + diff --git a/developer/src/kmc-copy/test/messages.tests.ts b/developer/src/kmc-copy/test/messages.tests.ts deleted file mode 100644 index aca98c3ea7..0000000000 --- a/developer/src/kmc-copy/test/messages.tests.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Keyman is copyright (C) SIL International. MIT License. - */ - -import 'mocha'; -// import * as path from 'path'; -// import * as fs from 'fs'; -// import { fileURLToPath } from 'url'; -// import { assert } from 'chai'; -import { CopierMessages } from '../src/copier-messages.js'; -import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; -import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; - -describe('CopierMessages', function () { - it('should have a valid CopierMessages object', function() { - return verifyCompilerMessagesObject(CopierMessages, CompilerErrorNamespace.Copier); - }); - - // it('should generate ERROR_OutputPathAlreadyExists if output path already exists', async function () { - // const ag = new AbstractGenerator(); - // const callbacks = new TestCompilerCallbacks(); - // const options: GeneratorOptions = { - // id: 'ERROR_OutputPathAlreadyExists', - // outPath: path.dirname(fileURLToPath(import.meta.url)), - // }; - // assert(await ag.init(callbacks, options)); - // const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'ERROR_OutputPathAlreadyExists'); - // if(!fs.existsSync(dir)) - // fs.mkdirSync(dir); - // assert.isFalse(await ag.write({})); - // assert.isTrue(callbacks.hasMessage(CopierMessages.ERROR_OutputPathAlreadyExists), - // `messageId ERROR_OutputPathAlreadyExists not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); - - // fs.rmdirSync(dir); - // }); - - // it('should generate ERROR_CannotWriteOutputFile if it cannot create a folder, e.g. invalid filename', async function () { - // const ag = new AbstractGenerator(); - // const callbacks = new TestCompilerCallbacks(); - // const options: GeneratorOptions = { - // id: 'ERROR_CannotWriteOutputFile', - // outPath: path.dirname(fileURLToPath(import.meta.url)), - // }; - // assert(await ag.init(callbacks, options)); - // assert.isFalse(await ag.write({ - // '.': {filename: '.', data: new Uint8Array([1,2,3])} - // })); - // assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_CannotWriteOutputFile), - // `messageId ERROR_CannotWriteOutputFile not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); - // }); -}); diff --git a/developer/src/kmc-generate/package.json b/developer/src/kmc-generate/package.json index a8e0810338..ba488ff575 100644 --- a/developer/src/kmc-generate/package.json +++ b/developer/src/kmc-generate/package.json @@ -54,7 +54,8 @@ "exclude-after-remap": true, "exclude": [ "test/", - "src/template/" + "src/template/", + "src/main.ts" ] }, "repository": { diff --git a/developer/src/kmc-generate/src/abstract-generator.ts b/developer/src/kmc-generate/src/abstract-generator.ts index 6cb8b1e43e..5a6ed46cc0 100644 --- a/developer/src/kmc-generate/src/abstract-generator.ts +++ b/developer/src/kmc-generate/src/abstract-generator.ts @@ -53,11 +53,14 @@ export interface GeneratorOptions /* not inheriting from CompilerBaseOptions */ * name of the author of the keyboard */ author?: string; - /** + + // TODO-GENERATE: icon support + /* * * set to true to generate an icon for a Keyman keyboard with the first * characters of the first specified BCP 47 tag */ - icon?: boolean; + // icon?: boolean; + /** * description of the keyboard, Markdown permissible */ diff --git a/developer/src/kmc-generate/src/basic-generator.ts b/developer/src/kmc-generate/src/basic-generator.ts index cb69ef5fdd..fe5a5cd304 100644 --- a/developer/src/kmc-generate/src/basic-generator.ts +++ b/developer/src/kmc-generate/src/basic-generator.ts @@ -7,6 +7,7 @@ import { KeymanTargets } from "@keymanapp/common-types"; import KEYMAN_VERSION from "@keymanapp/keyman-version"; import { AbstractGenerator, GeneratorArtifacts } from "./abstract-generator.js"; +import { GeneratorMessages } from "./generator-messages.js"; /** * @internal @@ -28,7 +29,19 @@ export class BasicGenerator extends AbstractGenerator { this.languageTags = this.options.languageTags.length ? this.options.languageTags.map(tag => new Intl.Locale(tag).minimize().toString()) : [this.DEFAULT_LOCALE]; - //TODO-GENERATE: validate targets + + // Verify that targets passed in are valid + let failed = false; + for(const target of this.options.targets) { + if(!KeymanTargets.AllKeymanTargets.includes(target)) { + this.callbacks.reportMessage(GeneratorMessages.Error_InvalidTarget({target})); + failed = true; + } + } + if(failed) { + return false; + } + if(this.options.targets.includes(KeymanTargets.KeymanTarget.any) || this.options.targets.length == 0) { this.resolvedTargets = KeymanTargets.AllKeymanTargets.filter(target => target != KeymanTargets.KeymanTarget.any); } else { @@ -50,6 +63,8 @@ export class BasicGenerator extends AbstractGenerator { dt.getDate().toString().padStart(2, '0'); this.tokenMap['$PACKAGE_LANGUAGES'] = this.generateLanguageListForPackage(); this.tokenMap['$PLATFORMS_DOTLIST_README'] = this.getPlatformDotListForReadme(); + + return true; } protected generate(artifacts: GeneratorArtifacts): boolean { @@ -89,6 +104,8 @@ export class BasicGenerator extends AbstractGenerator { sourceFile = this.callbacks.path.join(this.templateBasePath, this.templatePath, sourceFile); const sourceData = this.callbacks.loadFile(sourceFile); + + /* c8 ignore next 4 */ if(sourceData == null) { // internal error -- source file should be readable throw new Error(`source file ${sourceFile} does not exist`); diff --git a/developer/src/kmc-generate/src/generator-messages.ts b/developer/src/kmc-generate/src/generator-messages.ts index cda0d82fd2..6c9ccfa7d8 100644 --- a/developer/src/kmc-generate/src/generator-messages.ts +++ b/developer/src/kmc-generate/src/generator-messages.ts @@ -17,30 +17,67 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace; * @internal */ export class GeneratorMessages { - static Fatal_UnexpectedException = (o:{e: any}) => m(this.FATAL_UnexpectedException, null, o.e ?? 'unknown error'); static FATAL_UnexpectedException = SevFatal | 0x0001; + static Fatal_UnexpectedException = (o:{e: any}) => m( + this.FATAL_UnexpectedException, null, o.e ?? 'unknown error' + ); - static Info_GeneratingProject = (o:{type: string, id: string}) => m(this.INFO_GeneratingProject, - `Generating project of type ${def(o.type)} with id ${def(o.id)}`); static INFO_GeneratingProject = SevInfo | 0x0002; + static Info_GeneratingProject = (o:{type: string, id: string}) => m( + this.INFO_GeneratingProject, + `Generating project of type ${def(o.type)} with id ${def(o.id)}` + ); static ERROR_CannotCreateFolder = SevError | 0x0003; - static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException(this.ERROR_CannotCreateFolder, null, - `Unable to create folder ${def(o.folderName)}: ${o.e ?? 'unknown error'}`); + static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException( + this.ERROR_CannotCreateFolder, null, + `Unable to create folder ${def(o.folderName)}: ${o.e ?? 'unknown error'}` + ); static ERROR_OutputPathAlreadyExists = SevError | 0x0004; static Error_OutputPathAlreadyExists = (o:{outPath:string}) => m( this.ERROR_OutputPathAlreadyExists, - `Output path ${def(o.outPath)} already exists, not overwriting`); + `Output path ${def(o.outPath)} already exists, not overwriting` + ); static ERROR_CannotWriteOutputFile = SevError | 0x0005; - static Error_CannotWriteOutputFile = (o:{filename:string, e: any}) => CompilerMessageSpecWithException(this.ERROR_CannotWriteOutputFile, null, - `Unable to write file ${def(o.filename)}: ${o.e ?? 'unknown error'}`); + static Error_CannotWriteOutputFile = (o:{filename:string, e: any}) => CompilerMessageSpecWithException( + this.ERROR_CannotWriteOutputFile, null, + `Unable to write file ${def(o.filename)}: ${o.e ?? 'unknown error'}` + ); // See also PackageCompilerMessages.WARN_PackageNameDoesNotFollowLexicalModelConventions static WARN_ModelIdDoesNotFollowLexicalModelConventions = SevWarn | 0x0006; - static Warn_ModelIdDoesNotFollowLexicalModelConventions = (o:{id: string}) => m(this.WARN_ModelIdDoesNotFollowLexicalModelConventions, + static Warn_ModelIdDoesNotFollowLexicalModelConventions = (o:{id: string}) => m( + this.WARN_ModelIdDoesNotFollowLexicalModelConventions, `The id ${def(o.id)} does not follow the recommended model id conventions. The id should be all lower case, `+ `include only alphanumeric characters and underscore (_), not start with a digit, and should have the structure `+ - `..`); + `..` + ); + + static ERROR_InvalidTarget = SevError | 0x0007; + static Error_InvalidTarget = (o:{target:string}) => m( + this.ERROR_InvalidTarget, + `The specified target platform '${def(o.target)}' is not a valid target.` + ); + + // See also copier-messages.ERROR_InvalidKeyboardId + static ERROR_InvalidKeymanKeyboardId = SevError | 0x0008; + static Error_InvalidKeymanKeyboardId = (o:{id:string}) => m( + this.ERROR_InvalidKeymanKeyboardId, + `The specified keyboard id '${def(o.id)}' contains characters that are not permitted for a .kmn keyboard id or filename.` + ); + + static ERROR_InvalidLdmlKeyboardId = SevError | 0x0009; + static Error_InvalidLdmlKeyboardId = (o:{id:string}) => m( + this.ERROR_InvalidLdmlKeyboardId, + `The specified keyboard id '${def(o.id)}' contains characters that are not permitted for an LDML .xml keyboard id or filename.` + ); + + // See also copier-messages.ERROR_InvalidLexicalModelId + static ERROR_InvalidLexicalModelId = SevError | 0x000A; + static Error_InvalidLexicalModelId = (o:{id:string}) => m( + this.ERROR_InvalidLexicalModelId, + `The specified lexical model id '${def(o.id)}' contains characters that are not permitted or does not match the required pattern of 'author.bcp47.uniq'.` + ); }; diff --git a/developer/src/kmc-generate/src/keyman-keyboard-generator.ts b/developer/src/kmc-generate/src/keyman-keyboard-generator.ts index c3d5c7665a..13591fb472 100644 --- a/developer/src/kmc-generate/src/keyman-keyboard-generator.ts +++ b/developer/src/kmc-generate/src/keyman-keyboard-generator.ts @@ -5,9 +5,10 @@ */ import { KeymanFileTypes, KeymanTargets } from '@keymanapp/common-types'; -import { KeymanCompiler, } from '@keymanapp/developer-utils'; +import { KeymanCompiler, ValidIds, } from '@keymanapp/developer-utils'; import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js'; import { BasicGenerator } from './basic-generator.js'; +import { GeneratorMessages } from './generator-messages.js'; /** * @public @@ -39,7 +40,15 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom * @returns Binary artifacts on success, null on failure. */ async run(): Promise { - this.preGenerate(); + if(!ValidIds.isValidKeymanKeyboardId(this.options.id)) { + this.callbacks.reportMessage(GeneratorMessages.Error_InvalidKeymanKeyboardId({id:this.options.id})); + return null; + } + + if(!this.preGenerate()) { + // errors will have been reported in preGenerate + return null; + } const artifacts: GeneratorArtifacts = this.defaultArtifacts(); @@ -63,9 +72,10 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom KeymanKeyboardGenerator.SPath_Source+this.options.id+KeymanFileTypes.Source.TouchLayout; } - if(this.hasIcon()) { - this.includedPrefixes.push('Icon'); - } + // TODO-GENERATE: icon support + // if(this.hasIcon()) { + // this.includedPrefixes.push('Icon'); + // } if(this.hasKMX()) { this.includedPrefixes.push('KMX'); @@ -82,9 +92,9 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom // Special case for creating icon, run after successful creation of other // project bits and pieces - if(this.hasIcon()) { - this.writeIcon(artifacts); - } + // if(this.hasIcon()) { + // this.writeIcon(artifacts); + // } return {artifacts}; } @@ -98,13 +108,13 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom private readonly hasKMX = () => this.targetIncludes(KeymanTargets.KMXKeymanTargets); private readonly hasTouchLayout = () => this.targetIncludes(KeymanTargets.TouchKeymanTargets); - // TODO-GENERATE, once writeIcon is implemented: + // TODO-GENERATE: icon support // hasIcon = () => this.options.icon && this.targetIncludes(KeymanTargets.KMXKeymanTargets); - private readonly hasIcon = () => false; + // private readonly hasIcon = () => false; - private writeIcon(artifacts: GeneratorArtifacts) { - // TODO-GENERATE: this will require some effort - // proposal: generate 16x16 icon with 2-3 letters. Following TKeyboardIconGenerator.GenerateIcon - // research for .ico writer in node - } + // TODO-GENERATE: icon support + // private writeIcon(artifacts: GeneratorArtifacts) { + // proposal: generate 16x16 icon with 2-3 letters. Following TKeyboardIconGenerator.GenerateIcon + // research for .ico writer in node + // } } diff --git a/developer/src/kmc-generate/src/ldml-keyboard-generator.ts b/developer/src/kmc-generate/src/ldml-keyboard-generator.ts index e9885e7fb3..f8afb49013 100644 --- a/developer/src/kmc-generate/src/ldml-keyboard-generator.ts +++ b/developer/src/kmc-generate/src/ldml-keyboard-generator.ts @@ -5,9 +5,10 @@ */ import { KeymanFileTypes } from '@keymanapp/common-types'; -import { KeymanCompiler, } from '@keymanapp/developer-utils'; +import { KeymanCompiler, ValidIds, } from '@keymanapp/developer-utils'; import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js'; import { BasicGenerator } from './basic-generator.js'; +import { GeneratorMessages } from './generator-messages.js'; /** * @public @@ -28,7 +29,15 @@ export class LdmlKeyboardGenerator extends BasicGenerator implements KeymanCompi * @returns Binary artifacts on success, null on failure. */ async run(): Promise { - this.preGenerate(); + if(!ValidIds.isValidLdmlKeyboardId(this.options.id)) { + this.callbacks.reportMessage(GeneratorMessages.Error_InvalidLdmlKeyboardId({id:this.options.id})); + return null; + } + + if(!this.preGenerate()) { + // errors will have been reported in preGenerate + return null; + } const artifacts: GeneratorArtifacts = this.defaultArtifacts(); this.templatePath = 'ldml-keyboard'; diff --git a/developer/src/kmc-generate/src/lexical-model-generator.ts b/developer/src/kmc-generate/src/lexical-model-generator.ts index f78807798c..86316549cf 100644 --- a/developer/src/kmc-generate/src/lexical-model-generator.ts +++ b/developer/src/kmc-generate/src/lexical-model-generator.ts @@ -5,7 +5,7 @@ */ import { KeymanFileTypes } from '@keymanapp/common-types'; -import { KeymanCompiler, SourceFilenamePatterns } from '@keymanapp/developer-utils'; +import { KeymanCompiler, SourceFilenamePatterns, ValidIds } from '@keymanapp/developer-utils'; import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js'; import { BasicGenerator } from './basic-generator.js'; import { GeneratorMessages } from './generator-messages.js'; @@ -31,7 +31,15 @@ export class LexicalModelGenerator extends BasicGenerator implements KeymanCompi * @returns Binary artifacts on success, null on failure. */ async run(): Promise { - this.preGenerate(); + if(!ValidIds.isValidLexicalModelId(this.options.id)) { + this.callbacks.reportMessage(GeneratorMessages.Error_InvalidLexicalModelId({id:this.options.id})); + return null; + } + + if(!this.preGenerate()) { + // errors will have been reported in preGenerate + return null; + } const artifacts: GeneratorArtifacts = this.defaultArtifacts(); diff --git a/developer/src/kmc-generate/test/basic-generator.tests.ts b/developer/src/kmc-generate/test/basic-generator.tests.ts index b1eb1e420d..f5cff4044a 100644 --- a/developer/src/kmc-generate/test/basic-generator.tests.ts +++ b/developer/src/kmc-generate/test/basic-generator.tests.ts @@ -2,22 +2,71 @@ * Keyman is copyright (C) SIL International. MIT License. */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + import 'mocha'; import { assert } from 'chai'; -import { BasicGenerator } from '../src/basic-generator.js'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; +import { BasicGenerator } from '../src/basic-generator.js'; +import { AbstractGenerator, GeneratorArtifacts } from '../src/abstract-generator.js'; import { options } from './shared-options.js'; +import { makePathToFixture } from './helpers/index.js'; + +// this class is here to make it possible to access protected members +class BasicGeneratorTest extends BasicGenerator { + static files() { + return [ + 'sample/' + AbstractGenerator.SFile_WelcomeHTM, + 'sample/' + AbstractGenerator.SFile_ReadmeHTM, + 'sample/' + AbstractGenerator.SFile_HistoryMD, + 'sample/' + AbstractGenerator.SFile_LicenseMD, + 'sample/' + AbstractGenerator.SFile_ReadmeMD, + 'sample/' + AbstractGenerator.SFile_GitIgnore, + ]; + }; + + setup() { + this.templatePath = 'kmn-keyboard'; + } +} describe('BasicGenerator', function () { it('should configure default settings', async function() { const bg = new BasicGenerator(); const callbacks = new TestCompilerCallbacks(); - assert(await bg.init(callbacks, options)); - bg.test_preGenerate(); + assert.isTrue(await bg.init(callbacks, options)); + assert.isTrue(bg.test_preGenerate()); assert.equal(bg.test_tokenMap['$NAME'], 'Sample Project'); }); - // TODO-GENERATE: test transform + it('should successfully transform common project files', async function() { + const bg = new BasicGeneratorTest(); + bg.setup(); + + const callbacks = new TestCompilerCallbacks(); + assert.isTrue(await bg.init(callbacks, options)); + assert.isTrue(bg.test_preGenerate()); + + const artifacts: GeneratorArtifacts = { "kmc-generate:outputPath": { data: null, filename: '' } }; + assert.isTrue(bg.test_generate(artifacts)); + + // standard artifacts are listed in AbstractGenerator + const keys = [].concat( + 'kmc-generate:outputPath', + BasicGeneratorTest.files().map(f => f.replaceAll('/', path.sep)) + ); + + assert.hasAllKeys(artifacts, keys); + + // pick one file and compare it + const filename = BasicGeneratorTest.files()[0].replaceAll('/', path.sep); // welcome.htm + assert.equal(artifacts[filename].filename, filename); + const fixturePath = makePathToFixture('basic', 'welcome.htm'); + const fixture = fs.readFileSync(fixturePath, 'utf-8').replace(/\r\n/g, '\n'); + const actual = new TextDecoder().decode(artifacts[filename].data); + assert.equal(actual, fixture); + }); }); diff --git a/developer/src/kmc-generate/test/fixtures/basic/welcome.htm b/developer/src/kmc-generate/test/fixtures/basic/welcome.htm new file mode 100644 index 0000000000..1454a604b6 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/basic/welcome.htm @@ -0,0 +1,26 @@ + + + + + + Start Using Sample Project + + + + +

Start Using Sample Project

+ +

+ # A mighty description +

+ +

Keyboard Layout

+ + + + + \ No newline at end of file diff --git a/developer/src/kmc-generate/test/messages.tests.ts b/developer/src/kmc-generate/test/generator-messages.tests.ts similarity index 50% rename from developer/src/kmc-generate/test/messages.tests.ts rename to developer/src/kmc-generate/test/generator-messages.tests.ts index 68c7f741fa..b5d3a8837a 100644 --- a/developer/src/kmc-generate/test/messages.tests.ts +++ b/developer/src/kmc-generate/test/generator-messages.tests.ts @@ -2,15 +2,24 @@ * Keyman is copyright (C) SIL International. MIT License. */ +import * as path from 'node:path'; +import * as fs from 'node:fs'; + import 'mocha'; -import * as path from 'path'; -import * as fs from 'fs'; import { fileURLToPath } from 'url'; import { assert } from 'chai'; -import { GeneratorMessages } from '../src/generator-messages.js'; + import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; + +import { GeneratorMessages } from '../src/generator-messages.js'; import { AbstractGenerator, GeneratorOptions } from '../src/abstract-generator.js'; +import { BasicGenerator } from '../src/basic-generator.js'; +import { KeymanKeyboardGenerator } from '../src/keyman-keyboard-generator.js'; +import { LdmlKeyboardGenerator } from '../src/ldml-keyboard-generator.js'; +import { LexicalModelGenerator } from '../src/lexical-model-generator.js'; + +import { options } from './shared-options.js'; describe('GeneratorMessages', function () { const callbacks = new TestCompilerCallbacks(); @@ -62,4 +71,49 @@ describe('GeneratorMessages', function () { assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_CannotWriteOutputFile), `messageId ERROR_CannotWriteOutputFile not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); }); + + it('should generate ERROR_InvalidTarget if an invalid target is passed in', async function () { + const testOptions: GeneratorOptions = { ...options, + // @ts-ignore ('invalid' could be passed by a user, even if would be a compile error here) + targets: ['invalid'] + }; + const bg = new BasicGenerator(); + const callbacks = new TestCompilerCallbacks(); + assert(await bg.init(callbacks, testOptions)); + assert.isFalse(bg.test_preGenerate()); + assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidTarget)); + }); + + it('should generate ERROR_InvalidKeymanKeyboardId if an invalid keyboard id is passed in', async function () { + const testOptions: GeneratorOptions = { ...options, + id: '???invalid', + }; + const kkg = new KeymanKeyboardGenerator(); + const callbacks = new TestCompilerCallbacks(); + assert.isTrue(await kkg.init(callbacks, testOptions)); + assert.isNull(await kkg.run()); + assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidKeymanKeyboardId)); + }); + + it('should generate ERROR_InvalidLdmlKeyboardId if an invalid keyboard id is passed in', async function () { + const testOptions: GeneratorOptions = { ...options, + id: '???invalid', + }; + const lkg = new LdmlKeyboardGenerator(); + const callbacks = new TestCompilerCallbacks(); + assert.isTrue(await lkg.init(callbacks, testOptions)); + assert.isNull(await lkg.run()); + assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidLdmlKeyboardId)); + }); + + it('should generate ERROR_InvalidLexicalModelId if an invalid lexical model id is passed in', async function () { + const testOptions: GeneratorOptions = { ...options, + id: 'example.???.invalid', + }; + const lmg = new LexicalModelGenerator(); + const callbacks = new TestCompilerCallbacks(); + assert.isTrue(await lmg.init(callbacks, testOptions)); + assert.isNull(await lmg.run()); + assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidLexicalModelId)); + }); }); diff --git a/developer/src/kmc-generate/test/shared-options.ts b/developer/src/kmc-generate/test/shared-options.ts index 1010c417b4..cc36789fe3 100644 --- a/developer/src/kmc-generate/test/shared-options.ts +++ b/developer/src/kmc-generate/test/shared-options.ts @@ -6,7 +6,8 @@ import { KeymanTargets } from "@keymanapp/common-types"; import { GeneratorOptions } from "../src/abstract-generator.js"; export const options: GeneratorOptions = { - icon: false, + // TODO-GENERATE: icon support + // icon: false, id: 'sample', languageTags: ['en'], name: 'Sample Project', diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts index ee784429b9..3e1db45aed 100644 --- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts +++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts @@ -12,7 +12,7 @@ import { CompilerCallbacks, KeymanCompiler, CompilerOptions, KeymanCompilerResul import { KmpCompiler } from "@keymanapp/kmc-package"; import { SchemaValidators } from "@keymanapp/common-types"; -import { getFontFamily } from "./font-family.js"; +import { getFontFamily } from "@keymanapp/developer-utils"; const regionNames = new Intl.DisplayNames(['en'], { type: "region" }); const scriptNames = new Intl.DisplayNames(['en'], { type: "script" }); diff --git a/developer/src/kmc-keyboard-info/test/font-family.tests.ts b/developer/src/kmc-keyboard-info/test/font-family.tests.ts index b27fad8d74..d926e75c65 100644 --- a/developer/src/kmc-keyboard-info/test/font-family.tests.ts +++ b/developer/src/kmc-keyboard-info/test/font-family.tests.ts @@ -2,7 +2,7 @@ import * as fs from 'fs'; import { assert } from 'chai'; import 'mocha'; import { makePathToFixture } from './helpers/index.js'; -import { getFontFamily } from '../src/font-family.js'; +import { getFontFamily } from '@keymanapp/developer-utils'; const AFGHAN_TURKMEN_DISPLAY_FONT = makePathToFixture('afghan_turkmen', "Lateef-Regular.ttf"); const AFGHAN_TURKMEN_OSK_FONT = makePathToFixture('afghan_turkmen', "Lateef-Bold.ttf"); diff --git a/developer/src/kmc-keyboard-info/tsconfig.json b/developer/src/kmc-keyboard-info/tsconfig.json index d09b25c560..124c812ecf 100644 --- a/developer/src/kmc-keyboard-info/tsconfig.json +++ b/developer/src/kmc-keyboard-info/tsconfig.json @@ -13,7 +13,6 @@ }, "include": [ "src/**/*.ts", - "src/ttfmeta/lib/*.js", "src/imports/langtags.js", ], "references": [ diff --git a/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts b/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts index 9cdb28a3d9..d31ccdb14c 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/javascript-strings.ts @@ -669,7 +669,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp switch(recContext.Code) { case KMX.KMXFile.CODE_ANY: - Index = AdjustIndex(fkp.dpContext, ContextIndex) + 1; // I3910 // I4611 + Index = AdjustIndex(fkp.dpContext, ContextIndex); // I3910 // I4611 Result += nlt + `k.KIO(${len},this.s${JavaScript_Name(recContext.Any.StoreIndex, recContext.Any.Store.dpName)},${Index},t);`; // I4611 break; case KMX.KMXFile.CODE_DEADKEY: @@ -681,7 +681,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp // Note that this is checked in compiler.cpp as well, so this error can probably never occur throw new Error('Unexpected: notany() encountered with invalid version'); } - Result += nlt + `k.KCXO(${len},t,${AdjustIndex(fkp.dpContext, xstrlen(fkp.dpContext))},${AdjustIndex(fkp.dpContext, ContextIndex)+1});`; + Result += nlt + `k.KCXO(${len},t,${AdjustIndex(fkp.dpContext, xstrlen(fkp.dpContext))},${AdjustIndex(fkp.dpContext, ContextIndex)});`; break; case KMX.KMXFile.CODE_IFOPT: case KMX.KMXFile.CODE_IFSYSTEMSTORE: @@ -759,7 +759,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp case KMX.KMXFile.CODE_CONTEXT: if (x > 0 || len == -1) { let xContext = 0; - let n = 0; + let n = 1; while(xContext < fkp.dpContext.length) { // I4611 if(!isGroupReadOnly(fk, fgp)) { Result += ContextChar(n, fkp.dpContext, xContext); @@ -767,7 +767,6 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp n++; xContext = incxstr(fkp.dpContext, xContext); } - //Result := Result + Format('k.KO(%d,t,k.KC(%d,%d,t));', [len, xstrlen_printing(fkp.dpContext), xstrlen_printing(fkp.dpContext)]); } // else, we don't need to output anything - just don't delete the context len = -1; @@ -779,7 +778,7 @@ export function JavaScript_OutputString(fk: KMX.KEYBOARD, FTabStops: string, fkp } if(!isGroupReadOnly(fk, fgp)) { - Result += ContextChar(rec.ContextEx.Index, fkp.dpContext, xContext); // I4611 + Result += ContextChar(rec.ContextEx.Index + 1, fkp.dpContext, xContext); // I4611 } len = -1; break; diff --git a/developer/src/kmc-kmn/test/fixtures/kmw/test_index_12980.kmn b/developer/src/kmc-kmn/test/fixtures/kmw/test_index_12980.kmn new file mode 100644 index 0000000000..26d2164928 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/kmw/test_index_12980.kmn @@ -0,0 +1,29 @@ +c test_index_12980 generated from template at 2025-01-22 13:25:35 +c Validates index() offsets when used with if() - see #12980 + +store(&VERSION) '14.0' +store(&NAME) 'test_index_12980' +store(&TARGETS) 'web' + + +begin Unicode > use(main) + +group(main) using keys + +store(a) '1' +store(b) '2' +store(c) '3' +store(d) '4' +store(e) '5' +store(f) '6' +store(g) '7' +store(h) '8' + +if(&platform = 'web') any(a) + 'a' > context 'A' +if(&platform = 'web') any(b) + 'b' > context(2) 'B' +if(&platform = 'web') notany(c) + 'c' > context 'C' +if(&platform = 'web') notany(d) + 'd' > context(2) 'D' +nul any(e) + 'e' > context 'E' +nul any(f) + 'f' > context(2) 'F' +nul notany(g) + 'g' > context 'G' +nul notany(h) + 'h' > context(2) 'H' diff --git a/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts b/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts index d90b33c312..5060dc8757 100644 --- a/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts +++ b/developer/src/kmc-kmn/test/kmw/kmw-compiler.tests.ts @@ -218,6 +218,30 @@ describe('KeymanWeb Compiler', function() { assert.lengthOf(callbacks.messages, 2); }); + it('should generate correct index offsets (#12980) for context and context(n) when building a KMW keyboard', async function() { + const filenames = generateTestFilenames('test_index_12980'); + + let result = await kmnCompiler.run(filenames.source, null); + assert.isNotNull(result); + const data = new TextDecoder('utf-8').decode(result.artifacts.js.data); + + function doMatchKIO(s: string) { + const regex = new RegExp('k\\.KIO\\(-1,this\\.'+s+',(.+?),t\\);'); + const m = regex.exec(data); + assert.isNotNull(m, `Could not find match for ${s}`); + assert.equal(m[1], '1', `Expected '${s}' usage to have an index parameter == 1`); + } + + doMatchKIO('s_a_6'); + doMatchKIO('s_b_7'); + + // matches the 4 notany() rules + const matches = [...data.matchAll(/k\.KCXO\(-1,t,1,1\);/g)]; + assert.lengthOf(matches, 4); + + doMatchKIO('s_e_10'); + doMatchKIO('s_f_11'); + }); }); async function run_test_keyboard(kmnCompiler: KmnCompiler, id: string): diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index 607823c7dd..cd6cf6df36 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -18,6 +18,8 @@ import { PackageKeyboardTargetValidator } from './package-keyboard-target-valida import { PackageMetadataUpdater } from './package-metadata-updater.js'; import { markdownToHTML } from './markdown.js'; import { PackageValidation } from './package-validation.js'; +import { getFontFamilySync } from "@keymanapp/developer-utils"; +import { setKvkFontData } from './kvk-font-facename.js'; const KMP_JSON_FILENAME = 'kmp.json'; const KMP_INF_FILENAME = 'kmp.inf'; @@ -483,36 +485,36 @@ export class KmpCompiler implements KeymanCompiler { this.callbacks.reportMessage(PackageCompilerMessages.Warn_AbsolutePath({filename: filename})); } + let memberFileData = this.getMemberFileData(kpsFilename, filename); + if(!memberFileData) { + failed = true; + return; + } filename = this.callbacks.resolveFilename(kpsFilename, filename); const basename = this.callbacks.path.basename(filename); - if(!this.callbacks.fs.existsSync(filename)) { - this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: filename})); - failed = true; - return; + if(KeymanFileTypes.filenameIs(filename, KeymanFileTypes.Binary.VisualKeyboard)) { + if(!this.isKvkFileBinary(memberFileData)) { + // warn the few users who are still doing this -- non-binary .kvk should not be included in package! + this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename})); + } else { + memberFileData = this.setKvkFontData(kpsFilename, data, filename, memberFileData); + } } - let memberFileData; - try { - memberFileData = this.callbacks.loadFile(filename); - } catch(e) { - this.callbacks.reportMessage(PackageCompilerMessages.Error_FileCouldNotBeRead({filename: filename, e: e})); - failed = true; - return; - } - - this.warnIfKvkFileIsNotBinary(filename, memberFileData); - zip.file(basename, memberFileData); - - // Remove path data from files before JSON save - value.name = basename; }); if(failed) { return null; } + data.files.forEach((value) => { + const basename = this.callbacks.path.basename(value.name); + // Remove path data from files before JSON save + value.name = basename; + }); + // TODO #9477: transform .md to .htm // Remove path data from file references in options @@ -557,26 +559,87 @@ export class KmpCompiler implements KeymanCompiler { return transcodeToCP1252(s); } + private getMemberFileData(kpsFilename: string, filename: string) { + filename = this.callbacks.resolveFilename(kpsFilename, filename); + + if(!this.callbacks.fs.existsSync(filename)) { + this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: filename})); + return null; + } + + try { + return this.callbacks.loadFile(filename); + } catch(e) { + this.callbacks.reportMessage(PackageCompilerMessages.Error_FileCouldNotBeRead({filename: filename, e: e})); + return null; + } + } + + private setKvkFontData(kpsFilename: string, data: KmpJsonFile.KmpJsonFile, filename: string, kvk: Uint8Array) { + // find the appropriate font to set + const kvkId = this.callbacks.path.basename(filename.toLowerCase(), '.kvk'); + const kbd = data.keyboards.find(k => k.id == kvkId); + if(!kbd) { + // cannot find a matching keyboard + this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindMatchingKeyboardForVisualKeyboard({filename})); + return kvk; + } + const fontFilename = kbd.oskFont || kbd.displayFont; + if(!fontFilename) { + // no font data to set + return kvk; + } + + // Look up the full font filename + const fontFile = data.files.find(file => this.callbacks.path.basename(file.name.toLowerCase()) == fontFilename.toLowerCase()); + if(!fontFile) { + // font cannot be found + this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindFontForKeyboard({id: kbd.id, fontFilename})); + return kvk; + } + + // the font is a filename, included in the .kps + const fontData = this.getMemberFileData(kpsFilename, fontFile.name); + if(!fontData) { + // cannot find source font + this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindFontForKeyboard({id: kbd.id, fontFilename: fontFile.name})); + return kvk; + } + + const fontFacename = getFontFamilySync(fontData); + if(!fontFacename) { + // Font facename could not be extracted from ttf + this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotReadFont({fontFilename: fontFile.name})); + return kvk; + } + + const result = setKvkFontData(kvk, fontFacename); + if(!result) { + // KVK is invalid + this.callbacks.reportMessage(PackageCompilerMessages.Warn_VisualKeyboardFileIsInvalid({filename})); + return kvk; + } + + return result; + } + /** * Legacy .kmp compiler would transform xml-format .kvk files into a binary .kvk file; now - * we want that to remain the responsibility of the keyboard compiler, so we'll warn the - * few users who are still doing this + * we want that to remain the responsibility of the keyboard compiler. */ - private warnIfKvkFileIsNotBinary(filename: string, data: Uint8Array) { - if(!KeymanFileTypes.filenameIs(filename, KeymanFileTypes.Binary.VisualKeyboard)) { - return; - } - + private isKvkFileBinary(data: Uint8Array) { if(data.byteLength < 4) { // TODO: Not a valid .kvk file; should we be reporting this? - return; + return false; } + // Must start with 'KVKF' if(data[0] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[0] || data[1] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[1] || data[2] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[2] || data[3] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[3]) { - this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename})); + return false; } + return true; } } diff --git a/developer/src/kmc-package/src/compiler/kvk-font-facename.ts b/developer/src/kmc-package/src/compiler/kvk-font-facename.ts new file mode 100644 index 0000000000..b7c69da8e8 --- /dev/null +++ b/developer/src/kmc-package/src/compiler/kvk-font-facename.ts @@ -0,0 +1,16 @@ +import { KvkFileReader, KvkFileWriter } from "@keymanapp/common-types"; + +export function setKvkFontData(fileData: Uint8Array, fontFacename: string) { + const reader = new KvkFileReader(); + const kvk = reader.read(fileData); + if(!kvk) { + // kvk is invalid + return null; + } + + kvk.header.ansiFont.name = fontFacename; + kvk.header.unicodeFont.name = fontFacename; + + const writer = new KvkFileWriter(); + return writer.write(kvk); +} \ No newline at end of file diff --git a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts index 9a593a3a11..6545b7d963 100644 --- a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts +++ b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts @@ -164,5 +164,25 @@ export class PackageCompilerMessages { static Error_RequiredParameterMissing = (o:{param: string}) => m( this.ERROR_RequiredParameterMissing, `Source parameter '${def(o.param)}' is required.` ); + + static WARN_CannotFindMatchingKeyboardForVisualKeyboard = SevWarn | 0x0023; + static Warn_CannotFindMatchingKeyboardForVisualKeyboard = (o:{filename: string}) => m( + this.WARN_CannotFindMatchingKeyboardForVisualKeyboard, `Cannot find matching keyboard for visual keyboard file '${def(o.filename)}'.` + ); + + static WARN_CannotFindFontForKeyboard = SevWarn | 0x0024; + static Warn_CannotFindFontForKeyboard = (o:{id: string, fontFilename: string}) => m( + this.WARN_CannotFindFontForKeyboard, `Cannot find font file '${def(o.fontFilename)}' for keyboard '${def(o.id)}'.` + ); + + static WARN_CannotReadFont = SevWarn | 0x0025; + static Warn_CannotReadFont = (o:{fontFilename: string}) => m( + this.WARN_CannotReadFont, `Cannot parse font file '${def(o.fontFilename)}' to get facename.` + ); + + static WARN_VisualKeyboardFileIsInvalid = SevWarn | 0x0026; + static Warn_VisualKeyboardFileIsInvalid = (o:{filename: string}) => m( + this.WARN_VisualKeyboardFileIsInvalid, `Visual keyboard file '${def(o.filename)}' is invalid.` + ); } diff --git a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx new file mode 100644 index 0000000000..bd6b832cab Binary files /dev/null and b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx differ diff --git a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps index de17212a84..1a02984beb 100644 --- a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps +++ b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps @@ -27,7 +27,7 @@ .kvk - ../../invalid/basic.kmx + binary_kvk_file.kmx Keyboard Basic 0 .kmx @@ -36,7 +36,7 @@ Basic - basic + binary_kvk_file 1.0 Khmer diff --git a/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts b/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts index 6309064dd0..3109038a3a 100644 --- a/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts +++ b/developer/src/kmc/src/commands/buildClasses/BuildKeyboardInfo.ts @@ -35,7 +35,11 @@ export class BuildKeyboardInfo extends BuildActivity { } const keyboard = project.files.find(file => file.fileType == KeymanFileTypes.Source.KeymanKeyboard); - const jsFilename = keyboard ? project.resolveOutputFilePath(keyboard, KeymanFileTypes.Source.KeymanKeyboard, KeymanFileTypes.Binary.WebKeyboard) : null; + + // If the project has more than one keyboard, we should not list any .js files (#12853) + const canListJsFile = project.files.filter(file => file.fileType == KeymanFileTypes.Source.KeymanKeyboard).length == 1; + + const jsFilename = canListJsFile && keyboard ? project.resolveOutputFilePath(keyboard, KeymanFileTypes.Source.KeymanKeyboard, KeymanFileTypes.Binary.WebKeyboard) : null; const historyPath = path.join(project.projectPath, KeymanFileTypes.HISTORY_MD); const lastCommitDate = getLastGitCommitDate(fs.existsSync(historyPath) ? historyPath : project.projectPath); const sources = { diff --git a/developer/src/kmc/src/commands/copy.ts b/developer/src/kmc/src/commands/copy.ts index 2f22b24a0a..1d4ab94dcf 100644 --- a/developer/src/kmc/src/commands/copy.ts +++ b/developer/src/kmc/src/commands/copy.ts @@ -19,7 +19,6 @@ export function declareCopy(program: Command) { command .description('Copy a Keyman keyboard or lexical model project') .option('-o, --out-path ', 'New name and path for project') - // TODO-COPY .option('-r, --rename', 'Rename instead of copying project') .option('-n, --dry-run', 'Show what would happen, without making changes') .action(copyProject) .addHelpText('before', ` @@ -36,7 +35,6 @@ export function declareCopy(program: Command) { function commanderOptionsToCopierOptions(options: any): CopierOptions { const result: CopierOptions = { outPath: options.outPath, - // TODO-COPY rename: options.rename ?? false, dryRun: options.dryRun ?? false, }; return result; diff --git a/developer/src/kmc/src/commands/generate.ts b/developer/src/kmc/src/commands/generate.ts index bac724d9d4..d5abcec989 100644 --- a/developer/src/kmc/src/commands/generate.ts +++ b/developer/src/kmc/src/commands/generate.ts @@ -30,7 +30,8 @@ function declareGenerateKmnKeyboard(command: Command) { .option('-L, --language-tag ', 'BCP-47 language tag', (value, previous) => previous.concat([value]), []) .option('-a, --author ', 'Name of keyboard author') - .option('-i, --icon', 'Include a generated icon', true) + // TODO-GENERATE: icon support + // .option('-i, --icon', 'Include a generated icon', true) .option('-d, --description ', 'Short description of the project, Markdown') .action(generateKmnKeyboard); } @@ -45,7 +46,7 @@ function declareGenerateLdmlKeyboard(command: Command) { .option('-o, --out-path ', 'Output path (may exist)') .option('-n, --name ', 'Keyboard descriptive name') .option('-c, --copyright ', 'Copyright holder') /* © yyyy */ - .option('-v, --version ', 'Keyboard version', '1.0') + .option('-v, --version ', 'Keyboard version', '1.0.0') .option('-L, --language-tag ', 'BCP-47 language tag', (value, previous) => previous.concat([value]), []) .option('-a, --author ', 'Name of keyboard author') @@ -59,12 +60,12 @@ function declareGenerateLexicalModel(command: Command) { modelSubCommand .description('Generate a wordlist lexical model project') .option('-o, --out-path ', 'Output path (may exist)') - .option('-n, --name ', 'Keyboard descriptive name') + .option('-n, --name ', 'Lexical model descriptive name') .option('-c, --copyright ', 'Copyright holder') /* © yyyy */ - .option('-v, --version ', 'Keyboard version', '1.0') + .option('-v, --version ', 'Lexical model version', '1.0') .option('-L, --language-tag ', 'BCP-47 language tag', (value, previous) => previous.concat([value]), []) - .option('-a, --author ', 'Name of keyboard author') + .option('-a, --author ', 'Name of lexical model author') .option('-d, --description ', 'Short description of the project, Markdown') .action(generateLexicalModel); @@ -73,7 +74,8 @@ function declareGenerateLexicalModel(command: Command) { function commanderOptionsToGeneratorOptions(id: string, options: any): GeneratorOptions { const result: GeneratorOptions = { - icon: options.icon ?? true, + // TODO-GENERATE: icon support + // icon: options.icon ?? true, id, languageTags: options.languageTag ?? [], name: options.name ?? id, diff --git a/developer/src/kmc/src/commands/messageCommand.ts b/developer/src/kmc/src/commands/messageCommand.ts index ee568dad9c..257eacc913 100644 --- a/developer/src/kmc/src/commands/messageCommand.ts +++ b/developer/src/kmc/src/commands/messageCommand.ts @@ -178,7 +178,7 @@ function allMessageDetails(): CompilerMessageDetail[] { const toTitleCase = (s: string) => s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); function getMessageDetail(cls: any, id: string, escapeMarkdown: boolean): CompilerEvent { - const o = /^(INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(id); + const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(id); if(!o) { throw new Error(`Unexpected compiler message ${id}, does not match message error format`); } diff --git a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts index fd5f29d212..f3cdf1e7b2 100644 --- a/developer/src/kmc/src/util/NodeCompilerCallbacks.ts +++ b/developer/src/kmc/src/util/NodeCompilerCallbacks.ts @@ -23,6 +23,8 @@ const severityColors: {[value in CompilerErrorSeverity]: chalk.Chalk} = { [CompilerErrorSeverity.Warn]: color.hex('FFA500'), // orange [CompilerErrorSeverity.Error]: color.redBright, [CompilerErrorSeverity.Fatal]: color.redBright, + [CompilerErrorSeverity.Verbose]: color.gray, + [CompilerErrorSeverity.Debug]: color.blueBright, }; /** diff --git a/developer/src/server/build.sh b/developer/src/server/build.sh index c73ed853f7..e0e82e3617 100755 --- a/developer/src/server/build.sh +++ b/developer/src/server/build.sh @@ -113,7 +113,17 @@ function installer_server() { # monorepo context cat package.json | "$JQ" \ '. | del(.devDependencies."@keymanapp/resources-gosh") | del(.devDependencies."@keymanapp/keyman-version") | del(.dependencies."@keymanapp/developer-utils")' \ + > "$PRODBUILDTEMP/package0.json" + + # #12950: Merge dependencies from @keymanapp/developer-utils and Server; this + # is a bit messy but will mean we don't lose the needed dependencies. + "$JQ" -s '.[0] + { dependencies: (.[1].dependencies * .[0].dependencies) }' "$PRODBUILDTEMP/package0.json" \ + "$KEYMAN_ROOT/developer/src/common/web/utils/package.json" > "$PRODBUILDTEMP/package1.json" + cat $PRODBUILDTEMP/package1.json | "$JQ" \ + '. | del(.dependencies."@keymanapp/common-types")' \ > "$PRODBUILDTEMP/package.json" + rm "$PRODBUILDTEMP/package0.json" + rm "$PRODBUILDTEMP/package1.json" pushd "$PRODBUILDTEMP" npm install --omit=dev --omit=optional diff --git a/developer/src/tike/child/UfrmKeymanWizard.pas b/developer/src/tike/child/UfrmKeymanWizard.pas index 59e0f8a341..afb1e715ed 100644 --- a/developer/src/tike/child/UfrmKeymanWizard.pas +++ b/developer/src/tike/child/UfrmKeymanWizard.pas @@ -369,6 +369,8 @@ type FLayoutSetup: Boolean; FCheckboxGridHelper: TCheckboxGridHelper; + FDisplayMapFont: TFont; + function GetCurrentRule: TKeyboardParser_LayoutRule; procedure EnableControls; @@ -475,6 +477,7 @@ type procedure OrderDetailsPanels; function ValidateFileHasNoUnicodeCharacters(sw: WideString): Boolean; procedure UpdateQRCode; + procedure SetDisplayMapFont(const Value: TFont); protected function GetHelpTopic: string; override; @@ -495,6 +498,8 @@ type function ShouldRememberFocus(Control: TWinControl): Boolean; override; // I4679 + property DisplayMapFont: TFont read FDisplayMapFont write SetDisplayMapFont; + public procedure StartDebugging(FStartTest: Boolean = False); procedure StopDebugging; @@ -630,6 +635,8 @@ procedure TfrmKeymanWizard.FormCreate(Sender: TObject); begin FLoading := True; + FDisplayMapFont := TFont.Create; + //SetWindowTheme(pages.Handle, 'Explorer', ''); cmdAddToProject.Caption := '&Add to Project'; @@ -681,6 +688,7 @@ begin FreeAndNil(FKeyboardParser); // I4557 FreeAndNil(FDebugForm); FreeAndNil(FCheckboxGridHelper); + FreeAndNil(FDisplayMapFont); UninitSystemKeyboard; end; @@ -1825,6 +1833,15 @@ begin Delete(Result.Size, Length(Result.Size)-1, 2); end; end; + kfontDisplayMap: + begin + Result.Enabled := FKeyboardParser.GetSystemStoreValue(ssDisplayMap) <> ''; + if Result.Enabled then + begin + Result.Name := DisplayMapFont.Name; + Result.Size := IntToStr(TFontUtils.FontSizeInPoints(DisplayMapFont.Name, DisplayMapFont.Size)); // I4872 + end; + end; end; end; @@ -2535,6 +2552,8 @@ begin FFont.Free; end; end; + + SetFontFromString(DisplayMapFont, ProjectFile.IDEState['DisplayMapFont']); end; procedure TfrmKeymanWizard.SaveSettings(SaveProject: Boolean); @@ -2548,6 +2567,7 @@ begin else ProjectFile.IDEState['DebugDefaultFont'] := '0'; // I4702 ProjectFile.IDEState['DebugFont'] := FontAsString(FDebugForm.memo.Font); // I4702 + ProjectFile.IDEState['DisplayMapFont'] := FontAsString(DisplayMapFont); inherited; end; @@ -2781,6 +2801,13 @@ begin TUtilExecute.URL(lbDebugHosts.Items[lbDebugHosts.ItemIndex]); end; +procedure TfrmKeymanWizard.SetDisplayMapFont(const Value: TFont); +begin + FDisplayMapFont.Assign(Value); + if ProjectFile <> nil then + ProjectFile.IDEState['DisplayMapFont'] := FontAsString(DisplayMapFont); +end; + procedure TfrmKeymanWizard.SetFontInfo(Index: TKeyboardFont; const Value: TKeyboardFontInfo); // I4057 var f: TFont; @@ -2814,6 +2841,11 @@ begin CharFont.Name := Value.Name; CharFont.Size := StrToIntDef(Value.Size, 12); end; + kfontDisplayMap: + begin + DisplayMapFont.Name := Value.Name; + DisplayMapFont.Size := StrToIntDef(Value.Size, 12); + end; kfontOSK: begin if FKeyboardParser.Features.ContainsKey(kfOSK) then // I4258 diff --git a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas index 9778119e9f..24c070a5e8 100644 --- a/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas +++ b/developer/src/tike/compile/Keyman.Developer.System.KmcWrapper.pas @@ -114,7 +114,7 @@ begin state := plsFatal; Result := False; end - else // assume msgType = 'info' + else // assume msgType = 'info' (fine also for 'verbose', 'debug') state := plsInfo; FGlobalProject.Log(state, msgFilename, msgText, msgCode, msgLine); end diff --git a/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm b/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm index bd8ba4de5b..b874db58a8 100644 --- a/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm +++ b/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm @@ -2,11 +2,11 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts BorderIcons = [biSystemMenu] BorderStyle = bsDialog Caption = 'Keyboard Fonts' - ClientHeight = 291 + ClientHeight = 322 ClientWidth = 512 Position = poScreenCenter ExplicitWidth = 518 - ExplicitHeight = 320 + ExplicitHeight = 351 PixelsPerInch = 96 TextHeight = 13 object lblDevEnv: TLabel @@ -147,6 +147,20 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts Height = 13 Caption = 'em' end + object lblDisplayMap: TLabel + Left = 32 + Top = 248 + Width = 109 + Height = 13 + Caption = '&displayMap target font' + end + object Label4: TLabel + Left = 463 + Top = 248 + Width = 14 + Height = 13 + Caption = 'em' + end object fcbCode: TscFontComboBox Left = 176 Top = 41 @@ -260,23 +274,23 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts end object cmdOK: TButton Left = 162 - Top = 248 + Top = 280 Width = 91 Height = 25 Caption = 'OK' Default = True - TabOrder = 12 + TabOrder = 14 OnClick = cmdOKClick end object cmdCancel: TButton Left = 259 - Top = 248 + Top = 280 Width = 90 Height = 25 Cancel = True Caption = 'Cancel' ModalResult = 2 - TabOrder = 13 + TabOrder = 15 end object fcbTouchLayoutPhone: TscFontComboBox Left = 176 @@ -347,4 +361,34 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts ShowPreviewFontName = False ShowPreviewInList = False end + object editDisplayMapSize: TEdit + Left = 391 + Top = 245 + Width = 66 + Height = 21 + TabOrder = 13 + end + object fcbDisplayMap: TscFontComboBox + Left = 176 + Top = 245 + Width = 209 + Height = 24 + DropDownCount = 12 + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -13 + Font.Name = 'Tahoma' + Font.Style = [] + ItemHeight = 18 + ParentFont = False + TabOrder = 12 + MoveUsedToTop = False + ColorUsed = clWindow + PreviewWidth = 240 + PreviewHeight = 60 + FontTypes = [ftTrueTypeAnsi, ftTrueTypeSymbol] + ShowPreview = False + ShowPreviewFontName = False + ShowPreviewInList = False + end end diff --git a/developer/src/tike/dialogs/UfrmKeyboardFonts.pas b/developer/src/tike/dialogs/UfrmKeyboardFonts.pas index 8c5bc103c2..7f5d4546bd 100644 --- a/developer/src/tike/dialogs/UfrmKeyboardFonts.pas +++ b/developer/src/tike/dialogs/UfrmKeyboardFonts.pas @@ -66,6 +66,10 @@ type fcbTouchLayoutPhone: TscFontComboBox; fcbTouchLayoutTablet: TscFontComboBox; fcbTouchLayoutDesktop: TscFontComboBox; + lblDisplayMap: TLabel; + editDisplayMapSize: TEdit; + fcbDisplayMap: TscFontComboBox; + Label4: TLabel; procedure FormCreate(Sender: TObject); procedure cmdOKClick(Sender: TObject); private @@ -120,6 +124,10 @@ begin FControls[kfontTouchLayoutDesktop].CaptionLabel := lblTouchLayoutDesktopFont; FControls[kfontTouchLayoutDesktop].NameCombo := fcbTouchLayoutDesktop; FControls[kfontTouchLayoutDesktop].SizeEdit := editTouchLayoutDesktopSize; + + FControls[kfontDisplayMap].CaptionLabel := lblDisplayMap; + FControls[kfontDisplayMap].NameCombo := fcbDisplayMap; + FControls[kfontDisplayMap].SizeEdit := editDisplayMapSize; end; function TfrmKeyboardFonts.GetFontInfo(Index: TKeyboardFont): TKeyboardFontInfo; diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas index 863b737dc0..10e9dcdd3c 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas @@ -200,7 +200,7 @@ end; function TkmnProjectFileUI.TestKeymanWeb(FSilent: Boolean): Boolean; // I4409 var - FCompiledName: string; + FCompiledName, OSKFont: string; editor: TfrmTikeEditor; wizard: TfrmKeymanWizard; i: TKeyboardFont; @@ -250,7 +250,7 @@ begin // We register all fonts that are used by the layout, // but just once for each reference! - for i := kfontChar to kfontTouchLayoutDesktop do + for i := kfontChar to kfontDisplayMap do begin Found := False; for j := kfontChar to TKeyboardFont(Ord(i)-1) do @@ -265,13 +265,18 @@ begin if TServerUI.VerifyServerRunning then begin + if Wizard.FontInfo[kfontDisplayMap].Enabled then + begin + OSKFont := Wizard.FontInfo[kfontDisplayMap].Name + end + else + OSKFont := Wizard.FontInfo[kfontOSK].Name; + TServerDebugAPI.RegisterKeyboard( FCompiledName, ProjectFile.FileVersion, - // We only need to specify the char + osk fonts here - // as the others are referenced in the touch layout definition directly Wizard.FontInfo[kfontChar].Name, - Wizard.FontInfo[kfontOSK].Name + OSKFont ); wizard.NotifyStartedWebDebug; // I4021 diff --git a/developer/src/tike/xml/layoutbuilder/builder.js b/developer/src/tike/xml/layoutbuilder/builder.js index 3f30b1061c..8915308d6f 100644 --- a/developer/src/tike/xml/layoutbuilder/builder.js +++ b/developer/src/tike/xml/layoutbuilder/builder.js @@ -348,6 +348,13 @@ $(function() { return builder.renameSpecialKey(hint); } + this.escapeFontName = function(fontName) { + if(!fontName) { + return ""; + } + return JSON.stringify(fontName); + } + this.prepareLayer = function () { var layer = KVKL[builder.lastPlatform].layer[builder.lastLayerIndex]; @@ -380,11 +387,11 @@ $(function() { } $('#kbd,#sub-key-groups') - .css('font-family', KVKL[builder.lastPlatform].font) + .css('font-family', this.escapeFontName(KVKL[builder.lastPlatform].font)) .css('font-size', KVKL[builder.lastPlatform].fontsize || "1em"); $('#inpKeyCap,#inpSubKeyCap') - .css('font-family', KVKL[builder.lastPlatform].font) + .css('font-family', this.escapeFontName(KVKL[builder.lastPlatform].font)) .css('font-size', '18pt'); $('#kbd div').remove(); @@ -421,7 +428,7 @@ $(function() { .css('height', (100 * this.yscale) + 'px') .css('margin-top', (builder.keyMargin * this.yscale) + 'px') .css('margin-left', p + 'px') - .css('font-family', key.font) + .css('font-family', this.escapeFontName(key.font)) .css('font-size', key.fontsize); if(this.specialCharacters[text]) diff --git a/developer/src/tike/xml/layoutbuilder/prepare-key.js b/developer/src/tike/xml/layoutbuilder/prepare-key.js index 2b7819f7ed..46c5e29839 100644 --- a/developer/src/tike/xml/layoutbuilder/prepare-key.js +++ b/developer/src/tike/xml/layoutbuilder/prepare-key.js @@ -140,7 +140,7 @@ $(function() { .data('direction', key.direction) // used only by flicks .data('default', key.default) // used only by longpress .css('width', (100 * 0.7) + 'px') - .css('font-family', key.font) + .css('font-family',builder.escapeFontName(key.font)) .css('font-size', key.fontsize); if(builder.specialCharacters[text]) { diff --git a/ios/docs/engine/guides/in-app/index.md b/ios/docs/engine/guides/in-app/index.md index 3dfebbf82d..14ba8ba4c5 100644 --- a/ios/docs/engine/guides/in-app/index.md +++ b/ios/docs/engine/guides/in-app/index.md @@ -1,5 +1,5 @@ --- -title: Guide: build an in-app keyboard +title: Guide: build an in-app keyboard for iPhone and iPad with Keyman Engine --- Keyman Engine for iPhone and iPad allows you to use any Keyman touch diff --git a/ios/docs/engine/guides/system-keyboard/index.md b/ios/docs/engine/guides/system-keyboard/index.md index ea652360b2..4a533f08d8 100644 --- a/ios/docs/engine/guides/system-keyboard/index.md +++ b/ios/docs/engine/guides/system-keyboard/index.md @@ -1,5 +1,5 @@ --- -title: Guide: build a system keyboard app +title: Guide: build a system keyboard app on iPhone and iPad with Keyman Engine --- In [part 1](../in-app/) of this series, we looked at the steps involved diff --git a/ios/docs/help/about/history.md b/ios/docs/help/about/history.md index 17905b7e5a..60aba2b0f3 100644 --- a/ios/docs/help/about/history.md +++ b/ios/docs/help/about/history.md @@ -1,5 +1,5 @@ --- -title: Version History +title: Version History of Keyman for iPhone and iPad --- A frequently-updated changelog for Keyman is kept at [help.keyman.com/version-history](https://help.keyman.com/version-history/) on the Keyman Help website. diff --git a/ios/docs/help/about/index.md b/ios/docs/help/about/index.md index 71ead99252..d37b9ed978 100644 --- a/ios/docs/help/about/index.md +++ b/ios/docs/help/about/index.md @@ -1,5 +1,5 @@ --- -title: About Keyman +title: About Keyman for iPhone and iPad --- * [Welcome to Keyman](welcome) diff --git a/ios/docs/help/about/welcome.md b/ios/docs/help/about/welcome.md index deeb156a54..05246c7ca4 100644 --- a/ios/docs/help/about/welcome.md +++ b/ios/docs/help/about/welcome.md @@ -1,5 +1,5 @@ --- -title: Welcome to Keyman +title: Welcome to Keyman for iPhone and iPad --- Thank you for installing Keyman for iPhone and iPad. Whether you're a new or returning user, diff --git a/ios/docs/help/about/whatsnew.md b/ios/docs/help/about/whatsnew.md index b744839398..aa4cf6b333 100644 --- a/ios/docs/help/about/whatsnew.md +++ b/ios/docs/help/about/whatsnew.md @@ -1,5 +1,5 @@ --- -title: What's New +title: What's New in Keyman 18.0 for iPhone and iPad --- Here are some of the new features we have added to Keyman for iPhone and iPad 18.0: diff --git a/ios/docs/help/basic/index.md b/ios/docs/help/basic/index.md index b67688e03c..e93f77f3aa 100644 --- a/ios/docs/help/basic/index.md +++ b/ios/docs/help/basic/index.md @@ -1,5 +1,5 @@ --- -title: Basic Help +title: Keyman for iPhone and iPad Basic Help --- * [Using the Banner on the Keyboard](using-the-banner) diff --git a/ios/docs/help/context/gestures.md b/ios/docs/help/context/gestures.md new file mode 100644 index 0000000000..ab15c3203d --- /dev/null +++ b/ios/docs/help/context/gestures.md @@ -0,0 +1,15 @@ +--- +title: Gesture Hints and Tips - Keyman for iPhone and iPad +--- + +## Using Gestures on the Keyboard Keys +There are now several gestures available to interact with some keys on the keyboard. Keys which have gestures available will normally show them as a hint in the top right of the key: + +![](../ios_images/gesture-hint.png) + +* **Long press**: Press and hold a key, and after a moment a submenu will appear. Slide the finger to the submenu to select a key. Release the finger, and the highlighted key from the submenu will be output. + +* **Flick**: Press and hold a key, and then slide the finger in various directions on a key to reach alternate outputs. The key will animate to show the expected output when the finger gets released. A common flick is to slide down to reach numerals on the top row of the keyboard. + + An up-flick on keys that support longpresses but do not have a defined up-flick will skip the wait and immediately display their longpress submenu. +* **Multitap**: Some keys can be pressed repeatedly and rapidly to reach alternate outputs. Tapping rapidly twice on Shift will activate Caps Lock on many keyboards. \ No newline at end of file diff --git a/ios/docs/help/context/index.md b/ios/docs/help/context/index.md index 99e41dd65c..90cfbfd5f6 100644 --- a/ios/docs/help/context/index.md +++ b/ios/docs/help/context/index.md @@ -2,6 +2,8 @@ title: Keyman for iPhone and iPad Help --- +[Keyboard gestures](gestures) + | | Keyboard Keys
(Keyboard Present) | |---|---| | ![](../ios_images/globe.png) | Install or select another language/keyboard | diff --git a/ios/docs/help/index.md b/ios/docs/help/index.md index c71c703e72..994bbbda9e 100644 --- a/ios/docs/help/index.md +++ b/ios/docs/help/index.md @@ -18,6 +18,7 @@ title: Keyman for iPhone and iPad 18.0 Help * [Using the Banner on the Keyboard](basic/using-the-banner) * [Switching Between Keyboards](basic/switching-between-keyboards) * [Using a Keyboard](basic/keyboard-usage) +* [Keyboard Gestures](context/gestures) * [Using the Menu](context/) * [Using the Settings Screen](basic/config/) * [Language Settings](basic/language-settings) diff --git a/ios/docs/help/ios_images/gesture-hint.png b/ios/docs/help/ios_images/gesture-hint.png new file mode 100644 index 0000000000..155ba67418 Binary files /dev/null and b/ios/docs/help/ios_images/gesture-hint.png differ diff --git a/ios/docs/help/start/index.md b/ios/docs/help/start/index.md index f48fc3f185..d7bb7ad72e 100644 --- a/ios/docs/help/start/index.md +++ b/ios/docs/help/start/index.md @@ -1,5 +1,5 @@ --- -title: Getting Started +title: Getting Started with Keyman for iPhone and iPad --- * [How To - Adding a Keyboard](searching-for-keyboards) diff --git a/ios/docs/help/troubleshooting/index.md b/ios/docs/help/troubleshooting/index.md index 2f31716c95..1509c74ee6 100644 --- a/ios/docs/help/troubleshooting/index.md +++ b/ios/docs/help/troubleshooting/index.md @@ -1,5 +1,5 @@ --- -title: Troubleshooting +title: Keyman for iPhone and iPad Troubleshooting --- * [How To - Installing fonts](installing-fonts) diff --git a/ios/engine/KMEI/KeymanEngine/es-419.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/es-419.lproj/Localizable.strings index e94c1e7da6..756eb0a49b 100644 --- a/ios/engine/KMEI/KeymanEngine/es-419.lproj/Localizable.strings +++ b/ios/engine/KMEI/KeymanEngine/es-419.lproj/Localizable.strings @@ -226,6 +226,24 @@ /* Text showing name of spacebar caption - language + keyboard */ "menu-settings-spacebar-item-languageKeyboard" = "Idioma y teclado"; +/* Label for the "Adjust Keyboard Height" item on the main settings screen */ +"menu-settings-adjust-keyboard-height" = "Ajustar altura del teclado"; + +/* Title for the "Adjust Keyboard Height" settings child screen */ +"adjust-keyboard-height-title" = "Ajustar teclado"; + +/* Label for "Reset to Default Keyboard Height" button on the adjust height screen */ +"button-label-reset-default-keyboard-height" = "Restablecer altura de teclado predeterminada"; + +/* Instruction text to drag keyboard to resize */ +"keyboard-drag-instructions" = "Arrastre el control de flecha para ajustar la altura del teclado."; + +/* Instruction to rotate to adjust landscape keyboard height (displayed when device is portrait) */ +"portrait-keyboard-rotate-instructions" = "Gire el dispositivo para pasar al modo horizontal."; + +/* Instruction to rotate to adjust portrait keyboard height (displayed when device is landscape) */ +"landscape-keyboard-rotate-instructions" = "Gire el dispositivo para pasar al modo vertical."; + /* Short text for notification: download failure for keyboard */ "notification-download-failure-keyboard" = "Error al descargar el teclado"; diff --git a/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings index 88dea57f70..98d3031a04 100644 --- a/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings +++ b/ios/engine/KMEI/KeymanEngine/pt-PT.lproj/Localizable.strings @@ -226,6 +226,24 @@ /* Text showing name of spacebar caption - language + keyboard */ "menu-settings-spacebar-item-languageKeyboard" = "Idioma e teclado"; +/* Label for the "Adjust Keyboard Height" item on the main settings screen */ +"menu-settings-adjust-keyboard-height" = "Adjust Keyboard Height"; + +/* Title for the "Adjust Keyboard Height" settings child screen */ +"adjust-keyboard-height-title" = "Adjust Keyboard"; + +/* Label for "Reset to Default Keyboard Height" button on the adjust height screen */ +"button-label-reset-default-keyboard-height" = "Reset to Default Keyboard Height"; + +/* Instruction text to drag keyboard to resize */ +"keyboard-drag-instructions" = "Drag arrow control to adjust keyboard height."; + +/* Instruction to rotate to adjust landscape keyboard height (displayed when device is portrait) */ +"portrait-keyboard-rotate-instructions" = "Rotate device to adjust for landscape."; + +/* Instruction to rotate to adjust portrait keyboard height (displayed when device is landscape) */ +"landscape-keyboard-rotate-instructions" = "Rotate device to adjust for portrait."; + /* Short text for notification: download failure for keyboard */ "notification-download-failure-keyboard" = "Download do teclado falhou"; diff --git a/linux/debian/changelog b/linux/debian/changelog index 3a2dae55f3..99ce27f238 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,15 @@ +keyman (17.0.334-1) unstable; urgency=medium + + * New upstream release. + + -- Eberhard Beilharz Tue, 28 Jan 2025 16:52:06 +0100 + +keyman (17.0.333-1) unstable; urgency=medium + + * New upstream release. + + -- Eberhard Beilharz Thu, 16 Jan 2025 16:29:44 +0100 + keyman (17.0.332-1) unstable; urgency=medium * set environment variable for rendering of downloads dialog (#12617) diff --git a/linux/docs/help/about/history.md b/linux/docs/help/about/history.md index 6343e0313e..5eb733255f 100644 --- a/linux/docs/help/about/history.md +++ b/linux/docs/help/about/history.md @@ -1,5 +1,5 @@ --- -title: Version History +title: Version History of Keyman for Linux --- A frequent updated changelog for Keyman is kept at [help.keyman.com/version-history](https://help.keyman.com/version-history) on the Keyman Help website. diff --git a/linux/docs/help/about/index.md b/linux/docs/help/about/index.md index 5064ed6a09..0bf3142c40 100644 --- a/linux/docs/help/about/index.md +++ b/linux/docs/help/about/index.md @@ -1,5 +1,5 @@ --- -title: About Keyman +title: About Keyman for Linux --- * [Welcome to Keyman](welcome) diff --git a/linux/docs/help/about/welcome.md b/linux/docs/help/about/welcome.md index 6dc3dd4265..eacc8c449c 100644 --- a/linux/docs/help/about/welcome.md +++ b/linux/docs/help/about/welcome.md @@ -1,5 +1,5 @@ --- -title: Welcome to Keyman +title: Welcome to Keyman for Linux --- Thank you for installing Keyman for Linux. Whether you're a new or returning user, we believe you will appreciate the significant enhancements in this latest version of Keyman for Linux. diff --git a/linux/docs/help/about/whatsnew.md b/linux/docs/help/about/whatsnew.md index 62228c5e20..4a4d5a30e1 100644 --- a/linux/docs/help/about/whatsnew.md +++ b/linux/docs/help/about/whatsnew.md @@ -1,5 +1,5 @@ --- -title: What's New +title: What's New in Keyman 18.0 for Linux --- Here are some of the new features we have added to Keyman for Linux 18.0: diff --git a/linux/docs/help/common/index.md b/linux/docs/help/common/index.md index e1bbbdc225..b2d606b38d 100644 --- a/linux/docs/help/common/index.md +++ b/linux/docs/help/common/index.md @@ -1,5 +1,5 @@ --- -title: Common Questions +title: Keyman for Linux Common Questions --- See also [Knowledge Base articles](https://help.keyman.com/knowledge-base/). diff --git a/linux/docs/help/reference/index.md b/linux/docs/help/reference/index.md index 7b8f704018..4553779096 100644 --- a/linux/docs/help/reference/index.md +++ b/linux/docs/help/reference/index.md @@ -1,8 +1,8 @@ --- -title: Keyman 17.0 for Linux Reference +title: Keyman 18.0 for Linux Reference --- -## Keyman 17.0 for Linux Overview +## Keyman 18.0 for Linux Overview Keyman for Linux makes it possible for you to type in over 2,000 languages on Linux. diff --git a/linux/docs/help/start/index.md b/linux/docs/help/start/index.md index b3aad00bd5..530b67016d 100644 --- a/linux/docs/help/start/index.md +++ b/linux/docs/help/start/index.md @@ -1,5 +1,5 @@ --- -title: Getting Started +title: Getting Started with Keyman for Linux --- * [How To - Installing Keyboards with Keyman for Linux](installing-keyboard) diff --git a/linux/ibus-keyman/meson.build b/linux/ibus-keyman/meson.build index 98281849e8..e94c90285b 100644 --- a/linux/ibus-keyman/meson.build +++ b/linux/ibus-keyman/meson.build @@ -18,9 +18,17 @@ conf = configuration_data() ibus = dependency('ibus-1.0', version: '>= 1.2.0') gtk = dependency('gtk+-3.0', version: '>= 2.4') json_glib = dependency('json-glib-1.0', version: '>= 1.0') -systemd = dependency('libsystemd') icu = dependency('icu-i18n') +systemd = dependency('libsystemd', 'libelogind', required: false) +if systemd.found() + conf.set('DBUS_IMPLEMENTATION', 'SYSTEMD') +else + # Gentoo can use libelogind or basu + systemd = dependency('basu', required: true) + conf.set('DBUS_IMPLEMENTATION', 'BASU') +endif + core_dir = meson.current_source_dir() / '../../core' common_dir = meson.current_source_dir() / '../../common' diff --git a/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp b/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp index 635c3ff22d..50dbccba8d 100644 --- a/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp +++ b/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp @@ -1,5 +1,10 @@ +#include "config.h" #include +#if DBUS_IMPLEMENTATION == SYSTEMD #include +#else +#include +#endif #include "KeymanSystemServiceClient.h" #define KEYMAN_BUS_NAME "com.keyman.SystemService1" diff --git a/linux/ibus-keyman/tests/KmDbusTestServer.cpp b/linux/ibus-keyman/tests/KmDbusTestServer.cpp index e64a9354d8..fda2a12c00 100644 --- a/linux/ibus-keyman/tests/KmDbusTestServer.cpp +++ b/linux/ibus-keyman/tests/KmDbusTestServer.cpp @@ -1,9 +1,14 @@ // DBus test server. The server will start and listen on a non-standard DBus. // It runs until the DBus Exit method gets called. +#include "config.h" #include #include #include +#if DBUS_IMPLEMENTATION == SYSTEMD #include +#else +#include +#endif #ifndef KEYMAN_TEST_SERVICE_PATH #warning KEYMAN_TEST_SERVICE_PATH is undefined @@ -73,7 +78,10 @@ KmDbusTestServer::~KmDbusTestServer() { if (bus) sd_bus_release_name(bus, KEYMAN_TESTSVC_BUS_NAME); if (slot) sd_bus_slot_unref(slot); - if (bus) sd_bus_close_unref(bus); + if (bus) { + sd_bus_close(bus); + sd_bus_unref(bus); + } g_test_dbus_down(dbus); g_object_unref(dbus); diff --git a/linux/ibus-keyman/tests/StopTestServer.cpp b/linux/ibus-keyman/tests/StopTestServer.cpp index 97ce5bfbe7..2790e5b30e 100644 --- a/linux/ibus-keyman/tests/StopTestServer.cpp +++ b/linux/ibus-keyman/tests/StopTestServer.cpp @@ -1,8 +1,13 @@ // Call the Exit method on the km-dbus-test-server. Remember to source // `/tmp/km-test-server.env` prior to running stop-test-server in order // to run on our non-standard DBus. +#include "config.h" #include +#if DBUS_IMPLEMENTATION == SYSTEMD #include +#else +#include +#endif using namespace std; diff --git a/linux/ibus-keyman/tests/meson.build b/linux/ibus-keyman/tests/meson.build index a1c51a35ae..5ae10ae211 100644 --- a/linux/ibus-keyman/tests/meson.build +++ b/linux/ibus-keyman/tests/meson.build @@ -40,6 +40,7 @@ dbus_test_server = executable( 'km-dbus-test-server', 'KmDbusTestServer.cpp', dependencies: dbus_deps, + include_directories: ['..'], cpp_args: [ '-DKEYMAN_TEST_SERVICE_PATH="' + system_service_dir + '"' ] @@ -49,6 +50,7 @@ stop_test_server = executable( 'stop-test-server', 'StopTestServer.cpp', dependencies: dbus_deps, + include_directories: ['..'], ) env_file = '/tmp/env.txt' diff --git a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh index a32fef8689..940c5ad310 100755 --- a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh +++ b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh @@ -251,7 +251,7 @@ function _setup_ibus() { #shellcheck disable=SC2086 ibus-daemon ${ARG_VERBOSE-} --daemonize --panel=disable --address=unix:abstract="${TEMP_DATA_DIR}/test-ibus" ${IBUS_CONFIG-} &> /tmp/ibus-daemon.log PID=$(pgrep -f "${TEMP_DATA_DIR}/test-ibus") - if [[ "${STANDALONE}" == "--standalone" ]]; then + if [[ "${STANDALONE}" == "--standalone" ]] && [[ ! "${DOCKER_RUNNING:-false}" ]]; then # manual test run echo "if kill -9 ${PID}; then ibus restart || ibus start; fi # ibus-daemon" >> "${CLEANUP_FILE}" else diff --git a/linux/keyman-system-service/meson.build b/linux/keyman-system-service/meson.build index 596ba48bcd..c8ff6994aa 100644 --- a/linux/keyman-system-service/meson.build +++ b/linux/keyman-system-service/meson.build @@ -3,8 +3,21 @@ project('keyman-system-service', 'c', 'cpp', license: 'GPL-2+', meson_version: '>=1.0') +conf = configuration_data() + evdev = dependency('libevdev', version: '>= 1.9') -systemd = dependency('libsystemd') + +systemd = dependency('libsystemd', 'libelogind', required: false) +if systemd.found() + conf.set('DBUS_IMPLEMENTATION', 'SYSTEMD') +else + # Gentoo can use libelogind or basu + systemd = dependency('basu', required: true) + conf.set('DBUS_IMPLEMENTATION', 'BASU') +endif + +configure_file(output : 'config.h', + configuration : conf) subdir('resources') subdir('src') diff --git a/linux/keyman-system-service/resources/com.keyman.SystemService1.service.basu b/linux/keyman-system-service/resources/com.keyman.SystemService1.service.basu new file mode 100644 index 0000000000..67dd16eb5a --- /dev/null +++ b/linux/keyman-system-service/resources/com.keyman.SystemService1.service.basu @@ -0,0 +1,7 @@ +# /usr/share/dbus-1/system-services/com.keyman.SystemService1.service +# This version used with elogind and basu (e.g. on Gentoo) + +[D-BUS Service] +Name=com.keyman.SystemService1 +Exec=/usr/libexec/systemd-keyman.service +User=root diff --git a/linux/keyman-system-service/resources/com.keyman.SystemService1.service b/linux/keyman-system-service/resources/com.keyman.SystemService1.service.systemd similarity index 78% rename from linux/keyman-system-service/resources/com.keyman.SystemService1.service rename to linux/keyman-system-service/resources/com.keyman.SystemService1.service.systemd index 3966fb1bbc..65acc4067a 100644 --- a/linux/keyman-system-service/resources/com.keyman.SystemService1.service +++ b/linux/keyman-system-service/resources/com.keyman.SystemService1.service.systemd @@ -1,4 +1,5 @@ # /usr/share/dbus-1/system-services/com.keyman.SystemService1.service +# This version used with systemd (e.g. on Debian) [D-BUS Service] Name=com.keyman.SystemService1 diff --git a/linux/keyman-system-service/resources/meson.build.in b/linux/keyman-system-service/resources/meson.build.in index 2743889f30..3ddafdaaf3 100644 --- a/linux/keyman-system-service/resources/meson.build.in +++ b/linux/keyman-system-service/resources/meson.build.in @@ -1,4 +1,11 @@ # This file will be appended to meson.build by build.sh install_data('com.keyman.SystemService1.conf', install_dir: get_option('datadir') / 'dbus-1/system.d/') -install_data('com.keyman.SystemService1.service', install_dir: get_option('datadir') / 'dbus-1/system-services/') + +if systemd.name() == 'libsystemd' + install_data('com.keyman.SystemService1.service.systemd', install_dir: get_option('datadir') / 'dbus-1/system-services/', rename: ['com.keyman.SystemService1.service']) +else + # libelogind or basu + install_data('com.keyman.SystemService1.service.basu', install_dir: get_option('datadir') / 'dbus-1/system-services/') +endif + install_data('systemd-keyman.service', install_dir: get_option('prefix') / 'lib/systemd/system/') diff --git a/linux/keyman-system-service/src/KeymanSystemService.cpp b/linux/keyman-system-service/src/KeymanSystemService.cpp index deceafd0c3..03d7add533 100644 --- a/linux/keyman-system-service/src/KeymanSystemService.cpp +++ b/linux/keyman-system-service/src/KeymanSystemService.cpp @@ -2,6 +2,7 @@ // based on the sd-bus library, see // https://0pointer.net/blog/the-new-sd-bus-api-of-systemd.html +#include "config.h" #include #include #include @@ -11,7 +12,11 @@ #include #include #include +#if DBUS_IMPLEMENTATION == SYSTEMD #include +#else +#include +#endif #include "KeymanSystemService.h" #include "KeyboardDevice.h" diff --git a/linux/keyman-system-service/src/KeymanSystemService.h b/linux/keyman-system-service/src/KeymanSystemService.h index 2686bfb99f..86637f27a9 100644 --- a/linux/keyman-system-service/src/KeymanSystemService.h +++ b/linux/keyman-system-service/src/KeymanSystemService.h @@ -1,8 +1,13 @@ #ifndef __KEYMANSYSTEMSERVICE_H__ #define __KEYMANSYSTEMSERVICE_H__ +#include "config.h" #include +#if DBUS_IMPLEMENTATION == SYSTEMD #include +#else +#include +#endif #include "KeyboardDevice.h" using namespace std; diff --git a/linux/keyman-system-service/src/meson.build b/linux/keyman-system-service/src/meson.build index 51820ab9fa..3c3dd27fa6 100644 --- a/linux/keyman-system-service/src/meson.build +++ b/linux/keyman-system-service/src/meson.build @@ -10,6 +10,7 @@ exe = executable( 'keyman-system-service', sources: [service_files], dependencies: deps, + include_directories: ['..'], install: true, install_dir: get_option('libexecdir'), ) diff --git a/linux/keyman-system-service/tests/meson.build b/linux/keyman-system-service/tests/meson.build index f90e12d16a..749e098591 100644 --- a/linux/keyman-system-service/tests/meson.build +++ b/linux/keyman-system-service/tests/meson.build @@ -17,7 +17,7 @@ exe = executable( c_args: test_c_args, cpp_args: test_c_args, dependencies: deps, - include_directories: [ '../src' ] + include_directories: [ '..', '../src' ] ) # we currently don't have any unit tests for keyman-system-service. diff --git a/linux/scripts/dist.sh b/linux/scripts/dist.sh index 6e04e027d8..1b8f0dee87 100755 --- a/linux/scripts/dist.sh +++ b/linux/scripts/dist.sh @@ -17,7 +17,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" BASEDIR=$(pwd) -if [[ ! -z ${1+x} ]] && [ "$1" == "origdist" ]; then +if [[ ! -z ${1+x} ]] && [[ "$1" == "origdist" ]]; then create_origdist=1 shift fi @@ -30,62 +30,69 @@ cp -a debian ../ cd .. echo "3.0 (native)" > debian/source/format dch keyman --newversion "${VERSION}" --force-bad-version --nomultimaint -dpkg-source --tar-ignore=*~ \ - --tar-ignore=./.git \ - --tar-ignore=./.gitattributes \ - --tar-ignore=./.gitignore \ - --tar-ignore=experiments \ - --tar-ignore=debian \ - --tar-ignore=./.github \ - --tar-ignore=./.vscode \ - --tar-ignore=./android \ - --tar-ignore=./.devcontainer \ - --tar-ignore=./artifacts \ - \ - --tar-ignore=./common/models \ - --tar-ignore=./common/resources \ - --tar-ignore=./common/schemas \ - --tar-ignore=./common/test/keyboards/build.* \ - --tar-ignore=./common/test/resources \ - --tar-ignore=./common/web \ - --tar-ignore=./common/windows \ - \ - --tar-ignore=./core/build \ - --tar-ignore=./developer \ - --tar-ignore=./docs \ - --tar-ignore=./ios \ - --tar-ignore=./linux/keyman-config/keyman_config/version.py \ - --tar-ignore=./linux/keyman-config/buildtools/build-langtags.py \ - --tar-ignore=__pycache__ \ - --tar-ignore=./linux/docs/help \ - --tar-ignore=./mac \ - --tar-ignore=node_modules \ - --tar-ignore=./oem \ - --tar-ignore=./linux/build \ - --tar-ignore=./linux/builddebs \ - --tar-ignore=./linux/ibus-keyman/build \ - --tar-ignore=./linux/keyman-system-service/build \ - --tar-ignore=./resources/devbox \ - --tar-ignore=./resources/environment.sh \ - --tar-ignore=./resources/git-hooks \ - --tar-ignore=./resources/scopes \ - --tar-ignore=./resources/build/*.lua \ - --tar-ignore=./resources/build/jq* \ - --tar-ignore=./results \ - --tar-ignore=./tmp \ - --tar-ignore=./web \ - --tar-ignore=./windows \ - --tar-ignore=keyman_1* \ - --tar-ignore=dist \ - --tar-ignore=VERSION \ - \ - -Zgzip -b . + +# Create the tarball +# Note: the files end up in subdirectories under `keyman`, so we can +# include that when matching files and directories to ignore. +dpkg-source \ + --tar-ignore=*~ \ + --tar-ignore=.git \ + --tar-ignore=.gitattributes \ + --tar-ignore=.gitignore \ + --tar-ignore=experiments \ + --tar-ignore=debian \ + --tar-ignore=.github \ + --tar-ignore=.vscode \ + --tar-ignore=.devcontainer \ + --tar-ignore=__pycache__ \ + --tar-ignore=node_modules \ + --tar-ignore=keyman_1* \ + --tar-ignore=dist \ + --tar-ignore=VERSION \ + \ + --tar-ignore=keyman/android \ + --tar-ignore=keyman/artifacts \ + \ + --tar-ignore=keyman/common/mac \ + --tar-ignore=keyman/common/models \ + --tar-ignore=keyman/common/resources \ + --tar-ignore=keyman/common/schemas \ + --tar-ignore=keyman/common/test/keyboards/build.* \ + --tar-ignore=keyman/common/test/resources \ + --tar-ignore=keyman/common/web \ + --tar-ignore=keyman/common/windows \ + \ + --tar-ignore=keyman/core/build \ + --tar-ignore=keyman/developer \ + --tar-ignore=keyman/docs \ + --tar-ignore=keyman/ios \ + --tar-ignore=keyman/linux/build \ + --tar-ignore=keyman/linux/builddebs \ + --tar-ignore=keyman/linux/docs/help \ + --tar-ignore=keyman/linux/ibus-keyman/build \ + --tar-ignore=keyman/linux/keyman-config/keyman_config/version.py \ + --tar-ignore=keyman/linux/keyman-config/buildtools/build-langtags.py \ + --tar-ignore=keyman/linux/keyman-system-service/build \ + --tar-ignore=keyman/mac \ + --tar-ignore=keyman/oem \ + --tar-ignore=keyman/resources/devbox \ + --tar-ignore=keyman/resources/environment.sh \ + --tar-ignore=keyman/resources/git-hooks \ + --tar-ignore=keyman/resources/scopes \ + --tar-ignore=keyman/resources/build/*.lua \ + --tar-ignore=keyman/resources/build/jq* \ + --tar-ignore=keyman/results \ + --tar-ignore=keyman/tmp \ + --tar-ignore=keyman/web \ + --tar-ignore=keyman/windows \ + \ + -Zgzip -b . mv ../keyman_"${VERSION}".tar.gz linux/dist/keyman-"${VERSION}".tar.gz echo "3.0 (quilt)" > debian/source/format -cd "$BASEDIR" +cd "${BASEDIR}" # create orig.tar.gz -if [ ! -z "${create_origdist+x}" ]; then +if [[ ! -z "${create_origdist+x}" ]]; then cd dist pkgvers="keyman-$VERSION" tar xfz keyman-"${VERSION}".tar.gz diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index 483998a16e..87b005309c 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -35,7 +35,9 @@ 29BE9D872CA3C21900B67DE7 /* KMModifierMapping.m in Sources */ = {isa = PBXBuildFile; fileRef = 29BE9D862CA3C21900B67DE7 /* KMModifierMapping.m */; }; 29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = D861B03E2C5747F70003675E /* KMSettingsRepository.m */; }; 29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = 29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */; }; + 29DBBF672D35053E00D8E33C /* KMSentryHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */; }; 29DD5F442CFEF88000683388 /* SILAndikaV1RGB.png in Resources */ = {isa = PBXBuildFile; fileRef = 29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */; }; + 29E67D042D3E4A86003CDE8F /* KMSentryHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */; }; 37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37A245C02565DFA6000BBF92 /* Assets.xcassets */; }; 37AE5C9D239A7B770086CC7C /* qrcode.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 37AE5C9C239A7B770086CC7C /* qrcode.min.js */; }; 37C2B0CB25FF2C350092E16A /* Help in Resources */ = {isa = PBXBuildFile; fileRef = 37C2B0CA25FF2C340092E16A /* Help */; }; @@ -275,6 +277,8 @@ 29D470972C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMKeyboardHelpWindowController.strings; sourceTree = ""; }; 29D470982C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainMenu.strings; sourceTree = ""; }; 29D470992C648D7100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; + 29DBBF652D35053E00D8E33C /* KMSentryHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMSentryHelper.h; sourceTree = ""; }; + 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMSentryHelper.m; sourceTree = ""; }; 29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = SILAndikaV1RGB.png; sourceTree = ""; }; 29DD8400276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMAboutWindowController.strings; sourceTree = ""; }; 29DD8401276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/preferences.strings; sourceTree = ""; }; @@ -433,6 +437,17 @@ path = Privacy; sourceTree = ""; }; + 29DBBF642D34FFE000D8E33C /* Logging */ = { + isa = PBXGroup; + children = ( + 29B4A0D42BF7675A00682049 /* KMLogs.h */, + 29B4A0D32BF7675A00682049 /* KMLogs.m */, + 29DBBF652D35053E00D8E33C /* KMSentryHelper.h */, + 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */, + ); + path = Logging; + sourceTree = ""; + }; 2CDC487CDFFBB26D621D73BD /* Pods */ = { isa = PBXGroup; children = ( @@ -564,8 +579,6 @@ 989C9C0A1A7876DE00A20425 /* Keyman4MacIM */ = { isa = PBXGroup; children = ( - 29B4A0D42BF7675A00682049 /* KMLogs.h */, - 29B4A0D32BF7675A00682049 /* KMLogs.m */, 299ABD6F29ECE75B00AA5948 /* KeySender.m */, 299ABD7029ECE75B00AA5948 /* KeySender.h */, D861B03D2C5747F70003675E /* KMSettingsRepository.h */, @@ -575,6 +588,7 @@ 297A501128DF4D360074EB1B /* Privacy */, 98FE105B1B4DE86300525F54 /* Categories */, 98D6DA791A799EE700B09822 /* Frameworks */, + 29DBBF642D34FFE000D8E33C /* Logging */, 989C9C121A7876DE00A20425 /* Images.xcassets */, 98BDD3681BC3511200FAC7C4 /* Keyman4MacIM.entitlements */, 98BF92401BF01CBE0002126A /* KMAboutWindow */, @@ -1000,6 +1014,7 @@ 9A3D6C5D221531B0008785A3 /* KMOSVersion.m in Sources */, 989C9C111A7876DE00A20425 /* main.m in Sources */, 290BC680274B9DB1005CD1C3 /* KMPackageInfo.m in Sources */, + 29DBBF672D35053E00D8E33C /* KMSentryHelper.m in Sources */, 29B4A0D52BF7675A00682049 /* KMLogs.m in Sources */, 98BF924F1BF02DC20002126A /* KMBarView.m in Sources */, E240F599202DED740000067D /* KMPackage.m in Sources */, @@ -1038,6 +1053,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 29E67D042D3E4A86003CDE8F /* KMSentryHelper.m in Sources */, 29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */, 29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */, 2915DC512BFE35DB0051FC52 /* KMLogs.m in Sources */, diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m index 202be65ef5..2b06db4cad 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m @@ -10,6 +10,7 @@ #import "KMDownloadKBWindowController.h" #import "KMDataRepository.h" #import "KMLogs.h" +#import "KMSentryHelper.h" @interface KMConfigurationWindowController () @property (nonatomic, weak) IBOutlet NSTableView *tableView; @@ -360,21 +361,27 @@ - (void)checkBoxAction:(id)sender { NSButton *checkBox = (NSButton *)sender; NSString *kmxFilePath = [self kmxFilePathAtIndex:checkBox.tag]; + NSString * kmxFileName = [kmxFilePath lastPathComponent]; NSString *partialPath = [KMDataRepository.shared trimToPartialPath:kmxFilePath]; os_log_debug([KMLogs uiLog], "checkBoxAction, kmxFilePath = %{public}@ for checkBox.tag %li, partialPath = %{public}@", kmxFilePath, checkBox.tag, partialPath); if (checkBox.state == NSOnState) { - os_log_debug([KMLogs uiLog], "Adding active keyboard: %{public}@", kmxFilePath); + os_log_debug([KMLogs uiLog], "enabling active keyboard: %{public}@", kmxFileName); + NSString *message = [NSString stringWithFormat:@"enabling active keyboard: %@", kmxFileName]; + [KMSentryHelper addUserBreadCrumb:@"config" message:message]; [self.activeKeyboards addObject:partialPath]; [self saveActiveKeyboards]; } else if (checkBox.state == NSOffState) { - os_log_debug([KMLogs uiLog], "Disabling active keyboard: %{public}@", kmxFilePath); + os_log_debug([KMLogs uiLog], "disabling active keyboard: %{public}@", kmxFileName); + NSString *message = [NSString stringWithFormat:@"disabling active keyboard: %@", kmxFileName]; + [KMSentryHelper addUserBreadCrumb:@"config" message:message]; [self.activeKeyboards removeObject:partialPath]; [self saveActiveKeyboards]; } } - (void)infoAction:(id)sender { + [KMSentryHelper addUserBreadCrumb:@"user" message:@"getting package information"]; NSButton *infoButton = (NSButton *)sender; NSString *packagePath = [self packagePathAtIndex:infoButton.tag]; if (packagePath != nil) { @@ -389,6 +396,7 @@ } - (void)helpAction:(id)sender { + [KMSentryHelper addUserBreadCrumb:@"user" message:@"displaying help"]; NSButton *helpButton = (NSButton *)sender; NSString *packagePath = [self packagePathAtIndex:helpButton.tag]; if (packagePath != nil) { @@ -406,22 +414,31 @@ NSButton *deleteButton = (NSButton *)sender; NSDictionary *info = [self.tableContents objectAtIndex:deleteButton.tag]; NSString *deleteKeyboardMessage = NSLocalizedString(@"message-confirm-delete-keyboard", nil); - - if ([info objectForKey:@"HeaderTitle"] != nil) - [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, [info objectForKey:@"HeaderTitle"]]]; - else - [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, [info objectForKey:kKMKeyboardNameKey]]]; - + NSString *keyboardName = nil; + + if ([info objectForKey:@"HeaderTitle"] != nil) { + keyboardName = [info objectForKey:@"HeaderTitle"]; + } else { + keyboardName = [info objectForKey:kKMKeyboardNameKey]; + } + [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, keyboardName]]; + os_log_debug([KMLogs configLog], "entered removeAction for keyboardName: %{public}@", keyboardName); + [self.deleteAlertView beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSAlertFirstButtonReturn) { os_log_debug([KMLogs uiLog], "confirm delete keyboard alert dismissed"); [self deleteFileAtIndex:[NSNumber numberWithInteger:deleteButton.tag]]; + + os_log_debug([KMLogs configLog], "removeAction for keyboardName: %{public}@", keyboardName); + [KMSentryHelper addUserBreadCrumb:@"configure" message:[NSString stringWithFormat:@"remove keyboard: %@", keyboardName]]; } self.deleteAlertView = nil; }]; } - (IBAction)downloadAction:(id)sender { + [KMSentryHelper addUserBreadCrumb:@"user" message:@"download keyboard"]; + if (self.AppDelegate.infoWindow_.window != nil) [self.AppDelegate.infoWindow_ close]; @@ -453,7 +470,8 @@ - (void)installPackageFile:(NSString *)kmpFile { // kmpFile could be a temp file (in fact, it always is!), so don't display the name. os_log_debug([KMLogs dataLog], "kmpFile - ready to unzip/install Package File: %{public}@", kmpFile); - + [KMSentryHelper addInfoBreadCrumb:@"configure" message:@"install package file"]; + BOOL didUnzip = [self.AppDelegate unzipFile:kmpFile]; if (!didUnzip) { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings index b6bbd130a0..def43607c3 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/pt-PT.lproj/preferences.strings @@ -17,7 +17,7 @@ "JOK-JV-n8w.title" = "Voltar"; /* Keyboards button text */ -"MPN-9N-wWc.label" = "Keyboards"; +"MPN-9N-wWc.label" = "Teclados"; /* text to explain verbose Console logging option */ "MrI-GM-7d6.title" = "Quando a opção de registro detalhada estiver ativada, o Keyman irá registrar ações que podem ajudar o suporte ao Keyman a diagnosticar um problema. O programa Console pode ser usado para ver um registro de mensagens do Keyman. Em Console, filtro para mostrar todas as mensagens do processo \"Keyman\". Este log pode ser exportado e enviado para o suporte a Keyman, se necessário."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index e97be3e389..dd53b49622 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -13,7 +13,7 @@ #import "KMLogs.h" #import "InputMethodKit/InputMethodKit.h" #import "KMInputMethodLifecycle.h" - +#import "KMSentryHelper.h" @implementation KMInputController @@ -25,7 +25,7 @@ KMInputMethodEventHandler* _eventHandler; - (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)inputClient { - os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getClientApplicationId]); + os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getRunningApplicationId]); self = [super initWithServer:server delegate:delegate client:inputClient]; if (self) { @@ -84,7 +84,7 @@ KMInputMethodEventHandler* _eventHandler; if (_eventHandler != nil) { [_eventHandler deactivate]; } - _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getClientApplicationId] client:self.client]; + _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getRunningApplicationId] client:self.client]; } @@ -111,17 +111,21 @@ KMInputMethodEventHandler* _eventHandler; NSInteger itag = mItem.tag; os_log_debug([KMLogs uiLog], "Keyman menu clicked - tag: %lu", itag); if (itag == CONFIG_MENUITEM_TAG) { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"Configuration..."]; [self showConfigurationWindow:sender]; } else if (itag == OSK_MENUITEM_TAG) { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"On-screen Keyboard"]; [KMSettingsRepository.shared writeShowOskOnActivate:YES]; os_log_debug([KMLogs oskLog], "menuAction OSK_MENUITEM_TAG, updating settings writeShowOsk to YES"); [self.appDelegate showOSK]; } else if (itag == ABOUT_MENUITEM_TAG) { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"About"]; [self.appDelegate showAboutWindow]; } else if (itag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"Selected Keyboard"]; [self.appDelegate selectKeyboardFromMenu:itag]; } } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h index fe06fa603a..eade3d6d2f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h @@ -124,6 +124,7 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; - (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) virtualKey postCallback:(PostEventCallback)postEvent; - (KeymanVersionInfo)versionInfo; - (void)registerConfigurationWindow:(NSWindowController *)window; +- (BOOL)canForceSentryEvents; @end #endif /* KMInputMethodAppDelegate_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index b938279197..0c4199959e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -17,8 +17,17 @@ #import "KMPackageInfo.h" #import "PrivacyConsent.h" #import "KMLogs.h" +#import "KMSentryHelper.h" @import Sentry; +#if TARGET_CPU_ARM64 +NSString *processorType = @"ARM"; +#elif TARGET_CPU_X86_64 +NSString *processorType = @"Intel"; +#else +NSString *processorType = @"Unknown"; +#endif + NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification"; @implementation NSString (VersionNumbers) @@ -48,6 +57,7 @@ NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboard @property (nonatomic, strong) KMPackageReader *packageReader; @property BOOL receivedKeyDownFromOsk; @property NSEventModifierFlags oskEventModifiers; +@property (nonatomic, assign) BOOL sentryTestingEnabled; @end @implementation KMInputMethodAppDelegate @@ -124,10 +134,12 @@ id _lastServerWithOSKShowing = nil; - (void)inputMethodDeactivated:(NSNotification *)notification { if ([self.oskWindow.window isVisible]) { os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK"); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"hiding OSK on input method deactivation"]; [self.oskWindow.window setIsVisible:NO]; } else { os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, OSK already hidden"); } + [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]]; if (self.lowLevelEventTap) { os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap"); @@ -136,16 +148,19 @@ id _lastServerWithOSKShowing = nil; } /** - * When the input method is activated, show the OSK and enable the low-level event tap + * When the input method is activated, enable the low-level event tap and show the OSK */ - (void)inputMethodActivated:(NSNotification *)notification { if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) { os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap..."); CGEventTapEnable(self.lowLevelEventTap, YES); } + + os_log_debug([KMLogs lifecycleLog], "--- inputMethodActivated, kvk is non-nil: %{public}@ showOskOnActivate: %{public}@", (_kvk!=nil)?@"true":@"false", [KMInputMethodLifecycle.shared shouldShowOskOnActivate]?@"true":@"false"); - if (_kvk != nil && ([KMInputMethodLifecycle.shared shouldShowOskOnActivate])) { + if ([KMInputMethodLifecycle.shared shouldShowOskOnActivate]) { os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK"); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"]; [self showOSK]; } } @@ -181,7 +196,7 @@ id _lastServerWithOSKShowing = nil; [self setDefaultKeymanMenuItems]; [self updateKeyboardMenuItems]; [self setPostLaunchKeymanSentryTags]; - // [SentrySDK captureMessage:@"Starting Keyman [test message]"]; + self.sentryTestingEnabled = [KMSettingsRepository.shared readForceSentryError]; } - (void)startSentry { @@ -192,32 +207,47 @@ id _lastServerWithOSKShowing = nil; options.dsn = @"https://960f8b8e574c46e3be385d60ce8e1fea@o1005580.ingest.sentry.io/5983522"; options.releaseName = releaseName; options.environment = keymanVersionInfo.sentryEnvironment; - //options.debug = YES; }]; } +/** + * Determine whether to force sentry events. + * When true, then typing certain characters will cause Sentry events or crashes to occur. + * + * To enable, set the flag in UserDefaults + * `defaults write keyman.inputmethod.Keyman KMForceSentryError 1` + * Also, use the SIL Euro Latin keyboard and type in the Stickies app + */ +- (BOOL)canForceSentryEvents { + NSString* const testKeyboardName = @"sil_euro_latin.kmx"; + NSString* const testClientApplicationId = @"com.apple.Stickies"; + BOOL canForce = NO; + + if (self.sentryTestingEnabled) { + NSString * applicationId = [KMInputMethodLifecycle.shared clientApplicationId]; + NSString * kmxFileName = [self getKmxFileName]; + canForce = [kmxFileName isEqualToString:testKeyboardName] && [applicationId isEqualToString:testClientApplicationId]; + os_log_info([KMLogs testLog], "canForceSentryEvents, self.sentryTestingEnabled: %{public}@ kmxName: %{public}@ applicationId: %{public}@ testingEnabled: %{public}@", self.sentryTestingEnabled?@"YES":@"NO", kmxFileName, applicationId, canForce?@"YES":@"NO"); + } + + return canForce; +} + +- (NSString*) getKmxFileName { + return [[self.kmx filePath] lastPathComponent]; +} + - (void)setPostLaunchKeymanSentryTags { NSString *keyboardFileName = [self.kmx.filePath lastPathComponent]; os_log_info([KMLogs keyboardLog], "initial kmx set to %{public}@", keyboardFileName); - NSString *hasAccessibility = [PrivacyConsent.shared checkAccessibility]?@"Yes":@"No"; - - // assign custom keyboard tag in Sentry to initial keyboard - [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { - [scope setTagValue:hasAccessibility forKey:@"accessibilityEnabled"]; - [scope setTagValue:keyboardFileName forKey:@"keyboard"]; - }]; + // assign custom keyboard tags in Sentry + [KMSentryHelper addKeyboardTag:keyboardFileName]; + [KMSentryHelper addHasAccessibilityTag:[PrivacyConsent.shared checkAccessibility]]; + [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]]; + [KMSentryHelper addArchitectureTag:processorType]; } -#ifdef USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE -- (BOOL)alertShowHelp:(NSAlert *)alert { - os_log_error([KMLogs startupLog], "Sentry - KME: Got call to force crash from engine"); - [SentrySDK crash]; - os_log_error([KMLogs startupLog], "Sentry - KME: should not have gotten this far!"); - return NO; -} -#endif - - (void)handleURLEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent { [self processURL:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]]; @@ -317,6 +347,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef os_log_debug([KMLogs eventsLog], "*** kKeymanEventKeyCode = 0xFF"); } else { if ([OSKView isOskKeyDownEvent:event]) { + [KMSentryHelper addInfoBreadCrumb:@"event" message:@"processing OSK-generated keydown event"]; NSEventModifierFlags oskEventModifiers = [OSKView extractModifierFlagsFromOskEvent:event]; appDelegate.receivedKeyDownFromOsk = YES; appDelegate.oskEventModifiers = oskEventModifiers; @@ -406,22 +437,26 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef _kmx = kmx; CoreKeyboardInfo *keyboardInfo = [self.kme loadKeyboardFromKmxFile:kmx]; - os_log_info([KMLogs keyboardLog], "setKmx loaded keyboard, keyboard info: %{public}@", keyboardInfo); - + os_log_info([KMLogs keyboardLog], "loadKeyboardFromKmxFile, keyboard info: %{public}@", keyboardInfo); + [KMSentryHelper addInfoBreadCrumb:@"configure" message:[NSString stringWithFormat:@"loadKeyboardFromKmxFile: %@", [self getKmxFileName]]]; + [KMSentryHelper addKeyboardTag:[self getKmxFileName]]; + _modifierMapping = [[KMModifierMapping alloc] init:keyboardInfo]; os_log_info([KMLogs keyboardLog], "modifierMapping bothOptionKeysGenerateRightAlt: %d", self.modifierMapping.bothOptionKeysGenerateRightAlt); - - // assign custom keyboard tag in Sentry to default keyboard - [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { - [scope setTagValue:keyboardInfo.keyboardId forKey:@"keyboard"]; - }]; } - (void)setKvk:(KVKFile *)kvk { + if (kvk != nil) { + os_log_info([KMLogs lifecycleLog], "*** setKvk, kvk.associatedKeyboard: %{public}@", kvk.associatedKeyboard); + } else { + os_log_info([KMLogs lifecycleLog], "*** setKvk, setting to nil"); + } _kvk = kvk; - if (_oskWindow != nil) + + if (_oskWindow != nil) { [_oskWindow resetOSK]; + } } - (void)setKeyboardName:(NSString *)keyboardName { @@ -616,6 +651,8 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef if (!_activeKeyboards) { os_log_debug([KMLogs dataLog], "initializing activeKeyboards"); _activeKeyboards = [[KMSettingsRepository.shared readActiveKeyboards] mutableCopy]; + + [KMSentryHelper addActiveKeyboardCountTag:_activeKeyboards.count]; } return _activeKeyboards; @@ -626,6 +663,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [KMSettingsRepository.shared writeActiveKeyboards:_activeKeyboards]; [self resetActiveKeyboards]; [self updateKeyboardMenuItems]; + [KMSentryHelper addActiveKeyboardCountTag:self.activeKeyboards.count]; } - (void)clearActiveKeyboards { @@ -825,6 +863,8 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [self setSelectedKeyboard:keyboardName inMenuItem:menuItem]; [self setSelectedKeyboard:keyboardName]; [self setContextBuffer:nil]; + + [KMSentryHelper addInfoBreadCrumb:@"startup" message:[NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName]]; } - (void) addKeyboardPlaceholderMenuItem { @@ -926,8 +966,11 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef } - (NSWindowController *)oskWindow { - if (!_oskWindow) + if (!_oskWindow) { _oskWindow = [[OSKWindowController alloc] initWithWindowNibName:@"OSKWindowController"]; + + os_log_debug([KMLogs oskLog], "Loaded oskWindow from Nib, isVisible: %{public}@ readShowOskOnActivate: %{public}@", [_oskWindow.window isVisible]?@"true":@"false", [KMSettingsRepository.shared readShowOskOnActivate]?@"true":@"false"); + } return _oskWindow; } @@ -948,6 +991,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [[self.oskWindow window] makeKeyAndOrderFront:nil]; [[self.oskWindow window] setLevel:NSStatusWindowLevel]; [[self.oskWindow window] setTitle:self.oskWindowTitle]; + [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]]; } - (void)showAboutWindow { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 2e38dd4bc3..d4ab67d10f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -12,6 +12,7 @@ #import "TextApiCompliance.h" #import "KMSettingsRepository.h" #import "KMLogs.h" +#import "KMSentryHelper.h" @import Sentry; @interface KMInputMethodEventHandler () @@ -22,6 +23,7 @@ @property (nonatomic, retain) NSString* clientApplicationId; @property NSString *queuedText; @property BOOL contextChanged; +@property BOOL sentryTestingEnabled; @end @implementation KMInputMethodEventHandler @@ -33,9 +35,6 @@ const NSTimeInterval kQueuedTextEventDelayInSeconds = 0.1; CGKeyCode _keyCodeOfOriginalEvent; CGEventSourceRef _sourceFromOriginalEvent = nil; CGEventSourceRef _sourceForGeneratedEvent = nil; -NSMutableString* _easterEggForSentry = nil; -NSString* const kEasterEggText = @"Sentry force now"; -NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; // This is the public initializer. - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender { @@ -44,24 +43,12 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; _generatedBackspaceCount = 0; _lowLevelBackspaceCount = 0; _queuedText = nil; - - BOOL forceSentryCrash = [KMSettingsRepository.shared readForceSentryError]; - - // In Xcode, if Keyman is the active IM and the settings include the - // forceSentryCrash flag and "English plus Spanish" is the current keyboard - // and you type "Sentry force now", it will force a simulated crash to - // test reporting to sentry.keyman.com - if (forceSentryCrash && [clientAppId isEqual: @"com.apple.dt.Xcode"]) { - os_log_debug([KMLogs testLog], "initWithClient, preparing to force Sentry crash."); - _easterEggForSentry = [[NSMutableString alloc] init]; - } - else - _easterEggForSentry = nil; - - [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { - [scope setTagValue:clientAppId forKey:@"clientAppId"]; - }]; + _apiCompliance = [[TextApiCompliance alloc]initWithClient:sender applicationId:clientAppId]; + os_log_info([KMLogs lifecycleLog], "KMInputMethodEventHandler initWithClient, clientAppId: %{public}@", clientAppId); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"KMInputMethodEventHandler initWithClient, clientAppId '%@'", clientAppId]]; + + [KMSentryHelper addClientAppIdTag:clientAppId]; return self; } @@ -110,9 +97,15 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; - (BOOL) handleEventWithKeymanEngine:(NSEvent *)event in:(id) sender { CoreKeyOutput *output = nil; + if ([self.appDelegate canForceSentryEvents]) { + if ([self forceSentryEvent:event]) { + return NO; + } + } + output = [self processEventWithKeymanEngine:event in:sender]; + if (output == nil) { - [self checkEventForSentryEasterEgg:event]; return NO; } @@ -139,28 +132,28 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; return coreKeyOutput; } -- (void)checkEventForSentryEasterEgg:(NSEvent *)event { - if (_easterEggForSentry != nil) { - NSString * kmxName = [[self.kme.kmx filePath] lastPathComponent]; - os_log_debug([KMLogs testLog], "Sentry - KMX name: %{public}@", kmxName); - if ([kmxName isEqualToString:kEasterEggKmxName]) { - NSUInteger len = [_easterEggForSentry length]; - os_log_debug([KMLogs testLog], "Sentry - Processing character(s): %{public}@", [event characters]); - if ([[event characters] characterAtIndex:0] == [kEasterEggText characterAtIndex:len]) { - NSString *characterToAdd = [kEasterEggText substringWithRange:NSMakeRange(len, 1)]; - os_log_debug([KMLogs testLog], "Sentry - Adding character to Easter Egg code string: %{public}@", characterToAdd); - [_easterEggForSentry appendString:characterToAdd]; - if ([_easterEggForSentry isEqualToString:kEasterEggText]) { - os_log_debug([KMLogs testLog], "Sentry - Forcing crash now"); - [SentrySDK crash]; - } - } - else if (len > 0) { - os_log_debug([KMLogs testLog], "Sentry - Clearing Easter Egg code string."); - [_easterEggForSentry setString:@""]; - } - } +/** + * When in the mode to test sentry, this method will force certain Sentry APIs to be called, + * such as forcing a crash or capturing a message, depending on the key being typed. + */ +- (BOOL)forceSentryEvent:(NSEvent *)event { + BOOL forcedEvent = YES; + + NSString *sentryCommand = event.characters; + if ([sentryCommand isEqualToString:@"{"]) { + os_log_debug([KMLogs testLog], "forceSentryEvent: forcing crash now"); + NSBeep(); + [SentrySDK crash]; + } else if ([sentryCommand isEqualToString:@"["]) { + os_log_debug([KMLogs testLog], "forceSentryEvent: forcing message now"); + NSBeep(); + [SentrySDK captureMessage:@"Forced test message"]; + } else { + os_log_debug([KMLogs testLog], "forceSentryEvent: unrecognized command"); + forcedEvent = NO; } + + return forcedEvent; } /** @@ -174,7 +167,8 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; */ - (void)handleBackspace:(NSEvent *)event { os_log_debug([KMLogs eventsLog], "KMInputMethodEventHandler handleBackspace, event = %{public}@", event); - + [KMSentryHelper addInfoBreadCrumb:@"user" message:@"handle backspace for non-compliant app"]; + if (self.generatedBackspaceCount > 0) { self.generatedBackspaceCount--; self.lowLevelBackspaceCount++; @@ -194,13 +188,10 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; //MARK: Core-related key processing - (void)checkTextApiCompliance:(id)client { - // If the TextApiCompliance object is nil or the app or the context has changed, - // then create a new object for the current application and context. + // If the TextApiCompliance object is stale, create a new one for the current application and context. // The text api compliance may vary from one text field to another of the same app. - if ((self.apiCompliance == nil) || - (![self.apiCompliance.clientApplicationId isEqualTo:self.clientApplicationId]) || - self.contextChanged) { + if ([self textApiComplianceIsStale]) { self.apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:self.clientApplicationId]; os_log_debug([KMLogs complianceLog], "KMInputMethodHandler initWithClient checkTextApiCompliance: %{public}@", _apiCompliance); } else if (self.apiCompliance.isComplianceUncertain) { @@ -209,6 +200,33 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; } } +- (BOOL)textApiComplianceIsStale { + BOOL stale = false; + NSString *complianceAppId = nil; + TextApiCompliance *currentApiCompliance = self.apiCompliance; + + // test for three scenarios in which the api compliance is stale + if (currentApiCompliance == nil) { // if we have no previous api compliance object + stale = true; + } else { // if we have one but for a different client + complianceAppId = self.apiCompliance.clientApplicationId; + if ([complianceAppId isNotEqualTo:self.clientApplicationId]) { + stale = true; + } else { + stale = false; + } + } + if (self.contextChanged) { // if the context has changed + stale = true; + } + + NSString *message = [NSString stringWithFormat:@"textApiComplianceIsStale = %@ for appId: %@, apiCompliance: %p, complianceAppId: %@", stale?@"true":@"false", self.clientApplicationId, currentApiCompliance, complianceAppId]; + [KMSentryHelper addDebugBreadCrumb:@"compliance" message:message]; + os_log_debug([KMLogs complianceLog], "%{public}@", message); + + return stale; +} + - (void) handleContextChangedByLowLevelEvent { if (self.appDelegate.contextChangedByLowLevelEvent) { if (!self.contextChanged) { @@ -230,6 +248,7 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; [self handleContextChangedByLowLevelEvent]; if (event.type == NSEventTypeKeyDown) { + [KMSentryHelper addDebugBreadCrumb:@"event" message:@"handling keydown event"]; // indicates that our generated backspace event(s) are consumed // and we can insert text that followed the backspace(s) if (event.keyCode == kKeymanEventKeyCode) { @@ -422,6 +441,7 @@ NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; NSString *value = [options objectForKey:key]; if(key && value) { os_log_debug([KMLogs keyLog], "persistOptions, key: %{public}@, value: %{public}@", key, value); + [KMSentryHelper addInfoBreadCrumb:@"event" message:@"persist options"]; [[KMSettingsRepository shared] writeOptionForSelectedKeyboard:key withValue:value]; } else { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h index 62a3aac35d..e467e645bd 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h @@ -15,7 +15,8 @@ extern NSString *const kInputMethodClientChangeNotification; @interface KMInputMethodLifecycle : NSObject + (KMInputMethodLifecycle *)shared; -+ (NSString*)getClientApplicationId; ++ (NSString*)getRunningApplicationId; +@property NSString *clientApplicationId; - (void)startLifecycle; - (void)activateClient:(id)client; - (void)deactivateClient:(id)client; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m index 9b877a530e..c60fbaebf4 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m @@ -10,7 +10,7 @@ */ /** - * This class is needed because many activateServer and deactivateServer messages sent from macOS + * This class is needed because many activateServer and deactivateServer messages are sent from macOS * to KMInputController, but they are not particularly reliable. Keyman receives some messages when it * is not active and should not become active. It also receives messages when it is active, but there is no * need to change state. For example, when a menu is clicked with Keyman active, macOS will send a @@ -36,6 +36,7 @@ #import #import "KMSettingsRepository.h" #import +#import "KMSentryHelper.h" NSString *const kInputMethodActivatedNotification = @"kInputMethodActivatedNotification"; NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedNotification"; @@ -61,11 +62,12 @@ typedef enum { @property LifecycleState lifecycleState; @property NSString *inputSourceId; -@property NSString *clientApplicationId; @end @implementation KMInputMethodLifecycle +@synthesize lifecycleState = _lifecycleState; + + (KMInputMethodLifecycle *)shared { static KMInputMethodLifecycle *shared = nil; static dispatch_once_t onceToken; @@ -86,11 +88,40 @@ typedef enum { return self; } +- (void)setLifecycleState:(LifecycleState)state { + _lifecycleState = state; + + // whenever the state is changed, update the Sentry tag + [self addLifecycleStateSentryTag]; +} + +- (void)addLifecycleStateSentryTag { + NSString *stateString = @"Unknown"; + switch (self.lifecycleState) { + case Started: + stateString = @"Started"; + break; + case Active: + stateString = @"Active"; + break; + case Inactive: + stateString = @"Inactive"; + break; + } + [KMSentryHelper addLifecycleStateTag:stateString]; + os_log_info([KMLogs lifecycleLog], "setLifecycleState: %{public}@", stateString); +} + + +- (LifecycleState)lifecycleState { + return _lifecycleState; +} + /** * called from Application Delgate during init */ - (void)startLifecycle { - _lifecycleState = Started; + self.lifecycleState = Started; } /** @@ -106,9 +137,10 @@ typedef enum { /** * Get the bundle ID of the currently active text input client.. */ -+ (NSString*)getClientApplicationId { ++ (NSString*)getRunningApplicationId { NSRunningApplication *currentApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; NSString *clientAppId = [currentApp bundleIdentifier]; + os_log_debug([KMLogs lifecycleLog], "getRunningApplicationId, frontmost: %{public}@", clientAppId); return clientAppId; } @@ -156,7 +188,7 @@ typedef enum { */ - (void)performTransition:(id)client { NSString *currentInputSource = [KMInputMethodLifecycle getCurrentInputSourceId]; - NSString *currentClientAppId = [KMInputMethodLifecycle getClientApplicationId]; + NSString *currentClientAppId = [KMInputMethodLifecycle getRunningApplicationId]; TransitionType transition = [self determineTransition:currentInputSource withAppId:currentClientAppId]; [self saveNewInputMethodState:currentInputSource withAppId:currentClientAppId]; @@ -166,7 +198,8 @@ typedef enum { os_log_info([KMLogs lifecycleLog], "performTransition: None, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); break; case Activate: - os_log_info([KMLogs lifecycleLog], "performTransition: Activate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + os_log_info([KMLogs lifecycleLog], "performTransition: Activate, new application ID: %{public}@", currentClientAppId); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"activated input method '%@' for application ID '%@'", currentInputSource, currentClientAppId]]; /** * Perform two actions when activating the input method. * Change the client first which prepares the event handler. @@ -177,10 +210,12 @@ typedef enum { break; case Deactivate: os_log_info([KMLogs lifecycleLog], "performTransition: Deactivate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"deactivated input method '%@' for application ID '%@'", currentInputSource, currentClientAppId]]; [self deactivateInputMethod]; break; case ChangeClients: os_log_info([KMLogs lifecycleLog], "performTransition: ChangeClients, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"change clients for input method '%@' to application ID '%@'", currentInputSource, currentClientAppId]]; [self changeClient]; break; } @@ -214,7 +249,7 @@ typedef enum { */ - (void)activateInputMethod { os_log_debug([KMLogs lifecycleLog], "activateInputMethod"); - _lifecycleState = Active; + self.lifecycleState = Active; [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodActivatedNotification object:self]; } @@ -223,7 +258,7 @@ typedef enum { */ - (void)deactivateInputMethod { os_log_debug([KMLogs lifecycleLog], "deactivateInputMethod"); - _lifecycleState = Inactive; + self.lifecycleState = Inactive; [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodDeactivatedNotification object:self]; } @@ -231,7 +266,7 @@ typedef enum { * Does not change lifecycleState, just fires notification so that InputController knows to change the event handler */ - (void)changeClient { - os_log_debug([KMLogs lifecycleLog], "changeClient"); + os_log_debug([KMLogs lifecycleLog], "changeClient, posting kInputMethodClientChangeNotification"); [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodClientChangeNotification object:self]; } @@ -243,11 +278,10 @@ typedef enum { } /** - * Returns true if lifecycleState is Active and the Settings require us to show the OSK + * Returns true the Settings require us to show the OSK on activation */ - (BOOL)shouldShowOskOnActivate { - return [KMSettingsRepository.shared readShowOskOnActivate] - && (self.lifecycleState == Active); + return [KMSettingsRepository.shared readShowOskOnActivate]; } @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.h similarity index 84% rename from mac/Keyman4MacIM/Keyman4MacIM/KMLogs.h rename to mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.h index 002bc5023c..f7f83702ec 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.h @@ -1,4 +1,4 @@ -/** +/* * Keyman is copyright (C) SIL International. MIT License. * * KMLogs.h @@ -6,7 +6,6 @@ * * Created by Shawn Schantz on 2024-05-16. * - * Contains methods to get singleton logger objects and constants for subsystem and category names. */ #import diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.m b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.m similarity index 99% rename from mac/Keyman4MacIM/Keyman4MacIM/KMLogs.m rename to mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.m index f2d4dc4d53..6a8a5cd957 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.m @@ -1,4 +1,4 @@ -/** +/* * Keyman is copyright (C) SIL International. MIT License. * * KMLogs.m diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h new file mode 100644 index 0000000000..75a5c27282 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h @@ -0,0 +1,28 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-01-13. + * + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KMSentryHelper : NSObject + ++ (void)addLifecycleStateTag: (NSString *)value; ++ (void)addApiComplianceTag: (NSString *)value; ++ (void)addArchitectureTag: (NSString *)value; ++ (void)addOskVisibleTag:(BOOL)value; ++ (void)addClientAppIdTag:(NSString *)value; ++ (void)addKeyboardTag:(NSString *)value; ++ (void)addHasAccessibilityTag:(BOOL)value; ++ (void)addActiveKeyboardCountTag:(NSUInteger)value; ++ (void)addInfoBreadCrumb:(NSString *)category message:(NSString *)messageText; ++ (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText; ++ (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText; + +@end + +NS_ASSUME_NONNULL_END diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m new file mode 100644 index 0000000000..7c37be9ad9 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m @@ -0,0 +1,98 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-01-13. + * + * Description + */ + +#import "KMSentryHelper.h" +@import Sentry; + +@implementation KMSentryHelper + +NSString * const kApiComplianceTagName = @"apiCompliance"; +NSString * const kArchitectureTagName = @"architecture"; +NSString * const kOskVisibleTagName = @"oskVisible"; +NSString * const kClientAppIdTagName = @"clientAppId"; +NSString * const kKeyboardTagName = @"keyboard"; +NSString * const kHasAccessibilityTagName = @"accessibilityEnabled"; +NSString * const kActiveKeyboardCountTagName = @"activeKeyboardCount"; +NSString * const kLifecycleStateTagName = @"lifecycleState"; + ++ (void)addLifecycleStateTag: (NSString *)value { + [self addCustomTag:kLifecycleStateTagName withValue:value]; +} + ++ (void)addApiComplianceTag: (NSString *)value { + [self addCustomTag:kApiComplianceTagName withValue:value]; +} + ++ (void)addArchitectureTag: (NSString *)value { + [self addCustomTag:kArchitectureTagName withValue:value]; +} + ++ (void)addOskVisibleTag:(BOOL)value { + [self addCustomTag:kOskVisibleTagName withValue:value?@"true":@"false"]; +} + ++ (void)addClientAppIdTag:(NSString *)value { + [self addCustomTag:kClientAppIdTagName withValue:value]; +} + ++ (void)addKeyboardTag:(NSString *)value { + [self addCustomTag:kKeyboardTagName withValue:value]; +} + ++ (void)addHasAccessibilityTag:(BOOL)value { + [self addCustomTag:kHasAccessibilityTagName withValue:value?@"true":@"false"]; +} + ++ (void)addActiveKeyboardCountTag:(NSUInteger)value { + [self addCustomTag:kActiveKeyboardCountTagName withValue:[[NSNumber numberWithUnsignedLong:value] stringValue]]; +} + +/** + *assign custom keyboard tag in Sentry for the given name and value + */ ++ (void)addCustomTag:(NSString *)tagName withValue:(NSString *)value { + [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { + [scope setTagValue:value forKey:tagName]; + }]; +} + +/** + *add a Sentry breadcrumb message of level Info with the specified category + */ ++ (void)addInfoBreadCrumb:(NSString *)category message:(NSString *)messageText { + SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init]; + crumb.level = kSentryLevelInfo; + crumb.category = category; + crumb.message = messageText; + [SentrySDK addBreadcrumb:crumb]; +} + +/** + *add a Sentry breadcrumb message of level debug with the specified category + */ ++ (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText { + SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init]; + crumb.level = kSentryLevelDebug; + crumb.category = category; + crumb.message = messageText; + [SentrySDK addBreadcrumb:crumb]; +} + +/** + *add a Sentry breadcrumb message of type 'user' and level debug with the specified category + */ ++ (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText { + SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init]; + crumb.type = @"user"; + crumb.level = kSentryLevelInfo; + crumb.category = category; + crumb.message = messageText; + [SentrySDK addBreadcrumb:crumb]; +} + +@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m index b3824fa897..d4f457c754 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m @@ -11,6 +11,7 @@ #import "KMInputMethodLifecycle.h" #import "KMSettingsRepository.h" #import "KMLogs.h" +#import "KMSentryHelper.h" @interface OSKWindowController () @property (nonatomic, strong) NSButton *helpButton; @@ -76,6 +77,7 @@ // whenever the OSK is closing clear all of its modifier keys [self.oskView clearOskModifiers]; + [KMSentryHelper addOskVisibleTag:NO]; } - (void)helpAction:(id)sender { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib index b753e43802..6a55541cb5 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib @@ -14,7 +14,7 @@ - + diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m index e83439794b..81f50c5edd 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m @@ -36,6 +36,7 @@ #import #import "KMInputMethodAppDelegate.h" #import "KMLogs.h" +#import "KMSentryHelper.h" // this is the user managed list of non-compliant apps persisted in User Defaults NSString *const kKMLegacyApps = @"KMLegacyApps"; @@ -62,6 +63,10 @@ NSString *const kKMLegacyApps = @"KMLegacyApps"; _clientApplicationId = appId; _complianceUncertain = YES; _initialSelection = NSMakeRange(NSNotFound, NSNotFound); + + NSString *message = [NSString stringWithFormat:@"TextApiCompliance initWithClient, client: %p applicationId: %@", client, appId]; + [KMSentryHelper addDebugBreadCrumb:@"compliance" message:message]; + os_log_debug([KMLogs complianceLog], "%{public}@", message); // if we do not have hard-coded noncompliance, then test the app if (![self applyNoncompliantAppLists:appId]) { @@ -76,6 +81,16 @@ NSString *const kKMLegacyApps = @"KMLegacyApps"; return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelectionApi: %d, canReadText: %d, canReplaceText: %d, mustBackspaceUsingEvents: %d, clientApplicationId: %@, client: %@", self.complianceUncertain, self.hasCompliantSelectionApi, [self canReadText], [self canReplaceText], [self mustBackspaceUsingEvents], _clientApplicationId, _client]; } +/** + * For Sentry, create a short description as the tag value is limited to 200 characters. + * We don't know how long the clientAppId will be, but this should not be well below 200 characters. + * If the text were to exceed 200 characters, then Sentry would display an error rather than truncating. + */ +-(NSString *)shortSentryTagDescription +{ + return [NSString stringWithFormat:@"uncertain: %@, compliant: %@, clientAppId: %@, ", self.complianceUncertain?@"true":@"false", self.hasCompliantSelectionApi?@"true":@"false", _clientApplicationId]; +} + /** test to see if the API selectedRange functions properly for the text input client */ -(void) checkCompliance:(id) client { // confirm that the API actually exists (this always seems to return true) @@ -91,6 +106,7 @@ NSString *const kKMLegacyApps = @"KMLegacyApps"; [self checkComplianceUsingInitialSelection]; } os_log_debug([KMLogs complianceLog], "checkCompliance workingSelectionApi for app %{public}@: set to %{public}@", self.clientApplicationId, self.complianceUncertain?@"YES":@"NO"); + [KMSentryHelper addApiComplianceTag:self.shortSentryTagDescription]; } -(void) checkComplianceUsingInitialSelection { @@ -135,6 +151,7 @@ NSString *const kKMLegacyApps = @"KMLegacyApps"; } os_log_info([KMLogs complianceLog], "checkComplianceAfterInsert, self.hasWorkingSelectionApi = %{public}@ for app %{public}@", self.hasCompliantSelectionApi?@"YES":@"NO", self.clientApplicationId); + [KMSentryHelper addApiComplianceTag:self.shortSentryTagDescription]; } - (BOOL)validateNewLocation:(NSUInteger)location delete:(NSString *)textToDelete { diff --git a/mac/docs/help/about/history.md b/mac/docs/help/about/history.md index 3a0b9af3a6..76320b31e5 100644 --- a/mac/docs/help/about/history.md +++ b/mac/docs/help/about/history.md @@ -1,5 +1,5 @@ --- -title: Version History +title: Version History of Keyman for macOS --- A frequent updated changelog for Keyman is kept at [help.keyman.com/version-history](https://help.keyman.com/version-history/) on the Keyman Help website. diff --git a/mac/docs/help/about/requirements.md b/mac/docs/help/about/requirements.md index d92272a730..07745f9efc 100644 --- a/mac/docs/help/about/requirements.md +++ b/mac/docs/help/about/requirements.md @@ -1,5 +1,5 @@ --- -title: System Requirements +title: System Requirements - Keyman for macOS Help --- ## Supported Mac Operating Systems diff --git a/mac/docs/help/about/welcome.md b/mac/docs/help/about/welcome.md index 5bd92f2fd8..ec67a871e8 100644 --- a/mac/docs/help/about/welcome.md +++ b/mac/docs/help/about/welcome.md @@ -1,5 +1,5 @@ --- -title: Welcome to Keyman +title: Welcome to Keyman for macOS --- Thank you for installing Keyman for macOS. Whether you're a new or returning user, diff --git a/mac/docs/help/about/whatsnew.md b/mac/docs/help/about/whatsnew.md index f4b11b973c..73ccb4d49b 100644 --- a/mac/docs/help/about/whatsnew.md +++ b/mac/docs/help/about/whatsnew.md @@ -1,5 +1,5 @@ --- -title: What's New +title: What's New in Keyman 18.0 for macOS --- Here are some of the new features we have added to Keyman 18.0 for macOS: diff --git a/mac/docs/help/basic/index.md b/mac/docs/help/basic/index.md index 8b3d0df07a..e09ff4f7b5 100644 --- a/mac/docs/help/basic/index.md +++ b/mac/docs/help/basic/index.md @@ -1,5 +1,5 @@ --- -title: Basic Help +title: Keyman for macOS Basic Help --- * [Keyman Configuration](config/) diff --git a/mac/docs/help/start/install-keyboard.md b/mac/docs/help/start/install-keyboard.md index bf9310c8bb..e22b3fad56 100644 --- a/mac/docs/help/start/install-keyboard.md +++ b/mac/docs/help/start/install-keyboard.md @@ -1,5 +1,5 @@ --- -title: Install a Keyman Keyboard +title: Install a Keyman Keyboard on macOS --- You must have a Keyman keyboard installed to type with Keyman for macOS in your language. diff --git a/mac/docs/help/troubleshooting/index.md b/mac/docs/help/troubleshooting/index.md index 2190a7c3cb..9325371dae 100644 --- a/mac/docs/help/troubleshooting/index.md +++ b/mac/docs/help/troubleshooting/index.md @@ -1,5 +1,5 @@ --- -title: Troubleshooting +title: Keyman for macOS Troubleshooting --- * [Configure macOS security for Keyman](configure-security) diff --git a/mac/docs/help/troubleshooting/uninstall-keyman.md b/mac/docs/help/troubleshooting/uninstall-keyman.md index 64ba2648e0..e1add66120 100644 --- a/mac/docs/help/troubleshooting/uninstall-keyman.md +++ b/mac/docs/help/troubleshooting/uninstall-keyman.md @@ -1,5 +1,5 @@ --- -title: Uninstalling Keyman +title: Uninstalling Keyman for macOS --- To uninstall Keyman for macOS: diff --git a/resources/build/increment-version.sh b/resources/build/increment-version.sh index da459776eb..3be11dba1f 100755 --- a/resources/build/increment-version.sh +++ b/resources/build/increment-version.sh @@ -179,7 +179,7 @@ if [ "$action" == "commit" ]; then # In order to avoid potential git conflicts, we run the history collater # again on the master HISTORY.md. Note that the script always exits 1 to # indicate it hasn't updated VERSION.md. We could tweak that in the future. - node resources/build/version/lib/index.js history --no-write-github-comment --github-pr -t "$GITHUB_TOKEN" -b "$base" || true + node resources/build/version/build/src/index.js history --no-write-github-comment --github-pr -t "$GITHUB_TOKEN" -b "$base" || true # If HISTORY.md has been updated, then we want to create a branch and push # it for review diff --git a/resources/docker-images/android/Dockerfile b/resources/docker-images/android/Dockerfile index 91cd095a69..2b289ce077 100644 --- a/resources/docker-images/android/Dockerfile +++ b/resources/docker-images/android/Dockerfile @@ -22,6 +22,7 @@ RUN <> /usr/bin/bashwrapper +RUN mkdir -p /run/user/1000 && \ + chown build:build /run/user/1000 && \ + chmod 700 /run/user/1000 && \ + mkdir -p /tmp/.X11-unix && \ + chmod 1777 /tmp/.X11-unix && \ + echo "export XDG_RUNTIME_DIR=/run/user/1000" >> /usr/bin/bashwrapper COPY run-tests.sh /usr/bin/run-tests.sh diff --git a/resources/docker-images/run.sh b/resources/docker-images/run.sh index c42d6c2a02..63d299f7e3 100755 --- a/resources/docker-images/run.sh +++ b/resources/docker-images/run.sh @@ -36,9 +36,11 @@ run_core() { run_linux() { mkdir -p "${KEYMAN_ROOT}/linux/build/docker-linux" + mkdir -p "${KEYMAN_ROOT}/linux/keyman-system-service/build/docker-linux" docker run -it --privileged --rm -v "${KEYMAN_ROOT}":/home/build/build \ -v "${KEYMAN_ROOT}/core/build/docker-core":/home/build/build/core/build \ -v "${KEYMAN_ROOT}/linux/build/docker-linux":/home/build/build/linux/build \ + -v "${KEYMAN_ROOT}/linux/keyman-system-service/build/docker-linux":/home/build/build/linux/keyman-system-service/build \ -e DESTDIR=/tmp \ keymanapp/keyman-linux-ci:default \ "${builder_extra_params[@]}" diff --git a/resources/docker-images/web/Dockerfile b/resources/docker-images/web/Dockerfile index 5a27c9724d..1f08daf92c 100644 --- a/resources/docker-images/web/Dockerfile +++ b/resources/docker-images/web/Dockerfile @@ -12,7 +12,8 @@ LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git" LABEL org.opencontainers.image.title="Keyman for Web Build Image" USER root -RUN apt-get install -qy git jq xvfb xserver-xephyr metacity +RUN apt-get update && \ + apt-get install -qy git jq xvfb xserver-xephyr metacity # For playwright: RUN apt-get install -qy libevent-2.1-7t64 libxslt1.1 libwoff1 libvpx9 \ libgstreamer-plugins-bad1.0-0 libwebpdemux2 libharfbuzz-icu0 \ diff --git a/web/docs/engine/guide/examples/__auto-control.html b/web/docs/engine/guide/examples/__auto-control.html index 9e1786c6d7..e7fc2768c6 100644 --- a/web/docs/engine/guide/examples/__auto-control.html +++ b/web/docs/engine/guide/examples/__auto-control.html @@ -20,9 +20,12 @@ +

Automatic Mode Example

+ +Back to Document \ No newline at end of file diff --git a/web/docs/engine/guide/examples/__control-by-control.html b/web/docs/engine/guide/examples/__control-by-control.html index b1f7ac5e42..440a7862d0 100644 --- a/web/docs/engine/guide/examples/__control-by-control.html +++ b/web/docs/engine/guide/examples/__control-by-control.html @@ -33,6 +33,7 @@ +

Control-by-Control Example

Email to (KeymanWeb not enabled)

@@ -43,5 +44,6 @@
+Back to Document \ No newline at end of file diff --git a/web/docs/engine/guide/examples/__first-example.html b/web/docs/engine/guide/examples/__first-example.html index 3b46160f65..1657db9440 100644 --- a/web/docs/engine/guide/examples/__first-example.html +++ b/web/docs/engine/guide/examples/__first-example.html @@ -18,6 +18,9 @@ +

First Example

Click me and type:

+ +Back to Document \ No newline at end of file diff --git a/web/docs/engine/guide/examples/__full-manual-control.html b/web/docs/engine/guide/examples/__full-manual-control.html index c9d76d90fb..24b0d004a7 100644 --- a/web/docs/engine/guide/examples/__full-manual-control.html +++ b/web/docs/engine/guide/examples/__full-manual-control.html @@ -43,7 +43,7 @@ window.addEventListener( 'load' , SetupDocument ); - +

Manual Control - Custom Interface

Keyboard:

@@ -51,5 +51,6 @@ window.addEventListener( 'load' , SetupDocument );

+Back to Document diff --git a/web/docs/engine/guide/examples/__manual-control.html b/web/docs/engine/guide/examples/__manual-control.html index 37776a7850..e9f5f219ca 100644 --- a/web/docs/engine/guide/examples/__manual-control.html +++ b/web/docs/engine/guide/examples/__manual-control.html @@ -32,7 +32,7 @@ - +

Manual Mode Example

KeymanWeb

@@ -40,6 +40,7 @@

+Back to Document
\ No newline at end of file diff --git a/web/src/app/browser/src/hardwareEventKeyboard.ts b/web/src/app/browser/src/hardwareEventKeyboard.ts index 0bb594e897..5e8b30ce4f 100644 --- a/web/src/app/browser/src/hardwareEventKeyboard.ts +++ b/web/src/app/browser/src/hardwareEventKeyboard.ts @@ -326,7 +326,7 @@ export default class HardwareEventKeyboard extends HardKeyboard { inputEle.form.submit(); } else { const nextElement = this.contextManager.page.findNeighboringInput(inputEle, false); - nextElement.focus(); + nextElement?.focus(); } } return true; diff --git a/web/src/engine/dom-utils/src/stylesheets.ts b/web/src/engine/dom-utils/src/stylesheets.ts index 977f253ff7..5318e28f61 100644 --- a/web/src/engine/dom-utils/src/stylesheets.ts +++ b/web/src/engine/dom-utils/src/stylesheets.ts @@ -136,8 +136,10 @@ export class StylesheetManager { } // Build the font-face definition according to the browser being used - var s='@font-face {\nfont-family:' - + fd.family + ';\nfont-style:normal;\nfont-weight:normal;\n'; + // The OSK will similarly remove double-quotes from font-family names and double-quote + // the name itself. (#13018, #13022) + var s='@font-face {\nfont-family:"' + + fd.family.replace(/\u0022/g, '') + '";\nfont-style:normal;\nfont-weight:normal;\n'; // Build the font source string according to the browser, // but return without adding the style sheet if the required font type is unavailable @@ -148,29 +150,32 @@ export class StylesheetManager { const formatStartIndex = 'data:font/'.length; const format = data.substring(formatStartIndex, data.indexOf(';', formatStartIndex)); s +=`src:url('${data}'), format('${format}');`; - } else if(os == DeviceSpec.OperatingSystem.iOS) { - if(ttf != '') { - if(this.doCacheBusting) { - ttf = this.cacheBust(ttf); - } - source = "url('"+encodeURI(ttf)+"') format('truetype')"; - } } else { - if(woff != '') { - source = "url('"+encodeURI(woff)+"') format('woff')"; + // Note: encodeURI("'") == "'", but encodeURI('"') == "%22" (#13018, #13022) + if(os == DeviceSpec.OperatingSystem.iOS) { + if(ttf != '') { + if(this.doCacheBusting) { + ttf = this.cacheBust(ttf); + } + source = "url(\""+encodeURI(ttf)+"\") format('truetype')"; + } + } else { + if(woff != '') { + source = "url(\""+encodeURI(woff)+"\"') format('woff')"; + } + + if(ttf != '') { + source = "url(\""+encodeURI(ttf)+"\") format('truetype')"; + } } - if(ttf != '') { - source = "url('"+encodeURI(ttf)+"') format('truetype')"; + if(!source) { + return null; } + + s += 'src:'+source+';'; } - if(!source) { - return null; - } - - s += 'src:'+source+';'; - s=s+'\n}\n'; const sheet = createStyleSheet(s); diff --git a/web/src/engine/js-processor/src/kbdInterface.ts b/web/src/engine/js-processor/src/kbdInterface.ts index b39b2baea2..efca7b8a80 100644 --- a/web/src/engine/js-processor/src/kbdInterface.ts +++ b/web/src/engine/js-processor/src/kbdInterface.ts @@ -373,18 +373,6 @@ export default class KeyboardInterface extends KeyboardHarness { // Now that we have the cache... var subCache = cache; subCache.valContext = subCache.valContext.slice(0, ln); - for(var i=0; i < subCache.valContext.length; i++) { - if(subCache.valContext[i] == '\ufffe') { - subCache.valContext.splice(0, 1); - subCache.deadContext.splice(0, 1); - } - } - - if(subCache.valContext.length == 0) { - subCache.valContext = ['\ufffe']; - subCache.deadContext = []; - } - this.cachedContextEx.set(n, ln, subCache); return subCache; diff --git a/web/src/engine/keyboard-storage/src/cloud/queryEngine.ts b/web/src/engine/keyboard-storage/src/cloud/queryEngine.ts index 5f38c32aea..e0ebe1aed6 100644 --- a/web/src/engine/keyboard-storage/src/cloud/queryEngine.ts +++ b/web/src/engine/keyboard-storage/src/cloud/queryEngine.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'eventemitter3'; import { PathConfiguration } from 'keyman/engine/interfaces'; -import { default as KeyboardStub, ErrorStub, KeyboardAPISpec, mergeAndResolveStubPromises } from '../keyboardStub.js'; +import { default as KeyboardStub, ErrorStub, KeyboardAPISpec } from '../keyboardStub.js'; import { LanguageAPIPropertySpec, ManagedPromise, Version } from 'keyman/engine/keyboard'; import CloudRequesterInterface from './requesterInterface.js'; @@ -310,7 +310,7 @@ export default class CloudQueryEngine extends EventEmitter { * @param x keyboard name string or keyboard metadata JSON object * @returns resolved or rejected promise with merged array of stubs. */ - async fetchCloudStubs(cloudList: string[]): Promise<(KeyboardStub|ErrorStub)[]> { + async fetchCloudStubs(cloudList: string[]): Promise<(KeyboardStub)[]> { // // Ensure keymanweb is initialized before continuing to add keyboards // if(!this.keymanweb.initialized) { // await this.deferment; @@ -328,17 +328,12 @@ export default class CloudQueryEngine extends EventEmitter { comma=','; } - let errorStubs: ErrorStub[] = []; // Request keyboard metadata from the Keyman Cloud keyboard metadata server try { - let result: (KeyboardStub|ErrorStub)[]|Error = await this.keymanCloudRequest(cmd,false); - return mergeAndResolveStubPromises(result, errorStubs); + return await this.keymanCloudRequest(cmd,false); } catch(err) { // We don't have keyboard info for this ErrorStub - console.error(err); - let stub: ErrorStub = {error: err}; - errorStubs.push(stub); - return Promise.reject(errorStubs); + return Promise.reject(err); } } } \ No newline at end of file diff --git a/web/src/engine/keyboard-storage/src/keyboardRequisitioner.ts b/web/src/engine/keyboard-storage/src/keyboardRequisitioner.ts index fb2807de6f..5ca184b217 100644 --- a/web/src/engine/keyboard-storage/src/keyboardRequisitioner.ts +++ b/web/src/engine/keyboard-storage/src/keyboardRequisitioner.ts @@ -18,6 +18,7 @@ import { toUnprefixedKeyboardId as unprefixed } from "./index.js"; import { default as CloudRequesterInterface } from "./cloud/requesterInterface.js"; +import { rejectErrorStubs } from "./keyboardStub.js"; class CloudRequestEntry { id: string; @@ -287,7 +288,7 @@ export default class KeyboardRequisitioner { if(cmd == '') { // No command so return errors - return Promise.reject(errorStubs); + return rejectErrorStubs(errorStubs); } return this.cloudQueryEngine.keymanCloudRequest('&keyboardid='+cmd, false).then(async (result) => { @@ -305,7 +306,7 @@ export default class KeyboardRequisitioner { console.error(err); let stub: ErrorStub = {error: err}; errorStubs.push(stub); - return Promise.reject(errorStubs); + return rejectErrorStubs(errorStubs); }); } diff --git a/web/src/engine/keyboard-storage/src/keyboardStub.ts b/web/src/engine/keyboard-storage/src/keyboardStub.ts index a2b1d87d6f..b2dfc32a88 100644 --- a/web/src/engine/keyboard-storage/src/keyboardStub.ts +++ b/web/src/engine/keyboard-storage/src/keyboardStub.ts @@ -205,12 +205,22 @@ export interface ErrorStub { error: Error; } +export function rejectErrorStubs(errorStubs: ErrorStub[]) { + if(errorStubs.length > 1) { + const msgs = errorStubs.map((entry) => `${entry.error.message}:\n ${entry.error.stack ?? ''}`); + return Promise.reject(new Error(msgs.join('\n\n'))); + } else { + // The messages for errors we'd return here should already include data about their triggering request. + return Promise.reject(errorStubs[0].error); + } +} + export function mergeAndResolveStubPromises(keyboardStubs: (KeyboardStub|ErrorStub)[], errorStubs: ErrorStub[]) : Promise<(KeyboardStub|ErrorStub)[]> { if (errorStubs.length == 0) { return Promise.resolve(keyboardStubs); } if (keyboardStubs.length == 0) { - return Promise.reject(errorStubs); + return rejectErrorStubs(errorStubs); } else { // Merge this with errorStubs let result: (KeyboardStub|ErrorStub)[] = keyboardStubs; diff --git a/web/src/engine/main/src/headless/languageProcessor.ts b/web/src/engine/main/src/headless/languageProcessor.ts index 82c9a0392c..6783e344c9 100644 --- a/web/src/engine/main/src/headless/languageProcessor.ts +++ b/web/src/engine/main/src/headless/languageProcessor.ts @@ -304,7 +304,7 @@ export class LanguageProcessor extends EventEmitter { } let alternates = transcription.alternates; - if(!alternates || alternates.length == 0) { + if(!this.mayCorrect || !alternates || alternates.length == 0) { alternates = [{ sample: transcription.transform, p: 1.0 diff --git a/web/src/engine/osk/src/banner/suggestionBanner.ts b/web/src/engine/osk/src/banner/suggestionBanner.ts index 0c4ccb420a..0cecd1aa46 100644 --- a/web/src/engine/osk/src/banner/suggestionBanner.ts +++ b/web/src/engine/osk/src/banner/suggestionBanner.ts @@ -176,7 +176,6 @@ export class BannerSuggestion { } this.currentWidth = this.collapsedWidth; - this.highlight(suggestion?.autoAccept); this.updateLayout(); } @@ -557,13 +556,10 @@ export class SuggestionBanner extends Banner { const autoselection = this._predictionContext.selected; this._predictionContext.selected = null; - if(autoselection) { - this.options.forEach((entry) => { - if(entry.suggestion == autoselection) { - entry.highlight(false); - }; - }); - } + // Always clear pre-existing selections when a new banner input/gesture starts. + this.options.forEach((entry) => { + entry.highlight(false); + }); this.scrollState = new BannerScrollState(source.currentSample, this.container.scrollLeft); const suggestion = source.baseItem; @@ -753,6 +749,9 @@ export class SuggestionBanner extends Banner { if(suggestions.length > i) { const suggestion = suggestions[i]; d.update(suggestion, optionFormat); + if(this.predictionContext.selected == suggestion) { + d.highlight(true); + } } else { d.update(null, optionFormat); } diff --git a/web/src/engine/osk/src/input/gestures/browser/multitap.ts b/web/src/engine/osk/src/input/gestures/browser/multitap.ts index 99e50810f9..76280f63e7 100644 --- a/web/src/engine/osk/src/input/gestures/browser/multitap.ts +++ b/web/src/engine/osk/src/input/gestures/browser/multitap.ts @@ -8,6 +8,7 @@ import { distributionFromDistanceMaps } from '../../../corrections.js'; import Modipress from './modipress.js'; import { keySupportsModipress } from '../specsForLayout.js'; import { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js'; +import { correctionKeyFilter } from '../../../correctionLayout.js'; /** * Represents a potential multitap gesture's implementation within KeymanWeb. @@ -104,20 +105,33 @@ export default class Multitap implements GestureHandler { keyEvent.baseTranscriptionToken = this.baseContextToken; const coord = tap.sources[0].currentSample; - const baseDistances = vkbd.getSimpleTapCorrectionDistances(coord, this.baseKey.key.spec as ActiveKey); + + const baseSpec = this.baseKey.key.spec; + const baseDistances = vkbd.getSimpleTapCorrectionDistances(coord, baseSpec as ActiveKey); + // If the key is on a different layer than is shown, what if the user meant to hit a different key? + // (Bypass if the tap is part of a modipress gesture.) if(coord.stateToken != vkbd.layerId && !tap.matchedId.includes('modipress')) { const matchKey = vkbd.layerGroup.findNearestKey({...coord, stateToken: vkbd.layerId}); - // Replace the key at the current location for the current layer key - // with the multitap base key. - const p = baseDistances.get(matchKey.key.spec); - if(p == null) { - console.warn("Could not find current layer's key") + // That said, if they directly touched a non-output key, simplify and assume + // that they hit the intended key. + if(correctionKeyFilter(matchKey.key.spec)) { + // Replace the key at the current location for the current layer key + // with the multitap base key. + const p = baseDistances.get(matchKey.key.spec); + if(p == null) { + console.warn("Could not find current layer's key") + } else { + baseDistances.delete(matchKey.key.spec); + baseDistances.set(coord.item.key.spec, p); + } } - baseDistances.delete(matchKey.key.spec); - baseDistances.set(coord.item.key.spec, p); } - keyEvent.keyDistribution = this.currentStageKeyDistribution(baseDistances); + + // Will also bypass if the base key is modipress-compatible. + if(coord.item && correctionKeyFilter(coord.item.key.spec)) { + keyEvent.keyDistribution = this.currentStageKeyDistribution(baseDistances); + } // TODO for future: multitap previews. diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts index c21533d90b..555e896c39 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/distance-modeler.ts @@ -251,6 +251,10 @@ export class SearchNode { class SearchSpaceTier { correctionQueue: PriorityQueue; processed: SearchNode[] = []; + + /** + * Indicates the depth searched, in terms of number of inputs, by this tier of the search space. + */ index: number; constructor(instance: SearchSpaceTier); @@ -476,6 +480,12 @@ export class SearchSpace { this.tierOrdering.forEach(function(tier) { tier.increaseMaxEditDistance() }); } + get correctionsEnabled() { + // When corrections are disabled, the Web engine will only provide individual Transforms + // for an input, not a distribution. No distributions means we shouldn't do corrections. + return !!this.inputSequence.find((distribution) => distribution.length > 1); + } + addInput(inputDistribution: Distribution) { this.inputSequence.push(inputDistribution); diff --git a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts index 70f739611b..ab07355d01 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/predict-helpers.ts @@ -258,6 +258,29 @@ export async function correctAndEnumerate( let bestCorrectionCost: number; let correctionPredictionMap: Record> = {}; + // If corrections are not enabled, bypass the correction search aspect entirely. + // No need to 'search' - just do a direct lookup. + if(!searchSpace.correctionsEnabled) { + const predictionRoot = { + sample: { + insert: wordbreak(postContext), // insert correction string + deleteLeft: deleteLeft, + id: inputTransform.id // The correction should always be based on the most recent external transform/transcription ID. + }, + p: 1.0 + }; + + let predictions = predictFromCorrections(lexicalModel, [predictionRoot], context); + predictions.forEach((entry) => entry.preservationTransform = contextChangeAnalysis.preservationTransform); + + // Only one 'correction' / prediction root is allowed - the actual text. + return { + postContextState: postContextState, + rawPredictions: predictions + } + } + + // Only run the correction search when corrections are enabled. for await(let match of searchSpace.getBestMatches(timer)) { // Corrections obtained: now to predict from them! const correction = match.matchString; diff --git a/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/worker-model-compositor.js b/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/worker-model-compositor.js index 0774385026..cc24991247 100644 --- a/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/worker-model-compositor.js +++ b/web/src/engine/predictive-text/worker-thread/src/tests/mocha/cases/worker-model-compositor.js @@ -20,6 +20,51 @@ describe('ModelCompositor', function() { {wordBreaker: wordBreakers.default} ); + it('does not correct when corrections are disabled', async function() { + const compositor = new ModelCompositor(plainModel, true); + + // Corrections are disabled when all inputs provided for a token are single-transform. + const suggestions1 = await compositor.predict({ + insert: "x", // Invalid due to the inserted transform; context root is fine. + deleteLeft: 0 + }, { + // Context + insert needs to match a word in the model; there is no matching word here. + left: "the app", startOfBuffer: true, endOfBuffer: true + }); + + assert.isEmpty(suggestions1.filter((entry) => entry.tag != 'keep')); + assert.isOk(suggestions1.find((entry) => entry.tag == 'keep')); + + const suggestions2 = await compositor.predict({ + insert: "l", + deleteLeft: 0 + }, { + // Context + insert needs to match a word in the model; there is no matching word here. + // Invalid due to the existing context root. + left: "the apl", startOfBuffer: true, endOfBuffer: true + }); + + assert.isEmpty(suggestions2.filter((entry) => entry.tag != 'keep')); + assert.isOk(suggestions2.find((entry) => entry.tag == 'keep')); + }); + + it('may predict when corrections are disabled', async function() { + const compositor = new ModelCompositor(plainModel, true); + + // Corrections are disabled when all inputs provided for a token are single-transform. + const suggestions = await compositor.predict({ + insert: "l", + deleteLeft: 0 + }, { + // Context + insert needs to match a word in the model; 'applied' is a valid entry. + left: "the app", startOfBuffer: true, endOfBuffer: true + }); + + assert.isNotEmpty(suggestions.filter((entry) => entry.tag != 'keep')); + assert.isOk(suggestions.find((entry) => entry.tag == 'keep')); + assert.isOk(suggestions.find((entry) => entry.displayAs == 'applied')); + }); + it('generates suggestions with expected properties', async function() { let compositor = new ModelCompositor(plainModel, true); let context = { @@ -177,7 +222,9 @@ describe('ModelCompositor', function() { insert: "l", deleteLeft: 0 }, { - left: "the apl", startOfBuffer: true, endOfBuffer: true + // Context + insert needs to match a word in the model; corrections are off with a single-transform + // input. + left: "the app", startOfBuffer: true, endOfBuffer: true }); assert.notEqual(compositor.activeTimer, firstTimer); diff --git a/web/src/test/auto/headless/engine/js-processor/engine/context.tests.js b/web/src/test/auto/headless/engine/js-processor/engine/context.tests.js index a355ead265..b09a20c01e 100644 --- a/web/src/test/auto/headless/engine/js-processor/engine/context.tests.js +++ b/web/src/test/auto/headless/engine/js-processor/engine/context.tests.js @@ -9,6 +9,8 @@ import { NodeKeyboardLoader } from 'keyman/engine/keyboard/node-keyboard-loader' import { NodeProctor, RecordedKeystrokeSequence } from '@keymanapp/recorder-core'; +const NUL = '\uFFFE'; + /* * ABOUT THIS TEST SUITE * --------------------- @@ -36,20 +38,15 @@ let device = { browser: 'native' } +/** @type {KeyboardInterface} */ let keyboardWithHarness; -var toSupplementaryPairString = function(code){ - var H = Math.floor((code - 0x10000) / 0x400) + 0xD800; - var L = (code - 0x10000) % 0x400 + 0xDC00; - - return String.fromCharCode(H, L); -} - function runEngineRuleSet(ruleSet, defaultNoun) { defaultNoun = defaultNoun ? defaultNoun : "Rule"; for(var i = 0; i < ruleSet.length; i++) { var ruleDef = ruleSet[i]; // for example, DEADKEY_TEST_1. + keyboardWithHarness.resetContextCache(); var matchDefs = [{ sequence: ruleDef.baseSequence, @@ -844,6 +841,122 @@ var DEADKEY_STORE_TEST_4 = { }] }; +/* Keyman language equivalent: + * + * nul > 'success' + */ +var NUL_TEST_1 = { + id: 1, + // Match condition for rule + rule: [{t: 'n'}], + // Start of context relative to cursor + n: 1, + ln: 1, + // Resulting context map + contextCache: [ NUL ], + + baseSequence: { "output": "", "inputs": []}, + fullMatchDefs: [] +}; + +/* Keyman language equivalent: + * + * nul any(abc) > 'success' + */ +var NUL_TEST_2 = { + id: 1, + // Match condition for rule + rule: [{t: 'n'}, {t: 'a', a: ['a', 'b', 'c']}], + // Start of context relative to cursor + n: 2, + ln: 2, + // Resulting context map + contextCache: [ NUL, 'a' ], + + baseSequence: { "output": "a", "inputs": [ + {"type":"key","keyCode":65,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //a + ]}, + fullMatchDefs: [] +}; + +/* Keyman language equivalent: + * + * nul nul any(abc) context(3) > 'success' + * + * This one may... "stretch" what's actually allowed by Keyman language rules, + * but we wish to ensure that the actual context management is capable of + * handling this. + */ +var NUL_TEST_3 = { + id: 1, + // Match condition for rule + rule: [{t: 'n'}, {t: 'n'}, {t: 'a', a: ['a', 'b', 'c']}, {t: 'c', c: 3}], + // Start of context relative to cursor + n: 4, + ln: 4, + // Resulting context map + contextCache: [ NUL, NUL, 'a', 'a' ], + + baseSequence: { "output": "a", "inputs": [ + {"type":"key","keyCode":65,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //a + {"type":"key","keyCode":65,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //a + ]}, + fullMatchDefs: [ + { + sequence: { "output": "a", "inputs": [ + {"type":"key","keyCode":50,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //1 + {"type":"key","keyCode":65,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //a + {"type":"key","keyCode":65,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true} //a + ]}, + result: false, + msg: "Rule 2: matched deadkey with nul" + }, { + sequence: { "output": "a", "inputs": [ + {"type":"key","keyCode":65,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //a + {"type":"key","keyCode":65,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //a + {"type":"key","keyCode":65,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true} //a + ]}, + result: false, + msg: "Rule 2: matched character with nul" + } + ] +}; + +/* Keyman language equivalent: + * + * nul nul dk(1) any(abc) > 'success' + * + * This may also "stretch" what's actually allowed by Keyman language rules, + * but we wish to ensure that the actual context management is capable of + * handling this. + */ +var NUL_TEST_4 = { + id: 1, + // Match condition for rule + rule: [{t: 'n'}, {t: 'n'}, {t: 'd', d: 1}, {t: 'a', a: ['a', 'b', 'c']}], + // Start of context relative to cursor + n: 4, + ln: 4, + // Resulting context map + contextCache: [ NUL, NUL, 1, 'a' ], + + baseSequence: { "output": "a", "inputs": [ + {"type":"key","keyCode":50,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //1 + {"type":"key","keyCode":65,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //a + ]}, + fullMatchDefs: [ + { + sequence: { "output": "a", "inputs": [ + {"type":"key","keyCode":50,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //1 + {"type":"key","keyCode":50,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true}, //1 + {"type":"key","keyCode":65,"states":10752,"modifiers":0,"modifierChanged":false,"isVirtualKey":true} //a + ]}, + result: false, + msg: "Rule 2: matched extra deadkey with nul" + } + ] +}; + /* Keyman language equivalent: * * store(match) 'abc' @@ -975,10 +1088,12 @@ var ANY_INDEX_RULE_SET = [ ANY_INDEX_TEST_1, ANY_INDEX_TEST_2, ANY_INDEX_TEST_3 var DEADKEY_STORE_RULE_SET = [ DEADKEY_STORE_TEST_1, DEADKEY_STORE_TEST_2, DEADKEY_STORE_TEST_3, DEADKEY_STORE_TEST_4 ]; +var NUL_RULE_SET = [ NUL_TEST_1, NUL_TEST_2, NUL_TEST_3, NUL_TEST_4 ]; + var NOTANY_NUL_RULE_SET = [ NOTANY_NUL_TEST_1, NOTANY_NUL_TEST_2, NOTANY_NUL_TEST_3 ]; var FULL_RULE_SET = [].concat(DEADKEY_RULE_SET, ANY_CONTEXT_RULE_SET, ANY_INDEX_RULE_SET, - DEADKEY_STORE_RULE_SET, NOTANY_NUL_RULE_SET); + DEADKEY_STORE_RULE_SET, NUL_RULE_SET, NOTANY_NUL_RULE_SET); // ----------- @@ -1087,6 +1202,24 @@ describe('Engine - Context Matching', function() { }); }); + describe('handles interactions with nul in requested context', function() { + it('with only a single nul in context range; no text: NUL_TEST_1', function () { + runEngineRuleSet([NUL_TEST_1]); + }); + + it(`with context [nul, 'a'] in range: NUL_TEST_2`, function () { + runEngineRuleSet([NUL_TEST_2]); + }); + + it(`with context [nul, 'a', 'a'] in range: NUL_TEST_3`, function () { + runEngineRuleSet([NUL_TEST_3]); + }); + + it(`with context [nul, dk(1), 'a'] in range: NUL_TEST_4`, function () { + runEngineRuleSet([NUL_TEST_4]); + }); + }); + describe('handles interactions between notany and nul in context', function() { it('with notany against a store with pure characters: NOTANY_NUL_TEST_1', function() { runEngineRuleSet([NOTANY_NUL_TEST_1]); diff --git a/web/src/test/auto/headless/engine/js-processor/engine/notany_context.tests.js b/web/src/test/auto/headless/engine/js-processor/engine/notany_context.tests.js index ecc7dd92d0..86a35058ae 100644 --- a/web/src/test/auto/headless/engine/js-processor/engine/notany_context.tests.js +++ b/web/src/test/auto/headless/engine/js-processor/engine/notany_context.tests.js @@ -20,10 +20,12 @@ const device = { browser: 'native' } +/** @type {KeyboardInterface} */ let keyboardWithHarness; function runEngineRuleSet(ruleSet) { for(let ruleDef of ruleSet) { + keyboardWithHarness.resetContextCache(); // Prepare the context! const ruleSeq = new RecordedKeystrokeSequence(ruleDef); const proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); diff --git a/web/src/test/manual/web/kmpHeader.js b/web/src/test/manual/web/kmpHeader.js new file mode 100644 index 0000000000..4cf3b20b11 --- /dev/null +++ b/web/src/test/manual/web/kmpHeader.js @@ -0,0 +1,89 @@ + +/* + This script allows its host page to add a keyboard via KMP file. + + Expectations for the host page: + - `

` + - +*/ + +window.addEventListener('load', () => { + const packageSelector = document.getElementById('package-file-select'); + packageSelector.addEventListener('change', async (ev) => { + let file = ev.target.files[0]; + + if(!file) { + // The user probably canceled file selection; abort. + return; + } + + // Load the package. + // https://gildas-lormeau.github.io/zip.js/api/classes/ZipReader.html + const packageReader = new zip.ZipReader(new zip.BlobReader(file)); + const packageEntries = await packageReader.getEntries(); + + if(packageEntries.length == 0) { + alert('Could not decompress the KMP; package loading failed.'); + return; + } else { + const metadataFile = packageEntries.find((entry) => entry.filename == 'kmp.json'); + if(!metadataFile) { + alert('Could not extract kmp.json; aborting'); + } + const rawMetadata = await metadataFile.getData(new zip.TextWriter()); + + // Full kmp.json load! + const packageMetadata = JSON.parse(rawMetadata); + const keyboards = packageMetadata.keyboards + + for(let keyboard of keyboards) { + const keyboardFile = `${keyboard.id}.js`; + const displayFont = keyboard.displayFont; + const oskFont = keyboard.oskFont; + + async function dataFor(filename, type) { + if(type == 'font') { + const ext = filename.substring(filename.lastIndexOf('.')+1); + type = type + '/' + ext; + } + const rawEntry = packageEntries.find((entry) => entry.filename == filename); + if(!rawEntry) { + alert(`Could not extract ${filename} from the keyboard package.`); + return null; + } + return await rawEntry.getData(new zip.Data64URIWriter(type)); + } + + const kbdData = await dataFor(keyboardFile, 'text/javascript'); + const dFontData = displayFont ? await dataFor(displayFont, 'font') : null; + const oFontData = oskFont ? await dataFor(oskFont, 'font') : null; + + keyboard.languages.forEach((language) => { + if(dFontData) { + language.font = { + family: keyboard.id, + source: [dFontData] + }; + } + + if(oFontData) { + language.oskFont = { + family: keyboard.id, + source: [oFontData] + }; + } + }) + + const stub = { + id: keyboard.id, + name: keyboard.name, + languages: keyboard.languages, + filename: kbdData + }; + + // Load 'er up! + keyman.addKeyboards(stub).then(() => document.getElementById('ta1').focus()); + } + } + }); +}); \ No newline at end of file diff --git a/web/src/test/manual/web/sentry-integration/errorhdr.js b/web/src/test/manual/web/sentry-integration/errorhdr.js index b784f6a476..f5e4ec283f 100644 --- a/web/src/test/manual/web/sentry-integration/errorhdr.js +++ b/web/src/test/manual/web/sentry-integration/errorhdr.js @@ -4,39 +4,49 @@ This script is designed to test KeymanWeb error message handling. */ - function loadKeyboards() - { - var kmw=keyman; +function loadKeyboards() { + var kmw=keyman; + + // We start by adding a keyboard correctly. It's best to include a 'control' in our experiment. + kmw.addKeyboards({id:'us',name:'English',languages:{id:'en',name:'English'}, + filename:'../us-1.0.js'}); - // We start by adding a keyboard correctly. It's best to include a 'control' in our experiment. - kmw.addKeyboards({id:'us',name:'English',languages:{id:'en',name:'English'}, - filename:'../us-1.0.js'}); - - // Insert a keyboard that cannot be found. - kmw.addKeyboards({id:'lao_2008_basic',name:'wrong-filename', - languages:{ - id:'lo',name:'debugging',region:'Asia', - font:{family:'LaoWeb',source:['../font/saysettha_web.ttf','../font/saysettha_web.woff','../font/saysettha_web.eot']} - }, - filename:'./missing_file.js' // Intentional error - the file doesn't exist, so the + + + + @@ -80,6 +84,9 @@ +

Add a keyboard via local KMP:

+

+

Return to testing home page

diff --git a/windows/docs/help/about/history.md b/windows/docs/help/about/history.md index 2d48a601db..ae2a2cb851 100644 --- a/windows/docs/help/about/history.md +++ b/windows/docs/help/about/history.md @@ -1,5 +1,5 @@ --- -title: Version History +title: Version History of Keyman for Windows --- A frequently updated changelog for Keyman is kept at diff --git a/windows/docs/help/about/requirements.md b/windows/docs/help/about/requirements.md index 3c241bc858..5730a6ec73 100644 --- a/windows/docs/help/about/requirements.md +++ b/windows/docs/help/about/requirements.md @@ -1,5 +1,5 @@ --- -title: System Requirements +title: System Requirements - Keyman for Windows Help --- ## Supported Windows Operating Systems diff --git a/windows/docs/help/about/welcome.md b/windows/docs/help/about/welcome.md index 79c01e4fcd..3512a566f8 100644 --- a/windows/docs/help/about/welcome.md +++ b/windows/docs/help/about/welcome.md @@ -1,5 +1,5 @@ --- -title: Welcome to Keyman +title: Welcome to Keyman for Windows --- Thank you for installing Keyman. Whether you're a new or returning user, diff --git a/windows/docs/help/about/whatsnew.md b/windows/docs/help/about/whatsnew.md index 82087dd701..4f11c82ec2 100644 --- a/windows/docs/help/about/whatsnew.md +++ b/windows/docs/help/about/whatsnew.md @@ -1,5 +1,5 @@ --- -title: What's New +title: What's New in Keyman 18.0 for Windows --- Here are some of the new features we have added to Keyman 18.0 for Windows: diff --git a/windows/docs/help/index.md b/windows/docs/help/index.md index 7f5ddfd463..c7cccc7695 100644 --- a/windows/docs/help/index.md +++ b/windows/docs/help/index.md @@ -1,5 +1,5 @@ --- -title: Keyman 18.0 Help +title: Keyman for Windows 18.0 Help --- Need help using Keyman for Windows? You'll find everything you need here, including product documentation, diff --git a/windows/docs/help/start/download-and-install-keyboard.md b/windows/docs/help/start/download-and-install-keyboard.md index cd1ddb18c0..b512fcc03b 100644 --- a/windows/docs/help/start/download-and-install-keyboard.md +++ b/windows/docs/help/start/download-and-install-keyboard.md @@ -1,5 +1,5 @@ --- -title: How To - Download and Install a Keyman Keyboard +title: How To - Download and Install a Keyman Keyboard on Windows --- You must have a Keyman keyboard installed to type with Keyman in your diff --git a/windows/docs/help/start/download-and-install-keyman.md b/windows/docs/help/start/download-and-install-keyman.md index 00a8538508..bf316692bb 100644 --- a/windows/docs/help/start/download-and-install-keyman.md +++ b/windows/docs/help/start/download-and-install-keyman.md @@ -1,5 +1,5 @@ --- -title: How To - Download and Install Keyman +title: How To - Download and Install Keyman on Windows --- ## Downloading Keyman diff --git a/windows/docs/help/start/tutorial.md b/windows/docs/help/start/tutorial.md index dd09de98db..7eaf4bd00a 100644 --- a/windows/docs/help/start/tutorial.md +++ b/windows/docs/help/start/tutorial.md @@ -16,7 +16,7 @@ it is running you will see the Keyman icon: ## Step 2 If the Keyman icon is in the Windows Taskbar near the clock, continue on -to [Step 5](#step-5-). +to [Step 5](#toc-step-5). ![](../desktop_images/start_tray.png) diff --git a/windows/docs/help/troubleshooting/index.md b/windows/docs/help/troubleshooting/index.md index cafa510647..d8d9e385c8 100644 --- a/windows/docs/help/troubleshooting/index.md +++ b/windows/docs/help/troubleshooting/index.md @@ -1,5 +1,5 @@ --- -title: Troubleshooting +title: Keyman for Windows Troubleshooting --- * [How To - Fix Font Issues](font) diff --git a/windows/src/desktop/kmshell/kmshell.dproj b/windows/src/desktop/kmshell/kmshell.dproj index 2bb3c24512..81a1d09e73 100644 --- a/windows/src/desktop/kmshell/kmshell.dproj +++ b/windows/src/desktop/kmshell/kmshell.dproj @@ -356,11 +356,9 @@
frmInstallNow
- dfm
frmStartInstall
- dfm
diff --git a/windows/src/desktop/kmshell/locale/es-419/strings.xml b/windows/src/desktop/kmshell/locale/es-419/strings.xml index 9d5f2a3696..24ef2431e7 100644 --- a/windows/src/desktop/kmshell/locale/es-419/strings.xml +++ b/windows/src/desktop/kmshell/locale/es-419/strings.xml @@ -295,6 +295,10 @@ Simular AltGr con Ctrl+Alt + + Allow right modifier for hotkeys + + Tratar las teclas muertas de hardware como teclas simples @@ -323,6 +327,10 @@ Mostrar pantalla de bienvenida + + Buscar actualizaciones y descargar automáticamente + + Buscar actualizaciones automáticamente en keyman.com cada semana @@ -420,6 +428,14 @@ Derechos de autor © SIL International. Todos los derechos reservados. + + Creado por %1$s + + + + Derechos de autor © %1$s. Todos los derechos reservados. + + Versión @@ -537,6 +553,10 @@ El atajo de teclado %1$s entra en conflicto con otro atajo de teclado. Si continúas, se eliminará el otro atajo. ¿Continuar\? + + Actualizar + + Componentes de Keyman actualizados disponibles @@ -606,16 +626,27 @@ Sa&lir de la búsqueda de actualizaciones en línea + + + + Aplicar actualización ahora + + + + Buscar nuevas actualizaciones - Keyman Iniciar Keyman + + Iniciar %1$s + + Salir @@ -657,7 +688,6 @@ - Keyman @@ -781,7 +811,6 @@ - Keyman @@ -904,6 +933,10 @@ Selecciona un teclado de Keyman para encontrar fuentes relacionadas. + + + + No se encontró ninguna fuente recomendada para el Teclado %1$s. diff --git a/windows/src/desktop/kmshell/locale/pt-PT/strings.xml b/windows/src/desktop/kmshell/locale/pt-PT/strings.xml index 319ded2bba..f6b601a2a1 100644 --- a/windows/src/desktop/kmshell/locale/pt-PT/strings.xml +++ b/windows/src/desktop/kmshell/locale/pt-PT/strings.xml @@ -191,7 +191,7 @@ - Add/remove language + Adicionar/remover idioma @@ -295,6 +295,10 @@ Simular AltGr com Ctrl+Alt + + Permitir modificador direito para atalhos + + Tratar teclas mortas como teclas simples @@ -323,6 +327,10 @@ Mostrar ecrã de bem-vindo + + Verificar automaticamente por atualizações e download + + Procurar semanal e automaticamente por atualizações no site www.tavultesoft.com @@ -420,6 +428,14 @@ Copyright 1994-2006 Tavultesoft Pty Ltd. Todos os direitos reservados. + + Criado por %1$s + + + + Copyright © %1$s. Todos os direitos reservados. + + Versão @@ -538,6 +554,10 @@ A tecla de atalho %1$s está em conflito com outra tecla de atalho. Se continuar, a outra tecla de atalho será apagada. Continuar? + + Atualizar + + Versão Atualizada Disponível @@ -607,16 +627,27 @@ E&xit online update check + + + + Aplicar atualização agora + + + + Procurar atualizações - Teclado Teclado Inicial + + Iniciar %1$s + + Sair @@ -658,7 +689,6 @@ - Teclado @@ -782,7 +812,6 @@ - Teclado diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm index b7766d09bc..0ca96f7123 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.dfm @@ -34,7 +34,7 @@ object frmStartInstall: TfrmStartInstall Top = 104 Width = 75 Height = 25 - Caption = 'Install' + Caption = 'Update' ModalResult = 1 TabOrder = 0 end diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas index 5d3b11767a..b2353bc6fd 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstall.pas @@ -26,14 +26,25 @@ type cmdInstall: TButton; cmdLater: TButton; lblInstallUpdate: TLabel; + procedure FormCreate(Sender: TObject); private public end; implementation +uses + MessageIdentifiers, + MessageIdentifierConsts; {$R *.dfm} +procedure TfrmStartInstall.FormCreate(Sender: TObject); +begin + inherited; + cmdInstall.Caption := MsgFromId(S_Update); + cmdLater.Caption := MsgFromId(S_Button_Close); + lblInstallUpdate.Caption := MsgFromId(S_Ready_To_Install); +end; end. diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.dfm b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.dfm index 8c0e467033..4bb30bbf56 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.dfm +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.dfm @@ -14,14 +14,15 @@ object frmStartInstallNow: TfrmStartInstallNow Font.Style = [] OldCreateOrder = False Position = poScreenCenter + OnCreate = FormCreate PixelsPerInch = 96 TextHeight = 13 object lblUpdateMessage: TLabel - Left = 32 - Top = 48 - Width = 290 - Height = 41 - Caption = 'Your computer will be restarted if you update now.' + Left = 16 + Top = 32 + Width = 323 + Height = 19 + Caption = 'Updating now will require a computer restart.' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -16 @@ -35,7 +36,7 @@ object frmStartInstallNow: TfrmStartInstallNow Top = 120 Width = 75 Height = 25 - Caption = 'Update now' + Caption = 'Update' ModalResult = 1 TabOrder = 0 end diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.pas index ce3fd29da0..b4b7290d5f 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.pas +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.UI.UfrmStartInstallNow.pas @@ -26,14 +26,25 @@ type cmdInstall: TButton; cmdLater: TButton; lblUpdateMessage: TLabel; + procedure FormCreate(Sender: TObject); private public end; implementation +uses + MessageIdentifiers, + MessageIdentifierConsts; {$R *.dfm} +procedure TfrmStartInstallNow.FormCreate(Sender: TObject); +begin + inherited; + cmdInstall.Caption := MsgFromId(S_Update); + cmdLater.Caption := MsgFromId(S_Button_Close); + lblUpdateMessage.Caption := MsgFromId(S_Update_Restart_Req); +end; end. diff --git a/windows/src/desktop/kmshell/main/Keyman.System.RemoteUpdateCheck.pas b/windows/src/desktop/kmshell/main/Keyman.System.RemoteUpdateCheck.pas index df892929f0..55dde4eb1a 100644 --- a/windows/src/desktop/kmshell/main/Keyman.System.RemoteUpdateCheck.pas +++ b/windows/src/desktop/kmshell/main/Keyman.System.RemoteUpdateCheck.pas @@ -239,9 +239,8 @@ begin begin Exit; end; - if Registry.ValueExists(SRegValue_LastUpdateCheckTime) and - (Now - Registry.ReadDateTime(SRegValue_LastUpdateCheckTime) > - CheckPeriod) then + if (not Registry.ValueExists(SRegValue_LastUpdateCheckTime)) or + ((Now - Registry.ReadDateTime(SRegValue_LastUpdateCheckTime)) > CheckPeriod) then begin Result := True; end; diff --git a/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas index 80fbf08634..34986cf238 100644 --- a/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas +++ b/windows/src/desktop/kmshell/main/Keyman.System.UpdateStateMachine.pas @@ -567,7 +567,7 @@ function TUpdateStateMachine.ReadyToInstall: Boolean; begin if not IsCurrentStateAssigned then Exit(False); - if (CurrentState.ClassName = 'WaitingRestartState') and not HasKeymanRun then + if (CurrentState is WaitingRestartState) and not HasKeymanRun then Result := True else Result := False; @@ -767,6 +767,7 @@ begin // IdleState to wait 'CheckPeriod' before trying again TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, 'Error Updates not downloaded after 3 attempts'); + bucStateContext.RemoveCachedFiles; ChangeState(IdleState); end else @@ -996,17 +997,28 @@ function InstallingState.DoInstallPackage(PackageFileName: String): Boolean; var FPackage: IKeymanPackageFile2; begin - Result := True; - FPackage := kmcom.Packages.GetPackageFromFile(PackageFileName) - as IKeymanPackageFile2; - FPackage.Install2(True); - // Force overwrites existing package and leaves most settings for it intact + Result := False; + try + FPackage := kmcom.Packages.GetPackageFromFile(PackageFileName) + as IKeymanPackageFile2; + // Force overwrites existing package and leaves most settings for it intact + FPackage.Install2(True); + Result := True; + except + on E:Exception do + begin + TKeymanSentryClient.ReportHandledException(E, + 'Failed to install keyboard package'); + end; + end; + FPackage := nil; - - kmcom.Refresh; - kmcom.Apply; - - System.SysUtils.DeleteFile(PackageFileName); + if Result then + begin + kmcom.Refresh; + kmcom.Apply; + System.SysUtils.DeleteFile(PackageFileName); + end; end; @@ -1021,9 +1033,15 @@ begin for i := 0 to High(Params.Packages) do begin PackageFullPath := SavePath + Params.Packages[i].FileName; + if not FileExists(PackageFullPath) then + begin + Continue; + end; + if not DoInstallPackage(PackageFullPath) then // I2742 begin - KL.Log('Installing Package failed' + PackageFullPath); + TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR, + 'Installing Package failed"' + PackageFullPath + '"'); end; end; Result := True; diff --git a/windows/src/desktop/kmshell/main/UfrmMain.pas b/windows/src/desktop/kmshell/main/UfrmMain.pas index 86df47ec70..394cb09377 100644 --- a/windows/src/desktop/kmshell/main/UfrmMain.pas +++ b/windows/src/desktop/kmshell/main/UfrmMain.pas @@ -188,7 +188,7 @@ uses MessageIdentifiers, Keyman.System.ExecutionHistory, Keyman.System.KeymanSentryClient, - Keyman.System.RemoteUpdateCheck, + Keyman.System.UpdateStateMachine, OptionsXMLRenderer, Keyman.Configuration.System.UmodWebHttpServer, Keyman.Configuration.System.HttpServer.App.ConfigMain, @@ -800,16 +800,14 @@ begin end; procedure TfrmMain.Update_CheckNow; -// TODO: epic-windows-update -// Get an instance to the state machine and call handle check so the state can change to update -// available. -var UpdateCheck : TRemoteUpdateCheck; +var + BUpdateSM : TUpdateStateMachine; begin - UpdateCheck := TRemoteUpdateCheck.Create(True); + BUpdateSM := TUpdateStateMachine.Create(False); try - UpdateCheck.Run; + BUpdateSM.HandleCheck; finally - UpdateCheck.Free; + BUpdateSM.Free; end; DoRefresh; end; diff --git a/windows/src/desktop/kmshell/xml/config.css b/windows/src/desktop/kmshell/xml/config.css index e452cca5b8..1c97ee3858 100644 --- a/windows/src/desktop/kmshell/xml/config.css +++ b/windows/src/desktop/kmshell/xml/config.css @@ -502,8 +502,8 @@ table tr .grid_container_update { display: grid; position: relative; - grid-template-columns: 50px 1fr 1fr 1fr; - margin: 10px 50px 10px 64px; + grid-template-columns: 1fr 1fr 1fr; + margin: 15px 50px 15px 59px; /* From https://geary.co/internal-borders-css-grid/ */ overflow: hidden; gap: var(--gap); diff --git a/windows/src/desktop/kmshell/xml/keyman_update.xsl b/windows/src/desktop/kmshell/xml/keyman_update.xsl index 0daa6ace3b..5d86ada04b 100644 --- a/windows/src/desktop/kmshell/xml/keyman_update.xsl +++ b/windows/src/desktop/kmshell/xml/keyman_update.xsl @@ -23,15 +23,14 @@
- Updates are available which will be applied when Windows is next restarted: + - No updates are available. +
-
Select
@@ -74,18 +73,6 @@ - -
- - javascript:updateTick(""); - Update_ - checked - Update_ - - - Update__RequiresAdmin - -
diff --git a/windows/src/desktop/kmshell/xml/strings.xml b/windows/src/desktop/kmshell/xml/strings.xml index 24909d3486..3c8f20a369 100644 --- a/windows/src/desktop/kmshell/xml/strings.xml +++ b/windows/src/desktop/kmshell/xml/strings.xml @@ -156,9 +156,6 @@ T - - - @@ -591,6 +588,26 @@ Diagnostics + + + + No Updates are available. + + + + + Updates are available which will be applied when Windows is next restarted. + + + + + Updating now will require a computer restart. + + + + + Keyman Update ready to install. + diff --git a/windows/src/desktop/setup/locale/pt-PT/strings.xml b/windows/src/desktop/setup/locale/pt-PT/strings.xml index 40684d10d1..20c4c7e843 100644 --- a/windows/src/desktop/setup/locale/pt-PT/strings.xml +++ b/windows/src/desktop/setup/locale/pt-PT/strings.xml @@ -1,6 +1,6 @@ - Português do Portugal + Português de Portugal Configuração de $APPNAME $VERSION Instalar $APPNAME $VERSION $APPNAME $VERSION foi instalado com sucesso. diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index 2010dee5b0..f56bd98620 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -114,10 +114,12 @@ void ProcessToggleChange(UINT key) { // I4793 } } -extern "C" __declspec(dllexport) BOOL WINAPI TIPProcessKey(WPARAM wParam, LPARAM lParam, // I3589 // I3588 - PKEYMANPROCESSOUTPUTFUNC outfunc, PKEYMANGETCONTEXTFUNC ctfunc, BOOL Updateable, BOOL Preserved) { - - PKEYMAN64THREADDATA _td = ThreadGlobals(); +BOOL TIPProcessKeyInternal( + PKEYMAN64THREADDATA _td, + WPARAM wParam, + LPARAM lParam, + BOOL Updateable, + BOOL Preserved) { if(!_td) { return FALSE; } @@ -202,13 +204,11 @@ extern "C" __declspec(dllexport) BOOL WINAPI TIPProcessKey(WPARAM wParam, LPARAM _td->state.charCode = CharFromVK(&_td->state.vkey, Globals::get_ShiftState()); // I4582 - _td->TIPProcessOutput = outfunc; - _td->TIPGetContext = ctfunc; - BOOL res = ProcessHook(); _td->TIPProcessOutput = NULL; _td->TIPGetContext = NULL; + _td->TIPGetContextIsSelected = NULL; SendDebugMessageFormat("Success, res=%d", res); @@ -226,6 +226,56 @@ extern "C" __declspec(dllexport) BOOL WINAPI TIPProcessKey(WPARAM wParam, LPARAM } return_SendDebugExit(res); + +} + +extern "C" __declspec(dllexport) BOOL WINAPI TIPProcessKey( + WPARAM wParam, + LPARAM lParam, // I3589 // I3588 + PKEYMANPROCESSOUTPUTFUNC outfunc, + PKEYMANGETCONTEXTFUNC ctfunc, + BOOL Updateable, + BOOL Preserved) { + + PKEYMAN64THREADDATA _td = ThreadGlobals(); + if(!_td) { + return FALSE; + } + + _td->TIPProcessOutput = outfunc; + _td->TIPGetContext = ctfunc; + BOOL res = TIPProcessKeyInternal(_td, wParam, lParam, Updateable, Preserved); + if (res == FALSE) { + _td->TIPProcessOutput = NULL; + _td->TIPGetContext = NULL; + } + return res; + +} + +// This includes the PKEYMANGETCONTEXTISSELECTEDFUNC to report if text is selected +extern "C" __declspec(dllexport) BOOL WINAPI TIPProcessKeyEx( + WPARAM wParam, + LPARAM lParam, // I3589 // I3588 + PKEYMANPROCESSOUTPUTFUNC outfunc, + PKEYMANGETCONTEXTISSELECTEDFUNC ctfunc, + BOOL Updateable, + BOOL Preserved) { + + PKEYMAN64THREADDATA _td = ThreadGlobals(); + if(!_td) { + return FALSE; + } + + _td->TIPProcessOutput = outfunc; + _td->TIPGetContextIsSelected = ctfunc; + BOOL res = TIPProcessKeyInternal(_td, wParam, lParam, Updateable, Preserved); + if (res == FALSE) { + _td->TIPProcessOutput = NULL; + _td->TIPGetContextIsSelected = NULL; + } + return res; + } /* AI constructor and destructor */ @@ -279,7 +329,13 @@ BOOL AITIP::ReadContext(PWSTR buf) { return FALSE; } - if (_td->TIPGetContext && (*_td->TIPGetContext)(MAXCONTEXT - 1, buf, &isTextSelected) == S_OK) { // I3575 // I4262 + if (_td->TIPGetContextIsSelected && (*_td->TIPGetContextIsSelected)(MAXCONTEXT - 1, buf, &isTextSelected) == S_OK) { // I3575 // I4262 + if(ShouldDebug()) { + SendDebugMessageFormat("full context [Updateable=%d] %s", _td->TIPFUpdateable, Debug_UnicodeString(buf)); + } + useLegacy = FALSE; // I3575 + return TRUE; + } else if (_td->TIPGetContext && (*_td->TIPGetContext)(MAXCONTEXT - 1, buf) == S_OK) { // I3575 // I4262 if(ShouldDebug()) { SendDebugMessageFormat("full context [Updateable=%d] %s", _td->TIPFUpdateable, Debug_UnicodeString(buf)); } diff --git a/windows/src/engine/keyman32/globals.h b/windows/src/engine/keyman32/globals.h index ae311f5ef2..3c71163446 100644 --- a/windows/src/engine/keyman32/globals.h +++ b/windows/src/engine/keyman32/globals.h @@ -151,7 +151,8 @@ public: /* External interface functions */ typedef HRESULT (WINAPI *PKEYMANPROCESSOUTPUTFUNC)(int n, WCHAR *buf, int nbuf); -typedef HRESULT (WINAPI *PKEYMANGETCONTEXTFUNC)(int n, PWSTR buf, BOOL* isTextSelected); +typedef HRESULT (WINAPI *PKEYMANGETCONTEXTFUNC)(int n, PWSTR buf); +typedef HRESULT (WINAPI *PKEYMANGETCONTEXTISSELECTEDFUNC)(int n, PWSTR buf, BOOL* isTextSelected); typedef BOOL (WINAPI *PTIPCALLBACK)(); // Tells the TIP to update its status (used to be done through 0x88) typedef BOOL (WINAPI *PKeymanOutputBackspace)(HWND hwnd); @@ -208,6 +209,7 @@ typedef struct tagKEYMAN64THREADDATA PKEYMANPROCESSOUTPUTFUNC TIPProcessOutput; PKEYMANGETCONTEXTFUNC TIPGetContext; + PKEYMANGETCONTEXTISSELECTEDFUNC TIPGetContextIsSelected; BOOL TIPFUpdateable, TIPFPreserved; // I4290 diff --git a/windows/src/engine/keyman32/keyman32.def b/windows/src/engine/keyman32/keyman32.def index 4b509e6f60..a61a78b463 100644 --- a/windows/src/engine/keyman32/keyman32.def +++ b/windows/src/engine/keyman32/keyman32.def @@ -17,6 +17,7 @@ EXPORTS TIPActivateEx TIPActivateKeyboard TIPProcessKey + TIPProcessKeyEx TIPIsKeymanRunning GetKeyboardPreservedKeys diff --git a/windows/src/engine/keyman32/keyman64.def b/windows/src/engine/keyman32/keyman64.def index 9f46c08cb8..69eddd8e3d 100644 --- a/windows/src/engine/keyman32/keyman64.def +++ b/windows/src/engine/keyman32/keyman64.def @@ -17,6 +17,7 @@ EXPORTS TIPActivateEx TIPActivateKeyboard TIPProcessKey + TIPProcessKeyEx TIPIsKeymanRunning GetKeyboardPreservedKeys diff --git a/windows/src/engine/kmtip/keyman32interface.cpp b/windows/src/engine/kmtip/keyman32interface.cpp index ae06c9edad..cd42af3d7a 100644 --- a/windows/src/engine/kmtip/keyman32interface.cpp +++ b/windows/src/engine/kmtip/keyman32interface.cpp @@ -12,6 +12,9 @@ extern "C" { BOOL WINAPI TIPProcessKey(WPARAM wParam, LPARAM lParam, PKEYMANPROCESSOUTPUTFUNC outfunc, PKEYMANGETCONTEXTFUNC ctfunc, BOOL Updateable, BOOL Preserved); + BOOL WINAPI TIPProcessKeyEx(WPARAM wParam, LPARAM lParam, PKEYMANPROCESSOUTPUTFUNC outfunc, + PKEYMANGETCONTEXTISSELECTEDFUNC ctfunc, BOOL Updateable, BOOL Preserved); + BOOL WINAPI GetKeyboardPreservedKeys(PreservedKey **pPreservedKeys, size_t *cPreservedKeys); BOOL WINAPI TIPIsKeymanRunning(); @@ -43,6 +46,11 @@ BOOL Keyman32Interface::TIPProcessKey(WPARAM wParam, LPARAM lParam, PKEYMANPROCE return ::TIPProcessKey(wParam, lParam, outfunc, ctfunc, Updateable, Preserved); } +BOOL Keyman32Interface::TIPProcessKeyEx(WPARAM wParam, LPARAM lParam, PKEYMANPROCESSOUTPUTFUNC outfunc, + PKEYMANGETCONTEXTISSELECTEDFUNC ctfunc, BOOL Updateable, BOOL Preserved) { + return ::TIPProcessKeyEx(wParam, lParam, outfunc, ctfunc, Updateable, Preserved); +} + BOOL Keyman32Interface::GetKeyboardPreservedKeys(PreservedKey **pPreservedKeys, size_t *cPreservedKeys) { return ::GetKeyboardPreservedKeys(pPreservedKeys, cPreservedKeys); } diff --git a/windows/src/engine/kmtip/kmkey.cpp b/windows/src/engine/kmtip/kmkey.cpp index 72c8220fd6..609c98e431 100644 --- a/windows/src/engine/kmtip/kmkey.cpp +++ b/windows/src/engine/kmtip/kmkey.cpp @@ -69,7 +69,7 @@ public: * @param[in] n The size of the buffer. * @param[out] buf The buffer to store the context. * @param[out] isTextSelected A flag indicating whether text is selected. - * @return S_OK if successful, otherwise an HRESULT error code. + * @return BOOL if successful. */ HRESULT WINAPI KeymanGetContext(int n, PWSTR buf, BOOL* isTextSelected); // I3567 HRESULT GetResult() { return _hr; } @@ -88,10 +88,9 @@ private: * @param[in] hrGetSelection The result of a call to GetSelection selection. * @param[in] cFetched The number of selections fetched. * @param[in] tfSelection The selection details. - * @param[out] isTextSelected A flag indicating whether text is selected. * @return S_OK if successful, otherwise an HRESULT error code. */ - HRESULT KeymanIsTextSelected(HRESULT hrGetSelection, ULONG cFetched, TF_SELECTION tfSelection, BOOL *isTextSelected); + BOOL CKeymanEditSession::KeymanIsTextSelected(const HRESULT hrGetSelection, const ULONG cFetched, const TF_SELECTION tfSelection); }; #define KEYEVENT_EXTRAINFO_KEYMAN 0xF00F0000 // I4370 @@ -174,7 +173,18 @@ extern "C" __declspec(dllexport) HRESULT WINAPI ExtKeymanProcessOutput(int n, WC return res; } -extern "C" __declspec(dllexport) HRESULT WINAPI ExtKeymanGetContext(int n, PWSTR buf, BOOL* isTextSelected) // I3567 +extern "C" __declspec(dllexport) HRESULT WINAPI ExtKeymanGetContext(int n, PWSTR buf) // I3567 +{ + SendDebugEntry(); + HRESULT res; + BOOL isTextSelectedDummy; // not used + if (ExtEditSession) res = ExtEditSession->KeymanGetContext(n, buf, &isTextSelectedDummy); + else res = E_FAIL; // I3567 + SendDebugExit(); + return res; +} + +extern "C" __declspec(dllexport) HRESULT WINAPI ExtKeymanGetContextIsSelected(int n, PWSTR buf, BOOL* isTextSelected) // I3567 { SendDebugEntry(); HRESULT res; @@ -194,8 +204,8 @@ STDAPI CKeymanEditSession::DoEditSession(TfEditCookie ec) ExtEditSession = this; - if (!Keyman32Interface::TIPProcessKey(_wParam, _lParam, ExtKeymanProcessOutput, ExtKeymanGetContext, _fUpdate, _fPreserved)) { - SendDebugMessage(L"TIPProcessKey did not handle the keystroke"); + if (!Keyman32Interface::TIPProcessKeyEx(_wParam, _lParam, ExtKeymanProcessOutput, ExtKeymanGetContextIsSelected, _fUpdate, _fPreserved)) { + SendDebugMessage(L"TIPProcessKeyEx did not handle the keystroke"); _hr = E_FAIL; // TODO: use S_FALSE -> this tells _KeymanProcessKeystroke to pass keystroke on } @@ -275,47 +285,33 @@ HRESULT WINAPI CKeymanEditSession::KeymanGetContext(int n, PWSTR buf, BOOL* isTe } // now check for selected text - if (!SUCCEEDED(hr = KeymanIsTextSelected(hrGetSelection, cFetched, tfSelection, isTextSelected))) { - SendDebugMessage(L"Warning: KeymanIsTextSelected failed"); - // Continue to return S_OK, even though checking if a text selection exists has failed. - // Reaching this point means the context in 'buf' is valid and will be passed to the caller. - // isTextSelected will be false, which is better than returning E_FAIL at this point. + *isTextSelected = KeymanIsTextSelected(hrGetSelection, cFetched, tfSelection); + + if(tfSelection.range) { + tfSelection.range->Release(); } + return_SendDebugExit(S_OK); } -HRESULT -CKeymanEditSession::KeymanIsTextSelected(HRESULT hrGetSelection, ULONG cFetched, TF_SELECTION tfSelection, BOOL *isTextSelected) { - SendDebugEntry(); +BOOL +CKeymanEditSession::KeymanIsTextSelected(const HRESULT hrGetSelection, const ULONG cFetched, const TF_SELECTION tfSelection) { + if (hrGetSelection == TF_E_NOSELECTION || cFetched == 0 || !SUCCEEDED(hrGetSelection) || !tfSelection.range) { + // No selection is present, or an error occurred trying to get the selection + return FALSE; + } + // check if the range is empty HRESULT hr; + BOOL isEmpty = TRUE; - *isTextSelected = FALSE; - - if (!SUCCEEDED(hrGetSelection)) { - return_SendDebugExit(E_FAIL); - } - if (hrGetSelection == TF_E_NOSELECTION) { - // No selection is present - return_SendDebugExit(S_OK); + // Compare the start and end of the range + if (!SUCCEEDED(hr = tfSelection.range->IsEmpty(_ec, &isEmpty))) { + SendDebugMessageFormat(L"KeymanIsTextSelected: Exit: Testing range->isEmpty failed with %x", hr); + return FALSE; } - if (cFetched > 0) { - // check if the range is empty - BOOL isEmpty = FALSE; - - // Compare the start and end of the range - if (!SUCCEEDED(hr = tfSelection.range->IsEmpty(_ec, &isEmpty))) { - SendDebugMessage(L"Exit: Testing range->isEmpty"); - return_SendDebugExit(hr); - } - - if (!isEmpty) { - *isTextSelected = TRUE; - } - tfSelection.range->Release(); - } - return_SendDebugExit(S_OK); + return !isEmpty; } BOOL CKMTipTextService::TIPNotifyActivate(GUID *guidProfile) // I3581 // I4274 diff --git a/windows/src/engine/kmtip/kmtip.h b/windows/src/engine/kmtip/kmtip.h index bb5459cbb0..b4f307488b 100644 --- a/windows/src/engine/kmtip/kmtip.h +++ b/windows/src/engine/kmtip/kmtip.h @@ -139,9 +139,9 @@ private: // typedef HRESULT(WINAPI *PKEYMANPROCESSOUTPUTFUNC)(int n, WCHAR *buf, int nbuf); // I3567 -// Adding a new interface call IsTextSelected could make sense however this would -// also require update TIPProcessKey to have another call back function as an argument -typedef HRESULT(WINAPI *PKEYMANGETCONTEXTFUNC)(int n, PWSTR buf, BOOL* isTextSelected); // I3567 + +typedef HRESULT(WINAPI *PKEYMANGETCONTEXTFUNC)(int n, PWSTR buf); // I3567 +typedef HRESULT(WINAPI *PKEYMANGETCONTEXTISSELECTEDFUNC)(int n, PWSTR buf, BOOL* isTextSelected); class Keyman32Interface { public: @@ -155,6 +155,9 @@ public: static BOOL TIPProcessKey(WPARAM wParam, LPARAM lParam, PKEYMANPROCESSOUTPUTFUNC outfunc, PKEYMANGETCONTEXTFUNC ctfunc, BOOL Updateable, BOOL Preserved); + static BOOL TIPProcessKeyEx(WPARAM wParam, LPARAM lParam, PKEYMANPROCESSOUTPUTFUNC outfunc, + PKEYMANGETCONTEXTISSELECTEDFUNC ctfunc, BOOL Updateable, BOOL Preserved); + static BOOL GetKeyboardPreservedKeys(PreservedKey **pPreservedKeys, size_t *cPreservedKeys); static BOOL TIPIsKeymanRunning();