diff --git a/HISTORY.md b/HISTORY.md index 654ae03b52..7909ef0351 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,30 @@ # Keyman Version History +## 19.0.229 alpha 2026-05-08 + +* maint(linux): fix source tarball used for packaging (#15926) + +## 19.0.228 alpha 2026-05-06 + +* fix(windows): caps lock stores to work in compliant applications (#15771) +* chore: move localization documentation to /docs (#15909) + +## 19.0.227 alpha 2026-05-05 + +* maint(linux): make emscripten setup on build agents idempotent (#15908) + +## 19.0.226 alpha 2026-05-04 + +* chore(common): cleanup optionals in ldml keyboard reader (#15865) + +## 19.0.225 alpha 2026-05-01 + +* fix(windows): contextual help chm file has blank pages (#15724) + +## 19.0.224 alpha 2026-04-30 + +* fix(linux): fix context after typing Bksp with Wayland (#15820) + ## 19.0.223 alpha 2026-04-23 * fix(android): Reorder display language list (#15863) diff --git a/VERSION.md b/VERSION.md index a015f73fc4..db15d64da1 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.224 +19.0.230 diff --git a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts index f25b03582d..d2ac028e72 100644 --- a/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/developer/src/common/web/utils/src/types/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -100,31 +100,31 @@ export class LDMLKeyboardXMLSourceFileReader implements EventResolver { boxXmlArray(source?.keyboard3?.locales, 'locale'); boxXmlArray(source?.keyboard3, 'transforms'); if(source?.keyboard3?.layers) { - for(const layers of source?.keyboard3?.layers) { + for(const layers of source.keyboard3.layers) { boxXmlArray(layers, 'layer'); - if(layers?.layer) { - for(const layer of layers?.layer) { + if(layers.layer) { + for(const layer of layers.layer) { boxXmlArray(layer, 'row'); } } } } if(source?.keyboard3?.forms?.form) { - boxXmlArray(source?.keyboard3?.forms, 'form'); - for(const form of source?.keyboard3?.forms?.form) { + boxXmlArray(source.keyboard3.forms, 'form'); + for(const form of source.keyboard3.forms.form) { boxXmlArray(form, 'scanCodes'); } } if(source?.keyboard3?.flicks) { - boxXmlArray(source?.keyboard3?.flicks, 'flick'); - for(const flick of source?.keyboard3?.flicks?.flick) { + boxXmlArray(source.keyboard3.flicks, 'flick'); + for(const flick of source.keyboard3.flicks.flick) { boxXmlArray(flick, 'flickSegment'); } } if(source?.keyboard3?.variables) { - boxXmlArray(source?.keyboard3?.variables, 'set'); - boxXmlArray(source?.keyboard3?.variables, 'string'); - boxXmlArray(source?.keyboard3?.variables, 'uset'); + boxXmlArray(source.keyboard3.variables, 'set'); + boxXmlArray(source.keyboard3.variables, 'string'); + boxXmlArray(source.keyboard3.variables, 'uset'); } if(source?.keyboard3?.transforms) { for(const transforms of source.keyboard3.transforms) { diff --git a/resources/build/l10n/README.md b/docs/localization/README.md similarity index 96% rename from resources/build/l10n/README.md rename to docs/localization/README.md index cb98d820a6..e4a0333b2e 100644 --- a/resources/build/l10n/README.md +++ b/docs/localization/README.md @@ -2,9 +2,9 @@ Localization for Keyman is maintained at https://translate.keyman.com -Downloading and updating files between Keyman and Crowdin happens +Downloading and updating files between Keyman and Crowdin happens on GitHub by way of the Crowdin git integration. The configuration file for all platforms -is a YAML file named [crowdin.yml](../../../crowdin.yml). Currently, the git integration tracks the `master` branch. +is a YAML file named [crowdin.yml](../../crowdin.yml). Currently, the git integration tracks the `master` branch. A daily GitHub action to update the source files in Crowdin, which are organized in a top-level folder "master". @@ -60,7 +60,7 @@ project. https://crowdin.com/project/keyman/settings#languages Only the project owner (sil_ltops) can edit custom language codes (at the bottom of the "Target Languages" list). -For some of these, the language mapping needs to be maintained in the "Language Mapping" button on the project settings menu (instead of the [crowdin.yml](../../../crowdin.yml)) configuration file. +For some of these, the language mapping needs to be maintained in the "Language Mapping" button on the project settings menu (instead of the [crowdin.yml](../../crowdin.yml)) configuration file. Language mappings have been set up for "Greek (Polytonic)" and "Shuwa (Latin)" so downloading with the Crowdin CLI would be: ``` diff --git a/resources/build/l10n/imgs/updating-ios-l10ns-1.png b/docs/localization/imgs/updating-ios-l10ns-1.png similarity index 100% rename from resources/build/l10n/imgs/updating-ios-l10ns-1.png rename to docs/localization/imgs/updating-ios-l10ns-1.png diff --git a/resources/build/l10n/imgs/updating-ios-l10ns-2.png b/docs/localization/imgs/updating-ios-l10ns-2.png similarity index 100% rename from resources/build/l10n/imgs/updating-ios-l10ns-2.png rename to docs/localization/imgs/updating-ios-l10ns-2.png diff --git a/linux/docs/internal/gtk-input-methods.md b/linux/docs/internal/gtk-input-methods.md index f340d72268..1127c91e96 100644 --- a/linux/docs/internal/gtk-input-methods.md +++ b/linux/docs/internal/gtk-input-methods.md @@ -18,25 +18,41 @@ support. ```mermaid flowchart LR - subgraph GTK - app(application) - im-module["im-ibus - (IBusIMContext)"] - app --- im-module + subgraph App["Application"] + style App fill:#9999ff + subgraph ClientApp["Client App"] + N1("GTK 3/4 Application") + end + subgraph GtkIMContext["Gtk.IMContext"] + N2["GTK 3:
in /usr/lib/x86_64-linux-gnu/
gtk-3.0/3.0.0/immodules:
im-ibus.so
im-wayland.so
im-xim.so"] + N3["GTK 4:
in /usr/lib/x86_64-linux-gnu/
gtk-4.0/4.0.0/immodules:
libim-ibus.so
(wayland built-in)"] + subgraph S2[" "] + IBusIMContext + N4["Source for IBus clients:
https://github.com/ibus/
ibus/tree/main/client"] + end + end end - subgraph ibus[ibus-daemon] - direction LR - IBusInputContext <--> IBusEngine + subgraph IBus["IBus Daemon"] + style IBus fill:#ff4dff + IBusInputContext["IBusInputContext"] + IBusEngine["IBusEngine"] + N5["Source for IBus Daemon in https://github.com/ibus/ibus/tree/main/src"] end - subgraph keyman - ibus-engine-keyman - end + KeymanEngine["Keyman Engine"] - GTK <--> ibus <--> keyman + ClientApp ~~~ GtkIMContext + IBusIMContext <-- IBus--> IBusInputContext + IBusInputContext <--> IBusEngine + IBusEngine <-- IBus --> KeymanEngine - style app color:#FFFFFF, stroke:#00C853, fill:#00C853 +style N1 color:#FFFFFF, stroke:#00C853, fill:#00C853 +style N2 fill:none +style N3 fill:none +style N4 fill:none +style N5 fill:none +style S2 fill:none, stroke-width:0 ``` The im-module implements a [GIOExtensionPoint](https://docs.gtk.org/gio/struct.IOExtensionPoint.html) @@ -129,4 +145,4 @@ After installing a new module the database needs to be updated with ## Links -- [GTK4 Input Handling](https://docs.gtk.org/gtk4/input-handling.html) \ No newline at end of file +- [GTK4 Input Handling](https://docs.gtk.org/gtk4/input-handling.html) diff --git a/linux/docs/internal/ibus-sequence-diagram.md b/linux/docs/internal/ibus-sequence-diagram.md new file mode 100644 index 0000000000..467eac43cf --- /dev/null +++ b/linux/docs/internal/ibus-sequence-diagram.md @@ -0,0 +1,48 @@ +# Communication between Application, IBus, and Keyman + +The sequence diagram below shows the sequence of events that happen in +response to IBus sending the "Enable" signal to the Keyman Engine. +This illustrates why `client_supports_surrounding_text()` might return +a wrong value initially. + +```mermaid +sequenceDiagram + autonumber + box rgb(153, 153, 255) Application + participant A as Client + participant IMC as IBusIMContext + end + box rgb(255, 77, 255) IBus + participant InC as IBusInputContext + participant E as IBusEngine + end + participant K as Keyman Engine + + E->>+K: enable signal + K->>+E: IBusEngine.get_surrounding_text(NULL, NULL, NULL) + Note left of K: Keyman wants to use surrounding text + E->>+InC: RequireSurroundingText + InC->>InC: set needs_surrounding_text flag = TRUE + InC->>+IMC: require-surrounding-text signal + IMC->>+A: retrieve_surrounding signal + A->>+IMC: gtk_im_context_set_surrounding() + IMC->>+InC: ibus_input_context_set_surrounding_text() + InC->>+E: SetSurroundingText + E->>+K: set_surrounding_text signal + Note right of K: client_supports_surrounding_text() might
report a wrong value
because the processing of
get_surrounding_text() hasn't finished yet + K->>-E: return from
set_surrounding_text signal + E->>-InC: return from
SetSurroundingText + InC->>-IMC: return from
ibus_input_context_set_surrounding_text() + IMC->>-A: return from
gtk_im_context_set_surrounding() + A->>-IMC: return from
retrieve_surrounding signal + IMC->>-InC: return from
require-surrounding-text signal + InC->>-E: return from
RequireSurroundingText + E->>-K: return from
get_surrounding_text() + K->>-E: return from
enable signal +``` + +## Links + +- +- +- diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index 5b1ccb6252..8231f7ed4d 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -83,10 +83,13 @@ struct _IBusKeymanEngine { gboolean rctrl_pressed; gboolean lalt_pressed; gboolean ralt_pressed; - IBusLookupTable *table; IBusProperty *status_prop; IBusPropList *prop_list; void *settings; + // Flag indicating if there are pending changes (forwarded keystrokes, + // deleted surrounding text) that need to be applied by committing an + //empty text to the input engine + gboolean is_dirty; commit_queue_item commit_queue[MAX_QUEUE_SIZE]; commit_queue_item *commit_item; @@ -246,45 +249,66 @@ debug_utf8_with_codepoints(const gchar *utf8) { static gboolean client_supports_surrounding_text(IBusEngine *engine) { + // This is not always reliable: IBus detects whether the client supports + // surrounding text by emitting the retrieve-surrounding signal. As part + // of that signal handler, the client is expected to call + // gtk_im_context_set_surrounding_with_selection which ends calling + // ibus_keyman_engine_set_surrounding_text at a time when IBus is still + // in the middle of determining whether the client supports surrounding text. g_assert(engine != NULL); + if (!engine->enabled) { + g_warning("%s: engine is not enabled, so return value is likley incorrect.", __FUNCTION__); + } return (engine->client_capabilities & IBUS_CAP_SURROUNDING_TEXT) != 0; } static void -set_context_if_needed(IBusEngine *engine) { - IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine; +set_context_impl( + IBusEngine *engine, + IBusText *text, + guint cursor_pos, + guint anchor_pos +) { + IBusKeymanEngine* keyman = (IBusKeymanEngine*)engine; + g_autofree gchar* application_context_utf8 = NULL; + guint context_start, context_end; + context_end = anchor_pos < cursor_pos ? anchor_pos : cursor_pos; + context_start = context_end > MAXCONTEXT_ITEMS ? context_end - MAXCONTEXT_ITEMS : 0; + application_context_utf8 = g_utf8_substring(ibus_text_get_text(text), context_start, context_end); + g_message( + "%s: new application context: |%s| (len:%u) cursor:%d anchor:%d", __FUNCTION__, application_context_utf8, + context_end - context_start, cursor_pos, anchor_pos); + km_core_cu* application_context_utf16 = g_utf8_to_utf16(application_context_utf8, -1, NULL, NULL, NULL); + km_core_context_status result; + result = km_core_state_context_set_if_needed(keyman->state, application_context_utf16); + g_free(application_context_utf16); + + g_message( + "%s: context %s", __FUNCTION__, + result == KM_CORE_CONTEXT_STATUS_UNCHANGED ? "unchanged" + : result == KM_CORE_CONTEXT_STATUS_UPDATED ? "updated" + : result == KM_CORE_CONTEXT_STATUS_CLEARED ? "cleared" + : result == KM_CORE_CONTEXT_STATUS_ERROR ? "error" + : "invalid argument"); +} + +static void +set_context_if_needed(IBusEngine *engine) { if (!client_supports_surrounding_text(engine)) { g_message("%s: not a compliant client app", __FUNCTION__); return; } IBusText *text; - g_autofree gchar *application_context_utf8 = NULL; - guint cursor_pos, anchor_pos, context_start, context_end; + guint cursor_pos, anchor_pos; g_autofree gchar *debug_context = NULL; g_message("%s: current core context : %s", __FUNCTION__, debug_context = get_context_debug(engine)); ibus_engine_get_surrounding_text(engine, &text, &cursor_pos, &anchor_pos); - context_end = anchor_pos < cursor_pos ? anchor_pos : cursor_pos; - context_start = context_end > MAXCONTEXT_ITEMS ? context_end - MAXCONTEXT_ITEMS : 0; - application_context_utf8 = g_utf8_substring(ibus_text_get_text(text), context_start, context_end); - g_message("%s: new application context: |%s| (len:%u) cursor:%d anchor:%d", __FUNCTION__, - application_context_utf8, context_end - context_start, cursor_pos, anchor_pos); - - km_core_cu *application_context_utf16 = g_utf8_to_utf16(application_context_utf8, -1, NULL, NULL, NULL); - km_core_context_status result; - result = km_core_state_context_set_if_needed(keyman->state, application_context_utf16); - g_free(application_context_utf16); - - g_message("%s: context %s", __FUNCTION__, - result == KM_CORE_CONTEXT_STATUS_UNCHANGED ? "unchanged" - : result == KM_CORE_CONTEXT_STATUS_UPDATED ? "updated" - : result == KM_CORE_CONTEXT_STATUS_CLEARED ? "cleared" - : result == KM_CORE_CONTEXT_STATUS_ERROR ? "error" - : "invalid argument"); + set_context_impl(engine, text, cursor_pos, anchor_pos); } static void @@ -461,6 +485,7 @@ ibus_keyman_engine_constructor( keyman->lctrl_pressed = FALSE; keyman->ralt_pressed = FALSE; keyman->rctrl_pressed = FALSE; + keyman->is_dirty = FALSE; initialize_queue_items(keyman, 0, MAX_QUEUE_SIZE); keyman->commit_item = &keyman->commit_queue[0]; gchar **split_name = g_strsplit(engine_name, ":", 2); @@ -557,8 +582,6 @@ ibus_keyman_engine_constructor( ping_keyman_system_service(); - set_context_if_needed(engine); - return (GObject *) keyman; } @@ -629,10 +652,29 @@ static void commit_string(IBusKeymanEngine *keyman, const gchar *string) IBusText *text; g_autofree gchar *debug = NULL; g_message("DAR: %s - %s", __FUNCTION__, debug = debug_utf8_with_codepoints(string)); - text = ibus_text_new_from_static_string (string); + text = ibus_text_new_from_string(string); g_object_ref_sink(text); ibus_engine_commit_text ((IBusEngine *)keyman, text); g_object_unref (text); + keyman->is_dirty = FALSE; +} + +/** + * Commit an empty string to flush the surrounding text buffer. + * + * Wayland uses double-buffering for surrounding text and some other + * functionality, so we have to commit to apply the changes. + * See https://wayland.app/protocols/input-method-unstable-v2 + */ +static void +apply_changes(IBusKeymanEngine* keyman) { + IBusText* text; + g_message("%s - committing", __FUNCTION__); + text = ibus_text_new_from_static_string(""); + g_object_ref_sink(text); + ibus_engine_commit_text((IBusEngine*)keyman, text); + g_object_unref(text); + keyman->is_dirty = FALSE; } // @@ -643,7 +685,7 @@ is_core_options_end(km_core_option_item *option) { } static void -process_output_action(IBusEngine *engine, const km_core_usv* output_utf32) { +process_output_action(IBusEngine* engine, const km_core_usv* output_utf32) { if (output_utf32 == NULL || output_utf32[0] == '\0') { return; } @@ -683,12 +725,13 @@ process_backspace_action(IBusEngine *engine, unsigned int code_points_to_delete) return; } + IBusKeymanEngine* keyman = (IBusKeymanEngine*)engine; if (client_supports_surrounding_text(engine)) { g_message("%s: compliant app: deleting surrounding text %d codepoints", __FUNCTION__, code_points_to_delete); ibus_engine_delete_surrounding_text(engine, -code_points_to_delete, code_points_to_delete); + keyman->is_dirty = TRUE; } else { g_message("%s: non-compliant app: queueing %d backspaces", __FUNCTION__, code_points_to_delete); - IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine; keyman->commit_item->code_points_to_delete = code_points_to_delete; } } @@ -761,6 +804,7 @@ commit_current_queue_item(IBusKeymanEngine *keyman) { ibus_engine_forward_key_event(engine, KEYMAN_BACKSPACE_KEYSYM, KEYMAN_BACKSPACE, 0); current_item->code_points_to_delete--; } + keyman->is_dirty = TRUE; // don't remove the item from the queue yet - we need to process it // again for the output and keystrokes. Instead emit the sentinel key // again. @@ -780,6 +824,7 @@ commit_current_queue_item(IBusKeymanEngine *keyman) { g_message("%s: Forwarding key from commit queue: keyval=0x%02x, keycode=0x%02x, state=0x%02x", __FUNCTION__, current_item->keyval, current_item->keycode, current_item->state); ibus_engine_forward_key_event(engine, current_item->keyval, current_item->keycode, current_item->state); + keyman->is_dirty = TRUE; } keyman->commit_item--; memmove(keyman->commit_queue, &keyman->commit_queue[1], sizeof(commit_queue_item) * (MAX_QUEUE_SIZE - 1)); @@ -790,6 +835,10 @@ static void finish_process_actions(IBusEngine *engine) { g_assert(engine != NULL); IBusKeymanEngine *keyman = (IBusKeymanEngine *)engine; + if (keyman->is_dirty) { + apply_changes(keyman); + } + if (client_supports_surrounding_text(engine)) { // compliant app return; @@ -840,6 +889,8 @@ process_actions( IBusEngine *engine, km_core_actions const *actions ) { + IBusKeymanEngine* keyman = (IBusKeymanEngine*)engine; + keyman->is_dirty = FALSE; process_backspace_action(engine, actions->code_points_to_delete); process_output_action(engine, actions->output); process_persist_action(engine, actions->persist_options); @@ -1032,8 +1083,12 @@ ibus_keyman_engine_set_surrounding_text( guint cursor_pos, guint anchor_pos ){ - parent_class->set_surrounding_text(engine, text, cursor_pos, anchor_pos); - set_context_if_needed(engine); + g_message( + "%s: text=%s (len: %u), cursor_pos=%d, anchor_pos=%d", __FUNCTION__, + ibus_text_get_text(text), ibus_text_get_length(text),cursor_pos, anchor_pos); + + parent_class->set_surrounding_text(engine, text, cursor_pos, anchor_pos); + set_context_impl(engine, text, cursor_pos, anchor_pos); } // static void ibus_keyman_engine_set_cursor_location (IBusEngine *engine, @@ -1062,7 +1117,11 @@ ibus_keyman_engine_focus_in (IBusEngine *engine) g_message("%s", __FUNCTION__); ibus_engine_register_properties (engine, keyman->prop_list); - set_context_if_needed(engine); + if (engine->enabled) { + // While the engine is not enabled ibus might not yet know if + // the client app supports surrounding text. + set_context_if_needed(engine); + } parent_class->focus_in (engine); } @@ -1123,7 +1182,8 @@ ibus_keyman_engine_enable (IBusEngine *engine) km_service_set_ldmlfile (service, keyman->ldmlfile); km_service_set_name (service, keyman->kb_name); } - parent_class->enable (engine); + engine->enabled = TRUE; + parent_class->enable(engine); } /** @@ -1148,6 +1208,8 @@ ibus_keyman_engine_disable (IBusEngine *engine) km_service_set_name (service, "None"); // g_clear_object(&service); + engine->enabled = FALSE; + parent_class->disable (engine); } diff --git a/linux/scripts/dist.sh b/linux/scripts/dist.sh index b9337c21cd..dfeaf26110 100755 --- a/linux/scripts/dist.sh +++ b/linux/scripts/dist.sh @@ -70,6 +70,7 @@ to_exclude=( linux/docs/help \ linux/keyman-config/keyman_config/version.py \ linux/keyman-config/buildtools/build-langtags.py \ + linux/upload \ ) if [[ -z "${create_origdist+x}" ]]; then diff --git a/linux/scripts/package-build.inc.sh b/linux/scripts/package-build.inc.sh index 3e37e16c0d..733659f673 100644 --- a/linux/scripts/package-build.inc.sh +++ b/linux/scripts/package-build.inc.sh @@ -43,7 +43,7 @@ function downloadSource() { cd .. mv "${proj}-${version}" "${BASEDIR}/${packageDir}" mv "${proj}_${version}.orig.tar.xz" "${BASEDIR}/${packageDir}" - mv "${proj}-${version}.tar.xz" "${BASEDIR}/${packageDir}" + mv "${proj}_${version}.pkg.tar.xz" "${BASEDIR}/${packageDir}" mv "${proj}"*.asc "${BASEDIR}/${packageDir}" rm "${proj}"*.debian.tar.xz cd "${BASEDIR}/${packageDir}" || exit diff --git a/linux/scripts/watch.in b/linux/scripts/watch.in index b4c0039a84..82a5ce6cde 100644 --- a/linux/scripts/watch.in +++ b/linux/scripts/watch.in @@ -1,3 +1,3 @@ version=4 # Tier replaced by package-build.inc.sh script -opts=pgpsigurlmangle=s/$/.asc/ https://downloads.keyman.com/linux/$tier/@ANY_VERSION@/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ debian uupdate +opts=pgpsigurlmangle=s/$/.asc/ https://downloads.keyman.com/linux/$tier/@ANY_VERSION@/@PACKAGE@@ANY_VERSION@.pkg@ARCHIVE_EXT@ debian uupdate diff --git a/resources/build/htm-link.lua b/resources/build/htm-link.lua index e85b91fb17..b2ed4fc45b 100644 --- a/resources/build/htm-link.lua +++ b/resources/build/htm-link.lua @@ -21,10 +21,8 @@ end -- Support for redirect meta key, adds a meta refresh to the resulting document function Meta(meta) - for k, v in pairs(meta) do - if k == 'redirect' and type(v) == 'table' and v.t == 'MetaInlines' then - redirect = v[1].c - end + if meta.redirect then + redirect = pandoc.utils.stringify(meta.redirect) end return meta end diff --git a/resources/teamcity/includes/tc-linux.inc.sh b/resources/teamcity/includes/tc-linux.inc.sh index cd02838682..c6a159643c 100644 --- a/resources/teamcity/includes/tc-linux.inc.sh +++ b/resources/teamcity/includes/tc-linux.inc.sh @@ -67,13 +67,25 @@ ba_linux_install_emscripten() { # shellcheck disable=SC2154 . "${KEYMAN_ROOT}/resources/build/minimum-versions.inc.sh" + local EMSDK_DIR + if [[ ! -z "${EMSCRIPTEN_BASE:-}" ]] && [[ -d "${EMSCRIPTEN_BASE}" ]]; then + # EMSCRIPTEN_BASE points to emsdk/upstream/emscripten + EMSDK_DIR="$(realpath "${EMSCRIPTEN_BASE}/../..")" + else + EMSDK_DIR="${HOME}/emsdk" + fi + builder_echo "Installing emscripten version ${KEYMAN_MIN_VERSION_EMSCRIPTEN}" export EMSDK_KEEP_DOWNLOADS=1 + if [[ -d "${EMSDK_DIR}" ]]; then + builder_echo "emsdk directory already exists at ${EMSDK_DIR}, skipping clone" + else + # shellcheck disable=SC2164 + cd "$(dirname "${EMSDK_DIR}")" + git clone https://github.com/emscripten-core/emsdk.git + fi # shellcheck disable=SC2164 - cd "${HOME}" - git clone https://github.com/emscripten-core/emsdk.git - # shellcheck disable=SC2164 - cd emsdk + cd "${EMSDK_DIR}" ./emsdk install "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" ./emsdk activate "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" fi diff --git a/resources/teamcity/linux/keyman-linux-release.sh b/resources/teamcity/linux/keyman-linux-release.sh index be29f77d97..61c5d93cf4 100755 --- a/resources/teamcity/linux/keyman-linux-release.sh +++ b/resources/teamcity/linux/keyman-linux-release.sh @@ -57,10 +57,14 @@ function _cleanup_before_creating_source_package() { function _make_release_source_tarball() { builder_echo start "make source tarball" "Make source tarball" rm -rf dist + mkdir -p "upload/${KEYMAN_VERSION}" ./scripts/reconf.sh PKG_CONFIG_PATH="${KEYMAN_ROOT}/core/build/arch/release/meson-private" ./scripts/dist.sh - mkdir -p "upload/${KEYMAN_VERSION}" - cp -a dist/*.tar.xz "upload/${KEYMAN_VERSION}" + mv dist/*.tar.xz "upload/${KEYMAN_VERSION}/" + builder_echo heading "Make source for packaging" + PKG_CONFIG_PATH="${KEYMAN_ROOT}/core/build/arch/release/meson-private" ./scripts/dist.sh origdist + mv "dist/keyman_${KEYMAN_VERSION}.orig.tar.xz" "dist/keyman_${KEYMAN_VERSION}.pkg.tar.xz" + mv dist/*.tar.xz "upload/${KEYMAN_VERSION}/" ( cd "upload/${KEYMAN_VERSION}" sha256sum ./*.tar.xz > SHA256SUMS @@ -84,10 +88,9 @@ function _sign_source_tarball() { function _publish_to_downloads() { builder_echo start "publish to downloads" "Publish to downloads.keyman.com" - local UPLOAD_DIR KEYMAN_TXZ + local UPLOAD_DIR UPLOAD_DIR="upload/${KEYMAN_VERSION}" - KEYMAN_TXZ="keyman-${KEYMAN_VERSION}.tar.xz" # Set permissions as required on download site builder_echo "Setting upload file permissions for downloads.keyman.com" @@ -96,7 +99,8 @@ function _publish_to_downloads() { chmod g+w "${UPLOAD_DIR}"/* chmod a+r "${UPLOAD_DIR}"/* - write_download_info "${UPLOAD_DIR}" "${KEYMAN_TXZ}" "Keyman for Linux" tar.xz linux + write_download_info "${UPLOAD_DIR}" "keyman-${KEYMAN_VERSION}.tar.xz" "Keyman for Linux source tarball" tar.xz linux + write_download_info "${UPLOAD_DIR}" "keyman_${KEYMAN_VERSION}.pkg.tar.xz" "Keyman for Linux source for packaging" tar.xz linux tc_rsync_upload "${UPLOAD_DIR}" "linux/${KEYMAN_TIER}" builder_echo end "publish to downloads" success "Publish to downloads.keyman.com" diff --git a/windows/src/engine/keyman32/appint/aiTIP.cpp b/windows/src/engine/keyman32/appint/aiTIP.cpp index f56bd98620..a2baada55b 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.cpp +++ b/windows/src/engine/keyman32/appint/aiTIP.cpp @@ -114,6 +114,12 @@ void ProcessToggleChange(UINT key) { // I4793 } } +void UpdateLastKeyCache(PKEYMAN64THREADDATA _td, WPARAM wParam, BYTE scan, BYTE keyTransition) { + _td->LastKey = wParam; + _td->LastScanCode = scan; + _td->LastTransition = keyTransition; +} + BOOL TIPProcessKeyInternal( PKEYMAN64THREADDATA _td, WPARAM wParam, @@ -128,23 +134,34 @@ BOOL TIPProcessKeyInternal( BOOL isUp = keyFlags & KF_UP ? TRUE : FALSE; BOOL extended = keyFlags & KF_EXTENDED ? TRUE : FALSE; BYTE scan = keyFlags & 0xFF; + BYTE keyTransition = KEYMSG_FLAG_TRANSITION(lParam); + WPARAM prevKey = _td->LastKey; + BYTE prevScanCode = _td->LastScanCode; + BYTE prevKeyTransition = _td->LastTransition; + UpdateLastKeyCache(_td, wParam, scan, keyTransition); SendDebugEntry(); SendDebugMessageFormat("VirtualKey=%s lParam=%x IsUp=%d Extended=%d Updateable=%d Preserved=%d", Debug_VirtualKey((WORD) wParam), lParam, isUp, extended, Updateable, Preserved); - if(_td->LastKey == wParam && scan == 0) { // I4642 + if (_td->LastKey == wParam && scan == 0) { // I4642 - handle issue with Logos application. scan = _td->LastScanCode; SendDebugMessageFormat("Scan code was zero so using cached scan code %x", scan); } - if(scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { // I4370 + // If this key event was generated by Keyman, then we should return FALSE without + // processing it in the core processor. If it is a CapsLock key we update our + // Globals::ShiftState. Since this could run before kmhook_getmessage processes the event, + // we also check for the scan code flag that we set when generating a key event from Keyman. + if ((prevKey == wParam && prevScanCode == SCAN_FLAG_KEYMAN_KEY_EVENT && prevKeyTransition == keyTransition) + || scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { if (wParam == VK_CAPITAL && !isUp) { // Must also record toggle state change when Keyman has generated // a Caps Lock event ProcessToggleChange((UINT)wParam); // I4793 } - SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=0xFF]"); + SendDebugMessageFormat("Virtual Key was generated by Keyman [Scan=%x wParam=%x] [prevKey=%x prevScanCode=%x prevKeyTransition=%x]", + scan, wParam, prevKey, prevScanCode, prevKeyTransition); return_SendDebugExit(FALSE); } diff --git a/windows/src/engine/keyman32/appint/aiTIP.h b/windows/src/engine/keyman32/appint/aiTIP.h index a5d84f92a8..e379b49941 100644 --- a/windows/src/engine/keyman32/appint/aiTIP.h +++ b/windows/src/engine/keyman32/appint/aiTIP.h @@ -80,10 +80,27 @@ public: /** * ProcessToggleChange - * Toggles the state of FLAGS in the Globals::ShiftState bit mask + * Sets or clears the state of FLAGS in the Globals::ShiftState bit mask + * Using the status of the key as determined by GetKeyState, + * ensuring it is consistent with the actual state of the key. * Supports VK_CAPITAL and VK_NUMLOCK * It DOES NOT generate a system event change for these flags * @param key */ void ProcessToggleChange(UINT key); + +// Forward declaration - final in globals.h +struct tagKEYMAN64THREADDATA; +typedef struct tagKEYMAN64THREADDATA *PKEYMAN64THREADDATA; + +/** + * Update the cache of the last key event received, this is set here - TIP processing, and by + * the GetMessage hook. + * @param _td Thread data to update + * @param wParam WPARAM of the key event + * @param scan Scan code of the key event + * @param keyTransition Transition state of the key event (00b = key down, 01b = repeat, 11b = keyup) + */ +void UpdateLastKeyCache(PKEYMAN64THREADDATA _td, WPARAM wParam, BYTE scan, BYTE keyTransition); + #endif diff --git a/windows/src/engine/keyman32/globals.h b/windows/src/engine/keyman32/globals.h index 3c71163446..9db8044394 100644 --- a/windows/src/engine/keyman32/globals.h +++ b/windows/src/engine/keyman32/globals.h @@ -239,6 +239,7 @@ typedef struct tagKEYMAN64THREADDATA WPARAM LastKey; // I4642 BYTE LastScanCode; // I4642 + BYTE LastTransition; /* Serialized key events */ diff --git a/windows/src/engine/keyman32/keyman64.h b/windows/src/engine/keyman32/keyman64.h index e46dad1782..eb199722c7 100644 --- a/windows/src/engine/keyman32/keyman64.h +++ b/windows/src/engine/keyman32/keyman64.h @@ -118,8 +118,15 @@ #define KEYMSG_FLAG_DLGMODE(lParam) (HIWORD(lParam) & KF_DLGMODE ? 1 : 0) #define KEYMSG_FLAG_MENUMODE(lParam) (HIWORD(lParam) & KF_MENUMODE ? 1 : 0) #define KEYMSG_FLAG_ALTDOWN(lParam) (HIWORD(lParam) & KF_ALTDOWN ? 1 : 0) +// Repeat is actually previous KF_UP value of the key. +// It is always set to 1 for WM_KEYUP and WM_SYSKEYUP messages. +// It is set to 1 for WM_KEYDOWN and WM_SYSKEYDOWN keystroke messages generated by the automatic repeat feature. +// see https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#previous-key-state-flag #define KEYMSG_FLAG_REPEAT(lParam) (HIWORD(lParam) & KF_REPEAT ? 1 : 0) #define KEYMSG_FLAG_UP(lParam) (HIWORD(lParam) & KF_UP ? 1 : 0) +// Combine the transition flags into a single value for higher chance of identification +// 00b = key down, 01b = repeat, 11b = keyup +#define KEYMSG_FLAG_TRANSITION(lParam) ((BYTE)((HIWORD(lParam) & (KF_UP | KF_REPEAT)) >> 14)) // TODO: Deprecate overloading of scancodes and use dwExtraInfo instead #define SCAN_FLAG_KEYMAN_KEY_EVENT 0xFF diff --git a/windows/src/engine/keyman32/kmhook_getmessage.cpp b/windows/src/engine/keyman32/kmhook_getmessage.cpp index 4fee835254..4c11dc31b5 100644 --- a/windows/src/engine/keyman32/kmhook_getmessage.cpp +++ b/windows/src/engine/keyman32/kmhook_getmessage.cpp @@ -156,9 +156,9 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) } BYTE scan = KEYMSG_LPARAM_SCAN(mp->lParam); + BYTE keyTransitionEvent = KEYMSG_FLAG_TRANSITION(mp->lParam); CheckScheduledRefresh(); - _td->LastScanCode = scan; - _td->LastKey = mp->wParam; + UpdateLastKeyCache(_td, mp->wParam, scan, keyTransitionEvent); switch (mp->wParam) { case VK_MENU: @@ -174,6 +174,11 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam) if (mp->wParam != VK_BACK) { if(scan == SCAN_FLAG_KEYMAN_KEY_EVENT) { mp->lParam = (mp->lParam & 0xFF00FFFFL) | (MapVirtualKey((UINT)mp->wParam, 0) << 16); + if (mp->wParam == VK_CAPITAL) { + ProcessToggleChange(VK_CAPITAL); + } + SendDebugMessageFormat("WMKEY=%x Clear `SCAN_FLAG_KEYMAN_KEY_EVENT` wParam=%x lParam=%x Set LastKey=%x LastScanCode=%x LastTransition=%x", + mp->message, mp->wParam, mp->lParam, _td->LastKey, _td->LastScanCode, _td->LastTransition); } } } diff --git a/windows/src/engine/keyman32/kmprocess.cpp b/windows/src/engine/keyman32/kmprocess.cpp index 8ba4c22a8b..6e1eff08e3 100644 --- a/windows/src/engine/keyman32/kmprocess.cpp +++ b/windows/src/engine/keyman32/kmprocess.cpp @@ -131,7 +131,7 @@ BOOL ProcessHook() _td->lpActiveKeyboard->lpCoreKeyboardState, KM_CORE_DEBUG_CONTEXT_CACHED ); - SendDebugMessageFormatW(L"Key %s: %hs Context '%s'", + SendDebugMessageFormatW(L"Key %s: %hs Core Cached Context '%s'", _td->state.isDown ? L"pressed" : L"released", Debug_VirtualKey(_td->state.vkey), debug_context); km_core_cu_dispose(debug_context); diff --git a/windows/src/engine/keyman32/kmprocessactions.cpp b/windows/src/engine/keyman32/kmprocessactions.cpp index bdc8c69e66..2694658762 100644 --- a/windows/src/engine/keyman32/kmprocessactions.cpp +++ b/windows/src/engine/keyman32/kmprocessactions.cpp @@ -77,38 +77,29 @@ processPersistOpt(km_core_actions const* actions, LPINTKEYBOARDINFO activeKeyboa } } -static void processCapsLock(const km_core_caps_state caps_lock_state, BOOL isUp, BOOL Updateable, BOOL externalEvent) { +static void processCapsLock(const km_core_caps_state caps_state_change, BOOL isUp, BOOL Updateable, BOOL externalEvent) { + BOOL isCapsOn = IsCapsLockOn(); - // We only want to process the Caps Lock key event once -- - // in the first pass (!Updateable). - if (Updateable){ + // This debug message is useful for understanding the sequence of events around caps lock changes + //SendDebugMessageFormat("ACTION CAPS STATE:%d FIsUp=%d Updateable=%d ExternalEvent=%d CapsState=%d", caps_state_change, isUp, Updateable, + // externalEvent, isCapsOn); + + // We only want to process the Caps Lock key event once; + // it has to be when updateble=1 as TSF does not consistently + // have updateable=0 events. + if (!Updateable || caps_state_change == KM_CORE_CAPS_UNCHANGED) { return; } + // Turn three state value into a boolean for whether capslock should be on or off, + // we only want to process the key event if the state is changing. + BOOL required_caps_state = (caps_state_change == KM_CORE_CAPS_ON); - if (caps_lock_state == KM_CORE_CAPS_ON) { - // This case would occur for the keyboard system store setting `store(&CapsOnOnly) '1'` - if (isUp && !IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - SendDebugMessageFormat("TURN CAPS ON: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - } - - // This case would occur for the keyboard system store setting `store(&CapsAlwaysOff) '1'` - // A trick is being played here of synthesising a release the CAPSLOCK key event - // then a depress CAPSLOCK key event - else if (!isUp && IsCapsLockOn()) { // I267 - 24/11/2006 invert GetKeyState test - SendDebugMessageFormat("TURN CAPS OFF: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - } - } else if (caps_lock_state == KM_CORE_CAPS_OFF) { - // This case would occur for the keyboard system store setting `store(&ShiftFreesCaps) '1'` - // OR selecting a keyboard with CAPs always off rule - if ((!isUp && IsCapsLockOn()) || (externalEvent && IsCapsLockOn())) { - SendDebugMessageFormat("TURN CAPS OFF: FIsUp=%d CapsState=%d", isUp, IsCapsLockOn()); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); - keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); - } + if (isCapsOn != required_caps_state) { + SendDebugMessageFormat( + "Simulate CapsLock %s: FIsUp=%d CurrentCapsState=%d ExternalEvent=%d", + required_caps_state ? "ON" : "OFF", isUp, isCapsOn, externalEvent); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, 0, 0); + keybd_event(VK_CAPITAL, SCAN_FLAG_KEYMAN_KEY_EVENT, KEYEVENTF_KEYUP, 0); } } @@ -151,7 +142,6 @@ ProcessActionsNonUpdatableParse(BOOL* emitKeystroke) { km_core_actions const* core_actions = km_core_state_get_actions(_td->lpActiveKeyboard->lpCoreKeyboardState); - processCapsLock(core_actions->new_caps_lock_state, !_td->state.isDown, _td->TIPFUpdateable, FALSE); if (core_actions->emit_keystroke) { *emitKeystroke = TRUE; SendDebugMessageFormat("EMIT_KEYSTROKE"); @@ -167,6 +157,6 @@ ProcessActionsExternalEvent() { return FALSE; } km_core_actions const* core_actions = km_core_state_get_actions(_td->lpActiveKeyboard->lpCoreKeyboardState); - processCapsLock(core_actions->new_caps_lock_state, !_td->state.isDown, FALSE, TRUE); + processCapsLock(core_actions->new_caps_lock_state, !_td->state.isDown, TRUE, TRUE); return TRUE; } diff --git a/windows/src/engine/kmtip/keys.cpp b/windows/src/engine/kmtip/keys.cpp index 50f2ade3b9..9bf67a5f99 100644 --- a/windows/src/engine/kmtip/keys.cpp +++ b/windows/src/engine/kmtip/keys.cpp @@ -75,7 +75,8 @@ BOOL CKMTipTextService::_InitKeystrokeSink() pKeystrokeMgr->Release(); - memset(fEatenBuf, 0, sizeof(fEatenBuf)); + memset(fEatenBuf, 0, sizeof(fEatenBuf)); // OnKeyDown/Up + memset(fOnTestEatenBuf, 0, sizeof(fOnTestEatenBuf)); // OnTestKeyDown/Up return_SendDebugExit(_keystrokeSinkInitialized = (hr == S_OK)); } @@ -178,17 +179,8 @@ STDAPI CKMTipTextService::OnTestKeyDown(ITfContext *pContext, WPARAM wParam, LPA { SendDebugEntry(); LogKey(0, wParam, lParam); - // If the keystroke is a Keyman-generated key, ignore it - // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - // TODO: Fix magic constants - if ((lParam & 0x00FF0000L) == 0xFF0000L && - wParam != VK_CAPITAL) { - *pfEaten = FALSE; - } - else { - *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); - } + fOnTestEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 + SendDebugMessageFormat(L"pfEaten=%s, wParam=%x, lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; } @@ -205,8 +197,8 @@ STDAPI CKMTipTextService::OnKeyDown(ITfContext *pContext, WPARAM wParam, LPARAM { SendDebugEntry(); LogKey(1, wParam, lParam); - fEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + fEatenBuf[wParam] = *pfEaten = _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 + SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; } @@ -222,17 +214,9 @@ STDAPI CKMTipTextService::OnTestKeyUp(ITfContext *pContext, WPARAM wParam, LPARA { SendDebugEntry(); LogKey(2, wParam, lParam); - // If the keystroke is a Keyman-generated key, ignore it - // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - if ((lParam & 0x00FF0000L) == 0xFF0000L && - wParam != VK_CAPITAL) { // I3566 - *pfEaten = FALSE; - } - else { - _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 - *pfEaten = fEatenBuf[wParam]; - } -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + _KeymanProcessKeystroke(pContext, wParam, lParam, FALSE, FALSE); // I3588 + *pfEaten = fOnTestEatenBuf[wParam]; + SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; } @@ -251,16 +235,11 @@ STDAPI CKMTipTextService::OnKeyUp(ITfContext *pContext, WPARAM wParam, LPARAM lP LogKey(3, wParam, lParam); // If the keystroke is a Keyman-generated key, ignore it // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - if ((lParam & 0x00FF0000L) == 0xFF0000L && - wParam != VK_CAPITAL) { // I3566 // I3605 - *pfEaten = FALSE; - } - else - { - _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 // I3605 - *pfEaten = fEatenBuf[wParam]; - } -// SendDebugMessageFormat("pfEaten=%s", *pfEaten ? "TRUE" : "FALSE"); + + _KeymanProcessKeystroke(pContext, wParam, lParam, TRUE, FALSE); // I3588 // I3605 + *pfEaten = fEatenBuf[wParam]; + + SendDebugMessageFormat(L"pfEaten=%s wParam=%x lParam=%x", *pfEaten ? L"TRUE" : L"FALSE", wParam, lParam); SendDebugExit(); return S_OK; } diff --git a/windows/src/engine/kmtip/kmkey.cpp b/windows/src/engine/kmtip/kmkey.cpp index 609c98e431..1b767f4946 100644 --- a/windows/src/engine/kmtip/kmkey.cpp +++ b/windows/src/engine/kmtip/kmkey.cpp @@ -102,15 +102,16 @@ BOOL CKMTipTextService::_KeymanProcessKeystroke(ITfContext *pContext, WPARAM wPa // Don't process Unicode characters injected or ProcessKey events which are generated by Windows if (wParam == VK_PACKET || wParam == VK_PROCESSKEY) { + SendDebugMessageFormat(L"wParam=%x early return FALSE", wParam); return FALSE; // I3608 // I4201 } SendDebugEntry(); SendDebugMessageFormat(L"%x %x %s %s ex=%x", wParam, lParam, fUpdate ? L"update" : L"", fPreserved ? L"preserved" : L"", GetMessageExtraInfo()); // I4378 - // Don't process keystrokes generated by Keyman (scan code = 0xFF) - // But we need to pass Caps Lock through, even if we generated it, so we can track Caps Lock state. - // TODO: This is done in multiple places, but we probably only need to do it once + // Don't process keystrokes generated by Keyman (scan code = 0xFF = SCAN_FLAG_KEYMAN_KEY_EVENT) + // But we need to pass Caps Lock through to the keyman engine (but not the core), + // even if we generated it, so we can track Caps Lock state. if ((lParam & 0xFF0000) == 0xFF0000 && wParam != VK_CAPITAL) { return_SendDebugExit(FALSE); diff --git a/windows/src/engine/kmtip/kmtip.h b/windows/src/engine/kmtip/kmtip.h index b4f307488b..a067617d44 100644 --- a/windows/src/engine/kmtip/kmtip.h +++ b/windows/src/engine/kmtip/kmtip.h @@ -117,6 +117,7 @@ private: BOOL _keystrokeSinkInitialized; BOOL fEatenBuf[256]; + BOOL fOnTestEatenBuf[256]; ITfThreadMgr *_pThreadMgr; TfClientId _tfClientId;