From e803bed813fce98eb0aebc7f32c4c38c9107e3e6 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 23 Nov 2021 10:11:10 +1100 Subject: [PATCH 01/54] feat(web): start-of-sentence test keyboard Relates to #3621. Initial draft of keyboard to test start-of-sentence detection. --- .../start_of_sentence_3621/.gitignore | 1 + .../start_of_sentence_3621/HISTORY.md | 6 + .../start_of_sentence_3621/LICENSE.md | 21 + .../start_of_sentence_3621/README.md | 30 + .../start_of_sentence_3621/source/readme.htm | 24 + ...start_of_sentence_3621.keyman-touch-layout | 1316 ++++++++++++++++ .../source/start_of_sentence_3621.kmn | 32 + .../source/start_of_sentence_3621.kps | 67 + .../source/start_of_sentence_3621.kvks | 8 + .../start_of_sentence_3621/source/welcome.htm | 26 + .../start_of_sentence_3621.keyboard_info | 7 + .../start_of_sentence_3621.kpj | 102 ++ web/testing/start-of-sentence-3621/index.html | 89 ++ .../start_of_sentence_3621.js | 1389 +++++++++++++++++ 14 files changed, 3118 insertions(+) create mode 100644 common/test/keyboards/start_of_sentence_3621/.gitignore create mode 100644 common/test/keyboards/start_of_sentence_3621/HISTORY.md create mode 100644 common/test/keyboards/start_of_sentence_3621/LICENSE.md create mode 100644 common/test/keyboards/start_of_sentence_3621/README.md create mode 100644 common/test/keyboards/start_of_sentence_3621/source/readme.htm create mode 100644 common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.keyman-touch-layout create mode 100644 common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn create mode 100644 common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kps create mode 100644 common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kvks create mode 100644 common/test/keyboards/start_of_sentence_3621/source/welcome.htm create mode 100644 common/test/keyboards/start_of_sentence_3621/start_of_sentence_3621.keyboard_info create mode 100644 common/test/keyboards/start_of_sentence_3621/start_of_sentence_3621.kpj create mode 100644 web/testing/start-of-sentence-3621/index.html create mode 100644 web/testing/start-of-sentence-3621/start_of_sentence_3621.js diff --git a/common/test/keyboards/start_of_sentence_3621/.gitignore b/common/test/keyboards/start_of_sentence_3621/.gitignore new file mode 100644 index 0000000000..d16386367f --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/common/test/keyboards/start_of_sentence_3621/HISTORY.md b/common/test/keyboards/start_of_sentence_3621/HISTORY.md new file mode 100644 index 0000000000..5f3230a787 --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/HISTORY.md @@ -0,0 +1,6 @@ +start_of_sentence_3621 Change History +==================== + +1.0 (2021-11-22) +---------------- +* Created by diff --git a/common/test/keyboards/start_of_sentence_3621/LICENSE.md b/common/test/keyboards/start_of_sentence_3621/LICENSE.md new file mode 100644 index 0000000000..77a9531492 --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +© 2021 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/common/test/keyboards/start_of_sentence_3621/README.md b/common/test/keyboards/start_of_sentence_3621/README.md new file mode 100644 index 0000000000..29041c3083 --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/README.md @@ -0,0 +1,30 @@ +start_of_sentence_3621 keyboard +============== + +Version 1.0 + +Description +----------- +start_of_sentence_3621 generated from template + +Links +----- + +Copyright +--------- +See [LICENSE.md](LICENSE.md) + +Supported Platforms +------------------- + * Windows + * macOS + * Linux + * Web + * iPhone + * iPad + * Android phone + * Android tablet + * Mobile devices + * Desktop devices + * Tablet devices + diff --git a/common/test/keyboards/start_of_sentence_3621/source/readme.htm b/common/test/keyboards/start_of_sentence_3621/source/readme.htm new file mode 100644 index 0000000000..c8b434197f --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/source/readme.htm @@ -0,0 +1,24 @@ + + + + + + start_of_sentence_3621 + + + + +

start_of_sentence_3621

+ +

+ start_of_sentence_3621 1.0 generated from template. +

+ +

©

