chore(developer): Merge branch 'beta' into test/developer/keyboard-info-compiler-unit-tests-2

This commit is contained in:
Dr Mark C. Sinclair 2024-04-18 09:52:04 +01:00
commit 07522c964b
27 changed files with 379 additions and 211 deletions

View file

@ -1,5 +1,13 @@
# Keyman Version History
## 17.0.309 beta 2024-04-17
* fix(ios): sample build script --debug detection (#10953)
* chore(web): simple layout reflow polish 🪠 (#11237)
* chore(android): enables debugging and inspection of mobile app internal webviews (#11215)
* (#11232)
* (#11238)
## 17.0.308 beta 2024-04-13
* chore(developer): use keyboard3 tag rather than DTD to identify LDML keyboard xml files (#11214)
@ -7,9 +15,9 @@
## 17.0.307 beta 2024-04-12
* fix(common): specify title explicitly when opening PR with hub (#11173)
* refactor(web): better centralizes OSK layout internals to prepare for optimization efforts 🪠 (#11176)
* feat(web): VisualKeyboard layout-reflow optimization 🪠 (#11177)
* change(web): OSK optimization, improved responsiveness 🪠 (#11140)
* refactor(web): better centralizes OSK layout internals to prepare for optimization efforts (#11176)
* feat(web): VisualKeyboard layout-reflow optimization (#11177)
* change(web): OSK optimization, improved responsiveness (#11140)
* (#11216)
## 17.0.306 beta 2024-04-11
@ -20,9 +28,9 @@
## 17.0.305 beta 2024-04-10
* (#11169)
* change(web): merges split async method in gesture engine 🪠 (#11142)
* change(web): merges split async method in gesture engine (#11142)
* fix(web): blocks nextLayer for keys quickly typed when multitapping to new layer when final tap is held (#11189)
* refactor(web): OSK spacebar-label updates now managed by layer object 🪠 (#11175)
* refactor(web): OSK spacebar-label updates now managed by layer object (#11175)
## 17.0.304 beta 2024-04-09
@ -42,14 +50,14 @@
* feat(core): support modifiers=other (#11118)
* chore(core): dx better err message on embedded test vkeys (#11119)
* fix(web): key preview stickiness 🪠 (#10778)
* fix(web): early gesture-match abort when unable to extend existing gestures 🪠 (#10836)
* fix(web): infinite model-match replacement looping 🪠 (#10838)
* fix(web): proper gesture-match sequencing 🪠 (#10840)
* change(web): input-event sequentialization 🪠 (#10843)
* fix(web): proper linkage of sources to events 🪠 (#10960)
* fix(web): key preview stickiness (#10778)
* fix(web): early gesture-match abort when unable to extend existing gestures (#10836)
* fix(web): infinite model-match replacement looping (#10838)
* fix(web): proper gesture-match sequencing (#10840)
* change(web): input-event sequentialization (#10843)
* fix(web): proper linkage of sources to events (#10960)
* fix(developer): handle buffer boundaries in four cases (#11137)
* chore(linux): Build packages for next Ubuntu version separately :cherries: (#11153)
* chore(linux): Build packages for next Ubuntu version separately (#11153)
* fix(common): upgrade sentry-cli to 2.31.0 (#11151)
* fix(android/app): Track previous device orientation for SystemKeyboard (#11134)
* (#11129)
@ -2028,8 +2036,8 @@
* fix(developer): package editor no longer loses RTL flag for LMs (#8607)
* chore(common): use mac /usr/bin/stat rather than homebrew version (#8925)
* chore(ios): replace fv cert (#8923)
* fix(core): Fix compilation if hotdoc is installed :cherries: (#8929)
* chore(linux): Move some files to keyman-config :cherries: (#8930)
* fix(core): Fix compilation if hotdoc is installed (#8929)
* chore(linux): Move some files to keyman-config (#8930)
* fix(core): Fix compiling with GCC 13 (#8932)
## 16.0.139 stable 2023-03-16
@ -2266,7 +2274,7 @@
* fix(developer): URL parameters should be UTF-8 (#7631)
* fix(android/engine): Use IME package name if query permission denied (#7668)
* fix(linux): Fix keyboard icon in system tray (#7678)
* chore(linux): Update debian changelog :cherries: (#7682)
* chore(linux): Update debian changelog (#7682)
* chore(linux): Update Debian standards version (#7683)
## 16.0.100 beta 2022-11-10
@ -2480,7 +2488,7 @@
## 16.0.70 alpha 2022-09-26
* fix(android): Add language name when installing default lexical-model (#7347)
* chore(common): Add 15.0 stable entries to HISTORY.md :cherries: (#7350)
* chore(common): Add 15.0 stable entries to HISTORY.md (#7350)
## 16.0.69 alpha 2022-09-21
@ -2670,7 +2678,7 @@
## 16.0.37 alpha 2022-07-22
* fix(linux): Catch PermissionError exception (#6968)
* chore(linux): Update Debian changelog :cherries: (#6973)
* chore(linux): Update Debian changelog (#6973)
* chore(linux): Add support for Ubuntu 22.10 "Kinetic Kudu" (#6975)
* fix(developer): reduce timeouts if server shut down (#6943)
* fix(linux): Implement refresh after keyboard installation (#6956)
@ -2863,12 +2871,12 @@
## 15.0.269 stable 2022-08-29
* chore(linux): Update debian changelog (#7040)
* feat(linux): Replace deprecated distutils :cherries: (#7052)
* feat(linux): Replace deprecated distutils (#7052)
* fix(developer): compiler emitting garbage for readonly groups (#7014)
* chore: Change platform advocates per discussion (#7114)
* fix(windows): remove saving and restoring context kbd options (#7107)
* fix(windows): Add invalidate context action to non-updatable parse (#7108)
* fix(android/engine): :cherries: Lower the max height for landscape orientation (#7128)
* fix(android/engine): Lower the max height for landscape orientation (#7128)
## 15.0.268 stable 2022-08-04
@ -2885,10 +2893,10 @@
* fix(ios): ignore CFBundleShortVersionString (#6935)
* fix(web): context-only rule effects, set(&layer) from physical keystrokes (#6949)
* chore(web): remove invalid warning msg (#6951)
* fix(common): Fix `delete` :cherries: (#6966)
* fix(linux): Another attempt at fixing postinst script :cherries: (#6961)
* fix(linux): Fix uninstallation when using fcitx5 :cherries: (#6964)
* fix(linux): Catch PermissionError exception :cherries: (#6969)
* fix(common): Fix `delete` (#6966)
* fix(linux): Another attempt at fixing postinst script (#6961)
* fix(linux): Fix uninstallation when using fcitx5 (#6964)
* fix(linux): Catch PermissionError exception (#6969)
* chore(linux): Update Debian changelog (#6972)
* fix(developer): kmdecomp virtual character key output (#6945)
* fix(developer): crash on exit when checking for updates (#6946)
@ -2900,7 +2908,7 @@
## 15.0.266 stable 2022-07-08
* fix(linux): Fix post-install script :cherries: (#6895)
* fix(linux): Fix post-install script (#6895)
* fix(web): improve `console.error()` reporting (#6904)
* fix(web): ncaps rules not matching on touch (#6913)
@ -3350,7 +3358,7 @@
## 15.0.183 alpha 2022-01-21
* chore(linux): Update changelogs for 14.0.284 :cherries: (#6132)
* chore(linux): Update changelogs for 14.0.284 (#6132)
* chore(linux): Revert workaround for Python bug (#6133)
## 15.0.182 alpha 2022-01-21
@ -3469,12 +3477,12 @@
## 15.0.162 alpha 2021-12-05
* chore(linux): Update changelogs for 14.0.283 :package: :cherries: (#6008)
* chore(linux): Update changelogs for 14.0.283 (#6008)
## 15.0.161 alpha 2021-12-04
* chore(linux): Allow to specify debian revision :package: :cherries: (#5999)
* chore(linux): Remove lintian warning :package: :cherries: (#6000)
* chore(linux): Allow to specify debian revision (#5999)
* chore(linux): Remove lintian warning (#6000)
## 15.0.160 alpha 2021-12-03
@ -3660,7 +3668,7 @@
* fix(android/engine): Remove unnecessary permissions from Manifest (#5752)
* feat(developer): touch layout testing (#5723)
* fix(web): popup positioning (#5742)
* chore(linux): Update changelog files for 14.0.282 :cherries: (#5794)
* chore(linux): Update changelog files for 14.0.282 (#5794)
## 15.0.124 alpha 2021-10-04
@ -4476,8 +4484,8 @@
## 14.0.285 stable 2022-01-20
* fix(linux): Fix lintian errors :cherries: (#6107)
* fix(linux): Add workaround for Python bug :cherries: (#6125)
* fix(linux): Fix lintian errors (#6107)
* fix(linux): Add workaround for Python bug (#6125)
* fix(web): Use regex to determine display layer and functional layers (#6123)
## 14.0.284 stable 2022-01-11
@ -4488,12 +4496,12 @@
* fix(android/engine): Fix font paths (#5990)
* chore(linux): Remove lintian warning (#5993)
* chore(linux): Allow to specify debian revision (#5998)
* chore(linux): Update changelogs for 14.0.283 :package: (#6007)
* fix(linux): fix release version number for Sentry reporting :cherries: (#6053)
* chore(linux): Update changelogs for 14.0.283 (#6007)
* fix(linux): fix release version number for Sentry reporting (#6053)
* chore(common): Check in crowdin strings for Spanish (Latin America) (#6060)
* fix(linux): fix release version number for Sentry reporting :cherries: (#6069)
* fix(linux): fix release version number for Sentry reporting (#6069)
* chore(android/samples): Add -no-daemon flag to KMSample2 build script (#6083)
* fix(linux): Fix attribute error :cherries: (#6087)
* fix(linux): Fix attribute error (#6087)
## 14.0.283 stable 2021-11-17
@ -4501,7 +4509,7 @@
* chore(linux): copy Keyman for Linux 15 reference to 14 (#5764)
* fix(linux): Fix debian package script (#5772)
* chore(common): Enhance cherry-pick labeling (#5773)
* fix(common): Fix cherry-pick labeling (:cherries:) (#5783)
* fix(common): Fix cherry-pick labeling () (#5783)
* fix(linux): Don't crash displaying keyboard details (#5757)
* chore(linux): Update changelog files for 14.0.282 (#5793)
* fix(linux): Don't crash with non-keyboard package file (#5754)
@ -6020,7 +6028,7 @@
## 14.0.81 alpha 2020-05-27
* refactor(resources): convert gosh into npm package 🙃 (#3159)
* refactor(resources): convert gosh into npm package (#3159)
* chore(common,web): use consistent TypeScript dep on all packages (#3158)
* chore(common/resources): add `common/models` to build trigger definitions (#3144)
* fix(common/resources): adds package-lock.json for gosh package (#3171)

View file

@ -1 +1 @@
17.0.309
17.0.310

View file

@ -232,7 +232,13 @@ final class KMKeyboard extends WebView {
getSettings().setUseWideViewPort(true);
getSettings().setLoadWithOverviewMode(true);
if (0 != (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE)) {
// When `.isTestMode() == true`, the setWebContentsDebuggingEnabled method is not available
// and thus will trigger unit-test failures.
if (!KMManager.isTestMode() && (
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 ||
KMManager.getTier(null) != KMManager.Tier.STABLE
)) {
// Enable debugging of WebView via adb. Not used during unit tests
// Refer: https://developer.chrome.com/docs/devtools/remote-debugging/webviews/#configure_webviews_for_debugging
setWebContentsDebuggingEnabled(true);

View file

@ -18,8 +18,7 @@ builder_describe "Runs all tests for the gesture-recognizer module" \
"test+" \
":headless Runs headless user tests" \
":browser Runs browser-based user tests" \
"--ci Uses CI-based test configurations & emits CI-friendly test reports" \
"--debug,-d Activates developer-friendly debug mode for unit tests where applicable"
"--ci Uses CI-based test configurations & emits CI-friendly test reports"
builder_parse "$@"

View file

@ -107,8 +107,20 @@ export default class InputProcessor {
if(keyEvent.baseTranscriptionToken) {
const transcription = this.contextCache.get(keyEvent.baseTranscriptionToken);
if(transcription) {
// Restores full context, including deadkeys in their exact pre-keystroke state.
outputTarget.restoreTo(transcription.preInput);
// Has there been a context change at any point during the multitap? If so, we need
// to revert it. If not, we assume it's a layer-change multitap, in which case
// no such reset is needed.
if(!isEmptyTransform(transcription.transform) || !transcription.preInput.isEqual(Mock.from(outputTarget))) {
// Restores full context, including deadkeys in their exact pre-keystroke state.
outputTarget.restoreTo(transcription.preInput);
}
/*
else:
1. We don't need to restore the original context, as it's already
in-place.
2. Restoring anyway would obliterate any selected text, which is bad
if this is a purely-layer-switching multitap. (#11230)
*/
} else {
console.warn('The base context for the multitap could not be found');
}

View file

@ -38,6 +38,10 @@ export class Deadkey {
return dk;
}
equal(other: Deadkey) {
return this.d == other.d && this.p == other.d && this.o == other.o;
}
/**
* Sorts the deadkeys in reverse order.
*/
@ -151,6 +155,24 @@ export class DeadkeyTracker {
}
}
equal(other: DeadkeyTracker) {
if(this.dks.length != other.dks.length) {
return false;
}
const otherDks = other.dks;
const matchedDks: Deadkey[] = [];
for(let dk of this.dks) {
const match = otherDks.find((otherDk) => dk.equal(otherDk));
if(!match) {
return false;
}
}
return matchedDks.length == otherDks.length;
}
count(): number {
return this.dks.length;
}

View file

@ -446,6 +446,18 @@ export class Mock extends OutputTarget {
this.text = this.getTextBeforeCaret() + s;
}
/**
* Indicates if this Mock represents an identical context to that of another Mock.
* @param other
* @returns
*/
isEqual(other: Mock) {
return this.text == other.text
&& this.selStart == other.selStart
&& this.selEnd == other.selEnd
&& this.deadkeys().equal(other.deadkeys());
}
doInputEvent() {
// Mock isn't backed by an element, so it won't have any event listeners.
}

View file

@ -333,6 +333,55 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
}
}
}
func deleteSelection() -> Bool {
if let selected = textDocumentProxy.selectedText, selected.count > 0 {
/*
Since we're doing some funky text manipulation, it's best to add
a "canary" check in case something does go awry with it in
the future.
*/
let beforeManipulation = textDocumentProxy.documentContextBeforeInput ?? ""
/*
We have a problem to resolve here: we cannot simply delete the selection
with either .deleteBackward() or .insertText(""):
- If there is selected text immediately following a space (U+0020),
.deleteBackward() will delete that space IN ADDITION to the selected text.
- Unlike .insertText("-any-string-here"), .insertText("") does nothing;
it does not replace the selection with the new string.
Our policy (#9073) on handling the backspace key when there is a
text selection is to just delete the selection. We have to override
the special case of space being deleted by .deleteBackward() ourselves.
Additionally, the internal Web engine cannot anticipate the special
case and requires precise and consistent backspace handling in line
with our policy in order to keep the context on both sides synchronized.
As .insertText() does not delete the selection if the string to
be inserted is empty, we insert something that won't combine,
like a ZWNJ, and then delete it.
iOS does not allow users to select text in a way that splits
character clusters. This implies that it's impossible for an
inserted ZWNJ to combine with existing context, making this
operation safe.
*/
textDocumentProxy.insertText("\u{200c}")
textDocumentProxy.deleteBackward()
let afterManipulation = textDocumentProxy.documentContextBeforeInput ?? ""
// And now to finish our 'canary' check.
if beforeManipulation != afterManipulation {
os_log(.error, log: KeymanEngineLogger.engine, "Could not cleanly execute backspace for selected text")
}
return true
}
return false
}
func insertText(_ keymanWeb: KeymanWebViewController, numCharsToDelete: Int, newText: String) {
if keymanWeb.isSubKeysMenuVisible {
@ -347,18 +396,13 @@ open class InputViewController: UIInputViewController, KeymanWebDelegate {
perform(#selector(self.enableInputClickSound), with: nil, afterDelay: 0.1)
}
var hasDeletedSelection = false
// `true` if there was selected text to be deleted
let deletedSelection = self.deleteSelection()
if let selected = textDocumentProxy.selectedText {
if selected.count > 0 {
textDocumentProxy.deleteBackward()
hasDeletedSelection = true
}
}
if numCharsToDelete <= 0 || hasDeletedSelection {
// If text was selected, we generally act as if the context is nil - no back
// deletions allowed, so we skip that section.
if numCharsToDelete <= 0 || deletedSelection {
textDocumentProxy.insertText(newText)
sendContextUpdate()
return
}

View file

@ -30,7 +30,7 @@ if builder_is_debug_build; then
fi
builder_describe_outputs \
build /ios/build/Build/Products/Release-iphoneos/Keyman.xcarchive
build /ios/build/Build/Products/${CONFIG}-iphoneos/Keyman.xcarchive
# Base definitions (must be before do_clean call)
DERIVED_DATA="$KEYMAN_ROOT/ios/build"

View file

@ -8,9 +8,7 @@ function do_build() {
cp -Rf "$KEYMAN_ENGINE_FRAMEWORK_SRC" "$KEYMAN_ENGINE_FRAMEWORK_DST"
CODE_SIGN=
# `builder_is_debug_build` appears to fail here, while referring to the option does not?
# Perhaps it's due to the main build definition being within a function?
if builder_has_option --debug; then
if builder_is_debug_build; then
CODE_SIGN=CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED="NO" CODE_SIGNING_ENTITLEMENTS=""
fi
@ -30,7 +28,6 @@ function execute_sample_build() {
"clean" \
"configure" \
"build" \
"--debug Avoids codesigning and adds full sourcemaps for the embedded predictive-text engine" \
"--sim-artifact+ Unused by this build at present"
builder_parse "$@"

View file

@ -50,9 +50,8 @@ get_version_notes "ios" "${BUILD_NUMBER}" "$TIER" > $CHANGELOG_PATH
echo "* Minor fixes and performance improvements" >> $CHANGELOG_PATH
assertFileExists "${CHANGELOG_PATH}"
# Strip emoji to make Apple happy
emoji="\U1f300-\U1f5ff\U1f900-\U1f9ff\U1f600-\U1f64f\U1f680-\U1f6ff\U2600-\U26ff\U2700-\U27bf\U1f1e6-\U1f1ff\U1f191-\U1f251\U1f004\U1f0cf\U1f170-\U1f171\U1f17e-\U1f17f\U1f18e\U3030\U2b50\U2b55\U2934-\U2935\U2b05-\U2b07\U2b1b-\U2b1c\U3297\U3299\U303d\U00a9\U00ae\U2122\U23f3\U24c2\U23e9-\U23ef\U25b6\U23f8-\U23fa"
LC_ALL=UTF-8 sed -e "s/[$(printf $emoji)]//g" < "$CHANGELOG_PATH" > "$CHANGELOG_PATH.1"
# Strip emoji as App Store does not allow emoji in changelogs
node "$KEYMAN_ROOT/resources/tools/strip-emoji" < "$CHANGELOG_PATH" > "$CHANGELOG_PATH.1"
mv -f "$CHANGELOG_PATH.1" "$CHANGELOG_PATH"
assertFileExists "${CHANGELOG_PATH}"

View file

@ -22,7 +22,6 @@ builder_describe "Builds the $TARGET app for use on iOS devices - iPhone and iPa
"clean" \
"configure" \
"build" \
"--debug Avoids codesigning and adds full sourcemaps for the embedded predictive-text engine" \
"--sim-artifact Also outputs a simulator-friendly test artifact corresponding to the build"
builder_parse "$@"

29
package-lock.json generated
View file

@ -7,6 +7,7 @@
"name": "root",
"workspaces": [
"resources/gosh",
"resources/tools/strip-emoji",
"resources/build/version",
"core/include/ldml",
"developer/src/common/web/test-helpers",
@ -11672,6 +11673,10 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stripemoji": {
"resolved": "resources/tools/strip-emoji",
"link": true
},
"node_modules/supports-color": {
"version": "7.2.0",
"license": "MIT",
@ -12702,6 +12707,7 @@
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/github": "^2.1.0",
"emoji-regex": "^10.3.0",
"typescript": "^4.9.5",
"yargs": "^17.7.2"
},
@ -12739,6 +12745,11 @@
"node": ">=12"
}
},
"resources/build/version/node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
},
"resources/build/version/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -12760,6 +12771,11 @@
"node": ">=8"
}
},
"resources/build/version/node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"resources/build/version/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@ -12803,6 +12819,19 @@
"gosh": "gosh.js"
}
},
"resources/tools/strip-emoji": {
"name": "stripemoji",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0"
}
},
"resources/tools/strip-emoji/node_modules/emoji-regex": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
},
"web": {
"name": "keyman",
"license": "MIT",

View file

@ -25,6 +25,7 @@
"scripts": {},
"workspaces": [
"resources/gosh",
"resources/tools/strip-emoji",
"resources/build/version",
"core/include/ldml",
"developer/src/common/web/test-helpers",

View file

@ -4,6 +4,7 @@
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/github": "^2.1.0",
"emoji-regex": "^10.3.0",
"typescript": "^4.9.5",
"yargs": "^17.7.2"
},

View file

@ -4,6 +4,7 @@ import { GitHub } from '@actions/github';
import { findLastHistoryPR, getAssociatedPR} from './graphql/queries.js';
import { spawnChild } from './util/spawnAwait.js';
import emojiRegex from 'emoji-regex';
const getPullRequestInformation = async (
octokit: GitHub, base: string
@ -125,10 +126,10 @@ export const reportHistory = async (
} else {
let git_tag = 'Next Version', git_tag_data = 'Next Version';
const re = /#(\d+)/;
const emojiRE = /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/gu;
const emojiRE = emojiRegex();
for(const commit of new_commits) {
if(!useGitHubPRInfo) {
const git_pr_title = (await spawnChild('git', ['log', '--format=%b', '-n', '1', commit])).replace(emojiRE, ' ').trim();
const git_pr_title = (await spawnChild('git', ['log', '--format=%b', '-n', '1', commit])).replaceAll(emojiRE, ' ').trim();
if(git_pr_title.match(/^auto\:/)) continue;
const git_pr_data = (await spawnChild('git', ['log', '--format=%s', '-n', '1', commit])).trim();
const this_git_tag = (await spawnChild('git', ['tag', '--points-at', commit])).trim();
@ -164,7 +165,7 @@ export const reportHistory = async (
if(pulls.find(p => p.number == pr.number) == undefined) {
pr.tag_data = git_tag_data;
pr.version = git_tag;
pr.title = pr.title.replace(emojiRE, ' ').trim();
pr.title = pr.title.replaceAll(emojiRE, ' ').trim();
pulls.push(pr);
}
}

View file

@ -0,0 +1,18 @@
const emojiRegex = require('emoji-regex');
process.stdin.setEncoding('utf-8');
// We will concatenate all strings and assume we are not processing a huge file,
// so we don't break in the middle of a UTF-8 sequence or split emoji sequences
// in half.
let stream = '';
process.stdin.on('readable', () => {
const data = process.stdin.read();
if(data) {
stream += data;
} else {
process.stdout.write(stream.replaceAll(emojiRegex(), ''));
}
});

View file

@ -0,0 +1,14 @@
{
"name": "stripemoji",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0"
}
}

View file

@ -73,29 +73,33 @@ export default class KeymanEngine extends KeymanEngineBase<BrowserConfiguration,
const e = target?.getElement();
(this.osk.activationModel as TwoStateActivator<HTMLElement>).activationTrigger = e;
if(this.config.hostDevice.touchable) {
if(!e || !target || !this.osk) {
return;
}
// Get the absolute position of the caret
const y = getAbsoluteY(e);
const t = window.pageYOffset;
let dy = y-t;
if(y >= t) {
dy -= (window.innerHeight - this.osk._Box.offsetHeight - e.offsetHeight - 2);
if(dy < 0) {
dy=0;
}
}
// Hide OSK, then scroll, then re-anchor OSK with absolute position (on end of scroll event)
if(dy != 0) {
window.scrollTo(0, dy + t);
}
if(this.config.hostDevice.touchable && target) {
this.ensureElementVisibility(e);
}
});
}
public ensureElementVisibility(e: HTMLElement) {
if(!e || !this.osk) {
return;
}
// Get the absolute position of the caret
const y = getAbsoluteY(e);
const t = window.pageYOffset;
let dy = y-t;
if(y >= t) {
dy -= (window.innerHeight - this.osk._Box.offsetHeight - e.offsetHeight - 2);
if(dy < 0) {
dy=0;
}
}
// Hide OSK, then scroll, then re-anchor OSK with absolute position (on end of scroll event)
if(dy != 0) {
window.scrollTo(0, dy + t);
}
}
public get util() {
return this._util;
}

View file

@ -58,6 +58,15 @@ export class RotationProcessor {
window.clearInterval(this.updateTimer);
this.rotState = null;
}
const target = this.keyman.contextManager.activeTarget;
if(target) {
// This seems to help with scrolling accuracy in iOS Safari;
// the scroll tends to consistently go too far without it.
window.setTimeout(() => {
this.keyman.ensureElementVisibility(target.getElement());
}, 0);
}
}
// Used by both Android and iOS.

View file

@ -144,7 +144,7 @@ export default class KeymanEngine<
this.osk.startHide(false);
}
const earlyBatchClosure = () => {
const prepareKeyboardSwap = () => {
this.refreshModel();
// Triggers context resets that can trigger layout stuff.
// It's not the final such context-reset, though.
@ -156,17 +156,6 @@ export default class KeymanEngine<
});
}
/*
Needed to ensure the correct layer is displayed AND that deadkeys from
the old keyboard have been wiped.
Needs to be after the OSK has loaded for the keyboard in case the default
layer should be something other than "default" for the current context.
*/
const doContextReset = () => {
this.contextManager.resetContext();
}
/*
This pattern is designed to minimize layout reflow during the keyboard-swap process.
The 'default' layer is loaded by default, but some keyboards will start on different
@ -177,16 +166,24 @@ export default class KeymanEngine<
*/
if(this.osk) {
this.osk.batchLayoutAfter(() => {
earlyBatchClosure();
prepareKeyboardSwap();
this.osk.activeKeyboard = kbd;
// Note: when embedded within the mobile apps, the keyboard will still be visible
// at this time.
doContextReset();
/*
Needed to ensure the correct layer is displayed AND that deadkeys from
the old keyboard have been wiped.
Needs to be after the OSK has loaded for the keyboard in case the default
layer should be something other than "default" for the current context.
*/
this.contextManager.resetContext();
this.osk.present();
});
} else {
earlyBatchClosure();
doContextReset();
prepareKeyboardSwap();
this.contextManager.resetContext();
}
});
@ -242,14 +239,12 @@ export default class KeymanEngine<
resetContext: (target) => {
// Could reset the target's deadkeys here, but it's really more of a 'core' task.
// So we delegate that to keyboard-processor.
const doReset = () => this.core.resetContext(target);
if(this.osk) {
this.osk.batchLayoutAfter(() => {
doReset();
this.core.resetContext(target);
})
} else {
doReset();
this.core.resetContext(target);
}
},
predictionContext: new PredictionContext(this.core.languageProcessor, this.core.keyboardProcessor),

View file

@ -109,8 +109,8 @@ export const DEFAULT_GESTURE_PARAMS: GestureParams = {
noiseTolerance: 10
},
multitap: {
waitLength: 500,
holdLength: 500
waitLength: 300,
holdLength: 150
},
// Note: all actual runtime values are determined at runtime based upon row height.
// See `VisualKeyboard.refreshLayout`, CTRL-F "Step 3".

View file

@ -197,22 +197,15 @@ export default class OSKBaseKey extends OSKKey {
}
public refreshLayout(layoutParams: KeyLayoutParams) {
const keyTextClosure = super.refreshLayout(layoutParams); // key labels in particular.
super.refreshLayout(layoutParams); // key labels in particular.
return () => {
// part 2: key internals - these do depend on recalculating internal layout.
// Ideally, the rest would be in yet another calculation layer... need to figure out a good design for this.
keyTextClosure(); // we're already in that phase, so go ahead and run it.
const emFont = layoutParams.baseEmFontSize;
// Rescale keycap labels on small phones
if(emFont.val < 12) {
this.capLabel.style.fontSize = '6px';
} else {
// The default value set within kmwosk.css.
this.capLabel.style.fontSize = ParsedLengthStyle.forScalar(0.5).styleString;
}
const emFont = layoutParams.baseEmFontSize;
// Rescale keycap labels on small phones
if(emFont.val < 12) {
this.capLabel.style.fontSize = '6px';
} else {
// The default value set within kmwosk.css.
this.capLabel.style.fontSize = ParsedLengthStyle.forScalar(0.5).styleString;
}
}

View file

@ -311,10 +311,17 @@ export default abstract class OSKKey {
this.label.style.fontSize = '';
}
public refreshLayout(layoutParams: KeyLayoutParams) {
/**
* Any style-caching behavior needed for use in layout manipulation should be
* computed within this method, not within refreshLayout. This is to prevent
* unnecessary layout-reflow.
* @param layoutParams
* @returns
*/
public detectStyles(layoutParams: KeyLayoutParams) {
// Avoid doing any font-size related calculations if there's no text to display.
if(this.spec.sp == ButtonClasses.spacer || this.spec.sp == ButtonClasses.blank) {
return () => {};
return null;
}
// Attempt to detect static but key-specific style properties if they haven't yet
@ -324,7 +331,7 @@ export default abstract class OSKKey {
// Abort if the element is not currently in the DOM; we can't get any info this way.
if(!lblStyle.fontFamily) {
return () => {};
return null;
}
this._fontFamily = lblStyle.fontFamily;
@ -341,26 +348,26 @@ export default abstract class OSKKey {
this._fontSize = ParsedLengthStyle.forScalar(localFontScaling);
}
}
}
// Avoid any references to getComputedStyle, offset_, or other layout-reflow
// dependent values. Refer to https://gist.github.com/paulirish/5d52fb081b3570c81e3a.
public refreshLayout(layoutParams: KeyLayoutParams) {
// space bar may not define the text span!
if(this.label) {
if(!this.label.classList.contains('kmw-spacebar-caption')) {
// Do not use `this.keyText` - it holds *___* codes for special keys, not the actual glyph!
const keyCapText = this.label.textContent;
const fontSize = this.getIdealFontSize(keyCapText, layoutParams);
return () => {
this.label.style.fontSize = fontSize.styleString;
};
this.label.style.fontSize = fontSize.styleString;
} else {
// Spacebar text, on the other hand, is available via this.keyText.
// Using this field helps prevent layout reflow during updates.
const fontSize = this.getIdealFontSize(this.keyText, layoutParams);
return () => {
// Since the kmw-spacebar-caption version uses !important, we must specify
// it directly on the element too; otherwise, scaling gets ignored.
this.label.style.setProperty("font-size", fontSize.styleString, "important");
};
// Since the kmw-spacebar-caption version uses !important, we must specify
// it directly on the element too; otherwise, scaling gets ignored.
this.label.style.setProperty("font-size", fontSize.styleString, "important");
}
}
}

View file

@ -126,29 +126,35 @@ export default class OSKLayer {
**/
showLanguage(displayName: string) {
if(!this.spaceBarKey) {
return () => {};
return;
}
try {
const spacebarLabel = this.spaceBarKey.label;
// The key can read the text from here during the display update without us
// needing to trigger a reflow by running the closure below early.
// The key can read the text from here during the display update without
// triggering a reflow.
this.spaceBarKey.spec.text = displayName;
return () => {
// It sounds redundant, but this dramatically cuts down on browser DOM processing;
// but sometimes innerText is reported empty when it actually isn't, so set it
// anyway in that case (Safari, iOS 14.4)
if (spacebarLabel.innerText != displayName || displayName == '') {
spacebarLabel.innerText = displayName;
}
// It sounds redundant, but this dramatically cuts down on browser DOM processing;
// but sometimes innerText is reported empty when it actually isn't, so set it
// anyway in that case (Safari, iOS 14.4)
if (spacebarLabel.innerText != displayName || displayName == '') {
spacebarLabel.innerText = displayName;
}
}
catch (ex) { }
}
public refreshLayout(layoutParams: LayerLayoutParams) {
// Do all layout-reflow / style-refresh dependent precalculations here,
// before we perform any DOM manipulation.
this.rows.forEach((row) => row.detectStyles(layoutParams));
// Hereafter, avoid any references to getComputedStyle, offset_, or other
// layout-reflow dependent values. Refer to
// https://gist.github.com/paulirish/5d52fb081b3570c81e3a.
// Check the heights of each row, in case different layers have different row counts.
const layerHeight = layoutParams.keyboardHeight;
const nRows = this.rows.length;
@ -159,10 +165,9 @@ export default class OSKLayer {
this.element.style.height=(layerHeight)+'px';
}
const spacebarTextClosure = this.showLanguage(layoutParams.spacebarText);
this.showLanguage(layoutParams.spacebarText);
// Update row layout properties
const rowClosures: (() => void)[] = [];
for(let nRow=0; nRow<nRows; nRow++) {
const oskRow = this.rows[nRow];
const bottom = (nRows-nRow-1)*rowHeight+1;
@ -172,27 +177,14 @@ export default class OSKLayer {
this.spec.row[nRow].proportionalY = ((layerHeight - bottom) - rowHeight/2) / layerHeight;
}
let rowClosure = oskRow.refreshLayout(layoutParams);
oskRow.refreshLayout(layoutParams);
if(nRow == nRows-1) {
const oldRowClosure = rowClosure;
rowClosure = () => {
oldRowClosure();
oskRow.element.style.bottom = '1px';
};
oskRow.element.style.bottom = '1px';
}
rowClosures.push(rowClosure);
}
const rowKeyClosures: (() => void)[] = [];
for(const row of this.rows) {
const batchedUpdates = row.refreshKeyLayouts(layoutParams);
rowKeyClosures.push(batchedUpdates);
row.refreshKeyLayouts(layoutParams);
}
// After row layout properties have been updated, _then_ update key internals.
// Doing this separately like this helps to reduce layout reflow.
spacebarTextClosure();
rowClosures.forEach((closure) => closure());
rowKeyClosures.forEach((closure) => closure());
}
}

View file

@ -3,7 +3,7 @@ import { ActiveKey, ActiveLayer, ActiveRow } from '@keymanapp/keyboard-processor
import OSKBaseKey from './oskBaseKey.js';
import { ParsedLengthStyle } from '../lengthStyle.js';
import VisualKeyboard from '../visualKeyboard.js';
import { KeyLayoutParams } from './oskKey.js';
import OSKKey, { KeyLayoutParams } from './oskKey.js';
import { LayerLayoutParams } from './oskLayer.js';
/*
@ -66,42 +66,68 @@ export default class OSKRow {
}
}
// Avoid any references to getComputedStyle, offset_, or other layout-reflow
// dependent values. Refer to https://gist.github.com/paulirish/5d52fb081b3570c81e3a.
public refreshLayout(layoutParams: LayerLayoutParams) {
const rs = this.element.style;
const rowHeight = layoutParams.heightStyle.scaledBy(this.heightFraction);
const executeRowStyleUpdates = () => {
rs.maxHeight=rs.lineHeight=rs.height=rowHeight.styleString;
}
rs.maxHeight=rs.lineHeight=rs.height=rowHeight.styleString;
const keyHeightBase = layoutParams.heightStyle.absolute ? rowHeight : ParsedLengthStyle.forScalar(1);
const padTop = keyHeightBase.scaledBy(KEY_BTN_Y_PAD_RATIO / 2);
const keyHeight = keyHeightBase.scaledBy(1 - KEY_BTN_Y_PAD_RATIO);
// Update all key-square layouts.
const keyStyleUpdates = this.keys.map((key) => {
return () => {
const keySquare = key.square;
const keyElement = key.btn;
this.keys.forEach((key) => {
const keySquare = key.square;
const keyElement = key.btn;
// Set the kmw-key-square position
const kss = keySquare.style;
kss.height=kss.minHeight=keyHeightBase.styleString;
// Set the kmw-key-square position
const kss = keySquare.style;
kss.height=kss.minHeight=keyHeightBase.styleString;
const kes = keyElement.style;
kes.top = padTop.styleString;
kes.height=kes.lineHeight=kes.minHeight=keyHeight.styleString;
}
})
return () => {
executeRowStyleUpdates();
keyStyleUpdates.forEach((closure) => closure());
}
const kes = keyElement.style;
kes.top = padTop.styleString;
kes.height=kes.lineHeight=kes.minHeight=keyHeight.styleString;
});
}
private buildKeyLayout(layoutParams: LayerLayoutParams, key: OSKKey) {
// Calculate changes to be made...
const keyWidth = layoutParams.widthStyle.scaledBy(key.spec.proportionalWidth);
// We maintain key-btn padding within the key-square - the latter `scaledBy`
// adjusts for that, providing the final key-btn height.
const keyHeight = layoutParams.heightStyle.scaledBy(this.heightFraction).scaledBy(1 - KEY_BTN_Y_PAD_RATIO);
const keyStyle: KeyLayoutParams = {
keyWidth: keyWidth.val * (keyWidth.absolute ? 1 : layoutParams.keyboardWidth),
keyHeight: keyHeight.val * (keyHeight.absolute ? 1 : layoutParams.keyboardHeight),
baseEmFontSize: layoutParams.baseEmFontSize,
layoutFontSize: layoutParams.layoutFontSize
};
return keyStyle;
}
/**
* Any style-caching behavior needed for use in layout manipulation should be
* computed within this method, not within refreshLayout. This is to prevent
* unnecessary layout-reflow.
* @param layoutParams
* @returns
*/
public detectStyles(layoutParams: LayerLayoutParams) {
this.keys.forEach((key) => {
key.detectStyles(this.buildKeyLayout(layoutParams, key));
});
}
// Avoid any references to getComputedStyle, offset_, or other layout-reflow
// dependent values. Refer to https://gist.github.com/paulirish/5d52fb081b3570c81e3a.
public refreshKeyLayouts(layoutParams: LayerLayoutParams) {
const updateClosures = this.keys.map((key) => {
this.keys.forEach((key) => {
// Calculate changes to be made...
const keyElement = key.btn;
@ -118,26 +144,14 @@ export default class OSKRow {
// Match the row height (if fixed-height) or use full row height (if percent-based)
const styleHeight = heightStyle.absolute ? keyHeight.styleString : '100%';
const keyStyle: KeyLayoutParams = {
keyWidth: keyWidth.val * (keyWidth.absolute ? 1 : layoutParams.keyboardWidth),
keyHeight: keyHeight.val * (heightStyle.absolute ? 1 : layoutParams.keyboardHeight),
baseEmFontSize: layoutParams.baseEmFontSize,
layoutFontSize: layoutParams.layoutFontSize
};
//return keyElement.key ? keyElement.key.refreshLayout(keyStyle) : () => {};
const keyFontClosure = keyElement.key ? keyElement.key.refreshLayout(keyStyle) : () => {};
const keyStyle: KeyLayoutParams = this.buildKeyLayout(layoutParams, key);
keyElement.key?.refreshLayout(keyStyle);
// And queue them to be run in a single batch later. This helps us avoid layout reflow thrashing.
return () => {
key.square.style.width = keyWidth.styleString;
key.square.style.marginLeft = keyPad.styleString;
key.square.style.width = keyWidth.styleString;
key.square.style.marginLeft = keyPad.styleString;
key.btn.style.width = widthStyle.absolute ? keyWidth.styleString : '100%';
key.square.style.height = styleHeight;
keyFontClosure();
}
key.btn.style.width = widthStyle.absolute ? keyWidth.styleString : '100%';
key.square.style.height = styleHeight;
});
return () => updateClosures.forEach((closure) => closure());
}
}

View file

@ -231,20 +231,12 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
activeGestures: GestureHandler[] = [];
activeModipress: Modipress = null;
private _deferLayout: boolean;
public deferLayout: boolean;
// The keyboard object corresponding to this VisualKeyboard.
public readonly layoutKeyboard: Keyboard;
public readonly layoutKeyboardProperties: KeyboardProperties;
get deferLayout(): boolean {
return this._deferLayout;
}
set deferLayout(value: boolean) {
this._deferLayout = value;
}
get layerId(): string {
return this.layerGroup?.activeLayerId ?? 'default';
}
@ -262,7 +254,7 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
}
}
if(changedLayer && !this._deferLayout) {
if(changedLayer && !this.deferLayout) {
this.updateState();
// We changed the active layer, but not any layout property of the keyboard as a whole.
this.layerGroup.refreshLayout(this.constructLayoutParams());
@ -1215,7 +1207,7 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
* when needed.
*/
refreshLayout() {
if(this._deferLayout) {
if(this.deferLayout) {
return;
}