diff --git a/HISTORY.md b/HISTORY.md index 2342633ded..336b19f38b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,28 @@ # Keyman Version History +## 17.0.220 alpha 2023-11-30 + +* fix(core): set_if_needed updates an empty cached context (#10098) +* fix(core): check for null termination (#10101) + +## 17.0.219 alpha 2023-11-29 + +* fix(developer): path separator for kmc-package (#10064) +* fix(developer): projects 2.0 internal path enumeration (#10016) +* fix(web): Fix attachment-api tests (#10085) +* fix(web): Also move source map (#10089) + +## 17.0.218 alpha 2023-11-27 + +* feat(developer): ldml: err/hint on illegal/pua chars (#10029) + +## 17.0.217 alpha 2023-11-24 + +* feat(developer): warn on usage of virtual keys in rule output (#10062) +* fix(core): memory management of options in action struct (#10073) +* chore(linux): Update debian changelog (#10047) +* chore(core): Add test keyboard for text selection tests (#10026) + ## 17.0.216 alpha 2023-11-23 * fix(common): kmx struct alignment (#9977) diff --git a/VERSION.md b/VERSION.md index 980da1ad1a..62e9701dda 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.217 \ No newline at end of file +17.0.221 \ No newline at end of file diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 7930c76f11..8c4fd7da8f 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -34,6 +34,10 @@ function init() { keyman.getOskHeight = getOskHeight; keyman.getOskWidth = getOskWidth; keyman.beepKeyboard = beepKeyboard; + + // Readies the keyboard stub for instant loading during the init process. + KeymanWeb.registerStub(JSON.parse(jsInterface.initialKeyboard())); + keyman.init({ 'embeddingApp':device, 'fonts':'packages/', @@ -102,7 +106,7 @@ function setBannerHeight(h) { if (keyman.osk) { keyman.osk.bannerView.activeBannerHeight = bannerHeight; - } + } } // Refresh KMW's OSK @@ -149,8 +153,8 @@ function onStateChange(change) { keyman.refreshOskLayout(); fragmentToggle = (fragmentToggle + 1) % 100; - if(change != 'configured') { // doesn't change the display; only initiates suggestions. - window.location.hash = 'refreshBannerHeight-'+fragmentToggle+'+change='+change; + if(change != 'configured') { + window.location.hash = 'refreshBannerHeight-'+fragmentToggle; } } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index 215630cd03..d17c331697 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -66,7 +66,7 @@ final class KMKeyboard extends WebView { private boolean shouldIgnoreSelectionChange = false; protected KeyboardType keyboardType = KeyboardType.KEYBOARD_TYPE_UNDEFINED; - protected ArrayList javascriptAfterLoad = new ArrayList(); + protected ArrayList javascriptAfterLoad = new ArrayList<>(); private static String currentKeyboard = null; @@ -214,10 +214,8 @@ final class KMKeyboard extends WebView { } // Send console errors to Sentry in case they're missed by KMW sentryManager - // (Ignoring spurious message "No keyboard stubs exist = ...") - // 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"))) { + if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) { // Make Toast notification of error and send log about falling back to default keyboard (ignore language ID) // Sanitize sourceId info String NAVIGATION_PATTERN = "^(.*)?(keyboard\\.html#[^-]+)-.*$"; @@ -302,15 +300,21 @@ final class KMKeyboard extends WebView { this.postDelayed(new Runnable() { @Override public void run() { - if(javascriptAfterLoad.size() > 0) { - loadUrl("javascript:" + javascriptAfterLoad.get(0)); - javascriptAfterLoad.remove(0); - // Make sure we didn't reset the page in the middle of the queue! - if(keyboardSet) { - if (javascriptAfterLoad.size() > 0) { - callJavascriptAfterLoad(); - } - } + StringBuilder allCalls = new StringBuilder(); + if(javascriptAfterLoad.size() == 0) { + return; + } + + while(javascriptAfterLoad.size() > 0) { + String entry = javascriptAfterLoad.remove(0); + allCalls.append(entry); + allCalls.append(";"); + } + + loadUrl("javascript:" + allCalls.toString()); + + if(javascriptAfterLoad.size() > 0 && keyboardSet) { + callJavascriptAfterLoad(); } } }, 1); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java index 6c959b0056..a8c5291f36 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardJSHandler.java @@ -1,6 +1,7 @@ package com.keyman.engine; import android.content.Context; +import android.content.SharedPreferences; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -20,6 +21,7 @@ import android.webkit.JavascriptInterface; import static android.content.Context.VIBRATOR_SERVICE; import com.keyman.engine.KMManager.KeyboardType; +import com.keyman.engine.data.Keyboard; import com.keyman.engine.util.CharSequenceUtil; import com.keyman.engine.util.KMLog; @@ -62,6 +64,21 @@ public class KMKeyboardJSHandler { return kbWidth; } + @JavascriptInterface + public String initialKeyboard() { + // Note: KMManager.getCurrentKeyboard() (and similar) will throw errors until the host-page is first fully + // loaded and has set a keyboard. To allow the host-page to have earlier access, we instead get the stored + // keyboard index directly. + SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0); + if (index < 0) { + index = 0; + } + + Keyboard kbd = KMManager.getKeyboardInfo(this.context, index); + return kbd.toStub(context); + } + // This annotation is required in Jelly Bean and later: @JavascriptInterface public void beepKeyboard() { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java index 4d458bd061..7a315ff6e1 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java @@ -164,16 +164,13 @@ public final class KMKeyboardWebViewClient extends WebViewClient { // for the rest of the lifetime of this keyboard instance. kmKeyboard.setShouldShowHelpBubble(false); } else if (url.indexOf("refreshBannerHeight") >= 0) { - int start = url.indexOf("change=") + 7; - String change = url.substring(start); - boolean isModelActive = change.equals("active"); // appContext instead of context? SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); boolean modelPredictionPref = false; if (KMManager.currentLexicalModel != null) { modelPredictionPref = prefs.getBoolean(KMManager.getLanguagePredictionPreferenceKey(KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), true); } - KMManager.setBannerOptions(isModelActive && modelPredictionPref); + KMManager.setBannerOptions(modelPredictionPref); RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams(); kmKeyboard.setLayoutParams(params); } else if (url.indexOf("suggestPopup") >= 0) { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index 10e1068908..7f5f6fed2c 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -777,12 +777,10 @@ public final class KMManager { // KMKeyboard if (InAppKeyboard != null) { RelativeLayout.LayoutParams params = getKeyboardLayoutParams(); - InAppKeyboard.setLayoutParams(params); InAppKeyboard.onConfigurationChanged(newConfig); } if (SystemKeyboard != null) { RelativeLayout.LayoutParams params = getKeyboardLayoutParams(); - SystemKeyboard.setLayoutParams(params); SystemKeyboard.onConfigurationChanged(newConfig); } } @@ -1388,12 +1386,14 @@ public final class KMManager { RelativeLayout.LayoutParams params; if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP) && !InAppKeyboard.shouldIgnoreTextChange() && modelFileExists) { params = getKeyboardLayoutParams(); - InAppKeyboard.setLayoutParams(params); + + // Do NOT re-layout here; it'll be triggered once the banner loads. InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); } if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && !SystemKeyboard.shouldIgnoreTextChange() && modelFileExists) { params = getKeyboardLayoutParams(); - SystemKeyboard.setLayoutParams(params); + + // Do NOT re-layout here; it'll be triggered once the banner loads. SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); } return true; diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardPickerActivity.java b/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardPickerActivity.java index fff24c3b56..20d414aca8 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardPickerActivity.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KeyboardPickerActivity.java @@ -51,6 +51,7 @@ import android.widget.Toast; import androidx.appcompat.widget.Toolbar; public final class KeyboardPickerActivity extends BaseActivity { + private boolean hasDeleted = false; //TODO: view instances should not be static private static Toolbar toolbar = null; @@ -123,7 +124,7 @@ public final class KeyboardPickerActivity extends BaseActivity { listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - switchKeyboard(position,dismissOnSelect && ! KMManager.isTestMode()); + switchKeyboard(position); if (dismissOnSelect) finish(); } @@ -140,6 +141,7 @@ public final class KeyboardPickerActivity extends BaseActivity { public boolean onMenuItemClick(MenuItem item) { if (item.getItemId() == R.id.popup_delete) { deleteKeyboard(context, position); + KeyboardPickerActivity.this.hasDeleted = true; return true; } else { return false; @@ -267,11 +269,16 @@ public final class KeyboardPickerActivity extends BaseActivity { protected void onPause() { super.onPause(); - if (KMManager.InAppKeyboard != null) { - KMManager.InAppKeyboard.loadKeyboard(); - } - if (KMManager.SystemKeyboard != null) { - KMManager.SystemKeyboard.loadKeyboard(); + if (this.hasDeleted) { + this.hasDeleted = false; + + if (KMManager.InAppKeyboard != null) { + KMManager.InAppKeyboard.loadKeyboard(); + } + + if (KMManager.SystemKeyboard != null) { + KMManager.SystemKeyboard.loadKeyboard(); + } } } @@ -335,7 +342,7 @@ public final class KeyboardPickerActivity extends BaseActivity { * @param position the keyboard index in list * @param aPrepareOnly prepare switch, it is executed on keyboard reload */ - private static void switchKeyboard(int position, boolean aPrepareOnly) { + private static void switchKeyboard(int position) { setSelection(position); int size = KeyboardController.getInstance().get().size(); int listPosition = (position >= size) ? size-1 : position; @@ -344,10 +351,7 @@ public final class KeyboardPickerActivity extends BaseActivity { String kbId = kbInfo.getKeyboardID(); String langId = kbInfo.getLanguageID(); String kbName = kbInfo.getKeyboardName(); - if(aPrepareOnly) - KMManager.prepareKeyboardSwitch(pkgId, kbId, langId, kbName); - else - KMManager.setKeyboard(kbInfo); + KMManager.setKeyboard(kbInfo); } protected static boolean addKeyboard(Context context, Keyboard keyboardInfo) { @@ -449,7 +453,7 @@ public final class KeyboardPickerActivity extends BaseActivity { adapter.notifyDataSetChanged(); } if (position == curKbPos) { - switchKeyboard(0,false); + switchKeyboard(0); } else if(listView != null) { // A bit of a hack, since LanguageSettingsActivity calls this method too. curKbPos = KeyboardController.getInstance().getKeyboardIndex(KMKeyboard.currentKeyboard()); setSelection(curKbPos); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java index 984284fe09..0dbfe5d023 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/data/Keyboard.java @@ -16,11 +16,13 @@ import com.keyman.engine.util.FileUtils; import com.keyman.engine.util.KMLog; import com.keyman.engine.util.KMString; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.Serializable; +import java.util.ArrayList; public class Keyboard extends LanguageResource implements Serializable { private static final String TAG = "Keyboard"; @@ -171,6 +173,91 @@ public class Keyboard extends LanguageResource implements Serializable { return o; } + private String getKeyboardRoot(Context context) { + String keyboardRoot = context.getDir("data", Context.MODE_PRIVATE).toString() + + File.separator; + + if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { + return keyboardRoot + KMManager.KMDefault_UndefinedPackageID + File.separator; + } else { + return keyboardRoot + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator; + } + } + + public String getKeyboardPath(Context context) { + String keyboardID = this.getKeyboardID(); + String keyboardVersion = this.getVersion(); + if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) { + return getKeyboardRoot(context) + keyboardID + "-" + keyboardVersion + ".js"; + } else { + return getKeyboardRoot(context) + keyboardID + ".js"; + } + } + + public String toStub(Context context) { + JSONObject stubObj = new JSONObject(); + + try { + stubObj.put("KN", this.getKeyboardName()); + stubObj.put("KI", "Keyboard_" + this.getKeyboardID()); + stubObj.put("KLC", this.getLanguageID()); + stubObj.put("KL", this.getLanguageName()); + stubObj.put("KF", this.getKeyboardPath(context)); + stubObj.put("KP", this.getPackageID()); + + String displayFont = this.getFont(); + if(displayFont != null) { + stubObj.put("KFont", this.buildDisplayFontObject(displayFont, context)); + } + + String oskFont = this.getOSKFont(); + if(oskFont != null) { + stubObj.put("KOskFont", this.buildDisplayFontObject(oskFont, context)); + } + + String displayName = this.getDisplayName(); + if(displayName != null) { + stubObj.put("displayName", displayName); + } + + return stubObj.toString(); + } catch(JSONException e) { + KMLog.LogException(TAG, "", e); + return null; + } + } + + /** + * Take a font JSON object and adjust to pass to JS + * 1. Replace "source" keys for "files" keys + * 2. Create full font paths for .ttf or .svg + * @param font String font JSON object as a string + * @return JSONObject of modified font information with full paths. If font is invalid, return `null` + */ + private JSONObject buildDisplayFontObject(String font, Context context) { + if(font == null || font.equals("")) { + return null; + } + + String keyboardRoot = this.getKeyboardRoot(context); + + try { + if (FileUtils.hasFontExtension(font)) { + JSONObject jfont = new JSONObject(); + jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length() - 4)); + JSONArray jfiles = new JSONArray(); + jfiles.put(keyboardRoot + font); + jfont.put(KMManager.KMKey_FontFiles, jfiles); + return jfont; + } else { + return null; + } + } catch (JSONException e) { + KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e); + return null; + } + } + /** * Get the fallback keyboard. If never specified, use sil_euro_latin * @param context Context diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java b/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java index 1111d92978..bc749dc265 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/logic/ResourcesUpdateTool.java @@ -562,7 +562,9 @@ public class ResourcesUpdateTool implements KeyboardEventHandler.OnKeyboardDownl void tryFinalizeUpdate() { if (openUpdates.isEmpty()) { - + // Trigger a host-page reset - we need to transition to the up-to-date versions. + // TODO: make it smoother. Documented as #11097. + KMManager.clearKeyboardCache(); if (failedUpdateCount > 0) { BaseActivity.makeToast(currentContext, R.string.update_failed, Toast.LENGTH_SHORT); diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/HISTORY.md b/common/test/keyboards/text_selection_tests_keyboard_9073/HISTORY.md new file mode 100644 index 0000000000..e7e7675aa4 --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/HISTORY.md @@ -0,0 +1,6 @@ +Text Selection Tests Keyboard Change History +==================== + +1.0 (2023-11-14) +---------------- +* Created by Keyman Team diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/LICENSE.md b/common/test/keyboards/text_selection_tests_keyboard_9073/LICENSE.md new file mode 100644 index 0000000000..f199066a02 --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +© 2023 Keyman Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/README.md b/common/test/keyboards/text_selection_tests_keyboard_9073/README.md new file mode 100644 index 0000000000..1ab216d60f --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/README.md @@ -0,0 +1,31 @@ +Text Selection Tests Keyboard keyboard +============== + +Version 1.0 + +Description +----------- +Text Selection Tests Keyboard generated from template + +Links +----- +https://github.com/keymanapp/keyman/issues/9073 + +Copyright +--------- +See [LICENSE.md](LICENSE.md) + +Supported Platforms +------------------- + * Windows + * macOS + * Linux + * Web + * iPhone + * iPad + * Android phone + * Android tablet + * Mobile devices + * Desktop devices + * Tablet devices + diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/source/readme.htm b/common/test/keyboards/text_selection_tests_keyboard_9073/source/readme.htm new file mode 100644 index 0000000000..1d87395da8 --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/source/readme.htm @@ -0,0 +1,24 @@ + + + + + + Text Selection Tests Keyboard + + + + +

Text Selection Tests Keyboard

+ +

+ Text Selection Tests Keyboard 1.0 generated from template. +

+ +

© Keyman Team

+ + + diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.ico b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.ico new file mode 100644 index 0000000000..6a5271df0c Binary files /dev/null and b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.ico differ diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.keyman-touch-layout b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.keyman-touch-layout new file mode 100644 index 0000000000..6d02edfad8 --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.keyman-touch-layout @@ -0,0 +1,532 @@ +{ + "tablet": { + "displayUnderlying": false, + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "1", + "nextlayer": "shift" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + }, + { + "id": "K_HYPHEN", + "text": "-" + }, + { + "id": "K_EQUAL", + "text": "=" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 100, + "sp": 1 + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "q", + "pad": 75 + }, + { + "id": "K_W", + "text": "w" + }, + { + "id": "K_E", + "text": "e" + }, + { + "id": "K_R", + "text": "r" + }, + { + "id": "K_T", + "text": "t" + }, + { + "id": "K_Y", + "text": "y" + }, + { + "id": "K_U", + "text": "u" + }, + { + "id": "K_I", + "text": "i" + }, + { + "id": "K_O", + "text": "o" + }, + { + "id": "K_P", + "text": "p" + }, + { + "id": "K_LBRKT", + "text": "[" + }, + { + "id": "K_RBRKT", + "text": "]" + }, + { + "id": "T_new_136", + "width": 10, + "sp": 10 + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "dk(1)" + }, + { + "id": "K_A", + "text": "a" + }, + { + "id": "K_S", + "text": "s" + }, + { + "id": "K_D", + "text": "d" + }, + { + "id": "K_F", + "text": "f" + }, + { + "id": "K_G", + "text": "g" + }, + { + "id": "K_H", + "text": "h" + }, + { + "id": "K_J", + "text": "j" + }, + { + "id": "K_K", + "text": "k" + }, + { + "id": "K_L", + "text": "l" + }, + { + "id": "K_COLON", + "text": ";" + }, + { + "id": "K_QUOTE", + "text": "'" + }, + { + "id": "K_BKSLASH", + "text": "\\" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": 160, + "sp": 1, + "nextlayer": "shift" + }, + { + "id": "K_oE2", + "text": "\\" + }, + { + "id": "K_Z", + "text": "z" + }, + { + "id": "K_X", + "text": "x" + }, + { + "id": "K_C", + "text": "c" + }, + { + "id": "K_V", + "text": "v" + }, + { + "id": "K_B", + "text": "b" + }, + { + "id": "K_N", + "text": "n" + }, + { + "id": "K_M", + "text": "m" + }, + { + "id": "K_COMMA", + "text": "," + }, + { + "id": "K_PERIOD", + "text": "." + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "id": "T_new_162", + "width": 10, + "sp": 10 + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 140, + "sp": 1 + }, + { + "id": "K_SPACE", + "width": 930 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 145, + "sp": 1 + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "!" + }, + { + "id": "K_2", + "text": "@" + }, + { + "id": "K_3", + "text": "#" + }, + { + "id": "K_4", + "text": "$" + }, + { + "id": "K_5", + "text": "%" + }, + { + "id": "K_6", + "text": "^" + }, + { + "id": "K_7", + "text": "&" + }, + { + "id": "K_8", + "text": "*" + }, + { + "id": "K_9", + "text": "(" + }, + { + "id": "K_0", + "text": ")" + }, + { + "id": "K_HYPHEN", + "text": "_" + }, + { + "id": "K_EQUAL", + "text": "+" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": 100, + "sp": 1 + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_Q", + "text": "Q", + "pad": 75 + }, + { + "id": "K_W", + "text": "W" + }, + { + "id": "K_E", + "text": "E" + }, + { + "id": "K_R", + "text": "R" + }, + { + "id": "K_T", + "text": "T" + }, + { + "id": "K_Y", + "text": "Y" + }, + { + "id": "K_U", + "text": "U" + }, + { + "id": "K_I", + "text": "I" + }, + { + "id": "K_O", + "text": "O" + }, + { + "id": "K_P", + "text": "P" + }, + { + "id": "K_LBRKT", + "text": "{" + }, + { + "id": "K_RBRKT", + "text": "}" + }, + { + "id": "T_new_246", + "width": 10, + "sp": 10 + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_BKQUOTE", + "text": "~" + }, + { + "id": "K_A", + "text": "A" + }, + { + "id": "K_S", + "text": "S" + }, + { + "id": "K_D", + "text": "D" + }, + { + "id": "K_F", + "text": "F" + }, + { + "id": "K_G", + "text": "G" + }, + { + "id": "K_H", + "text": "H" + }, + { + "id": "K_J", + "text": "J" + }, + { + "id": "K_K", + "text": "K" + }, + { + "id": "K_L", + "text": "L" + }, + { + "id": "K_COLON", + "text": ":" + }, + { + "id": "K_QUOTE", + "text": "\"" + }, + { + "id": "K_BKSLASH", + "text": "|" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": 160, + "sp": 1, + "nextlayer": "default" + }, + { + "id": "K_oE2", + "text": "|" + }, + { + "id": "K_Z", + "text": "Z" + }, + { + "id": "K_X", + "text": "X" + }, + { + "id": "K_C", + "text": "C" + }, + { + "id": "K_V", + "text": "V" + }, + { + "id": "K_B", + "text": "B" + }, + { + "id": "K_N", + "text": "N" + }, + { + "id": "K_M", + "text": "M" + }, + { + "id": "K_COMMA", + "text": "<" + }, + { + "id": "K_PERIOD", + "text": ">" + }, + { + "id": "K_SLASH", + "text": "?" + }, + { + "id": "T_new_272", + "width": 10, + "sp": 10 + } + ] + }, + { + "id": 5, + "key": [ + { + "id": "K_LOPT", + "text": "*Menu*", + "width": 140, + "sp": 1 + }, + { + "id": "K_SPACE", + "width": 930 + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": 145, + "sp": 1 + } + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kmn b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kmn new file mode 100644 index 0000000000..b40278a930 --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kmn @@ -0,0 +1,26 @@ +c text_selection_tests_keyboard_9073 generated from template at 2023-11-14 15:23:49 +c with name "Text Selection Tests Keyboard" +store(&VERSION) '10.0' +store(&NAME) 'Text Selection Tests Keyboard' +store(©RIGHT) '© Keyman Team' +store(&KEYBOARDVERSION) '1.0' +store(&TARGETS) 'any' +store(&BITMAP) 'text_selection_tests_keyboard_9073.ico' +store(&VISUALKEYBOARD) 'text_selection_tests_keyboard_9073.kvks' +store(&LAYOUTFILE) 'text_selection_tests_keyboard_9073.keyman-touch-layout' + +begin Unicode > use(main) + +group(main) using keys +'^' + [K_A] > 'â' +'^' + [SHIFT K_A] > 'Â' +'^' + [K_BKSP] > 'foo' + ++ '`' > dk(1) + ++ [K_T] > U+0009 c TAB + +'a' dk(1) 'b' + [K_BKSP] > 'ok1' +'a' 'b' + [K_BKSP] > 'fail1' +'a' dk(1) + [K_BKSP] > 'fail2' +dk(1) + 'o' > 'ok3' diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kps b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kps new file mode 100644 index 0000000000..999160d84d --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kps @@ -0,0 +1,67 @@ + + + + 16.0.142.0 + 7.0 + + + + readme.htm + + + + + + + + + + Text Selection Tests Keyboard + © Keyman Team + Keyman Team + + + + + ..\build\text_selection_tests_keyboard_9073.kmx + + 0 + .kmx + + + ..\build\text_selection_tests_keyboard_9073.js + + 0 + .js + + + ..\build\text_selection_tests_keyboard_9073.kvk + + 0 + .kvk + + + welcome.htm + + 0 + .htm + + + readme.htm + + 0 + .htm + + + + + Text Selection Tests Keyboard + text_selection_tests_keyboard_9073 + 1.0 + + English + + + + + diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kvks b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kvks new file mode 100644 index 0000000000..9b69397b35 --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kvks @@ -0,0 +1,110 @@ + + +
+ 10.0 + text_selection_tests_keyboard_9073 + +
+ + + dk(1) + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 + - + = + q + w + e + r + t + y + u + i + o + p + [ + ] + \ + a + s + d + f + g + h + j + k + l + ; + ' + \ + z + x + c + v + b + n + m + , + . + / + + + ~ + ! + @ + # + $ + % + ^ + & + * + ( + ) + _ + + + Q + W + E + R + T + Y + U + I + O + P + { + } + | + A + S + D + F + G + H + J + K + L + : + " + | + Z + X + C + V + B + N + M + < + > + ? + + +
diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/source/welcome.htm b/common/test/keyboards/text_selection_tests_keyboard_9073/source/welcome.htm new file mode 100644 index 0000000000..18b821f8c9 --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/source/welcome.htm @@ -0,0 +1,26 @@ + + + + + + Start Using Text Selection Tests Keyboard + + + + +

Start Using Text Selection Tests Keyboard

+ +

+ Text Selection Tests Keyboard 1.0 generated from template. +

+ +

Keyboard Layout

+ + + + + \ No newline at end of file diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/text_selection_tests_keyboard_9073.keyboard_info b/common/test/keyboards/text_selection_tests_keyboard_9073/text_selection_tests_keyboard_9073.keyboard_info new file mode 100644 index 0000000000..db0a8bf7bd --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/text_selection_tests_keyboard_9073.keyboard_info @@ -0,0 +1,7 @@ +{ + "license": "mit", + "languages": [ + "en" + ], + "description": "Text Selection Tests Keyboard generated from template" +} diff --git a/common/test/keyboards/text_selection_tests_keyboard_9073/text_selection_tests_keyboard_9073.kpj b/common/test/keyboards/text_selection_tests_keyboard_9073/text_selection_tests_keyboard_9073.kpj new file mode 100644 index 0000000000..98b9ad7e0e --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/text_selection_tests_keyboard_9073.kpj @@ -0,0 +1,110 @@ + + + + $PROJECTPATH\build + True + True + True + keyboard + + + + id_dda967022de452e1fe199096e795f0ab + text_selection_tests_keyboard_9073.kmn + source\text_selection_tests_keyboard_9073.kmn + 1.0 + .kmn +
+ Text Selection Tests Keyboard + © Keyman Team +
+
+ + id_ba932837e6a67a86abc409a393242255 + text_selection_tests_keyboard_9073.kps + source\text_selection_tests_keyboard_9073.kps + + .kps +
+ Text Selection Tests Keyboard + © Keyman Team +
+
+ + id_ede98e4633e239f933cbfd1f4e1b766c + HISTORY.md + HISTORY.md + + .md + + + id_53e892b8b41cc4caece1cfd5ef21d6e7 + LICENSE.md + LICENSE.md + + .md + + + id_0730bb7c2e8f9ea2438b52e419dd86c9 + README.md + README.md + + .md + + + id_4b87bd35cc2e16f1ff8680a6f2caed52 + text_selection_tests_keyboard_9073.keyboard_info + text_selection_tests_keyboard_9073.keyboard_info + + .keyboard_info + + + id_bbf31cea8a9cfe0cb838f67055690bf8 + text_selection_tests_keyboard_9073.ico + source\text_selection_tests_keyboard_9073.ico + + .ico + id_dda967022de452e1fe199096e795f0ab + + + id_b8f7a473cac52dd0436273de657cdf46 + text_selection_tests_keyboard_9073.kmx + source\..\build\text_selection_tests_keyboard_9073.kmx + + .kmx + id_ba932837e6a67a86abc409a393242255 + + + id_73d0cd87e78d9b8d7f514809dbb36a47 + text_selection_tests_keyboard_9073.js + source\..\build\text_selection_tests_keyboard_9073.js + + .js + id_ba932837e6a67a86abc409a393242255 + + + id_71aafc060dc3251e4bb611ea539dc8e0 + text_selection_tests_keyboard_9073.kvk + source\..\build\text_selection_tests_keyboard_9073.kvk + + .kvk + id_ba932837e6a67a86abc409a393242255 + + + id_356e5d149c1e539356d72698c1e401a6 + welcome.htm + source\welcome.htm + + .htm + id_ba932837e6a67a86abc409a393242255 + + + id_8da344c4cea6f467013357fe099006f5 + readme.htm + source\readme.htm + + .htm + id_ba932837e6a67a86abc409a393242255 + +
+
diff --git a/common/web/input-processor/src/text/prediction/languageProcessor.ts b/common/web/input-processor/src/text/prediction/languageProcessor.ts index eecf82dc05..b928770669 100644 --- a/common/web/input-processor/src/text/prediction/languageProcessor.ts +++ b/common/web/input-processor/src/text/prediction/languageProcessor.ts @@ -128,8 +128,10 @@ export default class LanguageProcessor extends EventEmitter { this.configuration = config; - this._state = 'configured'; - this.emit('statechange', 'configured'); + if(this.mayPredict) { + this._state = 'configured'; + this.emit('statechange', 'configured'); + } }).catch((error) => { // Does this provide enough logging information? let message: string; diff --git a/common/web/keyboard-processor/build-bundler.js b/common/web/keyboard-processor/build-bundler.js index 25521c9409..f431e6781e 100644 --- a/common/web/keyboard-processor/build-bundler.js +++ b/common/web/keyboard-processor/build-bundler.js @@ -14,6 +14,7 @@ await esbuild.build({ // // Alternatively, we can just build it separately like the node-oriented one. fs.renameSync('build/lib/keyboards/loaders/dom-keyboard-loader.mjs', 'build/lib/dom-keyboard-loader.mjs'); +fs.renameSync('build/lib/keyboards/loaders/dom-keyboard-loader.mjs.map', 'build/lib/dom-keyboard-loader.mjs.map'); fs.rmSync('build/lib/keyboards', { recursive: true, force: true }); // The node-based keyboard loader needs an extra parameter due to Node-built-in imports: diff --git a/common/web/types/src/kmx/kmx-plus.ts b/common/web/types/src/kmx/kmx-plus.ts index 4ce22c33dd..0d2a166522 100644 --- a/common/web/types/src/kmx/kmx-plus.ts +++ b/common/web/types/src/kmx/kmx-plus.ts @@ -146,7 +146,11 @@ export interface StrsOptions { }; export class Strs extends Section { - strings: StrsItem[] = [ new StrsItem('') ]; // C7043: The null string is always requierd + /** the in-memory string table */ + strings: StrsItem[] = [ new StrsItem('') ]; // C7043: The null string is always required + + /** for validating */ + allProcessedStrings = new Set(); /** * Allocate a StrsItem given the string, unescaping if necessary. * @param s escaped string @@ -158,6 +162,11 @@ export class Strs extends Section { // Run the string processing pipeline s = Strs.processString(s, opts, sections); + // add to the set, for testing + if (s) { + this.allProcessedStrings.add(s); + } + // if it's a single char, don't push it into the strs table if (opts?.singleOk && isOneChar(s)) { return new CharStrsItem(s); diff --git a/common/web/types/src/util/util.ts b/common/web/types/src/util/util.ts index ce5ce9ce46..5c8eb70810 100644 --- a/common/web/types/src/util/util.ts +++ b/common/web/types/src/util/util.ts @@ -101,3 +101,161 @@ toOneChar(value: string) : number { } return value.codePointAt(0); } + +export function describeCodepoint(ch : number) : string { + let s; + const p = getProblem(ch); + if (p != null) { + // for example: 'PUA (U+E010)' + s = p; + } else { + // for example: '"a" (U+61)' + s = `"${String.fromCodePoint(ch)}"`; + } + return `${s} (U+${Number(ch).toString(16).toUpperCase()})`; +} + +export enum BadStringType { + pua = 'PUA', + unassigned = 'Unassigned', + illegal = 'Illegal', +}; + +// Following from kmx_xstring.h / .cpp + +const Uni_LEAD_SURROGATE_START = 0xD800; +const Uni_LEAD_SURROGATE_END = 0xDBFF; +const Uni_TRAIL_SURROGATE_START = 0xDC00; +const Uni_TRAIL_SURROGATE_END = 0xDFFF; +const Uni_SURROGATE_START = Uni_LEAD_SURROGATE_START; +const Uni_SURROGATE_END = Uni_TRAIL_SURROGATE_END; +const Uni_FD_NONCHARACTER_START = 0xFDD0; +const Uni_FD_NONCHARACTER_END = 0xFDEF; +const Uni_FFFE_NONCHARACTER = 0xFFFE; +const Uni_PLANE_MASK = 0x1F0000; +const Uni_MAX_CODEPOINT = 0x10FFFF; +// plane 0, 15, and 16 PUA +const Uni_PUA_00_START = 0xE000; +const Uni_PUA_00_END = 0xF8FF; +const Uni_PUA_15_START = 0x0F0000; +const Uni_PUA_15_END = 0x0FFFFD; +const Uni_PUA_16_START = 0x100000; +const Uni_PUA_16_END = 0x10FFFD; + + +/** + * @brief True if a lead surrogate + * \def Uni_IsSurrogate1 + */ +function Uni_IsSurrogate1(ch : number) { + return ((ch) >= Uni_LEAD_SURROGATE_START && (ch) <= Uni_LEAD_SURROGATE_END); +} +/** + * @brief True if a trail surrogate + * \def Uni_IsSurrogate2 + */ +function Uni_IsSurrogate2(ch : number) { + return ((ch) >= Uni_TRAIL_SURROGATE_START && (ch) <= Uni_TRAIL_SURROGATE_END); +} + +/** + * @brief True if any surrogate + * \def UniIsSurrogate +*/ +function Uni_IsSurrogate(ch : number) { + return (Uni_IsSurrogate1(ch) || Uni_IsSurrogate2(ch)); +} + +function Uni_IsEndOfPlaneNonCharacter(ch : number) { + return (((ch) & Uni_FFFE_NONCHARACTER) == Uni_FFFE_NONCHARACTER); // matches FFFF or FFFE +} + +function Uni_IsNoncharacter(ch : number) { + return (((ch) >= Uni_FD_NONCHARACTER_START && (ch) <= Uni_FD_NONCHARACTER_END) || Uni_IsEndOfPlaneNonCharacter(ch)); +} + +function Uni_InCodespace(ch : number) { + return (ch >= 0 && ch <= Uni_MAX_CODEPOINT); +}; + +function Uni_IsValid1(ch: number) { + return (Uni_InCodespace(ch) && !Uni_IsSurrogate(ch) && !Uni_IsNoncharacter(ch)); +} + +export function isValidUnicode(start: number, end?: number) { + if (!end) { + // single char + return Uni_IsValid1(start); + } else if (!Uni_IsValid1(end) || !Uni_IsValid1(start) || (end < start)) { + // start or end out of range, or inverted range + return false; + } else if ((start <= Uni_SURROGATE_END) && (end >= Uni_SURROGATE_START)) { + // contains some of the surrogate range + return false; + } else if ((start <= Uni_FD_NONCHARACTER_END) && (end >= Uni_FD_NONCHARACTER_START)) { + // contains some of the noncharacter range + return false; + } else if ((start & Uni_PLANE_MASK) != (end & Uni_PLANE_MASK)) { + // start and end are on different planes, meaning that the U+__FFFE/U+__FFFF noncharacters + // are contained. + // As a reminder, we already checked that start/end are themselves valid, + // so we know that 'end' is not on a noncharacter at end of plane. + return false; + } else { + return true; + } +} + +export function isPUA(ch: number) { + return ((ch >= Uni_PUA_00_START && ch <= Uni_PUA_00_END) || + (ch >= Uni_PUA_15_START && ch <= Uni_PUA_15_END) || + (ch >= Uni_PUA_16_START && ch <= Uni_PUA_16_END)); +} + +class BadStringMap extends Map> { + public toString() : string { + if (!this.size) { + return "{}"; + } + return Array.from(this.entries()).map(([t, s]) => `${t}: ${Array.from(s.values()).map(describeCodepoint).join(' ')}`).join(', '); + } +} + +function getProblem(ch : number) : BadStringType { + if (!isValidUnicode(ch)) { + return BadStringType.illegal; + } else if(isPUA(ch)) { + return BadStringType.pua; + } else { // TODO-LDML: unassigned + return null; + } +} +export class BadStringAnalyzer { + /** add a string for analysis */ + public add(s : string) { + for (const c of s) { + const ch = c.codePointAt(0); + const problem = getProblem(ch); + if (problem) { + this.addProblem(ch, problem); + } + } + } + + private addProblem(ch : number, type : BadStringType) { + if (!this.m.has(type)) { + this.m.set(type, new Set()); + } + this.m.get(type).add(ch); + } + + public analyze() : BadStringMap { + if (this.m.size == 0) { + return null; + } else { + return this.m; + } + } + + private m = new BadStringMap(); +} diff --git a/common/web/types/test/util/test-unescape.ts b/common/web/types/test/util/test-unescape.ts index c3630954b3..732e9af2bb 100644 --- a/common/web/types/test/util/test-unescape.ts +++ b/common/web/types/test/util/test-unescape.ts @@ -1,6 +1,6 @@ import 'mocha'; import {assert} from 'chai'; -import {unescapeString, UnescapeError, isOneChar, toOneChar, unescapeOneQuadString} from '../../src/util/util.js'; +import {unescapeString, UnescapeError, isOneChar, toOneChar, unescapeOneQuadString, BadStringAnalyzer, isValidUnicode, describeCodepoint, isPUA, BadStringType} from '../../src/util/util.js'; describe('test UTF32 functions()', function() { it('should properly categorize strings', () => { @@ -68,3 +68,146 @@ describe('test unescapeOneQuadString()', () => { assert.throws(() => unescapeOneQuadString('\uFFFFFFFFFFFF')); }); }); + +function titleize(o : any) { + const s = JSON.stringify(o); + if (!s) { + return `''`; + } else if (s.length < 10) { + return s; + } else { + return s.substring(0,10)+'…'; + } +} + +describe('test bad char functions', () => { + it('should match test_kmx_xstring.cpp', () => { + function Uni_IsValid(start: number, end?: number) { + return [ start, end ]; + } + function assert_equal(range: number[], expect : boolean) { + const [start, end] = range; + if (end) { + assert.equal(isValidUnicode(start, end), expect, `for ${describeCodepoint(start)}-${describeCodepoint(end)}}`); + } else { + // if branch just for the message + assert.equal(isValidUnicode(start), expect, `for ${describeCodepoint(start)}`); + } + } + // following lines are from test_kmx_xstring.cpp + assert_equal(Uni_IsValid(0x0000), true); + assert_equal(Uni_IsValid(0x0127), true); + assert_equal(Uni_IsValid('🙀'.codePointAt(0)), true); + assert_equal(Uni_IsValid(0xDECAFBAD), false); // out of range + assert_equal(Uni_IsValid(0x566D4128), false); + assert_equal(Uni_IsValid(0xFFFF), false); // nonchar + assert_equal(Uni_IsValid(0xFFFE), false); // nonchar + assert_equal(Uni_IsValid(0x10FFFF), false); // nonchar + assert_equal(Uni_IsValid(0x10FFFE), false); // nonchar + assert_equal(Uni_IsValid(0x01FFFF), false); // nonchar + assert_equal(Uni_IsValid(0x01FFFE), false); // nonchar + assert_equal(Uni_IsValid(0x02FFFF), false); // nonchar + assert_equal(Uni_IsValid(0x02FFFE), false); // nonchar + assert_equal(Uni_IsValid(0xFDD1), false); // nonchar + assert_equal(Uni_IsValid(0xD800), false); // orphaned surrogate + assert_equal(Uni_IsValid(0xFDD0), false); // nonchar + assert_equal(Uni_IsValid(0x100000, 0x10FFFD), true); + assert_equal(Uni_IsValid(0x10, 0x20), true); + assert_equal(Uni_IsValid(0x100000, 0x10FFFD), true); + assert_equal(Uni_IsValid(0x0000, 0xD7FF), true); + assert_equal(Uni_IsValid(0xD800, 0xDFFF), false); // orphaned surrogate + assert_equal(Uni_IsValid(0xE000, 0xFDCF), true); + assert_equal(Uni_IsValid(0xFDD0, 0xFDEF), false); + assert_equal(Uni_IsValid(0xFDF0, 0xFDFF), true); + assert_equal(Uni_IsValid(0xFDF0, 0xFFFD), true); + assert_equal(Uni_IsValid(0, 0x10FFFF), false); // ends with nonchar + assert_equal(Uni_IsValid(0, 0x10FFFD), false); // contains lots o' nonchars + assert_equal(Uni_IsValid(0x20, 0x10), false); // swapped + assert_equal(Uni_IsValid(0xFDEF, 0xFDF0), false); // just outside range + assert_equal(Uni_IsValid(0x0000, 0x010000), false); // crosses noncharacter plane boundary and other stuff + assert_equal(Uni_IsValid(0x010000, 0x020000), false); // crosses noncharacter plane boundary + assert_equal(Uni_IsValid(0x0000, 0xFFFF), false); // crosses other BMP prohibited and plane boundary + assert_equal(Uni_IsValid(0x0000, 0xFFFD), false); // crosses other BMP prohibited + assert_equal(Uni_IsValid(0x0000, 0xE000), false); // crosses surrogate space + assert_equal(Uni_IsValid(0x0000, 0x20FFFF), false); // out of bounds + assert_equal(Uni_IsValid(0x10FFFD, 0x20FFFF), false); // out of bounds + }); + it('should detect non-PUA', () => { + const strs = "abcd" + + ([ + 0xF900, + 0xFFFFF, + ].map(ch => String.fromCodePoint(ch)).join('')); + for (const s of strs) { + const ch = s.codePointAt(0); + assert.isFalse(isPUA(ch), describeCodepoint(ch)); + } + }); + it('should detect PUA', () => { + const strs = "\uE010" + + ([ + 0xE000,0xE001,0xE002, + 0xF000, + 0xF800, + 0xF8FF, + + 0x0F0000, + 0x0FFFFD, + + 0x100000, + 0x10FFFD + ].map(ch => String.fromCodePoint(ch)).join('')); + for (const s of strs) { + const ch = s.codePointAt(0); + assert.isTrue(isPUA(ch), describeCodepoint(ch)); + } + }); +}); + +describe('test BadStringAnalyzer', () => { + describe('should return nothing for all valid strings', () => { + const cases = [ + [], + ['a',], + ['a', 'b',] + ]; + for (const strs of cases) { + const title = titleize(strs); + it(`should analyze ${title}`, () => { + const bsa = new BadStringAnalyzer(); + for (const s of strs) { + bsa.add(s); + } + const m = bsa.analyze(); + assert.isNull(m, `${title}`); + }); + } + }); + describe('should return nothing for all valid strings', () => { + it('should handle a case with some odd strs in it', () => { + const strs = "But you can call me “\uE010\uFDD0\uFFFE\uD800”, for short." + + ([ + 0xF800, + 0x05FFFF, + 0x102222, + 0x04FFFE, + ].map(ch => String.fromCodePoint(ch)).join('')); + + const bsa = new BadStringAnalyzer(); + for (const s of strs) { + bsa.add(s); + } + const m = bsa.analyze(); + assert.isNotNull(m); + assert.containsAllKeys(m, [BadStringType.pua, BadStringType.illegal]); + assert.sameDeepMembers(Array.from(m.get(BadStringType.pua).values()), [ + 0xE010,0xF800, 0x102222, + ], `pua analysis`); + assert.sameDeepMembers(Array.from(m.get(BadStringType.illegal).values()), [ + 0xFDD0,0xD800,0xFFFE, + 0x05FFFF, + 0x04FFFE, + ], `illegal analysis`); + }); + }); +}); diff --git a/common/windows/delphi/general/utildir.pas b/common/windows/delphi/general/utildir.pas index 727b9f30dc..b0f22f5657 100644 --- a/common/windows/delphi/general/utildir.pas +++ b/common/windows/delphi/general/utildir.pas @@ -45,13 +45,19 @@ function KGetTempPath: string; function GetLongFileName(const fname: string): string; +function DosSlashes(const filename: string): string; + implementation uses + System.StrUtils, System.SysUtils, Winapi.Windows; - +function DosSlashes(const filename: string): string; +begin + Result := ReplaceStr(filename, '/', '\'); +end; function DirectoryEmpty(dir: WideString): Boolean; var diff --git a/core/src/action.cpp b/core/src/action.cpp index 3387d8f85d..4c018cabbd 100644 --- a/core/src/action.cpp +++ b/core/src/action.cpp @@ -95,13 +95,15 @@ km_core_actions * km::core::action_item_list_to_actions_object( output.push_back({KM_CORE_CT_MARKER,{0},{action_items->marker}}); break; case KM_CORE_IT_PERSIST_OPT: + { // TODO: lowpri: replace existing item if already present in options vector? - options.push_back(km::core::option( - static_cast(action_items->option->scope), + km::core::option opt(static_cast(action_items->option->scope), action_items->option->key, action_items->option->value - )); + ); + options.push_back(opt.release()); // hand over memory management of the option item to the action struct break; + } default: assert(false); } diff --git a/core/src/km_core_state_api.cpp b/core/src/km_core_state_api.cpp index 8a8165c9c0..2a25d6fcc7 100644 --- a/core/src/km_core_state_api.cpp +++ b/core/src/km_core_state_api.cpp @@ -262,6 +262,10 @@ void km_core_state_imx_deregister_callback(km_core_state *state) } bool is_context_valid(km_core_cp const * context, km_core_cp const * cached_context) { + if (context == nullptr || cached_context == nullptr || *cached_context == '\0') { + // If the cached_context is "empty" then it needs updating + return false; + } km_core_cp const* context_p = context; while(*context_p) { context_p++; @@ -355,4 +359,4 @@ km_core_status km_core_state_context_clear( } km_core_context_clear(km_core_state_context(state)); return KM_CORE_STATUS_OK; -} \ No newline at end of file +} diff --git a/core/src/kmx/kmx_processor.cpp b/core/src/kmx/kmx_processor.cpp index c72d2fe857..6790d5b46d 100644 --- a/core/src/kmx/kmx_processor.cpp +++ b/core/src/kmx/kmx_processor.cpp @@ -6,7 +6,7 @@ using namespace km::core; using namespace kmx; -// TODO consolodate with appint.cpp and put in public library. + static KMX_BOOL ContextItemsFromAppContext(KMX_WCHAR *buf, km_core_context_item** outPtr) { assert(buf); diff --git a/core/src/option.cpp b/core/src/option.cpp index dd9242ef0a..71cb395ddf 100644 --- a/core/src/option.cpp +++ b/core/src/option.cpp @@ -43,6 +43,14 @@ option::option(km_core_option_scope s, char16_t const *k, char16_t const *v) } } +km_core_option_item +option::release() { + km_core_option_item opt = *this; + key = nullptr; + value = nullptr; + return opt; +} + // TODO: Relocate this and fix it json & km::core::operator << (json &j, abstract_processor const &) { diff --git a/core/src/option.hpp b/core/src/option.hpp index cbfbf76436..43f3397049 100644 --- a/core/src/option.hpp +++ b/core/src/option.hpp @@ -34,10 +34,15 @@ namespace core option & operator=(option const & rhs); option & operator=(option && rhs); + /** + * Returns contents of this object as a C struct, releasing memory + * management of key and value, and invalidates this object. + */ + km_core_option_item release(); + bool empty() const; }; - inline option::option(km_core_option_scope s, std::u16string const & k, std::u16string const & v) diff --git a/core/tests/unit/kmnkbd/action_api.cpp b/core/tests/unit/kmnkbd/action_api.cpp index 79512352a3..e46d207194 100644 --- a/core/tests/unit/kmnkbd/action_api.cpp +++ b/core/tests/unit/kmnkbd/action_api.cpp @@ -258,6 +258,27 @@ void test_context_set_if_needed_different_context() { teardown(); } +void test_context_set_if_needed_cached_context_cleared() { + km_core_cp const *application_context = u"This is a test"; + km_core_cp const *cached_context = u""; + setup("k_000___null_keyboard.kmx", cached_context); + km_core_state_context_clear(test_state); + assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UPDATED); + assert(!is_identical_context(cached_context)); + assert(is_identical_context(application_context)); + teardown(); +} + +void test_context_set_if_needed_application_context_empty() { + km_core_cp const *application_context = u""; + km_core_cp const *cached_context = u"This is a test"; + setup("k_000___null_keyboard.kmx", cached_context); + assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UPDATED); + assert(!is_identical_context(cached_context)); + assert(is_identical_context(application_context)); + teardown(); +} + void test_context_set_if_needed_app_context_is_longer() { km_core_cp const *application_context = u"Longer This is a test"; km_core_cp const *cached_context = u"This is a test"; @@ -319,6 +340,8 @@ void test_context_set_if_needed_cached_context_has_markers() { void test_context_set_if_needed() { test_context_set_if_needed_identical_context(); test_context_set_if_needed_different_context(); + test_context_set_if_needed_cached_context_cleared(); + test_context_set_if_needed_application_context_empty(); test_context_set_if_needed_app_context_is_longer(); test_context_set_if_needed_app_context_is_shorter(); test_context_set_if_needed_cached_context_has_markers(); @@ -374,6 +397,7 @@ int main(int argc, char *argv []) { test_alert(); test_emit_keystroke(); test_invalidate_context(); + test_persist_opt(); // context -- todo move to another file test_context_set_if_needed(); diff --git a/developer/src/common/include/kmn_compiler_errors.h b/developer/src/common/include/kmn_compiler_errors.h index fe7ba11a15..e0bea39549 100644 --- a/developer/src/common/include/kmn_compiler_errors.h +++ b/developer/src/common/include/kmn_compiler_errors.h @@ -240,6 +240,8 @@ #define CHINT_UnreachableRule 0x000010AE +#define CWARN_VirtualKeyInOutput 0x000020AF + #define CERR_BufferOverflow 0x000080C0 #define CERR_Break 0x000080C1 diff --git a/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts b/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts index 2995c8f016..4ad9d02598 100644 --- a/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts +++ b/developer/src/kmc-kmn/src/compiler/kmn-compiler-messages.ts @@ -302,6 +302,8 @@ export class KmnCompilerMessages { static HINT_UnreachableRule = SevHint | 0x0AE; + static WARN_VirtualKeyInOutput = SevWarn | 0x0AF; + static FATAL_BufferOverflow = SevFatal | 0x0C0; static FATAL_Break = SevFatal | 0x0C1; }; diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_virtual_key_in_output.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_virtual_key_in_output.kmn new file mode 100644 index 0000000000..e4919689b1 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_virtual_key_in_output.kmn @@ -0,0 +1,9 @@ +store(&NAME) 'WARN_VirtualKeyInOutput' +store(&VERSION) '9.0' + +begin Unicode > use(main) + +group(main) using keys + +c WARN_VirtualKeyInOutput ++ 'a' > [K_BKQUOTE] diff --git a/developer/src/kmc-kmn/test/test-messages.ts b/developer/src/kmc-kmn/test/test-messages.ts index 6970b99741..51e6894daa 100644 --- a/developer/src/kmc-kmn/test/test-messages.ts +++ b/developer/src/kmc-kmn/test/test-messages.ts @@ -87,4 +87,11 @@ describe('CompilerMessages', function () { assert.equal(callbacks.messages[0].message, "Statement 'return' is not currently supported in output for web and touch targets"); }); + // WARN_VirtualKeyInOutput + + it('should generate WARN_VirtualKeyInOutput if a virtual key is found in the output part of a rule', async function() { + await testForMessage(this, ['invalid-keyboards', 'warn_virtual_key_in_output.kmn'], KmnCompilerMessages.WARN_VirtualKeyInOutput); + assert.equal(callbacks.messages[0].message, "Virtual keys are not supported in output"); + }); + }); diff --git a/developer/src/kmc-ldml/src/compiler/compiler.ts b/developer/src/kmc-ldml/src/compiler/compiler.ts index f5f27c0a39..b6f51f553b 100644 --- a/developer/src/kmc-ldml/src/compiler/compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/compiler.ts @@ -147,8 +147,8 @@ export class LdmlKeyboardCompiler { * @param source * @returns true if the file validates */ - public validate(source: LDMLKeyboardXMLSourceFile): boolean { - return !!this.compile(source); + public async validate(source: LDMLKeyboardXMLSourceFile): Promise { + return !!(await this.compile(source, true)); } /** @@ -157,7 +157,7 @@ export class LdmlKeyboardCompiler { * @param source in-memory representation of LDML keyboard xml file * @returns KMXPlusFile intermediate file */ - public async compile(source: LDMLKeyboardXMLSourceFile): Promise { + public async compile(source: LDMLKeyboardXMLSourceFile, postValidate?: boolean): Promise { const sections = this.buildSections(source); let passed = true; @@ -207,6 +207,15 @@ export class LdmlKeyboardCompiler { kmx.kmxplus[section.id] = sect as any; } + // give all sections a chance to postValidate + if (postValidate) { + for(let section of sections) { + if(!section.postValidate(kmx.kmxplus[section.id])) { + passed = false; + } + } + } + return passed ? kmx : null; } } diff --git a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts index 93476d4497..ac45243734 100644 --- a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts @@ -1,7 +1,8 @@ import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; import { SectionCompiler } from "./section-compiler.js"; -import { LDMLKeyboard, KMXPlus, CompilerCallbacks } from "@keymanapp/common-types"; +import { LDMLKeyboard, KMXPlus, CompilerCallbacks, util, MarkerParser } from "@keymanapp/common-types"; import { VarsCompiler } from './vars.js'; +import { CompilerMessages } from './messages.js'; /** * Compiler for typrs that don't actually consume input XML @@ -28,6 +29,45 @@ export class StrsCompiler extends EmptyCompiler { public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { return new KMXPlus.Strs(); } + public postValidate(section?: KMXPlus.Section): boolean { + const strs = section; + + if (strs) { + const badStringAnalyzer = new util.BadStringAnalyzer(); + const CONTAINS_MARKER_REGEX = new RegExp(MarkerParser.ANY_MARKER_MATCH); + for (let s of strs.allProcessedStrings.values()) { + // skip marker strings + if (CONTAINS_MARKER_REGEX.test(s)) { + // it had a marker, take out all marker strings, as the sentinel is illegal + // need a new regex to match + const REPLACE_MARKER_REGEX = new RegExp(MarkerParser.ANY_MARKER_MATCH, 'g'); + s = s.replaceAll(REPLACE_MARKER_REGEX, ''); // remove markers. + } + badStringAnalyzer.add(s); + } + const m = badStringAnalyzer.analyze(); + if (m?.size > 0) { + const puas = m.get(util.BadStringType.pua); + const unassigneds = m.get(util.BadStringType.unassigned); + const illegals = m.get(util.BadStringType.illegal); + if (puas) { + const [count, lowestCh] = [puas.size, Array.from(puas.values()).sort((a, b) => a - b)[0]]; + this.callbacks.reportMessage(CompilerMessages.Hint_PUACharacters({ count, lowestCh })) + } + if (unassigneds) { + const [count, lowestCh] = [unassigneds.size, Array.from(unassigneds.values()).sort((a, b) => a - b)[0]]; + this.callbacks.reportMessage(CompilerMessages.Warn_UnassignedCharacters({ count, lowestCh })) + } + if (illegals) { + // do this last, because we will return false. + const [count, lowestCh] = [illegals.size, Array.from(illegals.values()).sort((a, b) => a - b)[0]]; + this.callbacks.reportMessage(CompilerMessages.Error_IllegalCharacters({ count, lowestCh })) + return false; + } + } + } + return true; + } } export class ElemCompiler extends EmptyCompiler { diff --git a/developer/src/kmc-ldml/src/compiler/messages.ts b/developer/src/kmc-ldml/src/compiler/messages.ts index 640081635c..750cc38c0f 100644 --- a/developer/src/kmc-ldml/src/compiler/messages.ts +++ b/developer/src/kmc-ldml/src/compiler/messages.ts @@ -1,5 +1,4 @@ -import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m } from "@keymanapp/common-types"; - +import { util, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m } from "@keymanapp/common-types"; // const SevInfo = CompilerErrorSeverity.Info | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevHint = CompilerErrorSeverity.Hint | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevWarn = CompilerErrorSeverity.Warn | CompilerErrorNamespace.LdmlKeyboardCompiler; @@ -146,6 +145,18 @@ export class CompilerMessages { static Error_DisplayNeedsToOrId = (o:{output?: string, keyId?: string}) => m(this.ERROR_DisplayNeedsToOrId, `display ${CompilerMessages.outputOrKeyId(o)} needs output= or keyId=, but not both`); static ERROR_DisplayNeedsToOrId = SevError | 0x0022; + + static Hint_PUACharacters = (o: { count: number, lowestCh: number }) => + m(this.HINT_PUACharacters, `File contains ${o.count} PUA character(s), including ${util.describeCodepoint(o.lowestCh)}`); + static HINT_PUACharacters = SevHint | 0x0023; + + static Warn_UnassignedCharacters = (o: { count: number, lowestCh: number }) => + m(this.WARN_UnassignedCharacters, `File contains ${o.count} unassigned character(s), including ${util.describeCodepoint(o.lowestCh)}`); + static WARN_UnassignedCharacters = SevWarn | 0x0024; + + static Error_IllegalCharacters = (o: { count: number, lowestCh: number }) => + m(this.ERROR_IllegalCharacters, `File contains ${o.count} illegal character(s), including ${util.describeCodepoint(o.lowestCh)}`); + static ERROR_IllegalCharacters = SevError | 0x0025; } diff --git a/developer/src/kmc-ldml/src/compiler/section-compiler.ts b/developer/src/kmc-ldml/src/compiler/section-compiler.ts index 8744059f7b..f2b14e56a3 100644 --- a/developer/src/kmc-ldml/src/compiler/section-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/section-compiler.ts @@ -1,8 +1,9 @@ import { LDMLKeyboard, KMXPlus, CompilerCallbacks } from "@keymanapp/common-types"; import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; -/* istanbul ignore next */ -export class SectionCompiler { +/** newable interface to SectionCompiler c'tor */ +export type SectionCompilerNew = new (source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) => SectionCompiler; +export abstract class SectionCompiler { protected readonly keyboard3: LDMLKeyboard.LKKeyboard; protected readonly callbacks: CompilerCallbacks; @@ -11,19 +12,35 @@ export class SectionCompiler { this.callbacks = callbacks; } - /* c8 ignore next 11 */ - public get id(): SectionIdent { - throw Error(`Internal Error: id() not implemented`); - } - - public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { - throw Error(`Internal Error: compile() not implemented`); - } + public abstract get id(): SectionIdent; + /** + * This is called before compile. + * @returns false if this compiler failed to validate. + */ public validate(): boolean { return true; } + /** + * Perform the compilation for this section, returning the correct Section subclass + * object. + * + * @param sections any declared dependency sections per dependencies() + */ + public abstract compile(sections: KMXPlus.DependencySections): KMXPlus.Section; + + /** + * This is called after all other compile phases have completed, + * when being called by validate(), and provides an + * opportunity for late error reporting, for example for invalid strings. + * @param section the compiled section, if any. + * @returns false if validate fails + */ + public postValidate(section?: KMXPlus.Section): boolean { + return true; + } + /** * Get the dependencies for this compiler. * @returns set of dependent sections diff --git a/developer/src/kmc-ldml/src/compiler/tran.ts b/developer/src/kmc-ldml/src/compiler/tran.ts index 89a9d61f1c..2e1944a7cc 100644 --- a/developer/src/kmc-ldml/src/compiler/tran.ts +++ b/developer/src/kmc-ldml/src/compiler/tran.ts @@ -19,7 +19,7 @@ import { MarkerTracker, MarkerUse } from "./marker-tracker.js"; type TransformCompilerType = 'simple' | 'backspace'; -export class TransformCompiler extends SectionCompiler { +export abstract class TransformCompiler extends SectionCompiler { static validateMarkers(keyboard: LDMLKeyboard.LKKeyboard, mt : MarkerTracker): boolean { keyboard?.transforms?.forEach(transforms => diff --git a/developer/src/kmc-ldml/test/fixtures/sections/strs/hint-pua.xml b/developer/src/kmc-ldml/test/fixtures/sections/strs/hint-pua.xml new file mode 100644 index 0000000000..23c85d4023 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/strs/hint-pua.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/strs/invalid-illegal.xml b/developer/src/kmc-ldml/test/fixtures/sections/strs/invalid-illegal.xml new file mode 100644 index 0000000000..a21429d833 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/strs/invalid-illegal.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/strs/warn-unassigned.xml b/developer/src/kmc-ldml/test/fixtures/sections/strs/warn-unassigned.xml new file mode 100644 index 0000000000..79f3291dca --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/strs/warn-unassigned.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index ea76da94b3..8f6d73ef43 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -4,7 +4,7 @@ import 'mocha'; import * as path from 'path'; import { fileURLToPath } from 'url'; -import { SectionCompiler } from '../../src/compiler/section-compiler.js'; +import { SectionCompiler, SectionCompilerNew } from '../../src/compiler/section-compiler.js'; import { KMXPlus, LDMLKeyboardXMLSourceFileReader, VisualKeyboard, CompilerEvent, LDMLKeyboardTestDataXMLSourceFile, compilerEventFormat, LDMLKeyboard, UnicodeSetParser, CompilerCallbacks } from '@keymanapp/common-types'; import { LdmlKeyboardCompiler } from '../../src/main.js'; // make sure main.js compiles import { assert } from 'chai'; @@ -52,7 +52,7 @@ afterEach(function() { }); -export async function loadSectionFixture(compilerClass: typeof SectionCompiler, filename: string, callbacks: TestCompilerCallbacks, dependencies?: typeof SectionCompiler[]): Promise
{ +export async function loadSectionFixture(compilerClass: SectionCompilerNew, filename: string, callbacks: TestCompilerCallbacks, dependencies?: SectionCompilerNew[], postValidateFail?: boolean): Promise
{ callbacks.messages = []; const inputFilename = makePathToFixture(filename); const data = callbacks.loadFile(inputFilename); @@ -83,13 +83,16 @@ export async function loadSectionFixture(compilerClass: typeof SectionCompiler, compiler.dependencies.forEach(dep => assert.ok(sections[dep], `Required dependency '${dep}' for '${compiler.id}' was not supplied: Check the 'dependencies' argument to loadSectionFixture or testCompilationCases`)); - return compiler.compile(sections); + const section = await compiler.compile(sections); + const postValidate = compiler.postValidate(section); + assert.equal(postValidate, !postValidateFail, `expected postValidate() to return ${!postValidateFail}`); + return section; } /** * Recursively load dependencies. Normally they are loaded in SECTION_COMPILERS order */ -async function loadDepsFor(sections: DependencySections, parentCompiler: SectionCompiler, source: LDMLKeyboardXMLSourceFile, callbacks: TestCompilerCallbacks, dependencies?: typeof SectionCompiler[]) { +async function loadDepsFor(sections: DependencySections, parentCompiler: SectionCompiler, source: LDMLKeyboardXMLSourceFile, callbacks: TestCompilerCallbacks, dependencies?: SectionCompilerNew[]) { const parentId = parentCompiler.id; if (!dependencies) { // default dependencies @@ -115,18 +118,29 @@ export function loadTestdata(inputFilename: string, options: LdmlCompilerOptions return source; } -export async function compileKeyboard(inputFilename: string, options: LdmlCompilerOptions): Promise { +export async function compileKeyboard(inputFilename: string, options: LdmlCompilerOptions, validateMessages?: CompilerEvent[], expectFailValidate?: boolean, compileMessages?: CompilerEvent[]): Promise { const k = new LdmlKeyboardCompiler(compilerTestCallbacks, options); const source = k.load(inputFilename); checkMessages(); assert.isNotNull(source, 'k.load should not have returned null'); - const valid = k.validate(source); - checkMessages(); - assert.isTrue(valid, 'k.validate should not have failed'); + const valid = await k.validate(source); + if (validateMessages) { + assert.sameDeepMembers(compilerTestCallbacks.messages, validateMessages, "validation messages mismatch"); + assert.notEqual(valid, expectFailValidate, 'validation failure'); + } else { + checkMessages(); + assert.isTrue(valid, 'k.validate should not have failed'); + } + + if (!valid) return null; // get out, if the above asserts didn't get us out. const kmx = await k.compile(source); - checkMessages(); + if (compileMessages) { + assert.sameDeepMembers(compilerTestCallbacks.messages, compileMessages, "compiler messages mismatch"); + } else { + checkMessages(); + } assert.isNotNull(kmx, 'k.compile should not have returned null'); // In order for the KMX file to be loaded by non-KMXPlus components, it is helpful @@ -136,13 +150,13 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil return kmx; } -export function compileVisualKeyboard(inputFilename: string, options: LdmlCompilerOptions): VisualKeyboard.VisualKeyboard { +export async function compileVisualKeyboard(inputFilename: string, options: LdmlCompilerOptions): Promise { const k = new LdmlKeyboardCompiler(compilerTestCallbacks, options); const source = k.load(inputFilename); checkMessages(); assert.isNotNull(source, 'k.load should not have returned null'); - const valid = k.validate(source); + const valid = await k.validate(source); checkMessages(); assert.isTrue(valid, 'k.validate should not have failed'); @@ -184,7 +198,11 @@ export interface CompilationCase { /** * Optional dependent sections to load. Will be strs+list+elem if falsy. */ - dependencies?: (typeof SectionCompiler)[]; + dependencies?: (SectionCompilerNew)[]; + /** + * Optional, if true, postValidate() must return false. (must be != postValidate()) + */ + postValidateFail?: boolean; } /** @@ -193,7 +211,7 @@ export interface CompilationCase { * @param compiler argument to loadSectionFixture() * @param callbacks argument to loadSectionFixture() */ -export function testCompilationCases(compiler: typeof SectionCompiler, cases : CompilationCase[], dependencies?: (typeof SectionCompiler)[]) { +export function testCompilationCases(compiler: SectionCompilerNew, cases : CompilationCase[], dependencies?: (SectionCompilerNew)[]) { // we need our own callbacks rather than using the global so messages don't get mixed const callbacks = new TestCompilerCallbacks(); for (let testcase of cases) { diff --git a/developer/src/kmc-ldml/test/test-compiler-e2e.ts b/developer/src/kmc-ldml/test/test-compiler-e2e.ts index 343f6cc7ac..c58d623f33 100644 --- a/developer/src/kmc-ldml/test/test-compiler-e2e.ts +++ b/developer/src/kmc-ldml/test/test-compiler-e2e.ts @@ -4,10 +4,15 @@ import hextobin from '@keymanapp/hextobin'; import { KMXBuilder } from '@keymanapp/common-types'; import {checkMessages, compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture} from './helpers/index.js'; import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js'; +import { CompilerMessages } from '../src/compiler/messages.js'; describe('compiler-tests', function() { this.slow(500); // 0.5 sec -- json schema validation takes a while + before(function() { + compilerTestCallbacks.clear(); + }); + it('should-build-fixtures', async function() { // Let's build basic.xml // It should match basic.kmx (built from basic.txt) @@ -61,4 +66,50 @@ describe('compiler-tests', function() { const source = k.load(filename); assert.notOk(source, `Trying to loadTestData(${filename})`); }); + it('should fail on illegal chars', async function() { + const inputFilename = makePathToFixture('sections/strs/invalid-illegal.xml'); + const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, + [ + // validation messages + CompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: 0xFDD0 }), + CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), + ], + true, // validation should fail + [ + // compiler messages (not reached, we've already failed) + ]); + assert.isNull(kmx); // should fail post-validate + }); + it('should hint on pua chars', async function() { + const inputFilename = makePathToFixture('sections/strs/hint-pua.xml'); + // Compile the keyboard + const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, + [ + // validation messages + CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), + ], + false, // validation should pass + [ + // same messages + CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), + ]); + assert.isNotNull(kmx); + }); + it.skip('should warn on unassigned chars', async function() { + // unassigned not implemented yet + const inputFilename = makePathToFixture('sections/strs/warn-unassigned.xml'); + const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, + [ + // validation messages + CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), + CompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }), + ], + false, // validation should pass + [ + // same messages + CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }), + CompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }), + ]); + assert.isNotNull(kmx); + }); }); diff --git a/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts b/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts index ab8c4b2fc2..e1a570ea12 100644 --- a/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts +++ b/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts @@ -22,7 +22,7 @@ describe('LdmlKeyboardKeymanWebCompiler', function() { assert.isNotNull(source, 'k.load should not have returned null'); // Sanity check ... this is also checked in other tests - const valid = k.validate(source); + const valid = await k.validate(source); checkMessages(); assert.isTrue(valid, 'k.validate should not have failed'); diff --git a/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts b/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts index 133f4286c8..df0aea0391 100644 --- a/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts +++ b/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts @@ -15,7 +15,7 @@ describe('visual-keyboard-compiler', function() { const binaryFilename = makePathToFixture('basic-kvk.txt'); // Compile the visual keyboard - const vk = compileVisualKeyboard(inputFilename, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false}); + const vk = await compileVisualKeyboard(inputFilename, {...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false}); assert.isNotNull(vk); // Use the builder to generate the binary output file @@ -28,4 +28,4 @@ describe('visual-keyboard-compiler', function() { let expected = await hextobin(binaryFilename, undefined, {silent:true}); assert.deepEqual(code, expected); }); -}); \ No newline at end of file +}); diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index d795a21d55..3dce88c9d5 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -140,7 +140,7 @@ export class KmpCompiler { if(kps.Files && kps.Files.File) { kmp.files = this.arrayWrap(kps.Files.File).map((file: KpsFile.KpsFileContentFile) => { return { - name: file.Name.trim(), + name: file.Name.trim().replaceAll('\\','/'), description: file.Description.trim(), copyLocation: parseInt(file.CopyLocation, 10) || undefined // note: we don't emit fileType as that is not permitted in kmp.json diff --git a/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.intermediate.json b/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.intermediate.json index e02186e8d9..9d3de2aa64 100644 --- a/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.intermediate.json +++ b/developer/src/kmc-package/test/fixtures/withfolders.qaa.sencoten/withfolders.qaa.sencoten.model.kmp.intermediate.json @@ -21,7 +21,7 @@ }, "files": [ { - "name": "..\\build\\withfolders.qaa.sencoten.model.js", + "name": "../build/withfolders.qaa.sencoten.model.js", "description": "Lexical model withfolders.qaa.sencoten.model.js" }, { diff --git a/developer/src/kmcmplib/src/CompMsg.cpp b/developer/src/kmcmplib/src/CompMsg.cpp index 8ab5db81f6..715750d4f2 100644 --- a/developer/src/kmcmplib/src/CompMsg.cpp +++ b/developer/src/kmcmplib/src/CompMsg.cpp @@ -143,6 +143,7 @@ const struct CompilerError CompilerErrors[] = { { 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)"}, { CWARN_KeyShouldIncludeNCaps , "Other rules which reference this key include CAPS or NCAPS modifiers, so this rule must include NCAPS modifier to avoid inconsistent matches"}, + { CWARN_VirtualKeyInOutput , "Virtual keys are not supported in output"}, { 0, nullptr } }; diff --git a/developer/src/kmcmplib/src/Compiler.cpp b/developer/src/kmcmplib/src/Compiler.cpp index e2720f8608..045b1c39ad 100644 --- a/developer/src/kmcmplib/src/Compiler.cpp +++ b/developer/src/kmcmplib/src/Compiler.cpp @@ -1331,6 +1331,7 @@ KMX_BOOL CheckContextStatementPositions(PKMX_WCHAR context) { return TRUE; } + /** * Checks if a use() statement is followed by other content in the output of a rule */ @@ -1348,6 +1349,22 @@ KMX_DWORD CheckUseStatementsInOutput(PKMX_WCHAR output) { return CERR_None; } +/** + * Warn if output has virtual keys in it, which is not supported by Core at all, + * but was unofficially supported, but never worked properly, in Keyman for + * Windows for many years + */ +KMX_DWORD CheckVirtualKeysInOutput(PKMX_WCHAR output) { + PKMX_WCHAR p; + for (p = output; *p; p = incxstr(p)) { + if (*p == UC_SENTINEL && *(p + 1) == CODE_EXTENDED) { + AddWarning(CWARN_VirtualKeyInOutput); + break; + } + } + return CERR_None; +} + /** * Adds implicit `context` to start of output of rules for readonly groups */ @@ -1472,6 +1489,11 @@ KMX_DWORD ProcessKeyLineImpl(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_BOOL IsUnico return msg; // I4867 } + // Warn if virtual keys are used in the output, as they are unsupported by Core + if ((msg = CheckVirtualKeysInOutput(pklOut)) != CERR_None) { + return msg; + } + if (gp->fReadOnly) { // Ensure no output is made from the rule, and that // use() statements meet required readonly semantics diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas index e5ddbf9e31..a5f55a0470 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectFile.pas @@ -134,8 +134,8 @@ const DefaultProjectOptions: array[TProjectVersion] of TProjectOptionsRecord = ( ProjectType: ptKeyboard; Version: pv10 ), ( // 2.0 - BuildPath: '$PROJECTPATH/build'; - SourcePath: '$PROJECTPATH/source'; + BuildPath: '$PROJECTPATH\build'; + SourcePath: '$PROJECTPATH\source'; CompilerWarningsAsErrors: False; WarnDeprecatedCode: True; CheckFilenameConventions: False; @@ -621,8 +621,8 @@ begin Exit(True); // Only return true if the file is directly in the ProjectOptions.SourcePath folder - SourcePath := ReplaceStr(IncludeTrailingPathDelimiter(FProject.ResolveProjectPath(FProject.Options.SourcePath)), '/', '\'); - FilePath := ReplaceStr(ExtractFilePath(FFileName), '/', '\'); + SourcePath := DosSlashes(FProject.ResolveProjectPath(FProject.Options.SourcePath)); + FilePath := DosSlashes(ExtractFilePath(FFileName)); Result := SameFileName(SourcePath, FilePath); end; @@ -662,7 +662,7 @@ procedure TProjectFile.Save(node: IXMLNode); // I4698 begin node.AddChild('ID').NodeValue := FID; node.AddChild('Filename').NodeValue := ExtractFileName(FFileName); - node.AddChild('Filepath').NodeValue := ExtractRelativePath(FProject.FileName, FFileName); + node.AddChild('Filepath').NodeValue := ExtractRelativePath(FProject.FileName, DosSlashes(FFileName)); node.AddChild('FileVersion').NodeValue := FFileVersion; // I4701 // Note: FileType is only ever written in Delphi code; it is used by xsl @@ -978,18 +978,21 @@ end; /// function TProject.PopulateFiles: Boolean; var - ProjectPath: string; + SourcePath, ProjectPath: string; begin if FOptions.Version <> pv20 then raise EProjectLoader.Create('PopulateFiles can only be called on a v2.0 project'); FFiles.Clear; - ProjectPath := ExtractFilePath(FileName); + ProjectPath := ExpandFileName(ExtractFilePath(FileName)); if not DirectoryExists(ProjectPath) then Exit(False); PopulateFolder(ProjectPath); + SourcePath := ResolveProjectPath(FOptions.SourcePath); + if not SameFileName(ProjectPath, SourcePath) and DirectoryExists(SourcePath) then + PopulateFolder(SourcePath); Result := True; end; @@ -999,7 +1002,7 @@ var ff: string; f: TSearchRec; begin - if FindFirst(path + '*', faDirectory, f) = 0 then + if FindFirst(path + '*', 0, f) = 0 then begin repeat ff := path + f.Name; @@ -1009,12 +1012,6 @@ begin Continue; end; - if (f.Attr and faDirectory) = faDirectory then - begin - PopulateFolder(ff + '\'); - Continue; - end; - CreateProjectFile(Self, ff, nil); until FindNext(f) <> 0; System.SysUtils.FindClose(f); @@ -1231,7 +1228,7 @@ end; function TProject.ResolveProjectPath(APath: string): string; begin - Result := ReplaceText(APath, '$PROJECTPATH', ExtractFileDir(ExpandFileName(FFileName))); + Result := IncludeTrailingPathDelimiter(ReplaceText(APath, '$PROJECTPATH', ExtractFileDir(ExpandFileName(FFileName)))); end; function TProject.GetTargetFilename10(ATargetFile, ASourceFile, AVersion: string): string; // I4688 @@ -1256,7 +1253,6 @@ begin Exit(ExtractFilePath(ExpandFileName(ASourceFile)) + ExtractFileName(ATargetFile)); end; - Result := IncludeTrailingPathDelimiter(Result); Result := ResolveProjectPath(Result); Result := Result + ExtractFileName(ATargetFile); end; diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectLoader.pas b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectLoader.pas index c68339ab86..1c8d0c425d 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.ProjectLoader.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.ProjectLoader.pas @@ -66,6 +66,7 @@ uses Keyman.Developer.System.Project.ProjectFiles, Keyman.Developer.System.Project.ProjectFileType, + utildir, utilfiletypes; { TProjectLoader } @@ -131,10 +132,10 @@ begin FProject.Options.Assign(DefaultProjectOptions[FProject.Options.Version]); if not VarIsNull(node.ChildValues['BuildPath']) then - FProject.Options.BuildPath := VarToStr(node.ChildValues['BuildPath']); + FProject.Options.BuildPath := DosSlashes(VarToStr(node.ChildValues['BuildPath'])); if not VarIsNull(node.ChildValues['SourcePath']) then - FProject.Options.SourcePath := VarToStr(node.ChildValues['SourcePath']); + FProject.Options.SourcePath := DosSlashes(VarToStr(node.ChildValues['SourcePath'])); if not VarIsNull(node.ChildValues['CompilerWarningsAsErrors']) then FProject.Options.CompilerWarningsAsErrors := node.ChildValues['CompilerWarningsAsErrors']; diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProjectSettings20.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProjectSettings20.pas index 2bda4a3bb6..78c945a693 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProjectSettings20.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmProjectSettings20.pas @@ -1,22 +1,22 @@ (* Name: Keyman.Developer.UI.Project.UfrmProjectSettings Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 4 May 2015 Modified Date: 24 Aug 2015 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 04 May 2015 - mcdurdin - I4688 - V9.0 - Add build path to project settings 24 Aug 2015 - mcdurdin - I4865 - Add treat hints and warnings as errors into project 24 Aug 2015 - mcdurdin - I4866 - Add warn on deprecated features to project and compile - + *) unit Keyman.Developer.UI.Project.UfrmProjectSettings20; // I4688 @@ -54,12 +54,13 @@ implementation {$R *.dfm} uses - Keyman.Developer.System.Project.Project; + Keyman.Developer.System.Project.Project, + utildir; procedure TfrmProjectSettings20.cmdOKClick(Sender: TObject); begin - FGlobalProject.Options.BuildPath := Trim(editOutputPath.Text); - FGlobalProject.Options.SourcePath := Trim(editSourcePath.Text); + FGlobalProject.Options.BuildPath := Trim(DosSlashes(editOutputPath.Text)); + FGlobalProject.Options.SourcePath := Trim(DosSlashes(editSourcePath.Text)); FGlobalProject.Options.SkipMetadataFiles := not chkBuildMetadataFiles.Checked; FGlobalProject.Options.CompilerWarningsAsErrors := chkCompilerWarningsAsErrors.Checked; // I4865 FGlobalProject.Options.WarnDeprecatedCode := chkWarnDeprecatedCode.Checked; // I4866 diff --git a/developer/src/tike/xml/project/distribution.xsl b/developer/src/tike/xml/project/distribution.xsl index 840a3287c0..d30403e84a 100644 --- a/developer/src/tike/xml/project/distribution.xsl +++ b/developer/src/tike/xml/project/distribution.xsl @@ -83,6 +83,7 @@ false + true diff --git a/developer/src/tike/xml/project/elements.xsl b/developer/src/tike/xml/project/elements.xsl index 0bcf796cc2..ba5a41fdee 100644 --- a/developer/src/tike/xml/project/elements.xsl +++ b/developer/src/tike/xml/project/elements.xsl @@ -76,6 +76,7 @@ + file @@ -103,7 +104,10 @@
diff --git a/linux/debian/changelog b/linux/debian/changelog index 1ad0f861fb..431728feb1 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,11 @@ +keyman (16.0.143-1) unstable; urgency=medium + + * Fix failure to build source after successful build (Closes #1046776) + * New upstream release. + * Re-release to Debian + + -- Eberhard Beilharz Wed, 22 Nov 2023 15:24:59 +0100 + keyman (16.0.141-1) unstable; urgency=medium * Work around mips64el build failure (#1041499) diff --git a/web/src/engine/main/src/keyboardInterface.ts b/web/src/engine/main/src/keyboardInterface.ts index a36a4fb626..705db8ff3c 100644 --- a/web/src/engine/main/src/keyboardInterface.ts +++ b/web/src/engine/main/src/keyboardInterface.ts @@ -88,15 +88,21 @@ export default class KeyboardInterface { + const pathConfig = this.engine.config.paths; + return new KeyboardStub(Pstub, pathConfig.keyboards, pathConfig.fonts); + }; if(!this.engine.config.deferForInitialization.isResolved) { - this.engine.config.deferForInitialization.then(() => this.engine.keyboardRequisitioner.cache.addStub(stub)); + // pathConfig is not ready until KMW initializes, which prevents proper stub-building. + this.engine.config.deferForInitialization.then(() => this.engine.keyboardRequisitioner.cache.addStub(buildStub())); } else { + const stub = buildStub(); + + if(this.engine.keyboardRequisitioner?.cache.findMatchingStub(stub)) { + return 1; + } this.engine.keyboardRequisitioner.cache.addStub(stub); } diff --git a/web/src/engine/osk/src/banner/bannerController.ts b/web/src/engine/osk/src/banner/bannerController.ts index 4f77524a1b..45ee746bbf 100644 --- a/web/src/engine/osk/src/banner/bannerController.ts +++ b/web/src/engine/osk/src/banner/bannerController.ts @@ -68,7 +68,8 @@ export class BannerController { const oldBanner = this.container.banner; if(oldBanner instanceof SuggestionBanner) { - this.predictionContext.off('update', oldBanner.onSuggestionUpdate); + // Frees all handlers, etc registered previously by the banner. + oldBanner.predictionContext = null; } if(!on) { @@ -78,7 +79,7 @@ export class BannerController { suggestBanner.predictionContext = this.predictionContext; suggestBanner.events.on('apply', (selection) => this.predictionContext.accept(selection.suggestion)); - this.predictionContext.on('update', suggestBanner.onSuggestionUpdate); + // Registers for prediction-engine events & handles its needed connections. this.container.banner = suggestBanner; } } @@ -92,4 +93,10 @@ export class BannerController { // Only display a SuggestionBanner when LanguageProcessor states it is active. this.activateBanner(state == 'active' || state == 'configured'); } + + public shutdown() { + if(this.container.banner instanceof SuggestionBanner) { + this.container.banner.predictionContext = null; + } + } } \ No newline at end of file diff --git a/web/src/engine/osk/src/views/oskView.ts b/web/src/engine/osk/src/views/oskView.ts index eb94667fbd..36cb2dc839 100644 --- a/web/src/engine/osk/src/views/oskView.ts +++ b/web/src/engine/osk/src/views/oskView.ts @@ -692,6 +692,7 @@ export default abstract class OSKView private loadActiveKeyboard() { this.setBoxStyling(); + // Do not erase / 'shutdown' the banner-controller; we simply re-use its elements. if(this.vkbd) { this.vkbd.shutdown(); } @@ -1140,6 +1141,8 @@ export default abstract class OSKView this.kbdStyleSheetManager.unlinkAll(); this.uiStyleSheetManager.unlinkAll(); + + this.bannerController.shutdown(); } /** diff --git a/web/src/test/manual/web/attachment-api/utilities.js b/web/src/test/manual/web/attachment-api/utilities.js index a0c15fd3af..5793c12af6 100644 --- a/web/src/test/manual/web/attachment-api/utilities.js +++ b/web/src/test/manual/web/attachment-api/utilities.js @@ -1,6 +1,7 @@ // Page-global variable definitions. { var inputCounter = 0; +var kmw = keyman; } function generateDiagnosticDiv(elem) { diff --git a/web/src/test/manual/web/index.html b/web/src/test/manual/web/index.html index 6cd39c620e..520808b960 100644 --- a/web/src/test/manual/web/index.html +++ b/web/src/test/manual/web/index.html @@ -19,7 +19,7 @@ - +

KeymanWeb 17 Testing

Test unminified Keymanweb

Test unminified Keymanweb in manual-attachment mode.

@@ -27,7 +27,7 @@

Desktop UI module testing

Tests keyboard documentation rendering.

Stand-alone gesture recognition

- +

Miscellaneous tests for smaller features

Chirality testing/bootstrapping

Tests option/variable store functionality

@@ -44,7 +44,7 @@

Test desktop MutationObserver functionality

Light test for touch-based MutationObserver functionality

Test/stress-test touch-based MutationObserver functionality

- +

Issue Testing

Tests OSK handling of empty rows

Tests keyboard error-handling functionality

@@ -72,7 +72,7 @@

Tests predictive text & other handling of rule matching when the final rule group does not match (#6005)

Tests handling of new default-subkey feature (#9430)

Test special characters rendering with keymanweb-osk.ttf (#9469)

- +

Test text selection (#9073)

Other

Keystroke processing regression test engine.


diff --git a/web/src/test/manual/web/text_selection_tests_9073/index.html b/web/src/test/manual/web/text_selection_tests_9073/index.html new file mode 100644 index 0000000000..20bd3cda68 --- /dev/null +++ b/web/src/test/manual/web/text_selection_tests_9073/index.html @@ -0,0 +1,78 @@ + + + + + + + + + KeymanWeb #9073 + + + + + + + + + + + + + + +

Text Selection Test Cases (#9073)

+ +
+ +
+ + +
+ +
+

Return to testing home page

+ + + + diff --git a/web/src/test/manual/web/text_selection_tests_9073/text_selection_tests_keyboard_9073.js b/web/src/test/manual/web/text_selection_tests_9073/text_selection_tests_keyboard_9073.js new file mode 100644 index 0000000000..f1e93eb48e --- /dev/null +++ b/web/src/test/manual/web/text_selection_tests_9073/text_selection_tests_keyboard_9073.js @@ -0,0 +1,641 @@ +if(typeof keyman === 'undefined') { + console.log('Keyboard requires KeymanWeb 10.0 or later'); + if(typeof tavultesoft !== 'undefined') tavultesoft.keymanweb.util.alert("This keyboard requires KeymanWeb 10.0 or later"); +} else { +KeymanWeb.KR(new Keyboard_text_selection_tests_keyboard_9073()); +} +function Keyboard_text_selection_tests_keyboard_9073() +{ + var modCodes = keyman.osk.modifierCodes; + var keyCodes = keyman.osk.keyCodes; + + this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9; + this.KI="Keyboard_text_selection_tests_keyboard_9073"; + this.KN="Text Selection Tests Keyboard"; + this.KMINVER="10.0"; + this.KV={F:' 1em "Arial"',K102:0}; + this.KV.KLS={ + "default": ["dk(1)","1","2","3","4","5","6","7","8","9","0","-","=","","","","q","w","e","r","t","y","u","i","o","p","[","]","\\","","","","a","s","d","f","g","h","j","k","l",";","'","","","","","","\\","z","x","c","v","b","n","m",",",".","/","","","","","",""], + "shift": ["~","!","@","#","$","%","^","&","*","(",")","_","+","","","","Q","W","E","R","T","Y","U","I","O","P","{","}","|","","","","A","S","D","F","G","H","J","K","L",":","\"","","","","","","|","Z","X","C","V","B","N","M","<",">","?","","","","","",""] + }; + this.KV.BK=(function(x){ + var + empty=Array.apply(null, Array(65)).map(String.prototype.valueOf,""), + result=[], v, i, + modifiers=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt']; + for(i=modifiers.length-1;i>=0;i--) { + v = x[modifiers[i]]; + if(v || result.length > 0) { + result=(v ? v : empty).slice().concat(result); + } + } + return result; + })(this.KV.KLS); + this.KDU=0; + this.KH=''; + this.KM=0; + this.KBVER="1.0"; + this.KMBM=modCodes.SHIFT /* 0x0010 */; + this.KVKL={ + "tablet": { + "displayUnderlying": false, + "layer": [ + { + "id": "default", + "row": [ + { + "id": "1", + "key": [ + { + "nextlayer": "shift", + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + }, + { + "id": "K_HYPHEN", + "text": "-" + }, + { + "id": "K_EQUAL", + "text": "=" + }, + { + "width": "100", + "id": "K_BKSP", + "sp": "1", + "text": "*BkSp*" + } + ] + }, + { + "id": "2", + "key": [ + { + "id": "K_Q", + "pad": "75", + "text": "q" + }, + { + "id": "K_W", + "text": "w" + }, + { + "id": "K_E", + "text": "e" + }, + { + "id": "K_R", + "text": "r" + }, + { + "id": "K_T", + "text": "t" + }, + { + "id": "K_Y", + "text": "y" + }, + { + "id": "K_U", + "text": "u" + }, + { + "id": "K_I", + "text": "i" + }, + { + "id": "K_O", + "text": "o" + }, + { + "id": "K_P", + "text": "p" + }, + { + "id": "K_LBRKT", + "text": "[" + }, + { + "id": "K_RBRKT", + "text": "]" + }, + { + "width": "10", + "id": "T_new_136", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "K_BKQUOTE", + "text": "dk(1)" + }, + { + "id": "K_A", + "text": "a" + }, + { + "id": "K_S", + "text": "s" + }, + { + "id": "K_D", + "text": "d" + }, + { + "id": "K_F", + "text": "f" + }, + { + "id": "K_G", + "text": "g" + }, + { + "id": "K_H", + "text": "h" + }, + { + "id": "K_J", + "text": "j" + }, + { + "id": "K_K", + "text": "k" + }, + { + "id": "K_L", + "text": "l" + }, + { + "id": "K_COLON", + "text": ";" + }, + { + "id": "K_QUOTE", + "text": "'" + }, + { + "id": "K_BKSLASH", + "text": "\\" + } + ] + }, + { + "id": "4", + "key": [ + { + "nextlayer": "shift", + "width": "160", + "id": "K_SHIFT", + "sp": "1", + "text": "*Shift*" + }, + { + "id": "K_oE2", + "text": "\\" + }, + { + "id": "K_Z", + "text": "z" + }, + { + "id": "K_X", + "text": "x" + }, + { + "id": "K_C", + "text": "c" + }, + { + "id": "K_V", + "text": "v" + }, + { + "id": "K_B", + "text": "b" + }, + { + "id": "K_N", + "text": "n" + }, + { + "id": "K_M", + "text": "m" + }, + { + "id": "K_COMMA", + "text": "," + }, + { + "id": "K_PERIOD", + "text": "." + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "width": "10", + "id": "T_new_162", + "sp": "10" + } + ] + }, + { + "id": "5", + "key": [ + { + "width": "140", + "id": "K_LOPT", + "sp": "1", + "text": "*Menu*" + }, + { + "width": "930", + "id": "K_SPACE" + }, + { + "width": "145", + "id": "K_ENTER", + "sp": "1", + "text": "*Enter*" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_1", + "text": "!" + }, + { + "id": "K_2", + "text": "@" + }, + { + "id": "K_3", + "text": "#" + }, + { + "id": "K_4", + "text": "$" + }, + { + "id": "K_5", + "text": "%" + }, + { + "id": "K_6", + "text": "^" + }, + { + "id": "K_7", + "text": "&" + }, + { + "id": "K_8", + "text": "*" + }, + { + "id": "K_9", + "text": "(" + }, + { + "id": "K_0", + "text": ")" + }, + { + "id": "K_HYPHEN", + "text": "_" + }, + { + "id": "K_EQUAL", + "text": "+" + }, + { + "width": "100", + "id": "K_BKSP", + "sp": "1", + "text": "*BkSp*" + } + ] + }, + { + "id": "2", + "key": [ + { + "id": "K_Q", + "pad": "75", + "text": "Q" + }, + { + "id": "K_W", + "text": "W" + }, + { + "id": "K_E", + "text": "E" + }, + { + "id": "K_R", + "text": "R" + }, + { + "id": "K_T", + "text": "T" + }, + { + "id": "K_Y", + "text": "Y" + }, + { + "id": "K_U", + "text": "U" + }, + { + "id": "K_I", + "text": "I" + }, + { + "id": "K_O", + "text": "O" + }, + { + "id": "K_P", + "text": "P" + }, + { + "id": "K_LBRKT", + "text": "{" + }, + { + "id": "K_RBRKT", + "text": "}" + }, + { + "width": "10", + "id": "T_new_246", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "K_BKQUOTE", + "text": "~" + }, + { + "id": "K_A", + "text": "A" + }, + { + "id": "K_S", + "text": "S" + }, + { + "id": "K_D", + "text": "D" + }, + { + "id": "K_F", + "text": "F" + }, + { + "id": "K_G", + "text": "G" + }, + { + "id": "K_H", + "text": "H" + }, + { + "id": "K_J", + "text": "J" + }, + { + "id": "K_K", + "text": "K" + }, + { + "id": "K_L", + "text": "L" + }, + { + "id": "K_COLON", + "text": ":" + }, + { + "id": "K_QUOTE", + "text": "\"" + }, + { + "id": "K_BKSLASH", + "text": "|" + } + ] + }, + { + "id": "4", + "key": [ + { + "nextlayer": "default", + "width": "160", + "id": "K_SHIFT", + "sp": "1", + "text": "*Shift*" + }, + { + "id": "K_oE2", + "text": "|" + }, + { + "id": "K_Z", + "text": "Z" + }, + { + "id": "K_X", + "text": "X" + }, + { + "id": "K_C", + "text": "C" + }, + { + "id": "K_V", + "text": "V" + }, + { + "id": "K_B", + "text": "B" + }, + { + "id": "K_N", + "text": "N" + }, + { + "id": "K_M", + "text": "M" + }, + { + "id": "K_COMMA", + "text": "<" + }, + { + "id": "K_PERIOD", + "text": ">" + }, + { + "id": "K_SLASH", + "text": "?" + }, + { + "width": "10", + "id": "T_new_272", + "sp": "10" + } + ] + }, + { + "id": "5", + "key": [ + { + "width": "140", + "id": "K_LOPT", + "sp": "1", + "text": "*Menu*" + }, + { + "width": "930", + "id": "K_SPACE" + }, + { + "width": "145", + "id": "K_ENTER", + "sp": "1", + "text": "*Enter*" + } + ] + } + ] + } + ] + } +} +; + this.KVER="16.0.142.0"; + this.KVS=[]; + this.gs=function(t,e) { + return this.g_main_0(t,e); + }; + this.gs=function(t,e) { + return this.g_main_0(t,e); + }; + this.g_main_0=function(t,e) { + var k=KeymanWeb,r=0,m=0; + if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_BKSP /* 0x08 */)) { + if(k.KFCM(3,t,['a',{t:'d',d:0},'b'])){ + r=m=1; // Line 23 + k.KDC(3,t); + k.KO(-1,t,"ok1"); + } + else if(k.KFCM(2,t,['a','b'])){ + r=m=1; // Line 24 + k.KDC(2,t); + k.KO(-1,t,"fail1"); + } + else if(k.KFCM(2,t,['a',{t:'d',d:0}])){ + r=m=1; // Line 25 + k.KDC(2,t); + k.KO(-1,t,"fail2"); + } + else if(k.KFCM(1,t,['^'])){ + r=m=1; // Line 17 + k.KDC(1,t); + k.KO(-1,t,"foo"); + } + } + else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_A /* 0x41 */)) { + if(k.KFCM(1,t,['^'])){ + r=m=1; // Line 16 + k.KDC(1,t); + k.KO(-1,t,"Â"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_BKQUOTE /* 0xC0 */)) { + if(1){ + r=m=1; // Line 19 + k.KDC(0,t); + k.KDO(-1,t,0); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_A /* 0x41 */)) { + if(k.KFCM(1,t,['^'])){ + r=m=1; // Line 15 + k.KDC(1,t); + k.KO(-1,t,"â"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_O /* 0x4F */)) { + if(k.KFCM(1,t,[{t:'d',d:0}])){ + r=m=1; // Line 26 + k.KDC(1,t); + k.KO(-1,t,"ok3"); + } + } + else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_T /* 0x54 */)) { + if(1){ + r=m=1; // Line 21 + k.KDC(0,t); + k.KO(-1,t,"\t"); + } + } + return r; + }; +} diff --git a/windows/src/engine/keyman32/appcontext.cpp b/windows/src/engine/keyman32/appcontext.cpp new file mode 100644 index 0000000000..de68efe0ae --- /dev/null +++ b/windows/src/engine/keyman32/appcontext.cpp @@ -0,0 +1,166 @@ +#include "pch.h" +// AppContext Class Methods +AppContext::AppContext() { + Reset(); +} + +WCHAR * +AppContext::BufMax(int n) { + WCHAR *p = wcschr(CurContext, 0); // I3091 + + if (CurContext == p || n == 0) + return p; /* empty context or 0 characters requested, return pointer to end of context */ // I3091 + + WCHAR *q = p; // I3091 + for (; p != NULL && p > CurContext && (INT_PTR)(q - p) < n; p = decxstr(p, CurContext)) + ; // I3091 + + if ((INT_PTR)(q - p) > n) + p = incxstr(p); /* Copes with deadkey or supplementary pair at start of returned buffer making it too long */ // I3091 + + return p; // I3091 +} + +void +AppContext::Delete() { + if (CharIsDeadkey()) { + pos -= 2; + } else if (CharIsSurrogatePair()) { + pos--; + } + // SendDebugMessageFormat(0, sdmAIDefault, 0, "AppContext::Delete"); + + if (pos > 0) + pos--; + CurContext[pos] = 0; + // if(--pos < 0) pos = 0; + // SendDebugMessageFormat(0, sdmAIDefault, 0, "AppContext: Delete"); +} + +void +AppContext::Reset() { + pos = 0; + CurContext[0] = 0; + + // SendDebugMessageFormat(0, sdmAIDefault, 0, "AppContext: Reset"); +} + +void +AppContext::Get(WCHAR *buf, int bufsize) { + // surrogate pairs need to be treated as a single unit, therefore use + // BufMax to find a start index. + // BufMax handles the case where a surrogate pair at the + // start of the buffer is split by bufsize + for (WCHAR *p = this->BufMax(bufsize); *p && bufsize > 0; p++, bufsize--) { + *buf = *p; + if (Uni_IsSurrogate1(*p) && bufsize - 2 > 0) { + buf++; + p++; + *buf = *p; + bufsize--; + } + buf++; + } + + *buf = 0; +} + +void +AppContext::Set(const WCHAR *buf) { + const WCHAR *p; + WCHAR *q; + + // We may be past a buffer longer than our internal + // buffer. So we shift to make sure we capture the end + // of the string, not the start + p = wcschr(buf, 0); + q = (WCHAR *)p; + while (p != NULL && p > buf && (intptr_t)(q - p) < MAXCONTEXT - 1) { + p = decxstr((WCHAR *)p, (WCHAR *)buf); + } + + // If the first character in the buffer is a surrogate pair, + // or a deadkey, our buffer may be too long, so move to the + // next character in the buffer + if ((intptr_t)(q - p) > MAXCONTEXT - 1) { + p = incxstr((WCHAR *)p); + } + + for (q = CurContext; *p; p++, q++) { + *q = *p; + } + + *q = 0; + pos = (int)(intptr_t)(q - CurContext); + CurContext[MAXCONTEXT - 1] = 0; +} + +BOOL +AppContext::CharIsDeadkey() { + if (pos < 3) // code_sentinel, deadkey, #, 0 + return FALSE; + return CurContext[pos - 3] == UC_SENTINEL && CurContext[pos - 2] == CODE_DEADKEY; +} + +BOOL +AppContext::CharIsSurrogatePair() { + if (pos < 2) // low_surrogate, high_surrogate + return FALSE; + + return Uni_IsSurrogate1(CurContext[pos - 2]) && Uni_IsSurrogate2(CurContext[pos - 1]); +} + +BOOL +AppContext::IsEmpty() { + return (BOOL)(pos == 0); +} + +BOOL +ContextItemToAppContext(km_core_context_item *contextItems, PWSTR outBuf, DWORD len) { + assert(contextItems); + assert(outBuf); + + km_core_context_item *km_core_context_it = contextItems; + uint8_t contextLen = 0; + for (; km_core_context_it->type != KM_CORE_CT_END; ++km_core_context_it) { + ++contextLen; + } + + WCHAR *buf = new WCHAR[(contextLen * 3) + 1]; // *3 if every context item was a deadkey + uint8_t idx = 0; + km_core_context_it = contextItems; + for (; km_core_context_it->type != KM_CORE_CT_END; ++km_core_context_it) { + switch (km_core_context_it->type) { + case KM_CORE_CT_CHAR: + if (Uni_IsSMP(km_core_context_it->character)) { + buf[idx++] = static_cast Uni_UTF32ToSurrogate1(km_core_context_it->character); + buf[idx++] = static_cast Uni_UTF32ToSurrogate2(km_core_context_it->character); + } else { + buf[idx++] = (km_core_cp)km_core_context_it->character; + } + break; + case KM_CORE_CT_MARKER: + assert(km_core_context_it->marker > 0); + buf[idx++] = UC_SENTINEL; + buf[idx++] = CODE_DEADKEY; + buf[idx++] = static_cast(km_core_context_it->marker); + break; + } + } + + buf[idx] = 0; // Null terminate character array + + if (wcslen(buf) > len) { + // Truncate to length 'len' using AppContext so that the context closest to the caret is preserved + // and the truncation will not split deadkeys or surrogate pairs + // Note by using the app context class we will truncate the context to the MAXCONTEXT length if 'len' + // is greater than MAXCONTEXT + AppContext context; + context.Set(buf); + context.Get(outBuf, len); + } else { + wcscpy_s(outBuf, wcslen(buf) + 1, buf); + } + delete[] buf; + return TRUE; +} diff --git a/windows/src/engine/keyman32/appcontext.h b/windows/src/engine/keyman32/appcontext.h new file mode 100644 index 0000000000..d0859262df --- /dev/null +++ b/windows/src/engine/keyman32/appcontext.h @@ -0,0 +1,107 @@ +#ifndef _APPCONTEXT_H +#define _APPCONTEXT_H +/* + Name: appcontext + Copyright: Copyright (C) SIL International. + Documentation: + Description: + Create Date: 23 Nov 2023 + + Modified Date: 23 Nov 2023 + Authors: rcruickshank + Related Files: + Dependencies: + + Bugs: + Todo: + Notes: AppContext is retained to support calldll with the external interface for the 3rd party IMX keyboards + that worked with KMX formatted Context Strings. It is also used once for debug logging the ProcessHook. + History: +*/ + +class AppContext { +private: + WCHAR CurContext[MAXCONTEXT]; //!< CurContext[0] is furthest from the caret and buffer is null terminated. + int pos; + +public: + AppContext(); + + /** + * Removes a single code point from the end of the CurContext closest to the caret; + * i.e. it will be both code units if a surrogate pair. If it is a deadkey it will + * remove three code points: UC_SENTINEL, CODE_DEADKEY and deadkey value. + */ + void Delete(); + + /** + * Clears the CurContext and resets the position - pos - index + */ + void Reset(); + + /** + * Copies the characters in CurContext to supplied buffer. + * If bufsize is reached before the entire context was copied, the buf + * will be truncated to number of valid characters possible with null character + * termination. e.g. it will be one code unit less than bufsize if that would + * have meant splitting a surrogate pair + * @param buf The data buffer to copy current context + * @param bufsize The number of code units ie size of the WCHAR buffer - not the code points + */ + void Get(WCHAR *buf, int bufsize); + + /** + * Sets the CurContext to the supplied buf character array and updates the pos index. + * + * @param buf + */ + void Set(const WCHAR *buf); + + /** + * Returns a pointer to the character in the current context buffer which + * will have at most n valid xstring units remaining until the null terminating + * character. It will be one code unit less than bufsize if that would + * have meant splitting a surrogate pair or deadkey. + * + * @param n The maximum number of valid xstring units (not code points or code units) + * @return WCHAR* Pointer to the start postion for a buffer of maximum n xstring units + */ + WCHAR *BufMax(int n); + + /** + * Returns TRUE if the last xstring unit in the context is a deadkey + * + * @return BOOL + */ + BOOL CharIsDeadkey(); + + /** + * Returns TRUE if the last xstring unit in the CurContext is a surrogate pair. + * @return BOOL + */ + BOOL CharIsSurrogatePair(); + + /** + * Returns TRUE if the context is empty + * @return BOOL + */ + BOOL AppContext::IsEmpty(); +}; + +/** + * Convert km_core_context_item array into an kmx char buffer. + * Caller is responsible for freeing the memory. + * The length is restricted to a maximum of MAXCONTEXT length. If the number + * of input km_core_context_items exceeds this length the characters furthest + * from the caret will be truncated. + * + * @param contextItems the input core context array. (km_core_context_item) + * @param [out] outBuf the kmx character array output. caller to free memory. + * + * @return BOOL True if array created successfully + */ +BOOL ContextItemToAppContext(km_core_context_item *contextItems, PWSTR outBuf, DWORD len); + + + +#endif diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index a7faa7170f..93ffcb445d 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -269,89 +269,27 @@ char *debugstr(PWSTR buf) { /* Context functions */ -void AITIP::MergeContextWithCache(PWSTR buf, AppContext *local_context) { // I4262 - WCHAR tmpbuf[MAXCONTEXT], contextExDeadkeys[MAXCONTEXT]; - local_context->Get(tmpbuf, MAXCONTEXT-1); - - int n = 0; - PWSTR p = tmpbuf, q = contextExDeadkeys, r = buf; // I4266 - while(*p) { - if(*p == UC_SENTINEL) { - p += 2; // We know the only UC_SENTINEL CODE in the context is CODE_DEADKEY, which has only 1 parameter: UC_SENTINEL CODE_DEADKEY - n++; - } else { - *q++ = *p; - } - p++; - } - *q = 0; - - if(n > 0 && wcslen(buf) > wcslen(contextExDeadkeys)) { // I4266 - r += wcslen(buf) - wcslen(contextExDeadkeys); +BOOL AITIP::ReadContext(PWSTR buf) { + if (buf == nullptr) { + return FALSE; } - // We have to cut off the context comparison from the left by #deadkeys matched to ensure we are comparing like with like, - // at least when tmpbuf len=MAXCONTEXT-1 at entry. - -#ifdef DEBUG_MERGECONTEXT - char *mc1 = debugstr(buf), *mc2 = debugstr(contextExDeadkeys), *mc3 = debugstr(tmpbuf); - - SendDebugMessageFormat(0, sdmAIDefault, 0, "AITIP::MergeContextWithCache TIP:'%s' Context:'%s' DKContext:'%s'", - mc1, mc2, mc3); - - delete mc1; - delete mc2; - delete mc3; -#endif - - if(wcscmp(r, contextExDeadkeys) != 0) { - // context has changed, reset context -#ifdef DEBUG_MERGECONTEXT - SendDebugMessageFormat(0, sdmAIDefault, 0, "AITIP::MergeContextWithCache --> load context from app (losing deadkeys)"); -#endif - local_context->Set(buf); - } else { -#ifdef DEBUG_MERGECONTEXT - SendDebugMessageFormat(0, sdmAIDefault, 0, "AITIP::MergeContextWithCache --> loading cached context"); -#endif - wcscpy_s(buf, MAXCONTEXT, tmpbuf); - } -} - -void AITIP::ReadContext() { - WCHAR buf[MAXCONTEXT]; PKEYMAN64THREADDATA _td = ThreadGlobals(); - if(!_td) return; + if(!_td) return FALSE; if(_td->TIPGetContext && (*_td->TIPGetContext)(MAXCONTEXT-1, buf) == S_OK) { // I3575 // I4262 if(ShouldDebug(sdmKeyboard)) { SendDebugMessageFormat(0, sdmAIDefault, 0, "AITIP::ReadContext: full context [Updateable=%d] %s", _td->TIPFUpdateable, Debug_UnicodeString(buf)); } useLegacy = FALSE; // I3575 - - // If the text content of the context is identical, inject the deadkeys - // Otherwise, reset the cachedContext to match buf, no deadkeys - - MergeContextWithCache(buf, context); - - if(ShouldDebug(sdmKeyboard)) { - SendDebugMessageFormat(0, sdmAIDefault, 0, "AITIP::ReadContext: after merge [Updateable=%d] %s", _td->TIPFUpdateable, Debug_UnicodeString(buf)); - } - - context->Set(buf); + return TRUE; } else { SendDebugMessageFormat(0, sdmAIDefault, 0, "AITIP::ReadContext: transitory context, so use buffered context [Updateable=%d]", _td->TIPFUpdateable); useLegacy = TRUE; // I3575 + return FALSE; } } -void AITIP::CopyContext(AppContext *savedContext) { - savedContext->CopyFrom(context); -} - -void AITIP::RestoreContextOnly(AppContext *savedContext) { - context->CopyFrom(savedContext); -} /* Output actions */ diff --git a/windows/src/engine/keyman32/appint/aiTIP.h b/windows/src/engine/keyman32/appint/aiTIP.h index 93c9acd4ab..bfbca87d89 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.h +++ b/windows/src/engine/keyman32/appint/aiTIP.h @@ -37,9 +37,6 @@ class AITIP : public AIWin2000Unicode { -private: - void MergeContextWithCache(PWSTR buf, AppContext *context); // I4262 - private: BOOL useLegacy; @@ -50,20 +47,6 @@ public: AITIP(); ~AITIP(); - /** - * Copy the member context - * - * @param[out] savedContext the copied context - */ - void CopyContext(AppContext *savedContext); - - /** - * Restore the passed context to the member context - * - * @param savedContext the context to restore - */ - void RestoreContextOnly(AppContext *savedContext); - /* Information functions */ virtual BOOL CanHandleWindow(HWND ahwnd); @@ -71,7 +54,12 @@ public: /* Context functions */ - virtual void ReadContext(); + /** + * Reads the current application context upto MAXCONTEXT length into the supplied buffer. + * @param buf The data buffer to copy current application context into, must + * be MAXCONTEXT WCHARs or larger. + */ + virtual BOOL ReadContext(PWSTR buf); /* Queue and sending functions */ diff --git a/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp b/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp index a6b540fd47..e8562bea35 100644 --- a/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp +++ b/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp @@ -43,16 +43,9 @@ #include "pch.h" // I4128 // I4287 #include "serialkeyeventclient.h" - -AIWin2000Unicode::AIWin2000Unicode() -{ - context = new AppContext; -} - -AIWin2000Unicode::~AIWin2000Unicode() -{ - delete context; +AIWin2000Unicode::AIWin2000Unicode() { } +AIWin2000Unicode::~AIWin2000Unicode(){} /* Information functions */ @@ -68,7 +61,7 @@ BOOL AIWin2000Unicode::HandleWindow(HWND ahwnd) if(hwnd != ahwnd) { hwnd = ahwnd; - context->Reset(); + ResetContext(); } return TRUE; } @@ -87,33 +80,20 @@ BOOL AIWin2000Unicode::IsUnicode() /* Context functions */ -void AIWin2000Unicode::ReadContext() -{ -} - -void AIWin2000Unicode::AddContext(WCHAR ch) //I2436 -{ - context->Add(ch); +BOOL AIWin2000Unicode::ReadContext(PWSTR buf) { + UNREFERENCED_PARAMETER(buf); + // We cannot read any context from legacy apps, so we return a + // failure here -- telling Core to maintain its own cached + // context. + return FALSE; } void AIWin2000Unicode::ResetContext() { - context->Reset(); -} - -WCHAR *AIWin2000Unicode::ContextBuf(int n) -{ - return context->Buf(n); -} - -WCHAR *AIWin2000Unicode::ContextBufMax(int n) -{ - return context->BufMax(n); -} - -void AIWin2000Unicode::SetContext(const WCHAR* buf) -{ - return context->Set(buf); + PKEYMAN64THREADDATA _td = ThreadGlobals(); + if (_td && _td->lpActiveKeyboard && _td->lpActiveKeyboard->lpCoreKeyboardState) { + km_core_state_context_clear(_td->lpActiveKeyboard->lpCoreKeyboardState); + } } BYTE SavedKbdState[256]; @@ -126,40 +106,6 @@ BOOL AIWin2000Unicode::SendActions() // I4196 return PostKeys(); } -BOOL AIWin2000Unicode::QueueAction(int ItemType, DWORD dwData) -{ - int result = AppIntegration::QueueAction(ItemType, dwData); - - //SendDebugMessageFormat(hwnd, sdmAIDefault, 0, "App::QueueAction ItemType=%d dwData=%x", ItemType, dwData); - - switch(ItemType) - { - case QIT_VKEYDOWN: - break; - - case QIT_DEADKEY: - context->Add(UC_SENTINEL); - context->Add(CODE_DEADKEY); - context->Add((WORD) dwData); - break; - - case QIT_CHAR: - context->Add((WORD) dwData); - break; - - case QIT_BACK: - if(dwData & BK_BACKSPACE) - while(context->CharIsDeadkey()) context->Delete(); - //if(dwData == CODE_DEADKEY) break; - context->Delete(); - if(dwData & BK_BACKSPACE) - while(context->CharIsDeadkey()) context->Delete(); - break; - } - - return result; -} - // I1512 - SendInput with VK_PACKET for greater robustness BOOL AIWin2000Unicode::PostKeys() diff --git a/windows/src/engine/keyman32/appint/aiWin2000Unicode.h b/windows/src/engine/keyman32/appint/aiWin2000Unicode.h index 1ee49ed001..523832742f 100644 --- a/windows/src/engine/keyman32/appint/aiWin2000Unicode.h +++ b/windows/src/engine/keyman32/appint/aiWin2000Unicode.h @@ -32,15 +32,9 @@ private: BOOL PostKeys(); - -protected: - AppContext *context; - public: - AIWin2000Unicode(); - ~AIWin2000Unicode(); - - virtual BOOL QueueAction(int ItemType, DWORD dwData); + AIWin2000Unicode(); + ~AIWin2000Unicode(); /* Information functions */ @@ -51,13 +45,9 @@ public: /* Context functions */ - virtual void ReadContext(); + virtual BOOL ReadContext(PWSTR buf); virtual void ResetContext(); - virtual void AddContext(WCHAR ch); //I2436 - virtual WCHAR *ContextBuf(int n); - virtual WCHAR *ContextBufMax(int n); - virtual void SetContext(const WCHAR* buf); - + /* Queue and sending functions */ virtual BOOL SendActions(); // I4196 diff --git a/windows/src/engine/keyman32/appint/appint.cpp b/windows/src/engine/keyman32/appint/appint.cpp index 8a2b92e5aa..13be8efda4 100644 --- a/windows/src/engine/keyman32/appint/appint.cpp +++ b/windows/src/engine/keyman32/appint/appint.cpp @@ -32,161 +32,6 @@ const LPSTR ItemTypes[8] = { "QIT_VKEYDOWN", "QIT_VKEYUP", "QIT_VSHIFTDOWN", "QIT_VSHIFTUP", "QIT_CHAR", "QIT_DEADKEY", "QIT_BELL", "QIT_BACK" }; -/* AppContext */ - -AppContext::AppContext() -{ - Reset(); -} - -void AppContext::Add(WCHAR ch) -{ - if(pos == MAXCONTEXT - 1) { -// SendDebugMessageFormat(0, sdmAIDefault, 0, "AppContext: MAXCONTEXT[%d]: %ws", pos, CurContext); - auto p = incxstr(CurContext); - auto n = p - CurContext; - memmove(CurContext, p, (MAXCONTEXT - n) * 2); - pos -= (int)n; - } - - CurContext[pos++] = ch; - CurContext[pos] = 0; - - SendDebugMessageFormat(0, sdmAIDefault, 0, "AppContext: Add(%x) [%d]: %s", ch, pos, Debug_UnicodeString(CurContext)); -} - -WCHAR *AppContext::Buf(int n) -{ - WCHAR *p; - - //SendDebugMessageFormat(0, sdmAIDefault, 0, "AppContext::Buf(%d)", n); - //if(n == 0) return wcschr(CurContext, 0); - //if(*CurContext == 0) return NULL; - - for(p = wcschr(CurContext, 0); p != NULL && n > 0 && p > CurContext; p = decxstr(p, CurContext), n--); - //for(p = wcschr(CurContext, 0); n > 0 && p > CurContext; p--, n--); - - if(n > 0) return NULL; - return p; -} - -WCHAR *AppContext::BufMax(int n) -{ - WCHAR *p = wcschr(CurContext, 0); // I3091 - - if(CurContext == p || n == 0) return p; /* empty context or 0 characters requested, return pointer to end of context */ // I3091 - - WCHAR *q = p; // I3091 - for(; p != NULL && p > CurContext && (INT_PTR)(q-p) < n; p = decxstr(p, CurContext)); // I3091 - - if((INT_PTR)(q-p) > n) p = incxstr(p); /* Copes with deadkey or supplementary pair at start of returned buffer making it too long */ // I3091 - - return p; // I3091 -} - -void AppContext::Delete() -{ - if (CharIsDeadkey()) { - pos -= 2; - } else if (CharIsSurrogatePair()) { - pos--; - } - //SendDebugMessageFormat(0, sdmAIDefault, 0, "AppContext::Delete"); - - if(pos > 0) pos--; - CurContext[pos] = 0; - //if(--pos < 0) pos = 0; - //SendDebugMessageFormat(0, sdmAIDefault, 0, "AppContext: Delete"); -} - -void AppContext::Reset() -{ - pos = 0; - CurContext[0] = 0; - -// SendDebugMessageFormat(0, sdmAIDefault, 0, "AppContext: Reset"); -} - -void AppContext::Get(WCHAR *buf, int bufsize) -{ - // surrogate pairs need to be treated as a single unit, therefore use - // BufMax to find a start index. - // BufMax handles the case where a surrogate pair at the - // start of the buffer is split by bufsize - for (WCHAR *p = this->BufMax(bufsize); *p && bufsize > 0; p++, bufsize--) - { - *buf = *p; - if(Uni_IsSurrogate1(*p) && bufsize - 2 > 0) { - buf++; p++; - *buf = *p; - bufsize--; - } - buf++; - } - - *buf = 0; -} - -void AppContext::CopyFrom(AppContext *source) // I3575 -{ - SendDebugMessageFormat(0, sdmAIDefault, 0, "AppContext::CopyFrom source=%s; before copy, dest=%s", Debug_UnicodeString(source->CurContext, 0), Debug_UnicodeString(CurContext, 0)); - wcscpy_s(CurContext, _countof(CurContext), source->CurContext); - pos = source->pos; -} - - -void AppContext::Set(const WCHAR *buf) -{ - const WCHAR *p; - WCHAR *q; - - // We may be past a buffer longer than our internal - // buffer. So we shift to make sure we capture the end - // of the string, not the start - p = wcschr(buf, 0); - q = (WCHAR *)p; - while (p != NULL && p > buf && (intptr_t)(q - p) < MAXCONTEXT - 1) { - p = decxstr((WCHAR *)p, (WCHAR *)buf); - } - - // If the first character in the buffer is a surrogate pair, - // or a deadkey, our buffer may be too long, so move to the - // next character in the buffer - if ((intptr_t)(q - p) > MAXCONTEXT - 1) { - p = incxstr((WCHAR *)p); - } - - for (q = CurContext; *p; p++, q++) { - *q = *p; - } - - *q = 0; - pos = (int)(intptr_t)(q - CurContext); - CurContext[MAXCONTEXT - 1] = 0; - -} - -BOOL AppContext::CharIsDeadkey() -{ - if(pos < 3) // code_sentinel, deadkey, #, 0 - return FALSE; - return CurContext[pos-3] == UC_SENTINEL && - CurContext[pos-2] == CODE_DEADKEY; -} - -BOOL AppContext::CharIsSurrogatePair() -{ - if (pos < 2) // low_surrogate, high_surrogate - return FALSE; - - return Uni_IsSurrogate1(CurContext[pos - 2]) && - Uni_IsSurrogate2(CurContext[pos - 1]); -} - -BOOL AppContext::IsEmpty() { - return (BOOL)(pos == 0); -} - /* AppActionQueue */ AppActionQueue::AppActionQueue() @@ -226,84 +71,3 @@ AppIntegration::AppIntegration() hwnd = NULL; FShiftFlags = 0; } - -BOOL ContextItemsFromAppContext(WCHAR const* buf, km_core_context_item** outPtr) -{ - assert(buf); - assert(outPtr); - km_core_context_item* context_items = new km_core_context_item[wcslen(buf) + 1]; - WCHAR const *p = buf; - uint8_t contextIndex = 0; - while (*p) { - if (*p == UC_SENTINEL) { - assert(*(p + 1) == CODE_DEADKEY); - // we know the only uc_sentinel code in the context is code_deadkey, which has only 1 parameter: uc_sentinel code_deadkey - // setup dead key context item - p += 2; - context_items[contextIndex++] = km_core_context_item{ KM_CORE_CT_MARKER, {0,}, {*p} }; - } else if (Uni_IsSurrogate1(*p) && Uni_IsSurrogate2(*(p + 1))) { - // handle surrogate - context_items[contextIndex++] = km_core_context_item{ KM_CORE_CT_CHAR, {0,}, {(char32_t)Uni_SurrogateToUTF32(*p, *(p + 1))} }; - p++; - } else { - context_items[contextIndex++] = km_core_context_item{ KM_CORE_CT_CHAR, {0,}, {*p} }; - } - p++; - } - // terminate the context_items array. - context_items[contextIndex] = km_core_context_item KM_CORE_CONTEXT_ITEM_END; - - *outPtr = context_items; - return true; -} - - -BOOL -ContextItemToAppContext(km_core_context_item *contextItems, PWSTR outBuf, DWORD len) { - assert(contextItems); - assert(outBuf); - - km_core_context_item *km_core_context_it = contextItems; - uint8_t contextLen = 0; - for (; km_core_context_it->type != KM_CORE_CT_END; ++km_core_context_it) { - ++contextLen; - } - - WCHAR *buf = new WCHAR[(contextLen*3)+ 1 ]; // *3 if every context item was a deadkey - uint8_t idx = 0; - km_core_context_it = contextItems; - for (; km_core_context_it->type != KM_CORE_CT_END; ++km_core_context_it) { - switch (km_core_context_it->type) { - case KM_CORE_CT_CHAR: - if (Uni_IsSMP(km_core_context_it->character)) { - buf[idx++] = static_cast Uni_UTF32ToSurrogate1(km_core_context_it->character); - buf[idx++] = static_cast Uni_UTF32ToSurrogate2(km_core_context_it->character); - } else { - buf[idx++] = (km_core_cp)km_core_context_it->character; - } - break; - case KM_CORE_CT_MARKER: - assert(km_core_context_it->marker > 0); - buf[idx++] = UC_SENTINEL; - buf[idx++] = CODE_DEADKEY; - buf[idx++] = static_cast(km_core_context_it->marker); - break; - } - } - - buf[idx] = 0; // Null terminate character array - - if (wcslen(buf) > len) { - // Truncate to length 'len' using AppContext so that the context closest to the caret is preserved - // and the truncation will not split deadkeys or surrogate pairs - // Note by using the app context class we will truncate the context to the MAXCONTEXT length if 'len' - // is greater than MAXCONTEXT - AppContext context; - context.Set(buf); - context.Get(outBuf, len); - } else { - wcscpy_s(outBuf, wcslen(buf) + 1, buf); - } - delete[] buf; - return TRUE; -} diff --git a/windows/src/engine/keyman32/appint/appint.h b/windows/src/engine/keyman32/appint/appint.h index 8048c4a8a9..ae31a1b54f 100644 --- a/windows/src/engine/keyman32/appint/appint.h +++ b/windows/src/engine/keyman32/appint/appint.h @@ -65,103 +65,6 @@ public: int GetQueueSize() { return QueueSize; } }; -class AppContext -{ -private: - WCHAR CurContext[MAXCONTEXT]; //!< CurContext[0] is furthest from the caret and buffer is null terminated. - int pos; - -public: - AppContext(); - /** - * Copy "source" AppContext to this AppContext - * - * @param source AppContext to copy - */ - void CopyFrom(AppContext *source); - - /** - * Add a single code unit to the Current Context. Not necessarily a complete code point - * - * @param Code unit to add - */ - void Add(WCHAR ch); - - /** - * Removes a single code point from the end of the CurContext closest to the caret; - * i.e. it will be both code units if a surrogate pair. If it is a deadkey it will - * remove three code points: UC_SENTINEL, CODE_DEADKEY and deadkey value. - */ - void Delete(); - - /** - * Clears the CurContext and resets the position - pos - index - */ - void Reset(); - - /** - * Copies the characters in CurContext to supplied buffer. - * If bufsize is reached before the entire context was copied, the buf - * will be truncated to number of valid characters possible with null character - * termination. e.g. it will be one code unit less than bufsize if that would - * have meant splitting a surrogate pair - * @param buf The data buffer to copy current context - * @param bufsize The number of code units ie size of the WCHAR buffer - not the code points - */ - void Get(WCHAR *buf, int bufsize); - - /** - * Sets the CurContext to the supplied buf character array and updates the pos index. - * - * @param buf - */ - void Set(const WCHAR *buf); - - /** - * Returns a pointer to the character in the current context buffer which - * will have at most n valid xstring units remaining until the null terminating - * character. It will be one code unit less than bufsize if that would - * have meant splitting a surrogate pair or deadkey. - * - * @param n The maximum number of valid xstring units (not code points or code units) - * @return WCHAR* Pointer to the start postion for a buffer of maximum n xstring units - */ - WCHAR *BufMax(int n); - - /** - * Returns a pointer to the character in the current context buffer which - * will have n valid xstring units remaining until the the null terminating character. - * OR - * Returns NULL if there are less than n valid xstring units in the current context. - * Background this was historically for performance during rule evaluation, if there - * are not enough characters to compare, don't event attempt the comparison. - * - * @param n The number of valid xstring units (not code points or code units) - * @return KMX_WCHAR* Pointer to the start postion for a buffer of maximum n characters - */ - WCHAR *Buf(int n); - - /** - * Returns TRUE if the last xstring unit in the context is a deadkey - * - * @return BOOL - */ - BOOL CharIsDeadkey(); - - /** - * Returns TRUE if the last xstring unit in the CurContext is a surrogate pair. - * @return BOOL - */ - BOOL CharIsSurrogatePair(); - - /** - * Returns TRUE if the context is empty - * @return BOOL - */ - BOOL AppContext::IsEmpty(); - -}; - class AppIntegration:public AppActionQueue { protected: @@ -180,12 +83,12 @@ public: virtual BOOL IsUnicode() = 0; /* Context functions */ - - virtual void ReadContext() = 0; + /** + * Reads the current application context upto MAXCONTEXT length into the supplied buffer. + * @param buf The data buffer to copy current application context + */ + virtual BOOL ReadContext(PWSTR buf) = 0; virtual void ResetContext() = 0; - virtual void AddContext(WCHAR ch) = 0; //I2436 - virtual WCHAR *ContextBuf(int n) = 0; - virtual WCHAR *ContextBufMax(int n) = 0; /* Queue and sending functions */ @@ -193,30 +96,6 @@ public: virtual BOOL SendActions() = 0; // I4196 }; -/** - * Convert AppContext array into an array of core context items. - * Caller is responsible for freeing the memory. - * - * @param buf appcontext character array - * @param outPtr The ouput array of context items. caller to free memory - * @return BOOL True if array created successfully - */ -BOOL ContextItemsFromAppContext(WCHAR const* buf, km_core_context_item** outPtr); - -/** - * Convert km_core_context_item array into an kmx char buffer. - * Caller is responsible for freeing the memory. - * The length is restricted to a maximum of MAXCONTEXT length. If the number - * of input km_core_context_items exceeds this length the characters furthest - * from the caret will be truncated. - * - * @param contextItems the input core context array. (km_core_context_item) - * @param [out] outBuf the kmx character array output. caller to free memory. - * - * @return BOOL True if array created successfully - */ -BOOL ContextItemToAppContext(km_core_context_item *contextItems, PWSTR outBuf, DWORD len); - extern const LPSTR ItemTypes[]; #endif diff --git a/windows/src/engine/keyman32/keyman32.vcxproj b/windows/src/engine/keyman32/keyman32.vcxproj index 70b8402ad7..021aa55d65 100644 --- a/windows/src/engine/keyman32/keyman32.vcxproj +++ b/windows/src/engine/keyman32/keyman32.vcxproj @@ -176,6 +176,7 @@ + %(AdditionalIncludeDirectories) %(PreprocessorDefinitions) @@ -354,6 +355,7 @@ + @@ -387,4 +389,4 @@ - + \ No newline at end of file diff --git a/windows/src/engine/keyman32/keyman32.vcxproj.filters b/windows/src/engine/keyman32/keyman32.vcxproj.filters index dcf5f5d4c5..0f16750b7c 100644 --- a/windows/src/engine/keyman32/keyman32.vcxproj.filters +++ b/windows/src/engine/keyman32/keyman32.vcxproj.filters @@ -138,6 +138,9 @@ Source Files + + Source Files + @@ -246,6 +249,9 @@ Header Files + + Header Files + diff --git a/windows/src/engine/keyman32/keymanengine.h b/windows/src/engine/keyman32/keymanengine.h index 3c6e2d3aff..c0db193fdf 100644 --- a/windows/src/engine/keyman32/keymanengine.h +++ b/windows/src/engine/keyman32/keymanengine.h @@ -126,7 +126,6 @@ BOOL IsSysTrayWindow(HWND hwnd); BOOL InitialiseProcess(HWND hwnd); BOOL UninitialiseProcess(BOOL Lock); -BOOL IsKeyboardUnicode(); BOOL IsFocusedThread(); @@ -231,6 +230,7 @@ void keybd_shift(LPINPUT pInputs, int* n, BOOL isReset, LPBYTE const kbd); #include "keymancontrol.h" #include "keyboardoptions.h" #include "kmprocessactions.h" +#include "appcontext.h" #include "syskbd.h" #include "vkscancodes.h" diff --git a/windows/src/engine/keyman32/kmprocess.cpp b/windows/src/engine/keyman32/kmprocess.cpp index 7e5e807056..b83079b03d 100644 --- a/windows/src/engine/keyman32/kmprocess.cpp +++ b/windows/src/engine/keyman32/kmprocess.cpp @@ -70,23 +70,29 @@ BOOL fOutputKeystroke; -/*char *getcontext() -{ - WCHAR buf[128]; - static char bufout[128]; - PKEYMAN64THREADDATA _td = ThreadGlobals(); - if(!_td) return ""; - _td->app->GetWindowContext(buf, 128); - WideCharToMultiByte(CP_ACP, 0, buf, -1, bufout, 128, NULL, NULL); - return bufout; -}*/ - - char *getcontext_debug() { - //return ""; + PKEYMAN64THREADDATA _td = ThreadGlobals(); - if(!_td) return ""; - return Debug_UnicodeString(_td->app->ContextBufMax(128)); + if (!_td || !_td->lpActiveKeyboard || !_td->lpActiveKeyboard->lpCoreKeyboardState){ + return ""; + } + + WCHAR buf[(MAXCONTEXT * 3) + 1]; // *3 if every context item was a deadkey + km_core_context_item *citems = nullptr; + + if (KM_CORE_STATUS_OK != km_core_context_get( + km_core_state_context(_td->lpActiveKeyboard->lpCoreKeyboardState), &citems)) { + return ""; + } + + DWORD context_length = (DWORD)km_core_context_item_list_size(citems); + if (!ContextItemToAppContext(citems, buf, context_length)) { + km_core_context_items_dispose(citems); + return ""; + } + km_core_context_items_dispose(citems); + return Debug_UnicodeString(buf); + } /** @@ -98,14 +104,15 @@ char *getcontext_debug() { static BOOL Process_Event_Core(PKEYMAN64THREADDATA _td) { - PWSTR contextBuf = _td->app->ContextBufMax(MAXCONTEXT); - km_core_context_item *citems = nullptr; - ContextItemsFromAppContext(contextBuf, &citems); - if (KM_CORE_STATUS_OK != km_core_context_set(km_core_state_context(_td->lpActiveKeyboard->lpCoreKeyboardState), citems)) { - km_core_context_items_dispose(citems); - return FALSE; + WCHAR application_context[MAXCONTEXT]; + if (_td->app->ReadContext(application_context)) { + km_core_context_status result; + result = km_core_state_context_set_if_needed(_td->lpActiveKeyboard->lpCoreKeyboardState, reinterpret_cast(application_context)); + if (result == KM_CORE_CONTEXT_STATUS_ERROR || result == KM_CORE_CONTEXT_STATUS_INVALID_ARGUMENT) { + SendDebugMessageFormat(0, sdmGlobal, 0, "Process_Event_Core: km_core_state_context_set_if_needed returned [%d]", result); + } } - km_core_context_items_dispose(citems); + SendDebugMessageFormat( 0, sdmGlobal, 0, "ProcessEvent: vkey[%d] ShiftState[%d] isDown[%d]", _td->state.vkey, static_cast(Globals::get_ShiftState() & (KM_CORE_MODIFIER_MASK_ALL | KM_CORE_MODIFIER_MASK_CAPS)), (uint8_t)_td->state.isDown); @@ -139,8 +146,6 @@ BOOL ProcessHook() fOutputKeystroke = FALSE; // TODO: 5442 no longer needs to be global once we use core processor - _td->app->ReadContext(); - if(_td->state.msg.message == wm_keymankeydown) { // I4827 if (ShouldDebug(sdmKeyboard)) { SendDebugMessageFormat(_td->state.msg.hwnd, sdmKeyboard, 0, "Key pressed: %s Context '%s'", @@ -212,9 +217,6 @@ BOOL ProcessHook() _td->app->SetCurrentShiftState(Globals::get_ShiftState()); _td->app->SendActions(); // I4196 } - // output context for debugging - // PWSTR contextBuf = _td->app->ContextBufMax(MAXCONTEXT); - // SendDebugMessageFormat(0, sdmAIDefault, 0, "Kmprocess::ProcessHook After cxt=%s", Debug_UnicodeString(contextBuf, 1)); return !fOutputKeystroke; } diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index c9d607a596..1eecfa6807 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -78,10 +78,8 @@ static BOOL processPersistOpt( } static BOOL processInvalidateContext( - AITIP* app, - km_core_state* keyboardState + AITIP* app ) { - km_core_context_clear(km_core_state_context(keyboardState)); app->ResetContext(); return TRUE; } @@ -158,7 +156,7 @@ BOOL ProcessActions(BOOL* emitKeyStroke) continueProcessingActions = TRUE; break; case KM_CORE_IT_INVALIDATE_CONTEXT: - continueProcessingActions = processInvalidateContext(_td->app, _td->lpActiveKeyboard->lpCoreKeyboardState); + continueProcessingActions = processInvalidateContext(_td->app); break; case KM_CORE_IT_CAPSLOCK: continueProcessingActions = processCapsLock(act, !_td->state.isDown, _td->TIPFUpdateable, FALSE); @@ -202,7 +200,7 @@ ProcessActionsNonUpdatableParse(BOOL* emitKeyStroke) { continueProcessingActions = processCapsLock(act, !_td->state.isDown, _td->TIPFUpdateable, FALSE); break; case KM_CORE_IT_INVALIDATE_CONTEXT: - continueProcessingActions = processInvalidateContext(_td->app, _td->lpActiveKeyboard->lpCoreKeyboardState); + continueProcessingActions = processInvalidateContext(_td->app); break; } if (!continueProcessingActions) { @@ -228,7 +226,7 @@ ProcessActionsExternalEvent() { continueProcessingActions = processCapsLock(act, !_td->state.isDown, FALSE, TRUE); break; case KM_CORE_IT_INVALIDATE_CONTEXT: - continueProcessingActions = processInvalidateContext(_td->app, _td->lpActiveKeyboard->lpCoreKeyboardState); + continueProcessingActions = processInvalidateContext(_td->app); break; } if (!continueProcessingActions) { diff --git a/windows/src/engine/keyman64/keyman64.vcxproj b/windows/src/engine/keyman64/keyman64.vcxproj index f80c93089d..d75962c596 100644 --- a/windows/src/engine/keyman64/keyman64.vcxproj +++ b/windows/src/engine/keyman64/keyman64.vcxproj @@ -177,6 +177,7 @@ + @@ -276,6 +277,7 @@ + @@ -327,4 +329,4 @@ - + \ No newline at end of file diff --git a/windows/src/engine/keyman64/keyman64.vcxproj.filters b/windows/src/engine/keyman64/keyman64.vcxproj.filters index cd8aeefb32..06ba0e951e 100644 --- a/windows/src/engine/keyman64/keyman64.vcxproj.filters +++ b/windows/src/engine/keyman64/keyman64.vcxproj.filters @@ -38,6 +38,7 @@ + @@ -72,6 +73,7 @@ +