From 55c0c6fd08223719edbba32c36d75c41794e6932 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 16 Nov 2023 11:47:35 +0700 Subject: [PATCH 01/46] fix(developer): projects 2.0 internal path enumeration Restricts enumeration of files for the project to the project folder and the SourcePath folder. This prevents problems where a project may be in a folder with many subfolders which would take a long time to enumerate, and avoids confusion where there are source-type files in other folders. At the same time, sorts out forward slash vs backslash in paths. While forward slash works in many scenarios, there are several filename manipulation functions, such as ExpandFileName, which would build valid but non-optimal paths when forward slashes were encountered, which cascaded into files appearing to be different and presentation issues. --- common/windows/delphi/general/utildir.pas | 8 +++++- ...n.Developer.System.Project.ProjectFile.pas | 28 ++++++++----------- ...Developer.System.Project.ProjectLoader.pas | 5 ++-- ...loper.UI.Project.UfrmProjectSettings20.pas | 23 +++++++-------- 4 files changed, 34 insertions(+), 30 deletions(-) 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/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 From 38f9b29cc985319b9e5aee3bffb50c23a83fba50 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 16 Nov 2023 11:50:54 +0700 Subject: [PATCH 02/46] fix(developer): show relative path in Distribution tab For the Project view, the Distribution tab now shows file relative paths, which helps with organization. Have opted _not_ to show the relative paths in the other tabs, because that information is visible when the file details are expanded, and because those files should always be in SourcePath anyway. --- developer/src/tike/xml/project/distribution.xsl | 1 + developer/src/tike/xml/project/elements.xsl | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) 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 @@
From df43d63b18714dad46ef8105093e85dd4f02f6a1 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 16 Nov 2023 13:31:56 +0700 Subject: [PATCH 03/46] fix(android): drops full-page reload from picker interactions. Fixes #8868 --- .../keyman/engine/KeyboardPickerActivity.java | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) 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..caf488a1df 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 @@ -123,7 +123,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(); } @@ -263,18 +263,6 @@ public final class KeyboardPickerActivity extends BaseActivity { return; } - @Override - protected void onPause() { - super.onPause(); - - if (KMManager.InAppKeyboard != null) { - KMManager.InAppKeyboard.loadKeyboard(); - } - if (KMManager.SystemKeyboard != null) { - KMManager.SystemKeyboard.loadKeyboard(); - } - } - @Override public boolean onSupportNavigateUp() { onBackPressed(); @@ -335,7 +323,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 +332,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 +434,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); From 6cc35c15d498582a3277e16f1a30b2857f2c041d Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 16 Nov 2023 14:42:04 +0700 Subject: [PATCH 04/46] change(android): JS queue flattening - concats all pending calls when ready --- .../java/com/keyman/engine/KMKeyboard.java | 72 ++++++++++++------- .../java/com/keyman/engine/KMManager.java | 24 +++---- 2 files changed, 58 insertions(+), 38 deletions(-) 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 6471e87510..e1c2b82a29 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 @@ -68,6 +68,16 @@ import io.sentry.Breadcrumb; import io.sentry.Sentry; import io.sentry.SentryLevel; +class JSQueueEntry { + public String call; + public boolean pause; + + JSQueueEntry(String call, boolean pauseAfterCall) { + this.call = call; + this.pause = pauseAfterCall; + } +} + final class KMKeyboard extends WebView { private static final String TAG = "KMKeyboard"; private final Context context; @@ -80,7 +90,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; @@ -173,7 +183,7 @@ final class KMKeyboard extends WebView { } if (KMManager.isKeyboardLoaded(this.keyboardType) && !shouldIgnoreTextChange) { - this.loadJavascript(KMString.format("updateKMText('%s')", kmText)); + this.loadJavascript(KMString.format("updateKMText('%s')", kmText), false); result = true; } @@ -190,7 +200,7 @@ final class KMKeyboard extends WebView { updateText(icText.text.toString()); } } - this.loadJavascript(KMString.format("updateKMSelectionRange(%d,%d)", selStart, selEnd)); + this.loadJavascript(KMString.format("updateKMSelectionRange(%d,%d)", selStart, selEnd), false); result = true; return result; @@ -301,8 +311,8 @@ final class KMKeyboard extends WebView { setBackgroundColor(0); } - public void loadJavascript(String func) { - this.javascriptAfterLoad.add(func); + public void loadJavascript(String func, boolean pauseAfterCall) { + this.javascriptAfterLoad.add(new JSQueueEntry(func, pauseAfterCall)); if((keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP && KMManager.InAppKeyboardWebViewClient.getKeyboardLoaded()) || (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM && KMManager.SystemKeyboardWebViewClient.getKeyboardLoaded())) { @@ -321,16 +331,26 @@ 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) { + JSQueueEntry entry = javascriptAfterLoad.remove(0); + allCalls.append(entry.call); + allCalls.append(";"); + + if(entry.pause) { + break; } } + + loadUrl("javascript:" + allCalls.toString()); + + if(javascriptAfterLoad.size() > 0 && keyboardSet) { + callJavascriptAfterLoad(); + } } }, 1); } @@ -339,18 +359,18 @@ final class KMKeyboard extends WebView { public void hideKeyboard() { String jsString = "hideKeyboard()"; - loadJavascript(jsString); + loadJavascript(jsString, false); } public void showKeyboard() { String jsString = "showKeyboard()"; - loadJavascript(jsString); + loadJavascript(jsString, false); } public void executeHardwareKeystroke(int code, int shift, int lstates, int eventModifiers) { String jsFormat = "executeHardwareKeystroke(%d,%d, %d, %d)"; String jsString = KMString.format(jsFormat, code, shift, lstates, eventModifiers); - loadJavascript(jsString); + loadJavascript(jsString, false); } @SuppressLint("ClickableViewAccessibility") @@ -378,7 +398,7 @@ final class KMKeyboard extends WebView { // Ensure window is loaded for javascript functions loadJavascript(KMString.format( "window.onload = function(){ setOskWidth(\"%d\");"+ - "setOskHeight(\"0\"); };", kbWidth)); + "setOskHeight(\"0\"); };", kbWidth), false); if (this.getShouldShowHelpBubble()) { this.showHelpBubbleAfterDelay(2000); } @@ -400,9 +420,9 @@ final class KMKeyboard extends WebView { int bannerHeight = KMManager.getBannerHeight(context); int oskHeight = KMManager.getKeyboardHeight(context); - loadJavascript(KMString.format("setBannerHeight(%d)", bannerHeight)); - loadJavascript(KMString.format("setOskWidth(%d)", newConfig.screenWidthDp)); - loadJavascript(KMString.format("setOskHeight(%d)", oskHeight)); + loadJavascript(KMString.format("setBannerHeight(%d)", bannerHeight), false); + loadJavascript(KMString.format("setOskWidth(%d)", newConfig.screenWidthDp), false); + loadJavascript(KMString.format("setOskHeight(%d)", oskHeight), false); this.dismissHelpBubble(); @@ -633,7 +653,7 @@ final class KMKeyboard extends WebView { } String jsString = KMString.format("setKeymanLanguage(%s)", reg.toString()); - loadJavascript(jsString); + loadJavascript(jsString, false); this.packageID = packageID; this.keyboardID = keyboardID; @@ -907,13 +927,13 @@ final class KMKeyboard extends WebView { suggestionJSON = null; suggestionMenuWindow = null; String jsString = "popupVisible(0)"; - loadJavascript(jsString); + loadJavascript(jsString, false); } }); suggestionMenuWindow.showAtLocation(KMKeyboard.this, Gravity.TOP | Gravity.LEFT, posX , posY); String jsString = "popupVisible(1)"; - loadJavascript(jsString); + loadJavascript(jsString, false); return; } @@ -995,7 +1015,7 @@ final class KMKeyboard extends WebView { textWrapper.put("text", hintText); // signalHelpBubbleDismissal - defined in android-host.js, gives a helpBubbleDismissed signal. - loadJavascript("keyman.showGlobeHint(" + textWrapper.toString() + ".text, signalHelpBubbleDismissal);"); + loadJavascript("keyman.showGlobeHint(" + textWrapper.toString() + ".text, signalHelpBubbleDismissal);", false); } catch(JSONException e) { KMLog.LogException(TAG, "", e); return; @@ -1023,7 +1043,7 @@ final class KMKeyboard extends WebView { } protected void dismissHelpBubble() { - loadJavascript("keyman.hideGlobeHint();"); + loadJavascript("keyman.hideGlobeHint();", false); } public static void addOnKeyboardEventListener(OnKeyboardEventListener listener) { @@ -1055,7 +1075,7 @@ final class KMKeyboard extends WebView { public void setSpacebarText(KMManager.SpacebarText mode) { String jsString = KMString.format("setSpacebarText('%s')", mode.toString()); - loadJavascript(jsString); + loadJavascript(jsString, false); } /* Implement handleTouchEvent to catch long press gesture without using Android system default time 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 cbcb28053a..2e6334c935 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 @@ -1333,12 +1333,12 @@ public final class KMManager { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP) && !InAppKeyboard.shouldIgnoreTextChange() && modelFileExists) { params = getKeyboardLayoutParams(); InAppKeyboard.setLayoutParams(params); - InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); + InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect), false); } if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && !SystemKeyboard.shouldIgnoreTextChange() && modelFileExists) { params = getKeyboardLayoutParams(); SystemKeyboard.setLayoutParams(params); - SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); + SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect), false); } return true; } @@ -1351,11 +1351,11 @@ public final class KMManager { String url = KMString.format("deregisterModel('%s')", modelID); if (InAppKeyboard != null) { - InAppKeyboard.loadJavascript(url); + InAppKeyboard.loadJavascript(url, false); } if (SystemKeyboard != null) { - SystemKeyboard.loadJavascript(url); + SystemKeyboard.loadJavascript(url, false); } return true; } @@ -1373,11 +1373,11 @@ public final class KMManager { public static boolean setBannerOptions(boolean mayPredict) { String url = KMString.format("setBannerOptions(%s)", mayPredict); if (InAppKeyboard != null) { - InAppKeyboard.loadJavascript(url); + InAppKeyboard.loadJavascript(url, false); } if (SystemKeyboard != null) { - SystemKeyboard.loadJavascript(url); + SystemKeyboard.loadJavascript(url, false); } return true; } @@ -1868,12 +1868,12 @@ public final class KMManager { public static void applyKeyboardHeight(Context context, int height) { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) { - InAppKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height)); + InAppKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height), false); RelativeLayout.LayoutParams params = getKeyboardLayoutParams(); InAppKeyboard.setLayoutParams(params); } if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)) { - SystemKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height)); + SystemKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height), false); RelativeLayout.LayoutParams params = getKeyboardLayoutParams(); SystemKeyboard.setLayoutParams(params); } @@ -1944,11 +1944,11 @@ public final class KMManager { public static void setNumericLayer(KeyboardType kbType) { if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP) && !InAppKeyboard.shouldIgnoreTextChange()) { - InAppKeyboard.loadJavascript("setNumericLayer()"); + InAppKeyboard.loadJavascript("setNumericLayer()", false); } } else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && !SystemKeyboard.shouldIgnoreTextChange()) { - SystemKeyboard.loadJavascript("setNumericLayer()"); + SystemKeyboard.loadJavascript("setNumericLayer()", false); } } } @@ -1987,11 +1987,11 @@ public final class KMManager { public static void resetContext(KeyboardType kbType) { if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) { - InAppKeyboard.loadJavascript("resetContext()"); + InAppKeyboard.loadJavascript("resetContext()", true); } } else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)) { - SystemKeyboard.loadJavascript("resetContext()"); + SystemKeyboard.loadJavascript("resetContext()", true); } } } From a29599d2727f62b19491e9d9cd3371c0818fbc7f Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 16 Nov 2023 14:42:30 +0700 Subject: [PATCH 05/46] change(android): eliminates a redundant setLayoutParams --- .../KMEA/app/src/main/java/com/keyman/engine/KMManager.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 2e6334c935..5d47ab8ce4 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 @@ -1332,12 +1332,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), false); } 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), false); } return true; From 33c69969b5a9d976b7b4ecef60536e6a4c0b7ec8 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 16 Nov 2023 15:43:53 +0700 Subject: [PATCH 06/46] change(android): drops redundant setLayoutParams call --- android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java | 2 -- 1 file changed, 2 deletions(-) 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 5d47ab8ce4..3f9a502918 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 @@ -747,12 +747,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); } } From c5b3bafcc7abec1677c0a84c59cdee0957d50615 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 17 Nov 2023 09:54:40 +0700 Subject: [PATCH 07/46] change(android): host-page init now retrieves current stub before KMW init --- .../KMEA/app/src/main/assets/android-host.js | 4 + .../keyman/engine/KMKeyboardJSHandler.java | 17 ++++ .../java/com/keyman/engine/data/Keyboard.java | 87 +++++++++++++++++++ web/src/engine/main/src/keyboardInterface.ts | 18 ++-- 4 files changed, 120 insertions(+), 6 deletions(-) diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index afa2825e33..543e82dca4 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -31,6 +31,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/', 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/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/web/src/engine/main/src/keyboardInterface.ts b/web/src/engine/main/src/keyboardInterface.ts index 305373aea3..971bcdcab5 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.hasFinalized) { - 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); } From 7c9061e869be3b52c19f1d13d82d29189b04699a Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 17 Nov 2023 10:31:27 +0700 Subject: [PATCH 08/46] chore(android): remove fixed TODO --- .../KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 e1c2b82a29..2298130253 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 @@ -243,10 +243,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#[^-]+)-.*$"; From eebb88699d31fc7e73a84a65b0053bfa092b0a5b Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 17 Nov 2023 18:08:07 +0000 Subject: [PATCH 09/46] feat(developer): refactor a little bit to give a postValidate() phase - postValidate() is called after all other compilation happens - also make SectionCompiler actually abstract - test fixes to support this For: #9446 --- .../src/kmc-ldml/src/compiler/compiler.ts | 7 ++++ .../kmc-ldml/src/compiler/section-compiler.ts | 36 +++++++++++++------ developer/src/kmc-ldml/src/compiler/tran.ts | 2 +- developer/src/kmc-ldml/test/helpers/index.ts | 19 ++++++---- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/developer/src/kmc-ldml/src/compiler/compiler.ts b/developer/src/kmc-ldml/src/compiler/compiler.ts index f5f27c0a39..95b1aabee6 100644 --- a/developer/src/kmc-ldml/src/compiler/compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/compiler.ts @@ -207,6 +207,13 @@ export class LdmlKeyboardCompiler { kmx.kmxplus[section.id] = sect as any; } + // give all sections a chance to 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/section-compiler.ts b/developer/src/kmc-ldml/src/compiler/section-compiler.ts index 8744059f7b..f200027786 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,34 @@ 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, 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/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index ea76da94b3..d21fe99498 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 @@ -184,7 +187,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 +200,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) { From 3a417ade794ac6b1275f17e35335bc4ddfd0bf93 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 17 Nov 2023 20:37:33 +0000 Subject: [PATCH 10/46] =?UTF-8?q?feat(common):=20PUA=20and=20illegal=20uni?= =?UTF-8?q?code=20analysis=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For: #9446 --- common/web/types/src/util/util.ts | 143 +++++++++++++++++++ common/web/types/test/util/test-unescape.ts | 145 +++++++++++++++++++- 2 files changed, 287 insertions(+), 1 deletion(-) diff --git a/common/web/types/src/util/util.ts b/common/web/types/src/util/util.ts index ce5ce9ce46..a1c3194079 100644 --- a/common/web/types/src/util/util.ts +++ b/common/web/types/src/util/util.ts @@ -101,3 +101,146 @@ toOneChar(value: string) : number { } return value.codePointAt(0); } + +export function describeCodepoint(ch : number) : string { + const s = isValidUnicode(ch) ? String.fromCodePoint(ch) : "INVALID"; + return `"${s}" (U+${Number(ch).toString(16)})`; +} + +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; + +/** + * @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) <= 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 >= 0xE000 && ch <= 0xF8FF) || + (ch >= 0xF0000 && ch <= 0xFFFFD) || + (ch >= 0x100000 && ch <= 0x10FFFD)); +} + +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`); + }); + }); +}); From c42615420d505d2261d01e395b1afc07bc4301ae Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Fri, 17 Nov 2023 22:41:58 +0000 Subject: [PATCH 11/46] =?UTF-8?q?feat(developer):=20err/hint=20on=20illega?= =?UTF-8?q?l/pua=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - also found that kmc's validate() should have been async but wasn't, a little bit of churn because of this - unassigned not implemented yet, but tests in place for it. - change SectionCompiler.postValidate to only be called during the validate() run of compilation. This way messages will only show once. For: #9446 --- common/web/types/src/kmx/kmx-plus.ts | 11 +++- common/web/types/src/util/util.ts | 10 +++- .../src/kmc-ldml/src/compiler/compiler.ts | 14 +++-- .../kmc-ldml/src/compiler/empty-compiler.ts | 42 ++++++++++++- .../src/kmc-ldml/src/compiler/messages.ts | 15 ++++- .../kmc-ldml/src/compiler/section-compiler.ts | 3 +- .../test/fixtures/sections/strs/hint-pua.xml | 58 ++++++++++++++++++ .../sections/strs/invalid-illegal.xml | 59 +++++++++++++++++++ .../sections/strs/warn-unassigned.xml | 58 ++++++++++++++++++ developer/src/kmc-ldml/test/helpers/index.ts | 25 +++++--- .../src/kmc-ldml/test/test-compiler-e2e.ts | 56 ++++++++++++++++++ .../kmc-ldml/test/test-keymanweb-compiler.ts | 2 +- .../test/test-visual-keyboard-compiler-e2e.ts | 4 +- 13 files changed, 334 insertions(+), 23 deletions(-) create mode 100644 developer/src/kmc-ldml/test/fixtures/sections/strs/hint-pua.xml create mode 100644 developer/src/kmc-ldml/test/fixtures/sections/strs/invalid-illegal.xml create mode 100644 developer/src/kmc-ldml/test/fixtures/sections/strs/warn-unassigned.xml 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 a1c3194079..af8539b9a4 100644 --- a/common/web/types/src/util/util.ts +++ b/common/web/types/src/util/util.ts @@ -103,7 +103,14 @@ toOneChar(value: string) : number { } export function describeCodepoint(ch : number) : string { - const s = isValidUnicode(ch) ? String.fromCodePoint(ch) : "INVALID"; + let s; + if (isPUA(ch)) { + s = "PUA"; + } else if (isValidUnicode(ch)) { + s = String.fromCodePoint(ch); + } else { + s = "INVALID"; + } return `"${s}" (U+${Number(ch).toString(16)})`; } @@ -214,7 +221,6 @@ function getProblem(ch : number) : BadStringType { return null; } } - export class BadStringAnalyzer { /** add a string for analysis */ public add(s : string) { diff --git a/developer/src/kmc-ldml/src/compiler/compiler.ts b/developer/src/kmc-ldml/src/compiler/compiler.ts index 95b1aabee6..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; @@ -208,9 +208,11 @@ export class LdmlKeyboardCompiler { } // give all sections a chance to postValidate - for(let section of sections) { - if(!section.postValidate(kmx.kmxplus[section.id])) { - passed = false; + if (postValidate) { + for(let section of sections) { + if(!section.postValidate(kmx.kmxplus[section.id])) { + passed = false; + } } } 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..d26073fd35 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 contained ${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 contained ${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 contained ${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 f200027786..f2b14e56a3 100644 --- a/developer/src/kmc-ldml/src/compiler/section-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/section-compiler.ts @@ -31,7 +31,8 @@ export abstract class SectionCompiler { public abstract compile(sections: KMXPlus.DependencySections): KMXPlus.Section; /** - * This is called after all other compile phases have completed, and provides an + * 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 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 d21fe99498..8f6d73ef43 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -118,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 @@ -139,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'); diff --git a/developer/src/kmc-ldml/test/test-compiler-e2e.ts b/developer/src/kmc-ldml/test/test-compiler-e2e.ts index 343f6cc7ac..f13c2594bd 100644 --- a/developer/src/kmc-ldml/test/test-compiler-e2e.ts +++ b/developer/src/kmc-ldml/test/test-compiler-e2e.ts @@ -4,11 +4,13 @@ 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 it('should-build-fixtures', async function() { + compilerTestCallbacks.messages = []; // Let's build basic.xml // It should match basic.kmx (built from basic.txt) @@ -32,33 +34,87 @@ describe('compiler-tests', function() { }); it('should handle non existent files', () => { + compilerTestCallbacks.messages = []; const filename = 'DOES_NOT_EXIST.xml'; const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); const source = k.load(filename); assert.notOk(source, `Trying to load(${filename})`); }); it('should handle unparseable files', () => { + compilerTestCallbacks.messages = []; const filename = makePathToFixture('basic-kvk.txt'); // not an .xml file const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); const source = k.load(filename); assert.notOk(source, `Trying to load(${filename})`); }); it('should handle not-valid files', () => { + compilerTestCallbacks.messages = []; const filename = makePathToFixture('test-fr.xml'); // not a keyboard .xml file const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); const source = k.load(filename); assert.notOk(source, `Trying to load(${filename})`); }); it('should handle non existent test files', () => { + compilerTestCallbacks.messages = []; const filename = 'DOES_NOT_EXIST.xml'; const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); const source = k.loadTestData(filename); assert.notOk(source, `Trying to loadTestData(${filename})`); }); it('should handle unparseable test files', () => { + compilerTestCallbacks.messages = []; const filename = makePathToFixture('basic-kvk.txt'); // not an .xml file const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); const source = k.load(filename); assert.notOk(source, `Trying to loadTestData(${filename})`); }); + it('should fail on illegal chars', async function() { + compilerTestCallbacks.messages = []; + 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() { + compilerTestCallbacks.messages = []; + 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 + compilerTestCallbacks.messages = []; + 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 +}); From 57b52d86a3d362fbdd2b7ab3288ca6d35252ff0f Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 22 Nov 2023 09:43:55 +0700 Subject: [PATCH 12/46] change(android): drops JS-queue 'pause' --- .../java/com/keyman/engine/KMKeyboard.java | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) 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 e1c2b82a29..aee5069220 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 @@ -70,11 +70,9 @@ import io.sentry.SentryLevel; class JSQueueEntry { public String call; - public boolean pause; - JSQueueEntry(String call, boolean pauseAfterCall) { + JSQueueEntry(String call) { this.call = call; - this.pause = pauseAfterCall; } } @@ -183,7 +181,7 @@ final class KMKeyboard extends WebView { } if (KMManager.isKeyboardLoaded(this.keyboardType) && !shouldIgnoreTextChange) { - this.loadJavascript(KMString.format("updateKMText('%s')", kmText), false); + this.loadJavascript(KMString.format("updateKMText('%s')", kmText)); result = true; } @@ -200,7 +198,7 @@ final class KMKeyboard extends WebView { updateText(icText.text.toString()); } } - this.loadJavascript(KMString.format("updateKMSelectionRange(%d,%d)", selStart, selEnd), false); + this.loadJavascript(KMString.format("updateKMSelectionRange(%d,%d)", selStart, selEnd)); result = true; return result; @@ -311,8 +309,8 @@ final class KMKeyboard extends WebView { setBackgroundColor(0); } - public void loadJavascript(String func, boolean pauseAfterCall) { - this.javascriptAfterLoad.add(new JSQueueEntry(func, pauseAfterCall)); + public void loadJavascript(String func) { + this.javascriptAfterLoad.add(new JSQueueEntry(func)); if((keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP && KMManager.InAppKeyboardWebViewClient.getKeyboardLoaded()) || (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM && KMManager.SystemKeyboardWebViewClient.getKeyboardLoaded())) { @@ -340,10 +338,6 @@ final class KMKeyboard extends WebView { JSQueueEntry entry = javascriptAfterLoad.remove(0); allCalls.append(entry.call); allCalls.append(";"); - - if(entry.pause) { - break; - } } loadUrl("javascript:" + allCalls.toString()); @@ -359,18 +353,18 @@ final class KMKeyboard extends WebView { public void hideKeyboard() { String jsString = "hideKeyboard()"; - loadJavascript(jsString, false); + loadJavascript(jsString); } public void showKeyboard() { String jsString = "showKeyboard()"; - loadJavascript(jsString, false); + loadJavascript(jsString); } public void executeHardwareKeystroke(int code, int shift, int lstates, int eventModifiers) { String jsFormat = "executeHardwareKeystroke(%d,%d, %d, %d)"; String jsString = KMString.format(jsFormat, code, shift, lstates, eventModifiers); - loadJavascript(jsString, false); + loadJavascript(jsString); } @SuppressLint("ClickableViewAccessibility") @@ -398,7 +392,7 @@ final class KMKeyboard extends WebView { // Ensure window is loaded for javascript functions loadJavascript(KMString.format( "window.onload = function(){ setOskWidth(\"%d\");"+ - "setOskHeight(\"0\"); };", kbWidth), false); + "setOskHeight(\"0\"); };", kbWidth)); if (this.getShouldShowHelpBubble()) { this.showHelpBubbleAfterDelay(2000); } @@ -420,9 +414,9 @@ final class KMKeyboard extends WebView { int bannerHeight = KMManager.getBannerHeight(context); int oskHeight = KMManager.getKeyboardHeight(context); - loadJavascript(KMString.format("setBannerHeight(%d)", bannerHeight), false); - loadJavascript(KMString.format("setOskWidth(%d)", newConfig.screenWidthDp), false); - loadJavascript(KMString.format("setOskHeight(%d)", oskHeight), false); + loadJavascript(KMString.format("setBannerHeight(%d)", bannerHeight)); + loadJavascript(KMString.format("setOskWidth(%d)", newConfig.screenWidthDp)); + loadJavascript(KMString.format("setOskHeight(%d)", oskHeight)); this.dismissHelpBubble(); @@ -653,7 +647,7 @@ final class KMKeyboard extends WebView { } String jsString = KMString.format("setKeymanLanguage(%s)", reg.toString()); - loadJavascript(jsString, false); + loadJavascript(jsString); this.packageID = packageID; this.keyboardID = keyboardID; @@ -927,13 +921,13 @@ final class KMKeyboard extends WebView { suggestionJSON = null; suggestionMenuWindow = null; String jsString = "popupVisible(0)"; - loadJavascript(jsString, false); + loadJavascript(jsString); } }); suggestionMenuWindow.showAtLocation(KMKeyboard.this, Gravity.TOP | Gravity.LEFT, posX , posY); String jsString = "popupVisible(1)"; - loadJavascript(jsString, false); + loadJavascript(jsString); return; } @@ -1015,7 +1009,7 @@ final class KMKeyboard extends WebView { textWrapper.put("text", hintText); // signalHelpBubbleDismissal - defined in android-host.js, gives a helpBubbleDismissed signal. - loadJavascript("keyman.showGlobeHint(" + textWrapper.toString() + ".text, signalHelpBubbleDismissal);", false); + loadJavascript("keyman.showGlobeHint(" + textWrapper.toString() + ".text, signalHelpBubbleDismissal);"); } catch(JSONException e) { KMLog.LogException(TAG, "", e); return; @@ -1043,7 +1037,7 @@ final class KMKeyboard extends WebView { } protected void dismissHelpBubble() { - loadJavascript("keyman.hideGlobeHint();", false); + loadJavascript("keyman.hideGlobeHint();"); } public static void addOnKeyboardEventListener(OnKeyboardEventListener listener) { @@ -1075,7 +1069,7 @@ final class KMKeyboard extends WebView { public void setSpacebarText(KMManager.SpacebarText mode) { String jsString = KMString.format("setSpacebarText('%s')", mode.toString()); - loadJavascript(jsString, false); + loadJavascript(jsString); } /* Implement handleTouchEvent to catch long press gesture without using Android system default time From 701bdb8e9de980239d6778e75d85076b4a014048 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 22 Nov 2023 09:45:27 +0700 Subject: [PATCH 13/46] fix(android): forgot the 'KMManager' entries --- .../java/com/keyman/engine/KMManager.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) 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 5d47ab8ce4..a5bc952b5a 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 @@ -1334,13 +1334,13 @@ public final class KMManager { params = getKeyboardLayoutParams(); // Do NOT re-layout here; it'll be triggered once the banner loads. - InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect), false); + InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); } if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && !SystemKeyboard.shouldIgnoreTextChange() && modelFileExists) { params = getKeyboardLayoutParams(); // Do NOT re-layout here; it'll be triggered once the banner loads. - SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect), false); + SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect)); } return true; } @@ -1353,11 +1353,11 @@ public final class KMManager { String url = KMString.format("deregisterModel('%s')", modelID); if (InAppKeyboard != null) { - InAppKeyboard.loadJavascript(url, false); + InAppKeyboard.loadJavascript(url); } if (SystemKeyboard != null) { - SystemKeyboard.loadJavascript(url, false); + SystemKeyboard.loadJavascript(url); } return true; } @@ -1375,11 +1375,11 @@ public final class KMManager { public static boolean setBannerOptions(boolean mayPredict) { String url = KMString.format("setBannerOptions(%s)", mayPredict); if (InAppKeyboard != null) { - InAppKeyboard.loadJavascript(url, false); + InAppKeyboard.loadJavascript(url); } if (SystemKeyboard != null) { - SystemKeyboard.loadJavascript(url, false); + SystemKeyboard.loadJavascript(url); } return true; } @@ -1870,12 +1870,12 @@ public final class KMManager { public static void applyKeyboardHeight(Context context, int height) { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) { - InAppKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height), false); + InAppKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height)); RelativeLayout.LayoutParams params = getKeyboardLayoutParams(); InAppKeyboard.setLayoutParams(params); } if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)) { - SystemKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height), false); + SystemKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height)); RelativeLayout.LayoutParams params = getKeyboardLayoutParams(); SystemKeyboard.setLayoutParams(params); } @@ -1946,11 +1946,11 @@ public final class KMManager { public static void setNumericLayer(KeyboardType kbType) { if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP) && !InAppKeyboard.shouldIgnoreTextChange()) { - InAppKeyboard.loadJavascript("setNumericLayer()", false); + InAppKeyboard.loadJavascript("setNumericLayer()"); } } else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && !SystemKeyboard.shouldIgnoreTextChange()) { - SystemKeyboard.loadJavascript("setNumericLayer()", false); + SystemKeyboard.loadJavascript("setNumericLayer()"); } } } From 804cecf66d93a91fd74906594968e556e4d6ae25 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 22 Nov 2023 09:46:44 +0700 Subject: [PATCH 14/46] fix(android): ...and the resetContext calls, which used true instead of false --- .../KMEA/app/src/main/java/com/keyman/engine/KMManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a5bc952b5a..264bddba9b 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 @@ -1989,11 +1989,11 @@ public final class KMManager { public static void resetContext(KeyboardType kbType) { if (kbType == KeyboardType.KEYBOARD_TYPE_INAPP) { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) { - InAppKeyboard.loadJavascript("resetContext()", true); + InAppKeyboard.loadJavascript("resetContext()"); } } else if (kbType == KeyboardType.KEYBOARD_TYPE_SYSTEM) { if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)) { - SystemKeyboard.loadJavascript("resetContext()", true); + SystemKeyboard.loadJavascript("resetContext()"); } } } From fb347bba8dfa9418611fc09e100aedaf7f0798f1 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Wed, 22 Nov 2023 15:25:14 +0100 Subject: [PATCH 15/46] chore(linux): Update debian changelog (cherry picked from commit 97d7f3a11c121a45389cdfb2c2eeb565ae612ce7) --- linux/debian/changelog | 8 ++++++++ 1 file changed, 8 insertions(+) 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) From 5bfb3288d2514016b2dedf80b8e2e294247f1025 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Wed, 22 Nov 2023 16:59:02 -0600 Subject: [PATCH 16/46] =?UTF-8?q?feat(developer):=20ldml=20improve=20bad?= =?UTF-8?q?=20character=20code=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refactor output section For: #9446 Co-authored-by: Darcy Wong --- common/web/types/src/util/util.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/common/web/types/src/util/util.ts b/common/web/types/src/util/util.ts index af8539b9a4..863509ebb6 100644 --- a/common/web/types/src/util/util.ts +++ b/common/web/types/src/util/util.ts @@ -104,20 +104,21 @@ toOneChar(value: string) : number { export function describeCodepoint(ch : number) : string { let s; - if (isPUA(ch)) { - s = "PUA"; - } else if (isValidUnicode(ch)) { - s = String.fromCodePoint(ch); + const p = getProblem(ch); + if (p != null) { + // for example: 'PUA (U+E010)' + s = p; } else { - s = "INVALID"; + // for example: '"a" (U+61)' + s = `"${String.fromCodePoint(ch)}"`; } - return `"${s}" (U+${Number(ch).toString(16)})`; + return `${s} (U+${Number(ch).toString(16).toUpperCase()})`; } export enum BadStringType { - pua = 'pua', - unassigned = 'unassigned', - illegal = 'illegal', + pua = 'PUA', + unassigned = 'Unassigned', + illegal = 'Illegal', }; // Following from kmx_xstring.h / .cpp From 76d05cdd3238ce0e9653409ee93c63b15f97d7c5 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 23 Nov 2023 08:50:11 +0700 Subject: [PATCH 17/46] fix(web): memory/handler leak when swapping pred-text on and off repeatedly --- web/src/engine/osk/src/banner/bannerController.ts | 11 +++++++++-- web/src/engine/osk/src/views/oskView.ts | 3 +++ 2 files changed, 12 insertions(+), 2 deletions(-) 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 0399991fe0..ebe20db18f 100644 --- a/web/src/engine/osk/src/views/oskView.ts +++ b/web/src/engine/osk/src/views/oskView.ts @@ -690,6 +690,7 @@ export default abstract class OSKView extends EventEmitter implements private loadActiveKeyboard() { this.setBoxStyling(); + // Do not erase / 'shutdown' the banner-controller; we simply re-use its elements. if(this.vkbd) { this.vkbd.shutdown(); } @@ -1135,6 +1136,8 @@ export default abstract class OSKView extends EventEmitter implements this.kbdStyleSheetManager.unlinkAll(); this.uiStyleSheetManager.unlinkAll(); + + this.bannerController.shutdown(); } /** From 77544272746ac164d1715ee56e5c2a60625e3b8f Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 23 Nov 2023 10:13:26 +0700 Subject: [PATCH 18/46] fix(android): banner event loop on model change --- .../java/com/keyman/engine/KMKeyboardWebViewClient.java | 5 +---- .../src/text/prediction/languageProcessor.ts | 6 ++++-- 2 files changed, 5 insertions(+), 6 deletions(-) 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/common/web/input-processor/src/text/prediction/languageProcessor.ts b/common/web/input-processor/src/text/prediction/languageProcessor.ts index 58c6e42993..38ded131f2 100644 --- a/common/web/input-processor/src/text/prediction/languageProcessor.ts +++ b/common/web/input-processor/src/text/prediction/languageProcessor.ts @@ -127,8 +127,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; From a2c30b9438ab3383950b7ff9c0e54448652f452b Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 23 Nov 2023 11:33:26 +0700 Subject: [PATCH 19/46] change(android): drops the 'change=' fragment --- android/KMEA/app/src/main/assets/android-host.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 7930c76f11..7b20ba8266 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -102,7 +102,7 @@ function setBannerHeight(h) { if (keyman.osk) { keyman.osk.bannerView.activeBannerHeight = bannerHeight; - } + } } // Refresh KMW's OSK @@ -149,9 +149,7 @@ 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; - } + window.location.hash = 'refreshBannerHeight-'+fragmentToggle; } // Query KMW if a given keyboard uses chiral modifiers. From ad0ed37049e0f9b34a73fc5f31c962d09bea8b43 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 23 Nov 2023 11:52:37 +0700 Subject: [PATCH 20/46] chore(android): restores removed conditional --- android/KMEA/app/src/main/assets/android-host.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 7b20ba8266..df1f8551df 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -149,7 +149,9 @@ function onStateChange(change) { keyman.refreshOskLayout(); fragmentToggle = (fragmentToggle + 1) % 100; - window.location.hash = 'refreshBannerHeight-'+fragmentToggle; + if(change != 'configured') { + window.location.hash = 'refreshBannerHeight-'+fragmentToggle; + } } // Query KMW if a given keyboard uses chiral modifiers. From e983818bda9a2bcfc8758d70ccd0c7b7aaf5285c Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 23 Nov 2023 15:13:56 +1000 Subject: [PATCH 21/46] feat(developer): warn on usage of virtual keys in rule output Fixes #10059. Use of the unsupported and undocumented virtual key output, that doesn't work in recent Keyman versions, at all, now results in a build warning. Only a warning, because it did kinda work in old versions of Keyman. --- .../src/common/include/kmn_compiler_errors.h | 2 ++ .../src/compiler/kmn-compiler-messages.ts | 2 ++ .../warn_virtual_key_in_output.kmn | 9 ++++++++ developer/src/kmc-kmn/test/test-messages.ts | 7 ++++++ developer/src/kmcmplib/src/CompMsg.cpp | 1 + developer/src/kmcmplib/src/Compiler.cpp | 22 +++++++++++++++++++ 6 files changed, 43 insertions(+) create mode 100644 developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_virtual_key_in_output.kmn 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/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 From b3b2825168b66a400960533059a216d361d8f8db Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 23 Nov 2023 16:45:12 +1000 Subject: [PATCH 22/46] fix(developer): path separator for kmc-package Fixes #10027. --- developer/src/kmc-package/src/compiler/kmp-compiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 4480a0541274827db4536bf68766f733b8616215 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:58:41 +1000 Subject: [PATCH 23/46] chore(windows): removed cached context windows engine Removed the cached context from the windows engine --- core/src/kmx/kmx_processor.cpp | 2 +- windows/src/desktop/kmshell/kmshell.res | Bin 7036 -> 7036 bytes windows/src/engine/keyman32/appcontext.cpp | 166 ++++++++++++ windows/src/engine/keyman32/appcontext.h | 108 ++++++++ windows/src/engine/keyman32/appint/aiTIP.cpp | 74 +----- windows/src/engine/keyman32/appint/aiTIP.h | 21 +- .../keyman32/appint/aiWin2000Unicode.cpp | 85 ++----- .../engine/keyman32/appint/aiWin2000Unicode.h | 18 +- windows/src/engine/keyman32/appint/appint.cpp | 236 ------------------ windows/src/engine/keyman32/appint/appint.h | 128 +--------- windows/src/engine/keyman32/keyman32.vcxproj | 4 +- .../engine/keyman32/keyman32.vcxproj.filters | 6 + windows/src/engine/keyman32/keymanengine.h | 2 +- windows/src/engine/keyman32/kmprocess.cpp | 57 +++-- .../src/engine/keyman32/kmprocessactions.cpp | 10 +- 15 files changed, 350 insertions(+), 567 deletions(-) create mode 100644 windows/src/engine/keyman32/appcontext.cpp create mode 100644 windows/src/engine/keyman32/appcontext.h 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/windows/src/desktop/kmshell/kmshell.res b/windows/src/desktop/kmshell/kmshell.res index cfcbd309f137d6a5f5221aba48a1c759c0d56682..0623d03fd306957fced5810b2839c7db7feb10a4 100644 GIT binary patch delta 15 Wcmexk_Qz~O35($$QKgL)EYbiu+6EZ_ delta 15 Xcmexk_Qz~O3Cr{S4;406ut);{LY)U( 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..8c6c99d960 --- /dev/null +++ b/windows/src/engine/keyman32/appcontext.h @@ -0,0 +1,108 @@ +#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: + History: +*/ +// AppContext is only kept here to support calldll with interface for the 3rdparty apps that worked with +// KMX formated Context. It is also used in one place for debug logs. + +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..a2fbda4d86 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,28 +47,14 @@ 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); virtual BOOL IsUnicode(); /* Context functions */ - - virtual void ReadContext(); +// TODO: #10052 Add doxy comments + 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..43fa723415 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,23 @@ BOOL AIWin2000Unicode::IsUnicode() /* Context functions */ -void AIWin2000Unicode::ReadContext() -{ +BOOL AIWin2000Unicode::ReadContext(PWSTR buf) { + UNREFERENCED_PARAMETER(buf); + return FALSE; } -void AIWin2000Unicode::AddContext(WCHAR ch) //I2436 +BOOL AIWin2000Unicode::ResetContext() { - context->Add(ch); -} - -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) { + return FALSE; + } + if (!_td->lpActiveKeyboard || !_td->lpActiveKeyboard->lpCoreKeyboardState) { + SendDebugMessageFormat(0, sdmAIDefault, 0, "ResetContext: no active keyboard state"); + return FALSE; + } + km_core_state_context_clear(_td->lpActiveKeyboard->lpCoreKeyboardState); + return TRUE; } BYTE SavedKbdState[256]; @@ -126,40 +109,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..40965fa063 100644 --- a/windows/src/engine/keyman32/appint/aiWin2000Unicode.h +++ b/windows/src/engine/keyman32/appint/aiWin2000Unicode.h @@ -32,16 +32,10 @@ private: BOOL PostKeys(); - -protected: - AppContext *context; - public: - AIWin2000Unicode(); + AIWin2000Unicode(); ~AIWin2000Unicode(); - virtual BOOL QueueAction(int ItemType, DWORD dwData); - /* Information functions */ virtual BOOL CanHandleWindow(HWND ahwnd); @@ -51,13 +45,9 @@ public: /* Context functions */ - virtual void ReadContext(); - 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); - + virtual BOOL ReadContext(PWSTR buf); + virtual BOOL ResetContext(); + /* 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..a735d18618 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: @@ -181,11 +84,8 @@ public: /* Context functions */ - virtual void ReadContext() = 0; - virtual void ResetContext() = 0; - virtual void AddContext(WCHAR ch) = 0; //I2436 - virtual WCHAR *ContextBuf(int n) = 0; - virtual WCHAR *ContextBufMax(int n) = 0; + virtual BOOL ReadContext(PWSTR buf) = 0; + virtual BOOL ResetContext() = 0; /* Queue and sending functions */ @@ -193,30 +93,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..f2cd9df277 100644 --- a/windows/src/engine/keyman32/kmprocess.cpp +++ b/windows/src/engine/keyman32/kmprocess.cpp @@ -70,23 +70,30 @@ 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)) { + km_core_context_items_dispose(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 +105,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, "ProcessEvent SetContext if needed Result:False %d ", FALSE); + } } - 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 +147,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 +218,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) { From fb7c19fee4187483762fcedd933a41cc068b238b Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Fri, 17 Nov 2023 17:47:02 +0100 Subject: [PATCH 24/46] chore(core): Add test keyboard for text selection tests Part of #9073. --- .../HISTORY.md | 6 + .../LICENSE.md | 21 + .../README.md | 31 ++ .../source/readme.htm | 24 + .../text_selection_tests_keyboard_9073.ico | Bin 0 -> 1150 bytes ...on_tests_keyboard_9073.keyman-touch-layout | 527 ++++++++++++++++++ .../text_selection_tests_keyboard_9073.kmn | 24 + .../text_selection_tests_keyboard_9073.kps | 67 +++ .../text_selection_tests_keyboard_9073.kvks | 110 ++++ .../source/welcome.htm | 26 + ...election_tests_keyboard_9073.keyboard_info | 7 + .../text_selection_tests_keyboard_9073.kpj | 110 ++++ web/src/test/manual/web/index.html | 1 + .../web/text_selection_tests_9073/index.html | 78 +++ .../text_selection_tests_keyboard_9073.js | 1 + 15 files changed, 1033 insertions(+) create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/HISTORY.md create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/LICENSE.md create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/README.md create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/source/readme.htm create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.ico create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.keyman-touch-layout create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kmn create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kps create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kvks create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/source/welcome.htm create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/text_selection_tests_keyboard_9073.keyboard_info create mode 100644 common/test/keyboards/text_selection_tests_keyboard_9073/text_selection_tests_keyboard_9073.kpj create mode 100644 web/src/test/manual/web/text_selection_tests_9073/index.html create mode 100644 web/src/test/manual/web/text_selection_tests_9073/text_selection_tests_keyboard_9073.js 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 0000000000000000000000000000000000000000..6a5271df0cfc45e0a53596020c181fa4decd151e GIT binary patch literal 1150 zcmZQzU<5(|0R;vS$Y5b$5ChU0Kr8^n3P8*VCV>o~96C5~=orID$1{Lo;OJPy;Na+t zg4eW0Gl1kGfN}@cb-=?ZZx%?*87_uR4J=I2<<@}gitHd;KgjO9d}kClq1yqo8$=_^ zf$WX|+C2m47i2$y)L`@ffmuZ;`ayQj0NRagCb}KScB9Kh" + }, + { + "id": "K_SLASH", + "text": "?" + }, + { + "width": 10, + "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*" + } + ] + } + ] + } + ] + } +} \ 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..4d72f81ded --- /dev/null +++ b/common/test/keyboards/text_selection_tests_keyboard_9073/source/text_selection_tests_keyboard_9073.kmn @@ -0,0 +1,24 @@ +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) + +'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..3a80bf4be5 --- /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_af1590e09d357f1c16c9a1fe9991a9d4 + 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_0993fe0cb7835cdfb2a101ceccc03e85 + text_selection_tests_keyboard_9073.ico + source\text_selection_tests_keyboard_9073.ico + + .ico + id_af1590e09d357f1c16c9a1fe9991a9d4 + + + id_aff9466042ad8bb0edf57fea7134c373 + text_selection_tests_keyboard_9073.kmx + source\..\build\text_selection_tests_keyboard_9073.kmx + + .kmx + id_ba932837e6a67a86abc409a393242255 + + + id_c463c12f68ab14a1f91927146a1942b8 + text_selection_tests_keyboard_9073.js + source\..\build\text_selection_tests_keyboard_9073.js + + .js + id_ba932837e6a67a86abc409a393242255 + + + id_4a6d9dbdd46a11790e170f92ffe90a6b + 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/web/src/test/manual/web/index.html b/web/src/test/manual/web/index.html index 61ee99d2de..e98979f094 100644 --- a/web/src/test/manual/web/index.html +++ b/web/src/test/manual/web/index.html @@ -66,6 +66,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..7235b9c022 --- /dev/null +++ b/web/src/test/manual/web/text_selection_tests_9073/text_selection_tests_keyboard_9073.js @@ -0,0 +1 @@ +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(){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 e=Array.apply(null,Array(65)).map(String.prototype.valueOf,""),r=[],v,i,m=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt'];for(i=m.length-1;i>=0;i--)if((v=x[m[i]])||r.length)r=(v?v:e).slice().concat(r);return r})(this.KV.KLS);this.KDU=0;this.KH='';this.KM=0;this.KBVER="1.0";this.KMBM=0x10;this.KVKL={"tablet":{"displayUnderlying":false,"layer":[{"id":"default","row":[{"id":"1","key":[{"id":"K_1","text":"1"},{"id":"K_2","text":"2"},{"id":"K_3","text":"3"},{"id":"K_4","text":"4"},{"id":"K_5","text":"5"},{"id":"K_6","text":"6"},{"id":"K_7","text":"7"},{"id":"K_8","text":"8"},{"id":"K_9","text":"9"},{"id":"K_0","text":"0"},{"id":"K_HYPHEN","text":"-"},{"id":"K_EQUAL","text":"="},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"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"},{"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":"+"},{"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","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":[{"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","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="17.0.211.0";this.KVS=[];this.gs=function(t,e) {return this.g0(t,e);};this.gs=function(t,e) {return this.g0(t,e);};this.g0=function(t,e) {var k=KeymanWeb,r=0,m=0;if(k.KKM(e,16384,8)) {if(k.KFCM(3,t,['a',{t:'d',d:0},'b'])){r=m=1;k.KDC(3,t);k.KO(-1,t,"ok1");}else if(k.KFCM(2,t,['a','b'])){r=m=1;k.KDC(2,t);k.KO(-1,t,"fail1");}else if(k.KFCM(2,t,['a',{t:'d',d:0}])){r=m=1;k.KDC(2,t);k.KO(-1,t,"fail2");}else if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"foo");}}else if(k.KKM(e,16400,65)) {if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"Â");}}else if(k.KKM(e,16384,192)) {if(1){r=m=1;k.KDC(0,t);k.KDO(-1,t,0);}}else if(k.KKM(e,16384,65)) {if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"â");}}else if(k.KKM(e,16384,79)) {if(k.KFCM(1,t,[{t:'d',d:0}])){r=m=1;k.KDC(1,t);k.KO(-1,t,"ok3");}}return r;};} \ No newline at end of file From 2dbad870caa714a81eb63df175722c946e4ed303 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:01:26 +1000 Subject: [PATCH 25/46] chore(windows): add comments ReadContext --- windows/src/engine/keyman32/appcontext.h | 5 ++--- windows/src/engine/keyman32/appint/aiTIP.h | 6 +++++- windows/src/engine/keyman32/appint/aiWin2000Unicode.h | 2 +- windows/src/engine/keyman32/appint/appint.h | 5 ++++- windows/src/engine/keyman32/kmprocess.cpp | 6 +++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/windows/src/engine/keyman32/appcontext.h b/windows/src/engine/keyman32/appcontext.h index 8c6c99d960..d0859262df 100644 --- a/windows/src/engine/keyman32/appcontext.h +++ b/windows/src/engine/keyman32/appcontext.h @@ -14,11 +14,10 @@ Bugs: Todo: - Notes: + 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: */ -// AppContext is only kept here to support calldll with interface for the 3rdparty apps that worked with -// KMX formated Context. It is also used in one place for debug logs. class AppContext { private: diff --git a/windows/src/engine/keyman32/appint/aiTIP.h b/windows/src/engine/keyman32/appint/aiTIP.h index a2fbda4d86..3b5e894134 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.h +++ b/windows/src/engine/keyman32/appint/aiTIP.h @@ -53,7 +53,11 @@ public: virtual BOOL IsUnicode(); /* Context functions */ -// TODO: #10052 Add doxy comments + + /** + * 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); /* Queue and sending functions */ diff --git a/windows/src/engine/keyman32/appint/aiWin2000Unicode.h b/windows/src/engine/keyman32/appint/aiWin2000Unicode.h index 40965fa063..70c5d658d0 100644 --- a/windows/src/engine/keyman32/appint/aiWin2000Unicode.h +++ b/windows/src/engine/keyman32/appint/aiWin2000Unicode.h @@ -34,7 +34,7 @@ private: public: AIWin2000Unicode(); - ~AIWin2000Unicode(); + ~AIWin2000Unicode(); /* Information functions */ diff --git a/windows/src/engine/keyman32/appint/appint.h b/windows/src/engine/keyman32/appint/appint.h index a735d18618..4524e3f949 100644 --- a/windows/src/engine/keyman32/appint/appint.h +++ b/windows/src/engine/keyman32/appint/appint.h @@ -83,7 +83,10 @@ public: virtual BOOL IsUnicode() = 0; /* Context functions */ - + /** + * 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 BOOL ResetContext() = 0; diff --git a/windows/src/engine/keyman32/kmprocess.cpp b/windows/src/engine/keyman32/kmprocess.cpp index f2cd9df277..1493285c0d 100644 --- a/windows/src/engine/keyman32/kmprocess.cpp +++ b/windows/src/engine/keyman32/kmprocess.cpp @@ -71,7 +71,7 @@ BOOL fOutputKeystroke; char *getcontext_debug() { - + PKEYMAN64THREADDATA _td = ThreadGlobals(); if (!_td || !_td->lpActiveKeyboard || !_td->lpActiveKeyboard->lpCoreKeyboardState){ return ""; @@ -79,7 +79,7 @@ char *getcontext_debug() { 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)) { km_core_context_items_dispose(citems); @@ -92,7 +92,7 @@ char *getcontext_debug() { return ""; } km_core_context_items_dispose(citems); - return Debug_UnicodeString(buf); + return Debug_UnicodeString(buf); } From ef8b9888c864039881e89c5088caa6179bfbe135 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 23 Nov 2023 17:19:04 +0100 Subject: [PATCH 26/46] chore(web): Update touch layout --- ...on_tests_keyboard_9073.keyman-touch-layout | 39 +++++++++++-------- .../text_selection_tests_keyboard_9073.kpj | 12 +++--- .../text_selection_tests_keyboard_9073.js | 2 +- 3 files changed, 29 insertions(+), 24 deletions(-) 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 index d6506b8fb2..6d02edfad8 100644 --- 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 @@ -10,7 +10,8 @@ "key": [ { "id": "K_1", - "text": "1" + "text": "1", + "nextlayer": "shift" }, { "id": "K_2", @@ -187,7 +188,8 @@ "id": "K_SHIFT", "text": "*Shift*", "width": 160, - "sp": 1 + "sp": 1, + "nextlayer": "shift" }, { "id": "K_oE2", @@ -318,10 +320,10 @@ "text": "+" }, { - "width": 100, "id": "K_BKSP", - "sp": 1, - "text": "*BkSp*" + "text": "*BkSp*", + "width": 100, + "sp": 1 } ] }, @@ -330,8 +332,8 @@ "key": [ { "id": "K_Q", - "pad": 75, - "text": "Q" + "text": "Q", + "pad": 75 }, { "id": "K_W", @@ -378,6 +380,7 @@ "text": "}" }, { + "id": "T_new_246", "width": 10, "sp": 10 } @@ -444,10 +447,11 @@ "id": 4, "key": [ { - "width": 160, "id": "K_SHIFT", + "text": "*Shift*", + "width": 160, "sp": 1, - "text": "*Shift*" + "nextlayer": "default" }, { "id": "K_oE2", @@ -494,6 +498,7 @@ "text": "?" }, { + "id": "T_new_272", "width": 10, "sp": 10 } @@ -503,20 +508,20 @@ "id": 5, "key": [ { - "width": 140, "id": "K_LOPT", - "sp": 1, - "text": "*Menu*" + "text": "*Menu*", + "width": 140, + "sp": 1 }, { - "width": 930, - "id": "K_SPACE" + "id": "K_SPACE", + "width": 930 }, { - "width": 145, "id": "K_ENTER", - "sp": 1, - "text": "*Enter*" + "text": "*Enter*", + "width": 145, + "sp": 1 } ] } 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 index 3a80bf4be5..98b9ad7e0e 100644 --- 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 @@ -9,7 +9,7 @@ - id_af1590e09d357f1c16c9a1fe9991a9d4 + id_dda967022de452e1fe199096e795f0ab text_selection_tests_keyboard_9073.kmn source\text_selection_tests_keyboard_9073.kmn 1.0 @@ -59,15 +59,15 @@ .keyboard_info - id_0993fe0cb7835cdfb2a101ceccc03e85 + id_bbf31cea8a9cfe0cb838f67055690bf8 text_selection_tests_keyboard_9073.ico source\text_selection_tests_keyboard_9073.ico .ico - id_af1590e09d357f1c16c9a1fe9991a9d4 + id_dda967022de452e1fe199096e795f0ab - id_aff9466042ad8bb0edf57fea7134c373 + id_b8f7a473cac52dd0436273de657cdf46 text_selection_tests_keyboard_9073.kmx source\..\build\text_selection_tests_keyboard_9073.kmx @@ -75,7 +75,7 @@ id_ba932837e6a67a86abc409a393242255 - id_c463c12f68ab14a1f91927146a1942b8 + id_73d0cd87e78d9b8d7f514809dbb36a47 text_selection_tests_keyboard_9073.js source\..\build\text_selection_tests_keyboard_9073.js @@ -83,7 +83,7 @@ id_ba932837e6a67a86abc409a393242255 - id_4a6d9dbdd46a11790e170f92ffe90a6b + id_71aafc060dc3251e4bb611ea539dc8e0 text_selection_tests_keyboard_9073.kvk source\..\build\text_selection_tests_keyboard_9073.kvk 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 index 7235b9c022..a50848e7ea 100644 --- 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 @@ -1 +1 @@ -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(){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 e=Array.apply(null,Array(65)).map(String.prototype.valueOf,""),r=[],v,i,m=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt'];for(i=m.length-1;i>=0;i--)if((v=x[m[i]])||r.length)r=(v?v:e).slice().concat(r);return r})(this.KV.KLS);this.KDU=0;this.KH='';this.KM=0;this.KBVER="1.0";this.KMBM=0x10;this.KVKL={"tablet":{"displayUnderlying":false,"layer":[{"id":"default","row":[{"id":"1","key":[{"id":"K_1","text":"1"},{"id":"K_2","text":"2"},{"id":"K_3","text":"3"},{"id":"K_4","text":"4"},{"id":"K_5","text":"5"},{"id":"K_6","text":"6"},{"id":"K_7","text":"7"},{"id":"K_8","text":"8"},{"id":"K_9","text":"9"},{"id":"K_0","text":"0"},{"id":"K_HYPHEN","text":"-"},{"id":"K_EQUAL","text":"="},{"id":"K_BKSP","text":"*BkSp*","width":"100","sp":"1"}]},{"id":"2","key":[{"id":"K_Q","text":"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"},{"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":"+"},{"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","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":[{"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","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="17.0.211.0";this.KVS=[];this.gs=function(t,e) {return this.g0(t,e);};this.gs=function(t,e) {return this.g0(t,e);};this.g0=function(t,e) {var k=KeymanWeb,r=0,m=0;if(k.KKM(e,16384,8)) {if(k.KFCM(3,t,['a',{t:'d',d:0},'b'])){r=m=1;k.KDC(3,t);k.KO(-1,t,"ok1");}else if(k.KFCM(2,t,['a','b'])){r=m=1;k.KDC(2,t);k.KO(-1,t,"fail1");}else if(k.KFCM(2,t,['a',{t:'d',d:0}])){r=m=1;k.KDC(2,t);k.KO(-1,t,"fail2");}else if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"foo");}}else if(k.KKM(e,16400,65)) {if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"Â");}}else if(k.KKM(e,16384,192)) {if(1){r=m=1;k.KDC(0,t);k.KDO(-1,t,0);}}else if(k.KKM(e,16384,65)) {if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"â");}}else if(k.KKM(e,16384,79)) {if(k.KFCM(1,t,[{t:'d',d:0}])){r=m=1;k.KDC(1,t);k.KO(-1,t,"ok3");}}return r;};} \ No newline at end of file +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(){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 e=Array.apply(null,Array(65)).map(String.prototype.valueOf,""),r=[],v,i,m=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt'];for(i=m.length-1;i>=0;i--)if((v=x[m[i]])||r.length)r=(v?v:e).slice().concat(r);return r})(this.KV.KLS);this.KDU=0;this.KH='';this.KM=0;this.KBVER="1.0";this.KMBM=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.g0(t,e);};this.gs=function(t,e) {return this.g0(t,e);};this.g0=function(t,e) {var k=KeymanWeb,r=0,m=0;if(k.KKM(e,16384,8)) {if(k.KFCM(3,t,['a',{t:'d',d:0},'b'])){r=m=1;k.KDC(3,t);k.KO(-1,t,"ok1");}else if(k.KFCM(2,t,['a','b'])){r=m=1;k.KDC(2,t);k.KO(-1,t,"fail1");}else if(k.KFCM(2,t,['a',{t:'d',d:0}])){r=m=1;k.KDC(2,t);k.KO(-1,t,"fail2");}else if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"foo");}}else if(k.KKM(e,16400,65)) {if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"Â");}}else if(k.KKM(e,16384,192)) {if(1){r=m=1;k.KDC(0,t);k.KDO(-1,t,0);}}else if(k.KKM(e,16384,65)) {if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"â");}}else if(k.KKM(e,16384,79)) {if(k.KFCM(1,t,[{t:'d',d:0}])){r=m=1;k.KDC(1,t);k.KO(-1,t,"ok3");}}return r;};} \ No newline at end of file From f56033f5379ac128957546e7943c5fb023768039 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 23 Nov 2023 18:02:36 +0100 Subject: [PATCH 27/46] chore(web): Add TAB output to test keyboard --- .../text_selection_tests_keyboard_9073.kmn | 2 + .../text_selection_tests_keyboard_9073.js | 642 +++++++++++++++++- 2 files changed, 643 insertions(+), 1 deletion(-) 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 index 4d72f81ded..b40278a930 100644 --- 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 @@ -18,6 +18,8 @@ group(main) using keys + '`' > 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' 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 index a50848e7ea..f1e93eb48e 100644 --- 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 @@ -1 +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(){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 e=Array.apply(null,Array(65)).map(String.prototype.valueOf,""),r=[],v,i,m=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt'];for(i=m.length-1;i>=0;i--)if((v=x[m[i]])||r.length)r=(v?v:e).slice().concat(r);return r})(this.KV.KLS);this.KDU=0;this.KH='';this.KM=0;this.KBVER="1.0";this.KMBM=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.g0(t,e);};this.gs=function(t,e) {return this.g0(t,e);};this.g0=function(t,e) {var k=KeymanWeb,r=0,m=0;if(k.KKM(e,16384,8)) {if(k.KFCM(3,t,['a',{t:'d',d:0},'b'])){r=m=1;k.KDC(3,t);k.KO(-1,t,"ok1");}else if(k.KFCM(2,t,['a','b'])){r=m=1;k.KDC(2,t);k.KO(-1,t,"fail1");}else if(k.KFCM(2,t,['a',{t:'d',d:0}])){r=m=1;k.KDC(2,t);k.KO(-1,t,"fail2");}else if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"foo");}}else if(k.KKM(e,16400,65)) {if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"Â");}}else if(k.KKM(e,16384,192)) {if(1){r=m=1;k.KDC(0,t);k.KDO(-1,t,0);}}else if(k.KKM(e,16384,65)) {if(k.KFCM(1,t,['^'])){r=m=1;k.KDC(1,t);k.KO(-1,t,"â");}}else if(k.KKM(e,16384,79)) {if(k.KFCM(1,t,[{t:'d',d:0}])){r=m=1;k.KDC(1,t);k.KO(-1,t,"ok3");}}return r;};} \ No newline at end of file +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; + }; +} From 08b3764496bc2e326f0758c5c27f1fcaf197a604 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Fri, 24 Nov 2023 07:48:23 +1000 Subject: [PATCH 28/46] chore(windows): review comments Co-authored-by: Marc Durdin --- windows/src/engine/keyman32/appint/aiTIP.h | 3 ++- windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp | 3 +++ windows/src/engine/keyman32/kmprocess.cpp | 3 +-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/windows/src/engine/keyman32/appint/aiTIP.h b/windows/src/engine/keyman32/appint/aiTIP.h index 3b5e894134..bfbca87d89 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.h +++ b/windows/src/engine/keyman32/appint/aiTIP.h @@ -56,7 +56,8 @@ public: /** * Reads the current application context upto MAXCONTEXT length into the supplied buffer. - * @param buf The data buffer to copy current application context + * @param buf The data buffer to copy current application context into, must + * be MAXCONTEXT WCHARs or larger. */ virtual BOOL ReadContext(PWSTR buf); diff --git a/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp b/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp index 43fa723415..a32aced606 100644 --- a/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp +++ b/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp @@ -82,6 +82,9 @@ BOOL AIWin2000Unicode::IsUnicode() 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; } diff --git a/windows/src/engine/keyman32/kmprocess.cpp b/windows/src/engine/keyman32/kmprocess.cpp index 1493285c0d..517b5a27ae 100644 --- a/windows/src/engine/keyman32/kmprocess.cpp +++ b/windows/src/engine/keyman32/kmprocess.cpp @@ -82,7 +82,6 @@ char *getcontext_debug() { if (KM_CORE_STATUS_OK != km_core_context_get( km_core_state_context(_td->lpActiveKeyboard->lpCoreKeyboardState), &citems)) { - km_core_context_items_dispose(citems); return ""; } @@ -110,7 +109,7 @@ Process_Event_Core(PKEYMAN64THREADDATA _td) { 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, "ProcessEvent SetContext if needed Result:False %d ", FALSE); + SendDebugMessageFormat(0, sdmGlobal, 0, "Process_Event_Core: km_core_state_context_set_if_needed returned [%d]", result); } } From 23da871f18e8b316e0191f99ab85f0366d747771 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Fri, 24 Nov 2023 07:52:16 +1000 Subject: [PATCH 29/46] chore(windows): remove res from PR change resetcontext --- windows/src/desktop/kmshell/kmshell.res | Bin 7036 -> 7036 bytes .../keyman32/appint/aiWin2000Unicode.cpp | 12 +++--------- .../engine/keyman32/appint/aiWin2000Unicode.h | 2 +- windows/src/engine/keyman32/appint/appint.h | 2 +- windows/src/engine/keyman64/keyman64.vcxproj | 4 +++- .../engine/keyman64/keyman64.vcxproj.filters | 2 ++ 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/windows/src/desktop/kmshell/kmshell.res b/windows/src/desktop/kmshell/kmshell.res index 0623d03fd306957fced5810b2839c7db7feb10a4..cfcbd309f137d6a5f5221aba48a1c759c0d56682 100644 GIT binary patch delta 15 Xcmexk_Qz~O3Cr{S4;406ut);{LY)U( delta 15 Wcmexk_Qz~O35($$QKgL)EYbiu+6EZ_ diff --git a/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp b/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp index a32aced606..e8562bea35 100644 --- a/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp +++ b/windows/src/engine/keyman32/appint/aiWin2000Unicode.cpp @@ -88,18 +88,12 @@ BOOL AIWin2000Unicode::ReadContext(PWSTR buf) { return FALSE; } -BOOL AIWin2000Unicode::ResetContext() +void AIWin2000Unicode::ResetContext() { PKEYMAN64THREADDATA _td = ThreadGlobals(); - if (!_td) { - return FALSE; + if (_td && _td->lpActiveKeyboard && _td->lpActiveKeyboard->lpCoreKeyboardState) { + km_core_state_context_clear(_td->lpActiveKeyboard->lpCoreKeyboardState); } - if (!_td->lpActiveKeyboard || !_td->lpActiveKeyboard->lpCoreKeyboardState) { - SendDebugMessageFormat(0, sdmAIDefault, 0, "ResetContext: no active keyboard state"); - return FALSE; - } - km_core_state_context_clear(_td->lpActiveKeyboard->lpCoreKeyboardState); - return TRUE; } BYTE SavedKbdState[256]; diff --git a/windows/src/engine/keyman32/appint/aiWin2000Unicode.h b/windows/src/engine/keyman32/appint/aiWin2000Unicode.h index 70c5d658d0..523832742f 100644 --- a/windows/src/engine/keyman32/appint/aiWin2000Unicode.h +++ b/windows/src/engine/keyman32/appint/aiWin2000Unicode.h @@ -46,7 +46,7 @@ public: /* Context functions */ virtual BOOL ReadContext(PWSTR buf); - virtual BOOL ResetContext(); + virtual void ResetContext(); /* Queue and sending functions */ diff --git a/windows/src/engine/keyman32/appint/appint.h b/windows/src/engine/keyman32/appint/appint.h index 4524e3f949..ae31a1b54f 100644 --- a/windows/src/engine/keyman32/appint/appint.h +++ b/windows/src/engine/keyman32/appint/appint.h @@ -88,7 +88,7 @@ public: * @param buf The data buffer to copy current application context */ virtual BOOL ReadContext(PWSTR buf) = 0; - virtual BOOL ResetContext() = 0; + virtual void ResetContext() = 0; /* Queue and sending functions */ 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 @@ + From 7ec4832edc617530152286d2e8d7782bb39a2533 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Fri, 24 Nov 2023 07:52:31 +1000 Subject: [PATCH 30/46] fix(core): memory management of options in action struct Fixes #10067. Management of memory for persisted options was wrong in the action struct, as the members key and value would be freed immediately after being added to the temporary vector (because the vector was of the struct rather than of the class). Given the struct is a C struct, we need the memory management to be explicit, so we now release() each option into the vector as we create it, which means that its member values will not be freed when the option is then immediately deleted. (This allows us to use the initial copy of the members of option that option() constructor does.) Added the release() function as that was a relatively clear way of indicating that the contents of the structure are now owned by the caller, following the pattern from std::unique_ptr. Finally, the unit test for persisted options was in the action_api.cpp test module, but it was never called, so this was not being tested. Now it is. --- core/src/action.cpp | 8 +++++--- core/src/option.cpp | 8 ++++++++ core/src/option.hpp | 7 ++++++- core/tests/unit/kmnkbd/action_api.cpp | 1 + 4 files changed, 20 insertions(+), 4 deletions(-) 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/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..fc04a3ee11 100644 --- a/core/tests/unit/kmnkbd/action_api.cpp +++ b/core/tests/unit/kmnkbd/action_api.cpp @@ -374,6 +374,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(); From b861eff3aa5b45208bdb89c27131225fdfa72a8b Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 24 Nov 2023 08:52:34 +0700 Subject: [PATCH 31/46] chore(android): restores picker onPause behavior when kbd is deleted by picker --- .../keyman/engine/KeyboardPickerActivity.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 caf488a1df..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; @@ -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; @@ -263,6 +265,23 @@ public final class KeyboardPickerActivity extends BaseActivity { return; } + @Override + protected void onPause() { + super.onPause(); + + if (this.hasDeleted) { + this.hasDeleted = false; + + if (KMManager.InAppKeyboard != null) { + KMManager.InAppKeyboard.loadKeyboard(); + } + + if (KMManager.SystemKeyboard != null) { + KMManager.SystemKeyboard.loadKeyboard(); + } + } + } + @Override public boolean onSupportNavigateUp() { onBackPressed(); From da39948ad9d7c41d03786341ae3919e458a7e7ea Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Fri, 24 Nov 2023 08:52:48 +0700 Subject: [PATCH 32/46] chore(android): JSQueueEntry -> String --- .../main/java/com/keyman/engine/KMKeyboard.java | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) 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 cfa515dd4f..c320127e99 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 @@ -54,14 +54,6 @@ import io.sentry.Breadcrumb; import io.sentry.Sentry; import io.sentry.SentryLevel; -class JSQueueEntry { - public String call; - - JSQueueEntry(String call) { - this.call = call; - } -} - final class KMKeyboard extends WebView { private static final String TAG = "KMKeyboard"; private final Context context; @@ -74,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; @@ -291,7 +283,7 @@ final class KMKeyboard extends WebView { } public void loadJavascript(String func) { - this.javascriptAfterLoad.add(new JSQueueEntry(func)); + this.javascriptAfterLoad.add(func); if((keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP && KMManager.InAppKeyboardWebViewClient.getKeyboardLoaded()) || (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM && KMManager.SystemKeyboardWebViewClient.getKeyboardLoaded())) { @@ -316,8 +308,8 @@ final class KMKeyboard extends WebView { } while(javascriptAfterLoad.size() > 0) { - JSQueueEntry entry = javascriptAfterLoad.remove(0); - allCalls.append(entry.call); + String entry = javascriptAfterLoad.remove(0); + allCalls.append(entry); allCalls.append(";"); } From d0b23c96b4ca848fca805524f22d71412fd4292b Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Fri, 24 Nov 2023 13:03:05 -0500 Subject: [PATCH 33/46] auto: increment master version to 17.0.218 --- HISTORY.md | 7 +++++++ VERSION.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 2342633ded..ec21fd7509 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,12 @@ # Keyman Version History +## 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..84c38c9084 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.217 \ No newline at end of file +17.0.218 \ No newline at end of file From e5da0aacc2e666fe040664476c9781179946fed4 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Sat, 25 Nov 2023 05:16:03 +1000 Subject: [PATCH 34/46] chore(developer): fixup test fixture --- .../withfolders.qaa.sencoten.model.kmp.intermediate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" }, { From bb63ba8a664309d126cf128cd0feb12e17073a66 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Sat, 25 Nov 2023 13:51:53 -0600 Subject: [PATCH 35/46] Apply suggestions from code review Co-authored-by: Marc Durdin --- common/web/types/src/util/util.ts | 2 +- developer/src/kmc-ldml/src/compiler/messages.ts | 6 +++--- developer/src/kmc-ldml/test/test-compiler-e2e.ts | 13 ++++--------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/common/web/types/src/util/util.ts b/common/web/types/src/util/util.ts index 863509ebb6..87ba3236c7 100644 --- a/common/web/types/src/util/util.ts +++ b/common/web/types/src/util/util.ts @@ -167,7 +167,7 @@ function Uni_IsNoncharacter(ch : number) { } function Uni_InCodespace(ch : number) { - return ((ch) <= Uni_MAX_CODEPOINT); + return (ch >= 0 && ch <= Uni_MAX_CODEPOINT); }; function Uni_IsValid1(ch: number) { diff --git a/developer/src/kmc-ldml/src/compiler/messages.ts b/developer/src/kmc-ldml/src/compiler/messages.ts index d26073fd35..750cc38c0f 100644 --- a/developer/src/kmc-ldml/src/compiler/messages.ts +++ b/developer/src/kmc-ldml/src/compiler/messages.ts @@ -147,15 +147,15 @@ export class CompilerMessages { static ERROR_DisplayNeedsToOrId = SevError | 0x0022; static Hint_PUACharacters = (o: { count: number, lowestCh: number }) => - m(this.HINT_PUACharacters, `File contained ${o.count} PUA character(s), including ${util.describeCodepoint(o.lowestCh)}`); + 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 contained ${o.count} unassigned character(s), including ${util.describeCodepoint(o.lowestCh)}`); + 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 contained ${o.count} illegal character(s), including ${util.describeCodepoint(o.lowestCh)}`); + 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/test/test-compiler-e2e.ts b/developer/src/kmc-ldml/test/test-compiler-e2e.ts index f13c2594bd..c58d623f33 100644 --- a/developer/src/kmc-ldml/test/test-compiler-e2e.ts +++ b/developer/src/kmc-ldml/test/test-compiler-e2e.ts @@ -9,8 +9,11 @@ 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() { - compilerTestCallbacks.messages = []; // Let's build basic.xml // It should match basic.kmx (built from basic.txt) @@ -34,42 +37,36 @@ describe('compiler-tests', function() { }); it('should handle non existent files', () => { - compilerTestCallbacks.messages = []; const filename = 'DOES_NOT_EXIST.xml'; const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); const source = k.load(filename); assert.notOk(source, `Trying to load(${filename})`); }); it('should handle unparseable files', () => { - compilerTestCallbacks.messages = []; const filename = makePathToFixture('basic-kvk.txt'); // not an .xml file const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); const source = k.load(filename); assert.notOk(source, `Trying to load(${filename})`); }); it('should handle not-valid files', () => { - compilerTestCallbacks.messages = []; const filename = makePathToFixture('test-fr.xml'); // not a keyboard .xml file const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); const source = k.load(filename); assert.notOk(source, `Trying to load(${filename})`); }); it('should handle non existent test files', () => { - compilerTestCallbacks.messages = []; const filename = 'DOES_NOT_EXIST.xml'; const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); const source = k.loadTestData(filename); assert.notOk(source, `Trying to loadTestData(${filename})`); }); it('should handle unparseable test files', () => { - compilerTestCallbacks.messages = []; const filename = makePathToFixture('basic-kvk.txt'); // not an .xml file const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }); const source = k.load(filename); assert.notOk(source, `Trying to loadTestData(${filename})`); }); it('should fail on illegal chars', async function() { - compilerTestCallbacks.messages = []; const inputFilename = makePathToFixture('sections/strs/invalid-illegal.xml'); const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, [ @@ -84,7 +81,6 @@ describe('compiler-tests', function() { assert.isNull(kmx); // should fail post-validate }); it('should hint on pua chars', async function() { - compilerTestCallbacks.messages = []; const inputFilename = makePathToFixture('sections/strs/hint-pua.xml'); // Compile the keyboard const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, @@ -101,7 +97,6 @@ describe('compiler-tests', function() { }); it.skip('should warn on unassigned chars', async function() { // unassigned not implemented yet - compilerTestCallbacks.messages = []; const inputFilename = makePathToFixture('sections/strs/warn-unassigned.xml'); const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false }, [ From d18134fcb36e2e802c0920d5067c63d49c7a8ba0 Mon Sep 17 00:00:00 2001 From: "Steven R. Loomis" Date: Sat, 25 Nov 2023 14:08:16 -0600 Subject: [PATCH 36/46] =?UTF-8?q?feat(common):=20ldml=20improve=20bad=20ch?= =?UTF-8?q?aracter=20code=20=F0=9F=99=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - use constants for PUA For: #9446 --- common/web/types/src/util/util.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/common/web/types/src/util/util.ts b/common/web/types/src/util/util.ts index 87ba3236c7..5c8eb70810 100644 --- a/common/web/types/src/util/util.ts +++ b/common/web/types/src/util/util.ts @@ -134,6 +134,14 @@ 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 @@ -199,9 +207,9 @@ export function isValidUnicode(start: number, end?: number) { } export function isPUA(ch: number) { - return ((ch >= 0xE000 && ch <= 0xF8FF) || - (ch >= 0xF0000 && ch <= 0xFFFFD) || - (ch >= 0x100000 && ch <= 0x10FFFD)); + 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> { From e0c4f4fc65d64e9f69bb27b8d19850e2f6f147cd Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Mon, 27 Nov 2023 15:37:07 +0100 Subject: [PATCH 37/46] fix(web): Fix attachment-api tests --- web/src/test/manual/web/attachment-api/utilities.js | 1 + 1 file changed, 1 insertion(+) 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) { From 6707ab8d0f080c30414448269040b20ed4e2d62a Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Mon, 27 Nov 2023 13:02:27 -0500 Subject: [PATCH 38/46] auto: increment master version to 17.0.219 --- HISTORY.md | 4 ++++ VERSION.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index ec21fd7509..ca833f7b93 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ # Keyman Version History +## 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) diff --git a/VERSION.md b/VERSION.md index 84c38c9084..052a7af313 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.218 \ No newline at end of file +17.0.219 \ No newline at end of file From 7e47fa29e63900ed6e475fee4c0ecff56cc8268e Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Tue, 28 Nov 2023 17:46:39 +0100 Subject: [PATCH 39/46] fix(web): Also move source map Previously we renamed `dom-keyboard-loader.mjs` but forgot to move the source map along. --- common/web/keyboard-processor/build-bundler.js | 1 + 1 file changed, 1 insertion(+) 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: From 5b17b051ce72459a25f9d265617df16c7a0be2df Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Wed, 29 Nov 2023 13:04:03 -0500 Subject: [PATCH 40/46] auto: increment master version to 17.0.220 --- HISTORY.md | 7 +++++++ VERSION.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index ca833f7b93..f96477fa4f 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,12 @@ # Keyman Version History +## 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) diff --git a/VERSION.md b/VERSION.md index 052a7af313..173b7ae0db 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.219 \ No newline at end of file +17.0.220 \ No newline at end of file From 8ff64d09d758f9ba01ae0286cabd1e572730b17c Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Thu, 30 Nov 2023 09:41:02 +0700 Subject: [PATCH 41/46] chore(android): simple host-page force-reset after kbd updates --- .../java/com/keyman/engine/logic/ResourcesUpdateTool.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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); From 63e43715139c085441f6aeb9f058cac8f93ac72b Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:51:52 +1000 Subject: [PATCH 42/46] chore(windows): fix argument passed into to km_core_state_context_set_if_needed --- windows/src/engine/keyman32/kmprocess.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/src/engine/keyman32/kmprocess.cpp b/windows/src/engine/keyman32/kmprocess.cpp index 517b5a27ae..b83079b03d 100644 --- a/windows/src/engine/keyman32/kmprocess.cpp +++ b/windows/src/engine/keyman32/kmprocess.cpp @@ -107,7 +107,7 @@ Process_Event_Core(PKEYMAN64THREADDATA _td) { 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)); + 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); } From 6820f36f7d46b3890918f00354f49ee56eeaa608 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:09:22 +1000 Subject: [PATCH 43/46] fix(core): set_if_needed updates a empty cached context In the case when the cached context had been cleared the km_core_state_context_set_if_needed call would just compare the null terminations of both strings and not set the cached context to the application context. --- core/src/km_core_state_api.cpp | 6 +++++- core/tests/unit/kmnkbd/action_api.cpp | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/core/src/km_core_state_api.cpp b/core/src/km_core_state_api.cpp index 8a8165c9c0..520e2b2a92 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 == NULL) { + // 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/tests/unit/kmnkbd/action_api.cpp b/core/tests/unit/kmnkbd/action_api.cpp index fc04a3ee11..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(); From 9e1f73573e67a19694737fb5b00b0596b2a7d253 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:56:01 +1000 Subject: [PATCH 44/46] fix(core): review comments Co-authored-by: Marc Durdin --- core/src/km_core_state_api.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/km_core_state_api.cpp b/core/src/km_core_state_api.cpp index 520e2b2a92..8756c80e2e 100644 --- a/core/src/km_core_state_api.cpp +++ b/core/src/km_core_state_api.cpp @@ -263,8 +263,8 @@ 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 == NULL) { - // If the cached_context is "empty" then it needs updating - return false; + // If the cached_context is "empty" then it needs updating + return false; } km_core_cp const* context_p = context; while(*context_p) { From d5963e05b6f2c7a6b36f7dce77f0ffee0c2147ed Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:31:18 +1000 Subject: [PATCH 45/46] fix: check for null termination Determine the best way to check for null termination all compilers --- core/src/km_core_state_api.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/km_core_state_api.cpp b/core/src/km_core_state_api.cpp index 8756c80e2e..2a25d6fcc7 100644 --- a/core/src/km_core_state_api.cpp +++ b/core/src/km_core_state_api.cpp @@ -262,7 +262,7 @@ 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 == NULL) { + if (context == nullptr || cached_context == nullptr || *cached_context == '\0') { // If the cached_context is "empty" then it needs updating return false; } From 396170d621c6314e4a56b5dc50986edc7401341d Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Thu, 30 Nov 2023 13:02:33 -0500 Subject: [PATCH 46/46] auto: increment master version to 17.0.221 --- HISTORY.md | 5 +++++ VERSION.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index f96477fa4f..336b19f38b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # 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) diff --git a/VERSION.md b/VERSION.md index 173b7ae0db..62e9701dda 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.220 \ No newline at end of file +17.0.221 \ No newline at end of file