diff --git a/HISTORY.md b/HISTORY.md index 152a3abe61..795cb33bf5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -232,6 +232,88 @@ * chore: prepare 15.0 alpha (#4129) +## 14.0.253 beta 2021-03-05 + +* fix(android/app): Fix welcome.htm responsiveness (#4531) +* feat(developer): &CasedKeys system store (#4586) +* fix(android): Localize Toast notifications (#4588) +* fix(windows): PreservedKeyMap::MapUSCharToVK line order bug (#4595) +* fix(developer): tidy up expansions tests (#4592) +* fix(ios): adds i18n for some error alerts (#4577) +* fix(windows): incxstr could run over buffer with malformed data (#4596) +* chore(android/app): Update whatsnew with available display languages (#4610) +* chore(linux): Improve Sentry environment setting (#4589) +* fix(developer): Expand filenames before load (#4606) + +## 14.0.252 beta 2021-03-04 + +* feat(ios): enables de, fr, and km localizations (#4585) +* fix(developer): improve CEF location search stability (#4571) +* fix(developer): Support all fonts in Keyboard Fonts dialog (#4574) +* feat(developer): Add different Open Containing Folder buttons (#4576) +* feat(developer): Range expansions (#4584) +* fix(web): fixes lack of respect for underlying-key display settings (#4572) +* fix(common/models): fixes application of suggestions immediately after a backspace (#4587) +* fix(linux): Improve version number (#4582) +* chore(linux): Don't report to Sentry in dev environment (#4581) + +## 14.0.251 beta 2021-03-03 + +* fix(developer): Improve stability of named code constants (#4547) +* fix(developer): schema conformance for model package compiler (#4548) +* fix(developer): touch layout osk import handling of multiple modifiers (#4552) +* fix(developer): require language tag when compiling keyboard package (#4563) +* fix(developer): Avoid blank keys when importing KMX to KVKS (#4564) +* feat(developer): isRTL support for lexical model editor (#4559) +* fix(developer): track modified state in wordlist editor better (#4562) +* chore(ios): better visual feedback for keyboard search during poor internet connectivity (#4573) +* chore(common): Update crowdin files for `de` (#4578) +* chore(common/core/desktop): write debug output to console (#4569) + +## 14.0.250 beta 2021-03-02 + +* fix(common/resources): Fix help.keyman.com path for CI (#4565) + +## 14.0.249 beta 2021-03-01 + +* fix(web): mnemonic keystrokes w FF keymapping (#4540) +* chore(ios/app): Adjust help titles for installing custom dictionaries (#4550) + +## 14.0.248 beta 2021-02-26 + +* fix(common/models): predictions after context reset / caret shift (#4411) +* change(oem/fv/ios): FV keyboards now package-based (#4471) +* fix(windows): Handle Caps Lock event correctly from TIP (#4536) +* fix(developer): run even if sentry unavailable (#4537) +* fix(developer): UTF-8 messages in LM compiler (#4539) +* feat(common/models): mid-context suggestions & reversions, fix(common/models): correction-search SMP issues (#4427) +* fix(ios): package installer completion requires welcome dismissal (#4543) +* fix(windows): Refresh settings on 64-bit apps (#4378) +* fix(windows): prevent re-registration of TIPs on 14.0 upgrade (#4535) + +## 14.0.247 beta 2021-02-25 + +* fix(web): keyboard documentation patch-up (#4512) +* fix(web): removes package namespacing from kbd's CSS class (#4516) + +## 14.0.246 beta 2021-02-24 + +* chore(common): allow forced version increment (#4522) + +## 14.0.245 beta 2021-02-24 + +* fix(common/core/web): core key-processing now always returns RuleBehavior type. (#4508) +* fix(common/resources): Set help-keyman.com.sh executable (#4510) +* fix(developer/compilers): fixes error when "constructor" is in wordlist (#4504) +* fix(web): hides touch-alias caret when keystroke causes focus change (#4514) + +## 14.0.244 beta 2021-02-22 + +* fix(common/models): merges identical suggestions after casing (#4502) +* fix(web): macOS 11 agent string parsing (#4497) +* fix(ios): app logging messages were transient, never stored (#4500) +* chore(ios): web-side sentry enablement try-catch (#4492) + ## 14.0.243 beta 2021-02-12 * change(ios/app): Generate offline help from markdown (#4470) diff --git a/VERSION.md b/VERSION.md index 6958fcdd06..a8a78a6b4a 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -15.0.19 \ No newline at end of file +15.0.19 diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/DownloadResultReceiver.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/DownloadResultReceiver.java index 09aaf07c9c..be20f668a9 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/DownloadResultReceiver.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/DownloadResultReceiver.java @@ -10,6 +10,7 @@ import android.os.Handler; import android.os.ResultReceiver; import android.widget.Toast; +import com.tavultesoft.kmea.BaseActivity; import com.tavultesoft.kmea.util.FileUtils; import java.io.File; @@ -27,8 +28,7 @@ public class DownloadResultReceiver extends ResultReceiver { protected void onReceiveResult(int resultCode, Bundle resultData) { switch(resultCode) { case FileUtils.DOWNLOAD_ERROR : - Toast.makeText(context, "Download failed", - Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(context, R.string.download_failed, Toast.LENGTH_SHORT); MainActivity.cleanupPackageInstall(); break; case FileUtils.DOWNLOAD_SUCCESS : 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 f25af7371a..2a2d47a440 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 @@ -1,5 +1,5 @@ /** - * Copyright (C) 2020 SIL International. All rights reserved. + * Copyright (C) 2020-2021 SIL International. All rights reserved. */ package com.tavultesoft.kmapro; @@ -13,6 +13,7 @@ import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; +import android.view.ViewGroup; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; @@ -22,6 +23,7 @@ import androidx.appcompat.app.AppCompatActivity; import com.tavultesoft.kmea.BaseActivity; import com.tavultesoft.kmea.KMManager; import com.tavultesoft.kmea.util.KMPLink; +import com.tavultesoft.kmea.util.WebViewUtil; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -162,6 +164,7 @@ public class KMPBrowserActivity extends BaseActivity { @Override protected void onDestroy() { super.onDestroy(); + WebViewUtil.cleanup(webView); } @Override diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java index c75b49e846..95f1e94cfa 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java @@ -1,7 +1,9 @@ +/** + * Copyright (C) SIL International. All rights reserved. + */ package com.tavultesoft.kmapro; import android.annotation.SuppressLint; -import androidx.appcompat.widget.Toolbar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; @@ -9,17 +11,8 @@ import androidx.fragment.app.Fragment; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.graphics.Bitmap; import android.os.Bundle; -import android.view.Gravity; import android.view.View; -import android.view.View.OnClickListener; -import android.webkit.WebChromeClient; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.Button; -import android.widget.TextView; import android.widget.Toast; @@ -36,9 +29,7 @@ import com.tavultesoft.kmea.util.KMLog; import org.json.JSONObject; import java.io.File; -import java.io.FileFilter; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -254,9 +245,6 @@ public class PackageActivity extends AppCompatActivity implements languageList = new ArrayList(); } - //Dataset kmpDataset = new Dataset(context); - //kmpDataset.keyboards.addAll(kbdsList); - List> installedPackageKeyboards = kmpProcessor.processKMP(kmpFile, tempPackagePath, PackageProcessor.PP_KEYBOARDS_KEY, languageList); // Do the notifications! @@ -270,8 +258,6 @@ public class PackageActivity extends AppCompatActivity implements Toast.LENGTH_SHORT).show(); } _cleanup = true; - notifyPackageInstallListeners(KeyboardEventHandler.EventType.PACKAGE_INSTALLED, - installedPackageKeyboards, 1); if (installedPackageKeyboards != null) { notifyPackageInstallListeners(KeyboardEventHandler.EventType.PACKAGE_INSTALLED, installedPackageKeyboards, 1); @@ -294,8 +280,6 @@ public class PackageActivity extends AppCompatActivity implements Toast.LENGTH_SHORT).show(); _cleanup = true; - notifyLexicalModelInstallListeners(KeyboardEventHandler.EventType.LEXICAL_MODEL_INSTALLED, - installedLexicalModels, 1); if (installedLexicalModels != null) { notifyLexicalModelInstallListeners(KeyboardEventHandler.EventType.LEXICAL_MODEL_INSTALLED, installedLexicalModels, 1); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebViewFragment.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebViewFragment.java index d463f38449..6ea360d9c2 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebViewFragment.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/WebViewFragment.java @@ -14,6 +14,7 @@ import com.stepstone.stepper.StepperLayout; import com.stepstone.stepper.VerificationError; import com.tavultesoft.kmea.packages.PackageProcessor; import com.tavultesoft.kmea.util.FileUtils; +import com.tavultesoft.kmea.util.WebViewUtil; import android.graphics.Bitmap; import android.os.Bundle; @@ -135,16 +136,7 @@ public class WebViewFragment extends Fragment implements BlockingStep { @Override public void onPageFinished(WebView view, String url) { // Inject a meta viewport tag into the head of the file if it doesn't exist - webView.loadUrl( - "javascript:(function() {" + - "if(!document.querySelectorAll('meta[name=viewport]').length) {"+ - "let meta=document.createElement('meta');"+ - "meta.name='viewport';"+ - "meta.content='width=device-width, initial-scale=1';"+ - "document.head.appendChild(meta);"+ - "}"+ - "})()" - ); + WebViewUtil.injectViewport(view); } }); diff --git a/android/KMAPro/kMAPro/src/main/res/values-de-rDE/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-de-rDE/strings.xml index 6fae23d0b9..8eb6ddda70 100644 --- a/android/KMAPro/kMAPro/src/main/res/values-de-rDE/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values-de-rDE/strings.xml @@ -43,7 +43,7 @@ Zeige \"%1$s\" beim Start - Um Tastaturpakete zu installieren, braucht Keyman die Berechtigung zum Lesen/Schreiben. + Um Tastaturpakete zu installieren, braucht Keyman die Berechtigung zum Lesen von externem Speicher. Zugriff auf den Speicher wurde abgelehnt. Möglicherweise funktioniert die Installation des Tastaturpakets nicht korrekt @@ -56,6 +56,8 @@ Tastatur oder Wörterbuch installieren + Anzeigesprache ändern + Banner immer anzeigen Nicht implementiert @@ -108,6 +110,8 @@ Wörterbuch installieren + %1$s ist keine gültige Keyman Paketdatei.\n%2$s\" + Tastaturpaket enthält keine berührungsoptimierte Tastatur, die installiert werden könnte Kein neues Wörterbuch zu installieren diff --git a/android/KMEA/app/src/main/AndroidManifest.xml b/android/KMEA/app/src/main/AndroidManifest.xml index fe795afa30..439d5a4d54 100644 --- a/android/KMEA/app/src/main/AndroidManifest.xml +++ b/android/KMEA/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ @@ -29,12 +30,6 @@ android:label="" android:theme="@style/Theme.AppCompat.Light.Dialog"> - - + + + + + diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/BaseActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/BaseActivity.java index fdd8ed2de1..4607e08e4c 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/BaseActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/BaseActivity.java @@ -9,6 +9,8 @@ import android.content.ContextWrapper; import android.content.SharedPreferences; import android.os.Build; import android.os.LocaleList; +import android.widget.Toast; + import androidx.appcompat.app.AppCompatActivity; import androidx.preference.PreferenceManager; @@ -18,6 +20,21 @@ import com.tavultesoft.kmea.util.ContextUtils; import java.util.Locale; public class BaseActivity extends AppCompatActivity { + static ContextWrapper localeUpdatedContext; + + /** + * Some classes aren't an AppCompatActivity and need this helper to localize Toast notifications + * in the updated locale. + * @param defaultContext - the context to fallback if localeUpdatedContext is null + * @param resID - resource ID of the string + * @param duration - length of the Toast notification (Toast.LENGTH_LONG or Toast.LENGTH_SHORT) + * @param args - optional format parameters for the string + */ + public static void makeToast(Context defaultContext, int resID, int duration, Object... args) { + Context context = (localeUpdatedContext != null) ? localeUpdatedContext : defaultContext; + String msg = context.getString(resID); + Toast.makeText(context, String.format(msg, args), duration).show(); + } @Override protected void attachBaseContext(Context newBase) { @@ -36,7 +53,7 @@ public class BaseActivity extends AppCompatActivity { } else { localeToSwitchTo = Locale.forLanguageTag(languageTag); } - ContextWrapper localeUpdatedContext = ContextUtils.updateLocale(newBase, localeToSwitchTo); + this.localeUpdatedContext = ContextUtils.updateLocale(newBase, localeToSwitchTo); super.attachBaseContext(localeUpdatedContext); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMHelpFileActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMHelpFileActivity.java index 38e96792de..fc0e5ddd17 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMHelpFileActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMHelpFileActivity.java @@ -11,8 +11,10 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.view.KeyEvent; import android.view.View; import android.view.View.OnClickListener; +import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; import android.webkit.WebChromeClient; @@ -29,6 +31,7 @@ import com.tavultesoft.kmea.packages.PackageProcessor; import com.tavultesoft.kmea.util.FileProviderUtils; import com.tavultesoft.kmea.util.FileUtils; import com.tavultesoft.kmea.util.HelpFile; +import com.tavultesoft.kmea.util.WebViewUtil; import java.io.File; @@ -42,6 +45,7 @@ public class KMHelpFileActivity extends BaseActivity { private WebView webView; private Button finishButton; private String pkgID; + private static boolean didSetDataDirectorySuffix = false; @SuppressLint({"SetJavaScriptEnabled", "InflateParams"}) @Override @@ -49,6 +53,16 @@ public class KMHelpFileActivity extends BaseActivity { super.onCreate(savedInstanceState); final Context context = this; + // Different processes in the same application cannot directly share WebView-related data + // https://developer.android.com/reference/android/webkit/WebView.html#setDataDirectorySuffix(java.lang.String) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + if (!didSetDataDirectorySuffix) { + String processName = getProcessName(); + WebView.setDataDirectorySuffix(processName); + didSetDataDirectorySuffix = true; + } + } + setContentView(R.layout.activity_help_file_layout); Bundle bundle = getIntent().getExtras(); @@ -63,7 +77,7 @@ public class KMHelpFileActivity extends BaseActivity { finishButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - finish(); + finishAfterTransition(); overridePendingTransition(0, android.R.anim.fade_out); } }); @@ -130,16 +144,7 @@ public class KMHelpFileActivity extends BaseActivity { @Override public void onPageFinished(WebView view, String url) { // Inject a meta viewport tag into the head of the file if it doesn't exist - webView.loadUrl( - "javascript:(function() {" + - "if(!document.querySelectorAll('meta[name=viewport]').length) {"+ - "let meta=document.createElement('meta');"+ - "meta.name='viewport';"+ - "meta.content='width=device-width, initial-scale=1';"+ - "document.head.appendChild(meta);"+ - "}"+ - "})()" - ); + WebViewUtil.injectViewport(view); } }); @@ -162,14 +167,22 @@ public class KMHelpFileActivity extends BaseActivity { @Override protected void onDestroy() { super.onDestroy(); + WebViewUtil.cleanup(webView); } @Override - public void onBackPressed() { - if (webView != null && webView.canGoBack()) { - webView.goBack(); - } else { - super.onBackPressed(); + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + // Dismiss the help file + super.onBackPressed(); + finishAndRemoveTask(); + break; + } } + + return true; } + } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java index 431c244fe6..1d40cc0bbf 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboard.java @@ -13,6 +13,7 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import com.tavultesoft.kmea.BaseActivity; import com.tavultesoft.kmea.data.Keyboard; import com.tavultesoft.kmea.data.KeyboardController; import com.tavultesoft.kmea.KMManager.KeyboardType; @@ -154,10 +155,7 @@ final class KMKeyboard extends WebView { // TODO: Fix base error rather than trying to ignore it "No keyboard stubs exist" if ((cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) && (!cm.message().startsWith("No keyboard stubs exist"))) { - Toast.makeText(context, "Fatal Error with " + currentKeyboard + - ". Loading default keyboard", Toast.LENGTH_LONG).show(); - - // Still send log about falling back to default keyboard (ignore language ID) + // Make Toast notification of error and send log about falling back to default keyboard (ignore language ID) sendError(packageID, keyboardID, ""); Keyboard firstKeyboard = KeyboardController.getInstance().getKeyboardInfo(0); if (firstKeyboard != null) { @@ -590,11 +588,13 @@ final class KMKeyboard extends WebView { } - // Display Toast notification that keyboard selection failed, so loading default keyboard. - // Also sends a message to Sentry + // Display localized Toast notification that keyboard selection failed, so loading default keyboard. + // Also sends a message to Sentry (not localized) private void sendError(String packageID, String keyboardID, String languageID) { - String msg = String.format("Can't set %s::%s for %s language. Loading default keyboard", packageID, keyboardID, languageID); - Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); + BaseActivity.makeToast(context, R.string.fatal_keyboard_error, Toast.LENGTH_LONG, packageID, keyboardID, languageID); + + // Don't localize msg for Sentry + String msg = String.format(context.getString(R.string.fatal_keyboard_error), packageID, keyboardID, languageID); Sentry.captureMessage(msg); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudApiTypes.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudApiTypes.java index 9a6b913aa0..e68bf488eb 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudApiTypes.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/CloudApiTypes.java @@ -7,6 +7,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; +import com.tavultesoft.kmea.BaseActivity; import com.tavultesoft.kmea.R; import com.tavultesoft.kmea.util.DownloadFileUtils; @@ -158,8 +159,7 @@ public class CloudApiTypes { if (filename == null || filename.isEmpty() || cachedFile == null || !cachedFile.exists()) { // failed to retrieve downloaded file - String message = context.getString(R.string.failed_to_retrieve_file); - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + BaseActivity.makeToast(context, R.string.failed_to_retrieve_file, Toast.LENGTH_LONG); } return cachedFile; diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudCatalogDownloadCallback.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudCatalogDownloadCallback.java index 65cf97ba84..74bea7d606 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudCatalogDownloadCallback.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudCatalogDownloadCallback.java @@ -5,6 +5,7 @@ import android.os.Bundle; import android.util.Log; import android.widget.Toast; +import com.tavultesoft.kmea.BaseActivity; import com.tavultesoft.kmea.KeyboardPickerActivity; import com.tavultesoft.kmea.R; import com.tavultesoft.kmea.cloud.CloudApiTypes; @@ -110,7 +111,7 @@ public class CloudCatalogDownloadCallback implements ICloudDownloadCallback aCloudResult) { if (aCloudResult.isEmpty()) { - String msg = aContext.getString(R.string.catalog_unavailable); - Toast.makeText(aContext, msg, Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(aContext, R.string.catalog_unavailable, Toast.LENGTH_SHORT); KMLog.LogError(TAG, "Could not reach server"); return; } @@ -104,16 +104,12 @@ public class CloudLexicalModelMetaDataDownloadCallback implements ICloudDownload if(_r.returnjson.target== CloudApiTypes.ApiTarget.KeyboardLexicalModels) { if( CloudDownloadMgr.getInstance().alreadyDownloadingData(_r.additionalDownloadid)) { - Toast.makeText(aContext, - aContext.getString(R.string.dictionary_download_is_running_in_background), - Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(aContext, R.string.dictionary_download_is_running_in_background, Toast.LENGTH_SHORT); continue; } CloudLexicalPackageDownloadCallback _callback = new CloudLexicalPackageDownloadCallback(); - Toast.makeText(aContext, - aContext.getString(R.string.dictionary_download_start_in_background), - Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(aContext, R.string.dictionary_download_start_in_background, Toast.LENGTH_SHORT); CloudDownloadMgr.getInstance().executeAsDownload(aContext, _r.additionalDownloadid, null, _callback, @@ -152,8 +148,7 @@ public class CloudLexicalModelMetaDataDownloadCallback implements ICloudDownload KMLog.LogException(TAG, "Error parsing lexical model from api.keyman.com. ", e); } } else { - String msg = aContext.getString(R.string.no_associated_model); - Toast.makeText(aContext, msg, Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(aContext, R.string.no_associated_model, Toast.LENGTH_SHORT); } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudLexicalPackageDownloadCallback.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudLexicalPackageDownloadCallback.java index 74f9f436cd..093d99161f 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudLexicalPackageDownloadCallback.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/cloud/impl/CloudLexicalPackageDownloadCallback.java @@ -4,6 +4,7 @@ import android.content.Context; import android.net.Uri; import android.widget.Toast; +import com.tavultesoft.kmea.BaseActivity; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KeyboardEventHandler; import com.tavultesoft.kmea.R; @@ -98,9 +99,7 @@ public class CloudLexicalPackageDownloadCallback implements ICloudDownloadCallba @Override public void applyCloudDownloadToModel(Context aContext, Void aModel, CloudKeyboardDownloadReturns aCloudResult) { - Toast.makeText(aContext, - aContext.getString(R.string.dictionary_download_finished), - Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(aContext, R.string.dictionary_download_finished, Toast.LENGTH_SHORT); if(aCloudResult.installedResource != null) { diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java index 6e26548437..af6e8ff8d4 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java @@ -6,6 +6,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; +import com.tavultesoft.kmea.BaseActivity; import com.tavultesoft.kmea.BuildConfig; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; @@ -347,8 +348,7 @@ public class CloudRepository { preCacheDataSet(context,null,null,null); if(CloudDownloadMgr.getInstance().alreadyDownloadingData(DOWNLOAD_IDENTIFIER_CATALOGUE)) { - String msg = context.getString(R.string.catalog_download_is_running_in_background); - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(context, R.string.catalog_download_is_running_in_background, Toast.LENGTH_SHORT); } return memCachedDataset; @@ -394,12 +394,10 @@ public class CloudRepository { cloudQueries.toArray(params); if (CloudDownloadMgr.getInstance().alreadyDownloadingData(DOWNLOAD_IDENTIFIER_CATALOGUE)) { - String msg = context.getString(R.string.catalog_download_is_running_in_background); - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(context, R.string.catalog_download_is_running_in_background, Toast.LENGTH_SHORT); } else { updateIsRunning = true; - String msg = context.getString(R.string.catalog_download_start_in_background); - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(context, R.string.catalog_download_start_in_background, Toast.LENGTH_SHORT); CloudDownloadMgr.getInstance().executeAsDownload( context, DOWNLOAD_IDENTIFIER_CATALOGUE, memCachedDataset, _download_callback, params); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/logic/ResourcesUpdateTool.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/logic/ResourcesUpdateTool.java index 281c8da218..9495ce30bd 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/logic/ResourcesUpdateTool.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/logic/ResourcesUpdateTool.java @@ -20,6 +20,7 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat.Builder; import androidx.core.app.NotificationManagerCompat; +import com.tavultesoft.kmea.BaseActivity; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KeyboardPickerActivity; import com.tavultesoft.kmea.KMManager; @@ -105,7 +106,7 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl return; } - Toast.makeText(currentContext, currentContext.getString(R.string.update_check_current), Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(currentContext, R.string.update_check_current, Toast.LENGTH_SHORT); lastUpdateCheck = Calendar.getInstance(); SharedPreferences prefs = currentContext.getSharedPreferences(currentContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); @@ -121,7 +122,7 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl return; } - Toast.makeText(currentContext, currentContext.getString(R.string.update_check_unavailable), Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(currentContext, R.string.update_check_unavailable, Toast.LENGTH_SHORT); lastUpdateCheck = Calendar.getInstance(); updateCheckFailed = true; checkingUpdates = false; @@ -193,7 +194,7 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl currentContext.startActivity(intent); } } else { - Toast.makeText(currentContext, "No internet connection", Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(currentContext, R.string.cannot_connect, Toast.LENGTH_SHORT); checkingUpdates = false; } } @@ -554,12 +555,12 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl if (failedUpdateCount > 0) { - Toast.makeText(currentContext, "One or more resources failed to update!", Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(currentContext, R.string.update_failed, Toast.LENGTH_SHORT); lastUpdateCheck = Calendar.getInstance(); updateFailed = true; checkingUpdates = false; } else { - Toast.makeText(currentContext, "Resources successfully updated!", Toast.LENGTH_SHORT).show(); + BaseActivity.makeToast(currentContext, R.string.update_success, Toast.LENGTH_SHORT); lastUpdateCheck = Calendar.getInstance(); SharedPreferences prefs = currentContext.getSharedPreferences(currentContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java index d3cda63a95..a2a3024d2d 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/HelpFile.java @@ -13,7 +13,9 @@ import android.widget.Toast; import androidx.core.content.FileProvider; +import com.tavultesoft.kmea.BaseActivity; import com.tavultesoft.kmea.KMManager; +import com.tavultesoft.kmea.R; import com.tavultesoft.kmea.util.FileProviderUtils; import com.tavultesoft.kmea.util.FileUtils; @@ -47,8 +49,11 @@ public final class HelpFile { i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); } catch (NullPointerException e) { - String message = "FileProvider undefined in app to load" + customHelp.toString(); - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + // Localize Toast error + BaseActivity.makeToast(context, R.string.fileprovider_undefined, Toast.LENGTH_LONG); + + // Don't localize message to Sentry + String message = String.format(context.getString(R.string.fileprovider_undefined), customHelp.toString()); KMLog.LogException(TAG, message, e); } } else { diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/WebViewUtil.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/WebViewUtil.java new file mode 100644 index 0000000000..43a67083c8 --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/WebViewUtil.java @@ -0,0 +1,41 @@ +/** + * Copyright (C) 2021 SIL International. All rights reserved. + */ +package com.tavultesoft.kmea.util; + +import android.view.ViewGroup; +import android.webkit.WebView; + +public final class WebViewUtil { + // Inject a meta viewport tag into the head of the file if it doesn't exist + public static void injectViewport(WebView webView) { + if (webView != null) { + webView.loadUrl( + "javascript:(function() {" + + "if(document.head && !document.querySelectorAll('meta[name=viewport]').length) {"+ + "let meta=document.createElement('meta');"+ + "meta.name='viewport';"+ + "meta.content='width=device-width, initial-scale=1';"+ + "document.head.appendChild(meta);"+ + "}"+ + "})()" + ); + } + } + + // Blank the webView and destroy completely + // Reference: https://stackoverflow.com/questions/17418503/destroy-webview-in-android/17458577#17458577 + public static void cleanup(WebView webView) { + if (webView != null) { + webView.loadUrl("about:blank"); + ViewGroup viewGroup = (ViewGroup) webView.getParent(); + if (viewGroup != null) { + viewGroup.removeView(webView); + } + webView.removeAllViews(); + webView.destroy(); + webView = null; + } + + } +} diff --git a/android/KMEA/app/src/main/res/values-de-rDE/strings.xml b/android/KMEA/app/src/main/res/values-de-rDE/strings.xml index 452d732bf2..4132f32eea 100644 --- a/android/KMEA/app/src/main/res/values-de-rDE/strings.xml +++ b/android/KMEA/app/src/main/res/values-de-rDE/strings.xml @@ -63,7 +63,7 @@ [neu] %1$s Scannen Sie diesen Code, um diese\nTastatur auf einem anderen Gerät zu laden - + Willkommen bei %1$s Suche nach zugehörigem Wörterbuch zum Herunterladen @@ -91,6 +91,8 @@ Das ausgewählte Wörterbuch wird bereits heruntergeladen. Bitte versuchen Sie es gleich nochmal! Download des Wörterbuchs ist abgeschlossen. + + Laden der heruntergeladenen Datei fehlgeschlagen Fehler beim Zugriff auf den Server! @@ -123,6 +125,8 @@ (%1$d Tastatur) (%1$d Tastaturen) + + Standardsprache diff --git a/android/KMEA/app/src/main/res/values-fr-rFR/strings.xml b/android/KMEA/app/src/main/res/values-fr-rFR/strings.xml index 58e4805793..f6acf877d9 100644 --- a/android/KMEA/app/src/main/res/values-fr-rFR/strings.xml +++ b/android/KMEA/app/src/main/res/values-fr-rFR/strings.xml @@ -91,8 +91,6 @@ Le dictionnaire sélectionné est déjà en cours de téléchargement ; veuillez réessayer dans un instant ! Téléchargement du dictionnaire terminé. - - Impossible d\'accéder au fichier téléchargé Impossible de récupérer le fichier téléchargé diff --git a/android/KMEA/app/src/main/res/values/strings.xml b/android/KMEA/app/src/main/res/values/strings.xml index 982a3cc79b..a7a52e55b2 100644 --- a/android/KMEA/app/src/main/res/values/strings.xml +++ b/android/KMEA/app/src/main/res/values/strings.xml @@ -58,7 +58,7 @@ - \nCannot connect to Keyman server!\n + Cannot connect to Keyman server! Would you like to delete this keyboard? @@ -106,6 +106,13 @@ Welcome to %1$s + + + FileProvider library needed to view help file: %1$s + + + + Fatal keyboard error with %1$s:%2$s for %3$s language. Loading default keyboard. Checking for associated dictionary to download @@ -148,8 +155,8 @@ Dictionary download is finished. - - Failed to access downloaded file + + Download failed Failed to retrieve downloaded file @@ -160,6 +167,11 @@ "All resources are up to date!" + + One or more resources failed to update! + + + Resources successfully updated! Dictionary version diff --git a/android/help/about/whatsnew.md b/android/help/about/whatsnew.md index e1e04565ec..8ef08ac101 100644 --- a/android/help/about/whatsnew.md +++ b/android/help/about/whatsnew.md @@ -17,4 +17,8 @@ Here are some of the new features we have added to Keyman for Android 14.0: * Match user input capital letters when offering suggestions (#3845) * Update minimum Android SDK to 21 (Android 5.0 Lollipop) (#2993) * Keyman now works more reliably with WeChat and Telegram (#4254) -* Added new menu to change the display language (#4261) +* Added new Settings menu to [Change Display Language](../basic/config/index#Change-Display-Language): (#4261) + * French + * German + * Khmer + * Obolo diff --git a/common/core/desktop/.gitignore b/common/core/desktop/.gitignore index a1d99c33ae..cd78b2f40f 100644 --- a/common/core/desktop/.gitignore +++ b/common/core/desktop/.gitignore @@ -1,2 +1,3 @@ .vs/ build*/ +configure/ diff --git a/common/core/desktop/debian/control b/common/core/desktop/debian/control index 57bda43e45..64f5c721fd 100644 --- a/common/core/desktop/debian/control +++ b/common/core/desktop/debian/control @@ -5,11 +5,12 @@ Maintainer: Debian Input Method Team Uploaders: Daniel Glassey , Keyman team , + Eberhard Beilharz , Build-Depends: debhelper (>= 11), meson (>= 0.45), ninja-build, -Standards-Version: 4.5.0 +Standards-Version: 4.5.1 Vcs-Git: https://github.com/keymanapp/keyman.git Vcs-Browser: https://github.com/keymanapp/keyman/tree/master/common/core/desktop Homepage: https://www.keyman.com diff --git a/common/core/desktop/debian/upstream/signing-key.asc b/common/core/desktop/debian/upstream/signing-key.asc new file mode 100644 index 0000000000..a235c64d44 --- /dev/null +++ b/common/core/desktop/debian/upstream/signing-key.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=jcZL +-----END PGP PUBLIC KEY BLOCK----- diff --git a/common/core/desktop/doc/BUILDING.md b/common/core/desktop/doc/BUILDING.md index 066d3adb63..fa9e65340e 100644 --- a/common/core/desktop/doc/BUILDING.md +++ b/common/core/desktop/doc/BUILDING.md @@ -1,6 +1,7 @@ # How to build the keyboard processor ## Prerequisites + - Python 3 - Meson build system. - C++14 or later compiler (VC++ 2017 or later for Windows). @@ -8,46 +9,55 @@ - kmcomp (for tests) -- must be added to path ## Installing Python3 + ### Linux + You will be able to install a python3 package in any reputable recent version of linux using its package manager if it's not already installed. ### Mac OS X + You can get the official installer from the official Python site: -[https://www.python.org/downloads/mac-osx/]() + ### Windows -You can get the official installer from the official Python site: -[https://www.python.org/downloads/windows/]() +You can get the official installer from the official Python site: + ## Installing Meson + Ensure you have Python3 correctly installed and can run the command `pip3`. -``` + +```bash python3 -m pip install meson ``` ## Building ### Building on Windows + For Windows, use `build.bat` -- this handles environment and x86/x64 cross-compiles with Visual Studio 2017+. You may need to set `SDKVER` environement variable to the current Windows SDK version, if it cannot be automatically detected, e.g.: -``` +```DOS set SDKVER=10.0.19041.0 ``` To build: -``` + +```DOS build.bat all ``` ### Building on Linux, macOS + For all other platforms, in your source directory do the following: -``` + +```bash cd desktop meson build --werror cd build @@ -55,24 +65,29 @@ ninja meson test ``` +For a debug build, pass `--buildtype debug` to meson. + ## Note on kmcomp -kmcomp is the command-line compiler from Keyman Developer, available from https://keyman.com/ or -in this repo in /windows/src/developer/kmcomp. The compiler is currently available as a Windows + +kmcomp is the command-line compiler from Keyman Developer, available from or +in this repo in `/windows/src/developer/kmcomp`. The compiler is currently available as a Windows PE executable only, but it does run under WINE. ## Additional configuration notes ### Windows + The search path can be edited through System settings / Advanced system settings / Environment Variables / User environment variables. -If you have Keyman Developer installed, add %KeymanDeveloperPath% to your path. Otherwise, add +If you have Keyman Developer installed, add `%KeymanDeveloperPath%` to your path. Otherwise, add the path where you extracted the kmcomp archive. ### Linux + You need a wrapper `kmcomp` shell script: -``` +```bash #!/bin/bash wine `dirname "$0"`/kmcomp.exe "$@" ``` @@ -81,4 +96,5 @@ Place this in the same folder as you extracted kmcomp.exe, and `chmod +x kmcomp` to the path (e.g. `export PATH=/path/to/kmcomp:$PATH`, which you can add to `.bashrc`) ### macOS + TODO diff --git a/common/core/desktop/doc/markdown_files/index.md b/common/core/desktop/doc/markdown_files/index.md index b584842896..98870c356d 100644 --- a/common/core/desktop/doc/markdown_files/index.md +++ b/common/core/desktop/doc/markdown_files/index.md @@ -1,6 +1,7 @@ # Keyman Keyboard Processor API ## Requirements + 1. Cross platform. 2. Cross language. 3. Facilitate stateless operation of the Engine. @@ -9,8 +10,8 @@ 6. Support querying Keyboard attributes. 7. Idempotent - ## Design decisions in support of requirements: + - Use C or C99 types and calling convention for the interface, it has the broadest language FFI support. [1,2] - Have client (platform glue) code load keyboards, manage & pass state. [3,4,7] @@ -18,8 +19,8 @@ engine [5,6] - Provide get/set calls for client accessible keyboard state information [3,4] - ## Glossary + - __Platform layer:__ the code that consumes the Keyman Keyboard Processor API, and provides the operating system-specific handling of keystroke events and integration with @@ -29,22 +30,32 @@ the application that has the focus and receives text events from the Platform layer. - __Context:__ Text preceding the insertion point - __Marker:__ Positional state that can be placed in the Context. -- __Keyboard:__ A set of rules for execution my an Engine +- __Keyboard:__ A set of rules for execution by an Engine - __Option:__ A variable in a dynamic or static key value store. - __Processor:__ The component that implements this API and can parse and execute a particular keyboard. -- __State:__ An object that hold internal state of the Processor for a given +- __State:__ An object that holds internal state of the Processor for a given insertion point - __Action:__ A directive output by the processor detailing how the Platform layer should transform the Client Application's text buffer. There may be several items produced by a single keyboard event. - __Keyboard Event:__ -A virtual key board event and modifier map recevied from the platform to be +A virtual key board event and modifier map recevied from the Platform layer to be processed with the state object for this Client application. - +- __Virtual Key:__ +A code based on the US English layout, with values matching the Windows +virtual key codes. See `keyboardprocessor_vkeys.h` for definitions. +- __Modifier Key:__ +The set of Control, Shift, Alt, Caps Lock keys. On some platforms these may +have other names (e.g. Alt is called Option on macOS); other platform-specific +modifiers such as Windows key are excluded from this set. Some modifiers are +transient, such as Control, and others have long-lasting state, such as +Caps Lock. ## API + ### Namespace + All calls, types and enums are prefixed with the namespace identifier `km_kbp_` diff --git a/common/core/desktop/include/keyman/keyboardprocessor.h.in b/common/core/desktop/include/keyman/keyboardprocessor.h.in index 2f31bb3654..ce86adb5a0 100644 --- a/common/core/desktop/include/keyman/keyboardprocessor.h.in +++ b/common/core/desktop/include/keyman/keyboardprocessor.h.in @@ -41,23 +41,23 @@ The application that has the focus and receives text events from the Platform layer. - __Context:__ Text preceding the insertion point - __Marker:__ Positional state that can be placed in the Context. -- __Keyboard:__ A set of rules for execution my an Engine +- __Keyboard:__ A set of rules for execution by an Engine - __Option:__ A variable in a dynamic or static key value store. - __Processor:__ The component that implements this API and can parse and execute a particular keyboard. -- __State:__ An object that hold internal state of the Processor for a given +- __State:__ An object that holds internal state of the Processor for a given insertion point - __Action:__ A directive output by the processor detailing how the Platform layer should transform the Client Application's text buffer. There may be several items produced by a single keyboard event. - __Keyboard Event:__ -A virtual key event and modifier map received from the platform to be +A virtual key event and modifier map received from the Platform layer to be processed with the state object for this Client application. - __Virtual Key:__ A code based on the US English layout, with values matching the Windows -virtual key codes. See keyboardprocessor_vkeys.h for definitions. +virtual key codes. See `keyboardprocessor_vkeys.h` for definitions. - __Modifier Key:__ The set of Control, Shift, Alt, Caps Lock keys. On some platforms these may have other names (e.g. Alt is called Option on macOS); other platform-specific diff --git a/common/core/desktop/src/kmx/kmx_debug.cpp b/common/core/desktop/src/kmx/kmx_debug.cpp index 46d75ac35d..a58fd8e17e 100644 --- a/common/core/desktop/src/kmx/kmx_debug.cpp +++ b/common/core/desktop/src/kmx/kmx_debug.cpp @@ -54,25 +54,29 @@ int km::kbp::kmx::DebugLog_1(const char *file, int line, const char *function, c fmtbuf[255] = 0; va_end(vars); - if(g_debug_KeymanLog) { - if(g_debug_ToConsole) { // I3951 - char windowinfo[1024]; - sprintf(windowinfo, - "%ld" TAB //"TickCount" TAB - "%s:%d" TAB //"SourceFile" TAB - "%s" TAB //"Function" - "%s" NL, //"Message" + if (!g_debug_KeymanLog) + return 0; - GetTickCount(), //"TickCount" TAB - file, line, //"SourceFile" TAB - function, //"Function" TAB - fmtbuf); //"Message" + char windowinfo[1024]; + sprintf(windowinfo, + "%ld" TAB //"TickCount" TAB + "%s:%d" TAB //"SourceFile" TAB + "%s" TAB //"Function" + "%s" NL, //"Message" + + GetTickCount(), //"TickCount" TAB + file, line, //"SourceFile" TAB + function, //"Function" TAB + fmtbuf); //"Message" + + if (g_debug_ToConsole) { // I3951 + std::cout << windowinfo << std::endl; // OutputDebugStringA(windowinfo); + } else { #ifdef _USE_WINDOWS - std::cout << windowinfo << std::endl; // OutputDebugStringA(windowinfo); + std::cout << windowinfo << std::endl; // OutputDebugStringA(windowinfo); #else - syslog(LOG_DEBUG, "%s", windowinfo); + syslog(LOG_DEBUG, "%s", windowinfo); #endif - } } return 0; @@ -125,7 +129,8 @@ const char *km::kbp::kmx::Debug_UnicodeString(PKMX_WCHAR s, int x) { bufout[x][0] = 0; for (p = s, q = bufout[x]; *p && (p - s < 128); p++) { - sprintf(q, "U+%4.4X ", *p); q = strchr(q, 0); + sprintf(q, "U+%4.4X ", *p); + q = strchr(q, 0); } //WideCharToMultiByte(CP_ACP, 0, buf, -1, bufout, 128, NULL, NULL); return bufout[x]; diff --git a/common/core/desktop/src/kmx/kmx_file.cpp b/common/core/desktop/src/kmx/kmx_file.cpp index 945186a172..81cd36cf0e 100644 --- a/common/core/desktop/src/kmx/kmx_file.cpp +++ b/common/core/desktop/src/kmx/kmx_file.cpp @@ -82,7 +82,7 @@ KMX_BOOL KMX_Processor::LoadKeyboard(km_kbp_path_name fileName, LPKEYBOARD *lpKe LPKEYBOARD kbp; PKMX_BYTE filebase; - DebugLog("Loading file %ws", fileName); + DebugLog("Loading file '%s'", fileName); if(!fileName || !lpKeyboard) { DebugLog("Bad Filename"); diff --git a/common/core/desktop/src/kmx/kmx_processor.cpp b/common/core/desktop/src/kmx/kmx_processor.cpp index 9a5dc643bd..01dfbc6e59 100644 --- a/common/core/desktop/src/kmx/kmx_processor.cpp +++ b/common/core/desktop/src/kmx/kmx_processor.cpp @@ -10,7 +10,8 @@ using namespace kmx; /* Globals */ -KMX_BOOL km::kbp::kmx::g_debug_ToConsole = TRUE, km::kbp::kmx::g_debug_KeymanLog = TRUE; +KMX_BOOL km::kbp::kmx::g_debug_ToConsole = FALSE; +KMX_BOOL km::kbp::kmx::g_debug_KeymanLog = TRUE; KMX_BOOL km::kbp::kmx::g_silent = FALSE; /* @@ -120,7 +121,7 @@ KMX_BOOL KMX_Processor::ProcessGroup(LPGROUP gp, KMX_BOOL *pOutputKeystroke) PKMX_WCHAR p; int sdmfI; - /* + /* If the number of nested groups goes higher than 50, then break out - this is a limitation of stack size. This is basically a catch-all for freaky apps that cause message loopbacks and nasty things like that. Okay, it's really a catch all diff --git a/common/core/desktop/src/kmx/kmx_processor.h b/common/core/desktop/src/kmx/kmx_processor.h index 52ec63b889..7a4ee36e32 100644 --- a/common/core/desktop/src/kmx/kmx_processor.h +++ b/common/core/desktop/src/kmx/kmx_processor.h @@ -128,10 +128,8 @@ const char *Debug_UnicodeString(PKMX_WCHAR s, int x = 0); const char *Debug_UnicodeString(std::u16string s, int x = 0); const char *Debug_ModifierName(KMX_UINT modifiers); - //inline KMX_BOOL ShouldDebug(); - inline KMX_BOOL ShouldDebug() { - return TRUE; // g_debug_KeymanLog; + return g_debug_KeymanLog; } diff --git a/common/core/desktop/src/kmx/kmx_xstring.cpp b/common/core/desktop/src/kmx/kmx_xstring.cpp index 8a3595734d..87267278a9 100644 --- a/common/core/desktop/src/kmx/kmx_xstring.cpp +++ b/common/core/desktop/src/kmx/kmx_xstring.cpp @@ -121,7 +121,7 @@ PKMX_WCHAR km::kbp::kmx::incxstr(PKMX_WCHAR p) case CODE_INDEX: return p+2; case CODE_USE: return p+1; case CODE_DEADKEY: return p+1; - case CODE_EXTENDED: p += 2; while(*p != UC_SENTINEL_EXTENDEDEND) p++; return p+1; + case CODE_EXTENDED: p += 2; while(*p && *p != UC_SENTINEL_EXTENDEDEND) p++; return p+1; case CODE_CLEARCONTEXT: return p+1; case CODE_CALL: return p+1; case CODE_CONTEXTEX: return p+1; diff --git a/common/core/desktop/tests/unit/kmx/README.md b/common/core/desktop/tests/unit/kmx/README.md new file mode 100644 index 0000000000..39de91f403 --- /dev/null +++ b/common/core/desktop/tests/unit/kmx/README.md @@ -0,0 +1,62 @@ +# KMX unit tests + +This directory contains unit tests that test the KMX processing. + +The driver (`kmx.cpp`) loads and executes the tests. Each test consist of a +Keyman keyboard source file (`0*.kmn`) and the compiled KMX keyboard. +The source file contains rules and defines the setup, input and expected results. + +## Defining tests in the `.kmn` file + +The top of the file contains comments that define the setup, input and expected results. +Following the comments are the normal rules of a Keyman keyboard. + +### Setup + +- **description**: this is just for commenting what we test here + +```text +c Description: Tests Caps Lock env set +``` + +- **option**: allows to put the system in a defined state. The example below turns + caps-lock off. + +```text +c option: &capsLock=0 +``` + +- **context**: allows to setup a string which serves as context + +```text +c context: +``` + +### Input + +- **keys**: defines one or more virtual keys that will be processed one after the + other + +```text +c keys: [K_1][K_CAPS][K_2][SHIFT K_3][K_4][K_CAPS][K_5][K_CAPS][K_6] +``` + +### Expected Result + +- **expected**: the resulting string of processing the input keys. In the example + below each key press is expected to output the string `'pass.'`. + +```text +c expected: pass.pass.pass.pass.pass.pass. +``` + +## Running the tests + +All tests can be run at once with `meson test`. + +Alternatively it's possible to run a single test with: + +```bash +cd common/core/desktop +build/tests/unit/kmx/kmx 'tests/unit/kmx/038 - punctkeys.kmn' 'tests/unit/kmx/038 - punctkeys.kmx' +``` diff --git a/common/core/desktop/tests/unit/kmx/kmx.cpp b/common/core/desktop/tests/unit/kmx/kmx.cpp index 32ca104e8f..bcb047f8d1 100644 --- a/common/core/desktop/tests/unit/kmx/kmx.cpp +++ b/common/core/desktop/tests/unit/kmx/kmx.cpp @@ -510,5 +510,6 @@ int main(int argc, char *argv[]) return 1; } + km::kbp::kmx::g_debug_ToConsole = TRUE; return run_test(argv[1], argv[2]); } diff --git a/common/core/web/input-processor/src/text/inputProcessor.ts b/common/core/web/input-processor/src/text/inputProcessor.ts index d5f7813fe9..759dccc8d0 100644 --- a/common/core/web/input-processor/src/text/inputProcessor.ts +++ b/common/core/web/input-processor/src/text/inputProcessor.ts @@ -55,8 +55,10 @@ namespace com.keyman.text { * Handles default output and keyboard processing for both OSK and physical keystrokes. * * @param {Object} e The abstracted KeyEvent to use for keystroke processing + * @returns {Object} A RuleBehavior object describing the cumulative effects of + * all matched keyboard rules. */ - processKeyEvent(keyEvent: KeyEvent) { + processKeyEvent(keyEvent: KeyEvent): RuleBehavior { let formFactor = keyEvent.device.formFactor; // Determine the current target for text output and create a "mock" backup @@ -70,14 +72,14 @@ namespace com.keyman.text { // If it's a desktop OSK style and this triggers a layer change, // a modifier key was clicked. No output expected, so it's safe to instantly exit. if(this.keyboardProcessor.selectLayer(keyEvent)) { - return true; + return new RuleBehavior(); } } // Will handle keystroke-based non-layer change modifier & state keys, mapping them through the physical keyboard's version // of state management. if(!fromOSK && this.keyboardProcessor.doModifierPress(keyEvent, !fromOSK)) { - return true; + return new RuleBehavior(); } // If suggestions exist AND space is pressed, accept the suggestion and do not process the keystroke. @@ -88,10 +90,10 @@ namespace com.keyman.text { // Can the suggestion UI revert a recent suggestion? If so, do that and swallow the backspace. if((keyEvent.kName == "K_BKSP" || keyEvent.Lcode == Codes.keyCodes["K_BKSP"]) && this.languageProcessor.tryRevertSuggestion()) { - return; + return new RuleBehavior(); // Can the suggestion UI accept an existing suggestion? If so, do that and swallow the space character. } else if((keyEvent.kName == "K_SPACE" || keyEvent.Lcode == Codes.keyCodes["K_SPACE"]) && this.languageProcessor.tryAcceptSuggestion('space')) { - return; + return new RuleBehavior(); } } @@ -171,15 +173,6 @@ namespace com.keyman.text { } } - /* I732 END - 13/03/2007 MCD: End Positional Layout support in OSK */ - - // TODO: rework the return value to be `ruleBehavior` instead. Functions that call this one are - // the ones that should worry about event handler returns, etc. Not this one. - // - // They should also be the ones to handle the TODOs seen earlier in this function - - // once THOSE are properly relocated. (They're too DOM-heavy to remain in web-core.) - - // Only return true (for the eventual event handler's return value) if we didn't match a rule. return ruleBehavior; } diff --git a/common/core/web/input-processor/src/text/prediction/languageProcessor.ts b/common/core/web/input-processor/src/text/prediction/languageProcessor.ts index 13d49a6e87..8abf1cb2a4 100644 --- a/common/core/web/input-processor/src/text/prediction/languageProcessor.ts +++ b/common/core/web/input-processor/src/text/prediction/languageProcessor.ts @@ -255,7 +255,7 @@ namespace com.keyman.text.prediction { // the input will be automatically rewound to the preInput state. transform: original.transform, // The ID part is critical; the reversion can't be applied without it. - transformId: original.token, // reversions use the additive inverse. + transformId: -original.token, // reversions use the additive inverse. displayAs: reversion.displayAs, // The real reason we needed to call the LMLayer. id: reversion.id, tag: reversion.tag diff --git a/common/core/web/keyboard-processor/src/keyboards/defaultLayouts.ts b/common/core/web/keyboard-processor/src/keyboards/defaultLayouts.ts index 859e870bb2..03acb6860f 100644 --- a/common/core/web/keyboard-processor/src/keyboards/defaultLayouts.ts +++ b/common/core/web/keyboard-processor/src/keyboards/defaultLayouts.ts @@ -37,15 +37,15 @@ namespace com.keyman.keyboards { shiftKey?: LayoutKey, capsKey?: LayoutKey, numKey?: LayoutKey, - scrollKey?: LayoutKey + scrollKey?: LayoutKey, aligned?: boolean } export type LayoutFormFactor = { + "displayUnderlying"?: boolean, "font": string, "layer": LayoutLayer[], - keyLabels?: boolean, - isDefault?: boolean; + isDefault?: boolean } export type LayoutSpec = { @@ -172,6 +172,11 @@ namespace com.keyman.keyboards { } } + // If there is no predefined layout, even touch layouts will follow the desktop's + // setting for the displayUnderlying flag. As the desktop layout uses a different + // format for its layout spec, that's found at the field referenced below. + layout["displayUnderlying"] = !!keyboard.scriptObject['KDU']; + // For desktop devices, we must create all layers, even if invalid. if(formFactor == 'desktop') { invalidIdList = Layouts.generateLayerIds(chiral); diff --git a/common/core/web/keyboard-processor/src/keyboards/keyboard.ts b/common/core/web/keyboard-processor/src/keyboards/keyboard.ts index 8c63754159..742507bd8f 100644 --- a/common/core/web/keyboard-processor/src/keyboards/keyboard.ts +++ b/common/core/web/keyboard-processor/src/keyboards/keyboard.ts @@ -71,11 +71,6 @@ namespace com.keyman.keyboards { return this.scriptObject['KN']; } - get displaysUnderlyingKeys(): boolean { - // Returns false if undefined or false-like (including 0), true otherwise. - return !!this.scriptObject['KDU']; - } - // TODO: Better typing. private get _legacyLayoutSpec(): any { return this.scriptObject['KV']; // used with buildDefaultLayout; layout must be constructed at runtime. diff --git a/common/core/web/keyboard-processor/src/text/keyMapping.ts b/common/core/web/keyboard-processor/src/text/keyMapping.ts index cb45620394..ba7cc09868 100644 --- a/common/core/web/keyboard-processor/src/text/keyMapping.ts +++ b/common/core/web/keyboard-processor/src/text/keyMapping.ts @@ -14,10 +14,12 @@ namespace com.keyman { Opera: KeyMap = new KeyMap(); constructor() { - //ffie['k109'] = 189; // - // These two number-pad VK rules are *not* correct for more recent FF! JMD 8/11/12 - //ffie['k107'] = 187; // = // FF 3.0 // I2062 + // All three have been around since at least May 2014 / FF 29. + // It'd hard to find precise history, but at least that much has been confirmed. + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode, on Feb 26 2021. this.FF['k61'] = 187; // = // FF 2.0 this.FF['k59'] = 186; // ; + this.FF['k173'] = 189; // -/_ } } diff --git a/common/core/web/keyboard-processor/src/text/outputTarget.ts b/common/core/web/keyboard-processor/src/text/outputTarget.ts index 64c12ea5cf..bab5a00e2e 100644 --- a/common/core/web/keyboard-processor/src/text/outputTarget.ts +++ b/common/core/web/keyboard-processor/src/text/outputTarget.ts @@ -116,7 +116,8 @@ namespace com.keyman.text { let toCaret = this.getDeadkeyCaret(); // Step 1: Determine the number of left-deletions. - for(var newCaret=0; newCaret < fromCaret; newCaret++) { + let maxLeftMatch = fromCaret < toCaret ? fromCaret : toCaret; + for(var newCaret=0; newCaret < maxLeftMatch; newCaret++) { if(from._kmwCharAt(newCaret) != to._kmwCharAt(newCaret)) { break; } @@ -132,8 +133,16 @@ namespace com.keyman.text { let undeletedRight = to._kmwLength() - toCaret; let originalRight = from._kmwLength() - fromCaret; + let deletedRight = originalRight - undeletedRight; - return new TextTransform(delta, deletedLeft, originalRight - undeletedRight); + // May occur when reverting a suggestion that had been applied mid-word. + if(deletedRight < 0) { + // Restores deleteRight characters. + delta = delta + to._kmwSubstr(toCaret, -deletedRight); + deletedRight = 0; + } + + return new TextTransform(delta, deletedLeft, deletedRight); } buildTranscriptionFrom(original: OutputTarget, keyEvent: KeyEvent, alternates?: Alternate[]): Transcription { diff --git a/common/core/web/keyboard-processor/src/text/ruleBehavior.ts b/common/core/web/keyboard-processor/src/text/ruleBehavior.ts index 012cb44c97..7532abd90b 100644 --- a/common/core/web/keyboard-processor/src/text/ruleBehavior.ts +++ b/common/core/web/keyboard-processor/src/text/ruleBehavior.ts @@ -4,9 +4,10 @@ namespace com.keyman.text { */ export class RuleBehavior { /** - * The before-and-after Transform from matching a keyboard rule. + * The before-and-after Transform from matching a keyboard rule. May be `null` + * if no keyboard rules were matched for the keystroke. */ - transcription: Transcription; + transcription: Transcription = null; /** * Indicates whether or not a BEEP command was issued by the matched keyboard rule. @@ -26,7 +27,7 @@ namespace com.keyman.text { /** * Denotes a non-output default behavior; this should be evaluated later, against the true keystroke. */ - triggersDefaultCommand?: boolean; + triggersDefaultCommand: boolean = false; /** * Denotes error log messages generated when attempting to generate this behavior. @@ -44,6 +45,10 @@ namespace com.keyman.text { predictionPromise?: Promise; finalize(processor: KeyboardProcessor) { + if(!this.transcription) { + throw "Cannot finalize a RuleBehavior with no transcription."; + } + let outputTarget = this.transcription.keystroke.Ltarg; if(processor.beepHandler && this.beep) { diff --git a/common/models/templates/src/common.ts b/common/models/templates/src/common.ts index babd2cf247..38b0d5f08f 100644 --- a/common/models/templates/src/common.ts +++ b/common/models/templates/src/common.ts @@ -4,16 +4,16 @@ namespace models { export function applyTransform(transform: Transform, context: Context): Context { // First, get the current context let fullLeftContext = context.left || ''; - let lLen = fullLeftContext.length; + let lLen = fullLeftContext.kmwLength(); let lDel = lLen < transform.deleteLeft ? lLen : transform.deleteLeft; - let leftContext = fullLeftContext.substring(0, lLen - lDel) + (transform.insert || ''); + let leftContext = fullLeftContext.kmwSubstr(0, lLen - lDel) + (transform.insert || ''); let fullRightContext = context.right || ''; - let rLen = fullRightContext.length; + let rLen = fullRightContext.kmwLength(); let rDel = rLen < transform.deleteRight ? rLen : transform.deleteRight; - let rightContext = fullRightContext.substring(rDel); + let rightContext = fullRightContext.kmwSubstr(rDel); return { left: leftContext, diff --git a/common/predictive-text/unit_tests/headless/worker-model-compositor.js b/common/predictive-text/unit_tests/headless/worker-model-compositor.js index 59e12de09c..af5c633bb8 100644 --- a/common/predictive-text/unit_tests/headless/worker-model-compositor.js +++ b/common/predictive-text/unit_tests/headless/worker-model-compositor.js @@ -10,6 +10,71 @@ var ModelCompositor = require('../../build/intermediate').ModelCompositor; describe('ModelCompositor', function() { describe('Prediction with 14.0+ models', function() { + describe('Basic suggestion generation', function() { + var plainModel = new TrieModel(jsonFixture('tries/english-1000'), + {wordBreaker: wordBreakers.default} + ); + + it('generates suggestions with expected properties', function() { + let compositor = new ModelCompositor(plainModel); + let context = { + left: 'th', startOfBuffer: true, endOfBuffer: true, + }; + + let inputTransform = { + insert: 'e', + deleteLeft: 0 + }; + + let suggestions = compositor.predict(inputTransform, context); + suggestions.forEach(function(suggestion) { + // Suggstions are built based on the context state BEFORE the triggering + // input, replacing the prediction's root with the complete word. + // + // This is necessary, in part, for proper display-string construction. + assert.equal(suggestion.transform.deleteLeft, 2); + }); + + let keep = suggestions.find(function(suggestion) { + return suggestion.tag == 'keep'; + }); + + assert.isDefined(keep); + assert.equal(keep.transform.insert, 'the '); + + // Expect an appended space. + let expectedEntries = ['they ', 'there ', 'their ', 'these ', 'themselves ']; + expectedEntries.forEach(function(entry) { + assert.isDefined(suggestions.find(function(suggestion) { + return suggestion.transform.insert == entry; + })); + }); + }); + + it('properly handles suggestions after a backspace', function() { + let compositor = new ModelCompositor(plainModel); + let context = { + left: 'the ', startOfBuffer: true, endOfBuffer: true, + }; + + let inputTransform = { + insert: '', + deleteLeft: 1 + }; + + let suggestions = compositor.predict(inputTransform, context); + suggestions.forEach(function(suggestion) { + // Suggestions always delete the full root of the suggestion. + // + // After a backspace, that means the text 'the' - 3 chars. + // Char 4 is for the original backspace, as suggstions are built + // based on the context state BEFORE the triggering input - + // here, a backspace. + assert.equal(suggestion.transform.deleteLeft, 4); + }); + }); + }); + describe('applySuggestionCasing', function() { let plainApplyCasing = function(caseToApply, text) { switch(caseToApply) { @@ -764,7 +829,7 @@ describe('ModelCompositor', function() { let baseSuggestion = initialSuggestions[1]; let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform); - assert.equal(reversion.transformId, baseSuggestion.transformId); + assert.equal(reversion.transformId, -baseSuggestion.transformId); assert.equal(reversion.id, -baseSuggestion.id); let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext); @@ -802,7 +867,7 @@ describe('ModelCompositor', function() { let baseSuggestion = initialSuggestions[1]; let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform); - assert.equal(reversion.transformId, baseSuggestion.transformId); + assert.equal(reversion.transformId, -baseSuggestion.transformId); assert.equal(reversion.id, -baseSuggestion.id); // Accepting the suggestion adds an extra context state. diff --git a/common/predictive-text/worker/model-compositor.ts b/common/predictive-text/worker/model-compositor.ts index f27bf50b31..3b05b9ad28 100644 --- a/common/predictive-text/worker/model-compositor.ts +++ b/common/predictive-text/worker/model-compositor.ts @@ -175,11 +175,23 @@ class ModelCompositor { finalInput = inputTransform; // A fallback measure. Greatly matters for empty contexts. } + let deleteLeft = 0; + // remove actual token string. If new token, there should be nothing to delete. + if(!newEmptyToken) { + // If this is triggered from a backspace, make sure to use its results + // and also include its left-deletions! It's the one post-input context case. + if(allowBksp) { + deleteLeft = this.wordbreak(postContext).kmwLength() + inputTransform.deleteLeft; + } else { + // Normal case - use the pre-input context. + deleteLeft = this.wordbreak(context).kmwLength(); + } + } + // Replace the existing context with the correction. let correctionTransform: Transform = { insert: correction, // insert correction string - // remove actual token string. If new token, there should be nothing to delete. - deleteLeft: newEmptyToken ? 0 : this.wordbreak(context).length, + deleteLeft: deleteLeft, id: inputTransform.id // The correction should always be based on the most recent external transform/transcription ID. } @@ -220,10 +232,15 @@ class ModelCompositor { } // Section 2 - post-analysis for our generated predictions, managing 'keep'. - // Assumption: Duplicated 'displayAs' properties indicate duplicated Suggestions. // When true, we can use an 'associative array' to de-duplicate everything. let suggestionDistribMap: {[key: string]: ProbabilityMass} = {}; + let currentCasing: CasingForm = null; + if(lexicalModel.languageUsesCasing) { + currentCasing = this.detectCurrentCasing(postContext); + } + + let baseWord = this.wordbreak(context); // Deduplicator + annotator of 'keep' suggestions. for(let prediction of rawPredictions) { @@ -253,6 +270,16 @@ class ModelCompositor { keepOption.p += prediction.p; } } else { + // Apply capitalization rules now; facilitates de-duplication of suggestions + // that may be caused as a result. + // + // Example: "apple" and "Apple" are separate when 'lower', but identical for 'initial' and 'upper'. + if(currentCasing && currentCasing != 'lower') { + this.applySuggestionCasing(prediction.sample, baseWord, currentCasing); + // update the mapping string, too. + displayText = prediction.sample.displayAs; + } + let existingSuggestion = suggestionDistribMap[displayText]; if(existingSuggestion) { existingSuggestion.p += prediction.p; @@ -306,21 +333,28 @@ class ModelCompositor { // Apply 'after word' punctuation and casing (when applicable). Also, set suggestion IDs. // We delay until now so that utility functions relying on the unmodified Transform may execute properly. - let currentCasing: CasingForm = null; - if(lexicalModel.languageUsesCasing) { - currentCasing = this.detectCurrentCasing(postContext); - } let compositor = this; - let baseWord = this.wordbreak(context); suggestions.forEach(function(suggestion) { - if(currentCasing && currentCasing != 'lower') { - compositor.applySuggestionCasing(suggestion, baseWord, currentCasing); - } - // Valid 'keep' suggestions may have zero length; we still need to evaluate the following code // for such cases. - suggestion.transform.insert += punctuation.insertAfterWord; + + // Do we need to manipulate the suggestion's transform based on the current state of the context? + if(!context.right) { + // Only insert wordbreak characters if we're at the end of the context. + suggestion.transform.insert += punctuation.insertAfterWord; + } else { + // If we're mid-word, delete its original post-caret text. + const tokenization = compositor.tokenize(context); + if(tokenization && tokenization.caretSplitsToken) { + // While we wait on the ability to provide a more 'ideal' solution, let's at least + // go with a more stable, if slightly less ideal, solution for now. + // + // A predictive text default (on iOS, at least) - immediately wordbreak + // on suggestions accepted mid-word. + suggestion.transform.insert += punctuation.insertAfterWord; + } + } // If this is a suggestion after wordbreak input, make sure we preserve the wordbreak transform! if(prefixTransform) { @@ -430,7 +464,6 @@ class ModelCompositor { // Step 1: generate and save the reversion's Transform. let sourceTransform = suggestion.transform; let deletedLeftChars = context.left.kmwSubstr(-sourceTransform.deleteLeft, sourceTransform.deleteLeft); - // right deletion is currently not implemented. let insertedLength = sourceTransform.insert.kmwLength(); let reversionTransform: Transform = { @@ -450,7 +483,15 @@ class ModelCompositor { postContext = models.applyTransform(postTransform, postContext); } - let revertedPrefix = this.wordbreak(postContext); + let revertedPrefix: string; + let postContextTokenization = this.tokenize(postContext); + if(postContextTokenization) { + // Handles display string for reversions triggered by accepting a suggestion mid-token. + revertedPrefix = postContextTokenization.left[postContextTokenization.left.length-1]; + revertedPrefix += postContextTokenization.caretSplitsToken ? postContextTokenization.right[0] : ''; + } else { + revertedPrefix = this.wordbreak(postContext); + } let firstConversion = models.transformToSuggestion(reversionTransform); firstConversion.displayAs = revertedPrefix; @@ -460,7 +501,7 @@ class ModelCompositor { // set the Reversion's ID directly. let reversion = this.toAnnotatedSuggestion(firstConversion, 'revert'); if(suggestion.transformId != null) { - reversion.transformId = suggestion.transformId; + reversion.transformId = -suggestion.transformId; } if(suggestion.id != null) { // Since a reversion inverts its source suggestion, we set its ID to be the @@ -493,7 +534,13 @@ class ModelCompositor { let compositor = this; let fallbackSuggestions = function() { let revertedContext = models.applyTransform(reversion.transform, context); - return compositor.predict({ insert: '', deleteLeft: 0}, revertedContext); + let suggestions = compositor.predict({insert: '', deleteLeft: 0}, revertedContext); + suggestions.forEach(function(suggestion) { + // A reversion's transform ID is the additive inverse of its original suggestion; + // we revert to the state of said original suggestion. + suggestion.transformId = -reversion.transformId; + }); + return suggestions; } if(!this.contextTracker) { @@ -525,9 +572,16 @@ class ModelCompositor { // Will need to be modified a bit if/when phrase-level suggestions are implemented. // Those will be tracked on the first token of the phrase, which won't be the tail // if they cover multiple tokens. - return this.contextTracker.newest.tail.replacements.map(function(trackedSuggestion) { + let suggestions = this.contextTracker.newest.tail.replacements.map(function(trackedSuggestion) { return trackedSuggestion.suggestion; }); + + suggestions.forEach(function(suggestion) { + // A reversion's transform ID is the additive inverse of its original suggestion; + // we revert to the state of said original suggestion. + suggestion.transformId = -reversion.transformId; + }); + return suggestions; } private wordbreak(context: Context): string { @@ -549,6 +603,16 @@ class ModelCompositor { } } + private tokenize(context: Context): models.Tokenization { + let model = this.lexicalModel; + + if(model.wordbreaker) { + return models.tokenize(model.wordbreaker, context); + } else { + return null; + } + } + public resetContext(context: Context) { // Force-resets the context, throwing out any previous fat-finger data, etc. // Designed for use when the caret has been directly moved and/or the context sourced from a different control diff --git a/crowdin.yml b/crowdin.yml index 722e840bad..379ddeec07 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -68,7 +68,7 @@ files: - source: /ios/engine/KMEI/KeymanEngine/Classes/en.lproj/ResourceInfoView.strings dest: /ios/engine/ResourceInfoView.strings - translation: /ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/%osx_code%/%original_file_name% + translation: /ios/engine/KMEI/KeymanEngine/Classes/%osx_code%/%original_file_name% - source: /ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings dest: /ios/engine/Localizable.strings diff --git a/developer/js/build.sh b/developer/js/build.sh index b1547cf52c..3f67c493dc 100755 --- a/developer/js/build.sh +++ b/developer/js/build.sh @@ -27,11 +27,12 @@ display_usage ( ) { echo "Usage: $0 [-test] [-publish-to-npm]" echo " $0 -help" echo - echo " -help displays this screen and exits" - echo " -test runs unit tests after building" - echo " -tdd runs unit tests WITHOUT building" - echo " -publish-to-npm publishes the current version to the npm package index" - echo " -dry-run do build, etc, but don't actually publish" + echo " -help displays this screen and exits" + echo " -test runs unit tests after building" + echo " -tdd runs unit tests WITHOUT building" + echo " -skip-package-install, -S skip package installation" + echo " -publish-to-npm publishes the current version to the npm package index" + echo " -dry-run do build, etc, but don't actually publish" } ################################ Main script ################################ diff --git a/developer/js/source/lexical-model-compiler/build-trie.ts b/developer/js/source/lexical-model-compiler/build-trie.ts index a64abab7a4..7703327bb1 100644 --- a/developer/js/source/lexical-model-compiler/build-trie.ts +++ b/developer/js/source/lexical-model-compiler/build-trie.ts @@ -138,7 +138,7 @@ function _parseWordList(wordlist: WordList, source: WordListSource): void { } wordsSeenInThisFile.add(wordform); - wordlist[wordform] = (wordlist[wordform] || 0) + count; + wordlist[wordform] = (isNaN(wordlist[wordform]) ? 0 : wordlist[wordform] || 0) + count; } } @@ -459,15 +459,22 @@ namespace Trie { * @param node The node to start summing weights. */ function sumWeights(node: Node): number { + let val: number; if (node.type === 'leaf') { - return node.entries + val = node.entries .map(entry => entry.weight) + //.map(entry => isNaN(entry.weight) ? 1 : entry.weight) .reduce((acc, count) => acc + count, 0); } else { - return Object.keys(node.children) + val = Object.keys(node.children) .map((key) => sumWeights(node.children[key])) .reduce((acc, count) => acc + count, 0); } + + if(isNaN(val)) { + console.error("Unexpected NaN has appeared!"); + } + return val; } } diff --git a/developer/js/source/package-compiler/kmp-compiler.ts b/developer/js/source/package-compiler/kmp-compiler.ts index 8538bf8c19..5a16738397 100644 --- a/developer/js/source/package-compiler/kmp-compiler.ts +++ b/developer/js/source/package-compiler/kmp-compiler.ts @@ -32,8 +32,9 @@ export default class KmpCompiler { // 1. Unwrap arrays (and convert to array where single object) // 2. Fix casing on `iD` // 3. Rewrap info, keyboard.languages, lexicalModel.languages, startMenu.items elements - // 4. Convert options.followKeyboardVersion to a bool - // 5. Filenames need to be basenames (but this comes after processing) + // 4. Remove options.followKeyboardVersion, file.fileType + // 5. Convert file.copyLocation to a Number + // 6. Filenames need to be basenames (but this comes after processing) // // Helper functions @@ -66,9 +67,7 @@ export default class KmpCompiler { let kmp: KmpJsonFile = { system: kps.system, - options: { - followKeyboardVersion: kps.options.followKeyboardVersion === '' - } + options: {} }; // Fill in additional fields @@ -76,10 +75,7 @@ export default class KmpCompiler { let keys: (keyof KpsFileOptions & keyof KmpJsonFileOptions)[] = ['executeProgram', 'graphicFile', 'msiFilename', 'msiOptions', 'readmeFile']; for (let element of keys) { if (kps.options[element]) { - // TypeScript thinks it's possible to assign to followKeyboardVersion (a - // boolean), but in reality, all of the other keys are strings. - // Politely inform TypeScript that it's okay to do this assignment: - ( kmp.options[element]) = kps.options[element]; + kmp.options[element] = kps.options[element]; } } @@ -88,7 +84,14 @@ export default class KmpCompiler { } if(kps.files && kps.files.file) { - kmp.files = arrayWrap(kps.files.file); + kmp.files = arrayWrap(kps.files.file).map((file: KpsFileContentFile) => { + return { + name: file.name, + description: file.description, + copyLocation: parseInt(file.copyLocation, 10) + // note: we don't emit fileType as that is not permitted in kmp.json + } + }); } if(kps.keyboards && kps.keyboards.keyboard) { diff --git a/developer/js/source/package-compiler/kmp-json-file.ts b/developer/js/source/package-compiler/kmp-json-file.ts index da47567dfb..3eadb3dd1d 100644 --- a/developer/js/source/package-compiler/kmp-json-file.ts +++ b/developer/js/source/package-compiler/kmp-json-file.ts @@ -15,7 +15,6 @@ interface KmpJsonFileSystem { } interface KmpJsonFileOptions { - followKeyboardVersion: boolean; readmeFile?: string; graphicFile?: string; executeProgram?: string; diff --git a/developer/js/tests/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.json b/developer/js/tests/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.json index 903b1d9023..b7215a3610 100644 --- a/developer/js/tests/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.json +++ b/developer/js/tests/fixtures/example.qaa.sencoten/example.qaa.sencoten.model.kmp.json @@ -3,9 +3,7 @@ "keymanDeveloperVersion": "12.0.1500.0", "fileVersion": "12.0" }, - "options": { - "followKeyboardVersion": true - }, + "options": {}, "info": { "author": { "description": "Eddie Antonio Santos", @@ -25,8 +23,7 @@ { "name": "..\\build\\example.qaa.sencoten.model.js", "description": "Lexical model example.qaa.sencoten.model.js", - "copyLocation": "0", - "fileType": ".model.js" + "copyLocation": 0 } ], "lexicalModels": [ diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 61f829145b..59feb6af20 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -510,7 +510,7 @@ CE9E95CC24CE786900F6DD78 /* UniversalLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalLinks.swift; sourceTree = ""; }; CE9E95CE24CE7A2C00F6DD78 /* UniversalLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalLinkTests.swift; sourceTree = ""; }; CE9ECB1D24D3A8CC007C5718 /* PackageInstallView_iPad.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PackageInstallView_iPad.xib; sourceTree = ""; }; - CEA1486E2407808F00C6ECD2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + CEA1486E2407808F00C6ECD2 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CEA14871240780E100C6ECD2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ResourceInfoView.xib; sourceTree = ""; }; CEA14874240780EF00C6ECD2 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/ResourceInfoView.strings; sourceTree = ""; }; CEA148772407869200C6ECD2 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/ResourceInfoView.strings; sourceTree = ""; }; @@ -520,6 +520,14 @@ CEA9670A24BEC8030035AACF /* EngineStateBundler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EngineStateBundler.swift; sourceTree = ""; }; CEA9670C24BEEFF80035AACF /* khmer_angkor update-base.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "khmer_angkor update-base.bundle"; sourceTree = ""; }; CEA9670E24BEF05A0035AACF /* Updates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updates.swift; sourceTree = ""; }; + CEACC90125F07C81006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/ResourceInfoView.strings; sourceTree = ""; }; + CEACC90325F07CAF006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + CEACC90425F07CB6006EAB45 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/Localizable.strings; sourceTree = ""; }; + CEACC90525F07CBA006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = ""; }; + CEACC90625F07CBC006EAB45 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = km; path = km.lproj/Localizable.stringsdict; sourceTree = ""; }; + CEACC90725F07CDA006EAB45 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/ResourceInfoView.strings; sourceTree = ""; }; + CEACC90825F07CE9006EAB45 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + CEACC90925F07CED006EAB45 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = ""; }; CEB8276624C6811800F3D39C /* KeymanHostTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymanHostTests.swift; sourceTree = ""; }; CEC0C66B2410AC9A003E1BCD /* Sentry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sentry.framework; path = ../../Carthage/Build/iOS/Sentry.framework; sourceTree = ""; }; CEC0C6762410E049003E1BCD /* SentryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryManager.swift; sourceTree = ""; }; @@ -1293,6 +1301,8 @@ en, Base, km, + de, + fr, ); mainGroup = F243887314BBD43000A3E055; productRefGroup = F243887F14BBD43000A3E055 /* Products */; @@ -1672,6 +1682,9 @@ isa = PBXVariantGroup; children = ( CE96E42C24D1229A005B8E5A /* en */, + CEACC90525F07CBA006EAB45 /* de */, + CEACC90625F07CBC006EAB45 /* km */, + CEACC90925F07CED006EAB45 /* fr */, ); name = Localizable.stringsdict; sourceTree = ""; @@ -1680,6 +1693,9 @@ isa = PBXVariantGroup; children = ( CEA1486E2407808F00C6ECD2 /* en */, + CEACC90325F07CAF006EAB45 /* de */, + CEACC90425F07CB6006EAB45 /* km */, + CEACC90825F07CE9006EAB45 /* fr */, ); name = Localizable.strings; sourceTree = ""; @@ -1690,6 +1706,8 @@ CEA14871240780E100C6ECD2 /* Base */, CEA14874240780EF00C6ECD2 /* en */, CEA148772407869200C6ECD2 /* km */, + CEACC90125F07C81006EAB45 /* de */, + CEACC90725F07CDA006EAB45 /* fr */, ); name = ResourceInfoView.xib; sourceTree = ""; diff --git a/ios/engine/KMEI/KeymanEngine/Classes/HTTPDownloader.swift b/ios/engine/KMEI/KeymanEngine/Classes/HTTPDownloader.swift index 7a2f0053c0..df2cc7fd02 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/HTTPDownloader.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/HTTPDownloader.swift @@ -19,7 +19,8 @@ protocol HTTPDownloadDelegate: class { class HTTPDownloader: NSObject { var queue: [HTTPDownloadRequest] = [] // TODO: Make unowned - weak var handler: HTTPDownloadDelegate? + /*weak*/ var handler: HTTPDownloadDelegate? // 'weak' interferes with installs + // from the app's browser. var currentRequest: HTTPDownloadRequest? var downloadSession: URLSession! public var userInfo: [String: Any] = [:] diff --git a/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift index 87d84bb7d1..35cabcb4b3 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/InputViewController.swift @@ -386,7 +386,7 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate { if(self.swallowBackspaceTextChange) { // A single keyboard processing command should never trigger two of these in a row; // only one output function will perform deletions. - + // This should allow us to debug any failures of this assumption. // So far, only occurs when debugging a breakpoint during a touch event on BKSP, // so all seems good. diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanWebViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanWebViewController.swift index a4f4ae626a..e20c4f7112 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeymanWebViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanWebViewController.swift @@ -366,7 +366,9 @@ extension KeymanWebViewController { * thereafter for settings updates. */ func setSentryState(enabled: Bool = SentryManager.enabled) { - webView?.evaluateJavaScript("sentryManager.enabled = \(enabled ? "true" : "false")") + // This may be called before the page - and thus the `sentryManager` variable - + // is ready. It's a known limitation, so why log error reports for it? + webView?.evaluateJavaScript("try { sentryManager.enabled = \(enabled ? "true" : "false") } catch(err) { }") } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/km.lproj/ResourceInfoView.strings b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/km.lproj/ResourceInfoView.strings deleted file mode 100644 index 10e17b7cc9..0000000000 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/km.lproj/ResourceInfoView.strings +++ /dev/null @@ -1,2 +0,0 @@ -/* Class = "UILabel"; text = "Scan this code to load this keyboard on another device"; ObjectID = "z2O-MT-IoV"; */ -"z2O-MT-IoV.text" = "ស្កេនកូដ​នេះដើម្បី​ផ្ទុក​ក្ដារចុចនេះ​នៅ​លើ​ឧបករណ៍​ផ្សេង"; diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Log.swift b/ios/engine/KMEI/KeymanEngine/Classes/Log.swift index 92cf9e28f2..7f8e47e413 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Log.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Log.swift @@ -8,4 +8,27 @@ import XCGLogger -public let log = XCGLogger(identifier: "KeymanEngine", includeDefaultDestinations: true) +// From XCGLogger docs: +// Note: This creates the log object lazily, which means it's not created until it's actually needed. +public let log: XCGLogger = { + // Default: the 'console', which is read by Xcode but doesn't reach the system logs. + let mainLog = XCGLogger(identifier: "KeymanEngine", includeDefaultDestinations: false) + + // Ensures our log messages go out to the device's system log as well as the console. + let systemLogDest = AppleSystemLogDestination(identifier: "") + systemLogDest.showLogIdentifier = true + + mainLog.add(destination: systemLogDest) + + // Temporary logging level to ensure that app details are reported properly. + mainLog.outputLevel = .info + mainLog.logAppDetails() + +#if DEBUG + mainLog.outputLevel = .debug +#else + mainLog.outputLevel = .warning +#endif + + return mainLog +}() diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/AssociatingPackageInstaller.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/AssociatingPackageInstaller.swift index f83b084324..2ac9436d30 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/AssociatingPackageInstaller.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/AssociatingPackageInstaller.swift @@ -121,7 +121,9 @@ public class AssociatingPackageInstaller Void)) { + public func promptForLanguages(inNavigationVC navVC: UINavigationController) { guard self.associationQueriers == nil, !closureShared.pickingCompleted else { fatalError("Invalid state - language picking has already been triggered.") } @@ -397,12 +405,21 @@ public class AssociatingPackageInstaller(for: self.package, defaultLanguageCode: defaultLgCode, languageAssociators: associationQueriers!, pickingCompletionHandler: coreInstallationClosure(), - uiCompletionHandler: uiCompletionHandler) + uiCompletionHandler: wrappedCompletionHandler) + closureShared.promptProgressCallback = { progress in + pickerPrompt.progressUpdate(progress) + } navVC.pushViewController(pickerPrompt, animated: true) } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift index a46a61e59d..1f9e5119e5 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/KeyboardSearchViewController.swift @@ -69,6 +69,9 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat private let languageCode: String? private let session: URLSession + private var progressView: UIProgressView? + private var observation: NSKeyValueObservation? = nil + private static var ENDPOINT_ROOT: URL { var baseURL = KeymanHosts.KEYMAN_COM baseURL.appendPathComponent("go") @@ -93,6 +96,10 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat fatalError("init(coder:) has not been implemented") } + deinit { + observation = nil + } + public override func loadView() { let webView = WKWebView() webView.navigationDelegate = self @@ -104,6 +111,18 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat webView.load(URLRequest(url: KeyboardSearchViewController.ENDPOINT_ROOT)) } + progressView = UIProgressView(progressViewStyle: .bar) + progressView!.translatesAutoresizingMaskIntoConstraints = false + observation = webView.observe(\.estimatedProgress) { _, _ in + if let progressView = self.progressView { + progressView.setProgress(Float(webView.estimatedProgress), animated: true) + progressView.isHidden = progressView.progress > 0.99 + } + } + progressView!.setProgress(1, animated: false) + progressView!.isHidden = true + + webView.addSubview(progressView!) view = webView } @@ -131,6 +150,25 @@ public class KeyboardSearchViewController: UIViewController, WKNavigationDelegat } decisionHandler(.allow) + // Makes it clear that there IS a progress bar, in case of super-slow response. + progressView?.setProgress(0, animated: false) + // This way, if the load is instant, the 0.01 doesn't really stand out. + progressView?.setProgress(0.01, animated: true) + progressView?.isHidden = false + } + + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if let navVC = self.navigationController { + progressView?.topAnchor.constraint(equalTo: navVC.navigationBar.bottomAnchor).isActive = true + } else { + progressView?.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true + } + + progressView?.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true + progressView?.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true } override public func viewWillDisappear(_ animated: Bool) { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift index 26cefffefa..748edfacab 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/PackageInstallViewController.swift @@ -59,6 +59,8 @@ public class PackageInstallViewController: UIViewCon private var navMapping: [NavigationMode : UIBarButtonItem] = [:] private var dismissalBlock: (() -> Void)? = nil + private weak var welcomeView: UIView? + private var mayPick: Bool = true public init(for package: Resource.Package, defaultLanguageCode: String? = nil, @@ -267,9 +269,11 @@ public class PackageInstallViewController: UIViewCon } set(mode) { - leftNavMode = mode + if mayPick { + leftNavMode = mode - navigationItem.leftBarButtonItem = navMapping[mode] + navigationItem.leftBarButtonItem = navMapping[mode] + } } } @@ -279,9 +283,11 @@ public class PackageInstallViewController: UIViewCon } set(mode) { - rightNavMode = mode + if mayPick { + rightNavMode = mode - navigationItem.rightBarButtonItem = navMapping[mode] + navigationItem.rightBarButtonItem = navMapping[mode] + } } } @@ -349,20 +355,29 @@ public class PackageInstallViewController: UIViewCon Manager.shared.shouldReloadKeyboard = true self.pickingCompletionHandler(selectedResources.map { $0.typedFullID }) - let dismissalBlock = { - if let nvc = self.navigationController { - self.dismiss(animated: true) - nvc.popToRootViewController(animated: false) - } else { // Otherwise, if the root view of a navigation controller, dismiss it outright. (pop not available) - self.dismiss(animated: true) - } + // Prevent swipe dismissal. + if #available(iOSApplicationExtension 13.0, *) { + self.isModalInPresentation = true + } + // No more selection-manipulation allowed. + // This matters when there's no welcome page available. + languageTable.isUserInteractionEnabled = false + // Prevent extra 'install' commands and nav-bar related manipulation. + self.navigationItem.leftBarButtonItem?.isEnabled = false + self.rightNavigationMode = .none + self.mayPick = false + + let dismissalBlock = { self.associators.forEach { $0.pickerFinalized() } } // First, show the package's welcome - if it exists. if let welcomeVC = PackageWebViewController(for: package, page: .welcome) { - self.dismissalBlock = dismissalBlock + // Prevent swipe dismissal. + if #available(iOSApplicationExtension 13.0, *) { + welcomeVC.isModalInPresentation = true + } let subNavVC = UINavigationController(rootViewController: welcomeVC) _ = subNavVC.view @@ -383,8 +398,16 @@ public class PackageInstallViewController: UIViewCon subNavVC.presentationController?.delegate = self self.present(subNavVC, animated: true, completion: nil) + self.welcomeView = welcomeVC.view + + self.dismissalBlock = { + // Tells the user that we've received the 'done' command. + doneItem.isEnabled = false + dismissalBlock() + } } else { - dismissalBlock() + self.dismissalBlock = dismissalBlock + onWelcomeDismissed() } } @@ -393,10 +416,33 @@ public class PackageInstallViewController: UIViewCon } @objc private func onWelcomeDismissed() { - self.dismissalBlock?() - self.dismissalBlock = nil + if let dismissalBlock = self.dismissalBlock { + dismissalBlock() + self.dismissalBlock = nil - self.uiCompletionHandler() + // Tell our owner (the AssociatingPackageInstaller) that all UI interactions are done. + // Triggers synchronization code, so make sure it only runs once! + self.uiCompletionHandler() + } + + // Show a spinner forever. + // When installation is complete, this controller's view will be dismissed, + // removing said spinner. + let activitySpinner = Alerts.constructActivitySpinner() + + // Determine the top-most view. If we're presenting the welcome page, THAT. + // If we aren't, our directly-owned view should be the top-most. + let activeView: UIView = self.welcomeView ?? view + + activitySpinner.center = activeView.center + activitySpinner.startAnimating() + activeView.addSubview(activitySpinner) + + activitySpinner.centerXAnchor.constraint(equalTo: activeView.centerXAnchor).isActive = true + activitySpinner.centerYAnchor.constraint(equalTo: activeView.centerYAnchor).isActive = true + + // Note: we do NOT block user interaction; we merely bind them to the active view. + // Why prevent them from reading more on the welcome page while they wait? } public func tableView(_ tableView: UITableView, titleForHeaderInSection: Int) -> String? { @@ -515,4 +561,20 @@ public class PackageInstallViewController: UIViewCon associators.forEach { $0.deselectLanguages( Set([languages[indexPath.row].id]) ) } } + + internal func progressUpdate>(_ status: AssociatingPackageInstaller.Progress) where Resource.Package == Package { + switch(status) { + case .starting, .inProgress: + // nothing worth note + break + // All UI interactions have been completed AND installation is fully complete. + case .complete, .cancelled: + if let nvc = self.navigationController { + self.dismiss(animated: true) + nvc.popToRootViewController(animated: false) + } else { // Otherwise, if the root view of a navigation controller, dismiss it outright. (pop not available) + self.dismiss(animated: true) + } + } + } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift index f7a43a2dd8..553c69d84e 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceDownloadManager.swift @@ -682,4 +682,66 @@ public class ResourceDownloadManager { value: PackageDownloadFailedNotification(packageKey: packageKey, error: error)) } } + + /** + * Designed for downloading KMP files when no metadata is available in advance. + */ + public func downloadRawKMP(from url: URL, handler: @escaping (URL?, Error?) -> Void) { + + // First, we need something to handle the download. + class NoMetadataDelegate: HTTPDownloadDelegate { + private let closure: (URL?, Error?) -> Void + + init(withHandler handler: @escaping (URL?, Error?) -> Void) { + self.closure = handler; + } + + func downloadRequestStarted(_ request: HTTPDownloadRequest) { + // Not relevant + } + + func downloadRequestFinished(_ request: HTTPDownloadRequest) { + if request.responseStatusCode != 200 { + // Possible request error (400 Bad Request, 404 Not Found, etc.) + let error = DownloadError.failed(.responseCode(request.responseStatusCode ?? 400, + request.responseStatusMessage ?? "", + request.url)) + + // Now that we've synthesized an appropriate error instance, use the same handler + // as for HTTPDownloader's 'failed' condition. + downloadRequestFailed(request, with: error) + } else { + self.closure(URL(fileURLWithPath: request.destinationFile!), nil) + } + } + + func downloadRequestFailed(_ request: HTTPDownloadRequest, with error: Error?) { + self.closure(nil, error) + } + + func downloadQueueFinished(_ queue: HTTPDownloader) { + // Not relevant + } + + func downloadQueueCancelled(_ queue: HTTPDownloader) { + // Not relevant, but to be safe... + self.closure(nil, nil) + } + } + + let delegate = NoMetadataDelegate(withHandler: handler) + let downloader = HTTPDownloader(delegate, session: self.session) + let request = HTTPDownloadRequest(url: url, downloadType: .downloadFile) + + // Since we don't know the package key in advance, we'll download it to + // the app's cache directory, then figure everything out once we open it. + let cachesDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let tempFilename = url.lastPathComponent + let cachedDest = cachesDir.appendingPathComponent(tempFilename) + + request.destinationFile = cachedDest.path + + downloader.addRequest(request) + downloader.run() + } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift index 2039f78480..3f1cc56e29 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -228,27 +228,23 @@ public class ResourceFileManager { activitySpinner.centerXAnchor.constraint(equalTo: rootVC.view.centerXAnchor).isActive = true activitySpinner.centerYAnchor.constraint(equalTo: rootVC.view.centerYAnchor).isActive = true rootVC.view.isUserInteractionEnabled = false - } else if status == .complete { + } else if status == .complete || status == .cancelled { // Report completion! activitySpinner.stopAnimating() activitySpinner.removeFromSuperview() rootVC.view.isUserInteractionEnabled = true - rootVC.dismiss(animated: true, completion: nil) + rootVC.dismiss(animated: true) { + Manager.shared.showKeyboard() + } successHandler?(package) } } if let navVC = rootVC as? UINavigationController { - packageInstaller.promptForLanguages(inNavigationVC: navVC) { - // The user will be on the main screen after this, so we should resummon the keyboard. - Manager.shared.showKeyboard() - } + packageInstaller.promptForLanguages(inNavigationVC: navVC) } else { let nvc = UINavigationController.init() - packageInstaller.promptForLanguages(inNavigationVC: nvc) { - // The user will be on the main screen after this, so we should resummon the keyboard. - Manager.shared.showKeyboard() - } + packageInstaller.promptForLanguages(inNavigationVC: nvc) rootVC.present(nvc, animated: true, completion: nil) } } @@ -265,13 +261,14 @@ public class ResourceFileManager { } public func buildKMPError(_ error: KMPError) -> UIAlertController { - return buildSimpleAlert(title: "Error", message: error.localizedDescription) + return buildSimpleAlert(title: NSLocalizedString("alert-error-title", bundle: engineBundle, comment: ""), + message: error.localizedDescription) } public func buildSimpleAlert(title: String, message: String, completionHandler: (() -> Void)? = nil ) -> UIAlertController { let alertController = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert) - alertController.addAction(UIAlertAction(title: "OK", + alertController.addAction(UIAlertAction(title: NSLocalizedString("command-ok", bundle: engineBundle, comment: ""), style: UIAlertAction.Style.default, handler: { _ in completionHandler?() diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift index 90aac970f0..9d4a270120 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Settings/LanguageSettingsViewController.swift @@ -253,7 +253,7 @@ class LanguageSettingsViewController: UITableViewController { } default: - cell.textLabel?.text = "error" + cell.textLabel?.text = NSLocalizedString("alert-error-title", bundle: engineBundle, comment: "") } } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/de.lproj/ResourceInfoView.strings b/ios/engine/KMEI/KeymanEngine/Classes/de.lproj/ResourceInfoView.strings similarity index 100% rename from ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/de.lproj/ResourceInfoView.strings rename to ios/engine/KMEI/KeymanEngine/Classes/de.lproj/ResourceInfoView.strings diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/fr.lproj/ResourceInfoView.strings b/ios/engine/KMEI/KeymanEngine/Classes/fr.lproj/ResourceInfoView.strings similarity index 100% rename from ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/fr.lproj/ResourceInfoView.strings rename to ios/engine/KMEI/KeymanEngine/Classes/fr.lproj/ResourceInfoView.strings diff --git a/ios/engine/KMEI/KeymanEngine/Classes/km.lproj/ResourceInfoView.strings b/ios/engine/KMEI/KeymanEngine/Classes/km.lproj/ResourceInfoView.strings index aa12fd1fb3..10e17b7cc9 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/km.lproj/ResourceInfoView.strings +++ b/ios/engine/KMEI/KeymanEngine/Classes/km.lproj/ResourceInfoView.strings @@ -1,3 +1,2 @@ - /* Class = "UILabel"; text = "Scan this code to load this keyboard on another device"; ObjectID = "z2O-MT-IoV"; */ -"z2O-MT-IoV.text" = "Scan this code to load this keyboard on another device"; +"z2O-MT-IoV.text" = "ស្កេនកូដ​នេះដើម្បី​ផ្ទុក​ក្ដារចុចនេះ​នៅ​លើ​ឧបករណ៍​ផ្សេង"; diff --git a/ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings index b13b4ec2dc..408de2f755 100644 Binary files a/ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings and b/ios/engine/KMEI/KeymanEngine/en.lproj/Localizable.strings differ diff --git a/ios/engine/KMEI/KeymanEngineDemo/AppDelegate.swift b/ios/engine/KMEI/KeymanEngineDemo/AppDelegate.swift index 1e643d76f0..9eb651ca01 100644 --- a/ios/engine/KMEI/KeymanEngineDemo/AppDelegate.swift +++ b/ios/engine/KMEI/KeymanEngineDemo/AppDelegate.swift @@ -15,8 +15,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? = nil) -> Bool { - KeymanEngine.log.outputLevel = .debug - KeymanEngine.log.logAppDetails() Manager.applicationGroupIdentifier = "group.KMEI" Manager.shared.canRemoveDefaultKeyboard = true diff --git a/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift b/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift index e72ffd6294..1b84fb6259 100644 --- a/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift +++ b/ios/engine/KMEI/KeymanEngineDemo/MainViewController.swift @@ -227,10 +227,11 @@ class MainViewController: UIViewController, UIAlertViewDelegate, TextViewDelegat } @objc func showAlert(_ message: String) { - let alertController = UIAlertController(title: "Keyboard Download Error", + let engineBundle = Bundle(for: Manager.self) + let alertController = UIAlertController(title: NSLocalizedString("alert-download-error-title", bundle: engineBundle, comment: ""), message: message, preferredStyle: UIAlertControllerStyle.alert) - alertController.addAction(UIAlertAction(title: "OK", + alertController.addAction(UIAlertAction(title: NSLocalizedString("command-ok", bundle: engineBundle, comment: ""), style: UIAlertActionStyle.default, handler: nil)) self.present(alertController, animated: true, completion: nil) diff --git a/ios/engine/KMEI/SystemKeyboard/KeyboardViewController.swift b/ios/engine/KMEI/SystemKeyboard/KeyboardViewController.swift index 9a54cdcc8e..8cf3c85e5a 100644 --- a/ios/engine/KMEI/SystemKeyboard/KeyboardViewController.swift +++ b/ios/engine/KMEI/SystemKeyboard/KeyboardViewController.swift @@ -10,8 +10,7 @@ import KeymanEngine class KeyboardViewController: InputViewController { override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - KeymanEngine.log.outputLevel = .debug - KeymanEngine.log.logAppDetails() + _ = log // forces init of the log, which is useful in sys-kbd contexts. Manager.applicationGroupIdentifier = "group.KMEI" super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) } diff --git a/ios/help/basic/index.md b/ios/help/basic/index.md index 0a5cbcc11d..491e7d952c 100644 --- a/ios/help/basic/index.md +++ b/ios/help/basic/index.md @@ -4,7 +4,7 @@ title: Basic Help * [Switching Between Keyboards](switching-between-keyboards) -* [Installing Custom Keyboards](installing-custom-keyboards) +* [Installing Custom Keyboards/Dictionaries](installing-custom-keyboards-dictionaries) * [Sharing Keyboards](sharing-keyboards) * [Using the Keyman Browser](using-keyman-browser) diff --git a/ios/help/basic/installing-custom-keyboards.md b/ios/help/basic/installing-custom-keyboards-dictionaries.md similarity index 90% rename from ios/help/basic/installing-custom-keyboards.md rename to ios/help/basic/installing-custom-keyboards-dictionaries.md index e56a240cb6..7bd9d66cad 100644 --- a/ios/help/basic/installing-custom-keyboards.md +++ b/ios/help/basic/installing-custom-keyboards-dictionaries.md @@ -1,10 +1,10 @@ --- -title: Installing Custom Keyboards - Keyman for iPhone and iPad Help +title: Installing Custom Keyboards/Dictionaries - Keyman for iPhone and iPad Help --- -The following steps can also be used to install a dictionary package. +The following steps can be used to install either a keyboard package or a dictionary package. -###Download the File +### Download the File If downloading a custom keyboard from the internet, click the link to your custom keyboard package file. For this example, we'll install a custom keyboard from a link in Safari. Our example keyboard is for the GFF Amharic 7 keyboard. diff --git a/ios/help/basic/sharing-keyboards.md b/ios/help/basic/sharing-keyboards.md index 8cd217ab55..1fc07a9068 100644 --- a/ios/help/basic/sharing-keyboards.md +++ b/ios/help/basic/sharing-keyboards.md @@ -45,4 +45,4 @@ Selecting the notification seen at the top will lead you to the following page: Select the big **Install on iPhone** (or **Install on iPad**) option will then download -the file for easy installation. (Refer to [Installing custom keyboards](installing-custom-keyboards) as necessary.) +the file for easy installation. (Refer to [Installing custom keyboards/dictionaries](installing-custom-keyboards-dictionaries) as necessary.) diff --git a/ios/help/index.md b/ios/help/index.md index 8602639a08..eba2b1bf3b 100644 --- a/ios/help/index.md +++ b/ios/help/index.md @@ -24,7 +24,7 @@ title: Keyman for iPhone and iPad 14.0 Help ### [Using Keyman for iPhone and iPad](basic/) * [Switching Between Keyboards](basic/switching-between-keyboards) -* [Installing Custom Keyboards](basic/installing-custom-keyboards) +* [Installing Custom Keyboards/Dictionaries](basic/installing-custom-keyboards-dictionaries) * [Sharing Keyboards](basic/sharing-keyboards) * [Using the Keyman Browser](basic/using-keyman-browser) * [Removing Keyboards](basic/uninstalling-keyboards) diff --git a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj index bf008e8cb0..dca88f86d3 100644 --- a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj +++ b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj @@ -317,7 +317,7 @@ CE002CB42408B4A5002026CE /* Sentry.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sentry.framework; path = ../../Carthage/Build/iOS/Sentry.framework; sourceTree = ""; }; CE1F5ECD23331DA400141F3E /* OfflineHelp.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = OfflineHelp.bundle; sourceTree = ""; }; CE2B1E4B21B6112B007D092E /* DeviceKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DeviceKit.framework; path = ../../Carthage/Build/iOS/DeviceKit.framework; sourceTree = ""; }; - CE4C654524D0020900070856 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + CE4C654524D0020900070856 /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CE6137FF1FB99538009D0EF2 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE6138031FB999C8009D0EF2 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE79CDB42370111200010C06 /* Themes+Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Themes+Colors.xcassets"; sourceTree = ""; }; @@ -325,6 +325,9 @@ CE7C1AE1236925D800100C2C /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; CE7FF1EF239A0293007859D9 /* PackageBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageBrowserViewController.swift; sourceTree = ""; }; CE80AD32257F2B4A008D2150 /* KeymanEngine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = KeymanEngine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CEACC90E25F07D58006EAB45 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + CEACC90F25F07D5A006EAB45 /* km */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = km; path = km.lproj/Localizable.strings; sourceTree = ""; }; + CEACC91225F07D77006EAB45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ImageBanner.xib; sourceTree = ""; }; CEF4E55823E967140065B9C7 /* ImageBannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBannerViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -772,6 +775,8 @@ en, Base, km, + fr, + de, ); mainGroup = 98ABADA7176935E400B62590; productRefGroup = 98ABADB1176935E400B62590 /* Products */; @@ -1090,6 +1095,9 @@ isa = PBXVariantGroup; children = ( CE4C654524D0020900070856 /* en */, + CEACC90E25F07D58006EAB45 /* fr */, + CEACC90F25F07D5A006EAB45 /* km */, + CEACC91225F07D77006EAB45 /* de */, ); name = Localizable.strings; sourceTree = ""; diff --git a/ios/keyman/Keyman/Keyman/AppDelegate.swift b/ios/keyman/Keyman/Keyman/AppDelegate.swift index c5ce2f46d5..90d40bac2f 100644 --- a/ios/keyman/Keyman/Keyman/AppDelegate.swift +++ b/ios/keyman/Keyman/Keyman/AppDelegate.swift @@ -36,7 +36,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { vc.dismiss(animated: true, completion: nil) if let package = rfm.prepareKMPInstall(from: destinationUrl, alertHost: vc) { - // We choose to prompt the user for comfirmation, rather + // First, explicitly hide the keyboard. Otherwise, the app may try to + // redisplay it before package installation is fully complete. + Manager.shared.hideKeyboard() + + // We choose to prompt the user for confirmation, rather // than automatically installing the package. // // Since we're operating at the root, we want to present in a separate UINavigationController. @@ -54,20 +58,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { SentryManager.start() + // Forces the logs to initialize, as their definitions result in lazy init. + // These references have been configured to also log app details. + _ = log + _ = KeymanEngine.log UniversalLinks.externalLinkLauncher = { url in UIApplication.shared.openURL(url) } - #if DEBUG - KeymanEngine.log.outputLevel = .debug - log.outputLevel = .debug - KeymanEngine.log.logAppDetails() - #else - KeymanEngine.log.outputLevel = .warning - log.outputLevel = .warning - #endif - Manager.applicationGroupIdentifier = "group.KM4I" // TODO: Assign a subclassed version of InputViewController that implements the image stuff. diff --git a/ios/keyman/Keyman/Keyman/KMWebBrowser/WebBrowserViewController.swift b/ios/keyman/Keyman/Keyman/KMWebBrowser/WebBrowserViewController.swift index bc776cda60..c6f1af76d4 100644 --- a/ios/keyman/Keyman/Keyman/KMWebBrowser/WebBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/KMWebBrowser/WebBrowserViewController.swift @@ -204,6 +204,38 @@ class WebBrowserViewController: UIViewController, UIWebViewDelegate, UIAlertView func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebView.NavigationType) -> Bool { + if request.url?.lastPathComponent.hasSuffix(".kmp") ?? false { + // Can't have the browser auto-download with no way to select a different page. + // Can't just ignore it, either, as the .kmp may result from a redirect from + // the previous URL. (Like if using the keyman.com keyboard search!) + let userData = UserDefaults.standard + userData.set(nil as String?, forKey: webBrowserLastURLKey) + userData.synchronize() + + // The user is trying to download a .kmp, but the standard + // UIWebView can't handle it properly. + + ResourceDownloadManager.shared.downloadRawKMP(from: request.url!) { file, error in + // do something! + if let error = error { + let alertTitle = NSLocalizedString("alert-error-title", bundle: Bundle(for: Manager.self), comment: "") + let alert = ResourceFileManager.shared.buildSimpleAlert(title: alertTitle, + message: error.localizedDescription) + self.present(alert, animated: true, completion: nil) + return + } + + // Re-use the standard 'open random file' code as when launching the + // app from a file. This will also auto-dismiss the browser, returning + // to the app's main screen. + if let file = file { + let appDelegate = UIApplication.shared.delegate as! AppDelegate + _ = appDelegate.application(UIApplication.shared, open: file) + } + } + + return false + } updateAddress(request) return true } @@ -222,13 +254,27 @@ class WebBrowserViewController: UIViewController, UIWebViewDelegate, UIAlertView func webView(_ webView: UIWebView, didFailLoadWithError error: Error) { UIApplication.shared.isNetworkActivityIndicatorVisible = false updateButtons() - let alertController = UIAlertController(title: "Cannot Open Page", - message: error.localizedDescription, - preferredStyle: UIAlertController.Style.alert) - alertController.addAction(UIAlertAction(title: "OK", - style: UIAlertAction.Style.default, - handler: nil)) - self.present(alertController, animated: true, completion: nil) + + var signalError: Bool = true + + // An error will likely result if the user attempts to download a KMP, + // despite the fact that we tell it not to attempt a load. + let nsError = error as NSError + if let url = nsError.userInfo["NSErrorFailingURLKey"] as? NSURL { + signalError = !(url.path?.hasSuffix(".kmp") ?? false) + } + + if signalError { + let alertController = UIAlertController(title: NSLocalizedString("error-opening-page", comment: ""), + message: error.localizedDescription, + preferredStyle: UIAlertController.Style.alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("command-ok", + bundle: Bundle(for: Manager.self), + comment: ""), + style: UIAlertAction.Style.default, + handler: nil)) + self.present(alertController, animated: true, completion: nil) + } } private func updateButtons() { diff --git a/ios/keyman/Keyman/Keyman/Log.swift b/ios/keyman/Keyman/Keyman/Log.swift index 1ba2871845..c074921570 100644 --- a/ios/keyman/Keyman/Keyman/Log.swift +++ b/ios/keyman/Keyman/Keyman/Log.swift @@ -8,4 +8,27 @@ import XCGLogger -let log = XCGLogger(identifier: "Keyman", includeDefaultDestinations: true) +// From XCGLogger docs: +// Note: This creates the log object lazily, which means it's not created until it's actually needed. +public let log: XCGLogger = { + // Default: the 'console', which is read by Xcode but doesn't reach the system logs. + let mainLog = XCGLogger(identifier: "Keyman", includeDefaultDestinations: false) + + // Ensures our log messages go out to the device's system log as well as the console. + let systemLogDest = AppleSystemLogDestination(identifier: "") + systemLogDest.showLogIdentifier = true + + mainLog.add(destination: systemLogDest) + + // Temporary logging level to ensure that app details are reported properly. + mainLog.outputLevel = .info + mainLog.logAppDetails() + +#if DEBUG + mainLog.outputLevel = .debug +#else + mainLog.outputLevel = .warning +#endif + + return mainLog +}() diff --git a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift index ad27110129..7388147997 100644 --- a/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/PackageBrowserViewController.swift @@ -116,9 +116,7 @@ class PackageBrowserViewController: UIDocumentPickerViewController, UIDocumentPi } if let navVC = self.navVC { - packageInstaller.promptForLanguages(inNavigationVC: navVC) { - // do nothing; the Settings menu dismissal will take care of displaying the keyboard - } + packageInstaller.promptForLanguages(inNavigationVC: navVC) } } } diff --git a/ios/keyman/Keyman/Keyman/en.lproj/Localizable.strings b/ios/keyman/Keyman/Keyman/en.lproj/Localizable.strings index 9395b890fa..58f1783a93 100644 Binary files a/ios/keyman/Keyman/Keyman/en.lproj/Localizable.strings and b/ios/keyman/Keyman/Keyman/en.lproj/Localizable.strings differ diff --git a/ios/keyman/Keyman/SWKeyboard/KeyboardViewController.swift b/ios/keyman/Keyman/SWKeyboard/KeyboardViewController.swift index 8e716607bb..0c5a2dcdb3 100644 --- a/ios/keyman/Keyman/SWKeyboard/KeyboardViewController.swift +++ b/ios/keyman/Keyman/SWKeyboard/KeyboardViewController.swift @@ -21,13 +21,9 @@ class KeyboardViewController: InputViewController { // is enabled. They seem to get blocked otherwise, except in the Simulator. SentryManager.start(sendingEnabled: true) } + _ = log + _ = KeymanEngine.log - #if DEBUG - KeymanEngine.log.outputLevel = .debug - KeymanEngine.log.logAppDetails() - #else - KeymanEngine.log.outputLevel = .warning - #endif Manager.applicationGroupIdentifier = "group.KM4I" let bundle = Bundle(for: KeyboardViewController.self) diff --git a/linux/.gitignore b/linux/.gitignore index 0724a21185..3bfb0eb6f6 100644 --- a/linux/.gitignore +++ b/linux/.gitignore @@ -1,28 +1,35 @@ # linux +.deps/ autom4te.cache +*.o *.orig Makefile.in configure aclocal.m4 config.guess config.sub +config.h config.h.in -config.h.in~ +config.log config.rpath +config.status compile depcomp install-sh +libtool ltmain.sh missing test-driver ylwrap INSTALL +Makefile ABOUT-NLS ltoptions.m4 ltsugar.m4 ltversion.m4 lt~obsolete.m4 libtool.m4 +stamp-h1 __pycache__ *.egg-info build-* @@ -83,3 +90,10 @@ keyman-config/keyman_config/version.py keyman-config/locale/*/LC_MESSAGES/ test.sh debianpackage/ +*.deb +*.build +*.buildinfo +*.changes +*.tar.xz +*.tar.gz +*.dsc \ No newline at end of file diff --git a/linux/LICENSE.md b/linux/LICENSE.md index a4aaeb93e2..153a83e91b 100644 --- a/linux/LICENSE.md +++ b/linux/LICENSE.md @@ -1,13 +1,21 @@ # LICENSE The [kmflcomp](./kmflcomp), [libkmfl](./libkmfl), and [keyman_config](./keyman_config) projects -are covered by the [MIT license](./libkmfl/COPYING) +are covered by the [MIT license](./libkmfl/COPYING). -The [ibus-kmfl](./ibus-kmfl) project is licensed under GNU General Public License as published -by the Free Software Foundation; either [version 2 of the License](./ibus-kmfl/COPYING), or -(at your option) any later version. +The [ibus-kmfl](./ibus-kmfl) and [ibus-keyman](./ibus-keyman) projects are licensed under GNU +General Public License as published by the Free Software Foundation; either +[version 2 of the License](./ibus-kmfl/COPYING), or (at your option) any later version. Two files in ibus-kmfl, [kmflutil.c](./ibus-kmfl/src/kmflutil.c) and -[kmflutil.h](./ibus-kmfl/src/kmflutil.h) are dual licensed by the MIT license and the -GNU General Public License which is described above. The MIT license alone may be chosen for -them for their use in other projects. +[kmflutil.h](./ibus-kmfl/src/kmflutil.h), as well as four files in ibus-keyman, +[keymanutil.c](./ibus-keyman/src/keymanutil.c), [keymanutil.h](./ibus-keyman/src/keymanutil.h), +[kmpdetails.c](./ibus-keyman/src/kmpdetails.c) and [kmpdetails.h](./ibus-keyman/src/kmpdetails.h), +are dual licensed by the MIT license and the GNU General Public License which is described above. +The MIT license alone may be chosen for them for their use in other projects. + +Two files in ibus-keyman, [keyman-service.c](./ibus-keyman/src/keyman-service.c) and +[keyman-service.h](./ibus-keyman/src/keyman-service.h) are licensed under GNU +General Public License version 3 as published by the Free Software Foundation; either +[version 3 of the License](https://spdx.org/licenses/GPL-3.0-or-later.html), or (at your option) +any later version. diff --git a/linux/ibus-keyman/.gitignore b/linux/ibus-keyman/.gitignore new file mode 100644 index 0000000000..b65a78d449 --- /dev/null +++ b/linux/ibus-keyman/.gitignore @@ -0,0 +1,4 @@ +po/POTFILES +src/keyman.xml +src/keyman.xml.in +src/ibus-engine-keyman \ No newline at end of file diff --git a/linux/ibus-keyman/README b/linux/ibus-keyman/README index 01a706f30d..09f7164272 100644 --- a/linux/ibus-keyman/README +++ b/linux/ibus-keyman/README @@ -1 +1,3 @@ -Keyman engine for IBus. +Keyman engine for IBus + +This readme is required by autoconf. Otherwise see README.md. \ No newline at end of file diff --git a/linux/ibus-keyman/README.md b/linux/ibus-keyman/README.md new file mode 100644 index 0000000000..b2a0cc0b2b --- /dev/null +++ b/linux/ibus-keyman/README.md @@ -0,0 +1,22 @@ +# Keyman engine for IBus + +## Requirements + +You need autoconf, autopoint, gettext, automake and libtool to generate the build system. + +Run `./autogen.sh` to run them. + +## Building + +```bash +./autogen.sh +./configure +make +sudo make install +``` + +For a debug build: + +```bash +./configure CPPFLAGS=-DG_MESSAGES_DEBUG CFLAGS="-g -O0" CXXFLAGS="-g -O0" +``` diff --git a/linux/ibus-keyman/debian/com.keyman.ibus_keyman.metainfo.xml b/linux/ibus-keyman/debian/com.keyman.ibus_keyman.metainfo.xml index 945bb6050c..c2be6d8403 100644 --- a/linux/ibus-keyman/debian/com.keyman.ibus_keyman.metainfo.xml +++ b/linux/ibus-keyman/debian/com.keyman.ibus_keyman.metainfo.xml @@ -1,8 +1,8 @@ com.keyman.ibus_keyman -​ FSFAP - MIT +​ GPL-2+ + GPL-2+ Keyman SIL International https://keyman.com/cdn/dev/img/keyman-logo.png diff --git a/linux/ibus-keyman/debian/control b/linux/ibus-keyman/debian/control index ebbf9afc1b..1c587e8fe2 100644 --- a/linux/ibus-keyman/debian/control +++ b/linux/ibus-keyman/debian/control @@ -4,6 +4,7 @@ Priority: optional Maintainer: Debian Input Method Team Uploaders: Keyman team , + Eberhard Beilharz , Build-Depends: debhelper (>= 11), libgtk-3-dev, @@ -11,7 +12,7 @@ Build-Depends: libjson-glib-dev (>= 1.4.0), libkmnkbp-dev (>=11.0.100), pkg-config, -Standards-Version: 4.5.0 +Standards-Version: 4.5.1 Vcs-Git: https://github.com/keymanapp/keyman.git Vcs-Browser: https://github.com/keymanapp/keyman/tree/master/linux/ibus-keyman Homepage: https://www.keyman.com diff --git a/linux/ibus-keyman/debian/copyright b/linux/ibus-keyman/debian/copyright index 9339eacfea..204e504f43 100644 --- a/linux/ibus-keyman/debian/copyright +++ b/linux/ibus-keyman/debian/copyright @@ -18,10 +18,6 @@ Files: src/keyman-service.c Copyright: 2018-2021 SIL International License: GPL-3+ -Files: debian/* -Copyright: 2004-2021 SIL International -License: GPL-2+ - License: GPL-2+ This package is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/linux/ibus-keyman/debian/upstream/signing-key.asc b/linux/ibus-keyman/debian/upstream/signing-key.asc new file mode 100644 index 0000000000..a235c64d44 --- /dev/null +++ b/linux/ibus-keyman/debian/upstream/signing-key.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=jcZL +-----END PGP PUBLIC KEY BLOCK----- diff --git a/linux/ibus-kmfl/debian/com.keyman.ibus_kmfl.metainfo.xml b/linux/ibus-kmfl/debian/com.keyman.ibus_kmfl.metainfo.xml index dbae8241c4..caa80345bb 100644 --- a/linux/ibus-kmfl/debian/com.keyman.ibus_kmfl.metainfo.xml +++ b/linux/ibus-kmfl/debian/com.keyman.ibus_kmfl.metainfo.xml @@ -1,8 +1,8 @@ com.keyman.ibus_kmfl -​ FSFAP - MIT +​ GPL-2+ + GPL-2+ Keyman SIL International https://keyman.com/cdn/dev/img/keyman-logo.png diff --git a/linux/ibus-kmfl/debian/control b/linux/ibus-kmfl/debian/control index 7a126630b5..1702603fe9 100644 --- a/linux/ibus-kmfl/debian/control +++ b/linux/ibus-kmfl/debian/control @@ -4,6 +4,7 @@ Priority: optional Maintainer: Debian Input Method Team Uploaders: Keyman team , + Eberhard Beilharz , Build-Depends: debhelper (>= 11), libibus-1.0-dev (>= 1.2), @@ -12,7 +13,7 @@ Build-Depends: libx11-dev, pkg-config, x11proto-core-dev, -Standards-Version: 4.5.0 +Standards-Version: 4.5.1 Vcs-Git: https://github.com/keymanapp/keyman.git Vcs-Browser: https://github.com/keymanapp/keyman/tree/master/linux/ibus-kmfl Homepage: https://www.keyman.com diff --git a/linux/ibus-kmfl/debian/upstream/signing-key.asc b/linux/ibus-kmfl/debian/upstream/signing-key.asc new file mode 100644 index 0000000000..a235c64d44 --- /dev/null +++ b/linux/ibus-kmfl/debian/upstream/signing-key.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=jcZL +-----END PGP PUBLIC KEY BLOCK----- diff --git a/linux/keyman-config/debian/com.keyman.config.appdata.xml b/linux/keyman-config/debian/com.keyman.config.appdata.xml index b3c363167b..e275797552 100644 --- a/linux/keyman-config/debian/com.keyman.config.appdata.xml +++ b/linux/keyman-config/debian/com.keyman.config.appdata.xml @@ -2,7 +2,7 @@ com.keyman.config - FSFAP + MIT MIT Keyman SIL International diff --git a/linux/keyman-config/debian/control b/linux/keyman-config/debian/control index 2795069759..5aade1aa5e 100644 --- a/linux/keyman-config/debian/control +++ b/linux/keyman-config/debian/control @@ -4,6 +4,7 @@ Priority: optional Maintainer: Debian Input Method Team Uploaders: Keyman team , + Eberhard Beilharz , Build-Depends: bash-completion, debhelper (>= 11), @@ -23,13 +24,12 @@ Build-Depends: python3-requests, python3-requests-cache, python3-setuptools, -Standards-Version: 4.5.0 +Standards-Version: 4.5.1 Vcs-Git: https://github.com/keymanapp/keyman.git Vcs-Browser: https://github.com/keymanapp/keyman/tree/master/linux/keyman-config Homepage: https://www.keyman.com Package: keyman -Section: utils Architecture: all Depends: python3-keyman-config, diff --git a/linux/keyman-config/debian/rules b/linux/keyman-config/debian/rules index bde563844e..205d6d56f5 100755 --- a/linux/keyman-config/debian/rules +++ b/linux/keyman-config/debian/rules @@ -1,5 +1,7 @@ #!/usr/bin/make -f +include /usr/share/dpkg/pkg-info.mk + #export DH_VERBOSE=1 export PYBUILD_NAME=keyman-config export PYBUILD_INSTALL_ARGS=--install-scripts=/usr/share/keyman-config/ @@ -9,6 +11,7 @@ export PYBUILD_INSTALL_ARGS=--install-scripts=/usr/share/keyman-config/ override_dh_auto_build: make man + sed -i -e "s/^__pkgversion__ = \"\"/__pkgversion__ = \"$(DEB_VERSION)\"/g" keyman_config/version.py make compile-po dh_auto_build $@ diff --git a/linux/keyman-config/debian/upstream/signing-key.asc b/linux/keyman-config/debian/upstream/signing-key.asc new file mode 100644 index 0000000000..a235c64d44 --- /dev/null +++ b/linux/keyman-config/debian/upstream/signing-key.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=jcZL +-----END PGP PUBLIC KEY BLOCK----- diff --git a/linux/keyman-config/keyman_config/__init__.py b/linux/keyman-config/keyman_config/__init__.py index 4a1b314b87..4322d69ad2 100644 --- a/linux/keyman-config/keyman_config/__init__.py +++ b/linux/keyman-config/keyman_config/__init__.py @@ -9,6 +9,8 @@ from .version import __versionwithtag__ from .version import __majorversion__ from .version import __releaseversion__ from .version import __tier__ +from .version import __pkgversion__ +from .version import __environment__ def _(txt): @@ -36,6 +38,8 @@ KeymanDownloadsUrl = 'https://downloads.keyman.com' if 'unittest' in sys.modules.keys(): print('Not reporting to Sentry') +elif os.environ.get('KEYMAN_NOSENTRY'): + print('Not reporting to Sentry because KEYMAN_NOSENTRY environment variable set') else: try: # Try new sentry-sdk first @@ -46,13 +50,15 @@ else: SentryUrl = "https://1d0edbf2d0dc411b87119b6e92e2c357@sentry.keyman.com/12" sentry_sdk.init( dsn=SentryUrl, - environment=__tier__, + environment=__environment__, release=__version__, ) with configure_scope() as scope: scope.set_tag("app", os.path.basename(sys.argv[0])) + scope.set_tag("pkgversion", __pkgversion__) scope.set_tag("platform", platform.platform()) scope.set_tag("system", platform.system()) + scope.set_tag("tier", __tier__) except ImportError: try: # sentry-sdk is not available, so use older raven @@ -61,11 +67,13 @@ else: HaveSentryNewSdk = False SentryUrl = "https://1d0edbf2d0dc411b87119b6e92e2c357:e6d5a81ee6944fc79bd9f0cbb1f2c2a4@sentry.keyman.com/12" - client = Client(SentryUrl, environment=__tier__, release=__version__) + client = Client(SentryUrl, environment=__environment__, release=__version__) client.tags_context({ 'app': os.path.basename(sys.argv[0]), + 'pkgversion': __pkgversion__, 'platform': platform.platform(), 'system': platform.system(), + 'tier': __tier__, }) except ImportError: # even raven is not available. This is the case on Ubuntu 16.04. Just ignore. diff --git a/linux/keyman-config/keyman_config/version.py.in b/linux/keyman-config/keyman_config/version.py.in index c2970be4be..49d5e84b13 100644 --- a/linux/keyman-config/keyman_config/version.py.in +++ b/linux/keyman-config/keyman_config/version.py.in @@ -9,3 +9,5 @@ __versionwithtag__ = "_VERSIONWITHTAG_" __majorversion__ = "_MAJORVERSION_" __releaseversion__ = "_RELEASEVERSION_" __tier__ = "_TIER_" +__pkgversion__ = "" +__environment__ = "_ENVIRONMENT_" diff --git a/linux/keyman-config/km-config b/linux/keyman-config/km-config index 0f0bfeae46..172e484055 100755 --- a/linux/keyman-config/km-config +++ b/linux/keyman-config/km-config @@ -3,13 +3,17 @@ import argparse import logging -from keyman_config import __versionwithtag__ +from keyman_config import __versionwithtag__, __pkgversion__ from keyman_config.handle_install import download_and_install_package if __name__ == '__main__': + if __pkgversion__: + versionstring = "%s (package version %s)" % (__versionwithtag__, __pkgversion__) + else: + versionstring = "%s" % __versionwithtag__ parser = argparse.ArgumentParser(description='Keyman keyboards installation and information') - parser.add_argument('--version', action='version', version='%(prog)s version ' + __versionwithtag__) + parser.add_argument('--version', action='version', version='%(prog)s version ' + versionstring) parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') parser.add_argument('-i', '--install', action='store', nargs='?', help='install .kmp package') diff --git a/linux/keyman-config/locale/de_DE.po b/linux/keyman-config/locale/de_DE.po index 9763ce6ef3..5824946c84 100644 --- a/linux/keyman-config/locale/de_DE.po +++ b/linux/keyman-config/locale/de_DE.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: keyman\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-08-19 19:17+0200\n" -"PO-Revision-Date: 2021-01-19 08:16\n" +"PO-Revision-Date: 2021-03-03 07:44\n" "Last-Translator: \n" "Language-Team: German\n" "Language: de_DE\n" diff --git a/linux/kmflcomp/debian/control b/linux/kmflcomp/debian/control index a63849a084..5fd4997389 100644 --- a/linux/kmflcomp/debian/control +++ b/linux/kmflcomp/debian/control @@ -4,10 +4,11 @@ Section: utils Maintainer: Debian Input Method Team Uploaders: Keyman team , + Eberhard Beilharz , Build-Depends: debhelper (>= 11), libx11-dev, -Standards-Version: 4.5.0 +Standards-Version: 4.5.1 Vcs-Git: https://github.com/keymanapp/keyman.git Vcs-Browser: https://github.com/keymanapp/keyman/tree/master/linux/kmflcomp Homepage: https://www.keyman.com diff --git a/linux/kmflcomp/debian/upstream/signing-key.asc b/linux/kmflcomp/debian/upstream/signing-key.asc new file mode 100644 index 0000000000..a235c64d44 --- /dev/null +++ b/linux/kmflcomp/debian/upstream/signing-key.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=jcZL +-----END PGP PUBLIC KEY BLOCK----- diff --git a/linux/libkmfl/debian/changelog b/linux/libkmfl/debian/changelog index e3801f454e..a99651aff6 100644 --- a/linux/libkmfl/debian/changelog +++ b/linux/libkmfl/debian/changelog @@ -44,7 +44,7 @@ libkmfl (0.9.12-1) unstable; urgency=low libkmfl (0.9.11-1) UNRELEASED; urgency=low - * fix bug when nomatch is used with "use" keyword + * fix bug when nomatch is used with "use" keyword -- Doug Rintoul Wed, 17 Nov 2010 13:27:33 -0800 diff --git a/linux/libkmfl/debian/control b/linux/libkmfl/debian/control index f9c624a852..126c694c92 100644 --- a/linux/libkmfl/debian/control +++ b/linux/libkmfl/debian/control @@ -4,10 +4,11 @@ Section: libs Maintainer: Debian Input Method Team Uploaders: Keyman team , + Eberhard Beilharz , Build-Depends: debhelper (>= 11), libkmflcomp-dev (>=10.99.30), -Standards-Version: 4.5.0 +Standards-Version: 4.5.1 Vcs-Git: https://github.com/keymanapp/keyman.git Vcs-Browser: https://github.com/keymanapp/keyman/tree/master/linux/libkmfl Homepage: https://www.keyman.com diff --git a/linux/libkmfl/debian/upstream/signing-key.asc b/linux/libkmfl/debian/upstream/signing-key.asc new file mode 100644 index 0000000000..a235c64d44 --- /dev/null +++ b/linux/libkmfl/debian/upstream/signing-key.asc @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=jcZL +-----END PGP PUBLIC KEY BLOCK----- diff --git a/linux/scripts/launchpad.sh b/linux/scripts/launchpad.sh index f7a9b75448..351cb73fa8 100755 --- a/linux/scripts/launchpad.sh +++ b/linux/scripts/launchpad.sh @@ -61,7 +61,6 @@ for proj in ${projects}; do cd ${proj}-${version} echo $(pwd) cp debian/changelog ../${proj}-changelog - #TODO separate source builds and dputs for each of $dists? for dist in ${distributions}; do cp ../${proj}-changelog debian/changelog dch -v ${version}-${packageversion}~${dist} "source package for PPA" diff --git a/linux/scripts/reconf.sh b/linux/scripts/reconf.sh index 0351477779..8ea015955d 100755 --- a/linux/scripts/reconf.sh +++ b/linux/scripts/reconf.sh @@ -60,8 +60,14 @@ for proj in ${extra_projects}; do cd keyman-config make clean cd keyman_config - sed -e "s/_VERSION_/${VERSION}/g" -e "s/_VERSIONWITHTAG_/${VERSION_WITH_TAG}/g" -e "s/_MAJORVERSION_/${VERSION_MAJOR}/g" \ - -e "s/_RELEASEVERSION_/${VERSION_RELEASE}/g" -e "s/_TIER_/${TIER}/g" version.py.in > version.py + sed \ + -e "s/_VERSION_/${VERSION}/g" \ + -e "s/_VERSIONWITHTAG_/${VERSION_WITH_TAG}/g" \ + -e "s/_MAJORVERSION_/${VERSION_MAJOR}/g" \ + -e "s/_RELEASEVERSION_/${VERSION_RELEASE}/g" \ + -e "s/_TIER_/${TIER}/g" \ + -e "s/_ENVIRONMENT_/${VERSION_ENVIRONMENT}/g" \ + version.py.in > version.py cd ../buildtools && python3 ./build-langtags.py fi diff --git a/linux/scripts/watch.in b/linux/scripts/watch.in index d9d38cd3b5..b4c0039a84 100644 --- a/linux/scripts/watch.in +++ b/linux/scripts/watch.in @@ -1,3 +1,3 @@ version=4 # Tier replaced by package-build.inc.sh script -https://downloads.keyman.com/linux/$tier/@ANY_VERSION@/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ debian uupdate +opts=pgpsigurlmangle=s/$/.asc/ https://downloads.keyman.com/linux/$tier/@ANY_VERSION@/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ debian uupdate diff --git a/resources/build/build-utils.sh b/resources/build/build-utils.sh index 0211dd14af..aca68ee6ce 100755 --- a/resources/build/build-utils.sh +++ b/resources/build/build-utils.sh @@ -73,12 +73,12 @@ function findVersion() { VERSION_TAG= fi - if [ -z "${TEAMCITY_VERSION-}" ]; then + if [ -z "${TEAMCITY_VERSION-}" -a -z "${JENKINS_HOME-}" ]; then # Local dev machine, not TeamCity VERSION_TAG="$VERSION_TAG-local" VERSION_ENVIRONMENT=local else - # On TeamCity; are we running a pull request build or a master/beta/stable build? + # On TeamCity: are we running a pull request build or a master/beta/stable build? if [ ! -z "${TEAMCITY_PR_NUMBER-}" ]; then VERSION_ENVIRONMENT=test # Note TEAMCITY_PR_NUMBER can also be 'master', 'beta', or 'stable-x.y' @@ -141,16 +141,17 @@ function printBuildNumberForTeamCity() { } function printVersionUtilsDebug() { - echo "KEYMAN_ROOT: $KEYMAN_ROOT" - echo "VERSION: $VERSION" - echo "VERSION_WIN: $VERSION_WIN" - echo "VERSION_RELEASE: $VERSION_RELEASE" - echo "VERSION_MAJOR: $VERSION_MAJOR" - echo "VERSION_MINOR: $VERSION_MINOR" - echo "VERSION_PATCH: $VERSION_PATCH" - echo "TIER: $TIER" - echo "VERSION_TAG: $VERSION_TAG" - echo "VERSION_WITH_TAG: $VERSION_WITH_TAG" + echo "KEYMAN_ROOT: $KEYMAN_ROOT" + echo "VERSION: $VERSION" + echo "VERSION_WIN: $VERSION_WIN" + echo "VERSION_RELEASE: $VERSION_RELEASE" + echo "VERSION_MAJOR: $VERSION_MAJOR" + echo "VERSION_MINOR: $VERSION_MINOR" + echo "VERSION_PATCH: $VERSION_PATCH" + echo "TIER: $TIER" + echo "VERSION_TAG: $VERSION_TAG" + echo "VERSION_WITH_TAG: $VERSION_WITH_TAG" + echo "VERSION_ENVIRONMENT: $VERSION_ENVIRONMENT" } function findShouldSentryRelease() { diff --git a/resources/build/help-keyman-com.sh b/resources/build/help-keyman-com.sh index f90e0fe5ad..4128bdbf0b 100755 --- a/resources/build/help-keyman-com.sh +++ b/resources/build/help-keyman-com.sh @@ -70,6 +70,15 @@ fi # Uploading Keyman documentation # +## Determine the help.keyman.com product path based on $platform +function help_product_path { + if [ $platform == 'ios' ]; then + echo "products/iphone-and-ipad/$VERSION_RELEASE" + else + echo "products/$platform/$VERSION_RELEASE" + fi +} + ## ## Upload documentation updates to help.keyman.com ## Paths depend on $platform @@ -82,29 +91,26 @@ function upload_keyman_help { case $platform in android) helppath=$KEYMAN_ROOT/android/help - dstpath="$HELP_KEYMAN_COM/products/android/$VERSION_RELEASE" ;; ios) helppath=$KEYMAN_ROOT/ios/help - dstpath="$HELP_KEYMAN_COM/products/iphone-and-ipad/$VERSION_RELEASE" ;; linux) helppath=$KEYMAN_ROOT/linux/help - dstpath="$HELP_KEYMAN_COM/products/linux/$VERSION_RELEASE" ;; mac) helppath=$KEYMAN_ROOT/mac/help - dstpath="$HELP_KEYMAN_COM/products/mac/$VERSION_RELEASE" ;; windows) # Note: `/windows/src/desktop/help/build.sh web` must be run first helppath=$KEYMAN_ROOT/windows/bin/help/md/desktop - dstpath="$HELP_KEYMAN_COM/products/windows/$VERSION_RELEASE" ;; *) display_usage esac + dstpath="$HELP_KEYMAN_COM/$(help_product_path)" + # # Look for help source folder. # @@ -138,7 +144,7 @@ function commit_and_push { fi local branchname="auto/$platform-help-$VERSION_WITH_TAG" - local modifiedfiles="products/$platform/$VERSION_RELEASE" + local modifiedfiles=$(help_product_path) local basebranch="master" diff --git a/resources/build/increment-version.sh b/resources/build/increment-version.sh index 8e29c2116c..1d545f577b 100755 --- a/resources/build/increment-version.sh +++ b/resources/build/increment-version.sh @@ -29,9 +29,11 @@ THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BA gitbranch=`git branch --show-current` FORCE=0 +HISTORY_FORCE= if [[ "$1" == "-f" ]]; then FORCE=1 + HISTORY_FORCE=--force shift fi @@ -86,7 +88,7 @@ popd > /dev/null pushd "$KEYMAN_ROOT" > /dev/null ABORT=0 -node resources/build/version/lib/index.js history version -t "$GITHUB_TOKEN" -b "$base" || ABORT=$? +node resources/build/version/lib/index.js history version -t "$GITHUB_TOKEN" -b "$base" $HISTORY_FORCE || ABORT=$? if [[ $ABORT = 1 ]]; then if [[ $FORCE = 0 ]]; then diff --git a/resources/build/l10n/README.md b/resources/build/l10n/README.md index 318e6815e3..7a3023a51a 100644 --- a/resources/build/l10n/README.md +++ b/resources/build/l10n/README.md @@ -6,10 +6,10 @@ Downloading and updating files between Keyman and Crowdin happens automatically on GitHub by way of the Crowdin git integration. The configuration file for all platforms is a YAML file named `/crowdin.yml`. Currently, the git integration tracks the `master` branch. -The only thing that needs to be done manually is to update `crowdin.yml` if new files get added +For most of our platforms, the only thing that needs to be done manually is to update `crowdin.yml` if new files get added (and of course translating the strings on the [Crowdin website](https://crowdin.com/project/keyman)). -## Manual up- and download with Crowdin CLI +## Manual upload and download with Crowdin CLI The following describes how alternatively the Crowdin CLI (v3) tool could be used to automate downloading and updating files between Keyman and Crowdin. @@ -92,6 +92,23 @@ android/KMEA/app/src/main/java/com/tavultesoft/kmea/DisplayLangugages.java For the BCP-47 language tags to use in that file, don't include script names since the Android locales only handle language ID and region. +### Updating Localization Targets in Keyman Engine / the Keyman App for iPhone and iPad + +When a language receives its first localization for our iOS platform, a few settings must be +tweaked within Xcode in order to enable it. + +1. For both the KeymanEngine and Keyman subprojects, go to the project page's info tab and inform Xcode of the appropriate language code. + +![Refer to [this image](imgs/updating-ios-l10ns-1.png) for guidance.](./imgs/updating-ios-l10ns-1.png) + +Note that Xcode will usually fail to actually _include_ the newly downloaded resources. + +2. So, to include those resources, search the workspace for each localized file and select them one at a time. When you do so, the right-hand side bar should a set of checkboxes for each localized language, with the new language unchecked. + +![[This image](imgs/updating-ios-l10ns-2.png) should provide a helpful reference.](./imgs/updating-ios-l10ns-2.png) + +Clicking the checkbox will summon a dialog that will allow the existing file to be utilized. + ## Tip Microsoft offers a website that allows to check how terms used in Windows are translated diff --git a/resources/build/l10n/imgs/updating-ios-l10ns-1.png b/resources/build/l10n/imgs/updating-ios-l10ns-1.png new file mode 100644 index 0000000000..ddcd11d4d1 Binary files /dev/null and b/resources/build/l10n/imgs/updating-ios-l10ns-1.png differ diff --git a/resources/build/l10n/imgs/updating-ios-l10ns-2.png b/resources/build/l10n/imgs/updating-ios-l10ns-2.png new file mode 100644 index 0000000000..7d7e0bdd90 Binary files /dev/null and b/resources/build/l10n/imgs/updating-ios-l10ns-2.png differ diff --git a/resources/build/version/src/fixupHistory.ts b/resources/build/version/src/fixupHistory.ts index 00b4813d04..b6683dc56d 100644 --- a/resources/build/version/src/fixupHistory.ts +++ b/resources/build/version/src/fixupHistory.ts @@ -168,7 +168,7 @@ const splicePullsIntoHistory = async (pulls: PRInformation[]): Promise<{count: n } if(!found) { - const entry = `* ${pull.title} (#${pull.number})`; + const entry = (pull.number == 0) ? `* ${pull.title}` : `* ${pull.title} (#${pull.number})`; console.log(`-- Adding ${entry}`); historyChunks.current.splice(2, 0, entry); currentPulls.push(pull.number); @@ -246,12 +246,12 @@ export const sendCommentToPullRequestAndRelatedIssues = async ( * Adds any outstanding pull request titles to HISTORY.md for the current * version. Retrieves pull request details from GitHub. * @returns number of history entries for the current version, - * 0 if no pulls associated with the current vesrion, or + * 0 if no pulls associated with the current version, or * -1 on error. */ export const fixupHistory = async ( - octokit: GitHub, base: string + octokit: GitHub, base: string, force: boolean ): Promise => { // @@ -269,7 +269,7 @@ export const fixupHistory = async ( // const git_result = (await spawnChild('git', ['log', '--merges', /*'--first-parent',*/ '--format=%H', base, `${commit_id}..`])).trim(); - if(git_result.length == 0) { + if(git_result.length == 0 && !force) { // We won't throw on this logWarning('No pull requests found since previous increment'); return 0; @@ -284,13 +284,21 @@ export const fixupHistory = async ( let pulls: PRInformation[] = []; - for(const commit of new_commits) { - const pr = await getAssociatedPRInformation(octokit, commit); - if(pr === undefined) { - logWarning(`commit ref ${commit} has no associated pull request.`); - continue; + if(git_result.length == 0) { + pulls.push({ + title: 'No changes made', + number: 0 + }); + } + else { + for(const commit of new_commits) { + const pr = await getAssociatedPRInformation(octokit, commit); + if(pr === undefined) { + logWarning(`commit ref ${commit} has no associated pull request.`); + continue; + } + pulls.push(pr); } - pulls.push(pr); } //logInfo(JSON.stringify(pulls, null, 2)); @@ -305,7 +313,9 @@ export const fixupHistory = async ( // Write a comment to GitHub for each of the pulls // - await sendCommentToPullRequestAndRelatedIssues(octokit, historyResult.pulls); + if(git_result.length > 0) { + await sendCommentToPullRequestAndRelatedIssues(octokit, historyResult.pulls); + } return historyResult.count; }; diff --git a/resources/build/version/src/index.ts b/resources/build/version/src/index.ts index 90c53f14b5..ffbc4877b1 100644 --- a/resources/build/version/src/index.ts +++ b/resources/build/version/src/index.ts @@ -8,7 +8,7 @@ import { readFileSync } from 'fs'; const argv = yargs .command(['history'], 'Fixes up HISTORY.md with pull request data') - .command(['version'], 'Increments the current ptach version in VERSION.md') + .command(['version'], 'Increments the current patch version in VERSION.md') .demandCommand(1, 2) .options({ 'base': { @@ -22,6 +22,11 @@ const argv = yargs demandOption: true, alias: 't', type: 'string' + }, + + 'force': { + description: 'Force a version increment even if no changes found', + type: 'boolean' } }) .help() @@ -53,7 +58,7 @@ const main = async (): Promise => { if(argv._.includes('history')) { logInfo(`# Validating history for ${version}`); - changeCount = await fixupHistory(octokit, argv.base); + changeCount = await fixupHistory(octokit, argv.base, argv.force); logInfo(`# ${changeCount} change(s) found for ${version}\n`); } diff --git a/web/source/dom/domDefaultOutput.ts b/web/source/dom/domDefaultOutput.ts index bf1988337a..b3471ef02c 100644 --- a/web/source/dom/domDefaultOutput.ts +++ b/web/source/dom/domDefaultOutput.ts @@ -28,14 +28,27 @@ namespace com.keyman.dom { let code = DefaultOutput.codeForEvent(Lkc); let domManager = com.keyman.singleton.domManager; + let hideCaret: () => void; + if(Lkc.Ltarg instanceof com.keyman.dom.targets.TouchAlias) { + hideCaret = function() { + let target = Lkc.Ltarg as com.keyman.dom.targets.TouchAlias; + target.root.hideCaret(); + } + } else { + hideCaret = function() {}; + } + switch(code) { case Codes.keyCodes['K_TAB']: + hideCaret(); domManager.moveToNext((Lkc.Lmodifiers & text.Codes.modifierCodes['SHIFT']) != 0); break; case Codes.keyCodes['K_TABBACK']: + hideCaret(); domManager.moveToNext(true); break; case Codes.keyCodes['K_TABFWD']: + hideCaret(); domManager.moveToNext(false); break; } diff --git a/web/source/dom/preProcessor.ts b/web/source/dom/preProcessor.ts index e397b4bfbc..ef2eb46d2b 100644 --- a/web/source/dom/preProcessor.ts +++ b/web/source/dom/preProcessor.ts @@ -157,6 +157,19 @@ namespace com.keyman.dom { */ s.Lmodifiers |= (e.metaKey ? modifierCodes['META']: 0); + // Physically-typed keys require use of a 'desktop' form factor and thus are based on a virtual "physical" Device. + s.device = keyman.util.physicalDevice.coreSpec; + + // Perform any browser-specific key remapping before other remaps and mnemonic transforms. + // (See https://github.com/keymanapp/keyman/issues/1125.) + if(!keyman.isEmbedded && s.device.browser == utils.Browser.Firefox) { + // Browser key identifiers are not completely consistent; Firefox has a few (for US punctuation) + // that differ from the norm. Refer to https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode. + if(KeyMapping.browserMap.FF['k'+s.Lcode]) { + s.Lcode = KeyMapping.browserMap.FF['k'+s.Lcode]; + } + } + // Mnemonic handling. if(activeKeyboard && activeKeyboard.isMnemonic) { // The following will never set a code corresponding to a modifier key, so it's fine to do this, @@ -168,9 +181,6 @@ namespace com.keyman.dom { var LisVirtualKeyCode = (typeof e.charCode != 'undefined' && e.charCode != null && (e.charCode == 0 || (s.Lmodifiers & 0x6F) != 0)); s.LisVirtualKey = LisVirtualKeyCode || e.type != 'keypress'; - // Physically-typed keys require use of a 'desktop' form factor and thus are based on a virtual "physical" Device. - s.device = keyman.util.physicalDevice.coreSpec; - // This is based on a KeyboardEvent, so it's not considered 'synthetic' within web-core. s.isSynthetic = false; @@ -200,15 +210,6 @@ namespace com.keyman.dom { }; } } - - // Check for any browser-based keymapping before returning the object. - if(!keyman.isEmbedded && s.device.browser == utils.Browser.Firefox) { - // I1466 - Convert the - keycode on mnemonic as well as positional layouts - // FireFox, Mozilla Suite - if(KeyMapping.browserMap.FF['k'+s.Lcode]) { - s.Lcode = KeyMapping.browserMap.FF['k'+s.Lcode]; - } - } return s; } diff --git a/web/source/dom/targets/touchAlias.ts b/web/source/dom/targets/touchAlias.ts index 11b68a1810..e4b75fb798 100644 --- a/web/source/dom/targets/touchAlias.ts +++ b/web/source/dom/targets/touchAlias.ts @@ -69,6 +69,7 @@ namespace com.keyman.dom.targets { this.insertTextBeforeCaret('\n'); } else if(dom.Utils.instanceof(this.root.base, "HTMLInputElement")) { // HTMLInputElements do not permit newlines; they instead have DOM-specific behaviors. + this.root.hideCaret(); Input.newlineHandler(this.root.base as HTMLInputElement); } else { console.warn("TouchAlias OutputTarget cannot output newlines for unexpected base element types!"); diff --git a/web/source/kmwdevice.ts b/web/source/kmwdevice.ts index 99d28fb247..1338de4dd8 100644 --- a/web/source/kmwdevice.ts +++ b/web/source/kmwdevice.ts @@ -84,11 +84,15 @@ namespace com.keyman { // // Firefox uses '.' between version components, while Chrome and Safari use // '_' instead. So, we have to check for both. Yay. - let regex = /Intel Mac OS X (10(?:[_\.]\d+)+)/i; + let regex = /Intel Mac OS X (\d+(?:[_\.]\d+)+)/i; let results = regex.exec(agent); // Match result: a version string with components separated by underscores. - if(results.length > 1 && results[1]) { + if(!results) { + console.warn("KMW could not properly parse the user agent string." + + "A suboptimal keyboard layout may result."); + this.OS='MacOSX'; + } else if(results.length > 1 && results[1]) { // Convert version string into a usable form. let versionString = results[1].replace('_', '.'); let version = new utils.Version(versionString); diff --git a/web/source/kmwuitoolbar.ts b/web/source/kmwuitoolbar.ts index eb15399a61..73fc9bb88b 100644 --- a/web/source/kmwuitoolbar.ts +++ b/web/source/kmwuitoolbar.ts @@ -531,7 +531,7 @@ if(!window['keyman']['ui']['name']) { var itemNode = ui.createNode('li'); kbdText = lang.keyboards[n]['Name'].replace(/\s?keyboard/i,''); // We append the full BCP-47 code here for disambiguation when regional and/or script variants exist. - kbdText = kbdText + " [" + lang.keyboards[n].LanguageCode + "]"; + kbdText = kbdText + " [" + lang.keyboards[n]['LanguageCode'] + "]"; aNode = ui.createNode('a', null, null, kbdText); aNode.href = '#'; aNode.title = ''; diff --git a/web/source/osk/oskManager.ts b/web/source/osk/oskManager.ts index 2e56761f18..8a5c2042bc 100644 --- a/web/source/osk/oskManager.ts +++ b/web/source/osk/oskManager.ts @@ -261,8 +261,14 @@ namespace com.keyman.osk { } // Correct the classname for the (inner) OSK frame (Build 360) + var kbdID: string = (activeKeyboard ? activeKeyboard.id.replace('Keyboard_','') : ''); + if(keymanweb.isEmbedded && kbdID.indexOf('::') != -1) { + // De-namespaces the ID for use with CSS classes. + // Assumes that keyboard IDs may not contain the ':' symbol. + kbdID = kbdID.substring(kbdID.indexOf('::') + 2); + } var innerFrame= this._Box.firstChild, - kbdClass = ' kmw-keyboard-' + (activeKeyboard ? activeKeyboard.id.replace('Keyboard_','') : ''); + kbdClass = ' kmw-keyboard-' + kbdID; if(innerFrame.id == 'keymanweb_title_bar') { // Desktop order is title_bar, banner_container, inner-frame innerFrame= innerFrame.nextSibling.nextSibling; diff --git a/web/source/osk/visualKeyboard.ts b/web/source/osk/visualKeyboard.ts index 7e1e3fc14b..131cd83275 100644 --- a/web/source/osk/visualKeyboard.ts +++ b/web/source/osk/visualKeyboard.ts @@ -170,7 +170,7 @@ namespace com.keyman.osk { } getKeyWidth(osk: VisualKeyboard): number { - let units = this.objectUnits(); + let units = this.objectUnits(osk.isStatic); if(units == 'px') { // For mobile devices, we presently specify width directly in pixels. Just use that! @@ -184,9 +184,9 @@ namespace com.keyman.osk { } } - objectUnits(): string { + objectUnits(isStatic: boolean): string { // Returns a unit string corresponding to how the width for each key is specified. - if(this.formFactor == 'desktop') { + if(this.formFactor == 'desktop' || isStatic) { return '%'; } else { return 'px'; @@ -389,7 +389,7 @@ namespace com.keyman.osk { kDiv.className='kmw-key-square'; let ks=kDiv.style; - ks.width=this.objectGeometry(spec['widthpc']); + ks.width=this.objectGeometry(spec['widthpc'], osk.isStatic); let originalPercent = totalPercent; @@ -402,23 +402,25 @@ namespace com.keyman.osk { // Set key and button positioning properties. if(!isDesktop) { // Regularize interkey spacing by rounding key width and padding (Build 390) - ks.left=this.objectGeometry(totalPercent+spec['padpc']); + ks.left=this.objectGeometry(totalPercent+spec['padpc'], osk.isStatic); if(!osk.isStatic) { ks.bottom=rowStyle.bottom; } ks.height=rowStyle.height; // must be specified in px for rest of layout to work correctly - // Set distinct phone and tablet button position properties - btn.style.left=ks.left; - btn.style.width=ks.width; + if(!osk.isStatic) { + // Set distinct phone and tablet button position properties + btn.style.left=ks.left; + btn.style.width=ks.width; + } } else { - ks.marginLeft=this.objectGeometry(spec['padpc']); + ks.marginLeft=this.objectGeometry(spec['padpc'], osk.isStatic); } totalPercent=totalPercent+spec['padpc']+spec['widthpc']; - // Add the (US English) keycap label for desktop OSK or if KDU flag is non-zero - if(layout.keyLabels || isDesktop) { + // Add the (US English) keycap label for layouts requesting display of underlying keys + if(layout["displayUnderlying"]) { let keyCap = this.generateKeyCapLabel(); if(keyCap) { @@ -461,8 +463,8 @@ namespace com.keyman.osk { return {element: kDiv, percent: totalPercent - originalPercent}; } - objectGeometry(v: number): string { - let unit = this.objectUnits(); + objectGeometry(v: number, isStatic: boolean): string { + let unit = this.objectUnits(isStatic); if(unit == '%') { return v + unit; } else { // unit == 'px' @@ -687,9 +689,6 @@ namespace com.keyman.osk { this.fontFamily=''; } - // Set flag to add default (US English) key label if specified by keyboard - layout.keyLabels = keyboard && keyboard.displaysUnderlyingKeys; - let divLayerContainer = this.deviceDependentLayout(keyboard, device.formFactor as utils.FormFactor); this.ddOSK = true; @@ -889,7 +888,7 @@ namespace com.keyman.osk { // Get the actual available document width and scale factor according to device type var objectWidth : number; - if(formFactor == 'desktop') { + if(formFactor == 'desktop' || this.isStatic) { objectWidth = 100; } else { objectWidth = oskManager.getWidth(); @@ -943,7 +942,7 @@ namespace com.keyman.osk { // Apply defaults, setting the width and other undefined properties for each key keys=row['key']; - if(!precalibrated) { + if(!precalibrated || this.isStatic) { // Calculate actual key widths by multiplying by the OSK's width and rounding appropriately, // adjusting the width of the last key to make the total exactly 100%. // Overwrite the previously-computed percent. @@ -1008,7 +1007,7 @@ namespace com.keyman.osk { lDiv.appendChild(gDiv); } - // Now that we've properly processed the keyboard's layout, mark it as calib + // Now that we've properly processed the keyboard's layout, mark it as calibrated. keyboard.markLayoutCalibrated(formFactor); return lDiv; } @@ -2336,7 +2335,9 @@ namespace com.keyman.osk { for(Ln=0; Ln Keyman läuft noch. Klicken Sie auf dieses Symbol, um die Tastatur für Ihre Sprache jederzeit zu verwenden + + + + Keyman läuft bereits. Klicken Sie auf das Keyman-Symbol im Benachrichtigungsbereich um Ihre Sprachtastatur zu verwenden @@ -224,11 +228,11 @@ Es sind keine Tastaturen installiert. Klicken Sie auf \'Tastatur herunterladen\', um eine Tastatur von der Keyman-Website zu installieren. - + - Sie können die \'%0:s\'-Tastatur nur deinstallieren, wenn Sie ein Administrator sind + Sie können die \'%1$s\' -Tastatur nur deinstallieren, wenn Sie ein Administrator sind @@ -340,7 +344,7 @@ - Benutzer Berechtigungen werden immer in Windows Vista erhöht + Benutzer Berechtigungen werden immer in Windows Vista erhöht @@ -365,7 +369,7 @@ - Keyman Menü öffnen + Keyman Menü öffnen @@ -450,11 +454,11 @@ Nicht installieren, nur herunterladen - + - Kann Tastatur nicht herunterladen. Fehler %1:d: %0:s + Kann Tastatur nicht herunterladen. Fehler %2$d: %1$s @@ -507,30 +511,30 @@ Tastenkürzel ändern - + - Standard-Tastenkürzel selektieren, oder \'Benutzerdefiniert\' auswählen und Strg, Umschalt- und/oder Alt- sowie gewünschte Taste für die Sprache %0:s drücken: + Standard-Tastenkürzel selektieren, oder \'Benutzerdefiniert\' auswählen und Strg, Umschalt- und/oder Alt- sowie Taste für das gewünschte Tastaturkürzel für die Sprache %1$s drücken: Standard-Tastenkürzel selektieren, oder \'Benutzerdefiniert\' auswählen und Strg, Umschalt- und/oder Alt- sowie Taste für das gewünschte Tastaturkürzel drücken: - + - Das Tastenkürzel %0:s steht im Konflikt mit der normalen Tastatur. Sie sollten mindestens Strg oder Alt verwenden. Möchten Sie dies jetzt ändern? - + Das Tastenkürzel %1$s steht im Konflikt mit der normalen Tastatur. Sie sollten mindestens Strg oder Alt verwenden. Möchten Sie dies jetzt ändern\? + - Das Tastenkürzel %0:s steht im Konflikt zu dem ausgewählten Tastenkürzel für die %1:s-Tastatur. Wenn Sie fortfahren wird das Tastenkürzel für %1:s gelöscht. Fortfahren? - + Das Tastenkürzel %1$s steht im Konflikt zu dem ausgewählten Tastenkürzel für die %2$s-Tastatur. Wenn Sie fortfahren wird das Tastenkürzel für %2$s gelöscht. Fortfahren\? + - Das Tastenkürzel %0:s steht in Konflikt mit einem anderen Tastenkürzel. Wenn Sie fortfahren, wird das andere Tastenkürzel gelöscht. Fortfahren? + Das Tastenkürzel %1$s steht in Konflikt mit einem anderen Tastenkürzel. Wenn Sie fortfahren, wird das andere Tastenkürzel gelöscht. Fortfahren? @@ -567,25 +571,25 @@ Größe - + - Keyman %0:s - + Keyman %1$s + - %0:s-Tastatur %1:s + Keyboard %1$s %2$s Die keyman.com-Webseite ist nicht erreichbar - bitte testen Sie Ihre Internet-Verbindung, und versuchen Sie es erneut. - + - Die keyman.com-Webseite ist nicht erreichbar - bitte testen Sie Ihre Internet-Verbindung, und versuchen Sie es erneut. Der Fehler war: %0:s + Die keyman.com-Webseite ist nicht erreichbar - bitte testen Sie Ihre Internet-Verbindung, und versuchen Sie es erneut. Der Fehler war: %1$s @@ -666,12 +670,12 @@ - Keyman verlassen? + Keyman beenden\? - Sind Sie sicher, dass Sie Keyman verlassen möchten? Ihre Keyman-Tastaturen werden weiterhin in den Windows-Sprachen aufgelistet, werden aber erst dann funktionieren, wenn Sie Keyman neu starten. + Sind Sie sicher, dass Sie Keyman verlassen möchten\? Ihre Keyman-Tastaturen werden weiterhin in den Windows-Sprachen aufgelistet, werden aber erst dann funktionieren, wenn Sie Keyman neu starten. @@ -778,35 +782,40 @@ Keyman - + - Version %0:s + Version %1$s &Drucken... - + - Ein Paket mit dem Namen %0:s ist bereits installiert. Möchten Sie es deinstallieren und das neue installieren? - + Ein Paket mit dem Namen %1$s ist bereits installiert. Möchten Sie es deinstallieren und das neue installieren\? + - Eine Tastatur mit dem Name \'%0:s\' ist bereits installiert. Wenn Sie fortfahren, wird sie deinstalliert bevor die neue Tastatur installiert wird. Fortfahren? - + Eine Tastatur mit dem Namen \'%1$s\' ist bereits installiert. Wenn Sie fortfahren, wird sie deinstalliert, bevor die neue Tastatur installiert wird. Fortfahren\? + - Die Tastatur \'%0:s\' ist Teil des Pakets \'%1:s\'. Sie müssen das gesamte Paket deinstallieren. Fortfahren? - + Die Tastatur \'%1$s\' ist Teil des Pakets \'%2$s\'. Sie müssen das gesamte Paket deinstallieren. Fortfahren\? + + + + + Die Tastatursprache kann nicht installiert werden; Windows hat ein Limit von 4 benutzerdefinierten \'transienten\' Sprachen, und Sie haben möglicherweise dieses Limit erreicht. + - Das Paket oder die Tastatur \'%0:s\' enthält keine Einführungshilfe. + Das Paket oder die Tastatur \'%1$s\' enthält keine Einführungshilfe. @@ -831,72 +840,62 @@ Datei wird heruntergeladen - + - Sind Sie sicher, dass Sie die Bildschirmtastatur für %0:s entfernen möchten? - + Sind Sie sicher, dass Sie die Bildschirmtastatur für %1$s entfernen möchten\? + - Sind Sie sicher, dass Sie die Tastatur %0:s deinstallieren wollen? - + Sind Sie sicher, dass Sie die Tastatur %1$s deinstallieren wollen\? + - Sind Sie sicher, dass Sie das Tastaturbelegungspaket %0:s deinstallieren wollen? - -%1:s - + Sind Sie sicher, dass Sie das Paket %1$s deinstallieren möchten\?\n\n%2$s + - Die folgenden Schriftarten werden ebenfalls deinstalliert: %0:s. + Die folgenden Schriftarten werden ebenfalls deinstalliert: %1$s. - Die Zeichentabelle enthält eine Datenbank von Zeichen, die erstellt werden muss, bevor sie verwendet werden kann. Jetzt erstellen? - + Die Zeichentabelle enthält eine Datenbank von Zeichen, die erstellt werden muss, bevor sie verwendet werden kann. Jetzt erstellen\? + - Die Unicode Zeichen-Datenbank konnte nicht gelöscht werden für einen Wiederaufbau. Der Fehler ist: %0:s - + Die Unicode Zeichen-Datenbank konnte nicht gelöscht werden für einen Wiederaufbau. Der Fehler ist: %1$s + - Die Unicode Zeichen-Datenbank konnte nicht erstellt werden und wurde deaktiviert. Der Fehler ist: %0:s - + Die Unicode-Zeichen-Datenbank konnte nicht erstellt werden und wurde deaktiviert. Fehlerdetails: %1$s + - Die Unicode Zeichen-Datenbank konnte nicht geladen werden (%0:s). Jetzt neu erstellen? - + Die Unicode Zeichen-Datenbank konnte nicht geladen werden (%1$s). Jetzt neu erstellen\? + - Keyman konnte nicht gestartet werden. Bevor Sie fortfahren überprüfen Sie bitte Ihre Sicherheitseinstellungen, um sicherzustellen, dass das Programm keyman.exe gestartet werden darf. Der gemeldete Fehler ist: - -\"%0:s\" - -Möchten Sie Keyman erneut starten? + Keyman konnte nicht gestartet werden. Bevor Sie fortfahren überprüfen Sie bitte Ihre Sicherheitseinstellungen, um sicherzustellen, dass das Programm keyman.exe gestartet werden darf. Der gemeldete Fehler ist:\n\n\"%1$s\"\n\nMöchten Sie Keyman erneut starten\? - Keyman Debug-Informationen werden in einer Protokolldatei mit dem Namen %LOCALAPPDATA%\Keyman\Diag\system#.etl gespeichert (wobei # eine Nummer ist). Sie sollten Keyman beenden, bevor Sie diese Datei löschen. - -WARNUNG: Diese Datei kann sehr schnell groß werden. Das Aktivieren der Debug-Protokollierung kann Ihr System verlangsamen und sollte nur auf Anweisung durch den technischen Support erfolgen. - -DATENSCHUTZ-HINWEIS: Bitte beachten Sie, dass die Debug-Logdatei alle von Ihnen eingegebenen Tastenanschläge aufzeichnet. Sie sollten das Debug-Log nur für die Dauer einer Debugging- oder Diagnosesitzung aktivieren. + Keyman Debug-Informationen werden in einer Protokolldatei mit dem Namen %LOCALAPPDATA%\Keyman\Diag\system#.etl gespeichert (wobei # eine Nummer ist). Sie sollten Keyman beenden, bevor Sie diese Datei löschen.\n\nWARNUNG: Diese Datei kann sehr schnell groß werden. Das Aktivieren der Debug-Protokollierung kann Ihr System verlangsamen und sollte nur auf Anweisung durch den technischen Support erfolgen.\n\nDATENSCHUTZ-HINWEIS: Bitte beachten Sie, dass die Debug-Logdatei alle von Ihnen eingegebenen Tastenanschläge aufzeichnet. Sie sollten das Debug-Log nur für die Dauer einer Debugging- oder Diagnosesitzung aktivieren. - Bitte warten Sie, während nach Schriftarten für Tastatur %0:s gesucht wird + Bitte warten Sie, während nach verwandten Schriftarten für Tastatur %1$s gesucht wird - Tastatur %0:s ist keine Unicode-Tastatur. Schriftarten können nicht automatisch gefunden werden. Bitte überprüfen Sie die Tastaturdokumentation für Schriftarten. + Tastatur %1$s ist keine Unicode-Tastatur. Schriftarten können nicht automatisch gefunden werden. Bitte überprüfen Sie die Tastaturdokumentation für Schriftarten. diff --git a/windows/src/desktop/kmshell/main/Keyman.Configuration.System.UImportOlderVersionKeyboards11To13.pas b/windows/src/desktop/kmshell/main/Keyman.Configuration.System.UImportOlderVersionKeyboards11To13.pas index faad0d9454..699a455fe3 100644 --- a/windows/src/desktop/kmshell/main/Keyman.Configuration.System.UImportOlderVersionKeyboards11To13.pas +++ b/windows/src/desktop/kmshell/main/Keyman.Configuration.System.UImportOlderVersionKeyboards11To13.pas @@ -4,10 +4,12 @@ interface type TImportOlderVersionKeyboards11To13 = class + private + class function ShouldRun: Boolean; + class procedure ReRegisterTips; public class procedure Execute; class procedure BackupCurrentUser; - class procedure ReRegisterTips; class procedure ImportCurrentUser; end; @@ -37,6 +39,9 @@ uses class procedure TImportOlderVersionKeyboards11To13.Execute; // I2361 begin + if not ShouldRun then + Exit; + // execute KMshell as login user to backup list of installed Keyman TIPs TUtilExecute.CreateProcessAsShellUser(ParamStr(0), '"'+ParamStr(0)+'" -upgradekeyboards=13,backup', True); @@ -101,6 +106,9 @@ var uks: TUpgradeKeyboardList; uk: TUpgradeKeyboard; begin + if not ShouldRun then + Exit; + uks := LoadUpgradeKeyboards; r := TRegistry.Create; try @@ -177,6 +185,36 @@ begin (kmcom.Keyboards as IKeymanKeyboardsInstalled2).RefreshInstalledKeyboards; end; +class function TImportOlderVersionKeyboards11To13.ShouldRun: Boolean; +var + r: TRegistry; + keys: TStringList; + i: Integer; +begin + r := TRegistry.Create(KEY_READ); + keys := TStringList.Create; + try + r.RootKey := HKEY_LOCAL_MACHINE; + if not r.OpenKeyReadOnly(SRegKey_InstalledKeyboards_LM) then + // No keyboards exist, no upgrade necessary + Exit(False); + + r.GetKeyNames(keys); + if keys.Count = 0 then + // No keyboards exist, no upgrade necessary + Exit(False); + + for i := 0 to keys.Count - 1 do + if r.KeyExists('\' + SRegKey_InstalledKeyboards_LM + '\' + keys[i] + '\' + SRegSubKey_TransientLanguageProfiles) then + // The keyboard has already been upgraded to 14.0, we mustn't upgrade again + Exit(False); + + Result := True; + finally + r.Free; + end; +end; + class procedure TImportOlderVersionKeyboards11To13.ImportCurrentUser; var r: TRegistry; @@ -191,6 +229,9 @@ var RegistrationRequired: WordBool; i: Integer; begin + if not ShouldRun then + Exit; + r := TRegistry.Create; strings := TStringList.Create; try diff --git a/windows/src/desktop/setup/locale/de/strings.xml b/windows/src/desktop/setup/locale/de/strings.xml index 7622a24850..734cf0df7a 100644 --- a/windows/src/desktop/setup/locale/de/strings.xml +++ b/windows/src/desktop/setup/locale/de/strings.xml @@ -1,12 +1,12 @@ - Englisch + Deutsch $APPNAME $VERSION Setup Installiere $APPNAME $VERSION $APPNAME $VERSION wurde erfolgreich installiert. Sind Sie sicher, dass Sie die Installation von $APPNAME abbrechen möchten? Entpacke Dateien... - Pakete werden überprüft... + Pakete werden überprüft... Prüfe online auf Updates... Installierte Versionen werden überprüft... Fertigstellen... @@ -18,7 +18,7 @@ • %0:s %1:s für %2:s %3:s Es gibt nichts zu installieren. - (%0:s herunterladen) + (%0:s herunterladen) Setup installiert: $APPNAME $VERSION ist kostenlos und Open Source &Lizenzbedingungen @@ -56,7 +56,7 @@ Jetzt neu starten? $APPNAME %0:s ist bereits installiert. - Version %0:s herunterladen (%1:s) + Version %0:s herunterladen (%1:s) Version %0:s diff --git a/windows/src/developer/TIKE/Tike.dpr b/windows/src/developer/TIKE/Tike.dpr index 401d6b6493..3977f72f49 100644 --- a/windows/src/developer/TIKE/Tike.dpr +++ b/windows/src/developer/TIKE/Tike.dpr @@ -302,27 +302,32 @@ const begin TKeymanSentryClient.Start(TSentryClientVcl, kscpDeveloper, LOGGER_DEVELOPER_IDE_TIKE); try - CoInitFlags := COINIT_APARTMENTTHREADED; - - FInitializeCEF := TCEFManager.Create; try - if FInitializeCEF.Start then - begin - InitThemeLibrary; - SetThemeAppProperties(STAP_ALLOW_NONCLIENT or STAP_ALLOW_CONTROLS or STAP_ALLOW_WEBCONTENT); - Application.MainFormOnTaskBar := True; - Application.Initialize; - // TStyleManager.TrySetStyle(FKeymanDeveloperOptions.DisplayTheme); - Application.Title := 'Keyman Developer'; - //TBX.TBXSetTheme('OfficeXP2'); - if TikeActive then Exit; - InitClasses; - Application.CreateForm(TmodWebHttpServer, modWebHttpServer); - Application.CreateForm(TfrmKeymanDeveloper, frmKeymanDeveloper); - Application.Run; + CoInitFlags := COINIT_APARTMENTTHREADED; + + FInitializeCEF := TCEFManager.Create; + try + if FInitializeCEF.Start then + begin + InitThemeLibrary; + SetThemeAppProperties(STAP_ALLOW_NONCLIENT or STAP_ALLOW_CONTROLS or STAP_ALLOW_WEBCONTENT); + Application.MainFormOnTaskBar := True; + Application.Initialize; + // TStyleManager.TrySetStyle(FKeymanDeveloperOptions.DisplayTheme); + Application.Title := 'Keyman Developer'; + //TBX.TBXSetTheme('OfficeXP2'); + if TikeActive then Exit; + InitClasses; + Application.CreateForm(TmodWebHttpServer, modWebHttpServer); + Application.CreateForm(TfrmKeymanDeveloper, frmKeymanDeveloper); + Application.Run; + end; + finally + FInitializeCEF.Free; end; - finally - FInitializeCEF.Free; + except + on E:Exception do + SentryHandleException(E); end; finally TKeymanSentryClient.Stop; diff --git a/windows/src/developer/TIKE/actions/dmActionsMain.pas b/windows/src/developer/TIKE/actions/dmActionsMain.pas index 910f9616c0..e7ac638883 100644 --- a/windows/src/developer/TIKE/actions/dmActionsMain.pas +++ b/windows/src/developer/TIKE/actions/dmActionsMain.pas @@ -573,6 +573,7 @@ end; procedure TmodActionsMain.OpenProject(FileName: WideString); begin + FileName := ExpandUNCFileName(FileName); if (FileName <> '') and not FileExists(FileName) then begin ShowMessage('The project '+FileName+' does not exist.'); diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas index c8739c0e59..152a6f993a 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UframeWordlistEditor.pas @@ -46,7 +46,7 @@ type frameSource: TframeTextEditor; FSetup: Integer; FModified: Boolean; - FOnModifiedChanged: TNotifyEvent; + FOnChanged: TNotifyEvent; FFilename: string; procedure UpdateData; function MoveCodeToWordlist: Boolean; @@ -67,7 +67,7 @@ type function SaveToFile(const Filename: string): Boolean; property Filename: string read FFilename; property Modified: Boolean read FModified write SetModified; - property OnModifiedChanged: TNotifyEvent read FOnModifiedChanged write FOnModifiedChanged; + property OnChanged: TNotifyEvent read FOnChanged write FOnChanged; end; implementation @@ -212,10 +212,9 @@ var w: TWordlistWord; begin if FSetup > 0 then Exit; + Inc(FSetup); try - Modified := True; - if ARow = gridWordlist.RowCount - 1 then begin if (Value = '') or (Value = S_AddRowText) then @@ -232,12 +231,16 @@ begin begin w := FWordlist.Word[ARow-1]; case ACol of - 0: w.Word := Value; - 1: w.Frequency := StrToIntDef(Value, 0); - 2: w.Comment := Value; + 0: if w.Word = Value then Exit else w.Word := Value; + 1: if w.Frequency = StrToIntDef(Value, 0) then Exit else w.Frequency := StrToIntDef(Value, 0); + 2: if w.Comment = Value then Exit else w.Comment := Value; end; FWordlist.Word[ARow-1] := w; end; + + Modified := True; + if Assigned(FOnChanged) then + FOnChanged(Self); finally Dec(FSetup); end; @@ -291,18 +294,17 @@ end; procedure TframeWordlistEditor.SetModified(const Value: Boolean); begin - if FModified <> Value then - begin - FModified := Value; - if Assigned(FOnModifiedChanged) then - FOnModifiedChanged(Self); - end; + FModified := Value; end; procedure TframeWordlistEditor.SourceChanged(Sender: TObject); begin if FSetup = 0 then + begin Modified := True; + if Assigned(FOnChanged) then + FOnChanged(Self); + end; end; procedure TframeWordlistEditor.FillCode; diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm index 82d20a01c9..d07f24d186 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.dfm @@ -5,6 +5,7 @@ inherited frmModelEditor: TfrmModelEditor ClientHeight = 708 ClientWidth = 712 Font.Name = 'Tahoma' + Position = poDesigned ExplicitWidth = 712 ExplicitHeight = 708 PixelsPerInch = 96 @@ -14,7 +15,7 @@ inherited frmModelEditor: TfrmModelEditor Top = 0 Width = 712 Height = 708 - ActivePage = pageCompile + ActivePage = pageDetails Align = alClient Font.Charset = ANSI_CHARSET Font.Color = clWindowText @@ -48,7 +49,7 @@ inherited frmModelEditor: TfrmModelEditor object panWordlists: TPanel AlignWithMargins = True Left = 4 - Top = 181 + Top = 261 Width = 611 Height = 232 Margins.Left = 4 @@ -80,7 +81,7 @@ inherited frmModelEditor: TfrmModelEditor Top = 200 Width = 415 Height = 13 - Caption = + Caption = 'The editor was unable to parse the source file. Details shown he' + 're are read-only.' end @@ -131,7 +132,7 @@ inherited frmModelEditor: TfrmModelEditor Left = 4 Top = 6 Width = 611 - Height = 167 + Height = 247 Margins.Left = 4 Margins.Top = 6 Margins.Right = 4 @@ -143,13 +144,14 @@ inherited frmModelEditor: TfrmModelEditor TabOrder = 0 DesignSize = ( 611 - 167) + 247) object lblFormat: TLabel Left = 12 Top = 30 Width = 36 Height = 13 Caption = '&Format' + FocusControl = cbFormat end object lblBasicInformation: TLabel AlignWithMargins = True @@ -174,7 +176,7 @@ inherited frmModelEditor: TfrmModelEditor end object lblComments: TLabel Left = 12 - Top = 83 + Top = 163 Width = 50 Height = 13 Caption = 'Comme&nts' @@ -185,8 +187,40 @@ inherited frmModelEditor: TfrmModelEditor Font.Style = [] ParentFont = False end + object lblInsertAfterWord: TLabel + Left = 12 + Top = 83 + Width = 86 + Height = 13 + Caption = '&Insert after word' + FocusControl = cbInsertAfterWord + end + object lblQuotationMarks: TLabel + Left = 12 + Top = 110 + Width = 86 + Height = 13 + Caption = '&Quotation marks' + FocusControl = cbOpenQuote + end + object lblOpenQuote: TLabel + Left = 106 + Top = 110 + Width = 27 + Height = 13 + Caption = 'open' + FocusControl = cbOpenQuote + end + object lblCloseQuote: TLabel + Left = 220 + Top = 110 + Width = 26 + Height = 13 + Caption = 'close' + FocusControl = cbCloseQuote + end object cbFormat: TComboBox - Left = 98 + Left = 106 Top = 26 Width = 145 Height = 21 @@ -198,7 +232,7 @@ inherited frmModelEditor: TfrmModelEditor 'Custom (custom-1.0)') end object cbWordBreaker: TComboBox - Left = 98 + Left = 106 Top = 53 Width = 145 Height = 21 @@ -211,15 +245,54 @@ inherited frmModelEditor: TfrmModelEditor 'custom') end object memoComments: TMemo - Left = 98 - Top = 80 - Width = 506 + Left = 106 + Top = 160 + Width = 498 Height = 87 Anchors = [akLeft, akTop, akRight] BevelOuter = bvNone - TabOrder = 2 + TabOrder = 6 OnChange = memoCommentsChange end + object cbInsertAfterWord: TComboBox + Left = 106 + Top = 80 + Width = 145 + Height = 21 + TabOrder = 2 + OnChange = cbInsertAfterWordClick + OnClick = cbInsertAfterWordClick + OnKeyUp = cbInsertAfterWordKeyUp + end + object cbOpenQuote: TComboBox + Left = 139 + Top = 107 + Width = 63 + Height = 21 + TabOrder = 3 + OnChange = cbOpenQuoteClick + OnClick = cbOpenQuoteClick + OnKeyUp = cbOpenQuoteKeyUp + end + object chkIsRTL: TCheckBox + Left = 106 + Top = 134 + Width = 111 + Height = 17 + Caption = 'Right-to-left script' + TabOrder = 5 + OnClick = chkIsRTLClick + end + object cbCloseQuote: TComboBox + Left = 252 + Top = 107 + Width = 63 + Height = 21 + TabOrder = 4 + OnChange = cbCloseQuoteClick + OnClick = cbCloseQuoteClick + OnKeyUp = cbCloseQuoteKeyUp + end end end end @@ -245,51 +318,19 @@ inherited frmModelEditor: TfrmModelEditor Top = 13 Width = 333 Height = 13 - Caption = + Caption = 'The keyboard must be compiled in order to distribute or install ' + 'it' end - object Label5: TLabel - Left = 10 - Top = 79 - Width = 82 - Height = 13 - Caption = 'Target filename:' - end - object cmdCompile: TButton - Left = 10 - Top = 40 - Width = 137 - Height = 25 - Action = modActionsModelEditor.actModelCompile - TabOrder = 0 - end - object cmdAddToProject: TButton - Left = 297 - Top = 40 - Width = 137 - Height = 25 - Action = modActionsMain.actProjectAddCurrentEditorFile - TabOrder = 1 - end - object cmdOpenContainingFolder2: TButton - Left = 153 - Top = 40 - Width = 138 - Height = 25 - Caption = '&Open Containing Folder' - TabOrder = 2 - OnClick = cmdOpenContainingFolder2Click - end object panBuildLexicalModel: TPanel Left = 10 - Top = 112 + Top = 235 Width = 471 Height = 353 BevelOuter = bvNone Color = 15921906 ParentBackground = False - TabOrder = 3 + TabOrder = 2 object lblDebugHostCaption: TLabel Left = 12 Top = 158 @@ -322,7 +363,7 @@ inherited frmModelEditor: TfrmModelEditor Top = 72 Width = 389 Height = 26 - Caption = + Caption = 'Optionally, select a compiled keyboard with which to test this l' + 'exical model. Any keyboards already loaded in the web debugger w' + 'ill also be available.' @@ -389,15 +430,111 @@ inherited frmModelEditor: TfrmModelEditor OnClick = cmdBrowseTestKeyboardClick end end - object editOutPath: TEdit - Left = 98 - Top = 76 - Width = 383 - Height = 21 - TabStop = False - ParentColor = True - ReadOnly = True - TabOrder = 4 + object panOpenInExplorer: TPanel + Left = 10 + Top = 148 + Width = 471 + Height = 67 + BevelOuter = bvNone + Color = 15921906 + ParentBackground = False + TabOrder = 1 + object lblOpenInExplorer: TLabel + Left = 9 + Top = 6 + Width = 115 + Height = 17 + Caption = 'Open in Explorer' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object cmdOpenSourceFolder: TButton + Left = 9 + Top = 33 + Width = 138 + Height = 25 + Caption = '&Open source folder' + TabOrder = 0 + OnClick = cmdOpenSourceFolderClick + end + object cmdOpenBuildFolder: TButton + Left = 153 + Top = 33 + Width = 138 + Height = 25 + Caption = 'Open &build folder' + TabOrder = 1 + OnClick = cmdOpenBuildFolderClick + end + object cmdOpenProjectFolder: TButton + Left = 297 + Top = 33 + Width = 138 + Height = 25 + Caption = 'Open pro&ject folder' + TabOrder = 2 + OnClick = cmdOpenProjectFolderClick + end + end + object panFileActions: TPanel + Left = 10 + Top = 32 + Width = 471 + Height = 97 + BevelOuter = bvNone + Color = 15921906 + ParentBackground = False + TabOrder = 0 + object lblFileActions: TLabel + Left = 9 + Top = 6 + Width = 75 + Height = 17 + Caption = 'File actions' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object Label5: TLabel + Left = 10 + Top = 71 + Width = 82 + Height = 13 + Caption = 'Target filename:' + end + object cmdAddToProject: TButton + Left = 153 + Top = 32 + Width = 137 + Height = 25 + Action = modActionsMain.actProjectAddCurrentEditorFile + TabOrder = 1 + end + object cmdCompile: TButton + Left = 10 + Top = 32 + Width = 137 + Height = 25 + Action = modActionsModelEditor.actModelCompile + TabOrder = 0 + end + object editOutPath: TEdit + Left = 98 + Top = 68 + Width = 359 + Height = 21 + TabStop = False + ParentColor = True + ReadOnly = True + TabOrder = 2 + end end end end diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas index 839abe1343..565d3f5a0d 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmModelEditor.pas @@ -1,4 +1,4 @@ -unit Keyman.Developer.UI.UfrmModelEditor; +unit Keyman.Developer.UI.UfrmModelEditor; interface @@ -49,9 +49,6 @@ type pageCompile: TTabSheet; Panel1: TPanel; lblCongrats: TLabel; - cmdCompile: TButton; - cmdAddToProject: TButton; - cmdOpenContainingFolder2: TButton; panBuildLexicalModel: TPanel; cbFormat: TComboBox; cbWordBreaker: TComboBox; @@ -71,9 +68,26 @@ type lblReadOnly: TLabel; dlgAddWordlist: TOpenDialog; dlgBrowseTestKeyboard: TOpenDialog; + imgQRCode: TImage; + lblInsertAfterWord: TLabel; + cbInsertAfterWord: TComboBox; + lblQuotationMarks: TLabel; + cbOpenQuote: TComboBox; + chkIsRTL: TCheckBox; + cbCloseQuote: TComboBox; + lblOpenQuote: TLabel; + lblCloseQuote: TLabel; + panOpenInExplorer: TPanel; + lblOpenInExplorer: TLabel; + cmdOpenSourceFolder: TButton; + cmdOpenBuildFolder: TButton; + cmdOpenProjectFolder: TButton; + panFileActions: TPanel; + lblFileActions: TLabel; + cmdAddToProject: TButton; + cmdCompile: TButton; Label5: TLabel; editOutPath: TEdit; - imgQRCode: TImage; procedure FormDestroy(Sender: TObject); procedure cmdAddWordlistClick(Sender: TObject); procedure cmdRemoveWordlistClick(Sender: TObject); @@ -83,12 +97,24 @@ type procedure cbFormatClick(Sender: TObject); procedure cbWordBreakerClick(Sender: TObject); procedure memoCommentsChange(Sender: TObject); - procedure cmdOpenContainingFolder2Click(Sender: TObject); procedure cmdOpenDebugHostClick(Sender: TObject); procedure cmdSendURLsToEmailClick(Sender: TObject); procedure cmdBrowseTestKeyboardClick(Sender: TObject); procedure editTestKeyboardChange(Sender: TObject); procedure lbDebugHostsClick(Sender: TObject); + procedure cbInsertAfterWordClick(Sender: TObject); + procedure cbInsertAfterWordKeyUp(Sender: TObject; var Key: Word; + Shift: TShiftState); + procedure cbOpenQuoteClick(Sender: TObject); + procedure cbOpenQuoteKeyUp(Sender: TObject; var Key: Word; + Shift: TShiftState); + procedure cbCloseQuoteClick(Sender: TObject); + procedure cbCloseQuoteKeyUp(Sender: TObject; var Key: Word; + Shift: TShiftState); + procedure chkIsRTLClick(Sender: TObject); + procedure cmdOpenSourceFolderClick(Sender: TObject); + procedure cmdOpenBuildFolderClick(Sender: TObject); + procedure cmdOpenProjectFolderClick(Sender: TObject); private type TWordlist = class @@ -118,7 +144,7 @@ type function CheckModifiedWordlistsForRemoval( newParser: TLexicalModelParser): Boolean; procedure UpdateQRCode; - procedure WordlistModifiedChanged(Sender: TObject); + procedure WordlistChanged(Sender: TObject); { Private declarations } protected function GetHelpTopic: string; override; @@ -161,19 +187,108 @@ begin Result := Ord(format) - 1; // unknown = -1 end; +function FormatFromIndex(index: Integer): TLexicalModelFormat; +begin + Result := TLexicalModelFormat(index+1); // unknown = -1 +end; + function WordBreakerToIndex(wordBreaker: TLexicalModelWordBreaker): Integer; begin Result := Ord(wordBreaker) - 1; // unknown = -1 end; -function FormatFromIndex(format: Integer): TLexicalModelFormat; +function WordBreakerFromIndex(index: Integer): TLexicalModelWordBreaker; begin - Result := TLexicalModelFormat(format+1); // unknown = -1 + Result := TLexicalModelWordBreaker(index+1); // unknown = -1 end; -function WordBreakerFromIndex(wordBreaker: Integer): TLexicalModelWordBreaker; +type + TComboStringOption = record + value, name: string; + class procedure FillCombo(const opts: array of TComboStringOption; + combo: TComboBox); static; + class function GetValue(const opts: array of TComboStringOption; + combo: TComboBox): string; static; + class procedure SetValue(const opts: array of TComboStringOption; + const text: string; combo: TComboBox); static; + end; + +const + CInsertAfterWordOptions: array[0..4] of TComboStringOption = ( + (value: ' '; name: '(Space U+0020)'), + (value: ''; name: '(No word break)'), + (value: Char($200B); name: '(Zero width space U+200B)'), + (value: Char($0F0B); name: '(Tibetan tsheg U+0F0B)'), + (value: Char($1361); name: '(Ethiopian wordspace U+1361)') + ); + + COpenQuoteOptions: array[0..11] of TComboStringOption = ( + (value: TLexicalModelParser.CDefaultOpenQuote), + (value: '«'), + (value: '„'), + (value: '»'), + (value: '"'), + (value: ''''), + (value: '‹'), + (value: '‘'), + (value: '‚'), + (value: '›'), + (value: '「'), + (value: '『') + ); + + CCloseQuoteOptions: array[0..10] of TComboStringOption = ( + (value: TLexicalModelParser.CDefaultCloseQuote), + (value: '»'), + (value: '“'), + (value: '«'), + (value: '"'), + (value: ''''), + (value: '›'), + (value: '’'), + (value: '‹'), + (value: '」'), + (value: '』') + ); + +class procedure TComboStringOption.SetValue(const opts: array of TComboStringOption; const text: string; + combo: TComboBox); +var + index: Integer; begin - Result := TLexicalModelWordBreaker(wordBreaker+1); // unknown = -1 + for index := 0 to High(opts) do + if opts[index].value = text then + begin + combo.ItemIndex := index; + Exit; + end; + combo.Text := text; +end; + +class function TComboStringOption.GetValue(const opts: array of TComboStringOption; + combo: TComboBox): string; +begin + if (combo.ItemIndex >= 0) and (combo.ItemIndex <= High(opts)) + then Result := opts[combo.ItemIndex].value + else Result := combo.Text; +end; + +class procedure TComboStringOption.FillCombo(const opts: array of TComboStringOption; combo: TComboBox); +var + opt: TComboStringOption; +begin + combo.Items.BeginUpdate; + try + combo.Items.Clear; + for opt in opts do + begin + if opt.name = '' + then combo.Items.Add(opt.value) + else combo.Items.Add(opt.name); + end; + finally + combo.Items.EndUpdate; + end; end; { TfrmModelEditor } @@ -269,6 +384,16 @@ begin lblWordBreaker.Enabled := e; cbWordBreaker.Enabled := e; lblComments.Enabled := e; + + lblInsertAfterWord.Enabled := e; + cbInsertAfterWord.Enabled := e; + lblQuotationMarks.Enabled := e; + lblOpenQuote.Enabled := e; + cbOpenQuote.Enabled := e; + lblCloseQuote.Enabled := e; + cbCloseQuote.Enabled := e; + chkIsRTL.Enabled := e; + memoComments.Enabled := e; cmdAddWordlist.Enabled := e; gridWordlists.Enabled := e and (parser.Wordlists.Count > 0); @@ -278,6 +403,12 @@ begin { Build tab } cmdOpenDebugHost.Enabled := lbDebugHosts.ItemIndex >= 0; cmdSendURLsToEmail.Enabled := lbDebugHosts.Items.Count > 0; // I4506 + + // We use FProjectFile because we don't want to accidentally create a standalone + // project file as GetProjectFile is side-effecty. EnableControls is called early + // in construction before FProjectFile is assigned. It is called again later so + // enabled state will be correct. + cmdOpenProjectFolder.Enabled := Assigned(FProjectFile) and Assigned(FProjectFile.Project); end; procedure TfrmModelEditor.NotifyStartedWebDebug; @@ -311,6 +442,10 @@ begin frameSource.OnChanged := SourceChanged; frameSource.TextFileFormat := tffUTF8; + TComboStringOption.FillCombo(CInsertAfterWordOptions, cbInsertAfterWord); + TComboStringOption.FillCombo(COpenQuoteOptions, cbOpenQuote); + TComboStringOption.FillCombo(CCloseQuoteOptions, cbCloseQuote); + pages.ActivePage := pageDetails; finally Dec(FSetup); @@ -459,6 +594,11 @@ var begin cbFormat.ItemIndex := FormatToIndex(parser.Format); cbWordBreaker.ItemIndex := WordBreakerToIndex(parser.WordBreaker); + TComboStringOption.SetValue(CInsertAfterWordOptions, parser.InsertAfterWord, cbInsertAfterWord); + TComboStringOption.SetValue(COpenQuoteOptions, parser.OpenQuote, cbOpenQuote); + TComboStringOption.SetValue(CCloseQuoteOptions, parser.CloseQuote, cbCloseQuote); + chkIsRTL.Checked := parser.IsRTL; + memoComments.Text := parser.Comment; if parser.Wordlists.Count = 0 then @@ -521,13 +661,12 @@ begin Result.Tab.PageControl := pages; Result.Frame := TframeWordlistEditor.Create(Self); + Result.Frame.OnChanged := WordlistChanged; Result.Frame.LoadFromFile(ExtractFilePath(Filename) + WordlistFilename); Result.Frame.Align := alClient; Result.Frame.Parent := Result.Tab; Result.Frame.Visible := True; - - Result.Frame.OnModifiedChanged := WordlistModifiedChanged; end; function TfrmModelEditor.WordlistFromTab( @@ -540,7 +679,7 @@ begin Result := nil; end; -procedure TfrmModelEditor.WordlistModifiedChanged(Sender: TObject); +procedure TfrmModelEditor.WordlistChanged(Sender: TObject); begin // We only go from clean to dirty, not vice-versa, on this notification if (Sender as TframeWordlistEditor).Modified then @@ -570,6 +709,20 @@ begin Result := WordlistFromTab(pages.ActivePage) <> nil; end; +procedure TfrmModelEditor.cbCloseQuoteClick(Sender: TObject); +begin + if FSetup > 0 then + Exit; + parser.CloseQuote := TComboStringOption.GetValue(CCloseQuoteOptions, cbCloseQuote); + Modified := True; +end; + +procedure TfrmModelEditor.cbCloseQuoteKeyUp(Sender: TObject; var Key: Word; + Shift: TShiftState); +begin + cbCloseQuoteClick(Sender); +end; + procedure TfrmModelEditor.cbFormatClick(Sender: TObject); begin if FSetup > 0 then @@ -578,6 +731,34 @@ begin Modified := True; end; +procedure TfrmModelEditor.cbInsertAfterWordClick(Sender: TObject); +begin + if FSetup > 0 then + Exit; + parser.InsertAfterWord := TComboStringOption.GetValue(CInsertAfterWordOptions, cbInsertAfterWord); + Modified := True; +end; + +procedure TfrmModelEditor.cbInsertAfterWordKeyUp(Sender: TObject; var Key: Word; + Shift: TShiftState); +begin + cbInsertAfterWordClick(Sender); +end; + +procedure TfrmModelEditor.cbOpenQuoteClick(Sender: TObject); +begin + if FSetup > 0 then + Exit; + parser.OpenQuote := TComboStringOption.GetValue(COpenQuoteOptions, cbOpenQuote); + Modified := True; +end; + +procedure TfrmModelEditor.cbOpenQuoteKeyUp(Sender: TObject; var Key: Word; + Shift: TShiftState); +begin + cbOpenQuoteClick(cbOpenQuote); +end; + procedure TfrmModelEditor.cbWordBreakerClick(Sender: TObject); begin if FSetup > 0 then @@ -586,6 +767,14 @@ begin Modified := True; end; +procedure TfrmModelEditor.chkIsRTLClick(Sender: TObject); +begin + if FSetup > 0 then + Exit; + parser.IsRTL := chkIsRTL.Checked; + Modified := True; +end; + procedure TfrmModelEditor.cmdAddWordlistClick(Sender: TObject); begin if dlgAddWordlist.Execute then @@ -608,14 +797,25 @@ begin editTestKeyboard.Text := dlgBrowseTestKeyboard.FileName; end; -procedure TfrmModelEditor.cmdOpenContainingFolder2Click(Sender: TObject); +procedure TfrmModelEditor.cmdOpenDebugHostClick(Sender: TObject); +begin + TUtilExecute.URL(lbDebugHosts.Items[lbDebugHosts.ItemIndex]); +end; + +procedure TfrmModelEditor.cmdOpenSourceFolderClick(Sender: TObject); begin OpenContainingFolder(FileName); end; -procedure TfrmModelEditor.cmdOpenDebugHostClick(Sender: TObject); +procedure TfrmModelEditor.cmdOpenBuildFolderClick(Sender: TObject); begin - TUtilExecute.URL(lbDebugHosts.Items[lbDebugHosts.ItemIndex]); + OpenContainingFolder((ProjectFile as TmodeltsProjectFile).TargetFilename); +end; + +procedure TfrmModelEditor.cmdOpenProjectFolderClick(Sender: TObject); +begin + if Assigned(ProjectFile.Project) then + OpenContainingFolder(ProjectFile.Project.FileName); end; procedure TfrmModelEditor.cmdRemoveWordlistClick(Sender: TObject); diff --git a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas index b1795f6069..0642b635bc 100644 --- a/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas +++ b/windows/src/developer/TIKE/child/Keyman.Developer.UI.UfrmWordlistEditor.pas @@ -53,7 +53,7 @@ begin frame := TframeWordlistEditor.Create(Self); frame.Align := alClient; frame.Parent := Self; - frame.OnModifiedChanged := FrameModified; + frame.OnChanged := FrameModified; end; procedure TfrmWordlistEditor.FormDestroy(Sender: TObject); diff --git a/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm b/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm index d785ba874a..b8cb54c560 100644 --- a/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm +++ b/windows/src/developer/TIKE/child/UfrmKeymanWizard.dfm @@ -177,7 +177,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard Top = 0 Width = 1043 Height = 645 - ActivePage = pageTouchLayout + ActivePage = pageDetails Align = alClient Font.Charset = ANSI_CHARSET Font.Color = clWindowText @@ -698,7 +698,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard Top = 22 Width = 482 Height = 13 - Caption = + Caption = 'In Keyman 10, language metadata should now be managed in the pac' + 'kage, not the keyboard.' end @@ -721,9 +721,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayout: TTabSheet Caption = 'Layout' ImageIndex = 5 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object pagesLayout: TPageControl Left = 0 Top = 0 @@ -738,10 +735,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayoutDesign: TTabSheet Caption = 'Design' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object panLayoutSimple: TPanel Left = 0 Top = 0 @@ -965,10 +958,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageLayoutCode: TTabSheet Caption = 'Code' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end end end @@ -1015,9 +1004,6 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageOnScreenKeyboard: TTabSheet Caption = 'On-Screen' ImageIndex = 7 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageTouchLayout: TTabSheet Caption = 'Touch Layout' @@ -1040,47 +1026,28 @@ inherited frmKeymanWizard: TfrmKeymanWizard object pageTouchLayoutCode: TTabSheet Caption = 'Code' ImageIndex = -1 - ExplicitLeft = 0 - ExplicitTop = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end end end object pageIncludeCodes: TTabSheet Caption = 'Char Codes' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageKMWEmbedJS: TTabSheet Caption = 'Embedded JS' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageKMWEmbedCSS: TTabSheet Caption = 'Embedded CSS' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageKMWHelp: TTabSheet Caption = 'Embedded Help' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Build' ImageIndex = 1 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 object Panel1: TPanel Left = 0 Top = 0 @@ -1096,52 +1063,19 @@ inherited frmKeymanWizard: TfrmKeymanWizard Top = 13 Width = 333 Height = 13 - Caption = + Caption = 'The keyboard must be compiled in order to distribute or install ' + 'it' end - object cmdCompile: TButton - Left = 10 - Top = 40 - Width = 137 - Height = 25 - Action = modActionsKeyboardEditor.actKeyboardCompile - TabOrder = 0 - end - object cmdStartDebugging: TButton - Left = 153 - Top = 40 - Width = 137 - Height = 25 - Action = modActionsKeyboardEditor.actDebugStartDebugger - TabOrder = 1 - end - object cmdAddToProject: TButton - Left = 442 - Top = 40 - Width = 137 - Height = 25 - Action = modActionsMain.actProjectAddCurrentEditorFile - TabOrder = 2 - end - object cmdOpenContainingFolder2: TButton - Left = 298 - Top = 40 - Width = 138 - Height = 25 - Caption = '&Open Containing Folder' - TabOrder = 3 - OnClick = cmdOpenContainingFolder2Click - end object panBuildWindows: TPanel Left = 10 - Top = 96 + Top = 202 Width = 295 Height = 265 BevelOuter = bvNone Color = 15921906 ParentBackground = False - TabOrder = 4 + TabOrder = 2 object lblInstallHint: TLabel Left = 9 Top = 39 @@ -1189,13 +1123,13 @@ inherited frmKeymanWizard: TfrmKeymanWizard end object panBuildKMW: TPanel Left = 323 - Top = 96 + Top = 202 Width = 454 Height = 265 BevelOuter = bvNone Color = 15921906 ParentBackground = False - TabOrder = 5 + TabOrder = 3 object lblDebugHostCaption: TLabel Left = 12 Top = 70 @@ -1260,6 +1194,103 @@ inherited frmKeymanWizard: TfrmKeymanWizard OnClick = cmdSendURLsToEmailClick end end + object panOpenInExplorer: TPanel + Left = 10 + Top = 117 + Width = 767 + Height = 67 + BevelOuter = bvNone + Color = 15921906 + ParentBackground = False + TabOrder = 1 + object lblOpenInExplorer: TLabel + Left = 9 + Top = 6 + Width = 115 + Height = 17 + Caption = 'Open in Explorer' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object cmdOpenSourceFolder: TButton + Left = 9 + Top = 33 + Width = 138 + Height = 25 + Caption = '&Open source folder' + TabOrder = 0 + OnClick = cmdOpenSourceFolderClick + end + object cmdOpenBuildFolder: TButton + Left = 153 + Top = 33 + Width = 138 + Height = 25 + Caption = 'Open &build folder' + TabOrder = 1 + OnClick = cmdOpenBuildFolderClick + end + object cmdOpenProjectFolder: TButton + Left = 297 + Top = 33 + Width = 138 + Height = 25 + Caption = 'Open pro&ject folder' + TabOrder = 2 + OnClick = cmdOpenProjectFolderClick + end + end + object panFileActions: TPanel + Left = 10 + Top = 32 + Width = 767 + Height = 67 + BevelOuter = bvNone + Color = 15921906 + ParentBackground = False + TabOrder = 0 + object lblFileActions: TLabel + Left = 9 + Top = 6 + Width = 75 + Height = 17 + Caption = 'File actions' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object cmdAddToProject: TButton + Left = 295 + Top = 33 + Width = 137 + Height = 25 + Action = modActionsMain.actProjectAddCurrentEditorFile + TabOrder = 2 + end + object cmdStartDebugging: TButton + Left = 152 + Top = 33 + Width = 137 + Height = 25 + Action = modActionsKeyboardEditor.actDebugStartDebugger + TabOrder = 1 + end + object cmdCompile: TButton + Left = 9 + Top = 33 + Width = 137 + Height = 25 + Action = modActionsKeyboardEditor.actKeyboardCompile + TabOrder = 0 + end + end end end end @@ -1300,7 +1331,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard end object dlgBrowseBitmap: TOpenPictureDialog DefaultExt = 'ico' - Filter = + Filter = 'All supported files (*.ico, *.bmp)|*.ico;*.bmp|Icon files (*.ico' + ')|*.ico|Bitmap files (*.bmp)|*.bmp|All files (*.*)|*.*' Options = [ofHideReadOnly, ofCreatePrompt, ofEnableSizing] @@ -1310,7 +1341,7 @@ inherited frmKeymanWizard: TfrmKeymanWizard end object dlgSaveExport: TSaveDialog DefaultExt = 'kmn' - Filter = + Filter = 'Keyman 5.0 Keyboard Wizard (*.kmn)|*.kmn|Windows NT/2000/XP keyb' + 'oard (*.dll)|*.dll|Windows 95/98/Me keyboard (*.kbd)|*.kbd' Options = [ofOverwritePrompt, ofHideReadOnly, ofPathMustExist, ofEnableSizing] diff --git a/windows/src/developer/TIKE/child/UfrmKeymanWizard.pas b/windows/src/developer/TIKE/child/UfrmKeymanWizard.pas index 5b9ab1d1c0..4b9041a29e 100644 --- a/windows/src/developer/TIKE/child/UfrmKeymanWizard.pas +++ b/windows/src/developer/TIKE/child/UfrmKeymanWizard.pas @@ -236,10 +236,6 @@ type cmdISOLang_Lookup: TButton; Panel1: TPanel; lblCongrats: TLabel; - cmdCompile: TButton; - cmdStartDebugging: TButton; - cmdAddToProject: TButton; - cmdOpenContainingFolder2: TButton; panBuildWindows: TPanel; lblInstallHint: TLabel; lblCompileTargetHeader: TLabel; @@ -267,6 +263,16 @@ type lblLanguageKeyman10Note: TLabel; lblLanguageKeyman10Title: TLabel; imgQRCode: TImage; + panOpenInExplorer: TPanel; + lblOpenInExplorer: TLabel; + cmdOpenSourceFolder: TButton; + cmdOpenBuildFolder: TButton; + cmdOpenProjectFolder: TButton; + panFileActions: TPanel; + lblFileActions: TLabel; + cmdAddToProject: TButton; + cmdStartDebugging: TButton; + cmdCompile: TButton; procedure FormCreate(Sender: TObject); procedure editNameChange(Sender: TObject); procedure editCopyrightChange(Sender: TObject); @@ -294,7 +300,7 @@ type Shift: TShiftState); procedure chkLayoutDisplay102KeyClick(Sender: TObject); procedure cmdInsertCopyrightClick(Sender: TObject); - procedure cmdOpenContainingFolder2Click(Sender: TObject); + procedure cmdOpenSourceFolderClick(Sender: TObject); procedure editKeyOutputCodeClick(Sender: TObject); procedure editKeyOutputTextClick(Sender: TObject); procedure tmrUpdateCharacterMapTimer(Sender: TObject); @@ -332,6 +338,8 @@ type procedure pagesTouchLayoutChanging(Sender: TObject; var AllowChange: Boolean); procedure lbDebugHostsClick(Sender: TObject); + procedure cmdOpenBuildFolderClick(Sender: TObject); + procedure cmdOpenProjectFolderClick(Sender: TObject); private frameSource: TframeTextEditor; @@ -738,6 +746,8 @@ begin gridFeatures.Enabled := gridFeatures.RowCount > 1; // I4587 // I4427 cmdEditFeature.Enabled := gridFeatures.RowCount > 1; // I4587 // I4427 cmdRemoveFeature.Enabled := gridFeatures.RowCount > 1; // I4587 // I4427 + + cmdOpenProjectFolder.Enabled := Assigned(ProjectFile.Project); end; procedure TfrmKeymanWizard.FocusTab; @@ -2754,11 +2764,22 @@ begin editCopyright.SetFocus; end; -procedure TfrmKeymanWizard.cmdOpenContainingFolder2Click(Sender: TObject); +procedure TfrmKeymanWizard.cmdOpenSourceFolderClick(Sender: TObject); begin OpenContainingFolder(FileName); end; +procedure TfrmKeymanWizard.cmdOpenBuildFolderClick(Sender: TObject); +begin + OpenContainingFolder((ProjectFile as TkmnProjectFile).TargetFilename); +end; + +procedure TfrmKeymanWizard.cmdOpenProjectFolderClick(Sender: TObject); +begin + if Assigned(ProjectFile.Project) then + OpenContainingFolder(ProjectFile.Project.FileName); +end; + procedure TfrmKeymanWizard.cmdOpenDebugHostClick(Sender: TObject); begin TUtilExecute.URL(lbDebugHosts.Items[lbDebugHosts.ItemIndex]); diff --git a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm index e3129b35ce..36134ab3ff 100644 --- a/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm +++ b/windows/src/developer/TIKE/child/UfrmPackageEditor.dfm @@ -1119,9 +1119,6 @@ inherited frmPackageEditor: TfrmPackageEditor object pageSource: TTabSheet Caption = 'Source' ImageIndex = 9 - ExplicitLeft = 0 - ExplicitWidth = 0 - ExplicitHeight = 0 end object pageCompile: TTabSheet Caption = 'Compile' @@ -1158,67 +1155,15 @@ inherited frmPackageEditor: TfrmPackageEditor Font.Style = [fsBold] ParentFont = False end - object Label5: TLabel - Left = 24 - Top = 121 - Width = 82 - Height = 13 - Caption = 'Target filename:' - end - object cmdBuildPackage: TButton - Left = 15 - Top = 80 - Width = 133 - Height = 25 - Caption = 'Compile &Package' - TabOrder = 0 - OnClick = cmdBuildPackageClick - end - object editOutPath: TEdit - Left = 111 - Top = 118 - Width = 383 - Height = 21 - TabStop = False - ParentColor = True - ReadOnly = True - TabOrder = 4 - end - object cmdOpenContainingFolder2: TButton - Left = 293 - Top = 80 - Width = 133 - Height = 25 - Caption = '&Open Containing Folder' - TabOrder = 2 - OnClick = cmdOpenContainingFolder2Click - end - object cmdAddToProject: TButton - Left = 432 - Top = 80 - Width = 133 - Height = 25 - Action = modActionsMain.actProjectAddCurrentEditorFile - TabOrder = 3 - end - object cmdCompileInstaller: TButton - Left = 154 - Top = 80 - Width = 133 - Height = 25 - Caption = 'Compile I&nstaller' - TabOrder = 1 - OnClick = cmdCompileInstallerClick - end object panBuildMobile: TPanel Left = 328 - Top = 162 + Top = 274 Width = 457 - Height = 265 + Height = 276 BevelOuter = bvNone Color = 15921906 ParentBackground = False - TabOrder = 5 + TabOrder = 4 object lblDebugHostCaption: TLabel Left = 12 Top = 70 @@ -1241,7 +1186,7 @@ inherited frmPackageEditor: TfrmPackageEditor end object imgQRCode: TImage Left = 315 - Top = 64 + Top = 96 Width = 128 Height = 128 Proportional = True @@ -1258,7 +1203,7 @@ inherited frmPackageEditor: TfrmPackageEditor end object cmdOpenDebugHost: TButton Left = 12 - Top = 198 + Top = 240 Width = 221 Height = 25 Caption = 'Open packages page in local &browser' @@ -1267,9 +1212,9 @@ inherited frmPackageEditor: TfrmPackageEditor end object lbDebugHosts: TListBox Left = 12 - Top = 95 + Top = 89 Width = 289 - Height = 97 + Height = 142 ItemHeight = 13 TabOrder = 1 OnClick = lbDebugHostsClick @@ -1277,26 +1222,26 @@ inherited frmPackageEditor: TfrmPackageEditor end object panBuildDesktop: TPanel Left = 15 - Top = 162 + Top = 274 Width = 295 Height = 124 BevelOuter = bvNone Color = 15921906 ParentBackground = False - TabOrder = 6 + TabOrder = 2 object Label4: TLabel Left = 9 Top = 35 - Width = 254 + Width = 236 Height = 13 - Caption = 'You can install the package into Keyman:' + Caption = 'Install the package into Keyman for Windows:' end object lblCompileTargetHeader: TLabel Left = 9 Top = 6 - Width = 202 + Width = 247 Height = 17 - Caption = 'Windows and macOS Targets' + Caption = 'Windows, Linux and macOS Targets' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -14 @@ -1327,13 +1272,13 @@ inherited frmPackageEditor: TfrmPackageEditor end object panBuildWindowsInstaller: TPanel Left = 15 - Top = 295 + Top = 418 Width = 295 Height = 132 BevelOuter = bvNone Color = 15921906 ParentBackground = False - TabOrder = 7 + TabOrder = 3 object Label9: TLabel Left = 9 Top = 6 @@ -1350,7 +1295,7 @@ inherited frmPackageEditor: TfrmPackageEditor object lblBootstrapMSI: TLabel Left = 9 Top = 69 - Width = 110 + Width = 64 Height = 13 Caption = 'Keyman MSI:' FocusControl = editBootstrapMSI @@ -1393,6 +1338,122 @@ inherited frmPackageEditor: TfrmPackageEditor OnClick = cmdInstallWithClick end end + object panOpenInExplorer: TPanel + Left = 15 + Top = 187 + Width = 767 + Height = 67 + BevelOuter = bvNone + Color = 15921906 + ParentBackground = False + TabOrder = 1 + object lblOpenInExplorer: TLabel + Left = 9 + Top = 6 + Width = 115 + Height = 17 + Caption = 'Open in Explorer' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object cmdOpenSourceFolder: TButton + Left = 9 + Top = 33 + Width = 138 + Height = 25 + Caption = '&Open source folder' + TabOrder = 0 + OnClick = cmdOpenSourceFolderClick + end + object cmdOpenBuildFolder: TButton + Left = 153 + Top = 33 + Width = 138 + Height = 25 + Caption = 'Open &build folder' + TabOrder = 1 + OnClick = cmdOpenBuildFolderClick + end + object cmdOpenProjectFolder: TButton + Left = 297 + Top = 33 + Width = 138 + Height = 25 + Caption = 'Open pro&ject folder' + TabOrder = 2 + OnClick = cmdOpenProjectFolderClick + end + end + object panFileActions: TPanel + Left = 15 + Top = 67 + Width = 767 + Height = 102 + BevelOuter = bvNone + Color = 15921906 + ParentBackground = False + TabOrder = 0 + object lblFileActions: TLabel + Left = 9 + Top = 6 + Width = 75 + Height = 17 + Caption = 'File actions' + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + end + object Label5: TLabel + Left = 9 + Top = 73 + Width = 82 + Height = 13 + Caption = 'Target filename:' + end + object editOutPath: TEdit + Left = 96 + Top = 70 + Width = 383 + Height = 21 + TabStop = False + ParentColor = True + ReadOnly = True + TabOrder = 0 + end + object cmdCompileInstaller: TButton + Left = 148 + Top = 32 + Width = 133 + Height = 25 + Caption = 'Compile I&nstaller' + TabOrder = 1 + OnClick = cmdCompileInstallerClick + end + object cmdAddToProject: TButton + Left = 287 + Top = 32 + Width = 133 + Height = 25 + Action = modActionsMain.actProjectAddCurrentEditorFile + TabOrder = 2 + end + object cmdBuildPackage: TButton + Left = 9 + Top = 32 + Width = 133 + Height = 25 + Caption = 'Compile &Package' + TabOrder = 3 + OnClick = cmdBuildPackageClick + end + end end end end diff --git a/windows/src/developer/TIKE/child/UfrmPackageEditor.pas b/windows/src/developer/TIKE/child/UfrmPackageEditor.pas index ffdd6c8e49..0889260205 100644 --- a/windows/src/developer/TIKE/child/UfrmPackageEditor.pas +++ b/windows/src/developer/TIKE/child/UfrmPackageEditor.pas @@ -135,11 +135,6 @@ type Panel4: TPanel; Label13: TLabel; lblCompilePackage: TLabel; - cmdBuildPackage: TButton; - editOutPath: TEdit; - cmdOpenContainingFolder2: TButton; - cmdAddToProject: TButton; - cmdCompileInstaller: TButton; pageKeyboards: TTabSheet; Panel5: TPanel; Label2: TLabel; @@ -174,7 +169,6 @@ type lblCompileTargetHeader: TLabel; cmdInstall: TButton; cmdUninstall: TButton; - Label5: TLabel; panBuildWindowsInstaller: TPanel; Label9: TLabel; lblBootstrapMSI: TLabel; @@ -198,6 +192,18 @@ type chkLexicalModelRTL: TCheckBox; editLexicalModelFilename: TEdit; imgQRCode: TImage; + panOpenInExplorer: TPanel; + lblOpenInExplorer: TLabel; + cmdOpenSourceFolder: TButton; + cmdOpenBuildFolder: TButton; + cmdOpenProjectFolder: TButton; + panFileActions: TPanel; + lblFileActions: TLabel; + editOutPath: TEdit; + cmdCompileInstaller: TButton; + cmdAddToProject: TButton; + cmdBuildPackage: TButton; + Label5: TLabel; procedure cmdCloseClick(Sender: TObject); procedure cmdAddFileClick(Sender: TObject); procedure cmdRemoveFileClick(Sender: TObject); @@ -226,7 +232,6 @@ type procedure editStartMenuPathChange(Sender: TObject); procedure cbKMPImageFileClick(Sender: TObject); procedure cmdUninstallClick(Sender: TObject); - procedure cmdOpenContainingFolder2Click(Sender: TObject); procedure cmdOpenContainingFolderClick(Sender: TObject); procedure cmdOpenFileClick(Sender: TObject); procedure cmdCompileInstallerClick(Sender: TObject); @@ -256,6 +261,9 @@ type procedure chkLexicalModelRTLClick(Sender: TObject); procedure editLexicalModelDescriptionChange(Sender: TObject); procedure lbDebugHostsClick(Sender: TObject); + procedure cmdOpenSourceFolderClick(Sender: TObject); + procedure cmdOpenBuildFolderClick(Sender: TObject); + procedure cmdOpenProjectFolderClick(Sender: TObject); private pack: TKPSFile; FSetup: Integer; @@ -1043,16 +1051,6 @@ begin lbStartMenuEntriesClick(lbStartMenuEntries); end; -procedure TfrmPackageEditor.cmdOpenContainingFolder2Click(Sender: TObject); -begin - OpenContainingFolder(FileName); -end; - -procedure TfrmPackageEditor.cmdOpenContainingFolderClick(Sender: TObject); -begin - OpenContainingFolder((lbFiles.Items.Objects[lbFiles.ItemIndex] as TPackageContentFile).FileName); -end; - procedure TfrmPackageEditor.cmdOpenDebugHostClick(Sender: TObject); begin TUtilExecute.URL(lbDebugHosts.Items[lbDebugHosts.ItemIndex] + '/packages.html'); @@ -1083,6 +1081,27 @@ begin end; end; +procedure TfrmPackageEditor.cmdOpenSourceFolderClick(Sender: TObject); +begin + OpenContainingFolder(FileName); +end; + +procedure TfrmPackageEditor.cmdOpenBuildFolderClick(Sender: TObject); +begin + OpenContainingFolder((ProjectFile as TkpsProjectFile).TargetFilename); +end; + +procedure TfrmPackageEditor.cmdOpenProjectFolderClick(Sender: TObject); +begin + if Assigned(ProjectFile.Project) then + OpenContainingFolder(ProjectFile.Project.FileName); +end; + +procedure TfrmPackageEditor.cmdOpenContainingFolderClick(Sender: TObject); +begin + OpenContainingFolder((lbFiles.Items.Objects[lbFiles.ItemIndex] as TPackageContentFile).FileName); +end; + procedure TfrmPackageEditor.cmdDeleteStartMenuEntryClick(Sender: TObject); var n: Integer; @@ -1195,17 +1214,15 @@ begin for i := 0 to pack.Info.Count - 1 do UpdateInfo(pack.Info[i].Name, pack.Info[i].Description, pack.Info[i].URL); chkFollowKeyboardVersion.Checked := pack.KPSOptions.FollowKeyboardVersion; - EnableDetailsTabControls; lbStartMenuEntries.Clear; - + for i := 0 to pack.StartMenu.Entries.Count - 1 do lbStartMenuEntries.Items.AddObject(pack.StartMenu.Entries[i].Name, pack.StartMenu.Entries[i]); chkCreateStartMenu.Checked := pack.StartMenu.DoCreate; chkStartMenuUninstall.Checked := pack.StartMenu.AddUninstallEntry; editStartMenuPath.Text := pack.StartMenu.Path; - EnableStartMenuControls; editOutPath.Text := (ProjectFile as TkpsProjectFile).TargetFilename; // I4688 editBootstrapMSI.Text := pack.KPSOptions.MSIFileName; @@ -1220,6 +1237,8 @@ begin RefreshKeyboardList; RefreshLexicalModelList; + + EnableControls; finally Dec(FSetup); end; @@ -1547,6 +1566,12 @@ end; procedure TfrmPackageEditor.EnableCompileTabControls; begin cmdOpenDebugHost.Enabled := lbDebugHosts.ItemIndex >= 0; + + // We use FProjectFile because we don't want to accidentally create a standalone + // project file as GetProjectFile is side-effecty. EnableControls is called early + // in construction before FProjectFile is assigned. It is called again later so + // enabled state will be correct. + cmdOpenProjectFolder.Enabled := Assigned(FProjectFile) and Assigned(FProjectFile.Project); end; procedure TfrmPackageEditor.EnableDetailsTabControls; diff --git a/windows/src/developer/TIKE/dialogs/UfrmKeyboardFonts.dfm b/windows/src/developer/TIKE/dialogs/UfrmKeyboardFonts.dfm index 499ed64986..bd8ba4de5b 100644 --- a/windows/src/developer/TIKE/dialogs/UfrmKeyboardFonts.dfm +++ b/windows/src/developer/TIKE/dialogs/UfrmKeyboardFonts.dfm @@ -166,6 +166,9 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts PreviewWidth = 240 PreviewHeight = 60 FontTypes = [ftTrueTypeAnsi, ftTrueTypeSymbol] + ShowPreview = False + ShowPreviewFontName = False + ShowPreviewInList = False end object editCodeSize: TEdit Left = 391 @@ -193,6 +196,9 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts PreviewWidth = 240 PreviewHeight = 60 FontTypes = [ftTrueTypeAnsi, ftTrueTypeSymbol] + ShowPreview = False + ShowPreviewFontName = False + ShowPreviewInList = False end object editCharSize: TEdit Left = 391 @@ -220,6 +226,9 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts PreviewWidth = 240 PreviewHeight = 60 FontTypes = [ftTrueTypeAnsi, ftTrueTypeSymbol] + ShowPreview = False + ShowPreviewFontName = False + ShowPreviewInList = False end object editOSKSize: TEdit Left = 391 @@ -288,6 +297,9 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts PreviewWidth = 240 PreviewHeight = 60 FontTypes = [ftTrueTypeAnsi, ftTrueTypeSymbol] + ShowPreview = False + ShowPreviewFontName = False + ShowPreviewInList = False end object fcbTouchLayoutTablet: TscFontComboBox Left = 176 @@ -308,6 +320,9 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts PreviewWidth = 240 PreviewHeight = 60 FontTypes = [ftTrueTypeAnsi, ftTrueTypeSymbol] + ShowPreview = False + ShowPreviewFontName = False + ShowPreviewInList = False end object fcbTouchLayoutDesktop: TscFontComboBox Left = 176 @@ -328,5 +343,8 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts PreviewWidth = 240 PreviewHeight = 60 FontTypes = [ftTrueTypeAnsi, ftTrueTypeSymbol] + ShowPreview = False + ShowPreviewFontName = False + ShowPreviewInList = False end end diff --git a/windows/src/developer/TIKE/dialogs/UfrmVisualKeyboardImportKMX.pas b/windows/src/developer/TIKE/dialogs/UfrmVisualKeyboardImportKMX.pas index 09a63d6eeb..6c3eb9d54c 100644 --- a/windows/src/developer/TIKE/dialogs/UfrmVisualKeyboardImportKMX.pas +++ b/windows/src/developer/TIKE/dialogs/UfrmVisualKeyboardImportKMX.pas @@ -355,19 +355,14 @@ var vk: TVKKey; n: Integer; begin -// lb.items.Add('AddKey: ENTER'); - if nkey >= keys.count then begin -// lb.items.Add('AddKey: EXIT: bug!'); - exit; + Exit; end; -// lb.items.Add(Format('AddKey: nkey: %d data: "%s" vk: %s ', [nkey, data, VKeyNames[TVKKey(keys[nkey]).vkey]])); - if (data <> '') then //and (nkey < keys.Count) then + if Trim(data) <> '' then begin vk := TVKKey(keys[nkey]); -// ShowMessage(Format('key: %d shift: %d text: "%s"', [vk.vkey, vk.shift, data])); n := FVK.Keys.IndexOf(vk.vkey, vk.shift); if n < 0 then k := TVisualKeyboardKey.Create @@ -379,11 +374,9 @@ begin if n < 0 then FVK.Keys.Add(k); end; -// data := ''; Inc(nkey); PostMessage(Handle, WM_User_SendNextKey, 0, 0); // I4156 -// lb.items.Add('AddKey: Exit (nkey now='+IntToStr(nkey)); end; {------------------------------------------------------------------------------- diff --git a/windows/src/developer/TIKE/help/kwhelp.pas b/windows/src/developer/TIKE/help/kwhelp.pas index 35837b1095..5e9684fb27 100644 --- a/windows/src/developer/TIKE/help/kwhelp.pas +++ b/windows/src/developer/TIKE/help/kwhelp.pas @@ -41,7 +41,7 @@ type end; const - HelpArray: array[0..73] of THelpArray = ( // I4840 + HelpArray: array[0..74] of THelpArray = ( // I4840 (Word: 'BITMAP' ), (Word: 'begin' ), (Word: 'COPYRIGHT' ), @@ -53,6 +53,7 @@ const {9} (Word: 'VERSION' ), (Word: '&BITMAP'; Topic: 'bitmap' ), + (Word: '&CasedKeys'; Topic: 'casedkeys' ), (Word: '©RIGHT'; Topic: 'copyright' ), (Word: '&EthnologueCode'; Topic: 'ethnologuecode' ), // I4840 (Word: '&HOTKEY'; Topic: 'hotkey' ), diff --git a/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas index c21d4b5e6f..41e5b3b43b 100644 --- a/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas +++ b/windows/src/developer/TIKE/main/Keyman.Developer.System.LexicalModelParser.pas @@ -18,6 +18,10 @@ type FWordlists: TStrings; FWordBreaker: TLexicalModelWordBreaker; FIsEditable: Boolean; + FInsertAfterWord: string; + FIsRTL: Boolean; + FOpenQuote: string; + FCloseQuote: string; function GetText: string; procedure PrepareText; procedure SetComment(const Value: string); @@ -30,6 +34,16 @@ type function ReplaceSources(const m: TMatch): string; function ReplaceComment(const m: TMatch): string; procedure WordlistsChange(Sender: TObject); + function ReplaceInsertAfterWord(const punctuation: string): string; + function ReplaceIsRTL(const punctuation: string): string; + function ReplaceQuotesForKeepSuggestion(const punctuation: string): string; + procedure SetCloseQuote(const Value: string); + procedure SetInsertAfterWord(const Value: string); + procedure SetIsRTL(const Value: Boolean); + procedure SetOpenQuote(const Value: string); + + function JSONEncodeString(const s: string): string; + function JSONDecodeString(s: string): string; public constructor Create(Source: string); destructor Destroy; override; @@ -39,11 +53,23 @@ type property Wordlists: TStrings read FWordlists; property Comment: string read FComment write SetComment; property IsEditable: Boolean read FIsEditable; + property IsRTL: Boolean read FIsRTL write SetIsRTL; + property OpenQuote: string read FOpenQuote write SetOpenQuote; + property CloseQuote: string read FCloseQuote write SetCloseQuote; + property InsertAfterWord: string read FInsertAfterWord write SetInsertAfterWord; + public + // These defaults are defined in common/lexical-model-types/index.d.ts + const + CDefaultInsertAfterWord = ' '; + CDefaultIsRTL = False; + CDefaultOpenQuote = Char($201c); + CDefaultCloseQuote = Char($201d); end; implementation uses + System.JSON, System.SysUtils; { TLexicalModelParser } @@ -55,6 +81,31 @@ const SSources = 'sources\s*:\s*\[(.+?)\]'; SSource = '\s*([''"])(.+?)(\1)\s*(,?)'; + SEndOfObject = '\s*};'; + SPunctuationID = 'punctuation'; + SPunctuation = + ',?\s*'+SPunctuationID+'\s*:\s*{\s*(('+ + '(insertAfterWord\s*:\s*(([''"]).*?(\5)))|'+ + '(isRTL\s*:\s*(true|false))|'+ + '(quotesForKeepSuggestion\s*:\s*{\s*'+ + 'open\s*:\s*(([''"]).*?(\11))\s*,\s*'+ + 'close\s*:\s*(([''"]).*?(\14))\s*})'+ + '),?\s*)*}'; + + // These may delete the entry so we need to take the `,` into account + SInsertAfterWordID = 'insertAfterWord'; + SInsertAfterWord = ',?\s*'+SInsertAfterWordID+'\s*:\s*(([''"]).*?(\2))'; + + SIsRTLID = 'isRTL'; + SIsRTL = ',?\s*'+SIsRTLID+'\s*:\s*(true|false)'; + + SQuotesForKeepSuggestionID = 'quotesForKeepSuggestion'; + SQuotesForKeepSuggestion = + ',?\s*'+SQuotesForKeepSuggestionID+'\s*:\s*{\s*'+ + 'open\s*:\s*(([''"]).*?(\2))\s*,\s*'+ + 'close\s*:\s*(([''"]).*?(\5))\s*'+ + '}'; + constructor TLexicalModelParser.Create(Source: string); begin inherited Create; @@ -79,6 +130,37 @@ begin Result := FText.Text; end; +function TLexicalModelParser.JSONDecodeString(s: string): string; +var + v: TJSONValue; +begin + if s[1] = '''' then + begin + // Javascript can have ' but JSON only " + s[1] := '"'; + s[Length(s)] := '"'; + end; + v := TJSONObject.ParseJSONValue(s); + try + if not v.TryGetValue(Result) then + Result := ''; + finally + v.Free; + end; +end; + +function TLexicalModelParser.JSONEncodeString(const s: string): string; +var + j: TJSONString; +begin + j := TJSONString.Create(s); + try + Result := j.ToJSON; + finally + j.Free; + end; +end; + procedure TLexicalModelParser.Modify; begin FModified := True; @@ -98,6 +180,11 @@ begin FWordBreaker := lmwbUnknown; FComment := ''; + FInsertAfterWord := CDefaultInsertAfterWord; + FIsRTL := CDefaultIsRTL; + FOpenQuote := CDefaultOpenQuote; + FCloseQuote := CDefaultCloseQuote; + s := FText.Text; // /* Comment */ @@ -131,7 +218,40 @@ begin else FIsEditable := False; - FIsEditable := FIsEditable and (FFormat <> lmfUnknown) and (FWordBreaker <> lmwbUnknown); + // insertAfterWord: + m := TRegEx.Match(s, SInsertAfterWord, [roMultiLine]); + if m.Success then + FInsertAfterWord := JSONDecodeString(m.Groups[1].Value) + else if TRegEx.Match(s, SInsertAfterWordID, [roMultiLine]).Success then + FIsEditable := False; + + + // isRTL: true + m := TRegEx.Match(s, SIsRTL, [roMultiLine]); + if m.Success then + FIsRTL := m.Groups[1].Value = 'true' + else if TRegEx.Match(s, SIsRTLID, [roMultiLine]).Success then + FIsEditable := False; + + // quotesForKeepSuggestion: We assume that both open and close are defined, + // in that order. + m := TRegEx.Match(s, SQuotesForKeepSuggestion, [roMultiLine]); + if m.Success then + begin + FOpenQuote := JSONDecodeString(m.Groups[1].Value); + FCloseQuote := JSONDecodeString(m.Groups[4].Value); + end + else if TRegEx.Match(s, SQuotesForKeepSuggestionID, [roMultiLine]).Success then + FIsEditable := False; + + if TRegEx.IsMatch(s, SPunctuationID) and not TRegEx.IsMatch(s, SPunctuation) then + FIsEditable := False; + + + FIsEditable := FIsEditable and + TRegEx.IsMatch(s, SEndOfObject) and + (FFormat <> lmfUnknown) and + (FWordBreaker <> lmwbUnknown); end; function TLexicalModelParser.ReplaceComment(const m: TMatch): string; @@ -173,9 +293,43 @@ begin m.Value.Substring(m.Groups[1].Index - m.Index + m.Groups[1].Length); end; +function TLexicalModelParser.ReplaceInsertAfterWord(const punctuation: string): string; +begin + if punctuation = '' + then Result := '' + else Result := punctuation + ','#13#10; + + Result := Result + + ' insertAfterWord: '+JSONEncodeString(FInsertAfterWord); +end; + +function TLexicalModelParser.ReplaceIsRTL(const punctuation: string): string; +begin + if punctuation = '' + then Result := '' + else Result := punctuation + ','#13#10; + + Result := Result + + ' isRTL: true'; +end; + +function TLexicalModelParser.ReplaceQuotesForKeepSuggestion(const punctuation: string): string; +begin + if punctuation = '' + then Result := '' + else Result := punctuation + ','#13#10; + + Result := Result + + ' quotesForKeepSuggestion: {'#13#10+ + ' open: '+JSONEncodeString(FOpenQuote)+','#13#10+ + ' close: '+JSONEncodeString(FCloseQuote)+#13#10+ + ' }'; +end; + procedure TLexicalModelParser.PrepareText; var - s: string; + punctuation, s: string; + m: TMatch; begin if not FModified then Exit; @@ -186,6 +340,31 @@ begin s := TRegEx.Replace(s, SWordBreaker, ReplaceWordBreaker, [roMultiLine]); s := TRegEx.Replace(s, SSources, ReplaceSources, [roMultiLine]); + s := TRegEx.Replace(s, SPunctuation, '', [roMultiLine]); + + punctuation := ''; + + if FInsertAfterWord <> CDefaultInsertAfterWord then + punctuation := ReplaceInsertAfterWord(punctuation); + + if FIsRTL <> CDefaultIsRTL then + punctuation := ReplaceIsRTL(punctuation); + + if (FOpenQuote <> CDefaultOpenQuote) or (FCloseQuote <> CDefaultCloseQuote) then + punctuation := ReplaceQuotesForKeepSuggestion(punctuation); + + if punctuation <> '' then + begin + punctuation := + ','#13#10+ + ' punctuation: {'+#13#10+ + punctuation + #13#10+ + ' }'; + m := TRegEx.Match(s, SEndOfObject, [roMultiLine]); + if m.Success then + s := s.Substring(0, m.Index - 1) + punctuation + s.Substring(m.Index); + end; + // Replace, remove or add comment if TRegEx.IsMatch(s, SComment, [roSingleLine]) then s := TRegEx.Replace(s, SComment, ReplaceComment, [roSingleLine]) @@ -196,6 +375,16 @@ begin FText.Text := s; end; +procedure TLexicalModelParser.SetCloseQuote(const Value: string); +begin + Assert(FIsEditable); + if FCloseQuote <> Value then + begin + FCloseQuote := Value; + Modify; + end; +end; + procedure TLexicalModelParser.SetComment(const Value: string); begin Assert(FIsEditable); @@ -216,6 +405,36 @@ begin end; end; +procedure TLexicalModelParser.SetInsertAfterWord(const Value: string); +begin + Assert(FIsEditable); + if FInsertAfterWord <> Value then + begin + FInsertAfterWord := Value; + Modify; + end; +end; + +procedure TLexicalModelParser.SetIsRTL(const Value: Boolean); +begin + Assert(FIsEditable); + if FIsRTL <> Value then + begin + FIsRTL := Value; + Modify; + end; +end; + +procedure TLexicalModelParser.SetOpenQuote(const Value: string); +begin + Assert(FIsEditable); + if FOpenQuote <> Value then + begin + FOpenQuote := Value; + Modify; + end; +end; + procedure TLexicalModelParser.SetWordBreaker(const Value: TLexicalModelWordBreaker); begin Assert(FIsEditable); diff --git a/windows/src/developer/TIKE/main/UframeOnScreenKeyboardEditor.pas b/windows/src/developer/TIKE/main/UframeOnScreenKeyboardEditor.pas index 7db2b78df9..b166d85536 100644 --- a/windows/src/developer/TIKE/main/UframeOnScreenKeyboardEditor.pas +++ b/windows/src/developer/TIKE/main/UframeOnScreenKeyboardEditor.pas @@ -674,7 +674,7 @@ procedure TframeOnScreenKeyboardEditor.cmdVKImportKMXClick(Sender: TObject); if ValidExtShiftStates[j] = [essAlt] then Continue; if ValidExtShiftStates[j] = [essShift, essAlt] then Continue; // I1471:9011 - if (kbdOnScreen.Keys[i].KeyCaps[j] <> '') and + if (Trim(kbdOnScreen.Keys[i].KeyCaps[j]) <> '') and (kbdOnScreen.Keys[i].KeyCaps[j][1] >= #32) then begin k := TVisualKeyboardKey.Create; diff --git a/windows/src/developer/TIKE/main/UfrmMain.pas b/windows/src/developer/TIKE/main/UfrmMain.pas index 02c5b217a0..5185a4b927 100644 --- a/windows/src/developer/TIKE/main/UfrmMain.pas +++ b/windows/src/developer/TIKE/main/UfrmMain.pas @@ -1141,6 +1141,8 @@ begin end else begin + FFileName := ExpandUNCFileName(FFileName); + if FCloseNewFile then if Assigned(ActiveEditor) then if not ActiveEditor.Modified and ActiveEditor.Untitled then @@ -1154,11 +1156,11 @@ begin else if ext = '.tsv' then Result := OpenTSVEditor(FFileName) else if FileHasModelTsExt(FFileName) then Result := OpenModelEditor(FFileName) else Result := OpenEditor(FFileName, TfrmEditor); + + if Assigned(Result) then + AddMRU(FFileName); end; - if Assigned(Result) then - AddMRU(FFileName); - except on E:EFOpenError do ShowMessage('Unable to open the file '''+FFileName+'''.'); diff --git a/windows/src/developer/TIKE/oskbuilder/TouchLayoutDefinitions.pas b/windows/src/developer/TIKE/oskbuilder/TouchLayoutDefinitions.pas index 0a55570d06..29d08b6098 100644 --- a/windows/src/developer/TIKE/oskbuilder/TouchLayoutDefinitions.pas +++ b/windows/src/developer/TIKE/oskbuilder/TouchLayoutDefinitions.pas @@ -123,12 +123,12 @@ const 'rightalt', 'rightctrl-rightalt', //'shift', - 'shift-lctrl', - 'shift-rctrl', - 'shift-lalt', - 'shift-ralt', - 'shift-lctrl-lalt', - 'shift-rctrl-ralt' + 'leftctrl-shift', + 'rightctrl-shift', + 'leftalt-shift', + 'rightalt-shift', + 'leftctrl-leftalt-shift', + 'rightctrl-rightalt-shift' ); implementation diff --git a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmProject.pas b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmProject.pas index 02aea2a260..b65fbcfe09 100644 --- a/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmProject.pas +++ b/windows/src/developer/TIKE/project/Keyman.Developer.UI.Project.UfrmProject.pas @@ -460,6 +460,20 @@ begin else if SelectedMRUFileName <> '' then OpenContainingFolder(SelectedMRUFileName); end + else if Command = 'openbuildfolder' then + begin + pf := SelectedProjectFile; + if Assigned(pf) and (pf is TkmnProjectFile) then + OpenContainingFolder((pf as TkmnProjectFile).TargetFileName) + else if Assigned(pf) and (pf is TkpsProjectFile) then + OpenContainingFolder((pf as TkpsProjectFile).TargetFileName) + else if Assigned(pf) and (pf is TmodelTsProjectFile) then + OpenContainingFolder((pf as TmodelTsProjectFile).TargetFileName); + end + else if Command = 'openprojectfolder' then + begin + OpenContainingFolder(FGlobalProject.FileName); + end else if Command = 'removefile' then begin pf := SelectedProjectFile; diff --git a/windows/src/developer/TIKE/xml/project/keyboards.xsl b/windows/src/developer/TIKE/xml/project/keyboards.xsl index 894a018fa7..9da09d7df3 100644 --- a/windows/src/developer/TIKE/xml/project/keyboards.xsl +++ b/windows/src/developer/TIKE/xml/project/keyboards.xsl @@ -163,10 +163,15 @@ - Open Containing Folder + Open Source Folder keyman:opencontainingfolder?id= + + Open Build Folder + keyman:openbuildfolder?id= + + Remove from Project keyman:removefile?id= diff --git a/windows/src/developer/TIKE/xml/project/models.xsl b/windows/src/developer/TIKE/xml/project/models.xsl index cc45fa3650..a1a41286b9 100644 --- a/windows/src/developer/TIKE/xml/project/models.xsl +++ b/windows/src/developer/TIKE/xml/project/models.xsl @@ -149,10 +149,15 @@ - Open Containing Folder + Open Source Folder keyman:opencontainingfolder?id= + + Open Build Folder + keyman:openbuildfolder?id= + + Remove from Project keyman:removefile?id= diff --git a/windows/src/developer/TIKE/xml/project/packages.xsl b/windows/src/developer/TIKE/xml/project/packages.xsl index 96361f02d7..94d45113cc 100644 --- a/windows/src/developer/TIKE/xml/project/packages.xsl +++ b/windows/src/developer/TIKE/xml/project/packages.xsl @@ -140,9 +140,14 @@ keyman:editfileexternal?id= - Open Containing Folder + Open Source Folder keyman:opencontainingfolder?id= + + Open Build Folder + keyman:openbuildfolder?id= + + Remove from Project keyman:removefile?id= diff --git a/windows/src/developer/TIKE/xml/project/welcome.xsl b/windows/src/developer/TIKE/xml/project/welcome.xsl index 35cc14f814..cc38756165 100644 --- a/windows/src/developer/TIKE/xml/project/welcome.xsl +++ b/windows/src/developer/TIKE/xml/project/welcome.xsl @@ -47,6 +47,11 @@
+ + Open project folder + keyman:openprojectfolder + +

Recent Files

diff --git a/windows/src/developer/inst/kmdev.wxs b/windows/src/developer/inst/kmdev.wxs index 1f9b8b84aa..e6580df231 100644 --- a/windows/src/developer/inst/kmdev.wxs +++ b/windows/src/developer/inst/kmdev.wxs @@ -166,6 +166,10 @@ + + + + diff --git a/windows/src/developer/kmcmpdll/CasedKeys.cpp b/windows/src/developer/kmcmpdll/CasedKeys.cpp new file mode 100644 index 0000000000..44473a5246 --- /dev/null +++ b/windows/src/developer/kmcmpdll/CasedKeys.cpp @@ -0,0 +1,163 @@ + +#include "pch.h" + +#include +#include +#include +#include +#include + +#include "CharToKeyConversion.h" + +extern BOOL FMnemonicLayout; // TODO: these globals should be consolidated one day + +DWORD ExpandCapsRule(PFILE_GROUP gp, PFILE_KEY kpp, PFILE_STORE sp); + +PFILE_STORE FindSystemStore(PFILE_KEYBOARD fk, DWORD dwSystemID) { + assert(fk != NULL); + assert(dwSystemID != 0); + + PFILE_STORE sp = fk->dpStoreArray; + for (DWORD i = 0; i < fk->cxStoreArray; i++, sp++) { + if (sp->dwSystemID == dwSystemID) { + return sp; + } + } + return NULL; +} + +DWORD VerifyCasedKeys(PFILE_STORE sp) { + assert(sp != NULL); + + if (FMnemonicLayout) { + // The &CasedKeys system store is not supported for + // mnemonic layouts in 14.0 + return CERR_CasedKeysNotSupportedWithMnemonicLayout; + } + + // We will rewrite this store with virtual keys + + PWSTR p = sp->dpString; + PWSTR buf = new WCHAR[wcslen(p) * 5 + 1]; // extended keys are 5 units long, so this is the max length + PWSTR q = buf; + + while (*p) { + UINT key = 0, shift = 0; + if (*p != UC_SENTINEL) { + if (!MapUSCharToVK(*p, &key, &shift)) { + return CERR_CasedKeysMustContainOnlyVirtualKeys; + } + if (shift & K_SHIFTFLAG) { + return CERR_CasedKeysMustNotIncludeShiftStates; + } + } + else { + if (*(p + 1) != CODE_EXTENDED) { + return CERR_CasedKeysMustContainOnlyVirtualKeys; + } + shift = *(p + 2); + key = *(p + 3); + if (shift != ISVIRTUALKEY) { + return CERR_CasedKeysMustNotIncludeShiftStates; + } + } + *q++ = UC_SENTINEL; + *q++ = CODE_EXTENDED; + *q++ = shift; + *q++ = key; + *q++ = UC_SENTINEL_EXTENDEDEND; + *q = 0; + + p = incxstr(p); + } + + delete[] sp->dpString; + sp->dpString = buf; + + return CERR_None; +} + +DWORD ExpandCapsRulesForGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp) { + assert(fk != NULL); + assert(gp != NULL); + + if (FMnemonicLayout) { + // The &CasedKeys system store is not supported for + // mnemonic layouts in 14.0 + return CERR_None; + } + + PFILE_STORE sp = FindSystemStore(fk, TSS_CASEDKEYS); + if (!sp) { + // If there is no &CasedKeys system store, then we do not + // process the key + return CERR_None; + } + + DWORD msg; + // ExpandCapsRule may add extra rules at the end of gp->dpKeyArray, + // reallocating it, so we (a) cache the original length, and (b) + // dereference the array every call + int cxKeyArray = gp->cxKeyArray; + for (int i = 0; i < cxKeyArray; i++) { + if ((msg = ExpandCapsRule(gp, &gp->dpKeyArray[i], sp)) != CERR_None) { + return msg; + } + } + return CERR_None; +} + +DWORD ExpandCapsRule(PFILE_GROUP gp, PFILE_KEY kpp, PFILE_STORE sp) { + UINT key = kpp->Key; + UINT shift = kpp->ShiftFlags; + + if (shift == 0) { + // Convert US key cap to a virtual key + if (!MapUSCharToVK(kpp->Key, &key, &shift)) { + return CERR_None; + } + } + + if (shift & (CAPITALFLAG | NOTCAPITALFLAG)) { + // Don't attempt expansion if either Caps Lock flag is specified in the key rule + return CERR_None; + } + + PWSTR p = sp->dpString; + for (; *p; p = incxstr(p)) { + // We've already verified that the store contains only virtual keys in VerifyCasedKeys + if (*(p + 3) == key) { + break; + } + } + + if (!*p) { + // This key is not modified by Caps Lock + return CERR_None; + } + + // This key is modified by Caps Lock, so we need to duplicate this rule + PFILE_KEY k = new FILE_KEY[gp->cxKeyArray + 1]; + if (!k) return CERR_CannotAllocateMemory; + memcpy(k, gp->dpKeyArray, gp->cxKeyArray * sizeof(FILE_KEY)); + + kpp = &k[(INT_PTR)(kpp - gp->dpKeyArray)]; + + delete gp->dpKeyArray; + gp->dpKeyArray = k; + gp->cxKeyArray++; + + k = &k[gp->cxKeyArray - 1]; + k->dpContext = new WCHAR[wcslen(kpp->dpContext) + 1]; + k->dpOutput = new WCHAR[wcslen(kpp->dpOutput) + 1]; + wcscpy_s(k->dpContext, wcslen(kpp->dpContext) + 1, kpp->dpContext); // copy the context. + wcscpy_s(k->dpOutput, wcslen(kpp->dpOutput) + 1, kpp->dpOutput); // copy the output. + k->Key = key; + k->Line = kpp->Line; + // Add the CAPITAL FLAG, invert shift flag for the rule + k->ShiftFlags = shift ^ K_SHIFTFLAG | CAPITALFLAG; + kpp->Key = key; + kpp->ShiftFlags = shift | NOTCAPITALFLAG; + + return CERR_None; +} diff --git a/windows/src/developer/kmcmpdll/CasedKeys.h b/windows/src/developer/kmcmpdll/CasedKeys.h new file mode 100644 index 0000000000..66ba8a312d --- /dev/null +++ b/windows/src/developer/kmcmpdll/CasedKeys.h @@ -0,0 +1,4 @@ +#pragma once + +DWORD VerifyCasedKeys(PFILE_STORE sp); +DWORD ExpandCapsRulesForGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp); diff --git a/windows/src/developer/kmcmpdll/CharToKeyConversion.cpp b/windows/src/developer/kmcmpdll/CharToKeyConversion.cpp new file mode 100644 index 0000000000..24723c980f --- /dev/null +++ b/windows/src/developer/kmcmpdll/CharToKeyConversion.cpp @@ -0,0 +1,187 @@ + +#include "pch.h" +#include + +/* Following code lifted from syskbd.cpp and tweaked for compiler use. Todo: consolidate */ + +WCHAR VKToChar(WORD keyCode, UINT shiftFlags) +{ + char shiftedDigit[] = ")!@#$%^&*("; + int n, Shift; + + if (!(shiftFlags & ISVIRTUALKEY)) return keyCode; + + if (shiftFlags & (LCTRLFLAG | RCTRLFLAG | LALTFLAG | RALTFLAG)) return 0; + + if (keyCode >= '0' && keyCode <= '9') + { + n = keyCode - '0'; + return ((shiftFlags & K_SHIFTFLAG) ? shiftedDigit[n] : keyCode); + } + + if (keyCode >= 'A' && keyCode <= 'Z') + { + Shift = (shiftFlags & K_SHIFTFLAG); + if (shiftFlags & (CAPITALFLAG)) Shift = !Shift; + return (Shift ? keyCode : keyCode + 32); + } + + if (keyCode >= VK_NUMPAD0 && keyCode <= VK_NUMPAD9) + { + if (!(shiftFlags & NUMLOCKFLAG)) return 0; + return keyCode - (VK_NUMPAD0 - '0'); + } + + Shift = (shiftFlags & K_SHIFTFLAG); + + switch (keyCode) + { + case VK_ACCENT: + return Shift ? '~' : '`'; + case VK_HYPHEN: + return Shift ? '_' : '-'; + case VK_EQUAL: + return Shift ? '+' : '='; + case VK_BKSLASH: + return Shift ? '|' : 92; + case VK_LBRKT: + return Shift ? '{' : '['; + case VK_RBRKT: + return Shift ? '}' : ']'; + case VK_COLON: + return Shift ? ':' : ';'; + case VK_QUOTE: + return Shift ? '"' : 39; + case VK_COMMA: + return Shift ? '<' : ','; + case VK_PERIOD: + return Shift ? '>' : '.'; + case VK_SLASH: + return Shift ? '?' : '/'; + case VK_SPACE: + return ' '; + } + return 0; + //keyCode; +} + + +/* This array is lifted from preservedkeymap.cpp */ +// TODO: share this +const struct +{ + UINT key; + BOOL shift; +} USCharMap[] = { + { VK_SPACE, FALSE }, // 20 ' ' + { '1', TRUE }, // 21 '!' + { VK_QUOTE, TRUE }, // 22 '"' + { '3', TRUE }, // 23 '#' + { '4', TRUE }, // 24 '$' + { '5', TRUE }, // 25 '%' + { '7', TRUE }, // 26 '&' + { VK_QUOTE, FALSE }, // 27 ''' + { '9', TRUE }, // 28 '(' + { '0', TRUE }, // 29 ')' + { '8', TRUE }, // 2A '*' + { VK_EQUAL, TRUE }, // 2B '+' + { VK_COMMA, FALSE }, // 2C ',' + { VK_HYPHEN, FALSE }, // 2D '-' + { VK_PERIOD, FALSE }, // 2E '.' + { VK_SLASH, FALSE }, // 2F '/' + + { '0', FALSE }, // 30 '0' + { '1', FALSE }, // 31 '1' + { '2', FALSE }, // 32 '2' + { '3', FALSE }, // 33 '3' + { '4', FALSE }, // 34 '4' + { '5', FALSE }, // 35 '5' + { '6', FALSE }, // 36 '6' + { '7', FALSE }, // 37 '7' + { '8', FALSE }, // 38 '8' + { '9', FALSE }, // 39 '9' + { VK_COLON, TRUE }, // 3A ':' + { VK_COLON, FALSE }, // 3B ';' + { VK_COMMA, TRUE }, // 3C '<' + { VK_EQUAL, FALSE }, // 3D '=' + { VK_PERIOD, TRUE }, // 3E '>' + { VK_SLASH, TRUE }, // 3F '?' + + { '2', TRUE }, // 40 '@' + { 'A', TRUE }, // 41 'A' + { 'B', TRUE }, // 42 'B' + { 'C', TRUE }, // 43 'C' + { 'D', TRUE }, // 44 'D' + { 'E', TRUE }, // 45 'E' + { 'F', TRUE }, // 46 'F' + { 'G', TRUE }, // 47 'G' + { 'H', TRUE }, // 48 'H' + { 'I', TRUE }, // 49 'I' + { 'J', TRUE }, // 4A 'J' + { 'K', TRUE }, // 4B 'K' + { 'L', TRUE }, // 4C 'L' + { 'M', TRUE }, // 4D 'M' + { 'N', TRUE }, // 4E 'N' + { 'O', TRUE }, // 4F 'O' + + { 'P', TRUE }, // 50 'P' + { 'Q', TRUE }, // 51 'Q' + { 'R', TRUE }, // 52 'R' + { 'S', TRUE }, // 53 'S' + { 'T', TRUE }, // 54 'T' + { 'U', TRUE }, // 55 'U' + { 'V', TRUE }, // 56 'V' + { 'W', TRUE }, // 57 'W' + { 'X', TRUE }, // 58 'X' + { 'Y', TRUE }, // 59 'Y' + { 'Z', TRUE }, // 5A 'Z' + { VK_LBRKT, FALSE }, // 5B '[' + { VK_BKSLASH, FALSE }, // 5C '\' + { VK_RBRKT, FALSE }, // 5D ']' + { '6', TRUE }, // 5E '^' + { VK_HYPHEN, TRUE }, // 5F '_' + + { VK_ACCENT, FALSE }, // 60 '`' + { 'A', FALSE }, // 61 'a' + { 'B', FALSE }, // 62 'b' + { 'C', FALSE }, // 63 'c' + { 'D', FALSE }, // 64 'd' + { 'E', FALSE }, // 65 'e' + { 'F', FALSE }, // 66 'f' + { 'G', FALSE }, // 67 'g' + { 'H', FALSE }, // 68 'h' + { 'I', FALSE }, // 69 'i' + { 'J', FALSE }, // 6A 'j' + { 'K', FALSE }, // 6B 'k' + { 'L', FALSE }, // 6C 'l' + { 'M', FALSE }, // 6D 'm' + { 'N', FALSE }, // 6E 'n' + { 'O', FALSE }, // 6F 'o' + + { 'P', FALSE }, // 70 'p' + { 'Q', FALSE }, // 71 'q' + { 'R', FALSE }, // 72 'r' + { 'S', FALSE }, // 73 's' + { 'T', FALSE }, // 74 't' + { 'U', FALSE }, // 75 'u' + { 'V', FALSE }, // 76 'v' + { 'W', FALSE }, // 77 'w' + { 'X', FALSE }, // 78 'x' + { 'Y', FALSE }, // 79 'y' + { 'Z', FALSE }, // 7A 'z' + { VK_LBRKT, TRUE }, // 7B '{' + { VK_BKSLASH, TRUE }, // 7C '|' + { VK_RBRKT, TRUE }, // 7D '}' + { VK_ACCENT, TRUE } // 7E '~' +}; + +BOOL MapUSCharToVK(UINT ch, UINT *puKey, UINT *puShiftFlags) { + assert(puKey != NULL); + assert(puShiftFlags != NULL); + if (ch >= 0x20 && ch < 0x7F) { + *puKey = USCharMap[ch - 0x20].key; + *puShiftFlags = ISVIRTUALKEY | (USCharMap[ch - 0x20].shift ? K_SHIFTFLAG : 0); + return TRUE; + } + return FALSE; +} diff --git a/windows/src/developer/kmcmpdll/CharToKeyConversion.h b/windows/src/developer/kmcmpdll/CharToKeyConversion.h new file mode 100644 index 0000000000..6524a73f99 --- /dev/null +++ b/windows/src/developer/kmcmpdll/CharToKeyConversion.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#define VK_COLON 0xBA +#define VK_EQUAL 0xBB +#define VK_COMMA 0xBC +#define VK_HYPHEN 0xBD +#define VK_PERIOD 0xBE +#define VK_SLASH 0xBF +#define VK_ACCENT 0xC0 +#define VK_LBRKT 0xDB +#define VK_BKSLASH 0xDC +#define VK_RBRKT 0xDD +#define VK_QUOTE 0xDE +#define VK_xDF 0xDF + +BOOL MapUSCharToVK(UINT ch, UINT *puKey, UINT *puShiftFlags); +WCHAR VKToChar(WORD keyCode, UINT shiftFlags); + diff --git a/windows/src/developer/kmcmpdll/Compiler.cpp b/windows/src/developer/kmcmpdll/Compiler.cpp index 6224b35a54..147224a0aa 100644 --- a/windows/src/developer/kmcmpdll/Compiler.cpp +++ b/windows/src/developer/kmcmpdll/Compiler.cpp @@ -85,6 +85,8 @@ #include "edition.h" #include "onlineconstants.h" +#include "CharToKeyConversion.h" +#include "CasedKeys.h" int xatoi(PWSTR *p); int atoiW(PWSTR p); @@ -108,7 +110,7 @@ DWORD ProcessGroupFinish(PFILE_KEYBOARD fk); DWORD ProcessGroupLine(PFILE_KEYBOARD fk, PWSTR p); DWORD ProcessStoreLine(PFILE_KEYBOARD fk, PWSTR p); DWORD AddDebugStore(PFILE_KEYBOARD fk, PWSTR str); -DWORD ProcessKeyLine(PFILE_KEYBOARD fk, PWSTR str, int IsUnicode); +DWORD ProcessKeyLine(PFILE_KEYBOARD fk, PWSTR str, BOOL IsUnicode); DWORD ProcessEthnologueStore(PWSTR p); // I2646 DWORD ProcessHotKey(PWSTR p, DWORD *hk); DWORD ImportBitmapFile(PFILE_KEYBOARD fk, PWSTR szName, PDWORD FileSize, PBYTE *Buf); @@ -135,6 +137,7 @@ DWORD process_save(PFILE_KEYBOARD fk, LPWSTR q, LPWSTR tstr, int *mx); DWORD process_platform(PFILE_KEYBOARD fk, LPWSTR q, LPWSTR tstr, int *mx); // I3430 DWORD process_baselayout(PFILE_KEYBOARD fk, LPWSTR q, LPWSTR tstr, int *mx); // I3430 DWORD process_set_synonym(DWORD dwSystemID, PFILE_KEYBOARD fk, LPWSTR q, LPWSTR tstr, int *mx); // I3437 +DWORD process_expansion(PFILE_KEYBOARD fk, LPWSTR q, LPWSTR tstr, int *mx, int max); BOOL IsValidKeyboardVersion(WCHAR *dpString); // I4140 @@ -189,6 +192,7 @@ const PWCHAR StoreTokens[TSS__MAX + 2] = { SSN__PREFIX L"KEYBOARDVERSION", // I4140 SSN__PREFIX L"KMW_EMBEDCSS", SSN__PREFIX L"TARGETS", // I4504 + SSN__PREFIX L"CASEDKEYS", // #2241 NULL }; @@ -868,83 +872,6 @@ DWORD ProcessGroupLine(PFILE_KEYBOARD fk, PWSTR p) return CERR_None; } -/* Following code lifted from syskbd.cpp and tweaked for compiler use. Todo: consolidate */ - -#define VK_COLON 0xBA -#define VK_EQUAL 0xBB -#define VK_COMMA 0xBC -#define VK_HYPHEN 0xBD -#define VK_PERIOD 0xBE -#define VK_SLASH 0xBF -#define VK_ACCENT 0xC0 -#define VK_LBRKT 0xDB -#define VK_BKSLASH 0xDC -#define VK_RBRKT 0xDD -#define VK_QUOTE 0xDE -#define VK_xDF 0xDF - -WCHAR VKToChar(WORD keyCode, UINT shiftFlags) -{ - char shiftedDigit[] = ")!@#$%^&*("; - int n, Shift; - - if (!(shiftFlags & ISVIRTUALKEY)) return keyCode; - - if (shiftFlags & (LCTRLFLAG | RCTRLFLAG | LALTFLAG | RALTFLAG)) return 0; - - if (keyCode >= '0' && keyCode <= '9') - { - n = keyCode - '0'; - return ((shiftFlags & K_SHIFTFLAG) ? shiftedDigit[n] : keyCode); - } - - if (keyCode >= 'A' && keyCode <= 'Z') - { - Shift = (shiftFlags & K_SHIFTFLAG); - if (shiftFlags & (CAPITALFLAG)) Shift = !Shift; - return (Shift ? keyCode : keyCode + 32); - } - - if (keyCode >= VK_NUMPAD0 && keyCode <= VK_NUMPAD9) - { - if (!(shiftFlags & NUMLOCKFLAG)) return 0; - return keyCode - (VK_NUMPAD0 - '0'); - } - - Shift = (shiftFlags & K_SHIFTFLAG); - - switch (keyCode) - { - case VK_ACCENT: - return Shift ? '~' : '`'; - case VK_HYPHEN: - return Shift ? '_' : '-'; - case VK_EQUAL: - return Shift ? '+' : '='; - case VK_BKSLASH: - return Shift ? '|' : 92; - case VK_LBRKT: - return Shift ? '{' : '['; - case VK_RBRKT: - return Shift ? '}' : ']'; - case VK_COLON: - return Shift ? ':' : ';'; - case VK_QUOTE: - return Shift ? '"' : 39; - case VK_COMMA: - return Shift ? '<' : ','; - case VK_PERIOD: - return Shift ? '>' : '.'; - case VK_SLASH: - return Shift ? '?' : '/'; - case VK_SPACE: - return ' '; - } - return 0; - //keyCode; -} - - int cmpkeys(const void *key, const void *elem) { PFILE_KEY akey, aelem; @@ -972,6 +899,7 @@ int cmpkeys(const void *key, const void *elem) DWORD ProcessGroupFinish(PFILE_KEYBOARD fk) { PFILE_GROUP gp; + DWORD msg; if (fk->currentGroup == 0xFFFFFFFF) return CERR_None; // Just got to first group - so nothing to finish yet @@ -979,6 +907,7 @@ DWORD ProcessGroupFinish(PFILE_KEYBOARD fk) gp = &fk->dpGroupArray[fk->currentGroup]; // Finish off the previous group stuff! + if ((msg = ExpandCapsRulesForGroup(fk, gp)) != CERR_None) return msg; qsort(gp->dpKeyArray, gp->cxKeyArray, sizeof(FILE_KEY), cmpkeys); return CERR_None; @@ -1051,12 +980,10 @@ DWORD ProcessStoreLine(PFILE_KEYBOARD fk, PWSTR p) // we don't mix up named character codes which weren't supported in 5.x VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_NamedCodes); // Add a single char store as a defined character constant - char *codename = wstrtostr(sp->szName); if (Uni_IsSurrogate1(*sp->dpString)) - CodeConstants->AddCode(Uni_SurrogateToUTF32(sp->dpString[0], sp->dpString[1]), codename, fk->cxStoreArray); + CodeConstants->AddCode(Uni_SurrogateToUTF32(sp->dpString[0], sp->dpString[1]), sp->szName, fk->cxStoreArray); else - CodeConstants->AddCode(sp->dpString[0], codename, fk->cxStoreArray); - delete[] codename; + CodeConstants->AddCode(sp->dpString[0], sp->szName, fk->cxStoreArray); CodeConstants->reindex(); // has to be done after every character add due to possible use in another store. // I4982 } @@ -1255,6 +1182,11 @@ DWORD ProcessSystemStore(PFILE_KEYBOARD fk, DWORD SystemID, PFILE_STORE sp) case TSS_MNEMONIC: VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_MnemonicLayout); FMnemonicLayout = atoiW(sp->dpString) == 1; + if (FMnemonicLayout) { + // The &CasedKeys system store is not supported for + // mnemonic layouts + return CERR_CasedKeysNotSupportedWithMnemonicLayout; + } break; case TSS_NAME: @@ -1385,6 +1317,13 @@ DWORD ProcessSystemStore(PFILE_KEYBOARD fk, DWORD SystemID, PFILE_STORE sp) } break; + + case TSS_CASEDKEYS: + if ((msg = VerifyCasedKeys(sp)) != CERR_None) { + return msg; + } + break; + default: return CERR_InvalidSystemStore; } @@ -1908,7 +1847,6 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma DWORD i; BOOL finished = FALSE; WCHAR c; - PSTR codename; PWCHAR tstr = NULL; int tstrMax = 0; @@ -1960,6 +1898,7 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma case '$': type = 16; break; // named code constants case 'P': type = 17; break; // platform (synonym for if(&platform)) // I3430 case 'L': type = 18; break; // layer (synonym for set(&layer)) // I3437 + case '.': type = 19; break; // .. allows us to specify ranges -- either vkeys or characters default: if (iswdigit(*p)) type = 0; // octal number else type = 99; // error! @@ -2461,12 +2400,10 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma case 16: VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_NamedCodes); q = p + 1; - while (iswalnum(*q) || *q == '-' || *q == '_') q++; + while (*q && !iswblank(*q)) q++; c = *q; *q = 0; - codename = wstrtostr(p + 1); + n = CodeConstants->GetCode(p + 1, &i); *q = c; - n = CodeConstants->GetCode(codename, &i); - delete[] codename; if (n == 0) return CERR_InvalidNamedCode; if (i < 0xFFFFFFFFL) CheckStoreUsage(fk, i, TRUE, FALSE, FALSE); // I2993 if (n > 0xFFFF) @@ -2499,6 +2436,13 @@ DWORD GetXString(PFILE_KEYBOARD fk, PWSTR str, PWSTR token, PWSTR output, int ma err = process_set_synonym(TSS_LAYER, fk, q, tstr, &mx); if (err != CERR_None) return err; continue; + case 19: // #2241 + if (*(p + 1) != '.') return CERR_InvalidToken; + if (sFlag) return CERR_InvalidInVirtualKeySection; + p += 2; + err = process_expansion(fk, p, tstr, &mx, max); + if (err != CERR_None) return err; + continue; default: return CERR_InvalidToken; } @@ -2668,6 +2612,104 @@ DWORD process_reset(PFILE_KEYBOARD fk, LPWSTR q, LPWSTR tstr, int *mx) return CERR_None; } +DWORD process_expansion(PFILE_KEYBOARD fk, LPWSTR q, LPWSTR tstr, int *mx, int max) { + BOOL isVKey = FALSE; + + WORD BaseKey=0, BaseShiftFlags=0; + DWORD BaseChar=0; + + if (*mx == 0) { + return CERR_ExpansionMustFollowCharacterOrVKey; + } + LPWSTR p = &tstr[*mx]; + p = decxstr(p, tstr); + if (*p == UC_SENTINEL) { + if (*(p + 1) != CODE_EXTENDED) { + return CERR_ExpansionMustFollowCharacterOrVKey; + } + isVKey = TRUE; + BaseKey = *(p + 3); + BaseShiftFlags = *(p + 2); + } + else { + BaseChar = Uni_UTF16ToUTF32(p); + } + + // Look ahead at next element + WCHAR temp[64]; + PWCHAR r = NULL; + + DWORD msg; + + if ((msg = GetXString(fk, q, L"", temp, _countof(temp) - 1, 0, &r, FALSE, TRUE)) != CERR_None) + { + return msg; + } + + WORD HighKey, HighShiftFlags; + DWORD HighChar; + + switch(temp[0]) { + case 0: + return isVKey ? CERR_VKeyExpansionMustBeFollowedByVKey : CERR_CharacterExpansionMustBeFollowedByCharacter; + case UC_SENTINEL: + // Verify that range is valid virtual key range + if(!isVKey) { + return CERR_CharacterExpansionMustBeFollowedByCharacter; + } + if (temp[1] != CODE_EXTENDED) { + return CERR_VKeyExpansionMustBeFollowedByVKey; + } + HighKey = temp[3], HighShiftFlags = temp[2]; + if (HighShiftFlags != BaseShiftFlags) { + return CERR_VKeyExpansionMustUseConsistentShift; + } + if (HighKey <= BaseKey) { + return CERR_ExpansionMustBePositive; + } + // Verify space in buffer + if (*mx + (HighKey - BaseKey) * 5 + 1 >= max) return CERR_BufferOverflow; + // Inject an expansion. + for (BaseKey++; BaseKey < HighKey; BaseKey++) { + // < HighKey because caller will add HighKey to output + tstr[(*mx)++] = UC_SENTINEL; + tstr[(*mx)++] = CODE_EXTENDED; + tstr[(*mx)++] = BaseShiftFlags; + tstr[(*mx)++] = BaseKey; + tstr[(*mx)++] = UC_SENTINEL_EXTENDEDEND; + } + tstr[*mx] = 0; + break; + default: + // Verify that range is a valid character range + if (isVKey) { + return CERR_VKeyExpansionMustBeFollowedByVKey; + } + + HighChar = Uni_UTF16ToUTF32(temp); + if (HighChar <= BaseChar) { + return CERR_ExpansionMustBePositive; + } + // Inject an expansion. + for (BaseChar++; BaseChar < HighChar; BaseChar++) { + // < HighChar because caller will add HighChar to output + if (Uni_IsSMP(BaseChar)) { + // We'll test on each char to avoid complex calculations crossing SMP boundary + if (*mx + 3 >= max) return CERR_BufferOverflow; + tstr[(*mx)++] = (WCHAR) Uni_UTF32ToSurrogate1(BaseChar); + tstr[(*mx)++] = (WCHAR) Uni_UTF32ToSurrogate2(BaseChar); + } + else { + if (*mx + 2 >= max) return CERR_BufferOverflow; + tstr[(*mx)++] = (WCHAR) BaseChar; + } + } + tstr[*mx] = 0; + } + + return CERR_None; +} + DWORD process_set_synonym(DWORD dwSystemID, PFILE_KEYBOARD fk, LPWSTR q, LPWSTR tstr, int *mx) // I3437 { /* set( <'='> ), layer */ diff --git a/windows/src/developer/kmcmpdll/Compiler.rc b/windows/src/developer/kmcmpdll/Compiler.rc index 78db3a3a20..1cb5a36b57 100644 --- a/windows/src/developer/kmcmpdll/Compiler.rc +++ b/windows/src/developer/kmcmpdll/Compiler.rc @@ -228,3 +228,19 @@ BEGIN CWARN_NulNotFirstStatementInContext "nul must be the first statement in the context" CWARN_IfShouldBeAtStartOfContext "if, platform and baselayout should be at start of context (after nul, if present])" END + +STRINGTABLE +BEGIN + CERR_ExpansionMustFollowCharacterOrVKey "An expansion must follow a character or a virtual key" + CERR_VKeyExpansionMustBeFollowedByVKey "A virtual key expansion must be terminated by a virtual key" + CERR_CharacterExpansionMustBeFollowedByCharacter "A character expansion must be terminated by a character key" + CERR_VKeyExpansionMustUseConsistentShift "A virtual key expansion must use the same shift state for both terminators" + CERR_ExpansionMustBePositive "An expansion must have positive difference (i.e. A-Z, not Z-A)" +END + +STRINGTABLE +BEGIN + CERR_CasedKeysMustContainOnlyVirtualKeys "The &CasedKeys system store must contain only virtual keys or characters found on a US English keyboard" + CERR_CasedKeysMustNotIncludeShiftStates "The &CasedKeys system store must not include shift states" + CERR_CasedKeysNotSupportedWithMnemonicLayout "The &CasedKeys system store is not supported with mnemonic layouts" +END diff --git a/windows/src/developer/kmcmpdll/NamedCodeConstants.cpp b/windows/src/developer/kmcmpdll/NamedCodeConstants.cpp index 6fc9c00b41..a676230a6e 100644 --- a/windows/src/developer/kmcmpdll/NamedCodeConstants.cpp +++ b/windows/src/developer/kmcmpdll/NamedCodeConstants.cpp @@ -25,149 +25,154 @@ #include #include #include "NamedCodeConstants.h" +#include "kmcmpdll.h" extern char CompileDir[]; -int IsHangulSyllable(const char *codename, int *code); -int IsCJKUnifiedIdeograph(const char *codename, int *code); + +int IsHangulSyllable(const wchar_t *codename, int *code); BOOL FileExists(const char *filename) { - _finddata_t fi; - intptr_t n; + _finddata_t fi; + intptr_t n; - if((n = _findfirst(filename, &fi)) != -1) // I3056 // I3512 - { - _findclose(n); - return TRUE; - } - return FALSE; + if((n = _findfirst(filename, &fi)) != -1) // I3056 // I3512 + { + _findclose(n); + return TRUE; + } + return FALSE; } NamedCodeConstants::NamedCodeConstants() { - nEntries = 0; - entries = NULL; - nEntries_file = 0; - entries_file = NULL; - reindex(); + nEntries = 0; + entries = NULL; + nEntries_file = 0; + entries_file = NULL; + reindex(); } NamedCodeConstants::~NamedCodeConstants() { - if(entries) delete entries; - if(entries_file) delete entries_file; + if(entries) delete entries; + if(entries_file) delete entries_file; } -void NamedCodeConstants::AddCode(int n, char *p, DWORD storeIndex) +void NamedCodeConstants::AddCode(int n, const wchar_t *p, DWORD storeIndex) { - if((nEntries_file % ALLOC_SIZE) == 0) - { - NCCENTRY *bn = new NCCENTRY[nEntries_file + ALLOC_SIZE]; - if(nEntries_file > 0) - { - memcpy(bn, entries_file, sizeof(NCCENTRY) * nEntries_file); - delete entries_file; - } - entries_file = bn; - } + if((nEntries_file % ALLOC_SIZE) == 0) + { + NCCENTRY *bn = new NCCENTRY[nEntries_file + ALLOC_SIZE]; + if(nEntries_file > 0) + { + memcpy(bn, entries_file, sizeof(NCCENTRY) * nEntries_file); + delete entries_file; + } + entries_file = bn; + } - for(char *r = p; *r; r++) - if(!isalnum(*r) && *r != '-') *r = '_'; + entries_file[nEntries_file].code = n; + wcsncpy_s(entries_file[nEntries_file].name, _countof(entries_file[nEntries_file].name), p, MAX_ENAME); // I3481 + entries_file[nEntries_file].name[MAX_ENAME] = 0; + + for (wchar_t *r = entries_file[nEntries_file].name; *r; r++) + if (iswblank(*r) && *r != '-') *r = '_'; - entries_file[nEntries_file].code = n; - strncpy_s(entries_file[nEntries_file].name, _countof(entries_file[nEntries_file].name), p, MAX_ENAME); // I3481 - entries_file[nEntries_file].name[MAX_ENAME] = 0; entries_file[nEntries_file].storeIndex = storeIndex; - nEntries_file++; + nEntries_file++; } -void NamedCodeConstants::AddCode_IncludedCodes(int n, char *p) +void NamedCodeConstants::AddCode_IncludedCodes(int n, const wchar_t *p) { - if((nEntries % ALLOC_SIZE) == 0) - { - NCCENTRY *bn = new NCCENTRY[nEntries + ALLOC_SIZE]; - if(nEntries > 0) - { - memcpy(bn, entries, sizeof(NCCENTRY) * nEntries); - delete entries; - } - entries = bn; - } + if((nEntries % ALLOC_SIZE) == 0) + { + NCCENTRY *bn = new NCCENTRY[nEntries + ALLOC_SIZE]; + if(nEntries > 0) + { + memcpy(bn, entries, sizeof(NCCENTRY) * nEntries); + delete entries; + } + entries = bn; + } - for(char *r = p; *r; r++) - if(!isalnum(*r) && *r != '-') *r = '_'; + entries[nEntries].code = n; + wcsncpy_s(entries[nEntries].name, _countof(entries[nEntries].name), p, MAX_ENAME); // I3481 + entries[nEntries].name[MAX_ENAME] = 0; + for (wchar_t *r = entries[nEntries].name; *r; r++) + if (iswblank(*r)) *r = '_'; - entries[nEntries].code = n; - strncpy_s(entries[nEntries].name, _countof(entries[nEntries].name), p, MAX_ENAME); // I3481 - entries[nEntries].name[MAX_ENAME] = 0; entries[nEntries].storeIndex = 0xFFFFFFFFL; - nEntries++; + nEntries++; } int __cdecl sort_entries(const void *elem1, const void *elem2) { - return _stricmp( - ((NCCENTRY *)elem1)->name, - ((NCCENTRY *)elem2)->name); + return _wcsicmp( + ((NCCENTRY *)elem1)->name, + ((NCCENTRY *)elem2)->name); } BOOL NamedCodeConstants::IntLoadFile(const char *filename) { - FILE *fp = NULL; + FILE *fp = NULL; if(fopen_s(&fp, filename, "rt") != 0) return FALSE; // I3481 - char str[256], *p, *q, *context = NULL; - BOOL neol, first = TRUE; + char str[256], *p, *q, *context = NULL; + BOOL neol, first = TRUE; - while(fgets(str, 256, fp)) - { - neol = *(strchr(str, 0) - 1) == '\n'; - p = strtok_s(str, ";", &context); // I3481 - q = strtok_s(NULL, ";\n", &context); - if(p && q) - { + while(fgets(str, 256, fp)) + { + neol = *(strchr(str, 0) - 1) == '\n'; + p = strtok_s(str, ";", &context); // I3481 + q = strtok_s(NULL, ";\n", &context); + if(p && q) + { if(first && *p == (char)0xEF && *(p+1) == (char)0xBB && *(p+2) == (char)0xBF) p += 3; // I3056 UTF-8 // I3512 first = FALSE; - _strupr_s(q, strlen(q)+1); // I3481 // I3641 - int n = strtol(p, NULL, 16); - if(*q != '<') - AddCode_IncludedCodes(n, q); - } - if(!neol) - { - while(fgets(str, 256, fp)) if(*(strchr(str, 0)-1) == '\n') break; - } - } + _strupr_s(q, strlen(q)+1); // I3481 // I3641 + int n = strtol(p, NULL, 16); + if (*q != '<') { + PWSTR q0 = strtowstr(q); + AddCode_IncludedCodes(n, q0); + delete[] q0; + } + } + if(!neol) + { + while(fgets(str, 256, fp)) if(*(strchr(str, 0)-1) == '\n') break; + } + } - fclose(fp); + fclose(fp); - return TRUE; + return TRUE; } BOOL NamedCodeConstants::LoadFile(const char *filename) { - char buf[260]; - // Look in current directory first - strncpy_s(buf, _countof(buf), filename, 259); buf[259] = 0; // I3481 - if(FileExists(buf)) - return IntLoadFile(buf); - // Then look in keyboard file directory (CompileDir) - strncpy_s(buf, _countof(buf), CompileDir, 259); buf[259] = 0; // I3481 - strncat_s(buf, _countof(buf), filename, 259-strlen(CompileDir)); buf[259] = 0; - if(FileExists(buf)) - return IntLoadFile(buf); - // Finally look in kmcmpdll.dll directory - GetModuleFileName(0, buf, 260); - char *p = strrchr(buf, '\\'); if(p) p++; else p = buf; + char buf[260]; + // Look in current directory first + strncpy_s(buf, _countof(buf), filename, 259); buf[259] = 0; // I3481 + if(FileExists(buf)) + return IntLoadFile(buf); + // Then look in keyboard file directory (CompileDir) + strncpy_s(buf, _countof(buf), CompileDir, 259); buf[259] = 0; // I3481 + strncat_s(buf, _countof(buf), filename, 259-strlen(CompileDir)); buf[259] = 0; + if(FileExists(buf)) + return IntLoadFile(buf); + // Finally look in kmcmpdll.dll directory + GetModuleFileName(0, buf, 260); + char *p = strrchr(buf, '\\'); if(p) p++; else p = buf; *p = 0; - strncat_s(buf, _countof(buf), filename, 259-strlen(buf)); buf[259] = 0; // I3481 // I3641 - if(FileExists(buf)) - return IntLoadFile(buf); + strncat_s(buf, _countof(buf), filename, 259-strlen(buf)); buf[259] = 0; // I3481 // I3641 + if(FileExists(buf)) + return IntLoadFile(buf); - reindex(); + reindex(); - return FALSE; + return FALSE; } void NamedCodeConstants::reindex() @@ -176,50 +181,50 @@ void NamedCodeConstants::reindex() qsort(entries, nEntries, sizeof(NCCENTRY), sort_entries); } - char c = '.', d; - int i; + wchar_t c = L'.', d; + int i; - for(i = 0; i < 128; i++) chrindexes[i] = -1; + for(i = 0; i < 128; i++) chrindexes[i] = -1; if (entries != NULL) { for (i = 0; i < nEntries; i++) { - d = toupper(entries[i].name[0]); + d = towupper(entries[i].name[0]); if (d != c && d >= 32 && d <= 127) chrindexes[c = d] = i; } } } -int NamedCodeConstants::GetCode(const char *codename, DWORD *storeIndex) +int NamedCodeConstants::GetCode(const wchar_t *codename, DWORD *storeIndex) { *storeIndex = 0xFFFFFFFFL; // I2993 - int code = GetCode_IncludedCodes(codename); - if(code) return code; - for(int i = 0; i < nEntries_file; i++) - if(!_stricmp(entries_file[i].name, codename)) + int code = GetCode_IncludedCodes(codename); + if(code) return code; + for(int i = 0; i < nEntries_file; i++) + if(!_wcsicmp(entries_file[i].name, codename)) { *storeIndex = entries_file[i].storeIndex; return entries_file[i].code; } - return 0; + return 0; } -int NamedCodeConstants::GetCode_IncludedCodes(const char *codename) +int NamedCodeConstants::GetCode_IncludedCodes(const wchar_t *codename) { - char c = toupper(*codename); - int code; + wchar_t c = towupper(*codename); + int code; - if(IsHangulSyllable(codename, &code)) return code; + if(IsHangulSyllable(codename, &code)) return code; - if(c < 32 || c > 127 || chrindexes[c] < 0) return 0; - for(int n = chrindexes[c]; n < nEntries && toupper(entries[n].name[0]) == c; n++) - { - int cmp = _stricmp(codename, entries[n].name); - if(cmp == 0) return entries[n].code; - if(cmp < 0) break; - } - return 0; + if(c < 32 || c > 127 || chrindexes[c] < 0) return 0; + for(int n = chrindexes[c]; n < nEntries && towupper(entries[n].name[0]) == c; n++) + { + int cmp = _wcsicmp(codename, entries[n].name); + if(cmp == 0) return entries[n].code; + if(cmp < 0) break; + } + return 0; } /* @@ -239,92 +244,80 @@ const int HangulNCount = HangulVCount * HangulTCount, // 588 HangulSCount = HangulLCount * HangulNCount; // 11172 -const char * - Hangul_JAMO_L_TABLE[] = { - "G", "GG", "N", "D", "DD", "R", "M", "B", "BB", - "S", "SS", "", "J", "JJ", "C", "K", "T", "P", "H" }; +const wchar_t * + Hangul_JAMO_L_TABLE[] = { + L"G", L"GG", L"N", L"D", L"DD", L"R", L"M", L"B", L"BB", + L"S", L"SS", L"", L"J", L"JJ", L"C", L"K", L"T", L"P", L"H" }; -const char * - Hangul_JAMO_V_TABLE[] = { - "A", "AE", "YA", "YAE", "EO", "E", "YEO", "YE", "O", - "WA", "WAE", "OE", "YO", "U", "WEO", "WE", "WI", - "YU", "EU", "YI", "I" }; +const wchar_t * + Hangul_JAMO_V_TABLE[] = { + L"A", L"AE", L"YA", L"YAE", L"EO", L"E", L"YEO", L"YE", L"O", + L"WA", L"WAE", L"OE", L"YO", L"U", L"WEO", L"WE", L"WI", + L"YU", L"EU", L"YI", L"I" }; -const char * - Hangul_JAMO_T_TABLE[] = { - "", "G", "GG", "GS", "N", "NJ", "NH", "D", "L", "LG", "LM", - "LB", "LS", "LT", "LP", "LH", "M", "B", "BS", - "S", "SS", "NG", "J", "C", "K", "T", "P", "H" }; +const wchar_t * + Hangul_JAMO_T_TABLE[] = { + L"", L"G", L"GG", L"GS", L"N", L"NJ", L"NH", L"D", L"L", L"LG", L"LM", + L"LB", L"LS", L"LT", L"LP", L"LH", L"M", L"B", L"BS", + L"S", L"SS", L"NG", L"J", L"C", L"K", L"T", L"P", L"H" }; -int IsHangulSyllable(const char *codename, int *code) +int IsHangulSyllable(const wchar_t *codename, int *code) { - if(_strnicmp(codename, "HANGUL_SYLLABLE_", 16)) return 0; - codename += 16; - if(!*codename) return 0; + if(_wcsnicmp(codename, L"HANGUL_SYLLABLE_", 16)) return 0; + codename += 16; + if(!*codename) return 0; - int i, LIndex, VIndex, TIndex; + int i, LIndex, VIndex, TIndex; /* Find initial */ - int ch = toupper(*codename); - if(strchr("GNDRMBSJCKTPH", ch)) - { - /* Has an initial syllable */ - int fdouble = toupper(*(codename+1)) == ch; + int ch = towupper(*codename); + if(strchr("GNDRMBSJCKTPH", ch)) + { + /* Has an initial syllable */ + int fdouble = towupper(*(codename+1)) == ch; - LIndex = -1; - for(i = 0; i < HangulLCount; i++) - if(Hangul_JAMO_L_TABLE[i][0] == ch && - (!fdouble || (Hangul_JAMO_L_TABLE[i][1] == ch && fdouble))) - { - LIndex = i; - break; - } - if(LIndex == -1) return 0; - codename++; - if(fdouble) codename++; - } - else LIndex = 11; /* no initial */ + LIndex = -1; + for(i = 0; i < HangulLCount; i++) + if(Hangul_JAMO_L_TABLE[i][0] == ch && + (!fdouble || (Hangul_JAMO_L_TABLE[i][1] == ch && fdouble))) + { + LIndex = i; + break; + } + if(LIndex == -1) return 0; + codename++; + if(fdouble) codename++; + } + else LIndex = 11; /* no initial */ /* Find vowel */ - char V[4] = ""; - V[0] = *codename; - if(V[0] && strchr("AEIOUWY", toupper(*(codename+1)))) V[1] = *(codename+1); - if(V[1] && strchr("AEIOUWY", toupper(*(codename+2)))) V[2] = *(codename+2); + wchar_t V[4] = L""; + V[0] = *codename; + if(V[0] && strchr("AEIOUWY", towupper(*(codename+1)))) V[1] = *(codename+1); + if(V[1] && strchr("AEIOUWY", towupper(*(codename+2)))) V[2] = *(codename+2); - VIndex = -1; - for(i = 0; i < HangulVCount; i++) - if(!_stricmp(Hangul_JAMO_V_TABLE[i], V)) { VIndex = i; break; } + VIndex = -1; + for(i = 0; i < HangulVCount; i++) + if(!_wcsicmp(Hangul_JAMO_V_TABLE[i], V)) { VIndex = i; break; } - if(VIndex == -1) return 0; + if(VIndex == -1) return 0; - codename += strlen(V); + codename += wcslen(V); - /* Find final */ + /* Find final */ - TIndex = -1; + TIndex = -1; - for(i = 0; i < HangulTCount; i++) - if(!_stricmp(Hangul_JAMO_T_TABLE[i], codename)) { TIndex = i; break; } + for(i = 0; i < HangulTCount; i++) + if(!_wcsicmp(Hangul_JAMO_T_TABLE[i], codename)) { TIndex = i; break; } - if(TIndex == -1) return 0; + if(TIndex == -1) return 0; - /* Composition */ + /* Composition */ - *code = (HangulSBase + (LIndex * HangulVCount + VIndex) * HangulTCount) + TIndex; + *code = (HangulSBase + (LIndex * HangulVCount + VIndex) * HangulTCount) + TIndex; - return 1; -} - -int IsCJKUnifiedIdeograph(const char *codename, int *code) -{ - if(_strnicmp(codename, "CJK_UNIFIED_IDEOGRAPH", 21)) return 0; - codename += 21; - - if(strlen(codename) != 4) return 0; - - *code = strtol(codename, NULL, 16); - if(*code == 0 || *code == LONG_MAX || *code == LONG_MIN) return 0; - return 1; + return 1; } diff --git a/windows/src/developer/kmcmpdll/NamedCodeConstants.h b/windows/src/developer/kmcmpdll/NamedCodeConstants.h index e39adf7820..3efaf4ed1d 100644 --- a/windows/src/developer/kmcmpdll/NamedCodeConstants.h +++ b/windows/src/developer/kmcmpdll/NamedCodeConstants.h @@ -7,7 +7,7 @@ struct NCCENTRY { - char name[MAX_ENAME+1]; + wchar_t name[MAX_ENAME+1]; int code; DWORD storeIndex; }; @@ -20,17 +20,17 @@ private: int nEntries, nEntries_file; int chrindexes[128]; // A-Z, 0-9, -, _; simple index - int GetCode_IncludedCodes(const char *codename); - void AddCode_IncludedCodes(int n, char *p); + int GetCode_IncludedCodes(const wchar_t *codename); + void AddCode_IncludedCodes(int n, const wchar_t *p); BOOL IntLoadFile(const char *filename); public: NamedCodeConstants(); ~NamedCodeConstants(); void reindex(); - void AddCode(int n, char *p, DWORD storeIndex); + void AddCode(int n, const wchar_t *p, DWORD storeIndex); BOOL LoadFile(const char *filename); - int GetCode(const char *codename, DWORD *storeIndex); + int GetCode(const wchar_t *codename, DWORD *storeIndex); }; -#endif //_NAMEDCODECONSTANTS_H \ No newline at end of file +#endif //_NAMEDCODECONSTANTS_H diff --git a/windows/src/developer/kmcmpdll/kmcmpdll.h b/windows/src/developer/kmcmpdll/kmcmpdll.h index 88535f93e5..4e5dea8641 100644 --- a/windows/src/developer/kmcmpdll/kmcmpdll.h +++ b/windows/src/developer/kmcmpdll/kmcmpdll.h @@ -10,3 +10,5 @@ BOOL AddCompileMessage(DWORD msg); extern BOOL FWarnDeprecatedCode; extern int currentLine; + +PWSTR strtowstr(PSTR in); diff --git a/windows/src/developer/kmcmpdll/kmcmpdll.vcxproj b/windows/src/developer/kmcmpdll/kmcmpdll.vcxproj index 15d2197391..6556493822 100644 --- a/windows/src/developer/kmcmpdll/kmcmpdll.vcxproj +++ b/windows/src/developer/kmcmpdll/kmcmpdll.vcxproj @@ -66,11 +66,10 @@ <_ProjectFileVersion>10.0.30319.1 - $(SolutionDir)$(Platform)\$(Configuration)\ + $(Platform)\$(Configuration)\ obj\$(Platform)\$(Configuration)\ false false - $(SolutionDir)$(Platform)\$(Configuration)\ obj\$(Platform)\$(Configuration)\ true true @@ -89,11 +88,13 @@ $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\ext\json;..\..\ext\json-schema-validator + $(Platform)\$(Configuration)\ $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\ext\json;..\..\ext\json-schema-validator obj\$(Platform)\$(Configuration)\ $(ProjectName).x64 + $(Platform)\$(Configuration)\ $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\ext\json;..\..\ext\json-schema-validator @@ -102,6 +103,7 @@ $(VC_IncludePath);$(WindowsSDK_IncludePath);..\..\ext\json;..\..\ext\json-schema-validator obj\$(Platform)\$(Configuration)\ $(ProjectName).x64 + $(Platform)\$(Configuration)\ @@ -292,6 +294,8 @@ + + @@ -328,6 +332,8 @@ NotUsing + + %(AdditionalIncludeDirectories) %(AdditionalIncludeDirectories) diff --git a/windows/src/developer/kmcmpdll/kmcmpdll.vcxproj.filters b/windows/src/developer/kmcmpdll/kmcmpdll.vcxproj.filters index 4d1a0d7f75..b20b6a6219 100644 --- a/windows/src/developer/kmcmpdll/kmcmpdll.vcxproj.filters +++ b/windows/src/developer/kmcmpdll/kmcmpdll.vcxproj.filters @@ -70,6 +70,12 @@ Header files + + Header files + + + Header files + @@ -89,6 +95,8 @@ + + diff --git a/windows/src/developer/kmdecomp/kmdecomp.cpp b/windows/src/developer/kmdecomp/kmdecomp.cpp index b43a3c50d8..7fa28ed79d 100644 --- a/windows/src/developer/kmdecomp/kmdecomp.cpp +++ b/windows/src/developer/kmdecomp/kmdecomp.cpp @@ -59,14 +59,20 @@ int run(int argc, char *argv[]) { puts("KMDECOMP: Decompile Keyman .kmx keyboard"); puts("(C) SIL International"); - puts("Usage: KMDECOMP \n"); + puts("Usage: KMDECOMP [output]\n"); puts("Will create a .kmn and optionally a .bmp/.ico with the same filename and in the same location as the input"); return 1; } if(!LoadKeyboard(argv[1], &kbd, &lpBitmap, &cbBitmap)) return 2; - _splitpath_s(argv[1], drive, _MAX_DRIVE, dir, _MAX_DIR, filename, _MAX_FNAME, NULL, 0); + if (argc < 3) { + _splitpath_s(argv[1], drive, _MAX_DRIVE, dir, _MAX_DIR, filename, _MAX_FNAME, NULL, 0); + } + else { + _splitpath_s(argv[2], drive, _MAX_DRIVE, dir, _MAX_DIR, filename, _MAX_FNAME, NULL, 0); + } + _makepath_s(buf, _MAX_PATH, drive, dir, filename, ".kmn"); _makepath_s(buf2, _MAX_PATH, drive, dir, filename, ".bmp"); diff --git a/windows/src/developer/kmdecomp/savekeyboard.cpp b/windows/src/developer/kmdecomp/savekeyboard.cpp index 74bc135147..0d323f3933 100644 --- a/windows/src/developer/kmdecomp/savekeyboard.cpp +++ b/windows/src/developer/kmdecomp/savekeyboard.cpp @@ -90,6 +90,7 @@ const PWCHAR StoreTokens[MAX_SYSTEM_STORE] = { // I4652 SSN__PREFIX L"KEYBOARDVERSION", // I4140 SSN__PREFIX L"KMW_EMBEDCSS", SSN__PREFIX L"TARGETS", // I4504 + SSN__PREFIX L"CASEDKEYS", NULL }; @@ -254,7 +255,7 @@ PWCHAR ExtString(PWCHAR str) else wsprintfW(p, L"[%s%s] ", flagstr(*str), VKeyNames[*(str+1)]); } - str++; + str+=2; // skip UC_SENTINEL_EXTENDEDEND p = wcschr(p, 0); break; case CODE_SWITCH: diff --git a/windows/src/engine/keyman32/Keyman32.cpp b/windows/src/engine/keyman32/Keyman32.cpp index 3b7eb03d1e..7f7476e249 100644 --- a/windows/src/engine/keyman32/Keyman32.cpp +++ b/windows/src/engine/keyman32/Keyman32.cpp @@ -679,9 +679,9 @@ BOOL UpdateRefreshTag(LONG tag) // I1835 - Reduce chatter { PKEYMAN64THREADDATA _td = ThreadGlobals(); if(!_td) return FALSE; - if(_td->RefreshTag_Process < tag) + if(_td->RefreshTag_Thread != tag) { - _td->RefreshTag_Process = tag; + _td->RefreshTag_Thread = tag; return TRUE; } return FALSE; @@ -695,9 +695,9 @@ void HandleRefresh(int code, LONG tag) // This is sent by Keyman COM API, ApplyToRunningKeymanEngine SendDebugMessageFormat(0, sdmGlobal, 0, "#### Refresh Requested ####"); - // We ask any controller window to tell all instances of keyman32/keyman64 - // that a refresh is coming through - Globals::PostControllers(wm_keyman_refresh, KR_PRE_REFRESH, 0); + // We ask the master controller window to tell all instances + // of keyman32/keyman64 that a refresh is coming through + Globals::PostMasterController(wm_keyman_refresh, KR_PRE_REFRESH, 0); // We need to tell the controller windows to refresh themselves also Globals::PostControllers(wm_keyman_control, KMC_REFRESH, 0); @@ -709,8 +709,9 @@ void HandleRefresh(int code, LONG tag) // a double-broadcast which could happen if both keyman32 and keyman64 // receive the message, as they have independently managed RefreshTags - // All controllers will receive this message; only one need act on it - tag = InterlockedIncrement(Globals::RefreshTag()); + // This RefreshTag is only ever incremented here by the master controller + // so this is a thread safe operation + tag = ++(*Globals::RefreshTag()); if (UpdateRefreshTag(tag)) { // The Keyman process gets the update first @@ -726,8 +727,17 @@ void HandleRefresh(int code, LONG tag) // refreshed after an update, but only once per // refresh request if (UpdateRefreshTag(tag)) { - // We'll update when we are called into action - ScheduleRefresh(); +#ifdef _WIN64 + if (Globals::get_InitialisingThread() == GetCurrentThreadId()) { + // If this is the keymanx64 thread, then we should + // go ahead and process the refresh immediately so + // that global settings are updated + RefreshKeyboards(FALSE); + } + else +#endif + // We'll update when we are called into action + ScheduleRefresh(); } break; } @@ -755,7 +765,7 @@ void LoadBaseLayoutSettings() { // I4552 // I4583 Globals::SetBaseKeyboardFlags(underlyingLayout, reg->ReadInteger(REGSZ_SimulateAltGr), !reg->ValueExists(REGSZ_DeadkeyConversionMode) || reg->ReadInteger(REGSZ_DeadkeyConversionMode)); } else { - Globals::SetBaseKeyboardFlags("", 0, 0); + Globals::SetBaseKeyboardFlags("", 0, 1); } delete reg; diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index bb8bf1a44e..7a2b763e95 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -118,8 +118,6 @@ void ProcessToggleChange(UINT key) { // I4793 } } -void ProcessModifierChange(UINT key, BOOL isUp, BOOL isExtended); - extern "C" __declspec(dllexport) BOOL WINAPI TIPProcessKey(WPARAM wParam, LPARAM lParam, // I3589 // I3588 PKEYMANPROCESSOUTPUTFUNC outfunc, PKEYMANGETCONTEXTFUNC ctfunc, BOOL Updateable, BOOL Preserved) { @@ -174,7 +172,6 @@ extern "C" __declspec(dllexport) BOOL WINAPI TIPProcessKey(WPARAM wParam, LPARAM // in the first pass (!Updateable). KeyCapsLockPress(isUp); // I4548 } - ProcessModifierChange((UINT) wParam, isUp, extended); return FALSE; case VK_SHIFT: if (!Updateable) { @@ -190,7 +187,6 @@ extern "C" __declspec(dllexport) BOOL WINAPI TIPProcessKey(WPARAM wParam, LPARAM case VK_NUMLOCK: if(!isUp) ProcessToggleChange((UINT) wParam); // I4793 - ProcessModifierChange((UINT) wParam, isUp, extended); return FALSE; } if(isUp) { diff --git a/windows/src/engine/keyman32/globals.h b/windows/src/engine/keyman32/globals.h index 80d90f2fc2..19d8025c3c 100644 --- a/windows/src/engine/keyman32/globals.h +++ b/windows/src/engine/keyman32/globals.h @@ -1,18 +1,18 @@ /* Name: globals Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 1 Aug 2006 Modified Date: 25 Oct 2016 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 01 Aug 2006 - mcdurdin - Add hotkeys 14 Sep 2006 - mcdurdin - Add UpdateActiveWindows function 16 May 2007 - mcdurdin - I819 - Test if Keyman is functioning @@ -83,7 +83,7 @@ public: static DWORD *FSingleThread(); static DWORD *InitialisingThread(); // I4326 - + //static DWORD *ActiveKeymanID(); //static HKL *ActiveKeyboardLayout(); @@ -98,7 +98,7 @@ public: static HHOOK get_hhookGetMessage(); static HHOOK get_hhookCallWndProc(); - + #ifndef _WIN64 static HHOOK get_hhookLowLevelKeyboardProc(); // I4124 #endif @@ -134,7 +134,7 @@ public: static LRESULT SendMasterController(UINT msg, WPARAM wParam, LPARAM lParam); static void PostMasterController(UINT msg, WPARAM wParam, LPARAM lParam); - + static BOOL ResetControllers(); static void PostControllers(UINT msg, WPARAM wParam, LPARAM lParam); @@ -220,12 +220,12 @@ typedef struct tagKEYMAN64THREADDATA PKEYMANPROCESSOUTPUTFUNC TIPProcessOutput; PKEYMANGETCONTEXTFUNC TIPGetContext; - + BOOL TIPFUpdateable, TIPFPreserved; // I4290 BOOL FInRefreshKeyboards; BOOL RefreshRequired; - LONG RefreshTag_Process; // TODO: we may be able to eliminate this with our delayed refresh pattern? + LONG RefreshTag_Thread; // TODO: we may be able to eliminate this with our delayed refresh pattern? /* Addin Globals */ @@ -254,7 +254,7 @@ typedef struct tagKEYMAN64THREADDATA } KEYMAN64THREADDATA, *PKEYMAN64THREADDATA; -extern UINT +extern UINT wm_keyman, // user message - ignore msg wm_kmdebug, // " " " " - debugging wm_keyman_control, // messages to main Keyman window - replaces WM_USER+* @@ -269,7 +269,7 @@ extern UINT wm_keymanim_close, wm_keymanim_contextchanged, wm_test_keyman_functioning, - wm_keyman_keyevent; // for serialized input + wm_keyman_keyevent; // for serialized input extern BOOL flag_ShouldSerializeInput; diff --git a/windows/src/engine/keyman32/k32_globals.cpp b/windows/src/engine/keyman32/k32_globals.cpp index cb7a71f69f..dc4b68de36 100644 --- a/windows/src/engine/keyman32/k32_globals.cpp +++ b/windows/src/engine/keyman32/k32_globals.cpp @@ -1,18 +1,18 @@ /* Name: k32_globals Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 1 Aug 2006 Modified Date: 6 Feb 2015 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 01 Aug 2006 - mcdurdin - Add hotkeys globals 14 Sep 2006 - mcdurdin - Add IsKeymanSystemWindow and UpdateActiveWindows functions 04 Dec 2006 - mcdurdin - Add PostProcessControllers and IsControllerProcess functions @@ -69,7 +69,7 @@ /* */ /***************************************************************************/ -UINT +UINT //TODO: consolidate these messages -- they are probably not all required now wm_keyman = 0, // user message - ignore msg // I3594 wm_keyman_keyevent = 0, // for serialized input @@ -86,7 +86,7 @@ UINT wm_keymanim_close = 0, wm_keymanim_contextchanged = 0, - + wm_test_keyman_functioning = 0; //HWND hwndIM = 0, hwndIMOldFocus = 0; @@ -100,7 +100,7 @@ static CRITICAL_SECTION csGlobals; PKEYMAN64THREADDATA Globals_InitThread() { EnterCriticalSection(&csGlobals); - + PKEYMAN64THREADDATA lpvData; if(Globals::get_Keyman_Shutdown()) // I3092 @@ -190,7 +190,7 @@ void Globals_UninitProcess() PKEYMAN64THREADDATA ThreadGlobals() { EnterCriticalSection(&csGlobals); - + PKEYMAN64THREADDATA lpvData = NULL; if(dwTlsIndex != TLS_OUT_OF_INDEXES) { @@ -220,7 +220,7 @@ BOOL Globals_ProcessInitialised() //static INI // I3158 // I3524 // f_Ini = {0}; // KEYMAN.INI options -static HHOOK +static HHOOK f_hhookGetMessage = NULL, // GETMESSAGE hook handle f_hhookCallWndProc = NULL; // CALLWNDPROC hook handle @@ -236,7 +236,7 @@ static HWND static UINT f_WindowsVersion = 0; // Windows Version -static BOOL +static BOOL f_Keyman_Initialised = FALSE, f_Keyman_Shutdown = FALSE; @@ -280,14 +280,14 @@ __declspec(align(8)) static LONG f_RefreshTag = 0; #endif -static BOOL - f_debug_KeymanLog = FALSE, +static BOOL + f_debug_KeymanLog = FALSE, f_debug_ToConsole = FALSE; #pragma data_seg() /***************************************************************************/ -DWORD //static +DWORD //static f_ShiftState = 0; HANDLE f_hLockMutex = 0; @@ -364,13 +364,15 @@ void Globals::SetBaseKeyboardName(wchar_t *baseKeyboardName, wchar_t *baseKeyboa } void Globals::SetBaseKeyboardFlags(char *baseKeyboard, BOOL simulateAltGr, BOOL mnemonicDeadkeyConversionMode) { // I4583 // I4552 + SendDebugMessageFormat(0, sdmAIDefault, 0, "Globals::SetBaseKeyboardFlags(baseKeyboard='%s', simulateAltGr=%d, mnemonicDeadkeyConversionMode=%d", + baseKeyboard, simulateAltGr, mnemonicDeadkeyConversionMode); strcpy_s(f_BaseKeyboard, baseKeyboard); f_SimulateAltGr = simulateAltGr; f_MnemonicDeadkeyConversionMode = mnemonicDeadkeyConversionMode; } /** - Loads settings that are globally applied at application startup. These cannot + Loads settings that are globally applied at application startup. These cannot be changed until Keyman is restarted. */ BOOL Globals::InitSettings() { @@ -392,7 +394,7 @@ BOOL Globals::Lock() f_hLockMutex = CreateMutex(NULL, FALSE, "Tavultesoft_KeymanEngine_GlobalLock"); if(f_hLockMutex == 0) return FALSE; } - + if(WaitForSingleObject(f_hLockMutex, 1000) == WAIT_OBJECT_0) return TRUE; // I3183 // I3530 SetLastError(ERROR_TIMEOUT); // I3183 // I3530 return FALSE; // I3183 // I3530 @@ -400,7 +402,7 @@ BOOL Globals::Lock() BOOL Globals::InitHandles() // I3040 { - if(f_hLockMutex == 0) + if(f_hLockMutex == 0) f_hLockMutex = CreateMutex(NULL, FALSE, "Tavultesoft_KeymanEngine_GlobalLock"); if(f_hLockMutex == 0) return FALSE; SetObjectToLowIntegrity(f_hLockMutex); @@ -477,7 +479,7 @@ void Globals::PostProcessControllers(UINT msg, WPARAM wParam, LPARAM lParam) for(int i = 0; i < f_MaxControllers; i++) { GetWindowThreadProcessId(f_ControllerWindow[i], &pid); - if(pid == GetCurrentProcessId()) + if(pid == GetCurrentProcessId()) PostMessage(f_ControllerWindow[i], msg, wParam, lParam); } Unlock(); @@ -492,7 +494,7 @@ BOOL Globals::IsControllerProcess() { GetWindowThreadProcessId(f_ControllerWindow[i], &pid); if(pid == GetCurrentProcessId()) - { + { Unlock(); return TRUE; } @@ -524,7 +526,7 @@ void Globals::PostMasterController(UINT msg, WPARAM wParam, LPARAM lParam) Unlock(); return; } - + if(ShouldDebug(sdmGlobal)) { char buf[64]; @@ -552,7 +554,7 @@ LRESULT Globals::SendMasterController(UINT msg, WPARAM wParam, LPARAM lParam) HWND hwnd = f_ControllerWindow[0]; Unlock(); - + if(ShouldDebug(sdmGlobal)) { char buf[64]; @@ -561,7 +563,7 @@ LRESULT Globals::SendMasterController(UINT msg, WPARAM wParam, LPARAM lParam) SendDebugMessageFormat(f_ControllerWindow[0], sdmGlobal, 0, "SendMasterController %s [%x : %x, %x]", buf, msg, wParam, lParam); } - // Window may be taken out of list between Lock and Unlock but let's not worry about this + // Window may be taken out of list between Lock and Unlock but let's not worry about this SetLastError(ERROR_SUCCESS); DWORD_PTR dwResult = 0; @@ -633,7 +635,7 @@ BOOL IsKeymanSystemWindow(HWND hwnd) DWORD pidC, pidHWND; GetWindowThreadProcessId(hwnd, &pidHWND); - + if(!Globals::Lock()) return FALSE; for(int i = 0; i < f_MaxControllers; i++) @@ -641,7 +643,7 @@ BOOL IsKeymanSystemWindow(HWND hwnd) if(f_ControllerWindow[i] == hwnd) { Globals::Unlock(); - return TRUE; + return TRUE; } GetWindowThreadProcessId(f_ControllerWindow[i], &pidC); if(pidC == pidHWND) @@ -666,10 +668,10 @@ void UpdateActiveWindows() GUITHREADINFO gti; gti.cbSize = sizeof(GUITHREADINFO); GetGUIThreadInfo(0, >i); - + if(IsKeymanSystemWindow(gti.hwndActive) || IsKeymanSystemWindow(gti.hwndFocus)) return; if(IsTooltipWindow(gti.hwndActive) || IsTooltipWindow(gti.hwndFocus)) return; - + *Globals::hLastActiveWnd() = gti.hwndActive; *Globals::hLastFocusWnd() = gti.hwndFocus; diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 9d9bacb9ba..5f9593a446 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -109,8 +109,6 @@ LRESULT CALLBACK kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) return res; } -void ProcessModifierChange(UINT key, BOOL isUp, BOOL isExtended); // I4793 - LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) { LPMSG mp; @@ -448,7 +446,7 @@ void GetCapsAndNumlockState() { // I4793 via TSF, and we don't receive the notifications via the GetMessage hook when in UWP apps (#4369). - TODO: test whether we still need the GetMessage hook for reading modifier state. + TODO: test whether we still need the GetMessage hook for reading modifier state. */ void ProcessModifierChange(UINT key, BOOL isUp, BOOL isExtended) { // I4793 UINT flag = 0; @@ -457,8 +455,6 @@ void ProcessModifierChange(UINT key, BOOL isUp, BOOL isExtended) { // I4793 case VK_SHIFT: flag = K_SHIFTFLAG; break; case VK_MENU: flag = isExtended ? RALTFLAG : LALTFLAG; break; case VK_CONTROL: flag = isExtended ? RCTRLFLAG : LCTRLFLAG; break; - case VK_CAPITAL: flag = CAPITALFLAG; break; - case VK_NUMLOCK: flag = NUMLOCKFLAG; break; } UINT oldShiftState = Globals::get_ShiftState(); diff --git a/windows/src/engine/keyman32/preservedkeymap.cpp b/windows/src/engine/keyman32/preservedkeymap.cpp index f37a2e4901..677ba5e193 100644 --- a/windows/src/engine/keyman32/preservedkeymap.cpp +++ b/windows/src/engine/keyman32/preservedkeymap.cpp @@ -1,18 +1,18 @@ /* Name: preservedkeymap Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 1 Dec 2012 Modified Date: 10 Feb 2015 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 01 Dec 2012 - mcdurdin - I3623 - V9.0 - Add preserved key mapping from .kmx file 17 Jan 2013 - mcdurdin - I3762 - V9.0 - underlying layout cannot be controlled cleanly via TSF so support translation 11 Aug 2013 - mcdurdin - I3775 - V9.0 - Fail I3762 - AltGr combinations are not matched on French base layout @@ -158,8 +158,8 @@ BOOL PreservedKeyMap::MapUSCharToVK(UINT *puKey, UINT *puShiftFlags) { if(*puKey >= 0x20 && *puKey < 0x7F) { + *puShiftFlags = ISVIRTUALKEY | (USCharMap[*puKey - 0x20].shift ? K_SHIFTFLAG : 0); *puKey = USCharMap[*puKey - 0x20].key; - *puShiftFlags = USCharMap[*puKey - 0x20].shift ? (ISVIRTUALKEY | K_SHIFTFLAG) : 0; return TRUE; } return FALSE; @@ -241,8 +241,8 @@ BOOL PreservedKeyMap::MapKeyboard(KEYBOARD *pKeyboard, PreservedKey **pPreserved m_BaseKeyboardUsesAltGr = KeyboardGivesCtrlRAltForRAlt(); // I4592 - // This is not the same as m_BaseKeyboardUsesAltGr -- we are turning - // the simulation back on after (possibly) turning it off, for a + // This is not the same as m_BaseKeyboardUsesAltGr -- we are turning + // the simulation back on after (possibly) turning it off, for a // consistent experience. TODO: determine if m_BaseKeyboardUseAltGr // is still needed given we always use kbdus as base for Keyman 10+ BOOL bSimulateAltGr = Globals::get_SimulateAltGr(); @@ -299,7 +299,7 @@ BOOL PreservedKeyMap::MapKeyboard(KEYBOARD *pKeyboard, PreservedKey **pPreserved CoCreateGuid(&pKeys[n].guid); n++; - if (bSimulateAltGr && (pKeys[n-1].key.uModifiers & RALT_MATCHING_MASK) == TF_MOD_RALT) + if (bSimulateAltGr && (pKeys[n-1].key.uModifiers & RALT_MATCHING_MASK) == TF_MOD_RALT) { // Do this for RALT and RALT+SHIFT only, so we've tested against that mask // Copy the key and fix modifiers diff --git a/windows/src/engine/mcompile/mc_crc32.cpp b/windows/src/engine/mcompile/mc_crc32.cpp index 9006b8ab3d..728deaf4c0 100644 --- a/windows/src/engine/mcompile/mc_crc32.cpp +++ b/windows/src/engine/mcompile/mc_crc32.cpp @@ -1,5 +1,5 @@ -#include "stdafx.h" +#include "pch.h" #define CRC32_POLYNOMIAL 0xEDB88320 diff --git a/windows/src/engine/mcompile/mc_import_rules.cpp b/windows/src/engine/mcompile/mc_import_rules.cpp index 403cbcd7f1..340f3a3aab 100644 --- a/windows/src/engine/mcompile/mc_import_rules.cpp +++ b/windows/src/engine/mcompile/mc_import_rules.cpp @@ -19,7 +19,7 @@ 06 Feb 2015 - mcdurdin - I4552 - V9.0 - Add mnemonic recompile option to ignore deadkeys */ -#include "stdafx.h" +#include "pch.h" #include enum ShiftState { diff --git a/windows/src/engine/mcompile/mc_kmxfile.cpp b/windows/src/engine/mcompile/mc_kmxfile.cpp index 99db7daf05..6f14a594e7 100644 --- a/windows/src/engine/mcompile/mc_kmxfile.cpp +++ b/windows/src/engine/mcompile/mc_kmxfile.cpp @@ -1,4 +1,4 @@ -#include "stdafx.h" +#include "pch.h" static BOOL LoadKeyboardFile(LPSTR fileName, LPKEYBOARD *lpKeyboard); diff --git a/windows/src/engine/mcompile/mc_savekeyboard.cpp b/windows/src/engine/mcompile/mc_savekeyboard.cpp index 35acde8b64..0eb67094a4 100644 --- a/windows/src/engine/mcompile/mc_savekeyboard.cpp +++ b/windows/src/engine/mcompile/mc_savekeyboard.cpp @@ -1,5 +1,5 @@ -#include "stdafx.h" +#include "pch.h" DWORD WriteCompiledKeyboard(LPKEYBOARD fk, HANDLE hOutfile, BOOL FSaveDebug); diff --git a/windows/src/engine/mcompile/mc_syskbd.cpp b/windows/src/engine/mcompile/mc_syskbd.cpp index 9fa1843a64..f2c5b61afc 100644 --- a/windows/src/engine/mcompile/mc_syskbd.cpp +++ b/windows/src/engine/mcompile/mc_syskbd.cpp @@ -1,4 +1,4 @@ -#include "stdafx.h" +#include "pch.h" BOOL IsWow64(); diff --git a/windows/src/engine/mcompile/mc_syskbdnt32.cpp b/windows/src/engine/mcompile/mc_syskbdnt32.cpp index e19cebf3a3..a5cc83d347 100644 --- a/windows/src/engine/mcompile/mc_syskbdnt32.cpp +++ b/windows/src/engine/mcompile/mc_syskbdnt32.cpp @@ -27,7 +27,7 @@ // //////////////////////////////////////////////////////////////////////////// -#include "stdafx.h" +#include "pch.h" #include "../../engine/keyman32/kbd.h" /* DDK kbdlayout */ diff --git a/windows/src/engine/mcompile/mc_syskbdnt64.cpp b/windows/src/engine/mcompile/mc_syskbdnt64.cpp index a15c957f5c..2034af6ec9 100644 --- a/windows/src/engine/mcompile/mc_syskbdnt64.cpp +++ b/windows/src/engine/mcompile/mc_syskbdnt64.cpp @@ -27,7 +27,7 @@ // //////////////////////////////////////////////////////////////////////////// -#include "stdafx.h" +#include "pch.h" #include "../../engine/keyman32/kbd.h" /* DDK kbdlayout */ typedef struct { diff --git a/windows/src/engine/mcompile/mc_unicode.cpp b/windows/src/engine/mcompile/mc_unicode.cpp index bc86536f69..5e62d5aac3 100644 --- a/windows/src/engine/mcompile/mc_unicode.cpp +++ b/windows/src/engine/mcompile/mc_unicode.cpp @@ -1,4 +1,4 @@ -#include "stdafx.h" +#include "pch.h" const WCHAR cp1252[256] = { /*0x00*/ 0x0000, //Null @@ -301,4 +301,4 @@ BOOL ConvertKeyboardToUnicode(LPKEYBOARD kbd) { kbd->StartGroup[BEGIN_ANSI] = 0xFFFFFFFF; return TRUE; -} \ No newline at end of file +} diff --git a/windows/src/engine/mcompile/mcompile.cpp b/windows/src/engine/mcompile/mcompile.cpp index 06e79bd44c..1b1e59642e 100644 --- a/windows/src/engine/mcompile/mcompile.cpp +++ b/windows/src/engine/mcompile/mcompile.cpp @@ -29,7 +29,7 @@ // for the subcomponents of the compiled keyboard is an unnecessary optimisation. Just so you know. // -#include "stdafx.h" +#include "pch.h" #include #include @@ -465,38 +465,6 @@ BOOL DoConvert(LPKEYBOARD kbd, LPWSTR kbid, BOOL bDeadkeyConversion) { // I455 return TRUE; } -PWSTR incxstr(PWSTR p) -{ - if(*p == 0) return p; - if(*p != UC_SENTINEL) - { - if(*p >= 0xD800 && *p <= 0xDBFF && *(p+1) >= 0xDC00 && *(p+1) <= 0xDFFF) return p+2; - return p+1; - } - - p+=2; - switch(*(p-1)) - { - case CODE_ANY: return p+1; - case CODE_NOTANY: return p+1; - case CODE_INDEX: return p+2; - case CODE_USE: return p+1; - case CODE_DEADKEY: return p+1; - case CODE_EXTENDED: p += 2; while(*p != UC_SENTINEL_EXTENDEDEND) p++; return p+1; - case CODE_CLEARCONTEXT: return p+1; - case CODE_CALL: return p+1; - case CODE_CONTEXTEX: return p+1; - case CODE_IFOPT: return p+3; - case CODE_IFSYSTEMSTORE: return p+3; - case CODE_SETOPT: return p+2; - case CODE_SETSYSTEMSTORE: return p+2; - case CODE_RESETOPT: return p+1; - case CODE_SAVEOPT: return p+1; - default: return p; - } -} - - void LogError(PWSTR fmt, ...) { WCHAR fmtbuf[256]; diff --git a/windows/src/engine/mcompile/mcompile.vcxproj b/windows/src/engine/mcompile/mcompile.vcxproj index 5dab03a750..7bb53badb2 100644 --- a/windows/src/engine/mcompile/mcompile.vcxproj +++ b/windows/src/engine/mcompile/mcompile.vcxproj @@ -60,6 +60,7 @@ WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions) MultiThreadedDebug ..\..\ext\sentry;..\..\global\inc + pch.h Console @@ -77,6 +78,7 @@ WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions) MultiThreaded ..\..\ext\sentry;..\..\global\inc + pch.h Console @@ -93,7 +95,7 @@ - + @@ -103,15 +105,18 @@ NotUsing + - + Create Create + pch.h + pch.h diff --git a/windows/src/engine/mcompile/mcompile.vcxproj.filters b/windows/src/engine/mcompile/mcompile.vcxproj.filters index 23da885b12..d6a1d2269d 100644 --- a/windows/src/engine/mcompile/mcompile.vcxproj.filters +++ b/windows/src/engine/mcompile/mcompile.vcxproj.filters @@ -21,9 +21,6 @@ - - Header Files - Header Files @@ -39,11 +36,11 @@ Header Files + + Header Files + - - Source Files - Library Source Files @@ -77,6 +74,12 @@ Source Files + + Source Files + + + Source Files + diff --git a/windows/src/engine/mcompile/pch.cpp b/windows/src/engine/mcompile/pch.cpp new file mode 100644 index 0000000000..a0a3189c40 --- /dev/null +++ b/windows/src/engine/mcompile/pch.cpp @@ -0,0 +1,8 @@ +// pch.cpp : source file that includes just the standard includes +// m-to-p.pch will be the pre-compiled header +// pch.obj will contain the pre-compiled type information + +#include "pch.h" + +// TODO: reference any additional headers you need in PC.H +// and not in this file diff --git a/windows/src/engine/mcompile/stdafx.h b/windows/src/engine/mcompile/pch.h similarity index 88% rename from windows/src/engine/mcompile/stdafx.h rename to windows/src/engine/mcompile/pch.h index 8763811a30..0ea469a5d9 100644 --- a/windows/src/engine/mcompile/stdafx.h +++ b/windows/src/engine/mcompile/pch.h @@ -1,4 +1,4 @@ -// stdafx.h : include file for standard system include files, +// pch.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently // diff --git a/windows/src/engine/mcompile/stdafx.cpp b/windows/src/engine/mcompile/stdafx.cpp deleted file mode 100644 index f9cf0b53ce..0000000000 --- a/windows/src/engine/mcompile/stdafx.cpp +++ /dev/null @@ -1,8 +0,0 @@ -// stdafx.cpp : source file that includes just the standard includes -// m-to-p.pch will be the pre-compiled header -// stdafx.obj will contain the pre-compiled type information - -#include "stdafx.h" - -// TODO: reference any additional headers you need in STDAFX.H -// and not in this file diff --git a/windows/src/ext/scfontcombobox/scFontCombobox.pas b/windows/src/ext/scfontcombobox/scFontCombobox.pas index d7cb453cec..8a73899896 100644 --- a/windows/src/ext/scfontcombobox/scFontCombobox.pas +++ b/windows/src/ext/scfontcombobox/scFontCombobox.pas @@ -60,6 +60,18 @@ type TscFontType = (ftTrueTypeAnsi, ftTrueTypeSymbol, ftRaster); TscFontTypes = set of TscFontType; + TscFontRefreshHandler = class + private + FFontNames: TStrings; + FRefreshWnd: THandle; + procedure RefreshWndProc(var Message: TMessage); + procedure ReadFontNames; + public + constructor Create; + destructor Destroy; override; + property FontNames: TStrings read FFontNames; + end; + TscFontComboBox = class(TCustomComboBox) private BitmapTrueTypeAnsi : TBitmap; @@ -82,6 +94,8 @@ type FBlnShowPreviewFontName: Boolean; FBlnShowPreviewInEdit: Boolean; + class var FontRefreshHandler: TscFontRefreshHandler; + procedure SetPopupHeight(const Value: Integer); procedure SetPopupWidth(const Value: Integer); procedure SetFontName(const Value: String); @@ -183,6 +197,11 @@ implementation var FormExample : TFormExampleFont = nil; +const + FTV_UNKNOWN = 0; + FTV_ANSI = 1; + FTV_SYMBOL = 2; + FTV_RASTER = 3; procedure Register; begin @@ -234,6 +253,8 @@ begin IntCountUsed := 0; BlnDown := False; + Sorted := True; + Style := csOwnerDrawFixed; ItemHeight := 18; DropDownCount := 12; @@ -270,7 +291,7 @@ begin with Canvas do begin StrFont := Items.Names[Index]; - StrFontType := Items.Values[StrFont]; + StrFontType := Items.ValueFromIndex[Index]; FillRect(Rect); @@ -458,58 +479,26 @@ begin end; procedure TscFontComboBox.GetFontNames; -type - TFontRec = record - FontTypes : TscFontTypes; - FontItems : TStrings; - end; var - DC: HDC; - FontRec : TFontRec; - - function EnumFontsProc(var LogFont: TLogFont; var TextMetric: TTextMetric; - FontType: Integer; Data: Pointer): Integer; stdcall; - var - StrType : String; - begin - StrType :=''; - if FontType = TRUETYPE_FONTTYPE then - case LogFont.lfCharset of - ANSI_CHARSET : StrType := 'TA'; - SYMBOL_CHARSET : StrType := 'TS'; - end - else - if FontType = RASTER_FONTTYPE then - StrType := 'R'; - - // Check which fonts have to be added to listbox - if (LogFont.lfFaceName[0] <> '@') - and - (((StrType = 'TA') and (ftTrueTypeAnsi in TFontRec(Data^).FontTypes)) or - ((StrType = 'TS') and (ftTrueTypeSymbol in TFontRec(Data^).FontTypes)) or - ((StrType = 'R') and (ftRaster in TFontRec(Data^).FontTypes))) then - begin - TStrings(TFontRec(Data^).FontItems).Add(string(LogFont.lfFaceName)+'='+StrType); - end; - - Result := 1; - end; + i: Integer; begin - IntCountUsed := 0; - Items.Clear; - - // Make record to provide a pointer to the items - // of the listbox and the property fonttypes - FontRec.FontItems := Pointer(Items); - FontRec.FontTypes := FFontTypes; - - DC := GetDC(0); + Items.BeginUpdate; try - EnumFonts(DC, nil, @EnumFontsProc, @FontRec); + Items.Clear; + if not Assigned(FontRefreshHandler) then + FontRefreshHandler := TscFontRefreshHandler.Create; + for i := 0 to FontRefreshHandler.FontNames.Count - 1 do + begin + case Integer(FontRefreshHandler.FontNames.Objects[i]) of + FTV_UNKNOWN, //#2789 support all charsets + FTV_ANSI: Items.Add(FontRefreshHandler.FontNames[i]+'=TA'); + FTV_SYMBOL: Items.Add(FontRefreshHandler.FontNames[i]+'=TS'); + FTV_RASTER: Items.Add(FontRefreshHandler.FontNames[i]+'=R'); + end; + end; finally - ReleaseDC(0, DC); + Items.EndUpdate; end; - Sorted := True; end; //------------------------------------------------------------------------------ @@ -556,8 +545,76 @@ begin FormExample.PanelFontName.Visible := FBlnShowPreviewFontName; end; +{ TscFontRefreshHandler } + +constructor TscFontRefreshHandler.Create; +begin + inherited Create; + FFontNames := TStringList.Create(dupIgnore, True, False); + FRefreshWnd := AllocateHWnd(RefreshWndProc); + ReadFontNames; +end; + +destructor TscFontRefreshHandler.Destroy; +begin + FFontNames.Free; + DeallocateHWnd(FRefreshWnd); + inherited Destroy; +end; + +function EnumFontFamExProc(var LogFont: TLogFont; var TextMetric: TTextMetric; + FontType: DWORD; Data: LPARAM): Integer; stdcall; +var + FontTypeValue : Integer; +begin + FontTypeValue := FTV_UNKNOWN; + if FontType = TRUETYPE_FONTTYPE then + case LogFont.lfCharset of + ANSI_CHARSET : FontTypeValue := FTV_ANSI; //'TA' + SYMBOL_CHARSET : FontTypeValue := FTV_SYMBOL; //'TS' + else FontTypeValue := FTV_ANSI; //'TA'; #2789 support all charsets + end + else if FontType = RASTER_FONTTYPE then + FontTypeValue := FTV_RASTER; //'R' + + TscFontRefreshHandler(Data).FFontNames.AddObject(LogFont.lfFaceName, Pointer(FontTypeValue)); + Result := 1; +end; + +procedure TscFontRefreshHandler.ReadFontNames; +var + DC: HDC; + lf: TLogFont; +begin + FFontNames.Clear; + + FillChar(lf, sizeof(TLogFont), 0); + lf.lfCharSet := DEFAULT_CHARSET; + + DC := GetDC(0); + try + EnumFontFamiliesEx(DC, lf, @EnumFontFamExProc, NativeInt(Self), 0); + finally + ReleaseDC(0, DC); + end; +end; + +procedure TscFontRefreshHandler.RefreshWndProc(var Message: TMessage); +begin + try + if Message.Msg = WM_FONTCHANGE then + begin + ReadFontNames; + end; + Message.Result := DefWindowProc(FRefreshWnd, Message.Msg, Message.WParam, Message.LParam); + except + on E:Exception do Application.HandleException(E); + end; +end; + initialization finalization FreeAndNil(FormExample); + FreeAndNil(TscFontComboBox.FontRefreshHandler); end. diff --git a/windows/src/global/delphi/general/CompileErrorCodes.pas b/windows/src/global/delphi/general/CompileErrorCodes.pas index 43f9c935ca..78d6b42abd 100644 --- a/windows/src/global/delphi/general/CompileErrorCodes.pas +++ b/windows/src/global/delphi/general/CompileErrorCodes.pas @@ -147,6 +147,16 @@ const CERR_ContextAndIndexInvalidInMatchNomatch = $4062; CERR_140FeatureOnlyContextAndNotAnyWeb = $4063; + CERR_ExpansionMustFollowCharacterOrVKey = $4064; + CERR_VKeyExpansionMustBeFollowedByVKey = $4065; + CERR_CharacterExpansionMustBeFollowedByCharacter = $4066; + CERR_VKeyExpansionMustUseConsistentShift = $4067; + CERR_ExpansionMustBePositive = $4068; + + CERR_CasedKeysMustContainOnlyVirtualKeys = $4069; + CERR_CasedKeysMustNotIncludeShiftStates = $406A; + CERR_CasedKeysNotSupportedWithMnemonicLayout = $406B; + CWARN_TooManyWarnings = $2080; CWARN_OldVersion = $2081; CWARN_BitmapNotUsed = $2082; diff --git a/windows/src/global/delphi/general/CompilePackage.pas b/windows/src/global/delphi/general/CompilePackage.pas index 84f937b3ea..edf7532c7f 100644 --- a/windows/src/global/delphi/general/CompilePackage.pas +++ b/windows/src/global/delphi/general/CompilePackage.pas @@ -41,7 +41,6 @@ uses System.IniFiles, System.Zip, - BCP47Tag, OnlineConstants, VisualKeyboard, utilfiletypes, @@ -452,41 +451,17 @@ begin end; const - SKKeyboardPackageLanguageNonCanonical = 'The keyboard %0:s has a non-canonical language ID "%1:s" (%2:s), should be "%3:s".'; + SKKeyboardPackageLanguageNonCanonical = 'The keyboard %0:s has a non-canonical language tag "%1:s" (%2:s), should be "%3:s".'; + SKKeyboardShouldHaveAtLeastOneLanguage = 'The keyboard %0:s has no language tags. It should have at least one language tag.'; procedure TCompilePackage.CheckKeyboardLanguages; var k: TPackageKeyboard; - l: TPackageKeyboardLanguage; - NewID: string; - Tag, NewTag: TBCP47Tag; begin for k in pack.Keyboards do begin - for l in k.Languages do - begin - Tag := TBCP47Tag.Create(l.ID); - NewID := TCanonicalLanguageCodeUtils.FindBestTag(l.ID, False, False); - if NewID = '' then - begin - // We don't have enough data to validate this tag - Continue; - end; - - NewTag := TBCP47Tag.Create(NewID); - try - if (Tag.Script = '') and (NewTag.Script <> '') then - begin - // Only give non-canonical warning if the script tag is missing but should - // be present. - NewTag.Region := Tag.Region; // We don't care about region, don't give unhelpful info to developer - WriteMessage(plsWarning, Format(SKKeyboardPackageLanguageNonCanonical, [k.ID, l.ID, l.Name, NewTag.Tag])); - end; - finally - NewTag.Free; - Tag.Free; - end; - end; + if k.Languages.Count = 0 then + WriteMessage(plsWarning, Format(SKKeyboardShouldHaveAtLeastOneLanguage, [k.ID])); end; end; diff --git a/windows/src/global/delphi/general/Keyman.System.KeymanSentryClient.pas b/windows/src/global/delphi/general/Keyman.System.KeymanSentryClient.pas index 095f94ce84..4af511157c 100644 --- a/windows/src/global/delphi/general/Keyman.System.KeymanSentryClient.pas +++ b/windows/src/global/delphi/general/Keyman.System.KeymanSentryClient.pas @@ -106,7 +106,13 @@ begin // Last gasp, if we are in a development situation Result := GetEnvironmentVariable(KEYMAN_ROOT); if Result <> '' then + begin Result := IncludeTrailingPathDelimiter(Result) + DEV_SENTRY_PATH; + if (Result <> '') and FileExists(Result) then + Exit; + end; + + Result := ''; end; procedure TKeymanSentryClient.ClientAfterEvent(Sender: TObject; @@ -282,7 +288,7 @@ var reg: TRegistry; o: TSentryClientOptions; f: TSentryClientFlags; - RegKey: string; + path, RegKey: string; begin Assert(not Assigned(FInstance)); @@ -293,7 +299,12 @@ begin if not Enabled then Exit; - sentry_set_library_path(FindSentryDLL); + path := FindSentryDLL; + if path = '' then + // Cannot find sentry.dll + Exit; + + sentry_set_library_path(path); o.Debug := False; diff --git a/windows/src/global/delphi/general/KeymanPaths.pas b/windows/src/global/delphi/general/KeymanPaths.pas index a209a18079..8a2e611f92 100644 --- a/windows/src/global/delphi/general/KeymanPaths.pas +++ b/windows/src/global/delphi/general/KeymanPaths.pas @@ -13,6 +13,7 @@ type const S_CEF_DebugPath = 'Debug_CEFPath'; const S_CEF_EnvVar = 'KEYMAN_CEF4DELPHI_ROOT'; const S_CEF_SubFolder = 'cef\'; + const S_CEF_LibCef = 'libcef.dll'; const S_CEF_SubProcess = 'kmbrowserhost.exe'; public const S_KMShell = 'kmshell.exe'; @@ -212,36 +213,89 @@ begin end; class function TKeymanPaths.CEFPath: string; -begin - Result := GetDebugPath(S_CEF_DebugPath, ''); - if Result = '' then + + function AppendSlash(const path: string): string; begin - Result := GetEnvironmentVariable(S_CEF_EnvVar); - if Result = '' - then Result := KeymanDesktopInstallPath+S_CEF_SubFolder - else Result := IncludeTrailingPathDelimiter(Result); + Result := path; + if Result <> '' then + Result := IncludeTrailingPathDelimiter(path); end; + + function IsValidPath(const path: string): Boolean; + begin + Result := (path <> '') and FileExists(path + S_CEF_LibCef); + end; + +begin + // cef\ subfolder of executable path + Result := ExtractFilePath(ParamStr(0)) + S_CEF_SubFolder; + if IsValidPath(Result) then + Exit; + + // Debug_CEFPath registry setting + Result := AppendSlash(GetDebugPath(S_CEF_DebugPath, '')); + if IsValidPath(Result) then + Exit; + + // KEYMAN_CEF4DELPHI_ROOT environment variable + Result := AppendSlash(GetEnvironmentVariable(S_CEF_EnvVar)); + if IsValidPath(Result) then + Exit; + + // Same folder as executable + Result := ExtractFilePath(ParamStr(0)); + if IsValidPath(Result) then + Exit; + + // Keyman Desktop installation folder + cef\ + try + Result := KeymanDesktopInstallPath+S_CEF_SubFolder + except + on E:EKeymanPath do + Result := ''; + end; + if IsValidPath(Result) then + Exit; + + // Failed, could not find libcef.dll + Result := ''; end; class function TKeymanPaths.CEFSubprocessPath: string; +var + keyman_root: string; begin - // Normal install location - in Keyman install folder - Result := KeymanDesktopInstallPath(S_CEF_SubProcess); - if FileExists(Result) then Exit; - // Same folder as executable Result := ExtractFilePath(ParamStr(0)) + S_CEF_SubProcess; if FileExists(Result) then Exit; - // Source repo, bin folder - Result := ExtractFilePath(ParamStr(0)) + '..\desktop\' + S_CEF_SubProcess; - if FileExists(Result) then Exit; + // On developer machines, if we are running within the source repo, then use + // those paths + keyman_root := GetEnvironmentVariable('KEYMAN_ROOT'); + if (keyman_root <> '') and SameText(keyman_root, ParamStr(0).Substring(0, keyman_root.Length)) then + begin + // Source repo, bin folder + Result := IncludeTrailingPathDelimiter(keyman_root) + 'windows\bin\desktop\' + S_CEF_SubProcess; + if FileExists(Result) then Exit; - // Source repo, source folder - Result := ExtractFilePath(ParamStr(0)) + '..\..\desktop\kmbrowserhost\win32\debug\' + S_CEF_SubProcess; - if FileExists(Result) then Exit; + // Source repo, source folder + Result := IncludeTrailingPathDelimiter(keyman_root) + 'windows\bin\desktop\kmbrowserhost\win32\debug\' + S_CEF_SubProcess; + if FileExists(Result) then Exit; - Result := ExtractFilePath(ParamStr(0)) + '..\..\desktop\kmbrowserhost\win32\release\' + S_CEF_SubProcess; + Result := IncludeTrailingPathDelimiter(keyman_root) + 'windows\bin\desktop\kmbrowserhost\win32\release\' + S_CEF_SubProcess; + if FileExists(Result) then Exit; + end; + + // Check final install location - in Keyman for Windows install folder + try + Result := KeymanDesktopInstallPath(S_CEF_SubProcess); + except + on E:EKeymanPath do + Result := ''; + end; + if (Result <> '') and FileExists(Result) then Exit; + + Result := ''; end; class function TKeymanPaths.CEFDataPath(const mode: string): string; diff --git a/windows/src/global/delphi/general/utilsystem.pas b/windows/src/global/delphi/general/utilsystem.pas index 1e014f722e..2117b2fa81 100644 --- a/windows/src/global/delphi/general/utilsystem.pas +++ b/windows/src/global/delphi/general/utilsystem.pas @@ -329,11 +329,13 @@ var sei: TShellExecuteInfo; s: string; begin - s := '/select,'+FileName; + if not FileExists(FileName) + then s := '"'+ExtractFilePath(FileName)+'"' + else s := '"/select,'+FileName+'"'; FillChar(sei, SizeOf(sei), 0); sei.cbSize := SizeOf(TShellExecuteInfo); sei.lpVerb := 'open'; - sei.lpFile := 'explorer'; //PChar(s); + sei.lpFile := 'explorer'; sei.lpParameters := PChar(s); sei.nShow := SW_SHOWNORMAL; ShellExecuteEx(@sei); diff --git a/windows/src/global/delphi/lexicalmodels/Keyman.Developer.System.LexicalModelCompile.pas b/windows/src/global/delphi/lexicalmodels/Keyman.Developer.System.LexicalModelCompile.pas index a61f8379b0..2e8e2d17cb 100644 --- a/windows/src/global/delphi/lexicalmodels/Keyman.Developer.System.LexicalModelCompile.pas +++ b/windows/src/global/delphi/lexicalmodels/Keyman.Developer.System.LexicalModelCompile.pas @@ -28,6 +28,8 @@ begin cmdline := Format('"%skmlmc.cmd" "%s" -o "%s"', [ExtractFilePath(ParamStr(0)), infile, outfile]); Result := TUtilExecute.Console(cmdline, ExtractFileDir(infile), logtext, ec); + logtext := UTF8ToString(AnsiString(logtext)); + if not Result then begin ProjectFile.Project.Log(plsError, infile, diff --git a/windows/src/global/delphi/packages/Keyman.System.PackageInfoRefreshKeyboards.pas b/windows/src/global/delphi/packages/Keyman.System.PackageInfoRefreshKeyboards.pas index 87b8102ba2..fa1e1bd245 100644 --- a/windows/src/global/delphi/packages/Keyman.System.PackageInfoRefreshKeyboards.pas +++ b/windows/src/global/delphi/packages/Keyman.System.PackageInfoRefreshKeyboards.pas @@ -209,7 +209,7 @@ begin end else if not TCanonicalLanguageCodeUtils.IsCanonical(tag, msg, False, False) then begin - DoError(Format(SWarning_LanguageTagIsNotCanonical, [kbd.ID, lang.ID, msg]), plsWarning); + DoError(Format(SWarning_LanguageTagIsNotCanonical, [kbd.ID, lang.ID, msg]), plsInfo); end; finally Free; diff --git a/windows/src/global/inc/Comperr.h b/windows/src/global/inc/Comperr.h index 09af93c431..a8cc49ec7f 100644 --- a/windows/src/global/inc/Comperr.h +++ b/windows/src/global/inc/Comperr.h @@ -143,6 +143,16 @@ #define CERR_ContextAndIndexInvalidInMatchNomatch 0x00004062 #define CERR_140FeatureOnlyContextAndNotAnyWeb 0x00004063 +#define CERR_ExpansionMustFollowCharacterOrVKey 0x00004064 +#define CERR_VKeyExpansionMustBeFollowedByVKey 0x00004065 +#define CERR_CharacterExpansionMustBeFollowedByCharacter 0x00004066 +#define CERR_VKeyExpansionMustUseConsistentShift 0x00004067 +#define CERR_ExpansionMustBePositive 0x00004068 + +#define CERR_CasedKeysMustContainOnlyVirtualKeys 0x00004069 +#define CERR_CasedKeysMustNotIncludeShiftStates 0x0000406A +#define CERR_CasedKeysNotSupportedWithMnemonicLayout 0x0000406B + #define CWARN_TooManyWarnings 0x00002080 #define CWARN_OldVersion 0x00002081 #define CWARN_BitmapNotUsed 0x00002082 diff --git a/windows/src/global/inc/Compiler.h b/windows/src/global/inc/Compiler.h index 1123dc5389..be1a7a7a41 100644 --- a/windows/src/global/inc/Compiler.h +++ b/windows/src/global/inc/Compiler.h @@ -199,9 +199,11 @@ #define TSS_KEYBOARDVERSION 36 // &keyboardversion system store // I4140 #define TSS_KMW_EMBEDCSS 37 -#define TSS_TARGETS 38 +#define TSS_TARGETS 38 -#define TSS__MAX 38 +#define TSS_CASEDKEYS 39 + +#define TSS__MAX 39 /* wm_keyman_control_internal message control codes */ diff --git a/windows/src/global/inc/keyman64.h b/windows/src/global/inc/keyman64.h index ddd54b8c89..9b7a508991 100644 --- a/windows/src/global/inc/keyman64.h +++ b/windows/src/global/inc/keyman64.h @@ -428,6 +428,7 @@ void ReportActiveKeyboard(WORD wCommand); // I3933 // I3949 void SelectKeyboardHKL(DWORD hkl, BOOL foreground); // I3933 // I3949 // I4271 BOOL SelectKeyboardTSF(DWORD KeymanID, BOOL foreground); // I3933 // I3949 // I4271 BOOL ReportKeyboardChanged(WORD wCommand, DWORD dwProfileType, UINT langid, HKL hkl, GUID clsid, GUID guidProfile); +void ProcessModifierChange(UINT key, BOOL isUp, BOOL isExtended); // I4793 #endif diff --git a/windows/src/global/inc/unicode.h b/windows/src/global/inc/unicode.h index 84dd71f565..e7edaaf68a 100644 --- a/windows/src/global/inc/unicode.h +++ b/windows/src/global/inc/unicode.h @@ -11,9 +11,10 @@ #define Uni_UTF32ToSurrogate1(ch) (((ch) - 0x10000) / 0x400 + 0xD800) #define Uni_UTF32ToSurrogate2(ch) (((ch) - 0x10000) % 0x400 + 0xDC00) +#define Uni_UTF16ToUTF32(p) (Uni_IsSurrogate1(*(p)) && Uni_IsSurrogate2(*((p)+1)) ? Uni_SurrogateToUTF32(*(p), *((p)+1)) : *(p)) int wcssuppos(PWSTR p1, PWSTR p); // return p-p1, surr pairs as one chr -int GetSuppChar(WCHAR *p); // return *p, + *(++p) if surr pair) +int GetSuppChar(WCHAR *p); // return *p, + *(++p) if surr pair), equivalent to Uni_UTF16ToUTF32 int wcspos(PWSTR s, int pos); // return pos, inc for s[xx] is surr. PWSTR wcschrsupp(PWSTR buf, int x); WCHAR ByteToWChar(char b); diff --git a/windows/src/global/vc/VKScanCodes.cpp b/windows/src/global/vc/VKScanCodes.cpp index c10bfbfe73..93fb01ba90 100644 --- a/windows/src/global/vc/VKScanCodes.cpp +++ b/windows/src/global/vc/VKScanCodes.cpp @@ -17,7 +17,7 @@ 17 Dec 2013 - mcdurdin - I4006 - V9.0 - Remove old aiDefault code */ -#include "stdafx.h" // I4006 +#include "pch.h" // I4006 const UINT USVirtualKeyToScanCode[256] = { diff --git a/windows/src/global/vc/xstring.cpp b/windows/src/global/vc/xstring.cpp index f729f3d36a..3d1807c5be 100644 --- a/windows/src/global/vc/xstring.cpp +++ b/windows/src/global/vc/xstring.cpp @@ -47,7 +47,7 @@ PWSTR incxstr(PWSTR p) case CODE_INDEX: return p+2; case CODE_USE: return p+1; case CODE_DEADKEY: return p+1; - case CODE_EXTENDED: p += 2; while(*p != UC_SENTINEL_EXTENDEDEND) p++; return p+1; + case CODE_EXTENDED: p += 2; while(*p && *p != UC_SENTINEL_EXTENDEDEND) p++; return p+1; case CODE_CLEARCONTEXT: return p+1; case CODE_CALL: return p+1; case CODE_CONTEXTEX: return p+1; diff --git a/windows/src/test/mnemonic-to-positional/importkeyboard/importkeyboard/importkeyboard.cpp b/windows/src/test/mnemonic-to-positional/importkeyboard/importkeyboard/importkeyboard.cpp index 82594e4469..0d632986d9 100644 --- a/windows/src/test/mnemonic-to-positional/importkeyboard/importkeyboard/importkeyboard.cpp +++ b/windows/src/test/mnemonic-to-positional/importkeyboard/importkeyboard/importkeyboard.cpp @@ -28,7 +28,7 @@ const int ShiftStateMap[] = { ISVIRTUALKEY | RALTFLAG | K_SHIFTFLAG, 0, 0}; - + class DeadKey { private: WCHAR m_deadchar; @@ -100,11 +100,11 @@ public: UINT SC() { return this->m_sc; } - + std::wstring GetShiftState(ShiftState shiftState, bool capsLock) { return this->m_rgss[(UINT)shiftState][(capsLock ? 1 : 0)]; } - + void SetShiftState(ShiftState shiftState, std::wstring value, bool isDeadKey, bool capsLock) { this->m_rgfDeadKey[(UINT)shiftState][(capsLock ? 1 : 0)] = isDeadKey; this->m_rgss[(UINT)shiftState][(capsLock ? 1 : 0)] = value; @@ -167,13 +167,13 @@ public: } return true; } - + bool IsKeymanUsedKey() { return (this->m_vk >= 0x20 && this->m_vk <= 0x5F) || (this->m_vk >= 0x88); } UINT GetShiftStateValue(int capslock, int caps, ShiftState ss) { - return + return ShiftStateMap[(int)ss] | (capslock ? (caps ? CAPITALFLAG : NOTCAPITALFLAG) : 0); } @@ -217,7 +217,7 @@ public: //TODO: Do we need to put this rule out? } else if (this->m_rgfDeadKey[(int)ss, caps]) { // It's a dead key, append an @ sign. - key->dpContext = new WCHAR[1]; + key->dpContext = new WCHAR[1]; *key->dpContext = 0; key->ShiftFlags = this->GetShiftStateValue(capslock, caps, (ShiftState) ss); key->Key = this->VK(); @@ -228,7 +228,7 @@ public: *p++ = CODE_DEADKEY; *p++ = DeadKeyMap(st[0], deadkeys, deadkeyBase); *p = 0; - //sbRow.Append(string.Format("+ [{0}{1}] > dk({2:x4})\r\n", this.GetShiftStateName(capslock, caps, ss, IsKMW), + //sbRow.Append(string.Format("+ [{0}{1}] > dk({2:x4})\r\n", this.GetShiftStateName(capslock, caps, ss, IsKMW), // this.GetVKeyName((int)this.VK), (ushort)st[0])); //sbRow.Append(string.Format("\t{0:x4}@", ((ushort)st[0]))); return true; @@ -237,7 +237,7 @@ public: for (size_t ich = 0; ich < st.size(); ich++) { if(st[ich] < 0x20 || st[ich] == 0x7F) { isvalid=false; break; } } - + if(isvalid) { key->Key = this->VK(); key->ShiftFlags = this->GetShiftStateValue(capslock, caps, (ShiftState) ss); @@ -322,7 +322,7 @@ public: while (rc >= 0) { // We know that this is a dead key coming up, otherwise // this function would never have been called. If we do - // *not* get a dead key then that means the state is + // *not* get a dead key then that means the state is // messed up so we run again and again to clear it up. // Risk is technically an infinite loop but per Hiroyama // that should be impossible here. @@ -336,7 +336,7 @@ public: if (rc == 1) { // That was indeed a base character for our dead key. // And we now have a composite character. Let's run - // through one more time to get the actual base + // through one more time to get the actual base // character that made it all possible? WCHAR combchar = sbBuffer[0]; rc = ToUnicodeEx(rgKey[iKey]->VK(), rgKey[iKey]->SC(), lpKeyState, sbBuffer, _countof(sbBuffer), 0, hkl); @@ -352,9 +352,9 @@ public: if ((((ss == Ctrl) || (ss == ShftCtrl)) && (IsControlChar(basechar))) || (basechar == combchar)) { - // ToUnicodeEx has an internal knowledge about those - // VK_A ~ VK_Z keys to produce the control characters, - // when the conversion rule is not provided in keyboard + // ToUnicodeEx has an internal knowledge about those + // VK_A ~ VK_Z keys to produce the control characters, + // when the conversion rule is not provided in keyboard // layout files // Additionally, dead key state is lost for some of these @@ -433,13 +433,13 @@ int wmain(int argc, WCHAR* argv[]) { int cKeyboards = GetKeyboardLayoutList(0, NULL); HKL *rghkl = new HKL[cKeyboards]; - GetKeyboardLayoutList(cKeyboards, rghkl); + GetKeyboardLayoutList(cKeyboards, rghkl); HKL hkl = LoadKeyboardLayout(inputHKL, KLF_NOTELLSHELL); if(hkl == NULL) { puts("Sorry, that keyboard does not seem to be valid."); return 1; } - + BYTE lpKeyState[256];// = new KeysEx[256]; std::vector rgKey; //= new VirtualKey[256]; std::vector alDead; @@ -447,7 +447,7 @@ int wmain(int argc, WCHAR* argv[]) { rgKey.resize(256); // Scroll through the Scan Code (SC) values and get the valid Virtual Key (VK) - // values in it. Then, store the SC in each valid VK so it can act as both a + // values in it. Then, store the SC in each valid VK so it can act as both a // flag that the VK is valid, and it can store the SC value. for(UINT sc = 0x01; sc <= 0x7f; sc++) { VirtualKey *key = new VirtualKey(sc, hkl); @@ -471,7 +471,7 @@ int wmain(int argc, WCHAR* argv[]) { UINT sc = MapVirtualKeyEx(vk, 0, hkl); UINT vkL = MapVirtualKeyEx(sc, 1, hkl); UINT vkR = MapVirtualKeyEx(sc, 3, hkl); - if((vkL != vkR) && + if((vkL != vkR) && (vk != vkL)) { switch(vk) { case VK_LCONTROL: @@ -515,9 +515,9 @@ int wmain(int argc, WCHAR* argv[]) { if((rc == 1) && (ss == Ctrl || ss == ShftCtrl) && (rgKey[iKey]->VK() == ((UINT)sbBuffer[0] + 0x40))) { - // ToUnicodeEx has an internal knowledge about those - // VK_A ~ VK_Z keys to produce the control characters, - // when the conversion rule is not provided in keyboard + // ToUnicodeEx has an internal knowledge about those + // VK_A ~ VK_Z keys to produce the control characters, + // when the conversion rule is not provided in keyboard // layout files continue; } @@ -586,7 +586,7 @@ int wmain(int argc, WCHAR* argv[]) { } - + LPKEYBOARD kp = new KEYBOARD; memset(kp, 0, sizeof(KEYBOARD)); int nDeadkey = 0; @@ -631,7 +631,7 @@ int wmain(int argc, WCHAR* argv[]) { for (UINT iKey = 0; iKey < rgKey.size(); iKey++) { if ((rgKey[iKey] != NULL) && rgKey[iKey]->IsKeymanUsedKey() && (!rgKey[iKey]->IsEmpty())) { - // for each item, + // for each item, if(rgKey[iKey]->LayoutRow(loader.MaxShiftState(), &gp->dpKeyArray[nKeys], &alDead, nDeadkey)) { nKeys++; } @@ -671,14 +671,14 @@ int wmain(int argc, WCHAR* argv[]) { sp->dpName = NULL; sp->dwSystemID = 0; sp->dpString = new WCHAR[dk->Count() + 1]; - for(int j = 0; j < dk->Count(); j++) + for(int j = 0; j < dk->Count(); j++) sp->dpString[j] = dk->GetBaseCharacter(j); sp++; sp->dpName = NULL; sp->dwSystemID = 0; sp->dpString = new WCHAR[dk->Count() + 1]; - for(int j = 0; j < dk->Count(); j++) + for(int j = 0; j < dk->Count(); j++) sp->dpString[j] = dk->GetCombinedCharacter(j); sp++; @@ -704,7 +704,7 @@ int wmain(int argc, WCHAR* argv[]) { kkp++; } } - + } PWSTR incxstr(PWSTR p) @@ -724,7 +724,7 @@ PWSTR incxstr(PWSTR p) case CODE_INDEX: return p+2; case CODE_USE: return p+1; case CODE_DEADKEY: return p+1; - case CODE_EXTENDED: p += 2; while(*p != UC_SENTINEL_EXTENDEDEND) p++; return p+1; + case CODE_EXTENDED: p += 2; while(*p && *p != UC_SENTINEL_EXTENDEDEND) p++; return p+1; case CODE_CLEARCONTEXT: return p+1; case CODE_CALL: return p+1; case CODE_CONTEXTEX: return p+1; diff --git a/windows/src/test/mnemonic-to-positional/m-to-p/m-to-p/m-to-p.cpp b/windows/src/test/mnemonic-to-positional/m-to-p/m-to-p/m-to-p.cpp index a5cb4d2378..7f5891cc46 100644 --- a/windows/src/test/mnemonic-to-positional/m-to-p/m-to-p/m-to-p.cpp +++ b/windows/src/test/mnemonic-to-positional/m-to-p/m-to-p/m-to-p.cpp @@ -17,7 +17,7 @@ int wmain(int argc, wchar_t * argv[]) " layout file given by kbdfile.dll\n\n" " kbid should be a hexadecimal number e.g. 409 for US English\n"); - */ + */ return 1; } @@ -25,7 +25,7 @@ int wmain(int argc, wchar_t * argv[]) // 1. Load the keyman keyboard file - // 2. For each key on the system layout, determine its output character and perform a + // 2. For each key on the system layout, determine its output character and perform a // 1-1 replacement on the keyman keyboard of that character with the base VK + shift // state. This fixup will transform the char to a vk, which will avoid any issues // with the key. @@ -36,8 +36,8 @@ int wmain(int argc, wchar_t * argv[]) // rule for that deadkey, e.g. [K_LBRKT] > dk(c101) // // Next, update each rule that references the output from that deadkey to add an extra - // context deadkey at the end of the context match, e.g. 'a' dk(c101) + [K_SPACE] > 'b'. - // This will require a memory layout change for the .kmx file, plus fixups on the + // context deadkey at the end of the context match, e.g. 'a' dk(c101) + [K_SPACE] > 'b'. + // This will require a memory layout change for the .kmx file, plus fixups on the // context+output index offsets // // --> virtual character keys @@ -46,7 +46,7 @@ int wmain(int argc, wchar_t * argv[]) // switch the shift state from the VIRTUALCHARKEY to VIRTUALKEY, without changing any // other properties of the key. // - + // 3. Write the new keyman keyboard file if(!LoadNewLibrary(indll)) { @@ -283,7 +283,7 @@ void ConvertDeadkey(LPKEYBOARD kbd, WORD vk, UINT shift, WCHAR deadkey) { GetDeadkeys(deadkey, pdk = deadkeys); // returns array of [usvk, ch_out] pairs while(*pdk) { - // Look up the ch + // Look up the ch UINT vkUnderlying = VKUnderlyingLayoutToVKUS(*pdk); TranslateDeadkeyKeyboard(kbd, dkid, vkUnderlying, *(pdk+1), *(pdk+2)); pdk+=3; @@ -321,7 +321,7 @@ BOOL DoConvert(LPKEYBOARD kbd, LPWSTR kbid) { UINT vkUnderlying = VKUSToVKUnderlyingLayout(VKMap[i]); // Go through each of the shift states - base, shift, ctrl+alt, ctrl+alt+shift, [caps vs ncaps?] for(int j = 0; VKShiftState[j] != 0xFFFF; j++) { - + WCHAR ch = CharFromVK(vkUnderlying, VKShiftState[j], &DeadKey); //LogError("--- VK_%d -> VK_%d [%c] dk=%d", VKMap[i], vkUnderlying, ch == 0 ? 32 : ch, DeadKey); @@ -362,7 +362,7 @@ PWSTR incxstr(PWSTR p) case CODE_INDEX: return p+2; case CODE_USE: return p+1; case CODE_DEADKEY: return p+1; - case CODE_EXTENDED: p += 2; while(*p != UC_SENTINEL_EXTENDEDEND) p++; return p+1; + case CODE_EXTENDED: p += 2; while(*p && *p != UC_SENTINEL_EXTENDEDEND) p++; return p+1; case CODE_CLEARCONTEXT: return p+1; case CODE_CALL: return p+1; case CODE_CONTEXTEX: return p+1; diff --git a/windows/src/test/unit-tests/kmcomp/test.bat b/windows/src/test/unit-tests/kmcomp/test.bat index 646a950dc0..fdb496a759 100644 --- a/windows/src/test/unit-tests/kmcomp/test.bat +++ b/windows/src/test/unit-tests/kmcomp/test.bat @@ -37,6 +37,24 @@ call :should-fail "#4280: if should be at start of context 2" test_4280_if_start call :should-fail "#4280: nul should be at start of context 1" test_4280_nul_start_1.kmn || goto :eof call :should-fail "#4280: nul should be at start of context 2" test_4280_nul_start_2.kmn || goto :eof +call :should-pass "#4423: named code constants, various tests" test_namedcodeconstants.kmn || goto :eof + +call :should-pass "#2241: expansions" test_expansion.kmn || goto :eof +call :should-pass "#2241: expansions" test_expansion_1.kmn || goto :eof +:: the two files should be identical. +fc /b test_expansion.kmx test_expansion_1.kmx || goto :eof + +call :should-fail "#2241: invalid expansions" test_expansion_invalid.kmn || goto :eof +call :should-fail "#2241: expansion absurdly long" test_expansion_absurd.kmn || goto :eof + +call :should-pass "#2241: &CasedKeys" test_casedkeys.kmn || goto :eof +call :should-pass "#2241: &CasedKeys (chars)" test_casedkeys_chars.kmn || goto :eof + +call :should-fail "#2241: &CasedKeys (mnemonic 1)" test_casedkeys_mnemonic_1.kmn || goto :eof +call :should-fail "#2241: &CasedKeys (mnemonic 2)" test_casedkeys_mnemonic_2.kmn || goto :eof +call :should-fail "#2241: &CasedKeys (invalid chars 1)" test_casedkeys_invalid_1.kmn || goto :eof +call :should-fail "#2241: &CasedKeys (invalid chars 2)" test_casedkeys_invalid_2.kmn || goto :eof + goto :eof :should-pass diff --git a/windows/src/test/unit-tests/kmcomp/test_casedkeys.kmn b/windows/src/test/unit-tests/kmcomp/test_casedkeys.kmn new file mode 100644 index 0000000000..4962ca039f --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_casedkeys.kmn @@ -0,0 +1,39 @@ +c Tests cased keys rule expansions +store(&NAME) 'Test &CasedKeys' +begin Unicode > use(main) + +group(main) using keys + +store(&CasedKeys) [K_A] [K_E] [K_I] [K_O] [K_U] [K_1] .. [K_4] + ++ [K_A] > 'aaah' ++ [SHIFT K_A] > 'AAAH' + ++ [K_E] > 'eh' ++ [SHIFT K_E] > 'EH' + ++ [K_O] > 'oh' ++ [SHIFT K_O] > 'OH' + ++ [K_1] > 'small one' ++ [SHIFT K_1] > 'big one' + +c These ones should not be expanded, because they are unreferenced + ++ [K_5] > 'small five' ++ [SHIFT K_5] > 'big five' + ++ [K_C] > 'small c' ++ [SHIFT K_C] > 'big c' + +c These should not be expanded either, because they are capsified already + ++ [CAPS K_B] > 'Caps B' ++ [NCAPS K_B] > 'NCaps B' ++ [CAPS SHIFT K_B] > 'Caps Shift B' ++ [NCAPS SHIFT K_B] > 'NCaps Shift B' + ++ [CAPS K_I] > 'Caps I' ++ [NCAPS K_I] > 'NCaps I' ++ [CAPS SHIFT K_I] > 'Caps Shift I' ++ [NCAPS SHIFT K_I] > 'NCaps Shift I' diff --git a/windows/src/test/unit-tests/kmcomp/test_casedkeys_chars.kmn b/windows/src/test/unit-tests/kmcomp/test_casedkeys_chars.kmn new file mode 100644 index 0000000000..c159a02a9b --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_casedkeys_chars.kmn @@ -0,0 +1,41 @@ +c Tests cased keys rule expansions - character-based rules +store(&NAME) 'Test &CasedKeys' +begin Unicode > use(main) + +group(main) using keys + +store(&CasedKeys) 'a'..'z1/' + ++ 'a' > 'aaah' ++ 'A' > 'AAAH' + ++ 'e' > 'eh' ++ 'E' > 'EH' + ++ 'o' > 'oh' ++ 'O' > 'OH' + ++ '1' > 'small one' ++ '!' > 'big one' + ++ '/' > 'little slash' ++ '?' > 'big slash' + +c These ones should not be expanded, because they are unreferenced + ++ '5' > 'small five' ++ '%' > 'big five' + + + +c These should not be expanded either, because they are capsified already + ++ [CAPS K_B] > 'Caps B' ++ [NCAPS K_B] > 'NCaps B' ++ [CAPS SHIFT K_B] > 'Caps Shift B' ++ [NCAPS SHIFT K_B] > 'NCaps Shift B' + ++ [CAPS K_I] > 'Caps I' ++ [NCAPS K_I] > 'NCaps I' ++ [CAPS SHIFT K_I] > 'Caps Shift I' ++ [NCAPS SHIFT K_I] > 'NCaps Shift I' diff --git a/windows/src/test/unit-tests/kmcomp/test_casedkeys_invalid_1.kmn b/windows/src/test/unit-tests/kmcomp/test_casedkeys_invalid_1.kmn new file mode 100644 index 0000000000..8bc65cced6 --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_casedkeys_invalid_1.kmn @@ -0,0 +1,10 @@ +c Tests cased keys - invalid codes +store(&NAME) 'Test &CasedKeys' +begin Unicode > use(main) + +group(main) using keys + +store(&CasedKeys) U+1000 + ++ [K_A] > 'aaah' ++ [SHIFT K_A] > 'AAAH' diff --git a/windows/src/test/unit-tests/kmcomp/test_casedkeys_invalid_2.kmn b/windows/src/test/unit-tests/kmcomp/test_casedkeys_invalid_2.kmn new file mode 100644 index 0000000000..508466c07e --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_casedkeys_invalid_2.kmn @@ -0,0 +1,10 @@ +c Tests cased keys - invalid codes +store(&NAME) 'Test &CasedKeys' +begin Unicode > use(main) + +group(main) using keys + +store(&CasedKeys) [SHIFT K_1] + ++ [K_A] > 'aaah' ++ [SHIFT K_A] > 'AAAH' diff --git a/windows/src/test/unit-tests/kmcomp/test_casedkeys_mnemonic_1.kmn b/windows/src/test/unit-tests/kmcomp/test_casedkeys_mnemonic_1.kmn new file mode 100644 index 0000000000..40b0bbb966 --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_casedkeys_mnemonic_1.kmn @@ -0,0 +1,11 @@ +c Tests cased keys - invalid with mnemonic layout +store(&NAME) 'Test &CasedKeys' +store(&mnemoniclayout) '1' +begin Unicode > use(main) + +group(main) using keys + +store(&CasedKeys) [K_A] [K_E] [K_I] [K_O] [K_U] [K_1] .. [K_4] + ++ [K_A] > 'aaah' ++ [SHIFT K_A] > 'AAAH' diff --git a/windows/src/test/unit-tests/kmcomp/test_casedkeys_mnemonic_2.kmn b/windows/src/test/unit-tests/kmcomp/test_casedkeys_mnemonic_2.kmn new file mode 100644 index 0000000000..d2ca8faceb --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_casedkeys_mnemonic_2.kmn @@ -0,0 +1,11 @@ +c Tests cased keys - invalid with mnemonic layout +store(&NAME) 'Test &CasedKeys' +begin Unicode > use(main) + +group(main) using keys + +store(&CasedKeys) [K_A] [K_E] [K_I] [K_O] [K_U] [K_1] .. [K_4] +store(&mnemoniclayout) '1' + ++ [K_A] > 'aaah' ++ [SHIFT K_A] > 'AAAH' diff --git a/windows/src/test/unit-tests/kmcomp/test_expansion.kmn b/windows/src/test/unit-tests/kmcomp/test_expansion.kmn new file mode 100644 index 0000000000..0aba85d90c --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_expansion.kmn @@ -0,0 +1,15 @@ +c This file tests that expansions are valid, using kmdecomp to verify +store(&NAME) 'Test Expansions' +begin Unicode > use(main) + +group(main) using keys + +store(char_range) 'a' .. 'z' +store(alpha_not_bq) 'ac'..'pr'..'z' +store(key_range) [K_A] .. [K_e] + +store(schar_range) U+10001 .. U+10006 +store(schar_range2) U+1000 .. U+1006 + +store(char_range_silly) 'c' .. 'd' +store(key_range_silly) [K_C] .. [K_D] diff --git a/windows/src/test/unit-tests/kmcomp/test_expansion_1.kmn b/windows/src/test/unit-tests/kmcomp/test_expansion_1.kmn new file mode 100644 index 0000000000..ced7ebf23b --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_expansion_1.kmn @@ -0,0 +1,15 @@ +c This file is the baseline for test_expansion; the two files should compile to be identical +store(&NAME) 'Test Expansions' +begin Unicode > use(main) + +group(main) using keys + +store(char_range) 'abcdefghijklmnopqrstuvwxyz' +store(alpha_not_bq) 'acdefghijklmnoprstuvwxyz' +store(key_range) [K_A] [K_B] [K_C] [K_D] [K_E] + +store(schar_range) U+10001 U+10002 U+10003 U+10004 U+10005 U+10006 +store(schar_range2) U+1000 U+1001 U+1002 U+1003 U+1004 U+1005 U+1006 + +store(char_range_silly) 'cd' +store(key_range_silly) [K_C] [K_D] \ No newline at end of file diff --git a/windows/src/test/unit-tests/kmcomp/test_expansion_absurd.kmn b/windows/src/test/unit-tests/kmcomp/test_expansion_absurd.kmn new file mode 100644 index 0000000000..905093bc87 --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_expansion_absurd.kmn @@ -0,0 +1,7 @@ +c This file tests absurdly long expansions +store(&NAME) 'Test Expansions' +begin Unicode > use(main) + +group(main) using keys + +store(schar_range) U+0020 .. U+10FFFF diff --git a/windows/src/test/unit-tests/kmcomp/test_expansion_invalid.kmn b/windows/src/test/unit-tests/kmcomp/test_expansion_invalid.kmn new file mode 100644 index 0000000000..b721946537 --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_expansion_invalid.kmn @@ -0,0 +1,34 @@ +c This file tests that invalid expansions are caught +store(&NAME) 'Test Invalid Expansions' +begin Unicode > use(main) + +group(main) using keys + +c Backward ranges + +store(char_range_backward) 'c' .. 'a' +store(key_range_backward) [K_E] .. [K_A] + +store(char_range_empty) 'c' .. 'c' +store(key_range_empty) [K_E] .. [K_E] + +c Incomplete ranges + +store(range_open) .. +store(char_range_open_end) 'a' .. +store(char_range_open_start) .. 'z' + +store(key_range_open_end) [K_A] .. +store(key_range_open_start) .. [K_Z] + +c Inconsistent ranges + +store(char_range_to_key) 'a' .. [K_Z] +store(key_range_to_char) [K_A] .. 'z' + +store(char_range_to_statement) 'a' .. nul +store(key_range_to_statement) [K_A] .. nul +store(char_range_from_statement) nul .. 'a' +store(key_range_from_statement) nul .. [K_A] + +store(key_range_shift_inconsistent) [SHIFT K_A] .. [K_Z] diff --git a/windows/src/test/unit-tests/kmcomp/test_namedcodeconstants.kmn b/windows/src/test/unit-tests/kmcomp/test_namedcodeconstants.kmn new file mode 100644 index 0000000000..0c696448c3 --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_namedcodeconstants.kmn @@ -0,0 +1,25 @@ +c This tests #4423 -- single character stores that trigger the named character constants +c code and resulted in a codepage conversion error. + +store(&VERSION) '10.0' + +begin Unicode > use(main) + +group(main) using keys + +store(&includecodes) 'test_namedcodeconstants.txt' + +store(főő) '0' +store(bar) '1' +store(smp) U+12345 +store(😉) '🤪' + +$főő + 'a' > 'b' +$bar + 'a' > 'c' + +$hangul_syllable_go + 'a' > 'd' + +$mycode + 'a' > 'e' + +$smp + 'a' > 'f' +$😉 + 'a' > '😡' diff --git a/windows/src/test/unit-tests/kmcomp/test_namedcodeconstants.txt b/windows/src/test/unit-tests/kmcomp/test_namedcodeconstants.txt new file mode 100644 index 0000000000..21fe039f6b --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_namedcodeconstants.txt @@ -0,0 +1 @@ +0041;MYCODE;test code for 'A' diff --git a/windows/src/test/unit-tests/kmcomp/tests.kpj b/windows/src/test/unit-tests/kmcomp/tests.kpj index 7ecc383d61..53c02033f5 100644 --- a/windows/src/test/unit-tests/kmcomp/tests.kpj +++ b/windows/src/test/unit-tests/kmcomp/tests.kpj @@ -86,30 +86,6 @@ .kmn
- - id_5f9e583f7392151b3c3a7d4577b31a4f - test_valid.kmx - test_valid.kmx - - .kmx - id_03dbd77eff6b6b923de6983fab2bd9d0 - - - id_416e28ba6ecd3550e599b16a2c37e864 - test_invalid_kmx.kmx - test_invalid_kmx.kmx - - .kmx - id_98467bd680620f99f9adeb5347541b6a - - - id_ee200303f52275647138898037a80e0e - test.doc - test.doc - - .doc - id_98467bd680620f99f9adeb5347541b6a - id_4c59db88c3869d699c9df7f46731a97a test_4280_if_start_1.kmn @@ -142,5 +118,137 @@ .kmn
+ + id_068020cc7b6376ed155d5b9fc0215cc6 + test_namedcodeconstants.kmn + test_namedcodeconstants.kmn + 1.0 + .kmn +
+ + + id_a45739f276db90d247a6b60b0faa281c + test_expansion.kmn + test_expansion.kmn + 1.0 + .kmn +
+ Test Expansions +
+
+ + id_103775d19d3ec8272e57e2e103960589 + test_expansion_1.kmn + test_expansion_1.kmn + 1.0 + .kmn +
+ Test Expansions +
+
+ + id_bb17cc25a8735482bba0fc34223c9fe8 + test_expansion_invalid.kmn + test_expansion_invalid.kmn + 1.0 + .kmn +
+ Test Invalid Expansions +
+
+ + id_c35b5f806c85252da940a60449e089e0 + test_casedkeys.kmn + test_casedkeys.kmn + 1.0 + .kmn +
+ Test &CasedKeys +
+
+ + id_3a9d89cd15f05037ef2d5a71fd78ed35 + test_casedkeys_chars.kmn + test_casedkeys_chars.kmn + 1.0 + .kmn +
+ Test &CasedKeys +
+
+ + id_70997591a064ca387dac08622a04c392 + test_casedkeys_invalid_1.kmn + test_casedkeys_invalid_1.kmn + 1.0 + .kmn +
+ Test &CasedKeys +
+
+ + id_8551121eb12d38673567587b753908f7 + test_casedkeys_invalid_2.kmn + test_casedkeys_invalid_2.kmn + 1.0 + .kmn +
+ Test &CasedKeys +
+
+ + id_2e7200332135cd7d8ce6109a31be82fe + test_casedkeys_mnemonic_1.kmn + test_casedkeys_mnemonic_1.kmn + 1.0 + .kmn +
+ Test &CasedKeys +
+
+ + id_7d86f03f3e1c308ab03429ac05556e5a + test_casedkeys_mnemonic_2.kmn + test_casedkeys_mnemonic_2.kmn + 1.0 + .kmn +
+ Test &CasedKeys +
+
+ + id_5f9e583f7392151b3c3a7d4577b31a4f + test_valid.kmx + test_valid.kmx + + .kmx + id_03dbd77eff6b6b923de6983fab2bd9d0 + + + id_416e28ba6ecd3550e599b16a2c37e864 + test_invalid_kmx.kmx + test_invalid_kmx.kmx + + .kmx + id_98467bd680620f99f9adeb5347541b6a + + + id_ee200303f52275647138898037a80e0e + test.doc + test.doc + + .doc + id_98467bd680620f99f9adeb5347541b6a + + + id_9d5ea0b72f9733e0e70e04d5b3c790af + test_expansion_absurd.kmn + test_expansion_absurd.kmn + 1.0 + .kmn +
+ Test Expansions +
+