+ + + diff --git a/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.keyman-touch-layout b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.keyman-touch-layout new file mode 100644 index 0000000000..bf09ed0bf9 --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.keyman-touch-layout @@ -0,0 +1,1316 @@ +{ + "tablet": { + "font": "Tahoma", + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "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": 2, + "key": [ + { + "id": "K_A", + "text": "a", + "pad": 70 + }, + { + "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" + }, + { + "sp": "10", + "width": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "110", + "sp": "1", + "nextlayer": "shift" + }, + { + "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_PERIOD", + "text": ".", + "sk": [ + { + "text": ",", + "id": "K_COMMA" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "'", + "id": "K_QUOTE" + }, + { + "text": "\"", + "id": "K_QUOTE", + "layer": "shift" + }, + { + "text": "\\", + "id": "K_BKSLASH" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "90", + "sp": "1" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": "140", + "sp": "1", + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "630", + "sp": "0" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "140", + "sp": "1" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "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": 2, + "key": [ + { + "id": "K_A", + "text": "A", + "pad": 70 + }, + { + "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" + }, + { + "sp": "10", + "width": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "110", + "sp": "2", + "nextlayer": "default" + }, + { + "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_PERIOD", + "text": ".", + "layer": "default", + "sk": [ + { + "text": ",", + "id": "K_COMMA", + "layer": "default" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "'", + "id": "K_QUOTE", + "layer": "default" + }, + { + "text": "\"", + "id": "K_QUOTE", + "layer": "shift" + }, + { + "text": "\\", + "id": "K_BKSLASH", + "layer": "default" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON", + "layer": "default" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "90", + "sp": "1" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": "140", + "sp": "1", + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "630", + "sp": "0" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "140", + "sp": "1" + } + ] + } + ] + }, + { + "id": "numeric", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_4", + "text": "$", + "layer": "shift", + "pad": 70 + }, + { + "id": "K_2", + "text": "@", + "layer": "shift" + }, + { + "id": "K_3", + "text": "#", + "layer": "shift" + }, + { + "id": "K_5", + "text": "%", + "layer": "shift" + }, + { + "id": "K_7", + "text": "&", + "layer": "shift" + }, + { + "id": "K_HYPHEN", + "text": "_", + "layer": "shift" + }, + { + "id": "K_EQUAL", + "text": "=", + "layer": "default" + }, + { + "id": "K_BKSLASH", + "text": "|", + "layer": "shift" + }, + { + "id": "K_BKSLASH", + "text": "\\", + "layer": "default" + }, + { + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "width": "110", + "sp": "1" + }, + { + "id": "K_LBRKT", + "text": "[", + "sk": [ + { + "id": "U_00AB", + "text": "\u00AB" + }, + { + "id": "K_COMMA", + "text": "<", + "layer": "shift" + }, + { + "id": "K_LBRKT", + "text": "{", + "layer": "shift" + } + ] + }, + { + "id": "K_9", + "text": "(", + "layer": "shift" + }, + { + "id": "K_0", + "text": ")", + "layer": "shift" + }, + { + "id": "K_RBRKT", + "text": "]", + "sk": [ + { + "id": "U_00BB", + "text": "\u00BB" + }, + { + "id": "K_PERIOD", + "text": ">", + "layer": "shift" + }, + { + "id": "K_RBRKT", + "text": "}", + "layer": "shift" + } + ] + }, + { + "id": "K_EQUAL", + "text": "+", + "layer": "shift" + }, + { + "id": "K_HYPHEN", + "text": "-", + "layer": "default" + }, + { + "id": "K_8", + "text": "*", + "layer": "shift" + }, + { + "id": "K_SLASH", + "text": "/", + "layer": "default" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "90", + "sp": "1" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_LOWER", + "text": "*abc*", + "width": "140", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "630", + "sp": "0" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "140", + "sp": "1" + } + ] + } + ] + } + ] + }, + "phone": { + "font": "Tahoma", + "layer": [ + { + "id": "default", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "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": 2, + "key": [ + { + "id": "K_A", + "text": "a", + "pad": "50" + }, + { + "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" + }, + { + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "sp": "1", + "nextlayer": "shift" + }, + { + "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_PERIOD", + "text": ".", + "sk": [ + { + "text": ",", + "id": "K_COMMA" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "'", + "id": "K_QUOTE" + }, + { + "text": "\"", + "id": "K_QUOTE", + "layer": "shift" + }, + { + "text": "\\", + "id": "K_BKSLASH" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": "150", + "sp": "1", + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "610", + "sp": "0" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "150", + "sp": "1" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_Q", + "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": 2, + "key": [ + { + "id": "K_A", + "text": "A", + "pad": "50" + }, + { + "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" + }, + { + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_SHIFT", + "text": "*Shift*", + "sp": "2", + "nextlayer": "default" + }, + { + "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_PERIOD", + "text": ".", + "layer": "default", + "sk": [ + { + "text": ",", + "id": "K_COMMA", + "layer": "default" + }, + { + "text": "!", + "id": "K_1", + "layer": "shift" + }, + { + "text": "?", + "id": "K_SLASH", + "layer": "shift" + }, + { + "text": "'", + "id": "K_QUOTE", + "layer": "default" + }, + { + "text": "\"", + "id": "K_QUOTE", + "layer": "shift" + }, + { + "text": "\\", + "id": "K_BKSLASH", + "layer": "default" + }, + { + "text": ":", + "id": "K_COLON", + "layer": "shift" + }, + { + "text": ";", + "id": "K_COLON", + "layer": "default" + } + ] + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "sp": "1" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_NUMLOCK", + "text": "*123*", + "width": "150", + "sp": "1", + "nextlayer": "numeric" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "610", + "sp": "0" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "150", + "sp": "1" + } + ] + } + ] + }, + { + "id": "numeric", + "row": [ + { + "id": 1, + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + } + ] + }, + { + "id": 2, + "key": [ + { + "id": "K_4", + "layer": "shift", + "text": "$", + "pad": "50" + }, + { + "id": "K_2", + "layer": "shift", + "text": "@" + }, + { + "id": "K_3", + "layer": "shift", + "text": "#" + }, + { + "id": "K_5", + "layer": "shift", + "text": "%" + }, + { + "id": "K_7", + "layer": "shift", + "text": "&" + }, + { + "id": "K_HYPHEN", + "layer": "shift", + "text": "_" + }, + { + "id": "K_EQUAL", + "text": "=", + "layer": "default" + }, + { + "id": "K_BKSLASH", + "layer": "shift", + "text": "|" + }, + { + "id": "K_BKSLASH", + "text": "\\", + "layer": "default" + }, + { + "text": "", + "width": "10", + "sp": "10" + } + ] + }, + { + "id": 3, + "key": [ + { + "id": "K_LBRKT", + "text": "[", + "pad": "110", + "sk": [ + { + "id": "U_00AB", + "text": "\u00AB" + }, + { + "id": "K_COMMA", + "text": "<", + "layer": "shift" + }, + { + "id": "K_LBRKT", + "text": "{", + "layer": "shift" + } + ] + }, + { + "id": "K_9", + "layer": "shift", + "text": "(" + }, + { + "id": "K_0", + "layer": "shift", + "text": ")" + }, + { + "id": "K_RBRKT", + "text": "]", + "sk": [ + { + "id": "U_00BB", + "text": "\u00BB" + }, + { + "id": "K_PERIOD", + "text": ">", + "layer": "shift" + }, + { + "id": "K_RBRKT", + "text": "}", + "layer": "shift" + } + ] + }, + { + "id": "K_EQUAL", + "layer": "shift", + "text": "+" + }, + { + "id": "K_HYPHEN", + "text": "-" + }, + { + "id": "K_8", + "layer": "shift", + "text": "*" + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "id": "K_BKSP", + "text": "*BkSp*", + "width": "100", + "sp": "1" + } + ] + }, + { + "id": 4, + "key": [ + { + "id": "K_LOWER", + "text": "*abc*", + "width": "150", + "sp": "1", + "nextlayer": "default" + }, + { + "id": "K_LOPT", + "text": "*Menu*", + "width": "120", + "sp": "1" + }, + { + "id": "K_SPACE", + "text": "", + "width": "610", + "sp": "0" + }, + { + "id": "K_ENTER", + "text": "*Enter*", + "width": "150", + "sp": "1" + } + ] + } + ] + } + ] + } +} diff --git a/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn new file mode 100644 index 0000000000..eb216137e8 --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn @@ -0,0 +1,32 @@ +c start_of_sentence_3621 generated from template at 2021-11-22 15:39:24 +c with name "start_of_sentence_3621" +store(&VERSION) '10.0' +store(&NAME) 'start_of_sentence_3621' +store(©RIGHT) '©' +store(&KEYBOARDVERSION) '1.0' +store(&TARGETS) 'any' +store(&VISUALKEYBOARD) 'start_of_sentence_3621.kvks' +store(&LAYOUTFILE) 'start_of_sentence_3621.keyman-touch-layout' + +begin Unicode > use(main) +begin NewContext > use(NewContext) +c begin PostKeystroke > use(PostKeystroke) + +group(NewContext) + + nomatch > use(nextLayer) + +c group(PostKeystroke) +c if(&layerChanged = "0") > use(nextLayer) +c nomatch > use(nextLayer) + +group(nextLayer) + + store(sentencePunctuation) '.?!' + nul > layer('shift') + any(sentencePunctuation) ' ' > context layer('shift') + any(sentencePunctuation) ' ' > context layer('shift') + nomatch > layer('default') + +group(main) using keys + diff --git a/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kps b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kps new file mode 100644 index 0000000000..7868ce9e00 --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kps @@ -0,0 +1,67 @@ + + + + 15.0.114.0 + 7.0 + + + + readme.htm + + + + + + + + + + start_of_sentence_3621 + © + + + + + + ..\build\start_of_sentence_3621.kmx + + 0 + .kmx + + + ..\build\start_of_sentence_3621.js + + 0 + .js + + + ..\build\start_of_sentence_3621.kvk + + 0 + .kvk + + + welcome.htm + + 0 + .htm + + + readme.htm + + 0 + .htm + + + + + start_of_sentence_3621 + start_of_sentence_3621 + 1.0 + + English + + + + + diff --git a/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kvks b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kvks new file mode 100644 index 0000000000..f9d21e8944 --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kvks @@ -0,0 +1,8 @@ + + +
+ 10.0 + start_of_sentence_3621 + +
+
diff --git a/common/test/keyboards/start_of_sentence_3621/source/welcome.htm b/common/test/keyboards/start_of_sentence_3621/source/welcome.htm new file mode 100644 index 0000000000..5d14db8311 --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/source/welcome.htm @@ -0,0 +1,26 @@ + + + + + + Start Using start_of_sentence_3621 + + + + +

Start Using start_of_sentence_3621

+ +

+ start_of_sentence_3621 1.0 generated from template. +

+ +

Keyboard Layout

+ + + + + \ No newline at end of file diff --git a/common/test/keyboards/start_of_sentence_3621/start_of_sentence_3621.keyboard_info b/common/test/keyboards/start_of_sentence_3621/start_of_sentence_3621.keyboard_info new file mode 100644 index 0000000000..70b79803fa --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/start_of_sentence_3621.keyboard_info @@ -0,0 +1,7 @@ +{ + "license": "mit", + "languages": [ + "en" + ], + "description": "start_of_sentence_3621 generated from template" +} diff --git a/common/test/keyboards/start_of_sentence_3621/start_of_sentence_3621.kpj b/common/test/keyboards/start_of_sentence_3621/start_of_sentence_3621.kpj new file mode 100644 index 0000000000..064471c950 --- /dev/null +++ b/common/test/keyboards/start_of_sentence_3621/start_of_sentence_3621.kpj @@ -0,0 +1,102 @@ + + + + $PROJECTPATH\build + True + True + True + keyboard + + + + id_51d2ccbdcbeeb798d532100b86252a19 + start_of_sentence_3621.kmn + source\start_of_sentence_3621.kmn + 1.0 + .kmn +
+ start_of_sentence_3621 + © +
+
+ + id_feb278f5f21f5a2c56418aef3d610100 + start_of_sentence_3621.kps + source\start_of_sentence_3621.kps + + .kps +
+ start_of_sentence_3621 + © +
+
+ + id_ede98e4633e239f933cbfd1f4e1b766c + HISTORY.md + HISTORY.md + + .md + + + id_53e892b8b41cc4caece1cfd5ef21d6e7 + LICENSE.md + LICENSE.md + + .md + + + id_0730bb7c2e8f9ea2438b52e419dd86c9 + README.md + README.md + + .md + + + id_31c9e544d5ff29ebb5ee3006e39f5624 + start_of_sentence_3621.keyboard_info + start_of_sentence_3621.keyboard_info + + .keyboard_info + + + id_8b42366ff2333c0b6d85e2d23cf930d7 + start_of_sentence_3621.kmx + source\..\build\start_of_sentence_3621.kmx + + .kmx + id_feb278f5f21f5a2c56418aef3d610100 + + + id_b0e85ab76446356d09d86b4187efbdad + start_of_sentence_3621.js + source\..\build\start_of_sentence_3621.js + + .js + id_feb278f5f21f5a2c56418aef3d610100 + + + id_3659e166ed1a8f97952e727f3a571a88 + start_of_sentence_3621.kvk + source\..\build\start_of_sentence_3621.kvk + + .kvk + id_feb278f5f21f5a2c56418aef3d610100 + + + id_356e5d149c1e539356d72698c1e401a6 + welcome.htm + source\welcome.htm + + .htm + id_feb278f5f21f5a2c56418aef3d610100 + + + id_8da344c4cea6f467013357fe099006f5 + readme.htm + source\readme.htm + + .htm + id_feb278f5f21f5a2c56418aef3d610100 + +
+
diff --git a/web/testing/start-of-sentence-3621/index.html b/web/testing/start-of-sentence-3621/index.html new file mode 100644 index 0000000000..8f2f2ff69f --- /dev/null +++ b/web/testing/start-of-sentence-3621/index.html @@ -0,0 +1,89 @@ + + + + + + + + + KeymanWeb start-of-sentence-3621 Tests + + + + + + + + + + + + + + + + +

KeymanWeb Sample Page - Start-of-Sentence-3621 Testing

+

See issue #3621 for details.

+ +
+
+ +
+ +

Type in your language in this text area:

+ + +

or in this input field:

+ + +
+

Return to testing home page

+ + + + + diff --git a/web/testing/start-of-sentence-3621/start_of_sentence_3621.js b/web/testing/start-of-sentence-3621/start_of_sentence_3621.js new file mode 100644 index 0000000000..0d406e4f62 --- /dev/null +++ b/web/testing/start-of-sentence-3621/start_of_sentence_3621.js @@ -0,0 +1,1389 @@ +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_start_of_sentence_3621()); +} +function Keyboard_start_of_sentence_3621() +{ + 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_start_of_sentence_3621"; + this.KN="start_of_sentence_3621"; + this.KMINVER="10.0"; + this.KV={F:' 1em "Arial"',K102:0}; + this.KV.KLS={ + + }; + 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=0 /* 0x0000 */; + this.KVKL={ + "phone": { + "font": "Tahoma", + "displayUnderlying": false, + "layer": [ + { + "id": "default", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_Q", + "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": "2", + "key": [ + { + "id": "K_A", + "pad": "50", + "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" + }, + { + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "nextlayer": "shift", + "id": "K_SHIFT", + "sp": "1", + "text": "*Shift*" + }, + { + "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_PERIOD", + "text": ".", + "sk": [ + { + "id": "K_COMMA", + "text": "," + }, + { + "layer": "shift", + "id": "K_1", + "text": "!" + }, + { + "layer": "shift", + "id": "K_SLASH", + "text": "?" + }, + { + "id": "K_QUOTE", + "text": "'" + }, + { + "layer": "shift", + "id": "K_QUOTE", + "text": "\"" + }, + { + "id": "K_BKSLASH", + "text": "\\" + }, + { + "layer": "shift", + "id": "K_COLON", + "text": ":" + }, + { + "id": "K_COLON", + "text": ";" + } + ] + }, + { + "width": "100", + "id": "K_BKSP", + "sp": "1", + "text": "*BkSp*" + } + ] + }, + { + "id": "4", + "key": [ + { + "nextlayer": "numeric", + "width": "150", + "id": "K_NUMLOCK", + "sp": "1", + "text": "*123*" + }, + { + "width": "120", + "id": "K_LOPT", + "sp": "1", + "text": "*Menu*" + }, + { + "width": "610", + "id": "K_SPACE" + }, + { + "width": "150", + "id": "K_ENTER", + "sp": "1", + "text": "*Enter*" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_Q", + "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": "2", + "key": [ + { + "id": "K_A", + "pad": "50", + "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" + }, + { + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "nextlayer": "default", + "id": "K_SHIFT", + "sp": "2", + "text": "*Shift*" + }, + { + "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" + }, + { + "layer": "default", + "id": "K_PERIOD", + "text": ".", + "sk": [ + { + "layer": "default", + "id": "K_COMMA", + "text": "," + }, + { + "layer": "shift", + "id": "K_1", + "text": "!" + }, + { + "layer": "shift", + "id": "K_SLASH", + "text": "?" + }, + { + "layer": "default", + "id": "K_QUOTE", + "text": "'" + }, + { + "layer": "shift", + "id": "K_QUOTE", + "text": "\"" + }, + { + "layer": "default", + "id": "K_BKSLASH", + "text": "\\" + }, + { + "layer": "shift", + "id": "K_COLON", + "text": ":" + }, + { + "layer": "default", + "id": "K_COLON", + "text": ";" + } + ] + }, + { + "id": "K_BKSP", + "sp": "1", + "text": "*BkSp*" + } + ] + }, + { + "id": "4", + "key": [ + { + "nextlayer": "numeric", + "width": "150", + "id": "K_NUMLOCK", + "sp": "1", + "text": "*123*" + }, + { + "width": "120", + "id": "K_LOPT", + "sp": "1", + "text": "*Menu*" + }, + { + "width": "610", + "id": "K_SPACE" + }, + { + "width": "150", + "id": "K_ENTER", + "sp": "1", + "text": "*Enter*" + } + ] + } + ] + }, + { + "id": "numeric", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + } + ] + }, + { + "id": "2", + "key": [ + { + "layer": "shift", + "id": "K_4", + "pad": "50", + "text": "$" + }, + { + "layer": "shift", + "id": "K_2", + "text": "@" + }, + { + "layer": "shift", + "id": "K_3", + "text": "#" + }, + { + "layer": "shift", + "id": "K_5", + "text": "%" + }, + { + "layer": "shift", + "id": "K_7", + "text": "&" + }, + { + "layer": "shift", + "id": "K_HYPHEN", + "text": "_" + }, + { + "layer": "default", + "id": "K_EQUAL", + "text": "=" + }, + { + "layer": "shift", + "id": "K_BKSLASH", + "text": "|" + }, + { + "layer": "default", + "id": "K_BKSLASH", + "text": "\\" + }, + { + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "id": "K_LBRKT", + "pad": "110", + "text": "[", + "sk": [ + { + "id": "U_00AB", + "text": "\u00AB" + }, + { + "layer": "shift", + "id": "K_COMMA", + "text": "<" + }, + { + "layer": "shift", + "id": "K_LBRKT", + "text": "{" + } + ] + }, + { + "layer": "shift", + "id": "K_9", + "text": "(" + }, + { + "layer": "shift", + "id": "K_0", + "text": ")" + }, + { + "id": "K_RBRKT", + "text": "]", + "sk": [ + { + "id": "U_00BB", + "text": "\u00BB" + }, + { + "layer": "shift", + "id": "K_PERIOD", + "text": ">" + }, + { + "layer": "shift", + "id": "K_RBRKT", + "text": "}" + } + ] + }, + { + "layer": "shift", + "id": "K_EQUAL", + "text": "+" + }, + { + "id": "K_HYPHEN", + "text": "-" + }, + { + "layer": "shift", + "id": "K_8", + "text": "*" + }, + { + "id": "K_SLASH", + "text": "/" + }, + { + "width": "100", + "id": "K_BKSP", + "sp": "1", + "text": "*BkSp*" + } + ] + }, + { + "id": "4", + "key": [ + { + "nextlayer": "default", + "width": "150", + "id": "K_LOWER", + "sp": "1", + "text": "*abc*" + }, + { + "width": "120", + "id": "K_LOPT", + "sp": "1", + "text": "*Menu*" + }, + { + "width": "610", + "id": "K_SPACE" + }, + { + "width": "150", + "id": "K_ENTER", + "sp": "1", + "text": "*Enter*" + } + ] + } + ] + } + ] + }, + "tablet": { + "font": "Tahoma", + "displayUnderlying": false, + "layer": [ + { + "id": "default", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_Q", + "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": "2", + "key": [ + { + "id": "K_A", + "pad": "70", + "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" + }, + { + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "nextlayer": "shift", + "width": "110", + "id": "K_SHIFT", + "sp": "1", + "text": "*Shift*" + }, + { + "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_PERIOD", + "text": ".", + "sk": [ + { + "id": "K_COMMA", + "text": "," + }, + { + "layer": "shift", + "id": "K_1", + "text": "!" + }, + { + "layer": "shift", + "id": "K_SLASH", + "text": "?" + }, + { + "id": "K_QUOTE", + "text": "'" + }, + { + "layer": "shift", + "id": "K_QUOTE", + "text": "\"" + }, + { + "id": "K_BKSLASH", + "text": "\\" + }, + { + "layer": "shift", + "id": "K_COLON", + "text": ":" + }, + { + "id": "K_COLON", + "text": ";" + } + ] + }, + { + "width": "90", + "id": "K_BKSP", + "sp": "1", + "text": "*BkSp*" + } + ] + }, + { + "id": "4", + "key": [ + { + "nextlayer": "numeric", + "width": "140", + "id": "K_NUMLOCK", + "sp": "1", + "text": "*123*" + }, + { + "width": "120", + "id": "K_LOPT", + "sp": "1", + "text": "*Menu*" + }, + { + "width": "630", + "id": "K_SPACE" + }, + { + "width": "140", + "id": "K_ENTER", + "sp": "1", + "text": "*Enter*" + } + ] + } + ] + }, + { + "id": "shift", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_Q", + "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": "2", + "key": [ + { + "id": "K_A", + "pad": "70", + "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" + }, + { + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "nextlayer": "default", + "width": "110", + "id": "K_SHIFT", + "sp": "2", + "text": "*Shift*" + }, + { + "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" + }, + { + "layer": "default", + "id": "K_PERIOD", + "text": ".", + "sk": [ + { + "layer": "default", + "id": "K_COMMA", + "text": "," + }, + { + "layer": "shift", + "id": "K_1", + "text": "!" + }, + { + "layer": "shift", + "id": "K_SLASH", + "text": "?" + }, + { + "layer": "default", + "id": "K_QUOTE", + "text": "'" + }, + { + "layer": "shift", + "id": "K_QUOTE", + "text": "\"" + }, + { + "layer": "default", + "id": "K_BKSLASH", + "text": "\\" + }, + { + "layer": "shift", + "id": "K_COLON", + "text": ":" + }, + { + "layer": "default", + "id": "K_COLON", + "text": ";" + } + ] + }, + { + "width": "90", + "id": "K_BKSP", + "sp": "1", + "text": "*BkSp*" + } + ] + }, + { + "id": "4", + "key": [ + { + "nextlayer": "numeric", + "width": "140", + "id": "K_NUMLOCK", + "sp": "1", + "text": "*123*" + }, + { + "width": "120", + "id": "K_LOPT", + "sp": "1", + "text": "*Menu*" + }, + { + "width": "630", + "id": "K_SPACE" + }, + { + "width": "140", + "id": "K_ENTER", + "sp": "1", + "text": "*Enter*" + } + ] + } + ] + }, + { + "id": "numeric", + "row": [ + { + "id": "1", + "key": [ + { + "id": "K_1", + "text": "1" + }, + { + "id": "K_2", + "text": "2" + }, + { + "id": "K_3", + "text": "3" + }, + { + "id": "K_4", + "text": "4" + }, + { + "id": "K_5", + "text": "5" + }, + { + "id": "K_6", + "text": "6" + }, + { + "id": "K_7", + "text": "7" + }, + { + "id": "K_8", + "text": "8" + }, + { + "id": "K_9", + "text": "9" + }, + { + "id": "K_0", + "text": "0" + } + ] + }, + { + "id": "2", + "key": [ + { + "layer": "shift", + "id": "K_4", + "pad": "70", + "text": "$" + }, + { + "layer": "shift", + "id": "K_2", + "text": "@" + }, + { + "layer": "shift", + "id": "K_3", + "text": "#" + }, + { + "layer": "shift", + "id": "K_5", + "text": "%" + }, + { + "layer": "shift", + "id": "K_7", + "text": "&" + }, + { + "layer": "shift", + "id": "K_HYPHEN", + "text": "_" + }, + { + "layer": "default", + "id": "K_EQUAL", + "text": "=" + }, + { + "layer": "shift", + "id": "K_BKSLASH", + "text": "|" + }, + { + "layer": "default", + "id": "K_BKSLASH", + "text": "\\" + }, + { + "width": "10", + "sp": "10" + } + ] + }, + { + "id": "3", + "key": [ + { + "width": "110", + "id": "K_SHIFT", + "sp": "1", + "text": "*Shift*" + }, + { + "id": "K_LBRKT", + "text": "[", + "sk": [ + { + "id": "U_00AB", + "text": "\u00AB" + }, + { + "layer": "shift", + "id": "K_COMMA", + "text": "<" + }, + { + "layer": "shift", + "id": "K_LBRKT", + "text": "{" + } + ] + }, + { + "layer": "shift", + "id": "K_9", + "text": "(" + }, + { + "layer": "shift", + "id": "K_0", + "text": ")" + }, + { + "id": "K_RBRKT", + "text": "]", + "sk": [ + { + "id": "U_00BB", + "text": "\u00BB" + }, + { + "layer": "shift", + "id": "K_PERIOD", + "text": ">" + }, + { + "layer": "shift", + "id": "K_RBRKT", + "text": "}" + } + ] + }, + { + "layer": "shift", + "id": "K_EQUAL", + "text": "+" + }, + { + "layer": "default", + "id": "K_HYPHEN", + "text": "-" + }, + { + "layer": "shift", + "id": "K_8", + "text": "*" + }, + { + "layer": "default", + "id": "K_SLASH", + "text": "/" + }, + { + "width": "90", + "id": "K_BKSP", + "sp": "1", + "text": "*BkSp*" + } + ] + }, + { + "id": "4", + "key": [ + { + "nextlayer": "default", + "width": "140", + "id": "K_LOWER", + "sp": "1", + "text": "*abc*" + }, + { + "width": "120", + "id": "K_LOPT", + "sp": "1", + "text": "*Menu*" + }, + { + "width": "630", + "id": "K_SPACE" + }, + { + "width": "140", + "id": "K_ENTER", + "sp": "1", + "text": "*Enter*" + } + ] + } + ] + } + ] + } +} +; + this.s_sentencePunctuation_11=".?!"; + this.s15="shift"; + this.s16="shift"; + this.s17="shift"; + this.s18="default"; + this.KVER="15.0.114.0"; + this.KVS=[]; + this.gs=function(t,e) { + return this.g_main_1(t,e); + }; + this.gn=function(t,e) { + return this.g_NewContext_0(t,e); + }; + this.gs=function(t,e) { + return this.g_main_1(t,e); + }; + this.g_NewContext_0=function(t,e) { + var k=KeymanWeb,r=1,m=0; + if(k.KFCM(3,t,[{t:'a',a:this.s_sentencePunctuation_11},' ',' '])){ + m=1; // Line 19 + k.KDC(3,t); + k.KIO(-1,this.s_sentencePunctuation_11,1,t); + k.KO(-1,t," "); + k.KSETS(33,this.s17,t); + } + else if(k.KFCM(2,t,[{t:'a',a:this.s_sentencePunctuation_11},' '])){ + m=1; // Line 18 + k.KDC(2,t); + k.KIO(-1,this.s_sentencePunctuation_11,1,t); + k.KO(-1,t," "); + k.KSETS(33,this.s16,t); + } + else if(k.KFCM(1,t,[{t:'n'}])){ + m=1; // Line 17 + k.KDC(0,t); + k.KSETS(33,this.s15,t); + } + if(!m) { + + k.KDC(-1,t); + k.KSETS(33,this.s18,t); + } + return r; + }; + this.g_main_1=function(t,e) { + var k=KeymanWeb,r=0,m=0; + return r; + }; +} From 80db98a5f942e144a9049603249f893f39af5c71 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 23 Nov 2021 10:13:14 +1100 Subject: [PATCH 02/54] feat(web): compiler support for newContext, postKeystroke Relates to #3621. This adds basic compiler support for the `begin newContext` and `begin postKeystroke` statements, along with the corresponding `gn` and `gpk` entry points in the compiled JavaScript keyboard. TODO: `readonly` semantics, usage constraints, `&layerChanged`. --- .../TIKE/compile/CompileKeymanWeb.pas | 31 +++++++++++++++- windows/src/developer/kmcmpdll/Compiler.cpp | 35 ++++++++++++++----- .../global/delphi/general/kmxfileconsts.pas | 17 +++++++-- windows/src/global/inc/Compiler.h | 9 ++++- 4 files changed, 79 insertions(+), 13 deletions(-) diff --git a/windows/src/developer/TIKE/compile/CompileKeymanWeb.pas b/windows/src/developer/TIKE/compile/CompileKeymanWeb.pas index 55e59479a7..36c785b478 100644 --- a/windows/src/developer/TIKE/compile/CompileKeymanWeb.pas +++ b/windows/src/developer/TIKE/compile/CompileKeymanWeb.pas @@ -252,6 +252,8 @@ type function JavaScript_SetupEpilog: string; function JavaScript_SetupProlog: string; function IsKeyboardVersion15OrLater: Boolean; + function WriteBeginStatement(const name: string; + groupIndex: Integer): string; public function Compile(AOwnerProject: TProject; const InFile: string; const OutFile: string; Debug: Boolean; Callback: TCompilerCallbackW): Boolean; // I3681 // I4140 // I4688 // I4866 constructor Create; @@ -1845,6 +1847,20 @@ begin Result := True; end; +function TCompileKeymanWeb.WriteBeginStatement(const name: string; groupIndex: Integer): string; +var + fgp: PFILE_GROUP; +begin + fgp := fk.dpGroupArray; Inc(fgp, groupIndex); + Result := Format( + '%sthis.%s=function(t,e) {%s'+ + '%sreturn this.g%s(t,e);%s'+ + '%s};%s', + [FTabStop, name, nl, + FTabStop+FTabStop, JavaScript_Name(groupIndex, fgp.szName), nl, + FTabStop, nl]); // I3681 +end; + //{$WARNINGS OFF} // bug in Delphi compiler returning W1035 return value undefined?!? function TCompileKeymanWeb.WriteCompiledKeyboard: string; {UTF8} function Requote(const S: string): string; @@ -1866,12 +1882,14 @@ var vMnemonic: Integer; s, sRTL, sHelp, sHelpFile, sName, sEmbedJS, sEmbedCSS: string; sVisualKeyboard, sFullName: WideString; + sBegin_NewContext, sBegin_PostKeystroke: string; sLayoutFile, sVKDictionary: string; linecomment: string; // I3438 HasRules: Boolean; sModifierBitmask: string; fDisplayUnderlying: Boolean; FOptionStores: string; + rec: TSentinelRecord; begin Result := '';//UTF16SignatureW; // + '// compiled by Keyman Developer'+nl; // I3474 { Locate the name of the keyboard } @@ -1909,7 +1927,11 @@ begin else if fsp.dwSystemID = TSS_VKDICTIONARY then // I3438 sVKDictionary := fsp.dpString else if fsp.dwSystemID = TSS_LAYOUTFILE then // I3483 - sLayoutFile := fsp.dpString; + sLayoutFile := fsp.dpString + else if fsp.dwSystemID = TSS_BEGIN_NEWCONTEXT then + sBegin_NewContext := fsp.dpString + else if fsp.dwSystemID = TSS_BEGIN_POSTKEYSTROKE then + sBegin_PostKeystroke := fsp.dpString; Inc(fsp); end; @@ -2127,6 +2149,13 @@ begin Exit; end;} + Result := Result + WriteBeginStatement('gs', fk.StartGroup[BEGIN_UNICODE]); + rec := ExpandSentinel(PChar(sBegin_NewContext)); + if rec.Code = CODE_USE then + Result := Result + WriteBeginStatement('gn', rec.Use.GroupIndex); + rec := ExpandSentinel(PChar(sBegin_PostKeystroke)); + if rec.Code = CODE_USE then + Result := Result + WriteBeginStatement('gpk', rec.Use.GroupIndex); fgp := fk.dpGroupArray; Inc(fgp, fk.StartGroup[BEGIN_UNICODE]); Result := Result + Format( diff --git a/windows/src/developer/kmcmpdll/Compiler.cpp b/windows/src/developer/kmcmpdll/Compiler.cpp index d1ed792a99..475e4f7e57 100644 --- a/windows/src/developer/kmcmpdll/Compiler.cpp +++ b/windows/src/developer/kmcmpdll/Compiler.cpp @@ -193,6 +193,8 @@ const PWCHAR StoreTokens[TSS__MAX + 2] = { SSN__PREFIX L"KMW_EMBEDCSS", SSN__PREFIX L"TARGETS", // I4504 SSN__PREFIX L"CASEDKEYS", // #2241 + SSN__PREFIX L"", // TSS_BEGIN_NEWCONTEXT + SSN__PREFIX L"", // TSS_BEGIN_POSTKEYSTROKE NULL }; @@ -578,22 +580,33 @@ DWORD ProcessBeginLine(PFILE_KEYBOARD fk, PWSTR p) while (iswspace(*p)) p++; if (_wcsnicmp(p, L"unicode", 7) == 0) BeginMode = BEGIN_UNICODE; else if (_wcsnicmp(p, L"ansi", 4) == 0) BeginMode = BEGIN_ANSI; + else if (_wcsnicmp(p, L"newContext", 10) == 0) BeginMode = BEGIN_NEWCONTEXT; + else if (_wcsnicmp(p, L"postKeystroke", 13) == 0) BeginMode = BEGIN_POSTKEYSTROKE; else if (*p != '>') return CERR_InvalidToken; else BeginMode = BEGIN_ANSI; if ((msg = GetRHS(fk, p, tstr, 80, (int)(INT_PTR)(p - pp), FALSE)) != CERR_None) return msg; - if (tstr[0] != UC_SENTINEL || tstr[1] != CODE_USE) return CERR_InvalidBegin; + if (tstr[0] != UC_SENTINEL || tstr[1] != CODE_USE) { + return CERR_InvalidBegin; + } + if (tstr[3] != 0) { + return CERR_InvalidToken; + } - fk->StartGroup[BeginMode] = tstr[2] - 1; - //mcd-03-01-2000: removed the secondary group idea; this was undocumented and - //is not supported under Keyman 5.0: ugly!! - //if(tstr[3] == UC_SENTINEL && tstr[4] == CODE_USE) fk->StartGroup[1] = tstr[5] - 1; - if (tstr[3] != 0) return CERR_InvalidToken; + if (BeginMode == BEGIN_ANSI || BeginMode == BEGIN_UNICODE) { + fk->StartGroup[BeginMode] = tstr[2] - 1; + // mcd-03-01-2000: removed the secondary group idea; this was undocumented and + // is not supported under Keyman 5.0: ugly!! + // if(tstr[3] == UC_SENTINEL && tstr[4] == CODE_USE) fk->StartGroup[1] = tstr[5] - 1; - if (FSaveDebug) - /* Record a system store for the line number of the begin statement */ - AddDebugStore(fk, BeginMode == BEGIN_UNICODE ? DEBUGSTORE_BEGIN L"Unicode" : DEBUGSTORE_BEGIN L"ANSI"); + if (FSaveDebug) { + /* Record a system store for the line number of the begin statement */ + AddDebugStore(fk, BeginMode == BEGIN_UNICODE ? DEBUGSTORE_BEGIN L"Unicode" : DEBUGSTORE_BEGIN L"ANSI"); + } + } else { + return AddStore(fk, BeginMode == BEGIN_NEWCONTEXT ? TSS_BEGIN_NEWCONTEXT : TSS_BEGIN_POSTKEYSTROKE, tstr, NULL); + } return CERR_None; } @@ -1325,6 +1338,10 @@ DWORD ProcessSystemStore(PFILE_KEYBOARD fk, DWORD SystemID, PFILE_STORE sp) } break; + case TSS_BEGIN_NEWCONTEXT: + case TSS_BEGIN_POSTKEYSTROKE: + break; + default: return CERR_InvalidSystemStore; } diff --git a/windows/src/global/delphi/general/kmxfileconsts.pas b/windows/src/global/delphi/general/kmxfileconsts.pas index 064465db5d..51c5de4ee0 100644 --- a/windows/src/global/delphi/general/kmxfileconsts.pas +++ b/windows/src/global/delphi/general/kmxfileconsts.pas @@ -200,6 +200,15 @@ const TSS_KMW_EMBEDCSS = 37; // I4368 TSS_TARGETS = 38; // I4504 + { V14.0 } + + TSS_CASEDKEYS = 39; + + { V15.0 } + + TSS_BEGIN_NEWCONTEXT = 40; + TSS_BEGIN_POSTKEYSTROKE = 41; + type TSystemStore = (ssNone = 0, ssBitmap = 1, ssCopyright = 2, ssHotkey = 3, ssLanguage = 4, ssLayout = 5, ssMessage = 6, ssName = 7, ssVersion = 8, ssCapsOnOnly = 9, ssCapsAlwaysOff = 10, ssShiftFreesCaps = 11, ssLanguageName = 12, @@ -210,7 +219,9 @@ type ssWindowsLanguages = 29, ssComparison = 30, ssPlatform = 31, ssBaseLayout = 32, ssLayer = 33, ssVKDictionary = 34, ssLayoutFile = 35, // I3438 // I3483 - ssKeyboardVersion = 36, ssKMW_EmbedCSS = 37, ssTargets = 38); // I4140 // I4368 // I4504 + ssKeyboardVersion = 36, ssKMW_EmbedCSS = 37, ssTargets = 38, + ssCasedKeys = 39, + ssBegin_NewContext = 40, ssBegin_PostKeystroke = 41); // I4140 // I4368 // I4504 const SystemStoreNames: array[TSystemStore] of WideString = ( @@ -223,7 +234,9 @@ const 'WINDOWSLANGUAGES', '', //8.0 '', '', '', '', 'LAYOUTFILE', 'KEYBOARDVERSION', 'KMW_EMBEDCSS', - 'TARGETS'); //9.0 // I3483 // I4140 // I4368 // I4504 + 'TARGETS', //9.0 // I3483 // I4140 // I4368 // I4504 + 'CASEDKEYS', //14.0, + '', ''); //15.0 implementation diff --git a/windows/src/global/inc/Compiler.h b/windows/src/global/inc/Compiler.h index 4a45e04a17..e69c306bf4 100644 --- a/windows/src/global/inc/Compiler.h +++ b/windows/src/global/inc/Compiler.h @@ -132,6 +132,8 @@ // Different begin types #define BEGIN_ANSI 0 #define BEGIN_UNICODE 1 +#define BEGIN_NEWCONTEXT 2 +#define BEGIN_POSTKEYSTROKE 3 //#define lpuch (LPBYTE) @@ -204,7 +206,12 @@ #define TSS_CASEDKEYS 39 -#define TSS__MAX 39 +#define TSS__KEYMAN_140_MAX 39 + +#define TSS_BEGIN_NEWCONTEXT 40 +#define TSS_BEGIN_POSTKEYSTROKE 41 + +#define TSS__MAX 41 /* wm_keyman_control_internal message control codes */ From bbbf1afce77ab53391e31788d47ff0577fbb7192 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 23 Nov 2021 15:00:47 +1100 Subject: [PATCH 03/54] feat(web): basic support for newContext Relates to #3621. Adds basic support for `begin newContext`. --- .../src/text/inputProcessor.ts | 53 ++- .../src/keyboards/keyboard.ts | 4 + .../src/text/kbdInterface.ts | 54 +++ .../src/text/keyboardProcessor.ts | 13 + web/source/dom/domEventHandlers.ts | 224 +++++----- web/source/dom/domManager.ts | 394 +++++++++--------- web/source/dom/preProcessor.ts | 50 +-- web/source/keyboards/kmwkeyboards.ts | 4 + web/source/kmwbase.ts | 2 +- web/source/osk/oskView.ts | 4 +- 10 files changed, 467 insertions(+), 335 deletions(-) diff --git a/common/core/web/input-processor/src/text/inputProcessor.ts b/common/core/web/input-processor/src/text/inputProcessor.ts index 501f90ed57..d67bb7687f 100644 --- a/common/core/web/input-processor/src/text/inputProcessor.ts +++ b/common/core/web/input-processor/src/text/inputProcessor.ts @@ -10,14 +10,16 @@ namespace com.keyman.text { baseLayout: 'us' } + private device: utils.DeviceSpec; private kbdProcessor: KeyboardProcessor; private lngProcessor: prediction.LanguageProcessor; - constructor(options?: ProcessorInitOptions) { + constructor(device: utils.DeviceSpec, options?: ProcessorInitOptions) { if(!options) { options = InputProcessor.DEFAULT_OPTIONS; } + this.device = device; this.kbdProcessor = new KeyboardProcessor(options); this.lngProcessor = new prediction.LanguageProcessor(); } @@ -50,11 +52,28 @@ namespace com.keyman.text { return this.languageProcessor.activeModel; } - /** + /** + * + * @param outputTarget + * @returns + */ + processNewContextEvent(outputTarget: OutputTarget): RuleBehavior { + // We presently need the true keystroke to run on the FULL context. That index is still + // needed for some indexing operations when comparing two different output targets. + const ruleBehavior = this.keyboardProcessor.processNewContextEvent(this.device, outputTarget); + + // Should we swallow any further processing of keystroke events for this? + if(ruleBehavior != null) { + ruleBehavior.finalize(this.keyboardProcessor, outputTarget); + } + return ruleBehavior; + } + + /** * Simulate a keystroke according to the touched keyboard button element * * Handles default output and keyboard processing for both OSK and physical keystrokes. - * + * * @param {Object} keyEvent The abstracted KeyEvent to use for keystroke processing * @param {Object} outputTarget The OutputTarget receiving the KeyEvent * @returns {Object} A RuleBehavior object describing the cumulative effects of @@ -113,7 +132,7 @@ namespace com.keyman.text { if(keyEvent.kNextLayer) { this.keyboardProcessor.selectLayer(keyEvent); } - + // Should we swallow any further processing of keystroke events for this keydown-keypress sequence? if(ruleBehavior != null) { let alternates: Alternate[]; @@ -123,7 +142,7 @@ namespace com.keyman.text { if(this.languageProcessor.isActive && !ruleBehavior.triggersDefaultCommand) { let keyDistribution = keyEvent.keyDistribution; - // We don't need to track absolute indexing during alternate-generation; + // We don't need to track absolute indexing during alternate-generation; // only position-relative, so it's better to use a sliding window for context // when making alternates. (Slightly worse for short text, matters greatly // for long text.) @@ -141,7 +160,7 @@ namespace com.keyman.text { let _globalThis = com.keyman.utils.getGlobalObject(); let timer: () => number; - // Available by default on `window` in browsers, but _not_ on `global` in Node, + // Available by default on `window` in browsers, but _not_ on `global` in Node, // surprisingly. Since we can't use code dependent on `require` statements // at present, we have to condition upon it actually existing. if(_globalThis['performance'] && _globalThis['performance']['now']) { @@ -152,15 +171,15 @@ namespace com.keyman.text { TIMEOUT_THRESHOLD = timer() + 16; // + 16ms. } // else { // We _could_ just use Date.now() as a backup... but that (probably) only matters - // when unit testing. So... we actually don't _need_ time thresholding when in + // when unit testing. So... we actually don't _need_ time thresholding when in // a Node environment. // } // Tracks a minimum probability for keystroke probability. Anything less will not be - // included in alternate calculations. + // included in alternate calculations. // // Seek to match SearchSpace.EDIT_DISTANCE_COST_SCALE from the predictive-text engine. - // Reasoning for the selected value may be seen there. Short version - keystrokes + // Reasoning for the selected value may be seen there. Short version - keystrokes // that _appear_ very precise may otherwise not even consider directly-neighboring keys. let KEYSTROKE_EPSILON = Math.exp(-5); @@ -169,7 +188,7 @@ namespace com.keyman.text { let activeLayout = this.activeKeyboard.layout(keyEvent.device.formFactor); alternates = []; - + let totalMass = 0; // Tracks sum of non-error probabilities. for(let pair of keyDistribution) { if(pair.p < KEYSTROKE_EPSILON) { @@ -184,7 +203,7 @@ namespace com.keyman.text { } let mock = Mock.from(windowedMock); - + let altKey = activeLayout.getLayer(keyEvent.kbdLayer).getKey(pair.keyId); if(!altKey) { console.warn("Potential fat-finger key could not be found in layer!"); @@ -193,14 +212,14 @@ namespace com.keyman.text { let altEvent = altKey.constructKeyEvent(this.keyboardProcessor, keyEvent.device); let alternateBehavior = this.keyboardProcessor.processKeystroke(altEvent, mock); - + // If alternateBehavior.beep == true, ignore it. It's a disallowed key sequence, // so we expect users to never intend their use. // // Also possible that this set of conditions fail for all evaluated alternates. if(alternateBehavior && !alternateBehavior.beep && pair.p > 0) { let transform: Transform = alternateBehavior.transcription.transform; - + // Ensure that the alternate's token id matches that of the current keystroke, as we only // record the matched rule's context (since they match) transform.id = ruleBehavior.transcription.token; @@ -224,7 +243,7 @@ namespace com.keyman.text { ruleBehavior.finalize(this.keyboardProcessor, outputTarget); // -- All keystroke (and 'alternate') processing is now complete. Time to finalize everything! -- - + // Notify the ModelManager of new input - it's predictive text time! if(alternates && alternates.length > 0) { ruleBehavior.transcription.alternates = alternates; @@ -245,6 +264,12 @@ namespace com.keyman.text { public resetContext(outputTarget?: OutputTarget) { this.keyboardProcessor.resetContext(); this.languageProcessor.invalidateContext(outputTarget); + + // Let the keyboard do its initial group processing + //console.log('processNewContextEvent called from resetContext'); + if(outputTarget) { + this.processNewContextEvent(outputTarget); + } } } } diff --git a/common/core/web/keyboard-processor/src/keyboards/keyboard.ts b/common/core/web/keyboard-processor/src/keyboards/keyboard.ts index 857d2c223d..9b9095cf79 100644 --- a/common/core/web/keyboard-processor/src/keyboards/keyboard.ts +++ b/common/core/web/keyboard-processor/src/keyboards/keyboard.ts @@ -64,6 +64,10 @@ namespace com.keyman.keyboards { return this.scriptObject['gs'](outputTarget, keystroke); } + processNewContextEvent(outputTarget: text.OutputTarget, keystroke: text.KeyEvent): boolean { + return this.scriptObject['gn'] ? this.scriptObject['gn'](outputTarget, keystroke) : false; + } + get isHollow(): boolean { return this.scriptObject == Keyboard.DEFAULT_SCRIPT_OBJECT; } diff --git a/common/core/web/keyboard-processor/src/text/kbdInterface.ts b/common/core/web/keyboard-processor/src/text/kbdInterface.ts index fb729c0468..0576100f94 100644 --- a/common/core/web/keyboard-processor/src/text/kbdInterface.ts +++ b/common/core/web/keyboard-processor/src/text/kbdInterface.ts @@ -948,6 +948,60 @@ namespace com.keyman.text { this.output(1, outputTarget, ""); } + processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { + // Clear internal state tracking data from prior keystrokes. + if(!outputTarget) { + throw "No target specified for keyboard output!"; + } else if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; + } + + outputTarget.invalidateSelection(); + + outputTarget.deadkeys().resetMatched(); // I3318 + this.resetContextCache(); + + // Capture the initial state of the OutputTarget before any rules are matched. + let preInput = Mock.from(outputTarget); + + // Capture the initial state of any variable stores + const cachedVariableStores = this.activeKeyboard.variableStores; + + // Establishes the results object, allowing corresponding commands to set values here as appropriate. + this.ruleBehavior = new RuleBehavior(); + + // Ensure the settings are in place so that KIFS/ifState activates and deactivates + // the appropriate rule(s) for the modeled device. + this.activeDevice = keystroke.device; + + // Calls the start-group of the active keyboard. + this.activeTargetOutput = outputTarget; + var matched = this.activeKeyboard.processNewContextEvent(outputTarget, keystroke); + this.activeTargetOutput = null; + + // Finalize the rule's results. + this.ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(preInput, keystroke); + + // We always backup the changes to variable stores to the RuleBehavior, to + // be applied during finalization, then restore them to the cached initial + // values to avoid side-effects with predictive text mocks. + this.ruleBehavior.variableStores = this.activeKeyboard.variableStores; + this.activeKeyboard.variableStores = cachedVariableStores; + + // `matched` refers to whether or not the FINAL rule (from any group) matched, rather than + // whether or not ANY rule matched. If the final rule doesn't match, we trigger the key's + // default behavior (if appropriate). + // + // See https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852 + this.ruleBehavior.triggerKeyDefault = !matched; + + // Clear our result-tracking variable to prevent any possible pollution for future processing. + let behavior = this.ruleBehavior; + this.ruleBehavior = null; + + return behavior; + } + /** * Function processKeystroke * Scope Private diff --git a/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts index f419d33716..7b2ced989f 100644 --- a/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -207,6 +207,19 @@ namespace com.keyman.text { } } + processNewContextEvent(device: utils.DeviceSpec, outputTarget: OutputTarget): RuleBehavior { + // Pass this key code and state to the keyboard program + if(!this.activeKeyboard) { + return null; + } + let keyEvent = new KeyEvent(); + keyEvent.Lcode = 0; + keyEvent.kName = ''; + keyEvent.device = device; + this.setSyntheticEventDefaults(keyEvent); + return this.keyboardInterface.processNewContextEvent(outputTarget, keyEvent); + } + processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { var matchBehavior: RuleBehavior; diff --git a/web/source/dom/domEventHandlers.ts b/web/source/dom/domEventHandlers.ts index aa362e3a3f..ae586a73d6 100644 --- a/web/source/dom/domEventHandlers.ts +++ b/web/source/dom/domEventHandlers.ts @@ -3,19 +3,19 @@ namespace com.keyman.dom { /* - * Note that for many of the actual events represented by methods in this file, `this` is replaced + * Note that for many of the actual events represented by methods in this file, `this` is replaced * automatically by JavaScript's event handling system. As such, many 'wrapper' variants of the events * exist to restore the object-oriented hierarchy below. - * + * */ export class CommonDOMStates { _DisableInput: boolean = false; // Should input be disabled? - _IgnoreNextSelChange: number = 0; // when a visual keyboard key is mouse-down, ignore the next sel change because this stuffs up our history + _IgnoreNextSelChange: number = 0; // when a visual keyboard key is mouse-down, ignore the next sel change because this stuffs up our history _IgnoreBlurFocus: boolean = false; // Used to temporarily ignore focus changes _Selection = null; _SelectionControl: any = null; // Type behavior is as with activeElement and the like. - + _activeElement: HTMLElement; _lastActiveElement: HTMLElement; @@ -43,7 +43,7 @@ namespace com.keyman.dom { // TODO: resolve/refactor out! protected keyman: KeymanBase; - // This is only static within a given initialization of KeymanWeb. Perhaps it would be best as an initialization + // This is only static within a given initialization of KeymanWeb. Perhaps it would be best as an initialization // parameter and member field? static states: CommonDOMStates = new CommonDOMStates(); @@ -53,12 +53,12 @@ namespace com.keyman.dom { /** * Handle receiving focus by simulated input field - */ + */ setFocus: (e?: TouchEvent|MSPointerEvent) => void = function(e?: TouchEvent|MSPointerEvent): void { // Touch-only handler. }.bind(this); - - /** + + /** * Handles touch-based loss of focus events. */ setBlur: (e: FocusEvent) => void = function(e: FocusEvent) { @@ -71,8 +71,8 @@ namespace com.keyman.dom { //TODO: add more complete description of what ControlFocus really does /** - * Respond to KeymanWeb-aware input element receiving focus - */ + * Respond to KeymanWeb-aware input element receiving focus + */ _ControlFocus: (e: FocusEvent) => boolean = function(this: DOMEventHandlers, e: FocusEvent): boolean { var Ltarg: HTMLElement; var device = this.keyman.util.device; @@ -87,7 +87,7 @@ namespace com.keyman.dom { if(Ltarg['body']) { Ltarg = Ltarg['body']; // Occurs in Firefox for design-mode iframes. } - + // Prevent any action if a protected input field if(device.touchable && (Ltarg.className == null || Ltarg.className.indexOf('keymanweb-input') < 0)) { return true; @@ -102,7 +102,7 @@ namespace com.keyman.dom { } } else if(Ltarg.ownerDocument && Ltarg.ownerDocument.designMode == 'on') { // continue; don't block this one! - } else if((device.touchable || !Ltarg.isContentEditable) + } else if((device.touchable || !Ltarg.isContentEditable) && !(Ltarg.ownerDocument && Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLTextAreaElement)) { return true; } @@ -113,7 +113,7 @@ namespace com.keyman.dom { if (Ltarg.nodeType == 3) { // defeat Safari bug Ltarg = Ltarg.parentNode as HTMLElement; } - + var LfocusTarg = Ltarg; // Ensure that focussed element is visible above the keyboard @@ -122,7 +122,7 @@ namespace com.keyman.dom { (this as DOMTouchHandlers).scrollBody(Ltarg); } } - + if(Ltarg.ownerDocument && Ltarg instanceof Ltarg.ownerDocument.defaultView.HTMLIFrameElement) { //**TODO: check case reference this.keyman.domManager._AttachToIframe(Ltarg as HTMLIFrameElement); Ltarg=Ltarg.contentWindow.document.body; @@ -131,8 +131,8 @@ namespace com.keyman.dom { // Must set before _Blur / _Focus to avoid infinite recursion due to complications // in setActiveKeyboard behavior with managed keyboard settings. this.keyman.domManager.lastActiveElement = Ltarg; - this.keyman.domManager.activeElement = Ltarg; // I3363 (Build 301) - + this.keyman.domManager.activeElement = Ltarg; // I3363 (Build 301) + if(this.keyman.uiManager.justActivated) { this._BlurKeyboardSettings(Ltarg); } else { @@ -160,9 +160,9 @@ namespace com.keyman.dom { * Scope Private * @param {Object} _target element gaining focus * @param {Object} _activeControl currently active control - * @return {boolean} + * @return {boolean} * Description Execute external (UI) code needed on focus - */ + */ doControlFocused(_target: HTMLElement, _activeControl: HTMLElement): boolean { var p={}; p['target']=_target; @@ -173,9 +173,9 @@ namespace com.keyman.dom { /** * Respond to KMW losing focus on event - */ + */ _ControlBlur: (e: FocusEvent) => boolean = function(this: DOMEventHandlers, e: FocusEvent): boolean { - var Ltarg: HTMLElement; + var Ltarg: HTMLElement; e = this.keyman._GetEventObject(e); // I2404 - Manage IE events in IFRAMEs Ltarg = this.keyman.util.eventTarget(e) as HTMLElement; @@ -187,7 +187,7 @@ namespace com.keyman.dom { Ltarg = Ltarg['body']; // Occurs in Firefox for design-mode iframes. } - // Makes sure we properly detect the TouchAliasElement root, + // Makes sure we properly detect the TouchAliasElement root, // rather than one of its constituent children. if(this.keyman.util.device.touchable) { Ltarg = findTouchAliasTarget(Ltarg); @@ -209,7 +209,7 @@ namespace com.keyman.dom { let lastAlias = this.keyman.domManager.activeElement; lastAlias.hideCaret(); } - + if (Ltarg.nodeType == 3) { // defeat Safari bug Ltarg = Ltarg.parentNode as HTMLElement; } @@ -219,7 +219,7 @@ namespace com.keyman.dom { Ltarg=Ltarg.contentWindow.frameElement as HTMLElement; } } - + ////keymanweb._SelectionControl = null; if(this.keyman.domManager.lastActiveElement) { this._BlurKeyboardSettings(this.keyman.domManager.lastActiveElement); @@ -233,7 +233,7 @@ namespace com.keyman.dom { * the user is manually specifying languages on a per-control basis. */ this.keyman.uiManager.justActivated = false; - + var isActivating = this.keyman.uiManager.isActivating; let activeKeyboard = com.keyman.singleton.core.activeKeyboard; if(!isActivating && activeKeyboard) { @@ -255,9 +255,9 @@ namespace com.keyman.dom { * @param {Object} _target element losing focus * @param {Event} _event event object * @param {(boolean|number)} _isActivating activation state - * @return {boolean} + * @return {boolean} * Description Execute external (UI) code needed on blur - */ + */ doControlBlurred(_target: HTMLElement, _event: Event, _isActivating: boolean|number): boolean { var p={}; p['target']=_target; @@ -275,7 +275,7 @@ namespace com.keyman.dom { _BlurKeyboardSettings(lastElem: HTMLElement, PInternalName?: string, PLgCode?: string) { var keyboardID = this.keyman.core.activeKeyboard ? this.keyman.core.activeKeyboard.id : ''; var langCode = this.keyman.keyboardManager.getActiveLanguage(); - + if(PInternalName !== undefined && PLgCode !== undefined) { keyboardID = PInternalName; langCode = PLgCode; @@ -296,7 +296,7 @@ namespace com.keyman.dom { * Description Restores the newly active element's keyboard settings. Should be called * whenever a KMW-enabled page element gains control, but only once the prior * element's loss of control is guaranteed. - */ + */ _FocusKeyboardSettings(lastElem: HTMLElement, blockGlobalChange: boolean) { if(lastElem && lastElem._kmwAttachment.keyboard != null) { this.keyman.keyboardManager.setActiveKeyboard(lastElem._kmwAttachment.keyboard, @@ -314,10 +314,10 @@ namespace com.keyman.dom { /** * Function _CommonFocusHelper - * @param {Element} target + * @param {Element} target * @returns {boolean} - * Description Performs common state management for the various focus events of KeymanWeb. - * The return value indicates whether (true) or not (false) the calling event handler + * Description Performs common state management for the various focus events of KeymanWeb. + * The return value indicates whether (true) or not (false) the calling event handler * should be terminated immediately after the call. */ _CommonFocusHelper(target: HTMLElement): boolean { @@ -328,23 +328,25 @@ namespace com.keyman.dom { if(target.ownerDocument && target instanceof target.ownerDocument.defaultView.HTMLIFrameElement) { if(!this.keyman.domManager._IsIEEditableIframe(target, 1) || !this.keyman.domManager._IsMozillaEditableIframe(target, 1)) { - DOMEventHandlers.states._DisableInput = true; + DOMEventHandlers.states._DisableInput = true; return true; } } - DOMEventHandlers.states._DisableInput = false; + DOMEventHandlers.states._DisableInput = false; + + const outputTarget = dom.Utils.getOutputTarget(target); let activeKeyboard = keyman.core.activeKeyboard; if(!uiManager.justActivated) { - if(target && Utils.getOutputTarget(target)) { - Utils.getOutputTarget(target).deadkeys().clear(); + if(target && outputTarget) { + outputTarget.deadkeys().clear(); } - + if(activeKeyboard) { - activeKeyboard.notify(0, Utils.getOutputTarget(target), 1); // I2187 + activeKeyboard.notify(0, outputTarget, 1); // I2187 } } - + if(!uiManager.justActivated && DOMEventHandlers.states._SelectionControl != target) { uiManager.isActivating = false; } @@ -352,8 +354,20 @@ namespace com.keyman.dom { DOMEventHandlers.states._SelectionControl = target; + if(target && outputTarget) { + // + // Call the current keyboard's newContext handler; + // timeout is required in order to get the current + // selection, swhich is not ready at time of focus event + // + window.setTimeout(() => { + //console.log('processNewContextEvent called from focus'); + com.keyman.singleton.core.processNewContextEvent(outputTarget); + }); + } + if(keyman.core.languageProcessor.isActive) { - keyman.core.languageProcessor.predictFromTarget(Utils.getOutputTarget(target)); + keyman.core.languageProcessor.predictFromTarget(outputTarget); } return false; } @@ -361,12 +375,12 @@ namespace com.keyman.dom { /** * Function _SelectionChange * Scope Private - * Description Respond to selection change event + * Description Respond to selection change event */ _SelectionChange: () => boolean = function(this: DOMEventHandlers): boolean { if(DOMEventHandlers.states._IgnoreNextSelChange) { DOMEventHandlers.states._IgnoreNextSelChange--; - } + } return true; }.bind(this); @@ -374,12 +388,12 @@ namespace com.keyman.dom { /** * Function _KeyDown * Scope Private - * Description Processes keydown event and passes data to keyboard. - * + * Description Processes keydown event and passes data to keyboard. + * * Note that the test-case oriented 'recorder' stubs this method to facilitate keystroke * recording for use in test cases. If changing this function, please ensure the recorder is * not affected. - */ + */ _KeyDown: (e: KeyboardEvent) => boolean = function(this: DOMEventHandlers, e: KeyboardEvent): boolean { var activeKeyboard = this.keyman.core.activeKeyboard; var util = this.keyman.util; @@ -395,7 +409,7 @@ namespace com.keyman.dom { return true; } } else if(el && el.className.indexOf('kmw-disabled') >= 0) { - return true; + return true; } return PreProcessor.keyDown(e); @@ -421,11 +435,23 @@ namespace com.keyman.dom { DOMEventHandlers.states.changed = false; } + _Click: (e: MouseEvent) => boolean = function(this: DOMEventHandlers, e: MouseEvent): boolean { + let target = e.target as HTMLElement; + if(target && target['base']) { + target = target['base']; + } + + //console.log('processNewContextEvent called from click'); + com.keyman.singleton.core.processNewContextEvent(dom.Utils.getOutputTarget(target)); + + return true; + }.bind(this); + /** * Function _KeyPress * Scope Private * Description Processes keypress event (does not pass data to keyboard) - */ + */ _KeyPress: (e: KeyboardEvent) => boolean = function(this: DOMEventHandlers, e: KeyboardEvent): boolean { if(DOMEventHandlers.states._DisableInput || this.keyman.core.activeKeyboard == null) { return true; @@ -438,7 +464,7 @@ namespace com.keyman.dom { * Function _KeyUp * Scope Private * Description Processes keyup event and passes event data to keyboard - */ + */ _KeyUp: (e: KeyboardEvent) => boolean = function(this: DOMEventHandlers, e: KeyboardEvent): boolean { var osk = this.keyman.osk; @@ -457,7 +483,7 @@ namespace com.keyman.dom { if(outputTarget instanceof inputEle.ownerDocument.defaultView.HTMLTextAreaElement) { ignore = true; } - + if(inputEle.base && inputEle.base instanceof inputEle.base.ownerDocument.defaultView.HTMLTextAreaElement) { ignore = true; } @@ -473,8 +499,8 @@ namespace com.keyman.dom { } return true; } - } - + } + return PreProcessor.keyUp(e); }.bind(this); } @@ -490,7 +516,7 @@ namespace com.keyman.dom { y: number; }; - + constructor(keyman: KeymanBase) { super(keyman); } @@ -510,7 +536,7 @@ namespace com.keyman.dom { // whether or not individual `Touch`es may be related to this specific event for // an ongoing multitouch scenario. let target = e.target; - + // Find the first touch affected by this event that matches the current target. for(let i=0; i < e.changedTouches.length; i++) { if(isValidTouch(e.changedTouches[i], target)) { @@ -524,9 +550,9 @@ namespace com.keyman.dom { } /** - * Handle receiving focus by simulated input field - * - */ + * Handle receiving focus by simulated input field + * + */ setFocus: (e?: TouchEvent|MSPointerEvent) => void = function(this: DOMTouchHandlers, e?: TouchEvent|MSPointerEvent): void { DOMEventHandlers.states.setFocusTimer(); @@ -580,7 +606,7 @@ namespace com.keyman.dom { // Some parts rely upon the scroller element. let scroller = target.firstChild as HTMLElement; - // Move the caret and refocus if necessary + // Move the caret and refocus if necessary if(this.keyman.domManager.activeElement != target) { // Hide the KMW caret let prevTarget = this.keyman.domManager.activeElement; @@ -600,26 +626,26 @@ namespace com.keyman.dom { // The issue here is that touching a DIV does not actually set the focus for iOS, even when enabled to accept focus (by setting tabIndex=0) // We must explicitly set the focus in order to remove focus from any non-KMW input target.focus(); //Android native browsers may not like this, but it is needed for Chrome, Safari - } - + } + // Correct element directionality if required - this.keyman.domManager._SetTargDir(target); - + this.keyman.domManager._SetTargDir(target); + // If clicked on DIV on the main element, rather than any part of the text representation, // set caret to end of text if(tTarg && tTarg == target) { var x,cp; - x=dom.Utils.getAbsoluteX(scroller.firstChild as HTMLElement); - if(target.dir == 'rtl') { - x += (scroller.firstChild as HTMLElement).offsetWidth; + x=dom.Utils.getAbsoluteX(scroller.firstChild as HTMLElement); + if(target.dir == 'rtl') { + x += (scroller.firstChild as HTMLElement).offsetWidth; cp=(touchX > x ? 0 : 100000); } else { cp=(touchX touchY && cp > cpMin && cp != cpMax) {cpMax=cp; cp=Math.round((cp+cpMin)/2);} else if(y < touchY-yRow && cp < cpMax && cp != cpMin) {cpMin=cp; cp=Math.round((cp+cpMax)/2);} else break; @@ -656,21 +682,21 @@ namespace com.keyman.dom { var snapOrder; if(target.dir == 'rtl') { // I would use arrow functions, but IE doesn't like 'em. snapOrder = function(a, b) { - return a < b; + return a < b; }; } else { - snapOrder = function(a, b) { - return a > b; + snapOrder = function(a, b) { + return a > b; }; } for(iLoop=0; iLoop<16; iLoop++) { - x=dom.Utils.getAbsoluteX(caret); //left of caret + x=dom.Utils.getAbsoluteX(caret); //left of caret if(snapOrder(x, touchX) && cp > cpMin && cp != cpMax) { - cpMax=cp; + cpMax=cp; cp=Math.round((cp+cpMin)/2); } else if(!snapOrder(x, touchX) && cp < cpMax && cp != cpMin) { - cpMin=cp; + cpMin=cp; cp=Math.round((cp+cpMax)/2); } else { break; @@ -691,7 +717,7 @@ namespace com.keyman.dom { * for controls, we have to act here to preserve the outgoing control's keyboard settings. * * If we 'just activated' the KeymanWeb UI, we need to save the new keyboard change as appropriate. - */ + */ if(this.keyman.domManager.lastActiveElement) { this._BlurKeyboardSettings(this.keyman.domManager.lastActiveElement); } @@ -705,7 +731,7 @@ namespace com.keyman.dom { * If not, we need to activate the control's preferred keyboard. */ this._FocusKeyboardSettings(target, false); - + // Always do the common focus stuff, instantly returning if we're in an editable iframe. // This parallels the if-statement in _ControlFocus - it may be needed as this if-statement in the future, // despite its present redundancy. @@ -716,15 +742,15 @@ namespace com.keyman.dom { /** * Close OSK and remove simulated caret on losing focus - */ + */ cancelInput(): void { - this.keyman.domManager.activeElement = null; + this.keyman.domManager.activeElement = null; this.keyman.domManager.lastActiveElement = null; this.keyman.osk.hideNow(); }; /** - * Handle losing focus from simulated input field + * Handle losing focus from simulated input field */ setBlur: (e: FocusEvent) => void = function(this: DOMTouchHandlers, e: FocusEvent) { // This works OK for iOS, but may need something else for other platforms @@ -734,7 +760,7 @@ namespace com.keyman.dom { elem = e.relatedTarget as HTMLElement; } - this.executeBlur(elem); + this.executeBlur(elem); }.bind(this); executeBlur(elem: HTMLElement) { @@ -743,7 +769,7 @@ namespace com.keyman.dom { if(elem) { this.doChangeEvent(elem); if(elem.nodeName != 'DIV' || elem.className.indexOf('keymanweb-input') == -1) { - this.cancelInput(); + this.cancelInput(); return; } } @@ -756,7 +782,7 @@ namespace com.keyman.dom { /** * Display and position a scrollbar in the input field if needed - * + * * @param {Object} e input DIV element (copy of INPUT or TEXTAREA) */ setScrollBar(e: HTMLElement) { @@ -767,21 +793,21 @@ namespace com.keyman.dom { sbs.width=100*(e.offsetWidth/scroller.offsetWidth)+'%'; sbs.left=100*(-scroller.offsetLeft/scroller.offsetWidth)+'%'; sbs.top='0'; - sbs.visibility='visible'; + sbs.visibility='visible'; } else if(scroller.offsetHeight > e.offsetHeight || scroller.offsetTop < 0) { sbs.width='4px'; sbs.height=100*(e.offsetHeight/scroller.offsetHeight)+'%'; sbs.top=100*(-scroller.offsetTop/scroller.offsetHeight)+'%'; - sbs.left='0'; - sbs.visibility='visible'; + sbs.left='0'; + sbs.visibility='visible'; } else { sbs.visibility='hidden'; } - } + } /** * Handle the touch move event for an input element - */ + */ dragInput: (e: TouchEvent|MouseEvent) => void = function(this: DOMTouchHandlers, e: TouchEvent|MouseEvent) { // Prevent dragging window if(e.cancelable) { @@ -789,12 +815,12 @@ namespace com.keyman.dom { // Tends to result in a spam of console errors when e.cancelable == false. e.preventDefault(); } - e.stopPropagation(); + e.stopPropagation(); // Identify the target from the touch list or the event argument (IE 10 only) var target: HTMLElement; let touch: Touch; - + if(dom.Utils.instanceof(e, "TouchEvent")) { try { touch=DOMTouchHandlers.selectTouch(e as TouchEvent); @@ -809,7 +835,7 @@ namespace com.keyman.dom { if(target == null) { return; } - + // Identify the input element from the touch event target (touched element may be contained by input) target = findTouchAliasTarget(target); @@ -826,22 +852,22 @@ namespace com.keyman.dom { x = (e as MouseEvent).screenX; y = (e as MouseEvent).screenY; } - + // Allow content of input elements to be dragged horizontally or vertically if(typeof this.firstTouch == 'undefined' || this.firstTouch == null) { this.firstTouch={x:x,y:y}; } else { var x0=this.firstTouch.x,y0=this.firstTouch.y, scroller=target.firstChild as HTMLElement,dx,dy,x1; - + if(target.base.nodeName == 'TEXTAREA') { var yOffset=parseInt(scroller.style.top,10); if(isNaN(yOffset)) yOffset=0; dy=y0-y; if(dy < -4 || dy > 4) { scroller.style.top=(yOffset xMin) x1=xMin; if(x1 < xMax) x1=xMax; scroller.style.left=x1+'px'; - this.firstTouch.x=x; - } + this.firstTouch.x=x; + } } } // Should refactor to use TouchAliasElement's version; target is an instance of the class. @@ -865,9 +891,9 @@ namespace com.keyman.dom { /** * Scroll the document body vertically to bring the active input into view - * + * * @param {Object} e simulated input field object being focussed - */ + */ scrollBody(e: HTMLElement): void { var osk = this.keyman.osk; @@ -882,7 +908,7 @@ namespace com.keyman.dom { } else { dy=y-t-(window.innerHeight-osk._Box.offsetHeight-s2.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+window.pageYOffset); diff --git a/web/source/dom/domManager.ts b/web/source/dom/domManager.ts index a4e2c8a79e..4820bc0d65 100644 --- a/web/source/dom/domManager.ts +++ b/web/source/dom/domManager.ts @@ -58,8 +58,8 @@ namespace com.keyman.dom { enablementObserver: MutationObserver; /** - * Tracks a list of event-listening elements. - * + * Tracks a list of event-listening elements. + * * In touch mode, this should contain touch-aliasing DIVs, but will contain other elements in non-touch mode. */ inputList: HTMLElement[] = []; // List of simulated input divisions for touch-devices I3363 (Build 301) @@ -70,7 +70,7 @@ namespace com.keyman.dom { sortedInputs: HTMLElement[] = []; // List of all INPUT and TEXTAREA elements ordered top to bottom, left to right _BeepObjects: BeepData[] = []; // BeepObjects - maintains a list of active 'beep' visual feedback elements - _BeepTimeout: number = 0; // BeepTimeout - a flag indicating if there is an active 'beep'. + _BeepTimeout: number = 0; // BeepTimeout - a flag indicating if there is an active 'beep'. // Set to 1 if there is an active 'beep', otherwise leave as '0'. // Used for special touch-based page interactions re: element activation on touch devices. @@ -84,7 +84,7 @@ namespace com.keyman.dom { constructor(keyman: KeymanBase) { this.keyman = keyman; - + if(keyman.util.device.touchable) { this.touchHandlers = new DOMTouchHandlers(keyman); } @@ -101,7 +101,7 @@ namespace com.keyman.dom { if(this.attachmentObserver) { this.attachmentObserver.disconnect(); } - + for(let input of this.inputList) { this.disableInputElement(input); } @@ -127,7 +127,7 @@ namespace com.keyman.dom { * Scope Public * @param {Object} Pelem element to flash * Description Flash body as substitute for audible beep; notify embedded device to vibrate - */ + */ doBeep(outputTarget: targets.OutputTarget) { // Handles embedded-mode beeps. let keyman = com.keyman.singleton; @@ -149,7 +149,7 @@ namespace com.keyman.dom { if(!Pelem) { return; // There's no way to signal a 'beep' to null, so just cut everything short. } - + if(!Pelem.style || typeof(Pelem.style.backgroundColor)=='undefined') { return; } @@ -160,7 +160,7 @@ namespace com.keyman.dom { return; } } - + this._BeepObjects = com.keyman.singleton._push(this._BeepObjects, new BeepData(Pelem)); // TODO: This is probably a bad color choice if "dark mode" is enabled. A proper implementation // would probably require some 'fun' CSS work, though. @@ -172,10 +172,10 @@ namespace com.keyman.dom { } /** - * Function beepReset + * Function beepReset * Scope Public * Description Reset/terminate beep or flash (not currently used: Aug 2011) - */ + */ beepReset(): void { com.keyman.singleton.core.keyboardInterface.resetContextCache(); @@ -191,7 +191,7 @@ namespace com.keyman.dom { * Function getHandlers * Scope Private * @param {Element} Pelem An input, textarea, or touch-alias element from the page. - * @returns {Object} + * @returns {Object} */ getHandlers(Pelem: HTMLElement): DOMEventHandlers { var _attachObj = Pelem.base ? Pelem.base._kmwAttachment : Pelem._kmwAttachment; @@ -213,9 +213,9 @@ namespace com.keyman.dom { * an outer DIV, matching the position, size and style of the base element * a scrollable DIV within that outer element * two SPAN elements within the scrollable DIV, to hold the text before and after the caret - * - * The left border of the second SPAN is flashed on and off as a visible caret - * + * + * The left border of the second SPAN is flashed on and off as a visible caret + * * Also ensures the element is registered on keymanweb's internal input list. */ enableTouchElement(Pelem: HTMLElement) { @@ -229,9 +229,9 @@ namespace com.keyman.dom { return false; } else { // Initialize and protect input elements for touch-screen devices (but never for apps) - // NB: now set disabled=true rather than readonly, since readonly does not always + // NB: now set disabled=true rather than readonly, since readonly does not always // prevent element from getting focus, e.g. within a LABEL element. - // c.f. http://kreotekdev.wordpress.com/2007/11/08/disabled-vs-readonly-form-fields/ + // c.f. http://kreotekdev.wordpress.com/2007/11/08/disabled-vs-readonly-form-fields/ Pelem.kmwInput = true; } @@ -249,7 +249,7 @@ namespace com.keyman.dom { } this.inputList.push(Pelem['kmw_ip']); - + console.log("Unexpected state - this element's simulated input DIV should have been removed from the page!"); return true; // May need setup elsewhere since it's just been re-added! @@ -263,18 +263,18 @@ namespace com.keyman.dom { this.setupElementAttachment(x); // The touch-alias should have its own wrapper. } Pelem._kmwAttachment = x._kmwAttachment; // It's an object reference we need to alias. - + // Set font for base element this.enableInputElement(x, true); - // Superimpose custom input fields for each input or textarea, unless readonly or disabled - + // Superimpose custom input fields for each input or textarea, unless readonly or disabled + // On touch event, reposition the text caret and prepare for OSK input - // Removed 'onfocus=' as that resulted in handling the event twice (on iOS, anyway) + // Removed 'onfocus=' as that resulted in handling the event twice (on iOS, anyway) // We know this to be the correct set of handlers because we're setting up a touch element. var touchHandlers = this.touchHandlers; - + x.addEventListener('touchstart', touchHandlers.setFocus); x.onmspointerdown=function(e: MSPointerEvent) { e.preventDefault(); @@ -289,14 +289,14 @@ namespace com.keyman.dom { x.onmspointerup=function(e) { e.stopPropagation(); }; - - // Disable internal scroll when input element in focus + + // Disable internal scroll when input element in focus x.addEventListener('touchmove', touchHandlers.dragInput, false); x.onmspointermove=touchHandlers.dragInput; - + // Hide keyboard and caret when losing focus from simulated input field x.onblur=touchHandlers.setBlur; - + // Note that touchend event propagates and is processed by body touchend handler // re-setting the first touch point for a drag @@ -331,7 +331,7 @@ namespace com.keyman.dom { // Disable touch-related handling code. this.disableInputElement(Pelem['kmw_ip']); Pelem._kmwAttachment.interface = dom.targets.wrapElement(Pelem); - + // We get weird repositioning errors if we don't remove our simulated input element - and permanently. if(Pelem.parentNode) { Pelem.parentNode.removeChild(Pelem['kmw_ip']); @@ -342,7 +342,7 @@ namespace com.keyman.dom { this.setupNonKMWTouchElement(Pelem); } - /** + /** * Function nonKMWTouchHandler * Scope Private * Description A handler for KMW-touch-disabled elements when operating on touch devices. @@ -377,8 +377,8 @@ namespace com.keyman.dom { * Note that this method is called for both desktop and touch control routes; the touch route calls it from within * enableTouchElement as it must first establish the simulated touch element to serve as the alias "input element" here. * Note that the 'kmw-disabled' property is managed by the MutationObserver and by the surface API calls. - */ - enableInputElement(Pelem: HTMLElement, isAlias?: boolean) { + */ + enableInputElement(Pelem: HTMLElement, isAlias?: boolean) { var baseElement = isAlias ? Pelem['base'] : Pelem; if(!this.isKMWDisabled(baseElement)) { @@ -394,14 +394,15 @@ namespace com.keyman.dom { this.keyman.util.attachDOMEvent(baseElement,'focus', this.getHandlers(Pelem)._ControlFocus); this.keyman.util.attachDOMEvent(baseElement,'blur', this.getHandlers(Pelem)._ControlBlur); + this.keyman.util.attachDOMEvent(baseElement,'click', this.getHandlers(Pelem)._Click); // These need to be on the actual input element, as otherwise the keyboard will disappear on touch. Pelem.onkeypress = this.getHandlers(Pelem)._KeyPress; Pelem.onkeydown = this.getHandlers(Pelem)._KeyDown; - Pelem.onkeyup = this.getHandlers(Pelem)._KeyUp; + Pelem.onkeyup = this.getHandlers(Pelem)._KeyUp; } - } - }; + } + }; /** * Function disableInputElement @@ -410,18 +411,18 @@ namespace com.keyman.dom { * @param {boolean=} isAlias A flag that indicates if the element is a simulated input element for touch. * Description Inverts the process of enableInputElement, removing all event-handling from the element. * Note that the 'kmw-disabled' property is managed by the MutationObserver and by the surface API calls. - */ - disableInputElement(Pelem: HTMLElement, isAlias?: boolean) { + */ + disableInputElement(Pelem: HTMLElement, isAlias?: boolean) { if(!Pelem) { return; } - + var baseElement = isAlias ? Pelem['base'] : Pelem; // Do NOT test for pre-disabledness - we also use this to fully detach without officially 'disabling' via kmw-disabled. if((Pelem.ownerDocument.defaultView && Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) || Pelem instanceof HTMLIFrameElement) { this._DetachFromIframe(Pelem); - } else { + } else { var cnIndex = baseElement.className.indexOf('keymanweb-font'); if(cnIndex > 0 && !isAlias) { // See note about the alias below. baseElement.className = baseElement.className.replace('keymanweb-font', '').trim(); @@ -436,11 +437,12 @@ namespace com.keyman.dom { if(!isAlias) { // See note about the alias below. this.keyman.util.detachDOMEvent(baseElement,'focus', this.getHandlers(Pelem)._ControlFocus); this.keyman.util.detachDOMEvent(baseElement,'blur', this.getHandlers(Pelem)._ControlBlur); + this.keyman.util.detachDOMEvent(baseElement,'click', this.getHandlers(Pelem)._Click); } // These need to be on the actual input element, as otherwise the keyboard will disappear on touch. Pelem.onkeypress = null; Pelem.onkeydown = null; - Pelem.onkeyup = null; + Pelem.onkeyup = null; } // If we're disabling an alias, we should fully enable the base version. (Thinking ahead to toggleable-touch mode.) @@ -449,7 +451,7 @@ namespace com.keyman.dom { baseElement.onkeypress = this.getHandlers(Pelem)._KeyPress; baseElement.onkeydown = this.getHandlers(Pelem)._KeyDown; - baseElement.onkeyup = this.getHandlers(Pelem)._KeyUp; + baseElement.onkeyup = this.getHandlers(Pelem)._KeyUp; } var lastElem = this.lastActiveElement; @@ -460,7 +462,7 @@ namespace com.keyman.dom { this.lastActiveElement = null; this.keyman.osk.startHide(false); } - + return; }; @@ -470,7 +472,7 @@ namespace com.keyman.dom { * @param {Element} x An element from the page. * @return {boolean} true if the element's properties indicate a 'disabled' state. * Description Examines attachable elements to determine their default enablement state. - */ + */ isKMWDisabled(x: HTMLElement): boolean { var c = x.className; @@ -481,15 +483,15 @@ namespace com.keyman.dom { return true; } - return false; + return false; } /** * Function attachToControl * Scope Public * @param {Element} Pelem Element to which KMW will be attached - * Description Attaches KMW to control (or IFrame) - */ + * Description Attaches KMW to control (or IFrame) + */ attachToControl(Pelem: HTMLElement) { var touchable = this.keyman.util.device.touchable; @@ -519,8 +521,8 @@ namespace com.keyman.dom { * Function detachFromControl * Scope Public * @param {Element} Pelem Element from which KMW will detach - * Description Detaches KMW from a control (or IFrame) - */ + * Description Detaches KMW from a control (or IFrame) + */ detachFromControl(Pelem: HTMLElement) { if(!(this.isAttached(Pelem) || Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement)) { return; // We never were attached. @@ -531,10 +533,10 @@ namespace com.keyman.dom { // Is it already disabled? if(!this.isKMWDisabled(Pelem)) { this._DisableControl(Pelem); - } + } } - // #2 - clear attachment data. + // #2 - clear attachment data. this.clearElementAttachment(Pelem); } @@ -555,7 +557,7 @@ namespace com.keyman.dom { * @return {boolean} true if the element is viable for KMW attachment. * Description Examines potential input elements to determine whether or not they are viable for KMW attachment. * Also filters elements not supported for touch devices when device.touchable == true. - */ + */ isKMWInput(x: HTMLElement): boolean { var touchable = this.keyman.util.device.touchable; @@ -573,8 +575,8 @@ namespace com.keyman.dom { } } // else nothing? } - catch(err) { - /* Do not attempt to access iframes outside this site */ + catch(err) { + /* Do not attempt to access iframes outside this site */ console.warn("Error during attachment to / detachment from iframe: "); console.warn(err); } @@ -582,7 +584,7 @@ namespace com.keyman.dom { return true; } - return false; + return false; } /** @@ -590,7 +592,7 @@ namespace com.keyman.dom { * Scope Private * @param {Element} x An element from the page valid for KMW attachment * Description Establishes the base KeymanWeb data for newly-attached elements. - * Does not establish input hooks, which are instead handled during enablement. + * Does not establish input hooks, which are instead handled during enablement. */ setupElementAttachment(x: HTMLElement) { // The `_kmwAttachment` property tag maintains all relevant KMW-maintained data regarding the element. @@ -617,7 +619,7 @@ namespace com.keyman.dom { * Scope Private * @param {Element} x An element from the page valid for KMW attachment * Description Establishes the base KeymanWeb data for newly-attached elements. - * Does not establish input hooks, which are instead handled during enablement. + * Does not establish input hooks, which are instead handled during enablement. */ clearElementAttachment(x: HTMLElement) { // We need to clear the object when de-attaching; helps prevent memory leaks. @@ -628,11 +630,11 @@ namespace com.keyman.dom { * Function _AttachToIframe * Scope Private * @param {Element} Pelem IFrame to which KMW will be attached - * Description Attaches KeymanWeb to IFrame - */ + * Description Attaches KeymanWeb to IFrame + */ _AttachToIframe(Pelem: HTMLIFrameElement) { var util = this.keyman.util; - + try { var Lelem=Pelem.contentWindow.document; /* editable Iframe */ @@ -662,15 +664,15 @@ namespace com.keyman.dom { catch(err) { // do not attempt to attach to the iframe as it is from another domain - XSS denied! - } + } } /** * Function _DetachFromIframe * Scope Private * @param {Element} Pelem IFrame to which KMW will be attached - * Description Detaches KeymanWeb from an IFrame - */ + * Description Detaches KeymanWeb from an IFrame + */ _DetachFromIframe(Pelem: HTMLIFrameElement) { var util = this.keyman.util; @@ -703,7 +705,7 @@ namespace com.keyman.dom { catch(err) { // do not attempt to attach to the iframe as it is from another domain - XSS denied! - } + } } /** @@ -735,9 +737,9 @@ namespace com.keyman.dom { * Function LiTmp * Scope Private * @param {string} _colon type of element - * @return {Array} array of elements of specified type + * @return {Array} array of elements of specified type * Description Local function to get list of editable controls - */ + */ var LiTmp = function(_colon: string): HTMLElement[] { return util.arrayFromNodeList(Pelem.getElementsByTagName(_colon)); }; @@ -745,12 +747,12 @@ namespace com.keyman.dom { // Note that isKMWInput() will block IFRAME elements as necessary for touch-based devices. possibleInputs = possibleInputs.concat(LiTmp('INPUT'), LiTmp('TEXTAREA'), LiTmp('IFRAME')); } - + // Not all active browsers may support the method, but only those that do would work with contenteditables anyway. if(Pelem.querySelectorAll) { possibleInputs = possibleInputs.concat(util.arrayFromNodeList(Pelem.querySelectorAll('[contenteditable]'))); } - + if(Pelem.ownerDocument && Pelem instanceof Pelem.ownerDocument.defaultView.HTMLElement && Pelem.isContentEditable) { possibleInputs.push(Pelem); } @@ -795,13 +797,13 @@ namespace com.keyman.dom { /** * Set target element text direction (LTR or RTL), but only if the element is empty - * + * * If the element base directionality is changed after it contains content, unless all the text * has the same directionality, text runs will be re-ordered which is confusing and causes * incorrect caret positioning - * + * * @param {Object} Ptarg Target element - */ + */ _SetTargDir(Ptarg: HTMLElement) { let activeKeyboard = com.keyman.singleton.core.activeKeyboard; var elDir=(activeKeyboard && activeKeyboard.isRTL) ? 'rtl' : 'ltr'; @@ -814,7 +816,7 @@ namespace com.keyman.dom { alias.setTextCaret(10000); } } else { - if(Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLInputElement + if(Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLInputElement || Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLTextAreaElement) { if((Ptarg as HTMLInputElement|HTMLTextAreaElement).value.length == 0) { Ptarg.dir=elDir; @@ -830,8 +832,8 @@ namespace com.keyman.dom { * Function _DisableControl * Scope Private * @param {Element} Pelem Element to be disabled - * Description Disable KMW control element - */ + * Description Disable KMW control element + */ _DisableControl(Pelem: HTMLElement) { // Only operate on attached elements! Non-design-mode IFrames don't get attachment markers, so we check them specifically instead. if(this.isAttached(Pelem) || Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement) { @@ -840,7 +842,7 @@ namespace com.keyman.dom { this.setupNonKMWTouchElement(Pelem); var keyman = this.keyman; - + // If a touch alias was removed, chances are it's gonna mess up our touch-based layout scheme, so let's update the touch elements. window.setTimeout(function() { this.listInputs(); @@ -854,7 +856,7 @@ namespace com.keyman.dom { } else { this.listInputs(); // Fix up our internal input ordering scheme. } - + this.disableInputElement(Pelem); } } @@ -863,8 +865,8 @@ namespace com.keyman.dom { * Function _EnableControl * Scope Private * @param {Element} Pelem Element to be enabled - * Description Enable KMW control element - */ + * Description Enable KMW control element + */ _EnableControl(Pelem: HTMLElement) { if(this.isAttached(Pelem)) { // Only operate on attached elements! if(this.keyman.util.device.touchable) { @@ -908,36 +910,36 @@ namespace com.keyman.dom { if(t1[i].className.indexOf('kmw-disabled') < 0) { eList.push({ip:t1[i], x: dom.Utils.getAbsoluteX(t1[i]), y: dom.Utils.getAbsoluteY(t1[i])}); } - break; + break; } } - for(i=0; i= 0 : false; var disabledAfter = (mutation.target as HTMLElement).className.indexOf('kmw-disabled') >= 0; - + if(disabledBefore && !disabledAfter) { this._EnableControl(mutation.target); } else if(!disabledBefore && disabledAfter) { @@ -981,10 +983,10 @@ namespace com.keyman.dom { for(var i=0; i < mutations.length; i++) { var mutation = mutations[i]; - + for(var j=0; j < mutation.addedNodes.length; j++) { inputElementAdditions = inputElementAdditions.concat(this._GetDocumentEditables(mutation.addedNodes[j])); - } + } for(j = 0; j < mutation.removedNodes.length; j++) { inputElementRemovals = inputElementRemovals.concat(this._GetDocumentEditables(mutation.removedNodes[j])); @@ -1024,12 +1026,12 @@ namespace com.keyman.dom { } }.bind(this); - /** + /** * Function _MutationAdditionObserved * Scope Private * @param {Element} Pelem A page input, textarea, or iframe element. * Description Used by the MutationObserver event handler to properly setup any elements dynamically added to the document post-initialization. - * + * */ _MutationAdditionObserved = function(Pelem: HTMLElement) { if(Pelem instanceof Pelem.ownerDocument.defaultView.HTMLIFrameElement && !this.keyman.util.device.touchable) { @@ -1039,7 +1041,7 @@ namespace com.keyman.dom { var attachFunctor = function() { // Triggers at the same time as iframe's onload property, after its internal document loads. // Provide a minor delay to allow 'load' event handlers to set the design-mode property. - window.setTimeout(function() { + window.setTimeout(function() { domManager.attachToControl(Pelem); }, 1); }; @@ -1047,11 +1049,11 @@ namespace com.keyman.dom { Pelem.addEventListener('load', attachFunctor); // The following block breaks for design-mode iframes, at least in Chrome; a blank document may exist - // before the load of the desired actual document. + // before the load of the desired actual document. // // /* If the iframe has somehow already loaded, we can't expect the onload event to be raised. We ought just // * go ahead and perform our callback's contents. - // * + // * // * keymanweb.domManager.attachToControl() is now idempotent, so even if our call 'whiffs', it won't cause long-lasting // * problems. // */ @@ -1060,7 +1062,7 @@ namespace com.keyman.dom { // } } else { this.attachToControl(Pelem); - } + } } // Used by the mutation event handler to properly decouple any elements dynamically removed from the document. @@ -1078,12 +1080,12 @@ namespace com.keyman.dom { * Function disableControl * Scope Public * @param {Element} Pelem Element to be disabled - * Description Disables a KMW control element - */ + * Description Disables a KMW control element + */ disableControl(Pelem: HTMLElement) { if(!this.isAttached(Pelem)) { console.warn("KeymanWeb is not attached to element " + Pelem); - } + } var cn = Pelem.className; if(cn.indexOf('kmw-disabled') < 0) { // if not already explicitly disabled... @@ -1098,12 +1100,12 @@ namespace com.keyman.dom { * Function enableControl * Scope Public * @param {Element} Pelem Element to be disabled - * Description Disables a KMW control element - */ + * Description Disables a KMW control element + */ enableControl = function(Pelem: HTMLElement) { if(!this.isAttached(Pelem)) { console.warn("KeymanWeb is not attached to element " + Pelem); - } + } var cn = Pelem.className; var tagIndex = cn.indexOf('kmw-disabled'); @@ -1129,12 +1131,12 @@ namespace com.keyman.dom { /** * Function _WindowUnload * Scope Private - * Description Remove handlers before detaching KMW window - */ + * Description Remove handlers before detaching KMW window + */ _WindowUnload: () => void = function(this: DOMManager) { // Allow the UI to release its own resources this.keyman.uiManager.doUnload(); - + // Allow the OSK to release its own resources if(this.keyman.osk) { this.keyman.osk.shutdown(); @@ -1142,7 +1144,7 @@ namespace com.keyman.dom { this.keyman.osk['_Unload'](); // I3363 (Build 301) } } - + this.lastActiveElement = null; }.bind(this); @@ -1150,12 +1152,12 @@ namespace com.keyman.dom { /** * Function setKeyboardForControl - * Scope Public - * @param {Element} Pelem Control element - * @param {string|null=} Pkbd Keyboard (Clears the set keyboard if set to null.) + * Scope Public + * @param {Element} Pelem Control element + * @param {string|null=} Pkbd Keyboard (Clears the set keyboard if set to null.) * @param {string|null=} Plc Language Code - * Description Set default keyboard for the control - */ + * Description Set default keyboard for the control + */ setKeyboardForControl(Pelem: HTMLElement, Pkbd?: string, Plc?: string) { /* pass null for kbd to specify no default, or '' to specify the default system keyboard. */ if(Pkbd !== null && Pkbd !== undefined) { @@ -1196,8 +1198,8 @@ namespace com.keyman.dom { /** * Function getKeyboardForControl - * Scope Public - * @param {Element} Pelem Control element + * Scope Public + * @param {Element} Pelem Control element * @return {string|null} The independently-managed keyboard for the control. * Description Returns the keyboard ID of the current independently-managed keyboard for this control. * If it is currently following the global keyboard setting, returns null instead. @@ -1210,11 +1212,11 @@ namespace com.keyman.dom { return Pelem._kmwAttachment.keyboard; } } - + /** * Function getLanguageForControl - * Scope Public - * @param {Element} Pelem Control element + * Scope Public + * @param {Element} Pelem Control element * @return {string|null} The independently-managed keyboard for the control. * Description Returns the language code used with the current independently-managed keyboard for this control. * If it is currently following the global keyboard setting, returns null instead. @@ -1232,7 +1234,7 @@ namespace com.keyman.dom { /** * Set focus to last active target element (browser-dependent) - */ + */ focusLastActiveElement() { var lastElem = this.lastActiveElement; if(!lastElem) { @@ -1247,9 +1249,9 @@ namespace com.keyman.dom { /** * Get the last active target element *before* KMW activated (I1297) - * - * @return {Element} - */ + * + * @return {Element} + */ get lastActiveElement(): HTMLElement { return DOMEventHandlers.states._lastActiveElement; } @@ -1298,10 +1300,10 @@ namespace com.keyman.dom { } /** - * Set the active input element directly optionally setting focus - * + * Set the active input element directly optionally setting focus + * * @param {Object|string} e element id or element - * @param {boolean=} setFocus optionally set focus (KMEW-123) + * @param {boolean=} setFocus optionally set focus (KMEW-123) **/ setActiveElement(e: string|HTMLElement, setFocus?: boolean) { if(typeof e == "string") { // Can't instanceof string, and String is a different type. @@ -1347,17 +1349,21 @@ namespace com.keyman.dom { clientY: 0, target: e as HTMLElement }; - + // Kinda hacky, but gets the job done. (this.keyman.touchAliasing as DOMTouchHandlers).setFocusWithTouch(tEvent); } else { this.focusLastActiveElement(); } } + + // Let the keyboard do its initial group processing + //console.log('processNewContextEvent [not] called from setActiveElement'); + com.keyman.singleton.core.processNewContextEvent(dom.Utils.getOutputTarget(e)); } /** Sets the active input element only if it is presently null. - * + * * @param {Element} */ initActiveElement(Lelem: HTMLElement) { @@ -1369,16 +1375,16 @@ namespace com.keyman.dom { /** * Move focus to next (or previous) input or text area element on TAB * Uses list of actual input elements - * + * * Note that activeElement() on touch devices returns the DIV that overlays * the input element, not the element itself. - * + * * @param {number|boolean} bBack Direction to move (0 or 1) */ moveToNext(bBack: number|boolean) { var i,t=this.sortedInputs, activeBase = this.activeElement; var touchable = this.keyman.util.device.touchable; - + if(t.length == 0) { return; } @@ -1401,7 +1407,7 @@ namespace com.keyman.dom { // Move to the selected element if(touchable) { - // Set focusing flag to prevent OSK disappearing + // Set focusing flag to prevent OSK disappearing DOMEventHandlers.states.focusing=true; var target=t[i]['kmw_ip']; @@ -1422,17 +1428,17 @@ namespace com.keyman.dom { /** * Move focus to user-specified element - * + * * @param {string|Object} e element or element id - * + * **/ moveToElement(e:string|HTMLElement) { var i; - + if(typeof(e) == "string") { // Can't instanceof string, and String is a different type. e=document.getElementById(e); } - + if(this.keyman.util.device.touchable && e['kmw_ip']) { e['kmw_ip'].focus(); } else { @@ -1446,10 +1452,10 @@ namespace com.keyman.dom { * Function _IsIEEditableIframe * Scope Private * @param {Object} Pelem Iframe element - * {boolean|number} PtestOn 1 to test if frame content is editable (TODO: unclear exactly what this is doing!) + * {boolean|number} PtestOn 1 to test if frame content is editable (TODO: unclear exactly what this is doing!) * @return {boolean} - * Description Test if element is an IE editable IFrame - */ + * Description Test if element is an IE editable IFrame + */ _IsIEEditableIframe(Pelem: HTMLIFrameElement, PtestOn?: number) { var Ldv, Lvalid = Pelem && (Ldv=Pelem.tagName) && Ldv.toLowerCase() == 'body' && (Ldv=Pelem.ownerDocument) && Ldv.parentWindow; return (!PtestOn && Lvalid) || (PtestOn && (!Lvalid || Pelem.isContentEditable)); @@ -1459,29 +1465,29 @@ namespace com.keyman.dom { * Function _IsMozillaEditableIframe * Scope Private * @param {Object} Pelem Iframe element - * @param {boolean|number} PtestOn 1 to test if 'designMode' is 'ON' - * @return {boolean} - * Description Test if element is a Mozilla editable IFrame - */ + * @param {boolean|number} PtestOn 1 to test if 'designMode' is 'ON' + * @return {boolean} + * Description Test if element is a Mozilla editable IFrame + */ _IsMozillaEditableIframe(Pelem: HTMLIFrameElement, PtestOn?: number) { var Ldv, Lvalid = Pelem && (Ldv=(Pelem).defaultView) && Ldv.frameElement; // Probable bug! return (!PtestOn && Lvalid) || (PtestOn && (!Lvalid || Ldv.document.designMode.toLowerCase()=='on')); } /* ----------------------- Initialization methods ------------------ */ - + /** * Get the user-specified (or default) font for the first mapped input or textarea element * before applying any keymanweb styles or classes - * + * * @return {string} - **/ + **/ getBaseFont() { var util = this.keyman.util; var ipInput = document.getElementsByTagName<'input'>('input'), ipTextArea=document.getElementsByTagName<'textarea'>('textarea'), n=0,fs,fsDefault='Arial,sans-serif'; - + // Find the first input element (if it exists) if(ipInput.length == 0 && ipTextArea.length == 0) { n=0; @@ -1503,7 +1509,7 @@ namespace com.keyman.dom { n=2; } } - + // Grab that font! switch(n) { case 0: @@ -1516,7 +1522,7 @@ namespace com.keyman.dom { if(typeof(fs) == 'undefined' || fs == 'monospace') { fs=fsDefault; } - + return fs; } @@ -1524,9 +1530,9 @@ namespace com.keyman.dom { * Function Initialization * Scope Public * @param {com.keyman.OptionType} arg object of user-defined properties - * Description KMW window initialization - */ - init: (arg: com.keyman.OptionType) => Promise = function(this: DOMManager, arg): Promise { + * Description KMW window initialization + */ + init: (arg: com.keyman.OptionType) => Promise = function(this: DOMManager, arg): Promise { var p,opt,dTrailer,ds; var util = this.keyman.util; var device = util.device; @@ -1537,22 +1543,22 @@ namespace com.keyman.dom { this.keyman.core.keyboardProcessor.errorLogger = console.error.bind(console); // Local function to convert relative to absolute URLs - // with respect to the source path, server root and protocol + // with respect to the source path, server root and protocol var fixPath = function(p) { if(p.length == 0) return p; - + // Add delimiter if missing if(p.substr(p.length-1,1) != '/') p = p+'/'; // Absolute - if((p.replace(/^(http)s?:.*/,'$1') == 'http') + if((p.replace(/^(http)s?:.*/,'$1') == 'http') || (p.replace(/^(file):.*/,'$1') == 'file')) - return p; - + return p; + // Absolute (except for protocol) if(p.substr(0,2) == '//') return this.keyman.protocol+p; - + // Relative to server root if(p.substr(0,1) == '/') return this.keyman.rootPath+p.substr(1); @@ -1560,40 +1566,40 @@ namespace com.keyman.dom { // Otherwise, assume relative to source path return this.keyman.srcPath+p; }.bind(this); - - // Explicit (user-defined) parameter initialization + + // Explicit (user-defined) parameter initialization opt=this.keyman.options; if(typeof(arg) == 'object' && arg !== null) { for(p in opt) - { + { if(arg.hasOwnProperty(p)) opt[p] = arg[p]; } } - + // Get default paths and device options if(opt['root'] != '') { - this.keyman.rootPath = fixPath(opt['root']); + this.keyman.rootPath = fixPath(opt['root']); } - // Keyboards and fonts are located with respect to the server root by default + // Keyboards and fonts are located with respect to the server root by default //if(opt['keyboards'] == '') opt['keyboards'] = keymanweb.rootPath+'keyboard/'; //if(opt['fonts'] == '') opt['fonts'] = keymanweb.rootPath+'font/'; - - // Resources are located with respect to the engine by default + + // Resources are located with respect to the engine by default if(opt['resources'] == '') { opt['resources'] = this.keyman.srcPath; } - + // Convert resource, keyboard and font paths to absolute URLs opt['resources'] = fixPath(opt['resources']); opt['keyboards'] = fixPath(opt['keyboards']); - opt['fonts'] = fixPath(opt['fonts']); + opt['fonts'] = fixPath(opt['fonts']); // Set default device options - this.keyman.setDefaultDeviceOptions(opt); - - // Only do remainder of initialization once! + this.keyman.setDefaultDeviceOptions(opt); + + // Only do remainder of initialization once! if(this.keyman.initialized) { return Promise.resolve(); } @@ -1617,12 +1623,12 @@ namespace com.keyman.dom { this.keyman._MasterDocument = window.document; /** - * Initialization of touch devices and browser interfaces must be done + * Initialization of touch devices and browser interfaces must be done * after all resources are loaded, during final stage of initialization - * - */ - - // Treat Android devices as phones if either (reported) screen dimension is less than 4" + * + */ + + // Treat Android devices as phones if either (reported) screen dimension is less than 4" if(device.OS == 'Android') { // Determine actual device characteristics I3363 (Build 301) @@ -1630,8 +1636,8 @@ namespace com.keyman.dom { var dpi = device.getDPI(); //TODO: this will not work when called from HEAD!! device.formFactor=((screen.height < 4.0 * dpi) || (screen.width < 4.0 * dpi)) ? 'phone' : 'tablet'; } - - // Set exposed initialization flag member for UI (and other) code to use + + // Set exposed initialization flag member for UI (and other) code to use this.keyman.setInitialized(1); // Finish keymanweb and initialize the OSK once all necessary resources are available @@ -1641,7 +1647,7 @@ namespace com.keyman.dom { this.keyman.osk = new com.keyman.osk.FloatingOSKView(device.coreSpec); } const osk = this.keyman.osk; - + // Create and save the remote keyboard loading delay indicator util.prepareWait(); @@ -1650,7 +1656,7 @@ namespace com.keyman.dom { // Initialize the desktop UI this.initializeUI(); - + // Exit initialization here if we're using an embedded code path. if(this.keyman.isEmbedded) { if(!this.keyman.keyboardManager.setDefaultKeyboard()) { @@ -1666,7 +1672,7 @@ namespace com.keyman.dom { // Initialize touch-screen device interface I3363 (Build 301) if(device.touchable) { this.keyman.handleRotationEvents(); - } + } // Initialize browser interface if(this.keyman.options['attachType'] != 'manual') { @@ -1675,16 +1681,16 @@ namespace com.keyman.dom { // Create an ordered list of all input and textarea fields this.listInputs(); - + // Initialize the OSK and set default OSK styles // Note that this should *never* be called before the OSK has been initialized. - // However, it possibly may be called before the OSK has been fully defined with the current keyboard, need to check. - //osk._Load(); - - //document.body.appendChild(osk._Box); + // However, it possibly may be called before the OSK has been fully defined with the current keyboard, need to check. + //osk._Load(); + + //document.body.appendChild(osk._Box); //osk._Load(false); - + // I3363 (Build 301) if(device.touchable) { const osk = keyman.osk as osk.AnchoredOSKView; @@ -1698,23 +1704,23 @@ namespace com.keyman.dom { ds=dTrailer.style; ds.width='100%'; ds.height=(screen.width/2)+'px'; - document.body.appendChild(dTrailer); - + document.body.appendChild(dTrailer); + // Sets up page-default touch-based handling for activation-state management. - // These always trigger for the page, wherever a touch may occur. Does not + // These always trigger for the page, wherever a touch may occur. Does not // prevent element-specific or OSK-key-specific handling from triggering. const _this = this; this.touchStartActivationHandler=function(e) { _this.deactivateOnRelease=true; _this.touchY=e.touches[0].screenY; - // On Chrome, scrolling up or down causes the URL bar to be shown or hidden + // On Chrome, scrolling up or down causes the URL bar to be shown or hidden // according to whether or not the document is at the top of the screen. // But when doing that, each OSK row top and height gets modified by Chrome - // looking very ugly. It would be best to hide the OSK then show it again + // looking very ugly. It would be best to hide the OSK then show it again // when the user scroll finishes, but Chrome has no way to reliably report // the touch end event after a move. c.f. http://code.google.com/p/chromium/issues/detail?id=152913 - // The best compromise behaviour is simply to hide the OSK whenever any + // The best compromise behaviour is simply to hide the OSK whenever any // non-input and non-OSK element is touched. _this.deactivateOnScroll=false; if(device.OS == 'Android' && navigator.userAgent.indexOf('Chrome') > 0) { @@ -1768,17 +1774,17 @@ namespace com.keyman.dom { } //document.body.appendChild(keymanweb._StyleBlock); - + // Restore and reload the currently selected keyboard, selecting a default keyboard if necessary. - this.keyman.keyboardManager.restoreCurrentKeyboard(); + this.keyman.keyboardManager.restoreCurrentKeyboard(); /* Setup of handlers for dynamically-added and (eventually) dynamically-removed elements. * Reference: https://developer.mozilla.org/en/docs/Web/API/MutationObserver - * + * * We place it here so that it loads after most of the other UI loads, reducing the MutationObserver's overhead. * Of course, we only want to dynamically add elements if the user hasn't enabled the manual attachment option. */ - + if(typeof MutationObserver == 'function') { var observationTarget = document.querySelector('body'), observationConfig: MutationObserverInit; if(this.keyman.options['attachType'] != 'manual') { //I1961 @@ -1794,7 +1800,7 @@ namespace com.keyman.dom { this.enablementObserver = new MutationObserver(this._EnablementMutationObserverCore); this.enablementObserver.observe(observationTarget, observationConfig); } else { - console.warn("Your browser is outdated and does not support MutationObservers, a web feature " + + console.warn("Your browser is outdated and does not support MutationObservers, a web feature " + "needed by KeymanWeb to support dynamically-added elements."); } @@ -1814,12 +1820,12 @@ namespace com.keyman.dom { /** * Initialize the desktop user interface as soon as it is ready - **/ + **/ initializeUI() { if(this.keyman.ui && this.keyman.ui['initialize'] instanceof Function) { this.keyman.ui['initialize'](); // Display the OSK (again) if enabled, in order to set its position correctly after - // adding the UI to the page + // adding the UI to the page this.keyman.osk.present(); } else if(this.keyman.isEmbedded) { // UI modules aren't utilized in embedded mode. There's nothing to init, so we simply diff --git a/web/source/dom/preProcessor.ts b/web/source/dom/preProcessor.ts index 24bd6e0559..1a8dac6594 100644 --- a/web/source/dom/preProcessor.ts +++ b/web/source/dom/preProcessor.ts @@ -21,13 +21,13 @@ namespace com.keyman.dom { * Scope Private * @param {Event} e Event object * @param {boolean=} keyState true if call results from a keyDown event, false if keyUp, undefined if keyPress - * @return {Object.} KMW keyboard event object: - * Description Get object with target element, key code, shift state, virtual key state + * @return {Object.} KMW keyboard event object: + * Description Get object with target element, key code, shift state, virtual key state * Lcode=keyCode * Lmodifiers=shiftState * LisVirtualKeyCode e.g. ctrl/alt key * LisVirtualKey e.g. Virtual key or non-keypress event - */ + */ static _GetKeyEventProperties(e: KeyboardEvent, keyState?: boolean): text.KeyEvent { let keyman = com.keyman.singleton; let core = keyman.core; @@ -46,7 +46,7 @@ namespace com.keyman.dom { // Stage 1 - track the true state of the keyboard's modifiers. var prevModState = core.keyboardProcessor.modStateFlags, curModState = 0x0000; var ctrlEvent = false, altEvent = false; - + let keyCodes = text.Codes.keyCodes; switch(s.Lcode) { case keyCodes['K_CTRL']: // The 3 shorter "K_*CTRL" entries exist in some legacy keyboards. @@ -68,16 +68,16 @@ namespace com.keyman.dom { /** * Two separate conditions exist that should trigger chiral modifier detection. Examples below use CTRL but also work for ALT. - * - * 1. The user literally just pressed CTRL, so the event has a valid `location` property we can utilize. + * + * 1. The user literally just pressed CTRL, so the event has a valid `location` property we can utilize. * Problem: its layer isn't presently activated within the OSK. - * + * * 2. CTRL has been held a while, so the OSK layer is valid, but the key event doesn't tell us the chirality of the active CTRL press. * Bonus issue: RAlt simulation may cause erasure of this location property, but it should ONLY be empty if pressed in this case. * We default to the 'left' variants since they're more likely to exist and cause less issues with RAlt simulation handling. - * + * * In either case, `e.getModifierState("Control")` is set to true, but as a result does nothing to tell us which case is active. - * + * * `e.location != 0` if true matches condition 1 and matches condition 2 if false. */ @@ -85,19 +85,19 @@ namespace com.keyman.dom { let modifierCodes = text.Codes.modifierCodes; if(e.getModifierState("Control")) { - curModState |= ((e.location != 0 && ctrlEvent) ? + curModState |= ((e.location != 0 && ctrlEvent) ? (e.location == 1 ? modifierCodes['LCTRL'] : modifierCodes['RCTRL']) : // Condition 1 prevModState & 0x0003); // Condition 2 } if(e.getModifierState("Alt")) { - curModState |= ((e.location != 0 && altEvent) ? + curModState |= ((e.location != 0 && altEvent) ? (e.location == 1 ? modifierCodes['LALT'] : modifierCodes['RALT']) : // Condition 1 prevModState & 0x000C); // Condition 2 } // Stage 2 - detect state key information. It can be looked up per keypress with no issue. s.Lstates = 0; - + s.Lstates |= e.getModifierState('CapsLock') ? modifierCodes['CAPS'] : modifierCodes['NO_CAPS']; s.Lstates |= e.getModifierState('NumLock') ? modifierCodes['NUM_LOCK'] : modifierCodes['NO_NUM_LOCK']; s.Lstates |= (e.getModifierState('ScrollLock') || e.getModifierState("Scroll")) // "Scroll" for IE9. @@ -134,14 +134,14 @@ namespace com.keyman.dom { } } else { // No need to sim AltGr here; we don't need chiral ALTs. - s.Lmodifiers = + s.Lmodifiers = (curModState & 0x10) | // SHIFT - ((curModState & (modifierCodes['LCTRL'] | modifierCodes['RCTRL'])) ? 0x20 : 0) | - ((curModState & (modifierCodes['LALT'] | modifierCodes['RALT'])) ? 0x40 : 0); + ((curModState & (modifierCodes['LCTRL'] | modifierCodes['RCTRL'])) ? 0x20 : 0) | + ((curModState & (modifierCodes['LALT'] | modifierCodes['RALT'])) ? 0x40 : 0); } - /* Tweak the modifiers if an OS meta key is detected; this will allow meta-key-based + /* Tweak the modifiers if an OS meta key is detected; this will allow meta-key-based * hotkeys to bypass Keyman processing. We do this AFTER the chiral modifier filtering * because some keyboards specify their own modifierBitmask, which won't include it. * We don't currently use that reference in this method, but that may change in the future. @@ -151,7 +151,7 @@ namespace com.keyman.dom { // Physically-typed keys require use of a 'desktop' form factor and thus are based on a virtual "physical" Device. s.device = keyman.util.physicalDevice.coreSpec; - // Perform any browser-specific key remapping before other remaps and mnemonic transforms. + // Perform any browser-specific key remapping before other remaps and mnemonic transforms. // (See https://github.com/keymanapp/keyman/issues/1125.) if(!keyman.isEmbedded && s.device.browser == utils.Browser.Firefox) { // Browser key identifiers are not completely consistent; Firefox has a few (for US punctuation) @@ -185,7 +185,7 @@ namespace com.keyman.dom { s.Lcode=Lbase['k'+s.Lcode]; } /* 13/03/2007 MCD: Swedish: End mapping of keystroke to US keyboard */ - + if(!activeKeyboard.definesPositionalOrMnemonic && !(s.Lmodifiers & 0x60)) { // Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic s = { @@ -200,11 +200,11 @@ namespace com.keyman.dom { }; } } - + return s; } - public static getEventOutputTarget(e: KeyboardEvent): text.OutputTarget { + public static getEventOutputTarget(e: Event): text.OutputTarget { let keyman = com.keyman.singleton; let target = keyman.util.eventTarget(e) as HTMLElement; if (target == null) { @@ -219,17 +219,17 @@ namespace com.keyman.dom { /** * Function keyDown * Scope Public - * Description Processes keydown event and passes data to keyboard. - * + * Description Processes keydown event and passes data to keyboard. + * * Note that the test-case oriented 'recorder' stubs this method to facilitate keystroke * recording for use in test cases. If changing this function, please ensure the recorder is * not affected. - */ + */ static keyDown(e: KeyboardEvent): boolean { let core = com.keyman.singleton.core; DOMEventHandlers.states.swallowKeypress = false; - // Get event properties + // Get event properties var Levent = this._GetKeyEventProperties(e, true); if(Levent == null) { return true; @@ -298,7 +298,7 @@ namespace com.keyman.dom { return false; } /* I732 END - 13/03/2007 MCD: Swedish: End positional keyboard layout code */ - + // Only reached if it's a mnemonic keyboard. let outputTarget = PreProcessor.getEventOutputTarget(e); if(DOMEventHandlers.states.swallowKeypress || core.keyboardInterface.processKeystroke(outputTarget, Levent)) { diff --git a/web/source/keyboards/kmwkeyboards.ts b/web/source/keyboards/kmwkeyboards.ts index 54c118baf1..17b2b18091 100644 --- a/web/source/keyboards/kmwkeyboards.ts +++ b/web/source/keyboards/kmwkeyboards.ts @@ -765,6 +765,10 @@ namespace com.keyman.keyboards { if(osk) { osk._Load(); } + + if(manager.keymanweb.domManager.lastActiveElement != null) { + core.processNewContextEvent(dom.Utils.getOutputTarget(manager.keymanweb.domManager.lastActiveElement)); + } } // Remove the wait message, if defined diff --git a/web/source/kmwbase.ts b/web/source/kmwbase.ts index c10fa5b7bd..29a6ab6da8 100644 --- a/web/source/kmwbase.ts +++ b/web/source/kmwbase.ts @@ -151,7 +151,7 @@ namespace com.keyman { } this._BrowserIsSafari = (navigator.userAgent.indexOf('AppleWebKit') >= 0); // I732 END - Support for European underlying keyboards #1 - this.core = new text.InputProcessor({ + this.core = new text.InputProcessor(this.util.device.coreSpec, { baseLayout: baseLayout, variableStoreSerializer: new dom.VariableStoreCookieSerializer() }); diff --git a/web/source/osk/oskView.ts b/web/source/osk/oskView.ts index c3bb941df5..362fcd5f57 100644 --- a/web/source/osk/oskView.ts +++ b/web/source/osk/oskView.ts @@ -616,10 +616,10 @@ namespace com.keyman.osk { // This handler is also triggered on state-key state changes (K_CAPS) that // may not actually change the layer. if(this.vkbd) { - this.vkbd._UpdateVKShiftStyle(); + this.vkbd._UpdateVKShiftStyle(newValue); } - if(source.value != newValue) { + if((this.vkbd && this.vkbd.layerId != newValue) || source.value != newValue) { // Prevents console errors when a keyboard only displays help. // Can occur when using SHIFT with sil_euro_latin on a desktop form-factor. if(this.vkbd) { From dcedb15b692b65e372bb7e0ccf87ce1cd94f5555 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 23 Nov 2021 16:08:19 +1100 Subject: [PATCH 04/54] chore: cleanup comments and defines --- common/core/desktop/src/kmx/kmx_file.h | 19 ++++++++++++++++++- .../src/text/inputProcessor.ts | 10 +++++----- .../KeymanEngine4Mac/KME/KMBinaryFileFormat.h | 12 +++++++++++- web/source/dom/domEventHandlers.ts | 5 ++--- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/common/core/desktop/src/kmx/kmx_file.h b/common/core/desktop/src/kmx/kmx_file.h index 5771b19999..13f60917c8 100644 --- a/common/core/desktop/src/kmx/kmx_file.h +++ b/common/core/desktop/src/kmx/kmx_file.h @@ -110,6 +110,8 @@ namespace kmx { #define TSS_COMPARISON 30 +#define TSS__KEYMAN_80_MAX 30 + /* Keyman 9.0 system stores */ #define TSS_PLATFORM 31 @@ -126,7 +128,22 @@ namespace kmx { #define TSS_TARGETS 38 -#define TSS__MAX 38 +#define TSS__KEYMAN_90_MAX 38 + +/* Keyman 14.0 system stores */ + +#define TSS_CASEDKEYS 39 + +#define TSS__KEYMAN_140_MAX 39 + +/* Keyman 15.0 system stores */ + +#define TSS_BEGIN_NEWCONTEXT 40 +#define TSS_BEGIN_POSTKEYSTROKE 41 + +#define TSS__KEYMAN_150_MAX 41 + +#define TSS__MAX 41 /* wm_keyman_control_internal message control codes */ diff --git a/common/core/web/input-processor/src/text/inputProcessor.ts b/common/core/web/input-processor/src/text/inputProcessor.ts index d67bb7687f..04a4ad9c0b 100644 --- a/common/core/web/input-processor/src/text/inputProcessor.ts +++ b/common/core/web/input-processor/src/text/inputProcessor.ts @@ -53,16 +53,16 @@ namespace com.keyman.text { } /** + * Tell the currently active keyboard that a new context has been selected, + * e.g. by focus change, selection change, keyboard change, etc. * - * @param outputTarget - * @returns + * @param {Object} outputTarget The OutputTarget that has focus + * @returns {Object} A RuleBehavior object describing the cumulative effects of + * all matched keyboard rules */ processNewContextEvent(outputTarget: OutputTarget): RuleBehavior { - // We presently need the true keystroke to run on the FULL context. That index is still - // needed for some indexing operations when comparing two different output targets. const ruleBehavior = this.keyboardProcessor.processNewContextEvent(this.device, outputTarget); - // Should we swallow any further processing of keystroke events for this? if(ruleBehavior != null) { ruleBehavior.finalize(this.keyboardProcessor, outputTarget); } diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h index 0f4614c1c9..23beb9716d 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h @@ -98,6 +98,7 @@ struct COMP_STORE { #define TSS__KEYMAN_70_MAX 29 /* Keyman 8.0 system stores */ #define TSS_COMPARISON 30 +#define TSS__KEYMAN_80_MAX 30 /* Keyman 9.0 system stores */ #define TSS_PLATFORM 31 #define TSS_BASELAYOUT 32 @@ -109,7 +110,16 @@ struct COMP_STORE { #define TSS_KEYBOARDVERSION 36 // &keyboardversion system store // I4140 #define TSS_KMW_EMBEDCSS 37 #define TSS_TARGETS 38 -#define TSS__MAX 38 +#define TSS__KEYMAN_90_MAX 38 +/* Keyman 14.0 system stores */ +#define TSS_CASEDKEYS 39 +#define TSS__KEYMAN_140_MAX 39 +/* Keyman 15.0 system stores */ +#define TSS_BEGIN_NEWCONTEXT 40 +#define TSS_BEGIN_POSTKEYSTROKE 41 +#define TSS__KEYMAN_150_MAX 41 + +#define TSS__MAX 41 // ShiftFlags #define LCTRLFLAG 0x0001 // Left Control flag diff --git a/web/source/dom/domEventHandlers.ts b/web/source/dom/domEventHandlers.ts index ae586a73d6..067ca2c6ee 100644 --- a/web/source/dom/domEventHandlers.ts +++ b/web/source/dom/domEventHandlers.ts @@ -355,11 +355,10 @@ namespace com.keyman.dom { DOMEventHandlers.states._SelectionControl = target; if(target && outputTarget) { - // // Call the current keyboard's newContext handler; // timeout is required in order to get the current - // selection, swhich is not ready at time of focus event - // + // selection, which is not ready at time of focus event, + // at least on Chrome window.setTimeout(() => { //console.log('processNewContextEvent called from focus'); com.keyman.singleton.core.processNewContextEvent(outputTarget); From f5fc41712d353b73fc2ccc7ec5a5613832510cf6 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 23 Nov 2021 16:56:16 +1100 Subject: [PATCH 05/54] feat(web): add support for begin postKeystroke Relates to #3621. This adds support for `begin postKeystroke` to KeymanWeb. This is now close to final, apart from support for `&layerChanged`. --- .../src/text/inputProcessor.ts | 9 ++ .../src/keyboards/keyboard.ts | 12 ++- .../src/text/kbdInterface.ts | 85 ++++++++----------- .../src/text/keyboardProcessor.ts | 13 ++- .../source/start_of_sentence_3621.kmn | 9 +- .../start_of_sentence_3621.js | 61 ++++++++----- 6 files changed, 113 insertions(+), 76 deletions(-) diff --git a/common/core/web/input-processor/src/text/inputProcessor.ts b/common/core/web/input-processor/src/text/inputProcessor.ts index 04a4ad9c0b..e03325af8b 100644 --- a/common/core/web/input-processor/src/text/inputProcessor.ts +++ b/common/core/web/input-processor/src/text/inputProcessor.ts @@ -256,6 +256,15 @@ namespace com.keyman.text { // For DOM-aware targets, this will trigger a DOM event page designers may listen for. outputTarget.doInputEvent(); } + + // The keyboard may want to take an action after all other keystroke processing is + // finished, for example to switch layers. This action may not have any output + // but may change system store or variable store values. Given this, we don't need to + // save anything about the post behavior, after finalizing it + let postRuleBehavior = this.keyboardProcessor.processPostKeystroke(keyEvent.device, outputTarget); + if(postRuleBehavior) { + postRuleBehavior.finalize(this.keyboardProcessor, outputTarget); + } } return ruleBehavior; diff --git a/common/core/web/keyboard-processor/src/keyboards/keyboard.ts b/common/core/web/keyboard-processor/src/keyboards/keyboard.ts index 9b9095cf79..d5d18593b0 100644 --- a/common/core/web/keyboard-processor/src/keyboards/keyboard.ts +++ b/common/core/web/keyboard-processor/src/keyboards/keyboard.ts @@ -58,16 +58,26 @@ namespace com.keyman.keyboards { } /** - * Calls the keyboard's `gs` function, which represents the keyboard source's group(main). + * Calls the keyboard's `gs` function, which represents the keyboard source's begin Unicode group. */ process(outputTarget: text.OutputTarget, keystroke: text.KeyEvent): boolean { return this.scriptObject['gs'](outputTarget, keystroke); } + /** + * Calls the keyboard's `gn` function, which represents the keyboard source's begin newContext group. + */ processNewContextEvent(outputTarget: text.OutputTarget, keystroke: text.KeyEvent): boolean { return this.scriptObject['gn'] ? this.scriptObject['gn'](outputTarget, keystroke) : false; } + /** + * Calls the keyboard's `gpk` function, which represents the keyboard source's begin postKeystroke group. + */ + processPostKeystroke(outputTarget: text.OutputTarget, keystroke: text.KeyEvent): boolean { + return this.scriptObject['gpk'] ? this.scriptObject['gpk'](outputTarget, keystroke) : false; + } + get isHollow(): boolean { return this.scriptObject == Keyboard.DEFAULT_SCRIPT_OBJECT; } diff --git a/common/core/web/keyboard-processor/src/text/kbdInterface.ts b/common/core/web/keyboard-processor/src/text/kbdInterface.ts index 0576100f94..33bd61db4b 100644 --- a/common/core/web/keyboard-processor/src/text/kbdInterface.ts +++ b/common/core/web/keyboard-processor/src/text/kbdInterface.ts @@ -948,74 +948,59 @@ namespace com.keyman.text { this.output(1, outputTarget, ""); } + /** + * Function processNewContextEvent + * Scope Private + * @param {Object} outputTarget The target receiving input + * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard. + * Description Calls the keyboard's `begin newContext` group + * @returns {number} 0 if no match is made, otherwise 1. + */ processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { - // Clear internal state tracking data from prior keystrokes. - if(!outputTarget) { - throw "No target specified for keyboard output!"; - } else if(!this.activeKeyboard) { + if(!this.activeKeyboard) { throw "No active keyboard for keystroke processing!"; } + return this.process(this.activeKeyboard.processNewContextEvent.bind(this.activeKeyboard), outputTarget, keystroke); + } - outputTarget.invalidateSelection(); - - outputTarget.deadkeys().resetMatched(); // I3318 - this.resetContextCache(); - - // Capture the initial state of the OutputTarget before any rules are matched. - let preInput = Mock.from(outputTarget); - - // Capture the initial state of any variable stores - const cachedVariableStores = this.activeKeyboard.variableStores; - - // Establishes the results object, allowing corresponding commands to set values here as appropriate. - this.ruleBehavior = new RuleBehavior(); - - // Ensure the settings are in place so that KIFS/ifState activates and deactivates - // the appropriate rule(s) for the modeled device. - this.activeDevice = keystroke.device; - - // Calls the start-group of the active keyboard. - this.activeTargetOutput = outputTarget; - var matched = this.activeKeyboard.processNewContextEvent(outputTarget, keystroke); - this.activeTargetOutput = null; - - // Finalize the rule's results. - this.ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(preInput, keystroke); - - // We always backup the changes to variable stores to the RuleBehavior, to - // be applied during finalization, then restore them to the cached initial - // values to avoid side-effects with predictive text mocks. - this.ruleBehavior.variableStores = this.activeKeyboard.variableStores; - this.activeKeyboard.variableStores = cachedVariableStores; - - // `matched` refers to whether or not the FINAL rule (from any group) matched, rather than - // whether or not ANY rule matched. If the final rule doesn't match, we trigger the key's - // default behavior (if appropriate). - // - // See https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852 - this.ruleBehavior.triggerKeyDefault = !matched; - - // Clear our result-tracking variable to prevent any possible pollution for future processing. - let behavior = this.ruleBehavior; - this.ruleBehavior = null; - - return behavior; + /** + * Function processPostKeystroke + * Scope Private + * @param {Object} outputTarget The target receiving input + * @param {Object} keystroke The input keystroke with relevant properties to be mapped by the keyboard. + * Description Calls the keyboard's `begin postKeystroke` group + * @returns {number} 0 if no match is made, otherwise 1. + */ + processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { + if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; + } + return this.process(this.activeKeyboard.processPostKeystroke.bind(this.activeKeyboard), outputTarget, keystroke); } /** * Function processKeystroke * Scope Private - * @param {Object} element The page element receiving input + * @param {Object} outputTarget The target receiving input * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard. * Description Encapsulates calls to keyboard input processing. * @returns {number} 0 if no match is made, otherwise 1. */ processKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { + if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; + } + return this.process(this.activeKeyboard.process.bind(this.activeKeyboard), outputTarget, keystroke); + } + + private process(callee, outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { // Clear internal state tracking data from prior keystrokes. if(!outputTarget) { throw "No target specified for keyboard output!"; } else if(!this.activeKeyboard) { throw "No active keyboard for keystroke processing!"; + } else if(!callee) { + throw "No callee for keystroke processing!"; } outputTarget.invalidateSelection(); @@ -1038,7 +1023,7 @@ namespace com.keyman.text { // Calls the start-group of the active keyboard. this.activeTargetOutput = outputTarget; - var matched = this.activeKeyboard.process(outputTarget, keystroke); + var matched = callee(outputTarget, keystroke); this.activeTargetOutput = null; // Finalize the rule's results. diff --git a/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts index 7b2ced989f..40c1eb53b5 100644 --- a/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -208,7 +208,6 @@ namespace com.keyman.text { } processNewContextEvent(device: utils.DeviceSpec, outputTarget: OutputTarget): RuleBehavior { - // Pass this key code and state to the keyboard program if(!this.activeKeyboard) { return null; } @@ -220,6 +219,18 @@ namespace com.keyman.text { return this.keyboardInterface.processNewContextEvent(outputTarget, keyEvent); } + processPostKeystroke(device: utils.DeviceSpec, outputTarget: OutputTarget): RuleBehavior { + if(!this.activeKeyboard) { + return null; + } + let keyEvent = new KeyEvent(); + keyEvent.Lcode = 0; + keyEvent.kName = ''; + keyEvent.device = device; + this.setSyntheticEventDefaults(keyEvent); + return this.keyboardInterface.processPostKeystroke(outputTarget, keyEvent); + } + processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { var matchBehavior: RuleBehavior; diff --git a/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn index eb216137e8..29dccd14f1 100644 --- a/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn +++ b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn @@ -10,15 +10,14 @@ store(&LAYOUTFILE) 'start_of_sentence_3621.keyman-touch-layout' begin Unicode > use(main) begin NewContext > use(NewContext) -c begin PostKeystroke > use(PostKeystroke) +begin PostKeystroke > use(PostKeystroke) group(NewContext) - nomatch > use(nextLayer) -c group(PostKeystroke) -c if(&layerChanged = "0") > use(nextLayer) -c nomatch > use(nextLayer) +group(PostKeystroke) +c if(&layerChanged = "0") > use(nextLayer) + nomatch > use(nextLayer) group(nextLayer) diff --git a/web/testing/start-of-sentence-3621/start_of_sentence_3621.js b/web/testing/start-of-sentence-3621/start_of_sentence_3621.js index 0d406e4f62..cedf04b382 100644 --- a/web/testing/start-of-sentence-3621/start_of_sentence_3621.js +++ b/web/testing/start-of-sentence-3621/start_of_sentence_3621.js @@ -1338,51 +1338,74 @@ function Keyboard_start_of_sentence_3621() } } ; - this.s_sentencePunctuation_11=".?!"; - this.s15="shift"; - this.s16="shift"; - this.s17="shift"; - this.s18="default"; + this.s_sentencePunctuation_13=".?!"; + this.s20="shift"; + this.s21="shift"; + this.s22="shift"; + this.s23="default"; this.KVER="15.0.114.0"; this.KVS=[]; this.gs=function(t,e) { - return this.g_main_1(t,e); + return this.g_main_3(t,e); }; this.gn=function(t,e) { return this.g_NewContext_0(t,e); }; + this.gpk=function(t,e) { + return this.g_PostKeystroke_1(t,e); + }; this.gs=function(t,e) { - return this.g_main_1(t,e); + return this.g_main_3(t,e); }; this.g_NewContext_0=function(t,e) { var k=KeymanWeb,r=1,m=0; - if(k.KFCM(3,t,[{t:'a',a:this.s_sentencePunctuation_11},' ',' '])){ - m=1; // Line 19 + if(!m) { + + k.KDC(-1,t); + r=this.g_nextLayer_2(t,e); + m=2; + } + return r; + }; + this.g_PostKeystroke_1=function(t,e) { + var k=KeymanWeb,r=1,m=0; + if(!m) { + + k.KDC(-1,t); + r=this.g_nextLayer_2(t,e); + m=2; + } + return r; + }; + this.g_nextLayer_2=function(t,e) { + var k=KeymanWeb,r=1,m=0; + if(k.KFCM(3,t,[{t:'a',a:this.s_sentencePunctuation_13},' ',' '])){ + m=1; // Line 27 k.KDC(3,t); - k.KIO(-1,this.s_sentencePunctuation_11,1,t); + k.KIO(-1,this.s_sentencePunctuation_13,1,t); k.KO(-1,t," "); - k.KSETS(33,this.s17,t); + k.KSETS(33,this.s22,t); } - else if(k.KFCM(2,t,[{t:'a',a:this.s_sentencePunctuation_11},' '])){ - m=1; // Line 18 + else if(k.KFCM(2,t,[{t:'a',a:this.s_sentencePunctuation_13},' '])){ + m=1; // Line 26 k.KDC(2,t); - k.KIO(-1,this.s_sentencePunctuation_11,1,t); + k.KIO(-1,this.s_sentencePunctuation_13,1,t); k.KO(-1,t," "); - k.KSETS(33,this.s16,t); + k.KSETS(33,this.s21,t); } else if(k.KFCM(1,t,[{t:'n'}])){ - m=1; // Line 17 + m=1; // Line 25 k.KDC(0,t); - k.KSETS(33,this.s15,t); + k.KSETS(33,this.s20,t); } if(!m) { k.KDC(-1,t); - k.KSETS(33,this.s18,t); + k.KSETS(33,this.s23,t); } return r; }; - this.g_main_1=function(t,e) { + this.g_main_3=function(t,e) { var k=KeymanWeb,r=0,m=0; return r; }; From ce2aa9a1669fb6418088a06995995c5bdeed7845 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 24 Nov 2021 06:45:20 +1100 Subject: [PATCH 06/54] feat(web): support for &layerChanged Relates to #3621. Add support for `&layerChanged` system store. This store is set to `1` before a `begin postKeystroke`, if the keystroke it follows resulted in a layer change, either programatically through a keyboard rule, or through a `nextlayer` property of the touched key. --- common/core/desktop/src/kmx/kmx_file.h | 5 +- .../src/text/inputProcessor.ts | 7 +++ .../src/text/kbdInterface.ts | 2 + .../src/text/keyboardProcessor.ts | 4 ++ .../source/start_of_sentence_3621.kmn | 18 ++++-- .../KeymanEngine4Mac/KME/KMBinaryFileFormat.h | 5 +- .../start_of_sentence_3621.js | 61 ++++++++++++------- .../TIKE/compile/CompileKeymanWeb.pas | 5 ++ windows/src/developer/kmcmpdll/Compiler.cpp | 8 ++- .../global/delphi/general/kmxfileconsts.pas | 21 ++++--- windows/src/global/inc/Compiler.h | 4 +- 11 files changed, 99 insertions(+), 41 deletions(-) diff --git a/common/core/desktop/src/kmx/kmx_file.h b/common/core/desktop/src/kmx/kmx_file.h index 13f60917c8..ba4980e911 100644 --- a/common/core/desktop/src/kmx/kmx_file.h +++ b/common/core/desktop/src/kmx/kmx_file.h @@ -140,10 +140,11 @@ namespace kmx { #define TSS_BEGIN_NEWCONTEXT 40 #define TSS_BEGIN_POSTKEYSTROKE 41 +#define TSS_LAYERCHANGED 42 -#define TSS__KEYMAN_150_MAX 41 +#define TSS__KEYMAN_150_MAX 42 -#define TSS__MAX 41 +#define TSS__MAX 42 /* wm_keyman_control_internal message control codes */ diff --git a/common/core/web/input-processor/src/text/inputProcessor.ts b/common/core/web/input-processor/src/text/inputProcessor.ts index e03325af8b..d565873db3 100644 --- a/common/core/web/input-processor/src/text/inputProcessor.ts +++ b/common/core/web/input-processor/src/text/inputProcessor.ts @@ -124,6 +124,8 @@ namespace com.keyman.text { // Current, long-existing assumption - it's DOM-backed. let preInputMock = Mock.from(outputTarget); + const startingLayerId = this.keyboardProcessor.layerId; + // We presently need the true keystroke to run on the FULL context. That index is still // needed for some indexing operations when comparing two different output targets. let ruleBehavior = this.keyboardProcessor.processKeystroke(keyEvent, outputTarget); @@ -261,6 +263,11 @@ namespace com.keyman.text { // finished, for example to switch layers. This action may not have any output // but may change system store or variable store values. Given this, we don't need to // save anything about the post behavior, after finalizing it + + // We need to tell the keyboard if the layer has been changed, either by a keyboard rule itself, + // or by the touch layout 'nextlayer' control. + this.keyboardProcessor.layerChangedStore.set(startingLayerId == this.keyboardProcessor.layerId ? '0' : '1'); + let postRuleBehavior = this.keyboardProcessor.processPostKeystroke(keyEvent.device, outputTarget); if(postRuleBehavior) { postRuleBehavior.finalize(this.keyboardProcessor, outputTarget); diff --git a/common/core/web/keyboard-processor/src/text/kbdInterface.ts b/common/core/web/keyboard-processor/src/text/kbdInterface.ts index 33bd61db4b..0c2db362ee 100644 --- a/common/core/web/keyboard-processor/src/text/kbdInterface.ts +++ b/common/core/web/keyboard-processor/src/text/kbdInterface.ts @@ -185,6 +185,7 @@ namespace com.keyman.text { static readonly TSS_LAYER: number = 33; static readonly TSS_PLATFORM: number = 31; + static readonly TSS_LAYERCHANGED: number = 42; systemStores: {[storeID: number]: SystemStore}; @@ -201,6 +202,7 @@ namespace com.keyman.text { this.systemStores[KeyboardInterface.TSS_PLATFORM] = new PlatformSystemStore(this); this.systemStores[KeyboardInterface.TSS_LAYER] = new MutableSystemStore(KeyboardInterface.TSS_LAYER, 'default'); + this.systemStores[KeyboardInterface.TSS_LAYERCHANGED] = new MutableSystemStore(KeyboardInterface.TSS_LAYERCHANGED, '0'); this.variableStoreSerializer = variableStoreSerializer; } diff --git a/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts index 40c1eb53b5..7be3c14c20 100644 --- a/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/core/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -91,6 +91,10 @@ namespace com.keyman.text { return this.keyboardInterface.systemStores[KeyboardInterface.TSS_LAYER] as MutableSystemStore; } + public get layerChangedStore(): MutableSystemStore { + return this.keyboardInterface.systemStores[KeyboardInterface.TSS_LAYERCHANGED] as MutableSystemStore; + } + public get layerId(): string { return this.layerStore.value; } diff --git a/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn index 29dccd14f1..7efd3d98e6 100644 --- a/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn +++ b/common/test/keyboards/start_of_sentence_3621/source/start_of_sentence_3621.kmn @@ -16,15 +16,25 @@ group(NewContext) nomatch > use(nextLayer) group(PostKeystroke) -c if(&layerChanged = "0") > use(nextLayer) - nomatch > use(nextLayer) + +store(caps) 'A'..'Z' +store(digit) '0'..'9' + + c until we get a 'CAPS' layer, stay on CAPS if 2+ chrs in caps + if(&layerChanged = "0") any(caps) any(caps) > context + + c stay on the numeric layer if we are there already + if(&layerChanged = "0") if(&layer = 'numeric') any(digit) > context + + c no other changes, so detect sentence or layer change + if(&layerChanged = "0") > use(nextLayer) group(nextLayer) store(sentencePunctuation) '.?!' nul > layer('shift') - any(sentencePunctuation) ' ' > context layer('shift') - any(sentencePunctuation) ' ' > context layer('shift') + any(sentencePunctuation) ' ' > context layer('shift') + any(sentencePunctuation) ' ' > context layer('shift') nomatch > layer('default') group(main) using keys diff --git a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h index 23beb9716d..5a49121580 100644 --- a/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h +++ b/mac/KeymanEngine4Mac/KeymanEngine4Mac/KME/KMBinaryFileFormat.h @@ -117,9 +117,10 @@ struct COMP_STORE { /* Keyman 15.0 system stores */ #define TSS_BEGIN_NEWCONTEXT 40 #define TSS_BEGIN_POSTKEYSTROKE 41 -#define TSS__KEYMAN_150_MAX 41 +#define TSS_LAYERCHANGED 42 +#define TSS__KEYMAN_150_MAX 42 -#define TSS__MAX 41 +#define TSS__MAX 42 // ShiftFlags #define LCTRLFLAG 0x0001 // Left Control flag diff --git a/web/testing/start-of-sentence-3621/start_of_sentence_3621.js b/web/testing/start-of-sentence-3621/start_of_sentence_3621.js index cedf04b382..b921ea94a3 100644 --- a/web/testing/start-of-sentence-3621/start_of_sentence_3621.js +++ b/web/testing/start-of-sentence-3621/start_of_sentence_3621.js @@ -1338,11 +1338,17 @@ function Keyboard_start_of_sentence_3621() } } ; - this.s_sentencePunctuation_13=".?!"; - this.s20="shift"; - this.s21="shift"; - this.s22="shift"; - this.s23="default"; + this.s_caps_12="ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + this.s_digit_13="0123456789"; + this.s_sentencePunctuation_15=".?!"; + this.s21="0"; + this.s22="0"; + this.s23="numeric"; + this.s24="0"; + this.s25="shift"; + this.s26="shift"; + this.s27="shift"; + this.s28="default"; this.KVER="15.0.114.0"; this.KVS=[]; this.gs=function(t,e) { @@ -1369,39 +1375,50 @@ function Keyboard_start_of_sentence_3621() }; this.g_PostKeystroke_1=function(t,e) { var k=KeymanWeb,r=1,m=0; - if(!m) { - - k.KDC(-1,t); - r=this.g_nextLayer_2(t,e); - m=2; - } + if(k.KFCM(2,t,[{t:'a',a:this.s_caps_12},{t:'a',a:this.s_caps_12}])&&k.KIFS(42,this.s21,t)){ + m=1; // Line 24 + k.KDC(2,t); + k.KIO(-1,this.s_caps_12,1,t); + k.KIO(-1,this.s_caps_12,2,t); + } + else if(k.KFCM(1,t,[{t:'a',a:this.s_digit_13}])&&k.KIFS(42,this.s22,t)&&k.KIFS(33,this.s23,t)){ + m=1; // Line 27 + k.KDC(1,t); + k.KIO(-1,this.s_digit_13,1,t); + } + else if(k.KIFS(42,this.s24,t)){ + m=1; // Line 30 + k.KDC(0,t); + r=this.g_nextLayer_2(t,e); + m=2; + } return r; }; this.g_nextLayer_2=function(t,e) { var k=KeymanWeb,r=1,m=0; - if(k.KFCM(3,t,[{t:'a',a:this.s_sentencePunctuation_13},' ',' '])){ - m=1; // Line 27 + if(k.KFCM(3,t,[{t:'a',a:this.s_sentencePunctuation_15},' ',' '])){ + m=1; // Line 37 k.KDC(3,t); - k.KIO(-1,this.s_sentencePunctuation_13,1,t); + k.KIO(-1,this.s_sentencePunctuation_15,1,t); k.KO(-1,t," "); - k.KSETS(33,this.s22,t); + k.KSETS(33,this.s27,t); } - else if(k.KFCM(2,t,[{t:'a',a:this.s_sentencePunctuation_13},' '])){ - m=1; // Line 26 + else if(k.KFCM(2,t,[{t:'a',a:this.s_sentencePunctuation_15},' '])){ + m=1; // Line 36 k.KDC(2,t); - k.KIO(-1,this.s_sentencePunctuation_13,1,t); + k.KIO(-1,this.s_sentencePunctuation_15,1,t); k.KO(-1,t," "); - k.KSETS(33,this.s21,t); + k.KSETS(33,this.s26,t); } else if(k.KFCM(1,t,[{t:'n'}])){ - m=1; // Line 25 + m=1; // Line 35 k.KDC(0,t); - k.KSETS(33,this.s20,t); + k.KSETS(33,this.s25,t); } if(!m) { k.KDC(-1,t); - k.KSETS(33,this.s23,t); + k.KSETS(33,this.s28,t); } return r; }; diff --git a/windows/src/developer/TIKE/compile/CompileKeymanWeb.pas b/windows/src/developer/TIKE/compile/CompileKeymanWeb.pas index 36c785b478..534a94d9f5 100644 --- a/windows/src/developer/TIKE/compile/CompileKeymanWeb.pas +++ b/windows/src/developer/TIKE/compile/CompileKeymanWeb.pas @@ -985,6 +985,11 @@ var ReportError(fkp.Line, CERR_NotSupportedInKeymanWebContext, Format('Statement notany in context() match requires version 14.0+ of KeymanWeb', [GetCodeName(recContext.Code)])); // I1971 // I4061 Result := Result + nlt + Format('k.KCXO(%d,t,%d,%d);', [len, AdjustIndex(fkp.dpContext, xstrlen(fkp.dpContext)), AdjustIndex(fkp.dpContext, ContextIndex)]); end; + CODE_IFOPT, + CODE_IFSYSTEMSTORE, + CODE_NUL: + // These have no output for a context emit + ; else begin ReportError(fkp.Line, CERR_NotSupportedInKeymanWebContext, Format('Statement %s is not currently supported in context() match', [GetCodeName(recContext.Code)])); // I1971 // I4061 diff --git a/windows/src/developer/kmcmpdll/Compiler.cpp b/windows/src/developer/kmcmpdll/Compiler.cpp index 475e4f7e57..010832ec89 100644 --- a/windows/src/developer/kmcmpdll/Compiler.cpp +++ b/windows/src/developer/kmcmpdll/Compiler.cpp @@ -152,7 +152,7 @@ const PWCHAR LineTokens[] = { #define SSN__PREFIX L"&" -const PWCHAR StoreTokens[TSS__MAX + 2] = { +const PWCHAR StoreTokens[] = { L"", SSN__PREFIX L"BITMAP", SSN__PREFIX L"COPYRIGHT", @@ -195,9 +195,12 @@ const PWCHAR StoreTokens[TSS__MAX + 2] = { SSN__PREFIX L"CASEDKEYS", // #2241 SSN__PREFIX L"", // TSS_BEGIN_NEWCONTEXT SSN__PREFIX L"", // TSS_BEGIN_POSTKEYSTROKE + SSN__PREFIX L"LAYERCHANGED", NULL }; +static_assert(_countof(StoreTokens) == TSS__MAX + 2, "StoreTokens should have exactly TSS__MAX+2 elements"); + HINSTANCE g_hInstance; CompilerMessageProc msgproc = NULL; int currentLine = 0, nErrors = 0; @@ -1342,6 +1345,9 @@ DWORD ProcessSystemStore(PFILE_KEYBOARD fk, DWORD SystemID, PFILE_STORE sp) case TSS_BEGIN_POSTKEYSTROKE: break; + case TSS_LAYERCHANGED: + break; + default: return CERR_InvalidSystemStore; } diff --git a/windows/src/global/delphi/general/kmxfileconsts.pas b/windows/src/global/delphi/general/kmxfileconsts.pas index 51c5de4ee0..82720948ca 100644 --- a/windows/src/global/delphi/general/kmxfileconsts.pas +++ b/windows/src/global/delphi/general/kmxfileconsts.pas @@ -1,18 +1,18 @@ (* Name: kmxfileconsts Copyright: Copyright (C) SIL International. - Documentation: - Description: + Documentation: + Description: Create Date: 27 Mar 2008 Modified Date: 27 Aug 2012 Authors: mcdurdin - Related Files: - Dependencies: + Related Files: + Dependencies: - Bugs: - Todo: - Notes: + Bugs: + Todo: + Notes: History: 27 Mar 2008 - mcdurdin - Initial version (Refactored) 25 May 2009 - mcdurdin - Use names for functions rather than codes in KMW compiler error messages 17 Aug 2012 - mcdurdin - I3429 - V9.0 - Add support for if, set, reset, save to KeymanWeb compiler @@ -208,6 +208,9 @@ const TSS_BEGIN_NEWCONTEXT = 40; TSS_BEGIN_POSTKEYSTROKE = 41; + TSS_LAYERCHANGED = 42; + + TSS__MAX = 42; type TSystemStore = (ssNone = 0, ssBitmap = 1, ssCopyright = 2, ssHotkey = 3, ssLanguage = 4, ssLayout = 5, ssMessage = 6, @@ -221,7 +224,7 @@ type ssPlatform = 31, ssBaseLayout = 32, ssLayer = 33, ssVKDictionary = 34, ssLayoutFile = 35, // I3438 // I3483 ssKeyboardVersion = 36, ssKMW_EmbedCSS = 37, ssTargets = 38, ssCasedKeys = 39, - ssBegin_NewContext = 40, ssBegin_PostKeystroke = 41); // I4140 // I4368 // I4504 + ssBegin_NewContext = 40, ssBegin_PostKeystroke = 41, ssLayerChanged = 42); // I4140 // I4368 // I4504 const SystemStoreNames: array[TSystemStore] of WideString = ( @@ -236,7 +239,7 @@ const '', '', '', '', 'LAYOUTFILE', 'KEYBOARDVERSION', 'KMW_EMBEDCSS', 'TARGETS', //9.0 // I3483 // I4140 // I4368 // I4504 'CASEDKEYS', //14.0, - '', ''); //15.0 + '', '', 'LAYERCHANGED'); //15.0 implementation diff --git a/windows/src/global/inc/Compiler.h b/windows/src/global/inc/Compiler.h index e69c306bf4..f6c88ce03e 100644 --- a/windows/src/global/inc/Compiler.h +++ b/windows/src/global/inc/Compiler.h @@ -211,7 +211,9 @@ #define TSS_BEGIN_NEWCONTEXT 40 #define TSS_BEGIN_POSTKEYSTROKE 41 -#define TSS__MAX 41 +#define TSS_LAYERCHANGED 42 + +#define TSS__MAX 42 /* wm_keyman_control_internal message control codes */ From e24b7561c6a69b9cdd5f16b2c1858f1a5ce159be Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 25 Nov 2021 14:27:05 +1100 Subject: [PATCH 07/54] feat(developer): add readonly group support Relates to #3621. Adds `readonly` group support and verifies that keyboards meet constraints around usage of these groups. (Note: fixes test_valid.kps and test.bat which seemed to have some invalid tests?) --- windows/src/developer/kmcmpdll/Compiler.cpp | 113 ++++++++++++++++-- windows/src/developer/kmcmpdll/Compiler.rc | 5 + windows/src/global/delphi/general/compile.pas | 15 +++ windows/src/global/inc/Comperr.h | 6 + windows/src/global/inc/Compfile.h | 1 + windows/src/test/unit-tests/kmcomp/test.bat | 16 ++- .../kmcomp/test_5963_newcontext_1.kmn | 7 ++ .../kmcomp/test_5963_newcontext_2.kmn | 9 ++ .../kmcomp/test_5963_postkeystroke_1.kmn | 7 ++ .../kmcomp/test_5963_postkeystroke_2.kmn | 8 ++ ...st_5963_readonlygroup_misplacedcontext.kmn | 11 ++ .../kmcomp/test_5963_readonlygroup_output.kmn | 9 ++ ...test_5963_readonlygroup_usenonreadonly.kmn | 12 ++ .../kmcomp/test_5963_start_of_sentence.kmn | 35 ++++++ .../src/test/unit-tests/kmcomp/test_valid.kps | 4 +- windows/src/test/unit-tests/kmcomp/tests.kpj | 76 +++++++++++- 16 files changed, 314 insertions(+), 20 deletions(-) create mode 100644 windows/src/test/unit-tests/kmcomp/test_5963_newcontext_1.kmn create mode 100644 windows/src/test/unit-tests/kmcomp/test_5963_newcontext_2.kmn create mode 100644 windows/src/test/unit-tests/kmcomp/test_5963_postkeystroke_1.kmn create mode 100644 windows/src/test/unit-tests/kmcomp/test_5963_postkeystroke_2.kmn create mode 100644 windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_misplacedcontext.kmn create mode 100644 windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_output.kmn create mode 100644 windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_usenonreadonly.kmn create mode 100644 windows/src/test/unit-tests/kmcomp/test_5963_start_of_sentence.kmn diff --git a/windows/src/developer/kmcmpdll/Compiler.cpp b/windows/src/developer/kmcmpdll/Compiler.cpp index 010832ec89..644ef11f55 100644 --- a/windows/src/developer/kmcmpdll/Compiler.cpp +++ b/windows/src/developer/kmcmpdll/Compiler.cpp @@ -608,6 +608,12 @@ DWORD ProcessBeginLine(PFILE_KEYBOARD fk, PWSTR p) AddDebugStore(fk, BeginMode == BEGIN_UNICODE ? DEBUGSTORE_BEGIN L"Unicode" : DEBUGSTORE_BEGIN L"ANSI"); } } else { + PFILE_GROUP gp = &fk->dpGroupArray[tstr[2] - 1]; + if (!gp->fReadOnly) { + return BeginMode == BEGIN_NEWCONTEXT ? + CERR_NewContextGroupMustBeReadonly : + CERR_PostKeystrokeGroupMustBeReadonly; + } return AddStore(fk, BeginMode == BEGIN_NEWCONTEXT ? TSS_BEGIN_NEWCONTEXT : TSS_BEGIN_POSTKEYSTROKE, tstr, NULL); } @@ -873,7 +879,11 @@ DWORD ProcessGroupLine(PFILE_KEYBOARD fk, PWSTR p) if (!q) return CERR_InvalidGroupLine; gp->fUsingKeys = FALSE; - if (IsSameToken(&p, L"using") && IsSameToken(&p, L"keys")) gp->fUsingKeys = TRUE; + gp->fReadOnly = IsSameToken(&p, L"readonly"); + if (!gp->fReadOnly) { + if (IsSameToken(&p, L"using") && IsSameToken(&p, L"keys")) + gp->fUsingKeys = TRUE; + } safe_wcsncpy(gp->szName, q, SZMAX_GROUPNAME); @@ -1504,21 +1514,90 @@ BOOL CheckContextStatementPositions(PWSTR context) { /** * Checks if a use() statement is followed by other content in the output of a rule */ -DWORD CheckUseStatementsInOutput(PWSTR output) { // I4867 - BOOL HasUse = FALSE; +DWORD +CheckUseStatementsInOutput(const PFILE_GROUP gp, const PWSTR output) { // I4867 + BOOL hasUse = FALSE; PWSTR p; for (p = output; *p; p = incxstr(p)) { if (*p == UC_SENTINEL && *(p + 1) == CODE_USE) { - HasUse = TRUE; - } else if (HasUse) { + hasUse = TRUE; + } else if (hasUse) { AddWarning(CWARN_UseNotLastStatementInRule); - return FALSE; + break; } } - return TRUE; + return CERR_None; } -DWORD ProcessKeyLine(PFILE_KEYBOARD fk, PWSTR str, BOOL IsUnicode) +/** + * Adds implicit `context` to start of output of rules for readonly groups + */ +DWORD +InjectContextToReadonlyOutput(PWSTR pklOut) { + if (pklOut[0] != UC_SENTINEL || pklOut[1] != CODE_CONTEXT) { + if (wcslen(pklOut) > GLOBAL_BUFSIZE - 3) { + return CERR_MEMORY; + } + memmove(pklOut + 2, pklOut, (wcslen(pklOut) + 1) * 2); + pklOut[0] = UC_SENTINEL; + pklOut[1] = CODE_CONTEXT; + } + return CERR_None; +} + +/** + * Verifies that a keyboard does not attempt to emit characters or + * other changes to text store when processing a readonly group + */ +DWORD +CheckOutputIsReadonly(const PFILE_KEYBOARD fk, const PWSTR output) { // I4867 + PWSTR p; + for (p = output; *p; p = incxstr(p)) { + if (*p != UC_SENTINEL) { + return CERR_OutputInReadonlyGroup; + } + switch (*(p + 1)) { + case CODE_CALL: + // We cannot be sure that the callee is going to be readonly + // but we have to operate on a trust basis for call() in any + // case, so we'll allow it. + continue; + case CODE_USE: + // We only allow use() of other readonly groups + { + PFILE_GROUP targetGroup = &fk->dpGroupArray[*(p + 2) - 1]; + if (!targetGroup->fReadOnly) { + return CERR_CannotUseReadWriteGroupFromReadonlyGroup; + } + } + continue; + case CODE_SETOPT: + case CODE_RESETOPT: + case CODE_SAVEOPT: + // it is okay to set, reset or save keyboard options + // although it's hard to see good use cases for this + continue; + case CODE_SETSYSTEMSTORE: + // it is okay to set system stores; Engine or Core will + // ignore set(&) that are not permissible in the given context + continue; + case CODE_CONTEXT: + // We allow `context` but only as the very first statement in output + if (p == output) { + continue; + } + return CERR_OutputInReadonlyGroup; + default: + // Note: conceptually, CODE_NUL could be transformed to CODE_CONTEXT + // if the context was also empty, but it is probably safest to avoid this, + // given CODE_CONTEXT does what we need anyway + return CERR_StatementNotPermittedInReadonlyGroup; + } + } + return CERR_None; +} + + DWORD ProcessKeyLine(PFILE_KEYBOARD fk, PWSTR str, BOOL IsUnicode) { PWSTR p, pp; DWORD msg; @@ -1562,7 +1641,23 @@ DWORD ProcessKeyLine(PFILE_KEYBOARD fk, PWSTR str, BOOL IsUnicode) if ((msg = CheckStatementOffsets(fk, gp, pklIn, pklOut, pklKey)) != CERR_None) return msg; // Test that use() statements are not followed by other content - CheckUseStatementsInOutput(pklOut); // I4867 + if ((msg = CheckUseStatementsInOutput(gp, pklOut)) != CERR_None) { + return msg; // I4867 + } + + if (gp->fReadOnly) { + // Ensure no output is made from the rule, and that + // use() statements meet required readonly semantics + if ((msg = CheckOutputIsReadonly(fk, pklOut)) != CERR_None) { + return msg; + } + + // Inject `context` to start of output if group is readonly + // to keep the output internally consistent + if ((msg = InjectContextToReadonlyOutput(pklOut)) != CERR_None) { + return msg; + } + } kp = new FILE_KEY[gp->cxKeyArray + 1]; if (!kp) return CERR_CannotAllocateMemory; diff --git a/windows/src/developer/kmcmpdll/Compiler.rc b/windows/src/developer/kmcmpdll/Compiler.rc index 1cb5a36b57..eb23363d33 100644 --- a/windows/src/developer/kmcmpdll/Compiler.rc +++ b/windows/src/developer/kmcmpdll/Compiler.rc @@ -243,4 +243,9 @@ BEGIN CERR_CasedKeysMustContainOnlyVirtualKeys "The &CasedKeys system store must contain only virtual keys or characters found on a US English keyboard" CERR_CasedKeysMustNotIncludeShiftStates "The &CasedKeys system store must not include shift states" CERR_CasedKeysNotSupportedWithMnemonicLayout "The &CasedKeys system store is not supported with mnemonic layouts" + CERR_CannotUseReadWriteGroupFromReadonlyGroup "Group used from a readonly group must also be readonly" + CERR_StatementNotPermittedInReadonlyGroup "Statement is not permitted in output of readonly group" + CERR_OutputInReadonlyGroup "Output is not permitted in a readonly group" + CERR_NewContextGroupMustBeReadonly "Group used in begin newContext must be readonly" + CERR_PostKeystrokeGroupMustBeReadonly "Group used in begin postKeystroke must be readonly" END diff --git a/windows/src/global/delphi/general/compile.pas b/windows/src/global/delphi/general/compile.pas index e7ba88d492..dc721062da 100644 --- a/windows/src/global/delphi/general/compile.pas +++ b/windows/src/global/delphi/general/compile.pas @@ -79,6 +79,7 @@ type dpNoMatch: PWideChar; // from start of group structure cxKeyArray: DWORD; // in array items fUsingKeys: BOOL; // group(xx) [using keys] <-- specified or not + fReadOnly: BOOL; end; PFILE_GROUP = ^FILE_GROUP; @@ -146,6 +147,15 @@ const CERR_MEMORY = $00001000; CWARN_Info = $0000208A; +const + // kcframe --sizeof returns these values + FILE_KEYBOARD_SIZE = 2952; + FILE_GROUP_SIZE = 184; + FILE_STORE_SIZE = 192; + FILE_KEY_SIZE = 20; + FILE_DEADKEY_SIZE = 160; + + function CompileKeyboardFile(kmnFile, kmxFile: PChar; FSaveDebug, CompilerWarningsAsErrors, WarnDeprecatedCode: BOOL; CallBack: TCompilerCallback): Integer; cdecl; // I4865 // I4866 function CompileKeyboardFileToBuffer(kmnFile: PChar; buf: PFILE_KEYBOARD; CompilerWarningsAsErrors, WarnDeprecatedCode: BOOL; CallBack: TCompilerCallback; Target: Integer): Integer; cdecl; // I4865 // I4866 function Compiler_Diagnostic(mode: Integer): Integer; @@ -285,6 +295,11 @@ initialization // We want to early load the compiler because we need it loaded for // sentry symbolication: https://github.com/getsentry/sentry-native/issues/213 LoadCompiler; + Assert(sizeof(FILE_KEYBOARD) = FILE_KEYBOARD_SIZE); + Assert(sizeof(FILE_GROUP) = FILE_GROUP_SIZE); + Assert(sizeof(FILE_STORE) = FILE_STORE_SIZE); + Assert(sizeof(FILE_KEY) = FILE_KEY_SIZE); + Assert(sizeof(FILE_DEADKEY) = FILE_DEADKEY_SIZE); finalization if HKMCmpDll > 0 then diff --git a/windows/src/global/inc/Comperr.h b/windows/src/global/inc/Comperr.h index a8cc49ec7f..94465a7611 100644 --- a/windows/src/global/inc/Comperr.h +++ b/windows/src/global/inc/Comperr.h @@ -153,6 +153,12 @@ #define CERR_CasedKeysMustNotIncludeShiftStates 0x0000406A #define CERR_CasedKeysNotSupportedWithMnemonicLayout 0x0000406B +#define CERR_CannotUseReadWriteGroupFromReadonlyGroup 0x0000406C +#define CERR_StatementNotPermittedInReadonlyGroup 0x0000406D +#define CERR_OutputInReadonlyGroup 0x0000406E +#define CERR_NewContextGroupMustBeReadonly 0x0000406F +#define CERR_PostKeystrokeGroupMustBeReadonly 0x00004070 + #define CWARN_TooManyWarnings 0x00002080 #define CWARN_OldVersion 0x00002081 #define CWARN_BitmapNotUsed 0x00002082 diff --git a/windows/src/global/inc/Compfile.h b/windows/src/global/inc/Compfile.h index 6aeacfd7e2..285b24fff0 100644 --- a/windows/src/global/inc/Compfile.h +++ b/windows/src/global/inc/Compfile.h @@ -101,6 +101,7 @@ struct FILE_GROUP { PWSTR dpNoMatch; // from start of group structure DWORD cxKeyArray; // in array items BOOL fUsingKeys; // group(xx) [using keys] <-- specified or not + BOOL fReadOnly; // group(xx) [readonly] <-- specified or not }; typedef FILE_GROUP *PFILE_GROUP; diff --git a/windows/src/test/unit-tests/kmcomp/test.bat b/windows/src/test/unit-tests/kmcomp/test.bat index f2cc22d979..71cf08dfa7 100644 --- a/windows/src/test/unit-tests/kmcomp/test.bat +++ b/windows/src/test/unit-tests/kmcomp/test.bat @@ -52,25 +52,33 @@ call :should-pass "#2241: &CasedKeys (chars)" test_casedkeys_chars.kmn || goto : call :should-fail "#2241: &CasedKeys (mnemonic 1)" test_casedkeys_mnemonic_1.kmn || goto :eof call :should-fail "#2241: &CasedKeys (mnemonic 2)" test_casedkeys_mnemonic_2.kmn || goto :eof -call :should-pass "#2241: &CasedKeys (mnemonic 3)" test_casedkeys_mnemonic_3.kmn || goto :eof +call :should-fail "#2241: &CasedKeys (mnemonic 3)" test_casedkeys_mnemonic_3.kmn || goto :eof call :should-fail "#2241: &CasedKeys (invalid chars 1)" test_casedkeys_invalid_1.kmn || goto :eof call :should-fail "#2241: &CasedKeys (invalid chars 2)" test_casedkeys_invalid_2.kmn || goto :eof +call :should-pass "#5963 start-of-sentence" test_5963_start_of_sentence.kmn || goto :eof +call :should-fail "#5963 begin newcontext (missing group)" test_5963_newcontext_1.kmn || goto :eof +call :should-fail "#5963 begin newcontext (not readonly)" test_5963_newcontext_2.kmn || goto :eof +call :should-fail "#5963 begin postkeystroke (missing group)" test_5963_postkeystroke_1.kmn || goto :eof +call :should-fail "#5963 begin postkeystroke (not readonly) " test_5963_postkeystroke_2.kmn || goto :eof +call :should-fail "#5963 context not first token in readonly group output" test_5963_readonlygroup_misplacedcontext.kmn || goto :eof +call :should-fail "#5963 emitting chars in readonly group" test_5963_readonlygroup_output.kmn || goto :eof +call :should-fail "#5963 using non-readonly group in readonly group" test_5963_readonlygroup_usenonreadonly.kmn || goto :eof goto :eof :should-pass echo %BLUE%TEST: %1 %WHITE% -"%compiler%" -s -w tests.kpj -t "%2" +"%compiler%" -no-color -s -w tests.kpj -t "%2" if %ERRORLEVEL% EQU 0 ( echo %GREEN%TEST PASSED%WHITE% exit /b 0 ) echo %RED%FAILED: expected %2 to be valid.%WHITE% 1>&2 -goto :eof +exit /b 1 :should-fail echo %BLUE%TEST: %1 %WHITE% -"%compiler%" -s -w tests.kpj -t "%2" +"%compiler%" -no-color -s -w tests.kpj -t "%2" if %ERRORLEVEL% GTR 0 ( echo %GREEN%TEST PASSED%WHITE% exit /b 0 diff --git a/windows/src/test/unit-tests/kmcomp/test_5963_newcontext_1.kmn b/windows/src/test/unit-tests/kmcomp/test_5963_newcontext_1.kmn new file mode 100644 index 0000000000..06313b89a6 --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_5963_newcontext_1.kmn @@ -0,0 +1,7 @@ +c Should fail to compile, missing group + +begin Unicode > use(main) +begin NewContext > use(NewContext) + +group(main) using keys + diff --git a/windows/src/test/unit-tests/kmcomp/test_5963_newcontext_2.kmn b/windows/src/test/unit-tests/kmcomp/test_5963_newcontext_2.kmn new file mode 100644 index 0000000000..a2916de43a --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_5963_newcontext_2.kmn @@ -0,0 +1,9 @@ +c Should fail to compile, group is not readonly + +begin Unicode > use(main) +begin NewContext > use(NewContext) + +group(NewContext) + +group(main) using keys + diff --git a/windows/src/test/unit-tests/kmcomp/test_5963_postkeystroke_1.kmn b/windows/src/test/unit-tests/kmcomp/test_5963_postkeystroke_1.kmn new file mode 100644 index 0000000000..d13fbd60ab --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_5963_postkeystroke_1.kmn @@ -0,0 +1,7 @@ +c Should fail to compile, missing group + +begin Unicode > use(main) +begin PostKeystroke > use(PostKeystroke) + +group(main) using keys + diff --git a/windows/src/test/unit-tests/kmcomp/test_5963_postkeystroke_2.kmn b/windows/src/test/unit-tests/kmcomp/test_5963_postkeystroke_2.kmn new file mode 100644 index 0000000000..12bb366150 --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_5963_postkeystroke_2.kmn @@ -0,0 +1,8 @@ +c Should fail to compile, group is not readonly + +begin Unicode > use(main) +begin PostKeystroke > use(PostKeystroke) + +group(PostKeystroke) + +group(main) using keys diff --git a/windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_misplacedcontext.kmn b/windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_misplacedcontext.kmn new file mode 100644 index 0000000000..251a3d1033 --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_misplacedcontext.kmn @@ -0,0 +1,11 @@ +c Should fail to compile, `context` must be omitted or first statement in output + +begin Unicode > use(main) + +group(main) using keys + +group(foo) readonly + +'a' > use(bar) context + +group(bar) readonly diff --git a/windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_output.kmn b/windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_output.kmn new file mode 100644 index 0000000000..38502f1274 --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_output.kmn @@ -0,0 +1,9 @@ +c should fail to compile, emits characters in a readonly group + +begin Unicode > use(main) + +group(main) using keys + +group(foo) readonly + +'a' > 'b' diff --git a/windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_usenonreadonly.kmn b/windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_usenonreadonly.kmn new file mode 100644 index 0000000000..8ced461fc7 --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_5963_readonlygroup_usenonreadonly.kmn @@ -0,0 +1,12 @@ +c should fail to compile, readonly group uses() non-readonly group + +begin Unicode > use(main) + +group(main) using keys + +group(foo) readonly + +'a' > use(bar) + +group(bar) + diff --git a/windows/src/test/unit-tests/kmcomp/test_5963_start_of_sentence.kmn b/windows/src/test/unit-tests/kmcomp/test_5963_start_of_sentence.kmn new file mode 100644 index 0000000000..7fce3f8c9f --- /dev/null +++ b/windows/src/test/unit-tests/kmcomp/test_5963_start_of_sentence.kmn @@ -0,0 +1,35 @@ +c should compile, uses all features of start-of-sentence #3621 (PR #5963) + +store(&targets) 'any' + +begin Unicode > use(main) +begin NewContext > use(NewContext) +begin PostKeystroke > use(PostKeystroke) + +group(NewContext) readonly + nomatch > use(nextLayer) + +group(PostKeystroke) readonly + +store(caps) 'A'..'Z' +store(digit) '0'..'9' + + c until we get a 'CAPS' layer, stay on CAPS if 2+ chrs in caps + if(&layerChanged = "0") any(caps) any(caps) > context + + c stay on the numeric layer if we are there already + if(&layerChanged = "0") if(&layer = 'numeric') any(digit) > context + + c no other changes, so detect sentence or layer change + if(&layerChanged = "0") > use(nextLayer) + +group(nextLayer) readonly + + store(sentencePunctuation) '.?!' + nul > layer('shift') + any(sentencePunctuation) ' ' > layer('shift') + any(sentencePunctuation) ' ' > layer('shift') + nomatch > layer('default') + +group(main) using keys + diff --git a/windows/src/test/unit-tests/kmcomp/test_valid.kps b/windows/src/test/unit-tests/kmcomp/test_valid.kps index c25cdce1f8..d64ee42848 100644 --- a/windows/src/test/unit-tests/kmcomp/test_valid.kps +++ b/windows/src/test/unit-tests/kmcomp/test_valid.kps @@ -31,7 +31,9 @@ test_valid 1.0 True - + + English + diff --git a/windows/src/test/unit-tests/kmcomp/tests.kpj b/windows/src/test/unit-tests/kmcomp/tests.kpj index 387b7ad989..086742588a 100644 --- a/windows/src/test/unit-tests/kmcomp/tests.kpj +++ b/windows/src/test/unit-tests/kmcomp/tests.kpj @@ -226,6 +226,16 @@ Test Expansions + + id_c8f8bfc9e4fd77bdd6db910b6a18eb0b + test_casedkeys_mnemonic_3.kmn + test_casedkeys_mnemonic_3.kmn + 1.0 + .kmn +
+ Test mnemonic layout but no cased keys +
+
id_5f9e583f7392151b3c3a7d4577b31a4f test_valid.kmx @@ -251,14 +261,68 @@ id_98467bd680620f99f9adeb5347541b6a - id_c8f8bfc9e4fd77bdd6db910b6a18eb0b - test_casedkeys_mnemonic_3.kmn - test_casedkeys_mnemonic_3.kmn + id_4de56227a5ee2e083ad7dfe971272f41 + test_5963_start_of_sentence.kmn + test_5963_start_of_sentence.kmn 1.0 .kmn -
- Test mnemonic layout but no cased keys -
+
+ + + id_617d47ee42934d7c9adcca257f8fc537 + test_5963_readonlygroup_usenonreadonly.kmn + test_5963_readonlygroup_usenonreadonly.kmn + 1.0 + .kmn +
+ + + id_7e12ebddcbd1fc497ef1d125c55e8ff2 + test_5963_readonlygroup_output.kmn + test_5963_readonlygroup_output.kmn + 1.0 + .kmn +
+ + + id_02e09e1457b2b2164e9ae62a897b0003 + test_5963_postkeystroke_1.kmn + test_5963_postkeystroke_1.kmn + 1.0 + .kmn +
+ + + id_7c92e58b16db283f505d0102c4833e02 + test_5963_newcontext_1.kmn + test_5963_newcontext_1.kmn + 1.0 + .kmn +
+ + + id_41c3aed69b2166aeae2fb77a4f9fb945 + test_5963_postkeystroke_2.kmn + test_5963_postkeystroke_2.kmn + 1.0 + .kmn +
+ + + id_43ae15b55415d3294219e26d13ebbb9e + test_5963_readonlygroup_misplacedcontext.kmn + test_5963_readonlygroup_misplacedcontext.kmn + 1.0 + .kmn +
+ + + id_d9f5051cafe945eb9f91aaaf9cfa94a3 + test_5963_newcontext_2.kmn + test_5963_newcontext_2.kmn + 1.0 + .kmn +
From 2de8c145c076b361494599fa1fd831809350c7c2 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Thu, 25 Nov 2021 14:29:11 +1100 Subject: [PATCH 08/54] fix(developer): kmdecomp string overflow Picked up in development of #5963. --- .../src/developer/kmdecomp/kmdecomp.vcxproj | 2 + .../src/developer/kmdecomp/savekeyboard.cpp | 40 +++++++++++-------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/windows/src/developer/kmdecomp/kmdecomp.vcxproj b/windows/src/developer/kmdecomp/kmdecomp.vcxproj index 490a97f401..8885b6f8e6 100644 --- a/windows/src/developer/kmdecomp/kmdecomp.vcxproj +++ b/windows/src/developer/kmdecomp/kmdecomp.vcxproj @@ -75,6 +75,7 @@ Level3 true EditAndContinue + stdcpp17 _DEBUG;%(PreprocessorDefinitions) @@ -111,6 +112,7 @@ Level3 true true + stdcpp17 NDEBUG;%(PreprocessorDefinitions) diff --git a/windows/src/developer/kmdecomp/savekeyboard.cpp b/windows/src/developer/kmdecomp/savekeyboard.cpp index 572644d3ac..79faa96f99 100644 --- a/windows/src/developer/kmdecomp/savekeyboard.cpp +++ b/windows/src/developer/kmdecomp/savekeyboard.cpp @@ -49,8 +49,8 @@ PWCHAR wcscat2(PWCHAR c1, size_t sz, const PWCHAR c2) return wcschr(c1, 0); } -#define MAX_SYSTEM_STORE 41 // I4652 -const PWCHAR StoreTokens[MAX_SYSTEM_STORE] = { // I4652 +// TODO: consolidate with list in compiler.cpp +const PWCHAR StoreTokens[] = { // I4652 L"", SSN__PREFIX L"BITMAP", SSN__PREFIX L"COPYRIGHT", @@ -91,9 +91,14 @@ const PWCHAR StoreTokens[MAX_SYSTEM_STORE] = { // I4652 SSN__PREFIX L"KMW_EMBEDCSS", SSN__PREFIX L"TARGETS", // I4504 SSN__PREFIX L"CASEDKEYS", + L"", // TSS_BEGIN_NEWCONTEXT + L"", // TSS_BEGIN_POSTKEYSTROKE + SSN__PREFIX L"LAYERCHANGED", NULL }; +static_assert(_countof(StoreTokens) == TSS__MAX + 2); + PWCHAR storename(int num) { static WCHAR buf[256]; @@ -199,13 +204,14 @@ PWCHAR ExtString(PWCHAR str) bufpointer = !bufpointer; p = buf[bufpointer]; *p = 0; + PWCHAR q = p; for(; *str; str++) { if(*str == UC_SENTINEL) { str++; - if(inquotes) p = wcscat2(p, BUFSIZE, L"\" "); + if(inquotes) p = wcscat2(q, BUFSIZE, L"\" "); inquotes = 0; switch(*str) { @@ -221,10 +227,10 @@ PWCHAR ExtString(PWCHAR str) p = wcschr(p, 0); break; case CODE_CONTEXT: - p = wcscat2(p, BUFSIZE, L"context "); + p = wcscat2(q, BUFSIZE, L"context "); break; case CODE_NUL: - p = wcscat2(p, BUFSIZE, L"nul "); + p = wcscat2(q, BUFSIZE, L"nul "); break; case CODE_USE: str++; @@ -232,10 +238,10 @@ PWCHAR ExtString(PWCHAR str) p = wcschr(p, 0); break; case CODE_RETURN: - p = wcscat2(p, BUFSIZE, L"return "); + p = wcscat2(q, BUFSIZE, L"return "); break; case CODE_BEEP: - p = wcscat2(p, BUFSIZE, L"beep "); + p = wcscat2(q, BUFSIZE, L"beep "); break; case CODE_DEADKEY: str++; @@ -259,13 +265,13 @@ PWCHAR ExtString(PWCHAR str) p = wcschr(p, 0); break; case CODE_SWITCH: - p = wcscat2(p, BUFSIZE, L"switch "); + p = wcscat2(q, BUFSIZE, L"switch "); break; case CODE_KEY: - p = wcscat2(p, BUFSIZE, L"key "); + p = wcscat2(q, BUFSIZE, L"key "); break; case CODE_CLEARCONTEXT: - p = wcscat2(p, BUFSIZE, L"clearcontext "); + p = wcscat2(q, BUFSIZE, L"clearcontext "); break; case CODE_CALL: str++; @@ -323,7 +329,7 @@ PWCHAR ExtString(PWCHAR str) break; default: - p = wcscat2(p, BUFSIZE, L"unknown() "); + p = wcscat2(q, BUFSIZE, L"unknown() "); break; } } @@ -331,13 +337,13 @@ PWCHAR ExtString(PWCHAR str) { if(*str == L'"') { - if(inquotes) p = wcscat2(p, BUFSIZE, L"\" "); + if(inquotes) p = wcscat2(q, BUFSIZE, L"\" "); inquotes = 0; - p = wcscat2(p, BUFSIZE, L"'\"' "); + p = wcscat2(q, BUFSIZE, L"'\"' "); } else if(*str < 32) { - if(inquotes) p = wcscat2(p, BUFSIZE, L"\" "); + if(inquotes) p = wcscat2(q, BUFSIZE, L"\" "); inquotes = 0; wsprintfW(p, L"x%x ", *str); p = wcschr(p, 0); @@ -347,7 +353,7 @@ PWCHAR ExtString(PWCHAR str) if(!inquotes) { inquotes = 1; - p = wcscat2(p, BUFSIZE, L"\""); + p = wcscat2(q, BUFSIZE, L"\""); } *p++ = *str; *p = 0; @@ -355,7 +361,7 @@ PWCHAR ExtString(PWCHAR str) } } - if(inquotes) p = wcscat2(p, BUFSIZE, L"\" "); + if(inquotes) p = wcscat2(q, BUFSIZE, L"\" "); return buf[bufpointer]; } @@ -393,7 +399,7 @@ int SaveKeyboardSource(LPKEYBOARD kbd, LPBYTE lpBitmap, DWORD cbBitmap, char *fi for(i = 0, sp = kbd->dpStoreArray; i < kbd->cxStoreArray; i++, sp++) { - if(sp->dwSystemID > 0 && (sp->dwSystemID >= MAX_SYSTEM_STORE || !StoreTokens[sp->dwSystemID][0])) // I4652 + if(sp->dwSystemID > 0 && (sp->dwSystemID > TSS__MAX || !StoreTokens[sp->dwSystemID][0])) // I4652 wsprintfW(buf, L"c store(&%d) %s\n", sp->dwSystemID, ExtString(sp->dpString)); else wsprintfW(buf, L"store(%s) %s\n", storename(i+1), ExtString(sp->dpString)); From 3601ae95e42bb06ffc22ef9e0d4d216e78cca7f9 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 29 Nov 2021 10:54:05 +1100 Subject: [PATCH 09/54] chore(web): fix broken test --- web/unit_tests/cases/attachmentAPI.js | 39 +++++++++++++++------------ 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/web/unit_tests/cases/attachmentAPI.js b/web/unit_tests/cases/attachmentAPI.js index 483823f68c..ea5fa2aa39 100644 --- a/web/unit_tests/cases/attachmentAPI.js +++ b/web/unit_tests/cases/attachmentAPI.js @@ -29,7 +29,7 @@ describe('Attachment API', function() { beforeEach(function() { fixture.load("robustAttachment.html"); }); - + afterEach(function(done) { fixture.cleanup(); window.setTimeout(function(){ @@ -79,7 +79,7 @@ describe('Attachment API', function() { DynamicElements.assertAttached(ele); let eventDriver = new KMWRecorder.BrowserDriver(ele['kmw_ip'] ? ele['kmw_ip'] : ele); eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(ele); + val = retrieveAndReset(ele); assert.equal(val, DynamicElements.disabledOutput, "'Disabled' element performed keystroke processing!"); keyman.enableControl(ele); @@ -147,25 +147,29 @@ describe('Attachment API', function() { keyman.attachToControl(input); keyman.attachToControl(textarea); - keyman.setActiveElement(input); // We assume from the other tests that running on the Lao keyboard will give proper output. // It'd be a redundant check. - // Set control with independent keyboard. + // We are testing that setting a specific keyboard for an inactive control does not affect + // the currently active control. The textarea control will be manually set to Khmer, + // and the input control will get the document default of Lao. + + keyman.setActiveElement(input); + // Set textarea control with independent keyboard khmer_angkor. keyman.setKeyboardForControl(textarea, "khmer_angkor", "km"); var eventDriver = new KMWRecorder.BrowserDriver(input['kmw_ip'] ? input['kmw_ip'] : input); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(input); assert.equal(val, DynamicElements.enabledLaoOutput, "KMW set independent keyboard for the incorrect control!"); - // Swap to a global-linked control... + // Swap to the textarea control with its overridden khmer_angkor keyboard... keyman.setActiveElement(textarea); eventDriver = new KMWRecorder.BrowserDriver(textarea['kmw_ip'] ? textarea['kmw_ip'] : textarea); eventDriver.simulateEvent(DynamicElements.keyCommand); val = retrieveAndReset(textarea); assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW did not properly store keyboard for the previously-inactive control!"); - // Swap back and check that the settings persist. + // Swap back to the input control and check that the settings persist. keyman.setActiveElement(input); keyman.setKeyboardForControl(textarea, null, null); @@ -175,9 +179,10 @@ describe('Attachment API', function() { assert.equal(val, DynamicElements.enabledLaoOutput, "KMW made a strange error when clearing an inactive control's keyboard setting!"); keyman.setActiveElement(textarea); - // Finally, clear the independent setting. + // Finally, after clearing the independent setting, check that we are back to Lao output as expected for the textarea + eventDriver = new KMWRecorder.BrowserDriver(textarea['kmw_ip'] ? textarea['kmw_ip'] : textarea); eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(input); + val = retrieveAndReset(textarea); assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not properly clear control's independent keyboard settings!"); }); }); @@ -194,22 +199,22 @@ Modernizr.on('touchevents', function(result) { fixture.setBase('fixtures'); setupKMW({ attachType:'auto' }, done, kmwconfig.timeouts.scriptLoad); }); - + beforeEach(function() { fixture.load("robustAttachment.html"); }); - + after(function() { teardownKMW(); }); - + afterEach(function(done) { fixture.cleanup(); window.setTimeout(function(){ done(); }, kmwconfig.timeouts.eventDelay); }); - + describe('Element Type', function() { it('', function(done) { var ID = DynamicElements.addInput(); @@ -227,7 +232,7 @@ Modernizr.on('touchevents', function(result) { it.skip('