Merge pull request #10120 from keymanapp/chore/web/conflict-fix

chore(web): merge conflict fix for feature-gestures 🐵
This commit is contained in:
Joshua Horton 2023-12-04 08:10:58 +07:00 committed by GitHub
commit d8a6467b2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 3052 additions and 691 deletions

View file

@ -1,5 +1,28 @@
# Keyman Version History
## 17.0.220 alpha 2023-11-30
* fix(core): set_if_needed updates an empty cached context (#10098)
* fix(core): check for null termination (#10101)
## 17.0.219 alpha 2023-11-29
* fix(developer): path separator for kmc-package (#10064)
* fix(developer): projects 2.0 internal path enumeration (#10016)
* fix(web): Fix attachment-api tests (#10085)
* fix(web): Also move source map (#10089)
## 17.0.218 alpha 2023-11-27
* feat(developer): ldml: err/hint on illegal/pua chars (#10029)
## 17.0.217 alpha 2023-11-24
* feat(developer): warn on usage of virtual keys in rule output (#10062)
* fix(core): memory management of options in action struct (#10073)
* chore(linux): Update debian changelog (#10047)
* chore(core): Add test keyboard for text selection tests (#10026)
## 17.0.216 alpha 2023-11-23
* fix(common): kmx struct alignment (#9977)

View file

@ -1 +1 @@
17.0.217
17.0.221

View file

@ -34,6 +34,10 @@ function init() {
keyman.getOskHeight = getOskHeight;
keyman.getOskWidth = getOskWidth;
keyman.beepKeyboard = beepKeyboard;
// Readies the keyboard stub for instant loading during the init process.
KeymanWeb.registerStub(JSON.parse(jsInterface.initialKeyboard()));
keyman.init({
'embeddingApp':device,
'fonts':'packages/',
@ -102,7 +106,7 @@ function setBannerHeight(h) {
if (keyman.osk) {
keyman.osk.bannerView.activeBannerHeight = bannerHeight;
}
}
}
// Refresh KMW's OSK
@ -149,8 +153,8 @@ function onStateChange(change) {
keyman.refreshOskLayout();
fragmentToggle = (fragmentToggle + 1) % 100;
if(change != 'configured') { // doesn't change the display; only initiates suggestions.
window.location.hash = 'refreshBannerHeight-'+fragmentToggle+'+change='+change;
if(change != 'configured') {
window.location.hash = 'refreshBannerHeight-'+fragmentToggle;
}
}

View file

@ -66,7 +66,7 @@ final class KMKeyboard extends WebView {
private boolean shouldIgnoreSelectionChange = false;
protected KeyboardType keyboardType = KeyboardType.KEYBOARD_TYPE_UNDEFINED;
protected ArrayList<String> javascriptAfterLoad = new ArrayList<String>();
protected ArrayList<String> javascriptAfterLoad = new ArrayList<>();
private static String currentKeyboard = null;
@ -214,10 +214,8 @@ final class KMKeyboard extends WebView {
}
// Send console errors to Sentry in case they're missed by KMW sentryManager
// (Ignoring spurious message "No keyboard stubs exist = ...")
// TODO: Fix base error rather than trying to ignore it "No keyboard stubs exist"
if ((cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) && (!cm.message().startsWith("No keyboard stubs exist"))) {
if (cm.messageLevel() == ConsoleMessage.MessageLevel.ERROR) {
// Make Toast notification of error and send log about falling back to default keyboard (ignore language ID)
// Sanitize sourceId info
String NAVIGATION_PATTERN = "^(.*)?(keyboard\\.html#[^-]+)-.*$";
@ -302,15 +300,21 @@ final class KMKeyboard extends WebView {
this.postDelayed(new Runnable() {
@Override
public void run() {
if(javascriptAfterLoad.size() > 0) {
loadUrl("javascript:" + javascriptAfterLoad.get(0));
javascriptAfterLoad.remove(0);
// Make sure we didn't reset the page in the middle of the queue!
if(keyboardSet) {
if (javascriptAfterLoad.size() > 0) {
callJavascriptAfterLoad();
}
}
StringBuilder allCalls = new StringBuilder();
if(javascriptAfterLoad.size() == 0) {
return;
}
while(javascriptAfterLoad.size() > 0) {
String entry = javascriptAfterLoad.remove(0);
allCalls.append(entry);
allCalls.append(";");
}
loadUrl("javascript:" + allCalls.toString());
if(javascriptAfterLoad.size() > 0 && keyboardSet) {
callJavascriptAfterLoad();
}
}
}, 1);

View file

@ -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() {

View file

@ -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) {

View file

@ -777,12 +777,10 @@ public final class KMManager {
// KMKeyboard
if (InAppKeyboard != null) {
RelativeLayout.LayoutParams params = getKeyboardLayoutParams();
InAppKeyboard.setLayoutParams(params);
InAppKeyboard.onConfigurationChanged(newConfig);
}
if (SystemKeyboard != null) {
RelativeLayout.LayoutParams params = getKeyboardLayoutParams();
SystemKeyboard.setLayoutParams(params);
SystemKeyboard.onConfigurationChanged(newConfig);
}
}
@ -1388,12 +1386,14 @@ public final class KMManager {
RelativeLayout.LayoutParams params;
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP) && !InAppKeyboard.shouldIgnoreTextChange() && modelFileExists) {
params = getKeyboardLayoutParams();
InAppKeyboard.setLayoutParams(params);
// Do NOT re-layout here; it'll be triggered once the banner loads.
InAppKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect));
}
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM) && !SystemKeyboard.shouldIgnoreTextChange() && modelFileExists) {
params = getKeyboardLayoutParams();
SystemKeyboard.setLayoutParams(params);
// Do NOT re-layout here; it'll be triggered once the banner loads.
SystemKeyboard.loadJavascript(KMString.format("enableSuggestions(%s, %s, %s)", model, mayPredict, mayCorrect));
}
return true;

View file

@ -51,6 +51,7 @@ import android.widget.Toast;
import androidx.appcompat.widget.Toolbar;
public final class KeyboardPickerActivity extends BaseActivity {
private boolean hasDeleted = false;
//TODO: view instances should not be static
private static Toolbar toolbar = null;
@ -123,7 +124,7 @@ public final class KeyboardPickerActivity extends BaseActivity {
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switchKeyboard(position,dismissOnSelect && ! KMManager.isTestMode());
switchKeyboard(position);
if (dismissOnSelect)
finish();
}
@ -140,6 +141,7 @@ public final class KeyboardPickerActivity extends BaseActivity {
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.popup_delete) {
deleteKeyboard(context, position);
KeyboardPickerActivity.this.hasDeleted = true;
return true;
} else {
return false;
@ -267,11 +269,16 @@ public final class KeyboardPickerActivity extends BaseActivity {
protected void onPause() {
super.onPause();
if (KMManager.InAppKeyboard != null) {
KMManager.InAppKeyboard.loadKeyboard();
}
if (KMManager.SystemKeyboard != null) {
KMManager.SystemKeyboard.loadKeyboard();
if (this.hasDeleted) {
this.hasDeleted = false;
if (KMManager.InAppKeyboard != null) {
KMManager.InAppKeyboard.loadKeyboard();
}
if (KMManager.SystemKeyboard != null) {
KMManager.SystemKeyboard.loadKeyboard();
}
}
}
@ -335,7 +342,7 @@ public final class KeyboardPickerActivity extends BaseActivity {
* @param position the keyboard index in list
* @param aPrepareOnly prepare switch, it is executed on keyboard reload
*/
private static void switchKeyboard(int position, boolean aPrepareOnly) {
private static void switchKeyboard(int position) {
setSelection(position);
int size = KeyboardController.getInstance().get().size();
int listPosition = (position >= size) ? size-1 : position;
@ -344,10 +351,7 @@ public final class KeyboardPickerActivity extends BaseActivity {
String kbId = kbInfo.getKeyboardID();
String langId = kbInfo.getLanguageID();
String kbName = kbInfo.getKeyboardName();
if(aPrepareOnly)
KMManager.prepareKeyboardSwitch(pkgId, kbId, langId, kbName);
else
KMManager.setKeyboard(kbInfo);
KMManager.setKeyboard(kbInfo);
}
protected static boolean addKeyboard(Context context, Keyboard keyboardInfo) {
@ -449,7 +453,7 @@ public final class KeyboardPickerActivity extends BaseActivity {
adapter.notifyDataSetChanged();
}
if (position == curKbPos) {
switchKeyboard(0,false);
switchKeyboard(0);
} else if(listView != null) { // A bit of a hack, since LanguageSettingsActivity calls this method too.
curKbPos = KeyboardController.getInstance().getKeyboardIndex(KMKeyboard.currentKeyboard());
setSelection(curKbPos);

View file

@ -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

View file

@ -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);

View file

@ -0,0 +1,6 @@
Text Selection Tests Keyboard Change History
====================
1.0 (2023-11-14)
----------------
* Created by Keyman Team

View file

@ -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.

View file

@ -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

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Text Selection Tests Keyboard</title>
<style type="text/css">
p { font: 10pt Tahoma; }
h1 { font: bold 16pt Tahoma; color: #4444cc; margin-bottom: 2px }
h2 { font: bold 12pt Tahoma; color: #4444cc; }
</style>
</head>
<body>
<h1>Text Selection Tests Keyboard</h1>
<p>
Text Selection Tests Keyboard 1.0 generated from template.
</p>
<p>© Keyman Team</p>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,532 @@
{
"tablet": {
"displayUnderlying": false,
"layer": [
{
"id": "default",
"row": [
{
"id": 1,
"key": [
{
"id": "K_1",
"text": "1",
"nextlayer": "shift"
},
{
"id": "K_2",
"text": "2"
},
{
"id": "K_3",
"text": "3"
},
{
"id": "K_4",
"text": "4"
},
{
"id": "K_5",
"text": "5"
},
{
"id": "K_6",
"text": "6"
},
{
"id": "K_7",
"text": "7"
},
{
"id": "K_8",
"text": "8"
},
{
"id": "K_9",
"text": "9"
},
{
"id": "K_0",
"text": "0"
},
{
"id": "K_HYPHEN",
"text": "-"
},
{
"id": "K_EQUAL",
"text": "="
},
{
"id": "K_BKSP",
"text": "*BkSp*",
"width": 100,
"sp": 1
}
]
},
{
"id": 2,
"key": [
{
"id": "K_Q",
"text": "q",
"pad": 75
},
{
"id": "K_W",
"text": "w"
},
{
"id": "K_E",
"text": "e"
},
{
"id": "K_R",
"text": "r"
},
{
"id": "K_T",
"text": "t"
},
{
"id": "K_Y",
"text": "y"
},
{
"id": "K_U",
"text": "u"
},
{
"id": "K_I",
"text": "i"
},
{
"id": "K_O",
"text": "o"
},
{
"id": "K_P",
"text": "p"
},
{
"id": "K_LBRKT",
"text": "["
},
{
"id": "K_RBRKT",
"text": "]"
},
{
"id": "T_new_136",
"width": 10,
"sp": 10
}
]
},
{
"id": 3,
"key": [
{
"id": "K_BKQUOTE",
"text": "dk(1)"
},
{
"id": "K_A",
"text": "a"
},
{
"id": "K_S",
"text": "s"
},
{
"id": "K_D",
"text": "d"
},
{
"id": "K_F",
"text": "f"
},
{
"id": "K_G",
"text": "g"
},
{
"id": "K_H",
"text": "h"
},
{
"id": "K_J",
"text": "j"
},
{
"id": "K_K",
"text": "k"
},
{
"id": "K_L",
"text": "l"
},
{
"id": "K_COLON",
"text": ";"
},
{
"id": "K_QUOTE",
"text": "'"
},
{
"id": "K_BKSLASH",
"text": "\\"
}
]
},
{
"id": 4,
"key": [
{
"id": "K_SHIFT",
"text": "*Shift*",
"width": 160,
"sp": 1,
"nextlayer": "shift"
},
{
"id": "K_oE2",
"text": "\\"
},
{
"id": "K_Z",
"text": "z"
},
{
"id": "K_X",
"text": "x"
},
{
"id": "K_C",
"text": "c"
},
{
"id": "K_V",
"text": "v"
},
{
"id": "K_B",
"text": "b"
},
{
"id": "K_N",
"text": "n"
},
{
"id": "K_M",
"text": "m"
},
{
"id": "K_COMMA",
"text": ","
},
{
"id": "K_PERIOD",
"text": "."
},
{
"id": "K_SLASH",
"text": "/"
},
{
"id": "T_new_162",
"width": 10,
"sp": 10
}
]
},
{
"id": 5,
"key": [
{
"id": "K_LOPT",
"text": "*Menu*",
"width": 140,
"sp": 1
},
{
"id": "K_SPACE",
"width": 930
},
{
"id": "K_ENTER",
"text": "*Enter*",
"width": 145,
"sp": 1
}
]
}
]
},
{
"id": "shift",
"row": [
{
"id": 1,
"key": [
{
"id": "K_1",
"text": "!"
},
{
"id": "K_2",
"text": "@"
},
{
"id": "K_3",
"text": "#"
},
{
"id": "K_4",
"text": "$"
},
{
"id": "K_5",
"text": "%"
},
{
"id": "K_6",
"text": "^"
},
{
"id": "K_7",
"text": "&"
},
{
"id": "K_8",
"text": "*"
},
{
"id": "K_9",
"text": "("
},
{
"id": "K_0",
"text": ")"
},
{
"id": "K_HYPHEN",
"text": "_"
},
{
"id": "K_EQUAL",
"text": "+"
},
{
"id": "K_BKSP",
"text": "*BkSp*",
"width": 100,
"sp": 1
}
]
},
{
"id": 2,
"key": [
{
"id": "K_Q",
"text": "Q",
"pad": 75
},
{
"id": "K_W",
"text": "W"
},
{
"id": "K_E",
"text": "E"
},
{
"id": "K_R",
"text": "R"
},
{
"id": "K_T",
"text": "T"
},
{
"id": "K_Y",
"text": "Y"
},
{
"id": "K_U",
"text": "U"
},
{
"id": "K_I",
"text": "I"
},
{
"id": "K_O",
"text": "O"
},
{
"id": "K_P",
"text": "P"
},
{
"id": "K_LBRKT",
"text": "{"
},
{
"id": "K_RBRKT",
"text": "}"
},
{
"id": "T_new_246",
"width": 10,
"sp": 10
}
]
},
{
"id": 3,
"key": [
{
"id": "K_BKQUOTE",
"text": "~"
},
{
"id": "K_A",
"text": "A"
},
{
"id": "K_S",
"text": "S"
},
{
"id": "K_D",
"text": "D"
},
{
"id": "K_F",
"text": "F"
},
{
"id": "K_G",
"text": "G"
},
{
"id": "K_H",
"text": "H"
},
{
"id": "K_J",
"text": "J"
},
{
"id": "K_K",
"text": "K"
},
{
"id": "K_L",
"text": "L"
},
{
"id": "K_COLON",
"text": ":"
},
{
"id": "K_QUOTE",
"text": "\""
},
{
"id": "K_BKSLASH",
"text": "|"
}
]
},
{
"id": 4,
"key": [
{
"id": "K_SHIFT",
"text": "*Shift*",
"width": 160,
"sp": 1,
"nextlayer": "default"
},
{
"id": "K_oE2",
"text": "|"
},
{
"id": "K_Z",
"text": "Z"
},
{
"id": "K_X",
"text": "X"
},
{
"id": "K_C",
"text": "C"
},
{
"id": "K_V",
"text": "V"
},
{
"id": "K_B",
"text": "B"
},
{
"id": "K_N",
"text": "N"
},
{
"id": "K_M",
"text": "M"
},
{
"id": "K_COMMA",
"text": "<"
},
{
"id": "K_PERIOD",
"text": ">"
},
{
"id": "K_SLASH",
"text": "?"
},
{
"id": "T_new_272",
"width": 10,
"sp": 10
}
]
},
{
"id": 5,
"key": [
{
"id": "K_LOPT",
"text": "*Menu*",
"width": 140,
"sp": 1
},
{
"id": "K_SPACE",
"width": 930
},
{
"id": "K_ENTER",
"text": "*Enter*",
"width": 145,
"sp": 1
}
]
}
]
}
]
}
}

View file

@ -0,0 +1,26 @@
c text_selection_tests_keyboard_9073 generated from template at 2023-11-14 15:23:49
c with name "Text Selection Tests Keyboard"
store(&VERSION) '10.0'
store(&NAME) 'Text Selection Tests Keyboard'
store(&COPYRIGHT) '© Keyman Team'
store(&KEYBOARDVERSION) '1.0'
store(&TARGETS) 'any'
store(&BITMAP) 'text_selection_tests_keyboard_9073.ico'
store(&VISUALKEYBOARD) 'text_selection_tests_keyboard_9073.kvks'
store(&LAYOUTFILE) 'text_selection_tests_keyboard_9073.keyman-touch-layout'
begin Unicode > use(main)
group(main) using keys
'^' + [K_A] > 'â'
'^' + [SHIFT K_A] > 'Â'
'^' + [K_BKSP] > 'foo'
+ '`' > dk(1)
+ [K_T] > U+0009 c TAB
'a' dk(1) 'b' + [K_BKSP] > 'ok1'
'a' 'b' + [K_BKSP] > 'fail1'
'a' dk(1) + [K_BKSP] > 'fail2'
dk(1) + 'o' > 'ok3'

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<Package>
<System>
<KeymanDeveloperVersion>16.0.142.0</KeymanDeveloperVersion>
<FileVersion>7.0</FileVersion>
</System>
<Options>
<ExecuteProgram></ExecuteProgram>
<ReadMeFile>readme.htm</ReadMeFile>
<MSIFileName></MSIFileName>
<MSIOptions></MSIOptions>
<FollowKeyboardVersion/>
</Options>
<StartMenu>
<Folder></Folder>
<Items/>
</StartMenu>
<Info>
<Name URL="">Text Selection Tests Keyboard</Name>
<Copyright URL="">© Keyman Team</Copyright>
<Author URL="">Keyman Team</Author>
<Version URL=""></Version>
</Info>
<Files>
<File>
<Name>..\build\text_selection_tests_keyboard_9073.kmx</Name>
<Description></Description>
<CopyLocation>0</CopyLocation>
<FileType>.kmx</FileType>
</File>
<File>
<Name>..\build\text_selection_tests_keyboard_9073.js</Name>
<Description></Description>
<CopyLocation>0</CopyLocation>
<FileType>.js</FileType>
</File>
<File>
<Name>..\build\text_selection_tests_keyboard_9073.kvk</Name>
<Description></Description>
<CopyLocation>0</CopyLocation>
<FileType>.kvk</FileType>
</File>
<File>
<Name>welcome.htm</Name>
<Description></Description>
<CopyLocation>0</CopyLocation>
<FileType>.htm</FileType>
</File>
<File>
<Name>readme.htm</Name>
<Description></Description>
<CopyLocation>0</CopyLocation>
<FileType>.htm</FileType>
</File>
</Files>
<Keyboards>
<Keyboard>
<Name>Text Selection Tests Keyboard</Name>
<ID>text_selection_tests_keyboard_9073</ID>
<Version>1.0</Version>
<Languages>
<Language ID="en">English</Language>
</Languages>
</Keyboard>
</Keyboards>
<Strings/>
</Package>

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<visualkeyboard>
<header>
<version>10.0</version>
<kbdname>text_selection_tests_keyboard_9073</kbdname>
<flags/>
</header>
<encoding name="unicode" fontname="Arial" fontsize="-12">
<layer shift="">
<key vkey="K_BKQUOTE">dk(1)</key>
<key vkey="K_1">1</key>
<key vkey="K_2">2</key>
<key vkey="K_3">3</key>
<key vkey="K_4">4</key>
<key vkey="K_5">5</key>
<key vkey="K_6">6</key>
<key vkey="K_7">7</key>
<key vkey="K_8">8</key>
<key vkey="K_9">9</key>
<key vkey="K_0">0</key>
<key vkey="K_HYPHEN">-</key>
<key vkey="K_EQUAL">=</key>
<key vkey="K_Q">q</key>
<key vkey="K_W">w</key>
<key vkey="K_E">e</key>
<key vkey="K_R">r</key>
<key vkey="K_T">t</key>
<key vkey="K_Y">y</key>
<key vkey="K_U">u</key>
<key vkey="K_I">i</key>
<key vkey="K_O">o</key>
<key vkey="K_P">p</key>
<key vkey="K_LBRKT">[</key>
<key vkey="K_RBRKT">]</key>
<key vkey="K_BKSLASH">\</key>
<key vkey="K_A">a</key>
<key vkey="K_S">s</key>
<key vkey="K_D">d</key>
<key vkey="K_F">f</key>
<key vkey="K_G">g</key>
<key vkey="K_H">h</key>
<key vkey="K_J">j</key>
<key vkey="K_K">k</key>
<key vkey="K_L">l</key>
<key vkey="K_COLON">;</key>
<key vkey="K_QUOTE">'</key>
<key vkey="K_oE2">\</key>
<key vkey="K_Z">z</key>
<key vkey="K_X">x</key>
<key vkey="K_C">c</key>
<key vkey="K_V">v</key>
<key vkey="K_B">b</key>
<key vkey="K_N">n</key>
<key vkey="K_M">m</key>
<key vkey="K_COMMA">,</key>
<key vkey="K_PERIOD">.</key>
<key vkey="K_SLASH">/</key>
</layer>
<layer shift="S">
<key vkey="K_BKQUOTE">~</key>
<key vkey="K_1">!</key>
<key vkey="K_2">@</key>
<key vkey="K_3">#</key>
<key vkey="K_4">$</key>
<key vkey="K_5">%</key>
<key vkey="K_6">^</key>
<key vkey="K_7">&amp;</key>
<key vkey="K_8">*</key>
<key vkey="K_9">(</key>
<key vkey="K_0">)</key>
<key vkey="K_HYPHEN">_</key>
<key vkey="K_EQUAL">+</key>
<key vkey="K_Q">Q</key>
<key vkey="K_W">W</key>
<key vkey="K_E">E</key>
<key vkey="K_R">R</key>
<key vkey="K_T">T</key>
<key vkey="K_Y">Y</key>
<key vkey="K_U">U</key>
<key vkey="K_I">I</key>
<key vkey="K_O">O</key>
<key vkey="K_P">P</key>
<key vkey="K_LBRKT">{</key>
<key vkey="K_RBRKT">}</key>
<key vkey="K_BKSLASH">|</key>
<key vkey="K_A">A</key>
<key vkey="K_S">S</key>
<key vkey="K_D">D</key>
<key vkey="K_F">F</key>
<key vkey="K_G">G</key>
<key vkey="K_H">H</key>
<key vkey="K_J">J</key>
<key vkey="K_K">K</key>
<key vkey="K_L">L</key>
<key vkey="K_COLON">:</key>
<key vkey="K_QUOTE">"</key>
<key vkey="K_oE2">|</key>
<key vkey="K_Z">Z</key>
<key vkey="K_X">X</key>
<key vkey="K_C">C</key>
<key vkey="K_V">V</key>
<key vkey="K_B">B</key>
<key vkey="K_N">N</key>
<key vkey="K_M">M</key>
<key vkey="K_COMMA">&lt;</key>
<key vkey="K_PERIOD">&gt;</key>
<key vkey="K_SLASH">?</key>
</layer>
</encoding>
</visualkeyboard>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Start Using Text Selection Tests Keyboard</title>
<style type="text/css">
p { font: 10pt Tahoma; }
h1 { font: bold 16pt Tahoma; color: #4444cc; margin-bottom: 2px }
h2 { font: bold 12pt Tahoma; color: #4444cc; }
</style>
</head>
<body>
<h1>Start Using Text Selection Tests Keyboard</h1>
<p>
Text Selection Tests Keyboard 1.0 generated from template.
</p>
<h1>Keyboard Layout</h1>
<!-- Insert Keyboard Layout Images or HTML here -->
</body>
</html>

View file

@ -0,0 +1,7 @@
{
"license": "mit",
"languages": [
"en"
],
"description": "Text Selection Tests Keyboard generated from template"
}

View file

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<KeymanDeveloperProject>
<Options>
<BuildPath>$PROJECTPATH\build</BuildPath>
<CompilerWarningsAsErrors>True</CompilerWarningsAsErrors>
<WarnDeprecatedCode>True</WarnDeprecatedCode>
<CheckFilenameConventions>True</CheckFilenameConventions>
<ProjectType>keyboard</ProjectType>
</Options>
<Files>
<File>
<ID>id_dda967022de452e1fe199096e795f0ab</ID>
<Filename>text_selection_tests_keyboard_9073.kmn</Filename>
<Filepath>source\text_selection_tests_keyboard_9073.kmn</Filepath>
<FileVersion>1.0</FileVersion>
<FileType>.kmn</FileType>
<Details>
<Name>Text Selection Tests Keyboard</Name>
<Copyright>© Keyman Team</Copyright>
</Details>
</File>
<File>
<ID>id_ba932837e6a67a86abc409a393242255</ID>
<Filename>text_selection_tests_keyboard_9073.kps</Filename>
<Filepath>source\text_selection_tests_keyboard_9073.kps</Filepath>
<FileVersion></FileVersion>
<FileType>.kps</FileType>
<Details>
<Name>Text Selection Tests Keyboard</Name>
<Copyright>© Keyman Team</Copyright>
</Details>
</File>
<File>
<ID>id_ede98e4633e239f933cbfd1f4e1b766c</ID>
<Filename>HISTORY.md</Filename>
<Filepath>HISTORY.md</Filepath>
<FileVersion></FileVersion>
<FileType>.md</FileType>
</File>
<File>
<ID>id_53e892b8b41cc4caece1cfd5ef21d6e7</ID>
<Filename>LICENSE.md</Filename>
<Filepath>LICENSE.md</Filepath>
<FileVersion></FileVersion>
<FileType>.md</FileType>
</File>
<File>
<ID>id_0730bb7c2e8f9ea2438b52e419dd86c9</ID>
<Filename>README.md</Filename>
<Filepath>README.md</Filepath>
<FileVersion></FileVersion>
<FileType>.md</FileType>
</File>
<File>
<ID>id_4b87bd35cc2e16f1ff8680a6f2caed52</ID>
<Filename>text_selection_tests_keyboard_9073.keyboard_info</Filename>
<Filepath>text_selection_tests_keyboard_9073.keyboard_info</Filepath>
<FileVersion></FileVersion>
<FileType>.keyboard_info</FileType>
</File>
<File>
<ID>id_bbf31cea8a9cfe0cb838f67055690bf8</ID>
<Filename>text_selection_tests_keyboard_9073.ico</Filename>
<Filepath>source\text_selection_tests_keyboard_9073.ico</Filepath>
<FileVersion></FileVersion>
<FileType>.ico</FileType>
<ParentFileID>id_dda967022de452e1fe199096e795f0ab</ParentFileID>
</File>
<File>
<ID>id_b8f7a473cac52dd0436273de657cdf46</ID>
<Filename>text_selection_tests_keyboard_9073.kmx</Filename>
<Filepath>source\..\build\text_selection_tests_keyboard_9073.kmx</Filepath>
<FileVersion></FileVersion>
<FileType>.kmx</FileType>
<ParentFileID>id_ba932837e6a67a86abc409a393242255</ParentFileID>
</File>
<File>
<ID>id_73d0cd87e78d9b8d7f514809dbb36a47</ID>
<Filename>text_selection_tests_keyboard_9073.js</Filename>
<Filepath>source\..\build\text_selection_tests_keyboard_9073.js</Filepath>
<FileVersion></FileVersion>
<FileType>.js</FileType>
<ParentFileID>id_ba932837e6a67a86abc409a393242255</ParentFileID>
</File>
<File>
<ID>id_71aafc060dc3251e4bb611ea539dc8e0</ID>
<Filename>text_selection_tests_keyboard_9073.kvk</Filename>
<Filepath>source\..\build\text_selection_tests_keyboard_9073.kvk</Filepath>
<FileVersion></FileVersion>
<FileType>.kvk</FileType>
<ParentFileID>id_ba932837e6a67a86abc409a393242255</ParentFileID>
</File>
<File>
<ID>id_356e5d149c1e539356d72698c1e401a6</ID>
<Filename>welcome.htm</Filename>
<Filepath>source\welcome.htm</Filepath>
<FileVersion></FileVersion>
<FileType>.htm</FileType>
<ParentFileID>id_ba932837e6a67a86abc409a393242255</ParentFileID>
</File>
<File>
<ID>id_8da344c4cea6f467013357fe099006f5</ID>
<Filename>readme.htm</Filename>
<Filepath>source\readme.htm</Filepath>
<FileVersion></FileVersion>
<FileType>.htm</FileType>
<ParentFileID>id_ba932837e6a67a86abc409a393242255</ParentFileID>
</File>
</Files>
</KeymanDeveloperProject>

View file

@ -128,8 +128,10 @@ export default class LanguageProcessor extends EventEmitter<LanguageProcessorEve
return this.lmEngine.loadModel(source, specType).then((config: Configuration) => {
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;

View file

@ -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:

View file

@ -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<string>();
/**
* 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);

View file

@ -101,3 +101,161 @@ toOneChar(value: string) : number {
}
return value.codePointAt(0);
}
export function describeCodepoint(ch : number) : string {
let s;
const p = getProblem(ch);
if (p != null) {
// for example: 'PUA (U+E010)'
s = p;
} else {
// for example: '"a" (U+61)'
s = `"${String.fromCodePoint(ch)}"`;
}
return `${s} (U+${Number(ch).toString(16).toUpperCase()})`;
}
export enum BadStringType {
pua = 'PUA',
unassigned = 'Unassigned',
illegal = 'Illegal',
};
// Following from kmx_xstring.h / .cpp
const Uni_LEAD_SURROGATE_START = 0xD800;
const Uni_LEAD_SURROGATE_END = 0xDBFF;
const Uni_TRAIL_SURROGATE_START = 0xDC00;
const Uni_TRAIL_SURROGATE_END = 0xDFFF;
const Uni_SURROGATE_START = Uni_LEAD_SURROGATE_START;
const Uni_SURROGATE_END = Uni_TRAIL_SURROGATE_END;
const Uni_FD_NONCHARACTER_START = 0xFDD0;
const Uni_FD_NONCHARACTER_END = 0xFDEF;
const Uni_FFFE_NONCHARACTER = 0xFFFE;
const Uni_PLANE_MASK = 0x1F0000;
const Uni_MAX_CODEPOINT = 0x10FFFF;
// plane 0, 15, and 16 PUA
const Uni_PUA_00_START = 0xE000;
const Uni_PUA_00_END = 0xF8FF;
const Uni_PUA_15_START = 0x0F0000;
const Uni_PUA_15_END = 0x0FFFFD;
const Uni_PUA_16_START = 0x100000;
const Uni_PUA_16_END = 0x10FFFD;
/**
* @brief True if a lead surrogate
* \def Uni_IsSurrogate1
*/
function Uni_IsSurrogate1(ch : number) {
return ((ch) >= Uni_LEAD_SURROGATE_START && (ch) <= Uni_LEAD_SURROGATE_END);
}
/**
* @brief True if a trail surrogate
* \def Uni_IsSurrogate2
*/
function Uni_IsSurrogate2(ch : number) {
return ((ch) >= Uni_TRAIL_SURROGATE_START && (ch) <= Uni_TRAIL_SURROGATE_END);
}
/**
* @brief True if any surrogate
* \def UniIsSurrogate
*/
function Uni_IsSurrogate(ch : number) {
return (Uni_IsSurrogate1(ch) || Uni_IsSurrogate2(ch));
}
function Uni_IsEndOfPlaneNonCharacter(ch : number) {
return (((ch) & Uni_FFFE_NONCHARACTER) == Uni_FFFE_NONCHARACTER); // matches FFFF or FFFE
}
function Uni_IsNoncharacter(ch : number) {
return (((ch) >= Uni_FD_NONCHARACTER_START && (ch) <= Uni_FD_NONCHARACTER_END) || Uni_IsEndOfPlaneNonCharacter(ch));
}
function Uni_InCodespace(ch : number) {
return (ch >= 0 && ch <= Uni_MAX_CODEPOINT);
};
function Uni_IsValid1(ch: number) {
return (Uni_InCodespace(ch) && !Uni_IsSurrogate(ch) && !Uni_IsNoncharacter(ch));
}
export function isValidUnicode(start: number, end?: number) {
if (!end) {
// single char
return Uni_IsValid1(start);
} else if (!Uni_IsValid1(end) || !Uni_IsValid1(start) || (end < start)) {
// start or end out of range, or inverted range
return false;
} else if ((start <= Uni_SURROGATE_END) && (end >= Uni_SURROGATE_START)) {
// contains some of the surrogate range
return false;
} else if ((start <= Uni_FD_NONCHARACTER_END) && (end >= Uni_FD_NONCHARACTER_START)) {
// contains some of the noncharacter range
return false;
} else if ((start & Uni_PLANE_MASK) != (end & Uni_PLANE_MASK)) {
// start and end are on different planes, meaning that the U+__FFFE/U+__FFFF noncharacters
// are contained.
// As a reminder, we already checked that start/end are themselves valid,
// so we know that 'end' is not on a noncharacter at end of plane.
return false;
} else {
return true;
}
}
export function isPUA(ch: number) {
return ((ch >= Uni_PUA_00_START && ch <= Uni_PUA_00_END) ||
(ch >= Uni_PUA_15_START && ch <= Uni_PUA_15_END) ||
(ch >= Uni_PUA_16_START && ch <= Uni_PUA_16_END));
}
class BadStringMap extends Map<BadStringType, Set<number>> {
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<number>());
}
this.m.get(type).add(ch);
}
public analyze() : BadStringMap {
if (this.m.size == 0) {
return null;
} else {
return this.m;
}
}
private m = new BadStringMap();
}

View file

@ -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`);
});
});
});

View file

@ -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

View file

@ -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<km_core_option_scope>(action_items->option->scope),
km::core::option opt(static_cast<km_core_option_scope>(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);
}

View file

@ -262,6 +262,10 @@ void km_core_state_imx_deregister_callback(km_core_state *state)
}
bool is_context_valid(km_core_cp const * context, km_core_cp const * cached_context) {
if (context == nullptr || cached_context == nullptr || *cached_context == '\0') {
// If the cached_context is "empty" then it needs updating
return false;
}
km_core_cp const* context_p = context;
while(*context_p) {
context_p++;
@ -355,4 +359,4 @@ km_core_status km_core_state_context_clear(
}
km_core_context_clear(km_core_state_context(state));
return KM_CORE_STATUS_OK;
}
}

View file

@ -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);

View file

@ -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 &)
{

View file

@ -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)

View file

@ -258,6 +258,27 @@ void test_context_set_if_needed_different_context() {
teardown();
}
void test_context_set_if_needed_cached_context_cleared() {
km_core_cp const *application_context = u"This is a test";
km_core_cp const *cached_context = u"";
setup("k_000___null_keyboard.kmx", cached_context);
km_core_state_context_clear(test_state);
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UPDATED);
assert(!is_identical_context(cached_context));
assert(is_identical_context(application_context));
teardown();
}
void test_context_set_if_needed_application_context_empty() {
km_core_cp const *application_context = u"";
km_core_cp const *cached_context = u"This is a test";
setup("k_000___null_keyboard.kmx", cached_context);
assert(km_core_state_context_set_if_needed(test_state, application_context) == KM_CORE_CONTEXT_STATUS_UPDATED);
assert(!is_identical_context(cached_context));
assert(is_identical_context(application_context));
teardown();
}
void test_context_set_if_needed_app_context_is_longer() {
km_core_cp const *application_context = u"Longer This is a test";
km_core_cp const *cached_context = u"This is a test";
@ -319,6 +340,8 @@ void test_context_set_if_needed_cached_context_has_markers() {
void test_context_set_if_needed() {
test_context_set_if_needed_identical_context();
test_context_set_if_needed_different_context();
test_context_set_if_needed_cached_context_cleared();
test_context_set_if_needed_application_context_empty();
test_context_set_if_needed_app_context_is_longer();
test_context_set_if_needed_app_context_is_shorter();
test_context_set_if_needed_cached_context_has_markers();
@ -374,6 +397,7 @@ int main(int argc, char *argv []) {
test_alert();
test_emit_keystroke();
test_invalidate_context();
test_persist_opt();
// context -- todo move to another file
test_context_set_if_needed();

View file

@ -240,6 +240,8 @@
#define CHINT_UnreachableRule 0x000010AE
#define CWARN_VirtualKeyInOutput 0x000020AF
#define CERR_BufferOverflow 0x000080C0
#define CERR_Break 0x000080C1

View file

@ -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;
};

View file

@ -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]

View file

@ -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");
});
});

View file

@ -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<boolean> {
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<KMXPlus.KMXPlusFile> {
public async compile(source: LDMLKeyboardXMLSourceFile, postValidate?: boolean): Promise<KMXPlus.KMXPlusFile> {
const sections = this.buildSections(source);
let passed = true;
@ -207,6 +207,15 @@ export class LdmlKeyboardCompiler {
kmx.kmxplus[section.id] = sect as any;
}
// give all sections a chance to postValidate
if (postValidate) {
for(let section of sections) {
if(!section.postValidate(kmx.kmxplus[section.id])) {
passed = false;
}
}
}
return passed ? kmx : null;
}
}

View file

@ -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 = <KMXPlus.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 {

View file

@ -1,5 +1,4 @@
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m } from "@keymanapp/common-types";
import { util, CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m } from "@keymanapp/common-types";
// const SevInfo = CompilerErrorSeverity.Info | CompilerErrorNamespace.LdmlKeyboardCompiler;
const SevHint = CompilerErrorSeverity.Hint | CompilerErrorNamespace.LdmlKeyboardCompiler;
const SevWarn = CompilerErrorSeverity.Warn | CompilerErrorNamespace.LdmlKeyboardCompiler;
@ -146,6 +145,18 @@ export class CompilerMessages {
static Error_DisplayNeedsToOrId = (o:{output?: string, keyId?: string}) =>
m(this.ERROR_DisplayNeedsToOrId, `display ${CompilerMessages.outputOrKeyId(o)} needs output= or keyId=, but not both`);
static ERROR_DisplayNeedsToOrId = SevError | 0x0022;
static Hint_PUACharacters = (o: { count: number, lowestCh: number }) =>
m(this.HINT_PUACharacters, `File contains ${o.count} PUA character(s), including ${util.describeCodepoint(o.lowestCh)}`);
static HINT_PUACharacters = SevHint | 0x0023;
static Warn_UnassignedCharacters = (o: { count: number, lowestCh: number }) =>
m(this.WARN_UnassignedCharacters, `File contains ${o.count} unassigned character(s), including ${util.describeCodepoint(o.lowestCh)}`);
static WARN_UnassignedCharacters = SevWarn | 0x0024;
static Error_IllegalCharacters = (o: { count: number, lowestCh: number }) =>
m(this.ERROR_IllegalCharacters, `File contains ${o.count} illegal character(s), including ${util.describeCodepoint(o.lowestCh)}`);
static ERROR_IllegalCharacters = SevError | 0x0025;
}

View file

@ -1,8 +1,9 @@
import { LDMLKeyboard, KMXPlus, CompilerCallbacks } from "@keymanapp/common-types";
import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants';
/* istanbul ignore next */
export class SectionCompiler {
/** newable interface to SectionCompiler c'tor */
export type SectionCompilerNew = new (source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) => SectionCompiler;
export abstract class SectionCompiler {
protected readonly keyboard3: LDMLKeyboard.LKKeyboard;
protected readonly callbacks: CompilerCallbacks;
@ -11,19 +12,35 @@ export class SectionCompiler {
this.callbacks = callbacks;
}
/* c8 ignore next 11 */
public get id(): SectionIdent {
throw Error(`Internal Error: id() not implemented`);
}
public compile(sections: KMXPlus.DependencySections): KMXPlus.Section {
throw Error(`Internal Error: compile() not implemented`);
}
public abstract get id(): SectionIdent;
/**
* This is called before compile.
* @returns false if this compiler failed to validate.
*/
public validate(): boolean {
return true;
}
/**
* Perform the compilation for this section, returning the correct Section subclass
* object.
*
* @param sections any declared dependency sections per dependencies()
*/
public abstract compile(sections: KMXPlus.DependencySections): KMXPlus.Section;
/**
* This is called after all other compile phases have completed,
* when being called by validate(), and provides an
* opportunity for late error reporting, for example for invalid strings.
* @param section the compiled section, if any.
* @returns false if validate fails
*/
public postValidate(section?: KMXPlus.Section): boolean {
return true;
}
/**
* Get the dependencies for this compiler.
* @returns set of dependent sections

View file

@ -19,7 +19,7 @@ import { MarkerTracker, MarkerUse } from "./marker-tracker.js";
type TransformCompilerType = 'simple' | 'backspace';
export class TransformCompiler<T extends TransformCompilerType, TranBase extends Tran> extends SectionCompiler {
export abstract class TransformCompiler<T extends TransformCompilerType, TranBase extends Tran> extends SectionCompiler {
static validateMarkers(keyboard: LDMLKeyboard.LKKeyboard, mt : MarkerTracker): boolean {
keyboard?.transforms?.forEach(transforms =>

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
@@keys: [K_Q][K_W][K_Q]
@@expected: \u0127\u1790\u17B6\u0127
-->
<!DOCTYPE keyboard3 SYSTEM "../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboard3.dtd">
<keyboard3 locale="mt" conformsTo="techpreview">
<version number="1.0.0" />
<info author="srl295" indicator="🙀" layout="qwerty" name="TestKbd"/>
<displays>
<!-- values chosen to reuse strings -->
<display output="a" display="^" />
<display keyId="\u{E020}" display="^e" />
<displayOptions baseCharacter="e" />
</displays>
<keys>
<!-- Note: implied keys here: 'a' and 'e' -->
<key id="hmaqtugha" output="hhh" longPressKeyIds="a e" />
</keys>
<layers formId="us" minDeviceWidth="123">
<layer id="zz">
<row keys="hmaqtugha" />
</layer>
</layers>
<variables>
<string id="a" value="\m{a}"/>
<string id="vst" value="pua:\u{E010}"/>
<set id="vse" value="a b"/>
<unicodeSet id="vus" value="[abc]"/>
</variables>
<transforms type="simple">
<transformGroup>
<transform from="^a" to="q" />
<transform from="a" to="\m{a}" />
</transformGroup>
<transformGroup>
<!-- delete that marker -->
<transform from="\m{a}\u{E020}" />
</transformGroup>
<transformGroup>
<!-- Northern Thai example from spec -->
<reorder before="\u{1A6B}" from="\u{1A60}[\u1A75-\u1A79]\u{1A45}" order="10 55 10" />
</transformGroup>
</transforms>
<transforms type="backspace">
<transformGroup>
<transform from="^e" />
</transformGroup>
</transforms>
</keyboard3>

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
@@keys: [K_Q][K_W][K_Q]
@@expected: \u0127\u1790\u17B6\u0127
-->
<!DOCTYPE keyboard3 SYSTEM "../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboard3.dtd">
<keyboard3 locale="mt" conformsTo="techpreview">
<version number="1.0.0" />
<info author="srl295" indicator="🙀" layout="qwerty" name="TestKbd"/>
<displays>
<!-- values chosen to reuse strings -->
<display output="a" display="^" />
<display keyId="e" display="^e" />
<displayOptions baseCharacter="e" />
</displays>
<keys>
<!-- Note: implied keys here: 'a' and 'e' -->
<key id="hmaqtugha" output="h\u{FDD0}" longPressKeyIds="a e" />
<key id="that" output="&#xFFFF;" /> <!-- illegal NCR (single char)-->
</keys>
<layers formId="us" minDeviceWidth="123">
<layer id="&#xFFFE;">
<row keys="hmaqtugha that" />
</layer>
</layers>
<variables>
<string id="a" value="\m{a}"/>
<string id="vst" value="abc pua:\u{E010}"/>
<set id="vse" value="a b \u{04FFFE} \u{5FFFF}"/>
<unicodeSet id="vus" value="[abc]"/>
</variables>
<transforms type="simple">
<transformGroup>
<transform from="^a" to="\u{FDD0}" />
<transform from="a" to="\m{a}\u{E020}" /> <!-- another PUA, with a marker -->
</transformGroup>
<transformGroup>
<!-- delete that marker -->
<transform from="\m{a}" />
</transformGroup>
<transformGroup>
<!-- Northern Thai example from spec -->
<reorder before="\u{1A6B}" from="\u{1A60}[\u1A75-\u1A79]\u{1A45}" order="10 55 10" />
</transformGroup>
</transforms>
<transforms type="backspace">
<transformGroup>
<transform from="^e" />
</transformGroup>
</transforms>
</keyboard3>

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
@@keys: [K_Q][K_W][K_Q]
@@expected: \u0127\u1790\u17B6\u0127
-->
<!DOCTYPE keyboard3 SYSTEM "../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboard3.dtd">
<keyboard3 locale="mt" conformsTo="techpreview">
<version number="1.0.0" />
<info author="srl295" indicator="🙀" layout="qwerty" name="TestKbd"/>
<displays>
<!-- values chosen to reuse strings -->
<display output="a" display="^" />
<display keyId="\u{E020}" display="^e" />
<displayOptions baseCharacter="e" />
</displays>
<keys>
<!-- unassigned char (as of this writing) -->
<key id="hmaqtugha" output="\u{CFFFD}" longPressKeyIds="a e" />
</keys>
<layers formId="us" minDeviceWidth="123">
<layer id="zz">
<row keys="hmaqtugha" />
</layer>
</layers>
<variables>
<string id="a" value="\m{a}"/>
<string id="vst" value="pua:\u{E010}"/>
<set id="vse" value="a b"/>
<unicodeSet id="vus" value="[abc]"/>
</variables>
<transforms type="simple">
<transformGroup>
<transform from="^a" to="q" />
<transform from="a" to="\m{a}" />
</transformGroup>
<transformGroup>
<!-- delete that marker -->
<transform from="\m{a}\u{E020}" />
</transformGroup>
<transformGroup>
<!-- Northern Thai example from spec -->
<reorder before="\u{1A6B}" from="\u{1A60}[\u1A75-\u1A79]\u{1A45}" order="10 55 10" />
</transformGroup>
</transforms>
<transforms type="backspace">
<transformGroup>
<transform from="^e" />
</transformGroup>
</transforms>
</keyboard3>

View file

@ -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<Section> {
export async function loadSectionFixture(compilerClass: SectionCompilerNew, filename: string, callbacks: TestCompilerCallbacks, dependencies?: SectionCompilerNew[], postValidateFail?: boolean): Promise<Section> {
callbacks.messages = [];
const inputFilename = makePathToFixture(filename);
const data = callbacks.loadFile(inputFilename);
@ -83,13 +83,16 @@ export async function loadSectionFixture(compilerClass: typeof SectionCompiler,
compiler.dependencies.forEach(dep => assert.ok(sections[dep],
`Required dependency '${dep}' for '${compiler.id}' was not supplied: Check the 'dependencies' argument to loadSectionFixture or testCompilationCases`));
return compiler.compile(sections);
const section = await compiler.compile(sections);
const postValidate = compiler.postValidate(section);
assert.equal(postValidate, !postValidateFail, `expected postValidate() to return ${!postValidateFail}`);
return section;
}
/**
* Recursively load dependencies. Normally they are loaded in SECTION_COMPILERS order
*/
async function loadDepsFor(sections: DependencySections, parentCompiler: SectionCompiler, source: LDMLKeyboardXMLSourceFile, callbacks: TestCompilerCallbacks, dependencies?: typeof SectionCompiler[]) {
async function loadDepsFor(sections: DependencySections, parentCompiler: SectionCompiler, source: LDMLKeyboardXMLSourceFile, callbacks: TestCompilerCallbacks, dependencies?: SectionCompilerNew[]) {
const parentId = parentCompiler.id;
if (!dependencies) {
// default dependencies
@ -115,18 +118,29 @@ export function loadTestdata(inputFilename: string, options: LdmlCompilerOptions
return source;
}
export async function compileKeyboard(inputFilename: string, options: LdmlCompilerOptions): Promise<KMXPlusFile> {
export async function compileKeyboard(inputFilename: string, options: LdmlCompilerOptions, validateMessages?: CompilerEvent[], expectFailValidate?: boolean, compileMessages?: CompilerEvent[]): Promise<KMXPlusFile> {
const k = new LdmlKeyboardCompiler(compilerTestCallbacks, options);
const source = k.load(inputFilename);
checkMessages();
assert.isNotNull(source, 'k.load should not have returned null');
const valid = k.validate(source);
checkMessages();
assert.isTrue(valid, 'k.validate should not have failed');
const valid = await k.validate(source);
if (validateMessages) {
assert.sameDeepMembers(compilerTestCallbacks.messages, validateMessages, "validation messages mismatch");
assert.notEqual(valid, expectFailValidate, 'validation failure');
} else {
checkMessages();
assert.isTrue(valid, 'k.validate should not have failed');
}
if (!valid) return null; // get out, if the above asserts didn't get us out.
const kmx = await k.compile(source);
checkMessages();
if (compileMessages) {
assert.sameDeepMembers(compilerTestCallbacks.messages, compileMessages, "compiler messages mismatch");
} else {
checkMessages();
}
assert.isNotNull(kmx, 'k.compile should not have returned null');
// In order for the KMX file to be loaded by non-KMXPlus components, it is helpful
@ -136,13 +150,13 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil
return kmx;
}
export function compileVisualKeyboard(inputFilename: string, options: LdmlCompilerOptions): VisualKeyboard.VisualKeyboard {
export async function compileVisualKeyboard(inputFilename: string, options: LdmlCompilerOptions): Promise<VisualKeyboard.VisualKeyboard> {
const k = new LdmlKeyboardCompiler(compilerTestCallbacks, options);
const source = k.load(inputFilename);
checkMessages();
assert.isNotNull(source, 'k.load should not have returned null');
const valid = k.validate(source);
const valid = await k.validate(source);
checkMessages();
assert.isTrue(valid, 'k.validate should not have failed');
@ -184,7 +198,11 @@ export interface CompilationCase {
/**
* Optional dependent sections to load. Will be strs+list+elem if falsy.
*/
dependencies?: (typeof SectionCompiler)[];
dependencies?: (SectionCompilerNew)[];
/**
* Optional, if true, postValidate() must return false. (must be != postValidate())
*/
postValidateFail?: boolean;
}
/**
@ -193,7 +211,7 @@ export interface CompilationCase {
* @param compiler argument to loadSectionFixture()
* @param callbacks argument to loadSectionFixture()
*/
export function testCompilationCases(compiler: typeof SectionCompiler, cases : CompilationCase[], dependencies?: (typeof SectionCompiler)[]) {
export function testCompilationCases(compiler: SectionCompilerNew, cases : CompilationCase[], dependencies?: (SectionCompilerNew)[]) {
// we need our own callbacks rather than using the global so messages don't get mixed
const callbacks = new TestCompilerCallbacks();
for (let testcase of cases) {

View file

@ -4,10 +4,15 @@ import hextobin from '@keymanapp/hextobin';
import { KMXBuilder } from '@keymanapp/common-types';
import {checkMessages, compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture} from './helpers/index.js';
import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js';
import { CompilerMessages } from '../src/compiler/messages.js';
describe('compiler-tests', function() {
this.slow(500); // 0.5 sec -- json schema validation takes a while
before(function() {
compilerTestCallbacks.clear();
});
it('should-build-fixtures', async function() {
// Let's build basic.xml
// It should match basic.kmx (built from basic.txt)
@ -61,4 +66,50 @@ describe('compiler-tests', function() {
const source = k.load(filename);
assert.notOk(source, `Trying to loadTestData(${filename})`);
});
it('should fail on illegal chars', async function() {
const inputFilename = makePathToFixture('sections/strs/invalid-illegal.xml');
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
// validation messages
CompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: 0xFDD0 }),
CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
],
true, // validation should fail
[
// compiler messages (not reached, we've already failed)
]);
assert.isNull(kmx); // should fail post-validate
});
it('should hint on pua chars', async function() {
const inputFilename = makePathToFixture('sections/strs/hint-pua.xml');
// Compile the keyboard
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
// validation messages
CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
],
false, // validation should pass
[
// same messages
CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
]);
assert.isNotNull(kmx);
});
it.skip('should warn on unassigned chars', async function() {
// unassigned not implemented yet
const inputFilename = makePathToFixture('sections/strs/warn-unassigned.xml');
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
// validation messages
CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
CompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }),
],
false, // validation should pass
[
// same messages
CompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
CompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }),
]);
assert.isNotNull(kmx);
});
});

View file

@ -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');

View file

@ -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<Uint8Array>(code, expected);
});
});
});

View file

@ -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

View file

@ -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"
},
{

View file

@ -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 }
};

View file

@ -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

View file

@ -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;

View file

@ -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'];

View file

@ -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

View file

@ -83,6 +83,7 @@
<xsl:call-template name="file">
<xsl:with-param name="file_description"></xsl:with-param>
<xsl:with-param name="file_has_details">false</xsl:with-param>
<xsl:with-param name="file_relative_path">true</xsl:with-param>
</xsl:call-template>
</xsl:for-each>
</div>

View file

@ -76,6 +76,7 @@
<xsl:param name="file_description" />
<xsl:param name="file_has_details" />
<xsl:param name="file_has_no_options" />
<xsl:param name="file_relative_path" />
<span tabindex="1" class="file" onmousedown="javascript:this.focus();">
<xsl:attribute name="id">file<xsl:value-of select="ID"/></xsl:attribute>
@ -103,7 +104,10 @@
<div class="filename">
<a tabindex="-1">
<xsl:attribute name="href">keyman:editfile?id=<xsl:value-of select="ID"/></xsl:attribute>
<xsl:value-of select="Filename" />
<xsl:choose>
<xsl:when test="$file_relative_path = 'true'"><xsl:value-of select="Filepath" /></xsl:when>
<xsl:otherwise><xsl:value-of select="Filename" /></xsl:otherwise>
</xsl:choose>
</a>
</div>
<div class="filedescription"><xsl:value-of select="$file_description"/></div>

View file

@ -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 <eb1@sil.org> Wed, 22 Nov 2023 15:24:59 +0100
keyman (16.0.141-1) unstable; urgency=medium
* Work around mips64el build failure (#1041499)

View file

@ -88,15 +88,21 @@ export default class KeyboardInterface<ContextManagerType extends ContextManager
//
// The mobile apps typically have fully-preconfigured paths, but Developer's
// test-host page does not.
const pathConfig = this.engine.config.paths;
const stub = new KeyboardStub(Pstub, pathConfig.keyboards, pathConfig.fonts);
if(this.engine.keyboardRequisitioner?.cache.findMatchingStub(stub)) {
return 1;
}
const buildStub = () => {
const pathConfig = this.engine.config.paths;
return new KeyboardStub(Pstub, pathConfig.keyboards, pathConfig.fonts);
};
if(!this.engine.config.deferForInitialization.isResolved) {
this.engine.config.deferForInitialization.then(() => this.engine.keyboardRequisitioner.cache.addStub(stub));
// pathConfig is not ready until KMW initializes, which prevents proper stub-building.
this.engine.config.deferForInitialization.then(() => this.engine.keyboardRequisitioner.cache.addStub(buildStub()));
} else {
const stub = buildStub();
if(this.engine.keyboardRequisitioner?.cache.findMatchingStub(stub)) {
return 1;
}
this.engine.keyboardRequisitioner.cache.addStub(stub);
}

View file

@ -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;
}
}
}

View file

@ -692,6 +692,7 @@ export default abstract class OSKView
private loadActiveKeyboard() {
this.setBoxStyling();
// Do not erase / 'shutdown' the banner-controller; we simply re-use its elements.
if(this.vkbd) {
this.vkbd.shutdown();
}
@ -1140,6 +1141,8 @@ export default abstract class OSKView
this.kbdStyleSheetManager.unlinkAll();
this.uiStyleSheetManager.unlinkAll();
this.bannerController.shutdown();
}
/**

View file

@ -1,6 +1,7 @@
// Page-global variable definitions.
{
var inputCounter = 0;
var kmw = keyman;
}
function generateDiagnosticDiv(elem) {

View file

@ -19,7 +19,7 @@
</style>
</head>
<body>
<h1>KeymanWeb 17 Testing</h1>
<h2><a href="./unminified.html">Test unminified Keymanweb</a></h2>
<h2><a href="./unminified - manual.html">Test unminified Keymanweb in manual-attachment mode.</a></h2>
@ -27,7 +27,7 @@
<h2><a href="./desktop-ui/index.html">Desktop UI module testing</a></h2>
<h2><a href="./build-visual-keyboard/index.html">Tests keyboard documentation rendering.</a></h2>
<h2><a href="./gesture-recognizer/index.html">Stand-alone gesture recognition</a></h2>
<h1>Miscellaneous tests for smaller features</h1>
<h2><a href="./chirality/index.html">Chirality testing/bootstrapping</a><h2>
<h2><a href="./options-with-save/index.html">Tests option/variable store functionality</a></h2>
@ -44,7 +44,7 @@
<h2><a href="./issue29/index.html">Test desktop MutationObserver functionality</a></h2>
<h2><a href="./issue62/index.html">Light test for touch-based MutationObserver functionality</a></h2>
<h2><a href="./issue63/index.html">Test/stress-test touch-based MutationObserver functionality</a></h2>
<h1>Issue Testing</h1>
<h2><a href="./empty-row/index.html">Tests OSK handling of empty rows</a></h2>
<h2><a href="./keyboard-errors/index.html">Tests keyboard error-handling functionality</a></h2>
@ -72,7 +72,7 @@
<h2><a href="./issue6005/index.html">Tests predictive text & other handling of rule matching when the final rule group does not match (#6005)</a></h2>
<h2><a href="./default-subkey/index.html">Tests handling of new default-subkey feature (#9430)</a></h2>
<h2><a href="./issue9469/index.html">Test special characters rendering with keymanweb-osk.ttf (#9469)</a></h2>
<h2><a href="./text_selection_tests_9073/index.html">Test text selection (#9073)</a></h2>
<h1>Other</h1>
<h2><a href="./regression-tests/index.html">Keystroke processing regression test engine.</a></h2>
<hr>

View file

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<!-- Set the viewport width to match phone and tablet device widths -->
<meta name="viewport" content="width=device-width,user-scalable=no" />
<title>KeymanWeb #9073</title>
<!-- Your page CSS -->
<style type='text/css'>
body {font-family: Tahoma,helvetica;}
h3 {font-size: 1em;font-weight:normal;color: darkred; margin-bottom: 4px}
.test {font-size: 1.5em; width:80%; min-height:30px; border: 1px solid gray;}
#KeymanWebControl {width:50%;min-width:600px;}
</style>
<script src="../../../../../build/publish/debug/keymanweb.js" type="application/javascript"></script>
<script src="../../../../../build/publish/debug/kmwuitoggle.js"></script>
<!-- Initialization: set paths to keyboards, resources and fonts as required -->
<script>
(function(kmw) {
kmw.init({
attachType:'auto'
}).then(function() {
kmw.addKeyboards({
id:'text_selection_tests_keyboard_9073',
name:'Text selection test cases (#9073)',
languages:{
id:'en',
name:'English'
},
filename:'./text_selection_tests_keyboard_9073.js',
displayName: 'Text selection test cases (#9073)'
});
});
})(keyman);
</script>
</head>
<!-- Sample page HTML -->
<body>
<h2>Text Selection Test Cases (#9073)</h2>
<div>
<!--
The following DIV is used to position the Button or Toolbar User Interfaces on the page.
If omitted, those User Interfaces will appear at the top of the document body.
(It is ignored by other User Interfaces.)
-->
<div id='KeymanWebControl'></div>
<textarea id='ta1' class='test' placeholder='Type here'></textarea>
</div>
<hr />
<h3><a href="../index.html">Return to testing home page</a></h3>
</body>
<!--
*** DEVELOPER NOTE -- FIREFOX CONFIGURATION FOR TESTING ***
*
* If the URL bar starts with <b>file://</b>, Firefox may not load the font used
* to display the special characters used in the On-Screen Keyboard.
*
* To work around this Firefox bug, navigate to <b>about:config</b>
* and set <b>security.fileuri.strict_origin_policy</b> to <b>false</b>
* while testing.
*
* Firefox resolves website-based CSS URI references correctly without needing
* any configuration change, so this change should only be made for file-based testing.
*
***
-->
</html>

View file

@ -0,0 +1,641 @@
if(typeof keyman === 'undefined') {
console.log('Keyboard requires KeymanWeb 10.0 or later');
if(typeof tavultesoft !== 'undefined') tavultesoft.keymanweb.util.alert("This keyboard requires KeymanWeb 10.0 or later");
} else {
KeymanWeb.KR(new Keyboard_text_selection_tests_keyboard_9073());
}
function Keyboard_text_selection_tests_keyboard_9073()
{
var modCodes = keyman.osk.modifierCodes;
var keyCodes = keyman.osk.keyCodes;
this._v=(typeof keyman!="undefined"&&typeof keyman.version=="string")?parseInt(keyman.version,10):9;
this.KI="Keyboard_text_selection_tests_keyboard_9073";
this.KN="Text Selection Tests Keyboard";
this.KMINVER="10.0";
this.KV={F:' 1em "Arial"',K102:0};
this.KV.KLS={
"default": ["dk(1)","1","2","3","4","5","6","7","8","9","0","-","=","","","","q","w","e","r","t","y","u","i","o","p","[","]","\\","","","","a","s","d","f","g","h","j","k","l",";","'","","","","","","\\","z","x","c","v","b","n","m",",",".","/","","","","","",""],
"shift": ["~","!","@","#","$","%","^","&","*","(",")","_","+","","","","Q","W","E","R","T","Y","U","I","O","P","{","}","|","","","","A","S","D","F","G","H","J","K","L",":","\"","","","","","","|","Z","X","C","V","B","N","M","<",">","?","","","","","",""]
};
this.KV.BK=(function(x){
var
empty=Array.apply(null, Array(65)).map(String.prototype.valueOf,""),
result=[], v, i,
modifiers=['default','shift','ctrl','shift-ctrl','alt','shift-alt','ctrl-alt','shift-ctrl-alt'];
for(i=modifiers.length-1;i>=0;i--) {
v = x[modifiers[i]];
if(v || result.length > 0) {
result=(v ? v : empty).slice().concat(result);
}
}
return result;
})(this.KV.KLS);
this.KDU=0;
this.KH='';
this.KM=0;
this.KBVER="1.0";
this.KMBM=modCodes.SHIFT /* 0x0010 */;
this.KVKL={
"tablet": {
"displayUnderlying": false,
"layer": [
{
"id": "default",
"row": [
{
"id": "1",
"key": [
{
"nextlayer": "shift",
"id": "K_1",
"text": "1"
},
{
"id": "K_2",
"text": "2"
},
{
"id": "K_3",
"text": "3"
},
{
"id": "K_4",
"text": "4"
},
{
"id": "K_5",
"text": "5"
},
{
"id": "K_6",
"text": "6"
},
{
"id": "K_7",
"text": "7"
},
{
"id": "K_8",
"text": "8"
},
{
"id": "K_9",
"text": "9"
},
{
"id": "K_0",
"text": "0"
},
{
"id": "K_HYPHEN",
"text": "-"
},
{
"id": "K_EQUAL",
"text": "="
},
{
"width": "100",
"id": "K_BKSP",
"sp": "1",
"text": "*BkSp*"
}
]
},
{
"id": "2",
"key": [
{
"id": "K_Q",
"pad": "75",
"text": "q"
},
{
"id": "K_W",
"text": "w"
},
{
"id": "K_E",
"text": "e"
},
{
"id": "K_R",
"text": "r"
},
{
"id": "K_T",
"text": "t"
},
{
"id": "K_Y",
"text": "y"
},
{
"id": "K_U",
"text": "u"
},
{
"id": "K_I",
"text": "i"
},
{
"id": "K_O",
"text": "o"
},
{
"id": "K_P",
"text": "p"
},
{
"id": "K_LBRKT",
"text": "["
},
{
"id": "K_RBRKT",
"text": "]"
},
{
"width": "10",
"id": "T_new_136",
"sp": "10"
}
]
},
{
"id": "3",
"key": [
{
"id": "K_BKQUOTE",
"text": "dk(1)"
},
{
"id": "K_A",
"text": "a"
},
{
"id": "K_S",
"text": "s"
},
{
"id": "K_D",
"text": "d"
},
{
"id": "K_F",
"text": "f"
},
{
"id": "K_G",
"text": "g"
},
{
"id": "K_H",
"text": "h"
},
{
"id": "K_J",
"text": "j"
},
{
"id": "K_K",
"text": "k"
},
{
"id": "K_L",
"text": "l"
},
{
"id": "K_COLON",
"text": ";"
},
{
"id": "K_QUOTE",
"text": "'"
},
{
"id": "K_BKSLASH",
"text": "\\"
}
]
},
{
"id": "4",
"key": [
{
"nextlayer": "shift",
"width": "160",
"id": "K_SHIFT",
"sp": "1",
"text": "*Shift*"
},
{
"id": "K_oE2",
"text": "\\"
},
{
"id": "K_Z",
"text": "z"
},
{
"id": "K_X",
"text": "x"
},
{
"id": "K_C",
"text": "c"
},
{
"id": "K_V",
"text": "v"
},
{
"id": "K_B",
"text": "b"
},
{
"id": "K_N",
"text": "n"
},
{
"id": "K_M",
"text": "m"
},
{
"id": "K_COMMA",
"text": ","
},
{
"id": "K_PERIOD",
"text": "."
},
{
"id": "K_SLASH",
"text": "/"
},
{
"width": "10",
"id": "T_new_162",
"sp": "10"
}
]
},
{
"id": "5",
"key": [
{
"width": "140",
"id": "K_LOPT",
"sp": "1",
"text": "*Menu*"
},
{
"width": "930",
"id": "K_SPACE"
},
{
"width": "145",
"id": "K_ENTER",
"sp": "1",
"text": "*Enter*"
}
]
}
]
},
{
"id": "shift",
"row": [
{
"id": "1",
"key": [
{
"id": "K_1",
"text": "!"
},
{
"id": "K_2",
"text": "@"
},
{
"id": "K_3",
"text": "#"
},
{
"id": "K_4",
"text": "$"
},
{
"id": "K_5",
"text": "%"
},
{
"id": "K_6",
"text": "^"
},
{
"id": "K_7",
"text": "&"
},
{
"id": "K_8",
"text": "*"
},
{
"id": "K_9",
"text": "("
},
{
"id": "K_0",
"text": ")"
},
{
"id": "K_HYPHEN",
"text": "_"
},
{
"id": "K_EQUAL",
"text": "+"
},
{
"width": "100",
"id": "K_BKSP",
"sp": "1",
"text": "*BkSp*"
}
]
},
{
"id": "2",
"key": [
{
"id": "K_Q",
"pad": "75",
"text": "Q"
},
{
"id": "K_W",
"text": "W"
},
{
"id": "K_E",
"text": "E"
},
{
"id": "K_R",
"text": "R"
},
{
"id": "K_T",
"text": "T"
},
{
"id": "K_Y",
"text": "Y"
},
{
"id": "K_U",
"text": "U"
},
{
"id": "K_I",
"text": "I"
},
{
"id": "K_O",
"text": "O"
},
{
"id": "K_P",
"text": "P"
},
{
"id": "K_LBRKT",
"text": "{"
},
{
"id": "K_RBRKT",
"text": "}"
},
{
"width": "10",
"id": "T_new_246",
"sp": "10"
}
]
},
{
"id": "3",
"key": [
{
"id": "K_BKQUOTE",
"text": "~"
},
{
"id": "K_A",
"text": "A"
},
{
"id": "K_S",
"text": "S"
},
{
"id": "K_D",
"text": "D"
},
{
"id": "K_F",
"text": "F"
},
{
"id": "K_G",
"text": "G"
},
{
"id": "K_H",
"text": "H"
},
{
"id": "K_J",
"text": "J"
},
{
"id": "K_K",
"text": "K"
},
{
"id": "K_L",
"text": "L"
},
{
"id": "K_COLON",
"text": ":"
},
{
"id": "K_QUOTE",
"text": "\""
},
{
"id": "K_BKSLASH",
"text": "|"
}
]
},
{
"id": "4",
"key": [
{
"nextlayer": "default",
"width": "160",
"id": "K_SHIFT",
"sp": "1",
"text": "*Shift*"
},
{
"id": "K_oE2",
"text": "|"
},
{
"id": "K_Z",
"text": "Z"
},
{
"id": "K_X",
"text": "X"
},
{
"id": "K_C",
"text": "C"
},
{
"id": "K_V",
"text": "V"
},
{
"id": "K_B",
"text": "B"
},
{
"id": "K_N",
"text": "N"
},
{
"id": "K_M",
"text": "M"
},
{
"id": "K_COMMA",
"text": "<"
},
{
"id": "K_PERIOD",
"text": ">"
},
{
"id": "K_SLASH",
"text": "?"
},
{
"width": "10",
"id": "T_new_272",
"sp": "10"
}
]
},
{
"id": "5",
"key": [
{
"width": "140",
"id": "K_LOPT",
"sp": "1",
"text": "*Menu*"
},
{
"width": "930",
"id": "K_SPACE"
},
{
"width": "145",
"id": "K_ENTER",
"sp": "1",
"text": "*Enter*"
}
]
}
]
}
]
}
}
;
this.KVER="16.0.142.0";
this.KVS=[];
this.gs=function(t,e) {
return this.g_main_0(t,e);
};
this.gs=function(t,e) {
return this.g_main_0(t,e);
};
this.g_main_0=function(t,e) {
var k=KeymanWeb,r=0,m=0;
if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_BKSP /* 0x08 */)) {
if(k.KFCM(3,t,['a',{t:'d',d:0},'b'])){
r=m=1; // Line 23
k.KDC(3,t);
k.KO(-1,t,"ok1");
}
else if(k.KFCM(2,t,['a','b'])){
r=m=1; // Line 24
k.KDC(2,t);
k.KO(-1,t,"fail1");
}
else if(k.KFCM(2,t,['a',{t:'d',d:0}])){
r=m=1; // Line 25
k.KDC(2,t);
k.KO(-1,t,"fail2");
}
else if(k.KFCM(1,t,['^'])){
r=m=1; // Line 17
k.KDC(1,t);
k.KO(-1,t,"foo");
}
}
else if(k.KKM(e, modCodes.SHIFT | modCodes.VIRTUAL_KEY /* 0x4010 */, keyCodes.K_A /* 0x41 */)) {
if(k.KFCM(1,t,['^'])){
r=m=1; // Line 16
k.KDC(1,t);
k.KO(-1,t,"Â");
}
}
else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_BKQUOTE /* 0xC0 */)) {
if(1){
r=m=1; // Line 19
k.KDC(0,t);
k.KDO(-1,t,0);
}
}
else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_A /* 0x41 */)) {
if(k.KFCM(1,t,['^'])){
r=m=1; // Line 15
k.KDC(1,t);
k.KO(-1,t,"â");
}
}
else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_O /* 0x4F */)) {
if(k.KFCM(1,t,[{t:'d',d:0}])){
r=m=1; // Line 26
k.KDC(1,t);
k.KO(-1,t,"ok3");
}
}
else if(k.KKM(e, modCodes.VIRTUAL_KEY /* 0x4000 */, keyCodes.K_T /* 0x54 */)) {
if(1){
r=m=1; // Line 21
k.KDC(0,t);
k.KO(-1,t,"\t");
}
}
return r;
};
}

View file

@ -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<WCHAR> Uni_UTF32ToSurrogate1(km_core_context_it->character);
buf[idx++] = static_cast<WCHAR> 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<WCHAR>(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;
}

View file

@ -0,0 +1,107 @@
#ifndef _APPCONTEXT_H
#define _APPCONTEXT_H
/*
Name: appcontext
Copyright: Copyright (C) SIL International.
Documentation:
Description:
Create Date: 23 Nov 2023
Modified Date: 23 Nov 2023
Authors: rcruickshank
Related Files:
Dependencies:
Bugs:
Todo:
Notes: AppContext is retained to support calldll with the external interface for the 3rd party IMX keyboards
that worked with KMX formatted Context Strings. It is also used once for debug logging the ProcessHook.
History:
*/
class AppContext {
private:
WCHAR CurContext[MAXCONTEXT]; //!< CurContext[0] is furthest from the caret and buffer is null terminated.
int pos;
public:
AppContext();
/**
* Removes a single code point from the end of the CurContext closest to the caret;
* i.e. it will be both code units if a surrogate pair. If it is a deadkey it will
* remove three code points: UC_SENTINEL, CODE_DEADKEY and deadkey value.
*/
void Delete();
/**
* Clears the CurContext and resets the position - pos - index
*/
void Reset();
/**
* Copies the characters in CurContext to supplied buffer.
* If bufsize is reached before the entire context was copied, the buf
* will be truncated to number of valid characters possible with null character
* termination. e.g. it will be one code unit less than bufsize if that would
* have meant splitting a surrogate pair
* @param buf The data buffer to copy current context
* @param bufsize The number of code units ie size of the WCHAR buffer - not the code points
*/
void Get(WCHAR *buf, int bufsize);
/**
* Sets the CurContext to the supplied buf character array and updates the pos index.
*
* @param buf
*/
void Set(const WCHAR *buf);
/**
* Returns a pointer to the character in the current context buffer which
* will have at most n valid xstring units remaining until the null terminating
* character. It will be one code unit less than bufsize if that would
* have meant splitting a surrogate pair or deadkey.
*
* @param n The maximum number of valid xstring units (not code points or code units)
* @return WCHAR* Pointer to the start postion for a buffer of maximum n xstring units
*/
WCHAR *BufMax(int n);
/**
* Returns TRUE if the last xstring unit in the context is a deadkey
*
* @return BOOL
*/
BOOL CharIsDeadkey();
/**
* Returns TRUE if the last xstring unit in the CurContext is a surrogate pair.
* @return BOOL
*/
BOOL CharIsSurrogatePair();
/**
* Returns TRUE if the context is empty
* @return BOOL
*/
BOOL AppContext::IsEmpty();
};
/**
* Convert km_core_context_item array into an kmx char buffer.
* Caller is responsible for freeing the memory.
* The length is restricted to a maximum of MAXCONTEXT length. If the number
* of input km_core_context_items exceeds this length the characters furthest
* from the caret will be truncated.
*
* @param contextItems the input core context array. (km_core_context_item)
* @param [out] outBuf the kmx character array output. caller to free memory.
*
* @return BOOL True if array created successfully
*/
BOOL ContextItemToAppContext(km_core_context_item *contextItems, PWSTR outBuf, DWORD len);
#endif

View file

@ -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 <deadkey_id>
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 */

View file

@ -37,9 +37,6 @@
class AITIP : public AIWin2000Unicode
{
private:
void MergeContextWithCache(PWSTR buf, AppContext *context); // I4262
private:
BOOL useLegacy;
@ -50,20 +47,6 @@ public:
AITIP();
~AITIP();
/**
* Copy the member context
*
* @param[out] savedContext the copied context
*/
void CopyContext(AppContext *savedContext);
/**
* Restore the passed context to the member context
*
* @param savedContext the context to restore
*/
void RestoreContextOnly(AppContext *savedContext);
/* Information functions */
virtual BOOL CanHandleWindow(HWND ahwnd);
@ -71,7 +54,12 @@ public:
/* Context functions */
virtual void ReadContext();
/**
* Reads the current application context upto MAXCONTEXT length into the supplied buffer.
* @param buf The data buffer to copy current application context into, must
* be MAXCONTEXT WCHARs or larger.
*/
virtual BOOL ReadContext(PWSTR buf);
/* Queue and sending functions */

View file

@ -43,16 +43,9 @@
#include "pch.h" // I4128 // I4287
#include "serialkeyeventclient.h"
AIWin2000Unicode::AIWin2000Unicode()
{
context = new AppContext;
}
AIWin2000Unicode::~AIWin2000Unicode()
{
delete context;
AIWin2000Unicode::AIWin2000Unicode() {
}
AIWin2000Unicode::~AIWin2000Unicode(){}
/* Information functions */
@ -68,7 +61,7 @@ BOOL AIWin2000Unicode::HandleWindow(HWND ahwnd)
if(hwnd != ahwnd)
{
hwnd = ahwnd;
context->Reset();
ResetContext();
}
return TRUE;
}
@ -87,33 +80,20 @@ BOOL AIWin2000Unicode::IsUnicode()
/* Context functions */
void AIWin2000Unicode::ReadContext()
{
}
void AIWin2000Unicode::AddContext(WCHAR ch) //I2436
{
context->Add(ch);
BOOL AIWin2000Unicode::ReadContext(PWSTR buf) {
UNREFERENCED_PARAMETER(buf);
// We cannot read any context from legacy apps, so we return a
// failure here -- telling Core to maintain its own cached
// context.
return FALSE;
}
void AIWin2000Unicode::ResetContext()
{
context->Reset();
}
WCHAR *AIWin2000Unicode::ContextBuf(int n)
{
return context->Buf(n);
}
WCHAR *AIWin2000Unicode::ContextBufMax(int n)
{
return context->BufMax(n);
}
void AIWin2000Unicode::SetContext(const WCHAR* buf)
{
return context->Set(buf);
PKEYMAN64THREADDATA _td = ThreadGlobals();
if (_td && _td->lpActiveKeyboard && _td->lpActiveKeyboard->lpCoreKeyboardState) {
km_core_state_context_clear(_td->lpActiveKeyboard->lpCoreKeyboardState);
}
}
BYTE SavedKbdState[256];
@ -126,40 +106,6 @@ BOOL AIWin2000Unicode::SendActions() // I4196
return PostKeys();
}
BOOL AIWin2000Unicode::QueueAction(int ItemType, DWORD dwData)
{
int result = AppIntegration::QueueAction(ItemType, dwData);
//SendDebugMessageFormat(hwnd, sdmAIDefault, 0, "App::QueueAction ItemType=%d dwData=%x", ItemType, dwData);
switch(ItemType)
{
case QIT_VKEYDOWN:
break;
case QIT_DEADKEY:
context->Add(UC_SENTINEL);
context->Add(CODE_DEADKEY);
context->Add((WORD) dwData);
break;
case QIT_CHAR:
context->Add((WORD) dwData);
break;
case QIT_BACK:
if(dwData & BK_BACKSPACE)
while(context->CharIsDeadkey()) context->Delete();
//if(dwData == CODE_DEADKEY) break;
context->Delete();
if(dwData & BK_BACKSPACE)
while(context->CharIsDeadkey()) context->Delete();
break;
}
return result;
}
// I1512 - SendInput with VK_PACKET for greater robustness
BOOL AIWin2000Unicode::PostKeys()

View file

@ -32,15 +32,9 @@ private:
BOOL PostKeys();
protected:
AppContext *context;
public:
AIWin2000Unicode();
~AIWin2000Unicode();
virtual BOOL QueueAction(int ItemType, DWORD dwData);
AIWin2000Unicode();
~AIWin2000Unicode();
/* Information functions */
@ -51,13 +45,9 @@ public:
/* Context functions */
virtual void ReadContext();
virtual BOOL ReadContext(PWSTR buf);
virtual void ResetContext();
virtual void AddContext(WCHAR ch); //I2436
virtual WCHAR *ContextBuf(int n);
virtual WCHAR *ContextBufMax(int n);
virtual void SetContext(const WCHAR* buf);
/* Queue and sending functions */
virtual BOOL SendActions(); // I4196

View file

@ -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 <deadkey_id>
// 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<WCHAR> Uni_UTF32ToSurrogate1(km_core_context_it->character);
buf[idx++] = static_cast<WCHAR> 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<WCHAR>(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;
}

View file

@ -65,103 +65,6 @@ public:
int GetQueueSize() { return QueueSize; }
};
class AppContext
{
private:
WCHAR CurContext[MAXCONTEXT]; //!< CurContext[0] is furthest from the caret and buffer is null terminated.
int pos;
public:
AppContext();
/**
* Copy "source" AppContext to this AppContext
*
* @param source AppContext to copy
*/
void CopyFrom(AppContext *source);
/**
* Add a single code unit to the Current Context. Not necessarily a complete code point
*
* @param Code unit to add
*/
void Add(WCHAR ch);
/**
* Removes a single code point from the end of the CurContext closest to the caret;
* i.e. it will be both code units if a surrogate pair. If it is a deadkey it will
* remove three code points: UC_SENTINEL, CODE_DEADKEY and deadkey value.
*/
void Delete();
/**
* Clears the CurContext and resets the position - pos - index
*/
void Reset();
/**
* Copies the characters in CurContext to supplied buffer.
* If bufsize is reached before the entire context was copied, the buf
* will be truncated to number of valid characters possible with null character
* termination. e.g. it will be one code unit less than bufsize if that would
* have meant splitting a surrogate pair
* @param buf The data buffer to copy current context
* @param bufsize The number of code units ie size of the WCHAR buffer - not the code points
*/
void Get(WCHAR *buf, int bufsize);
/**
* Sets the CurContext to the supplied buf character array and updates the pos index.
*
* @param buf
*/
void Set(const WCHAR *buf);
/**
* Returns a pointer to the character in the current context buffer which
* will have at most n valid xstring units remaining until the null terminating
* character. It will be one code unit less than bufsize if that would
* have meant splitting a surrogate pair or deadkey.
*
* @param n The maximum number of valid xstring units (not code points or code units)
* @return WCHAR* Pointer to the start postion for a buffer of maximum n xstring units
*/
WCHAR *BufMax(int n);
/**
* Returns a pointer to the character in the current context buffer which
* will have n valid xstring units remaining until the the null terminating character.
* OR
* Returns NULL if there are less than n valid xstring units in the current context.
* Background this was historically for performance during rule evaluation, if there
* are not enough characters to compare, don't event attempt the comparison.
*
* @param n The number of valid xstring units (not code points or code units)
* @return KMX_WCHAR* Pointer to the start postion for a buffer of maximum n characters
*/
WCHAR *Buf(int n);
/**
* Returns TRUE if the last xstring unit in the context is a deadkey
*
* @return BOOL
*/
BOOL CharIsDeadkey();
/**
* Returns TRUE if the last xstring unit in the CurContext is a surrogate pair.
* @return BOOL
*/
BOOL CharIsSurrogatePair();
/**
* Returns TRUE if the context is empty
* @return BOOL
*/
BOOL AppContext::IsEmpty();
};
class AppIntegration:public AppActionQueue
{
protected:
@ -180,12 +83,12 @@ public:
virtual BOOL IsUnicode() = 0;
/* Context functions */
virtual void ReadContext() = 0;
/**
* Reads the current application context upto MAXCONTEXT length into the supplied buffer.
* @param buf The data buffer to copy current application context
*/
virtual BOOL ReadContext(PWSTR buf) = 0;
virtual void ResetContext() = 0;
virtual void AddContext(WCHAR ch) = 0; //I2436
virtual WCHAR *ContextBuf(int n) = 0;
virtual WCHAR *ContextBufMax(int n) = 0;
/* Queue and sending functions */
@ -193,30 +96,6 @@ public:
virtual BOOL SendActions() = 0; // I4196
};
/**
* Convert AppContext array into an array of core context items.
* Caller is responsible for freeing the memory.
*
* @param buf appcontext character array
* @param outPtr The ouput array of context items. caller to free memory
* @return BOOL True if array created successfully
*/
BOOL ContextItemsFromAppContext(WCHAR const* buf, km_core_context_item** outPtr);
/**
* Convert km_core_context_item array into an kmx char buffer.
* Caller is responsible for freeing the memory.
* The length is restricted to a maximum of MAXCONTEXT length. If the number
* of input km_core_context_items exceeds this length the characters furthest
* from the caret will be truncated.
*
* @param contextItems the input core context array. (km_core_context_item)
* @param [out] outBuf the kmx character array output. caller to free memory.
*
* @return BOOL True if array created successfully
*/
BOOL ContextItemToAppContext(km_core_context_item *contextItems, PWSTR outBuf, DWORD len);
extern const LPSTR ItemTypes[];
#endif

View file

@ -176,6 +176,7 @@
<ItemGroup>
<ClCompile Include="..\..\global\cpp\kmtip_guids.cpp" />
<ClCompile Include="$(KEYMAN_ROOT)\common\windows\cpp\src\xstring.cpp" />
<ClCompile Include="appcontext.cpp" />
<ClCompile Include="appint\aiTIP.cpp">
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
@ -354,6 +355,7 @@
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="appcontext.h" />
<ClInclude Include="keymanengine.h" />
<ClInclude Include="..\..\..\include\kmtip_guids.h" />
<ClInclude Include="appint\aiTIP.h" />
@ -387,4 +389,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>

View file

@ -138,6 +138,9 @@
<ClCompile Include="CoreEnvironment.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="appcontext.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="KEYMAN32.DEF">
@ -246,6 +249,9 @@
<ClInclude Include="..\..\..\include\kmtip_guids.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="appcontext.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<CustomBuild Include="keyman-debug-etw.man" />

View file

@ -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"

View file

@ -70,23 +70,29 @@
BOOL fOutputKeystroke;
/*char *getcontext()
{
WCHAR buf[128];
static char bufout[128];
PKEYMAN64THREADDATA _td = ThreadGlobals();
if(!_td) return "";
_td->app->GetWindowContext(buf, 128);
WideCharToMultiByte(CP_ACP, 0, buf, -1, bufout, 128, NULL, NULL);
return bufout;
}*/
char *getcontext_debug() {
//return "";
PKEYMAN64THREADDATA _td = ThreadGlobals();
if(!_td) return "";
return Debug_UnicodeString(_td->app->ContextBufMax(128));
if (!_td || !_td->lpActiveKeyboard || !_td->lpActiveKeyboard->lpCoreKeyboardState){
return "";
}
WCHAR buf[(MAXCONTEXT * 3) + 1]; // *3 if every context item was a deadkey
km_core_context_item *citems = nullptr;
if (KM_CORE_STATUS_OK != km_core_context_get(
km_core_state_context(_td->lpActiveKeyboard->lpCoreKeyboardState), &citems)) {
return "";
}
DWORD context_length = (DWORD)km_core_context_item_list_size(citems);
if (!ContextItemToAppContext(citems, buf, context_length)) {
km_core_context_items_dispose(citems);
return "";
}
km_core_context_items_dispose(citems);
return Debug_UnicodeString(buf);
}
/**
@ -98,14 +104,15 @@ char *getcontext_debug() {
static BOOL
Process_Event_Core(PKEYMAN64THREADDATA _td) {
PWSTR contextBuf = _td->app->ContextBufMax(MAXCONTEXT);
km_core_context_item *citems = nullptr;
ContextItemsFromAppContext(contextBuf, &citems);
if (KM_CORE_STATUS_OK != km_core_context_set(km_core_state_context(_td->lpActiveKeyboard->lpCoreKeyboardState), citems)) {
km_core_context_items_dispose(citems);
return FALSE;
WCHAR application_context[MAXCONTEXT];
if (_td->app->ReadContext(application_context)) {
km_core_context_status result;
result = km_core_state_context_set_if_needed(_td->lpActiveKeyboard->lpCoreKeyboardState, reinterpret_cast<const km_core_cp *>(application_context));
if (result == KM_CORE_CONTEXT_STATUS_ERROR || result == KM_CORE_CONTEXT_STATUS_INVALID_ARGUMENT) {
SendDebugMessageFormat(0, sdmGlobal, 0, "Process_Event_Core: km_core_state_context_set_if_needed returned [%d]", result);
}
}
km_core_context_items_dispose(citems);
SendDebugMessageFormat(
0, sdmGlobal, 0, "ProcessEvent: vkey[%d] ShiftState[%d] isDown[%d]", _td->state.vkey,
static_cast<uint16_t>(Globals::get_ShiftState() & (KM_CORE_MODIFIER_MASK_ALL | KM_CORE_MODIFIER_MASK_CAPS)), (uint8_t)_td->state.isDown);
@ -139,8 +146,6 @@ BOOL ProcessHook()
fOutputKeystroke = FALSE; // TODO: 5442 no longer needs to be global once we use core processor
_td->app->ReadContext();
if(_td->state.msg.message == wm_keymankeydown) { // I4827
if (ShouldDebug(sdmKeyboard)) {
SendDebugMessageFormat(_td->state.msg.hwnd, sdmKeyboard, 0, "Key pressed: %s Context '%s'",
@ -212,9 +217,6 @@ BOOL ProcessHook()
_td->app->SetCurrentShiftState(Globals::get_ShiftState());
_td->app->SendActions(); // I4196
}
// output context for debugging
// PWSTR contextBuf = _td->app->ContextBufMax(MAXCONTEXT);
// SendDebugMessageFormat(0, sdmAIDefault, 0, "Kmprocess::ProcessHook After cxt=%s", Debug_UnicodeString(contextBuf, 1));
return !fOutputKeystroke;
}

View file

@ -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) {

View file

@ -177,6 +177,7 @@
<ItemGroup>
<ClCompile Include="..\..\global\cpp\kmtip_guids.cpp" />
<ClCompile Include="$(KEYMAN_ROOT)\common\windows\cpp\src\xstring.cpp" />
<ClCompile Include="..\keyman32\appcontext.cpp" />
<ClCompile Include="..\keyman32\appint\aiTIP.cpp" />
<ClCompile Include="..\keyman32\appint\aiWin2000Unicode.cpp" />
<ClCompile Include="..\keyman32\appint\appint.cpp" />
@ -276,6 +277,7 @@
<ResourceCompile Include="Keyman64.RC" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\keyman32\appcontext.h" />
<ClInclude Include="..\keyman32\keymanengine.h" />
<ClInclude Include="..\..\..\include\kmtip_guids.h" />
<ClInclude Include="..\keyman32\appint\aiTIP.h" />
@ -327,4 +329,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>

View file

@ -38,6 +38,7 @@
<ClCompile Include="..\keyman32\kmprocessactions.cpp" />
<ClCompile Include="..\..\global\cpp\kmtip_guids.cpp" />
<ClCompile Include="..\keyman32\CoreEnvironment.cpp" />
<ClCompile Include="..\keyman32\appcontext.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\keyman32\appint\aiTIP.h" />
@ -72,6 +73,7 @@
<ClInclude Include="..\keyman32\keymanengine.h" />
<ClInclude Include="..\keyman32\kmprocessactions.h" />
<ClInclude Include="..\..\..\include\kmtip_guids.h" />
<ClInclude Include="..\keyman32\appcontext.h" />
</ItemGroup>
<ItemGroup>
<CustomBuild Include="..\keyman32\keyman-debug-etw.man" />