Merge pull request #15020 from keymanapp/chore/A19S14-merge-master-into-win-arm

auto: A19S14 merge master into win-arm 🦾
This commit is contained in:
Marc Durdin 2025-10-27 07:41:04 +01:00 • committed by GitHub
commit f0ec10f390
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 2127 additions and 325 deletions

View file

@ -1,5 +1,48 @@
# Keyman Version History
## 19.0.144 alpha 2025-10-23
* chore(deps-dev): bump playwright from 1.46.1 to 1.56.1 (#14984)
* chore(web): revert "chore(deps-dev): bump playwright from 1.46.1 to 1.56.1" (#15000)
## 19.0.143 alpha 2025-10-20
* fix(android): Set the main app background white (#14935)
## 19.0.142 alpha 2025-10-17
* chore(common): add .model_info schema (#14967)
* chore(web): update helpURL documentation and float ui link (#14960)
* fix(linux): use `ibus` command to restart ibus (#14971)
* chore(linux): replace deprecated `parse_version` (#14973)
## 19.0.141 alpha 2025-10-16
* refactor(linux): extract functions (#14937)
* refactor(linux): remove no-longer-needed Python 3.7 code (#14956)
* chore(linux): don't report errors to Sentry when running autopkgtests (#14938)
* fix(linux): fix installing keyboard in shared area (#14953)
* fix(linux): install schema during `keyman-config/build.sh install` (#14962)
* test(linux): add regression tests for cli tools to autopkgtests (#14950)
## 19.0.140 alpha 2025-10-15
* fix(mac): add Microsoft Word to non-compliant app list (#14940)
## 19.0.139 alpha 2025-10-13
* fix(windows): osk: handle simulated left Control event when AltGr pressed (#14909)
## 19.0.138 alpha 2025-10-11
* fix(android): Update Sample1 text field to resize (#14922)
## 19.0.137 alpha 2025-10-10
* chore(common): Update crowdin strings for Vietnamese (#14915)
* maint(resources): fix watch bug for common files, and add more logging for triggering builds (#14893)
* docs(developer): Update keyboard-editor.md to clarify when entire row will be deleted (#14919)
## 19.0.136 alpha 2025-10-09
* fix(android): Refactor bottom inset calculations for in-app and system keyboard (#14873)
@ -846,6 +889,15 @@
* refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168)
* chore: increment to alpha 19.0 (#13187)
## 18.0.243 stable 2025-10-15
* chore(linux): Update debian changelog (#14855)
* fix(android): Convert longpress delay menu to ConstraintLayout (#14906)
* fix(linux): replace `dbus-x11` dependency (#14898)
* chore(ios): renew certificate (#14917)
* fix(android): Refactor bottom inset calculations for inapp and system keyboard (#14913)
* fix(mac): add Microsoft Word to non-compliant app list (#14951)
## 18.0.242 stable 2025-09-29
* maint(linux): remove Oracular, add Questing (#14781)

View file

@ -1 +1 @@
19.0.137
19.0.145

View file

@ -619,11 +619,14 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
getTheme().resolveAttribute(android.R.attr.actionBarSize, outValue, true);
int actionBarHeight = getResources().getDimensionPixelSize(outValue.resourceId);
// *** TO DO: Try to check if status bar is visible, set statusBarHeight to 0 if it is not visible ***
// Status bar height only used for portrait orientation
int statusBarHeight = 0;
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
if (lastOrientation == Configuration.ORIENTATION_PORTRAIT) {
// *** TO DO: Try to check if status bar is visible, set statusBarHeight to 0 if it is not visible ***
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
}
int navigationBarHeight = KMManager.getNavigationBarHeight(context, KeyboardType.KEYBOARD_TYPE_INAPP);

View file

@ -8,7 +8,7 @@
android:fitsSystemWindows="true"
android:layout_above="@+id/KMKeyboard"
android:orientation="vertical"
android:background="@color/yellow_background"
android:background="@android:color/white"
tools:context=".MainActivity">
<include layout="@layout/titlebar"

View file

@ -174,15 +174,10 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
// *** TO DO: Try to check if status bar is visible, set statusBarHeight to 0 if it is not visible ***
int statusBarHeight = 0;
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0)
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
// Navigation bar height
int navigationBarHeight = 0;
resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android");
if (resourceId > 0) {
navigationBarHeight = getResources().getDimensionPixelSize(resourceId);
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
int navigationBarHeight = KMManager.getNavigationBarHeight(context, KeyboardType.KEYBOARD_TYPE_INAPP);
Point size = KMManager.getWindowSize(context);
int screenHeight = size.y;

View file

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@ -5,16 +6,16 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:layout_above="@+id/KMKeyboard"
android:orientation="vertical"
tools:context=".MainActivity">
<com.keyman.engine.KMTextView
android:id="@+id/kmTextView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_alignParentTop="true"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textMultiLine"
android:inputType="textMultiLine|textNoSuggestions"
android:gravity="top"
android:scrollbars="vertical"
app:layout_constraintTop_toTopOf="parent"

View file

@ -0,0 +1,19 @@
# model_info
* **model_info.schema.json**
Documentation at https://help.keyman.com/developer/cloud/model_info
* Primary version:
* https://github.com/keymanapp/api.keyman.com/tree/master/schemas/model_info
* Synchronized copies at:
* https://github.com/keymanapp/keyman/tree/master/common/schemas/model_info
# .model_info version history
## 2023-08-11 2.0 stable
* Removed:
`.links`
`.related[].note`
- Source .model_info files are no longer needed, so source vs distribution
model_info distinction is removed

View file

@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/schema#",
"$ref": "#/definitions/ModelInfo",
"definitions": {
"ModelInfo": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"authorName": { "type": "string" },
"authorEmail": { "type": "string", "format": "email" },
"description": { "type": "string" },
"license": { "type": "string", "enum": ["mit"] },
"languages": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
"lastModifiedDate": { "type": "string", "format": "date-time" },
"packageFilename": { "type": "string" },
"packageFileSize": { "type": "number" },
"jsFilename": { "type": "string" },
"jsFileSize": { "type": "number" },
"isRTL": { "type": "boolean" },
"packageIncludes": { "type": "array", "items": { "type": "string", "enum": ["fonts"] }, "additionalItems": false },
"version": { "type": "string" },
"minKeymanVersion": { "type": "string", "pattern": "^\\d+\\.0$" },
"helpLink": { "type": "string", "pattern": "^http(s)?://help\\.keyman(-staging)?\\.com(.localhost)?/model/" },
"sourcePath": { "type": "string", "pattern": "^(release|experimental)/.+/.+$" },
"related": { "type": "object", "patternProperties": {
".": { "$ref": "#/definitions/ModelRelatedInfo" }
},
"additionalProperties": false
}
},
"required": [
"license", "languages"
],
"additionalProperties": false
},
"ModelRelatedInfo": {
"type": "object",
"properties": {
"deprecates": { "type": "boolean" },
"deprecatedBy": { "type": "boolean" }
},
"required": [],
"additionalProperties": false
}
}
}

View file

@ -1,18 +1,18 @@
(*
Name: UserMessages
Copyright: Copyright (C) SIL International.
Documentation:
Description:
Documentation:
Description:
Create Date: 5 Jul 2012
Modified Date: 6 Nov 2015
Authors: mcdurdin
Related Files:
Dependencies:
Related Files:
Dependencies:
Bugs:
Todo:
Notes:
Bugs:
Todo:
Notes:
History: 05 Jul 2012 - mcdurdin - I3390 - Font helper can crash when WM_FONTCHANGE received [CrashID:keyman.exe_8.0.350.0_2C53A3AE_EAccessViolation]
04 Nov 2012 - mcdurdin - I3519 - V9.0 - Merge of I3390 - Font helper can crash when WM_FONTCHANGE received
28 May 2014 - mcdurdin - I4242 - Crash when OSK closed/reopened without dismissing hint window [CrashID:keyman.exe_9.0.449.0_2C405C5D_EInvalidPointer]
@ -57,9 +57,14 @@ const
WM_USER_SYNTAXCOLOURCHANGE = WM_USER + 130;
const
// UfrmOSKOnScreenKeyboard
// UfrmPrintOSK
WM_USER_PrintKeyboard = WM_USER + 140;
const
// UfrmOSKOnScreenKeyboard
// Receive notification of modifier key changes from keyman32.dll
WM_KEYMAN_OSK_MODIFIER_EVENT = WM_USER + 141;
const
// UfrmOSKFontHelper
WM_USER_FontChange = WM_USER + 150; // I3390 // I3519

View file

@ -322,7 +322,7 @@ on any key to edit it. There are a number of controls:
Red circle with an X
: This button to the top right of the key will delete the key from the
row; if it is the last key on the row, the entire row will be
row; if it is the only remaining key on the row, the entire row will be
deleted.
Green arrow with a +
@ -649,4 +649,4 @@ web page.
The Send to email function makes it easier to open the addresses on your
mobile device without having to enter them manually.
Learn more about [testing on the web and mobile devices](../guides/test/).
Learn more about [testing on the web and mobile devices](../guides/test/).

View file

@ -31,5 +31,5 @@ builder_run_action clean rm -rf ./build/
builder_run_action configure node_select_version_and_npm_ci
builder_run_action build tsc --build
builder_run_action api api-extractor run --local --verbose
builder_run_action test typescript_run_eslint_mocha_tests 70
builder_run_action test typescript_run_eslint_mocha_tests 75
builder_run_action publish ci_publish_npm

View file

@ -406,4 +406,8 @@ export class AnalyzeOskCharacterUse {
return result.join(' ');
}
/** @internal */
public unitTestEndPoints = {
loadPreviousMap: this.loadPreviousMap.bind(this)
};
}

View file

@ -1,11 +1,13 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
// import { assert } from 'chai';
import { assert } from 'chai';
import 'mocha';
import { AnalyzerMessages } from '../src/analyzer-messages.js';
import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
import { CompilerErrorNamespace } from '@keymanapp/developer-utils';
import { AnalyzeOskCharacterUse } from '../src/osk-character-use/index.js';
describe('AnalyzerMessages', function () {
@ -25,5 +27,19 @@ describe('AnalyzerMessages', function () {
return verifyCompilerMessagesObject(AnalyzerMessages, CompilerErrorNamespace.Analyzer);
});
// TODO: test each message
it('should generate Warn_PreviousMapFileCouldNotBeLoaded when previous map file is missing or invalid', function () {
const analyzer = new AnalyzeOskCharacterUse(callbacks);
const invalidFile = 'nonexistent-file.json';
assert.isNull(
analyzer.unitTestEndPoints.loadPreviousMap(invalidFile),
'Expected null when map file cannot be loaded'
);
assert.isTrue(
callbacks.hasMessage(AnalyzerMessages.WARN_PreviousMapFileCouldNotBeLoaded),
'Expected warning when previous map file could not be loaded');
});
});

View file

@ -270,7 +270,7 @@ export function WriteCompiledKeyboard(
// Write the groups out
// I853 - begin unicode missing causes crash
if (keyboard.startGroup.unicode == 0xFFFFFFFF) {
if (keyboard.startGroup.unicode == -1 || keyboard.startGroup.unicode == 0xFFFFFFFF) {
callbacks.reportMessage(KmwCompilerMessages.Error_InvalidBegin());
return null;
}

View file

@ -0,0 +1,13 @@
c
c test for ERROR_InvalidBegin in kmw-compiler ('begin Unicode' is required)
c
store(&version) '9.0'
store(&name) 'Test ANSI'
store(&targets) 'web'
begin ANSI > use(main)
group(main) using keys
+ 'a' > 'aah'

View file

@ -45,6 +45,12 @@ describe('KmwCompilerMessages', function () {
await testForMessage(this, ['kmw', 'validate_gesture.kmn'], KmwCompilerMessages.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel);
});
// ERROR_InvalidBegin (from KmnCompilerMessages, but in KeymanWeb compiler context)
it('should generate ERROR_InvalidBegin if the keyboard has no "begin unicode" statement', async function() {
await testForMessage(this, ['invalid-keyboards', 'ansi.kmn'], KmwCompilerMessages.ERROR_InvalidBegin);
});
// TODO: other messages
// WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb:

View file

@ -53,8 +53,8 @@ define("language.keyman", ["require", "exports"], function (require, exports) {
[/\[/, { token: 'tag', bracket: '@open', next: '@virtualKey' } ],
// strings
[/"([^"]|\\.)*$/, 'string.invalid' ], // non-teminated string
[/'([^']|\\.)*$/, 'string.invalid' ], // non-teminated string
[/"([^"])*$/, 'string.invalid' ], // non-teminated string
[/'([^'])*$/, 'string.invalid' ], // non-teminated string
[/"/, { token: 'string.quote', bracket: '@open', next: '@stringDouble' } ],
[/'/, { token: 'string.quote', bracket: '@open', next: '@stringSingle' } ],
],

View file

@ -4,36 +4,49 @@ Code for `km-config` is in [linux/keyman-config](../../linux/keyman-config/).
## Preparing to run
### Install Dependencies
If you are running from the repo or installing keyman-config manually rather than from a package
then you will need to:
```bash
sudo apt install python3-lxml python3-magic python3-numpy python3-qrcode python3-pil \
python3-requests python3-requests-cache python3 python3-gi gir1.2-webkit2-4.1 dconf-cli \
python3-setuptools python3-pip python3-dbus ibus libglib2.0-bin liblocale-gettext-perl
python3-setuptools python3-pip python3-dbus ibus libglib2.0-bin liblocale-gettext-perl \
python3-build python3-fonttools python3-sentry-sdk
```
Either `python3-raven` or `python3-sentry-sdk` (>= 1.4) is required as well.
On Ubuntu 22.04 and later run:
Newer Ubuntu versions (>= 24.04) don't allow to modify the environment and directly
install non-Debian-packaged Python packages. Instead you'll have to
run the commands in a virtual environment.
This requires the installation of some additional dependencies:
```bash
sudo apt install python3-sentry-sdk
sudo apt install python3-venv libglib2.0-dev libdbus-1-dev pkg-config
libcairo2-dev libgirepository-2.0-dev
```
To install it on Ubuntu 18.04 and earlier run:
Then create the virtual environment with `python3 -m venv km-venv` and activate
it with `source km-venv/bin/activate`.
```bash
sudo apt install python3-raven
```
### Prepare code
For Ubuntu versions that don't provide `python3-raven` but instead provide
`python3-sentry-sdk` in a too old version (i.e. Ubuntu 20.04):
Install `python3-sentry-sdk` from `packages.sil.org`,
or install it with pip:
Run `linux/keyman-config/build.sh build` to prepare the code.
```bash
pip3 install sentry-sdk
```
### Installation
You can install the files with `linux/keyman-config/build.sh install`.
This will install the files into `/usr/local`.
To uninstall you can run `linux/keyman-config/build.sh uninstall`.
## Manually installing
If you prefer to manually copy the files you'll still have to run the
build step above. In addition you'll have to copy the GSettings schema:
### GSettings schema
Copy and compile the GSettings schema:
@ -48,15 +61,6 @@ Running `km-config` requires a language tag mapping file
`keyman_config/standards/lang_tags_map.py`. This file gets generated during a package
build, and also when running `build.sh build`.
## Installing manually from the repo
`linux/keyman-config/build.sh build && sudo linux/keyman-config/build.sh install`
will install locally to `/usr/local`.
`pip3 help install` will give you more install options.
To uninstall you can run `sudo linux/keyman-config/build.sh uninstall`.
## Things to run from the command line
### km-config
@ -119,15 +123,17 @@ Secondary-click gives you a menu including 'Back' to go back a page.
### km-kvk2ldml
`km-kvk2ldml [-p] [-k] [-o LDMLFILE] <kvk file>` Convert a Keyman kvk on-screen keyboard file to an LDML file. Optionally print the details of the kvk file (`-p`) optionally with all keys (`-k`).
`km-kvk2ldml [-p] [-k] [-o LDMLFILE] <kvk file>` Convert a Keyman kvk on-screen
keyboard file to an LDML file. Optionally print the details of the kvk file
(`-p`) optionally with all keys (`-k`).
This command is run automatically when installing a keyboard package to create
the necessary files for the onboard onscreen keyboard.
## Building the Debian package
You will need the build dependencies as well as the runtime dependencies above
`sudo apt install dh-python python3-all debhelper help2man`
Run `make deb`. This will build the Debian package in the `make_deb` directory.
See the [packaging.md](https://github.com/keymanapp/keyman/blob/master/docs/linux/packaging.md#L26)
document.
## Internationalization

View file

@ -8,8 +8,8 @@ We use different channels to build and distribute the Linux packages:
- Launchpad repo for [stable](https://launchpad.net/~keymanapp/+archive/ubuntu/keyman),
[beta](https://launchpad.net/~keymanapp/+archive/ubuntu/keyman-beta) and
[alpha](https://launchpad.net/~keymanapp/+archive/ubuntu/keyman-alpha) versions
- [pso](http://packages.sil.org/) and [llso](http://linux.lsdev.sil.org/ubuntu/)
for stable, beta, and alpha versions
- [pso](http://packages.sil.org/) for stable versions
- [llso](http://linux.lsdev.sil.org/ubuntu/) for stable, beta, and alpha versions
- artifacts on [GitHub](https://github.com/keymanapp/keyman/actions/workflows/deb-packaging.yml)
for pull requests
@ -18,6 +18,84 @@ intended as a staging environment for testing. Packages on [pso](http://packages
are uploaded manually after the packages received some testing. End users usually have only
pso enabled.
## Building packages locally
It is possible to build packages locally. This can be done either directly on the machine,
or in a Docker container which is more similar to the CI environment.
### Building packages locally directly on the machine
To build packages locally, you'll have to have the dependencies installed.
The easiest way to achieve this is to use the `mk-build-deps` tool:
```bash
sudo apt install dh-python python3-all debhelper help2man devscripts \
equivs jq quilt
sudo mk-build-deps --install linux/debian/control
```
Then you can build the Debian package with:
```bash
KEYMAN_TIER=$(cat TIER.md)
export KEYMAN_TIER
cd linux
./scripts/deb-packaging.sh source
mkdir -p make_deb
cd make_deb
dpkg-source --extract ../../keyman_*.dsc
cd keyman-*
dch -v$(cat VERSION.md)-1 ""
debuild -us -uc
```
### Local package builds with Docker
It is possible to use the usual Debian/Ubuntu tools to create the package locally.
For someone who only occasionally deals with packaging it might be easier to use
Docker and the scripts that run on GitHub actions:
#### Prerequisites for local package builds with Docker
You'll have to create the docker image.
- clone [gha-ubuntu-packaging](https://github.com/sillsdev/gha-ubuntu-packaging)
repo
- create the image:
```bash
cd /path/to/gha-ubuntu-packaging
docker build --build-arg DIST=jammy --build-arg PLATFORM=amd64 -t sillsdev/jammy .
```
#### Building packages with Docker
- create the source package
```bash
cd $KEYMAN_ROOT
KEYMAN_TIER=$(cat TIER.md)
export KEYMAN_TIER
cd linux
./scripts/deb-packaging.sh source
```
This will create the source package in $KEYMAN_ROOT directory.
- Create the binary packages with Docker:
```bash
cd $KEYMAN_ROOT
DIST=jammy
docker run -v $(pwd):/source -i -t -w /source --platform=amd64 \
--env INPUT_DIST=$DIST --env INPUT_SOURCE_DIR=. \
--env INPUT_SOURCEPACKAGE=$(ls keyman_*.dsc | sort | tail -1) \
sillsdev/$DIST
```
This will create the binary packages in `$KEYMAN_ROOT/artifacts`.
## Package builds
Package builds happen on [Launchpad](#package-builds-on-launchpad) and
@ -73,52 +151,6 @@ and of course the source code for the packages:
to the main section `jammy`)
- if the build is successful the job archives the artifacts
### Local package builds with Docker
It is possible to use the usual Debian/Ubuntu tools to create the package locally.
For someone who only occasionally deals with packaging it might be easier to use
Docker and the scripts that run on GitHub actions:
#### Prerequisites for local package builds with Docker
You'll have to create the docker image.
- clone [gha-ubuntu-packaging](https://github.com/sillsdev/gha-ubuntu-packaging)
repo
- create the image:
```bash
cd /path/to/gha-ubuntu-packaging
docker build --build-arg DIST=jammy --build-arg PLATFORM=amd64 -t sillsdev/jammy .
```
#### Building packages with Docker
- create the source package
```bash
cd $KEYMAN_ROOT
KEYMAN_TIER=$(cat TIER.md)
export KEYMAN_TIER
cd linux
./scripts/deb-packaging.sh source
```
This will create the source package in $KEYMAN_ROOT directory.
- Create the binary packages with Docker:
```bash
cd $KEYMAN_ROOT
DIST=jammy
docker run -v $(pwd):/source -i -t -w /source --platform=amd64 \
--env INPUT_DIST=$DIST --env INPUT_SOURCE_DIR=. \
--env INPUT_SOURCEPACKAGE=$(ls keyman_*.dsc | sort | tail -1) \
sillsdev/$DIST
```
This will create the binary packages in `$KEYMAN_ROOT/artifacts`.
## Package builds on Launchpad
Package builds on Launchpad are triggered manually by running the Keyman script

View file

@ -0,0 +1,2 @@
debian/tests/data/alephwithbeth.kmp
debian/tests/data/khmer_angkor.kmp

View file

@ -24,11 +24,40 @@ lowercase ASCII letters, plus or minus signs, dots or slashes.
For example:
```bash
autopkgtest-buildvm-ubuntu-cloud -v --release=jammy
autopkgtest-buildvm-ubuntu-cloud -v --release=noble
```
- Run tests
```bash
autopkgtest -B *.deb keyman_*.dsc -- qemu autopkgtest-jammy-amd64.img
autopkgtest --apt-upgrade --no-built-binaries *.deb keyman_*.dsc -- qemu autopkgtest-noble-amd64.img
```
## Developing tests
While developing tests, it is possible to run the tests directly from
the source tree:
```bash
cd linux
autopkgtest --apt-upgrade --no-built-binaries ../artifacts/*.deb . -- qemu /path/to/autopkgtest-noble-amd64.img
```
This requires packages to be available in `${KEYMAN_ROOT}/artifacts`.
These can be built from source with:
```bash
cd linux
rm -rf debian/patches/ keyman-config/keyman_config/.pc
scripts/deb-packaging.sh source
DIST=noble
cd ..
docker run -v $(pwd):/source -i -t -w /source --platform=amd64 \
--env INPUT_DIST=$DIST --env INPUT_SOURCE_DIR=. \
--env INPUT_SOURCEPACKAGE=$(ls keyman_*.dsc | sort | tail -1) \
sillsdev/$DIST
```
New packages have to be built for changes outside of `linux/debian`.
Changes in files in `linux/debian` are automatically considered when
running `autopkgtest`.

View file

@ -1,4 +1,4 @@
Tests: test-build
Tests: test-build test-gnome-cli test-ibus-cli
Depends: @,
build-essential,
pkg-config,

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,100 @@
# shellcheck shell=bash
verify_ibus_restarted() {
local ibus_count new_pid
ibus_count=$(pgrep --count ibus-daemon)
if ! (( ibus_count == 1 )); then
echo "FAIL ${TEST_SCRIPT}/${FUNCNAME[1]}: ibus-daemon not running"
exit 1
fi
new_pid=$(pgrep ibus-daemon)
if [[ "${new_pid}" == "${ibus_pid}" ]]; then
echo "FAIL ${TEST_SCRIPT}/${FUNCNAME[1]}: ibus-daemon was not restarted"
exit 1
fi
ibus_pid="${new_pid}"
}
verify_gnome_input_sources() {
local expected_sources="$1"
local sources
sources=$(gsettings get org.gnome.desktop.input-sources sources)
if [[ "${sources}" != "${expected_sources}" ]]; then
echo "FAIL ${TEST_SCRIPT}/${FUNCNAME[1]}: input-sources wrong. Got ${sources} instead of ${expected_sources}"
exit 1
fi
}
verify_ibus_preload_engines() {
local expected="$1"
local sources
sources=$(gsettings get org.freedesktop.ibus.general preload-engines)
if [[ "${sources}" != "${expected}" ]]; then
echo "FAIL ${TEST_SCRIPT}/${FUNCNAME[1]}: preload-engines wrong. Got ${sources} instead of ${expected}"
exit 1
fi
}
verify_custom_keyboards() {
local expected_custom_keyboards="$1"
local custom_keyboards
custom_keyboards=$(gsettings get com.keyman.engine additional-keyboards)
if [[ "${custom_keyboards}" != "${expected_custom_keyboards}" ]]; then
echo "FAIL ${TEST_SCRIPT}/${FUNCNAME[1]}: custom keyboards wrong. Got ${custom_keyboards} instead of ${expected_custom_keyboards}"
exit 1
fi
}
verify_keyboard_file_exists() {
local expected_keyboard_file="$1"
if [[ ! -f "${expected_keyboard_file}" ]]; then
echo "FAIL ${TEST_SCRIPT}/${FUNCNAME[1]}: keyboard file ${expected_keyboard_file} not found"
exit 1
fi
}
verify_keyboard_file_not_exists() {
local expected_keyboard_file="$1"
if [[ -f "${expected_keyboard_file}" ]]; then
echo "FAIL ${FUNCNAME[1]}: keyboard file ${expected_keyboard_file} still exists"
exit 1
fi
}
verify_no_error() {
local errocode="$1"
if [[ "${errocode}" != "0" ]]; then
echo "FAIL ${TEST_SCRIPT}/${FUNCNAME[1]}: non-zero exit code ${errocode}."
exit 1
fi
}
run_tests() {
export SLEEP_TIME=5
export TEST_SCRIPT
TEST_SCRIPT="$(basename "${BASH_SOURCE[${#BASH_SOURCE[@]}-1]}")"
gsettings reset com.keyman.engine additional-keyboards
echo "Keyman Version: $(km-config --version)"
if ! pgrep --uid "$(id -u)" ibus-daemon > /dev/null; then
echo "Starting ibus"
ibus start -d
sleep "${SLEEP_TIME}"
else
echo "ibus-daemon already running"
fi
km_package_install__user
km_package_install__shared
km_package_list_installed
km_package_uninstall__user
km_package_uninstall__shared
}

104
linux/debian/tests/test-gnome-cli Executable file
View file

@ -0,0 +1,104 @@
#!/bin/bash
# shellcheck disable=SC1091
. "$(dirname "$0")/test-cli.inc.sh"
#-------------------------------------------------------------------------------
# gnome-shell tests
#-------------------------------------------------------------------------------
km_package_install__user() {
# Setup
gsettings set org.gnome.desktop.input-sources sources "[('xkb', 'us')]"
# Run
km-package-install --file "$(dirname "$0")/data/khmer_angkor.kmp"
# Verify
verify_no_error $?
verify_ibus_restarted
verify_keyboard_file_exists "${HOME}/.local/share/keyman/khmer_angkor/khmer_angkor.kmx"
verify_gnome_input_sources "[('xkb', 'us'), ('ibus', 'km:${HOME}/.local/share/keyman/khmer_angkor/khmer_angkor.kmx')]"
verify_custom_keyboards "@as []"
echo "PASS ${TEST_SCRIPT}/${FUNCNAME[0]}"
}
km_package_install__shared() {
# Setup - continue from previous tests
# Run
sudo km-package-install --shared --bcp47 ady --file "$(dirname "$0")/data/alephwithbeth.kmp"
# Verify
verify_no_error $?
verify_ibus_restarted
verify_keyboard_file_exists "/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx"
verify_gnome_input_sources "[('xkb', 'us'), ('ibus', 'km:${HOME}/.local/share/keyman/khmer_angkor/khmer_angkor.kmx'), ('ibus', 'ady:/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx')]"
verify_custom_keyboards "['ady:/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx']"
echo "PASS ${TEST_SCRIPT}/${FUNCNAME[0]}"
}
km_package_list_installed() {
# Setup - continue from previous tests
local result expected
# Run
result=$(km-package-list-installed)
# Verify
verify_no_error $?
expected="--- Installed user Keyman keyboard packages ---
Khmer Angkor, version: 1.5, id: khmer_angkor
--- Installed shared Keyman keyboard packages ---
Aleph With Beth, version: 1.0.6, id: alephwithbeth
--- Installed OS Keyman keyboard packages ---"
if [[ "${result}" != "${expected}" ]]; then
echo "FAIL ${TEST_SCRIPT}/${FUNCNAME[0]}: result wrong."
echo "EXPECTED:
${expected}"
echo "ACTUAL:
${result}"
exit 1
fi
echo "PASS ${FUNCNAME[0]}"
}
km_package_uninstall__user() {
# Setup - continue from previous tests
# Run
km-package-uninstall khmer_angkor
# Verify
verify_no_error $?
verify_keyboard_file_not_exists "${HOME}/.local/share/keyman/khmer_angkor/khmer_angkor.kmx"
verify_gnome_input_sources "[('xkb', 'us'), ('ibus', 'ady:/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx')]"
verify_custom_keyboards "['ady:/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx']"
echo "PASS ${TEST_SCRIPT}/${FUNCNAME[0]}"
}
km_package_uninstall__shared() {
# Setup - continue from previous tests
# Run
sudo km-package-uninstall --shared alephwithbeth
# Verify
verify_no_error $?
verify_keyboard_file_not_exists "/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx"
verify_gnome_input_sources "[('xkb', 'us')]"
verify_custom_keyboards "@as []"
echo "PASS ${TEST_SCRIPT}/${FUNCNAME[0]}"
}
#-------------------------------------------------------------------------------
# Simulate running gnome-shell
export XDG_CURRENT_DESKTOP=gnome
run_tests

View file

@ -0,0 +1,97 @@
#!/bin/bash
# shellcheck disable=SC1091
. "$(dirname "$0")/test-cli.inc.sh"
km_package_install__user() {
# Setup
gsettings set org.freedesktop.ibus.general preload-engines "['xkb:us::eng']"
# Run
km-package-install --file "$(dirname "$0")/data/khmer_angkor.kmp"
# Verify
verify_no_error $?
verify_ibus_restarted
verify_keyboard_file_exists "${HOME}/.local/share/keyman/khmer_angkor/khmer_angkor.kmx"
verify_ibus_preload_engines "['xkb:us::eng', 'km:${HOME}/.local/share/keyman/khmer_angkor/khmer_angkor.kmx']"
verify_custom_keyboards "@as []"
echo "PASS ${TEST_SCRIPT}/${FUNCNAME[0]}"
}
km_package_install__shared() {
# Setup - continue from previous tests
# Run
sudo km-package-install --shared --bcp47 ady --file "$(dirname "$0")/data/alephwithbeth.kmp"
# Verify
verify_no_error $?
verify_ibus_restarted
verify_keyboard_file_exists "/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx"
verify_ibus_preload_engines "['xkb:us::eng', 'km:${HOME}/.local/share/keyman/khmer_angkor/khmer_angkor.kmx', 'ady:/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx']"
verify_custom_keyboards "['ady:/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx']"
echo "PASS ${TEST_SCRIPT}/${FUNCNAME[0]}"
}
km_package_list_installed() {
# Setup - continue from previous tests
local result expected
# Run
result=$(km-package-list-installed)
# Verify
verify_no_error $?
expected="--- Installed user Keyman keyboard packages ---
Khmer Angkor, version: 1.5, id: khmer_angkor
--- Installed shared Keyman keyboard packages ---
Aleph With Beth, version: 1.0.6, id: alephwithbeth
--- Installed OS Keyman keyboard packages ---"
if [[ "${result}" != "${expected}" ]]; then
echo "FAIL ${TEST_SCRIPT}/${FUNCNAME[0]}: result wrong."
echo "EXPECTED:
${expected}"
echo "ACTUAL:
${result}"
exit 1
fi
echo "PASS ${TEST_SCRIPT}/${FUNCNAME[0]}"
}
km_package_uninstall__user() {
# Setup - continue from previous tests
# Run
km-package-uninstall khmer_angkor
# Verify
verify_no_error $?
verify_keyboard_file_not_exists "${HOME}/.local/share/keyman/khmer_angkor/khmer_angkor.kmx"
verify_ibus_preload_engines "['xkb:us::eng', 'ady:/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx']"
verify_custom_keyboards "['ady:/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx']"
echo "PASS ${TEST_SCRIPT}/${FUNCNAME[0]}"
}
km_package_uninstall__shared() {
# Setup - continue from previous tests
# Run
sudo km-package-uninstall --shared alephwithbeth
# Verify
verify_no_error $?
verify_keyboard_file_not_exists "/usr/local/share/keyman/alephwithbeth/alephwithbeth.kmx"
verify_ibus_preload_engines "['xkb:us::eng']"
verify_custom_keyboards "@as []"
echo "PASS ${TEST_SCRIPT}/${FUNCNAME[0]}"
}
#-------------------------------------------------------------------------------
run_tests

View file

@ -1,47 +1,13 @@
#!/usr/bin/make
default:
./build.sh clean build
install:
./build.sh install
uninstall: # run as sudo
./build.sh uninstall
clean:
./build.sh clean
check:
./build.sh test
dist: clean version
# TODO: remove this if when we no longer support Ubuntu 22.04 Jammy
if dpkg --compare-versions "$(lsb_release -r -s)" lt 24.04; then \
python3 setup.py sdist ; \
else \
python3 -m build --outdir build --sdist ; \
fi
deb: dist
@KEYMAN_VERSION=$(shell echo `basename dist/*.gz .tar.gz|cut -d "-" -f2` > /tmp/keyman_version)
@echo KEYMAN_VERSION is $(shell cat /tmp/keyman_version)
@mkdir -p make_deb
cd make_deb && tar xf ../dist/keyman_config-$(shell cat /tmp/keyman_version).tar.gz && \
mv keyman_config-$(shell cat /tmp/keyman_version) keyman-config-$(shell cat /tmp/keyman_version) && \
tar cfz keyman-config_$(shell cat /tmp/keyman_version).orig.tar.gz keyman-config-$(shell cat /tmp/keyman_version) && \
cd keyman-config-$(shell cat /tmp/keyman_version) && cp -a ../../../debian . && dch -v$(shell cat /tmp/keyman_version)-1 ""
cd make_deb/keyman-config-$(shell cat /tmp/keyman_version) && debuild -us -uc
rm /tmp/keyman_version
man:
./build-help.sh --man --no-reconf
echo "Use ./build.sh command instead of make!"
version_reconf:
cd .. && ./scripts/reconf.sh
version: version_reconf
$(eval KEYMAN_VERSION := $(shell python3 -c "from keyman_config import __releaseversion__; print(__releaseversion__)"))
$(eval KEYMAN_VERSION := $(shell ./build.sh build && python3 -c "from keyman_config import __releaseversion__; print(__releaseversion__)"))
# i18n

View file

@ -39,18 +39,25 @@ clean_action() {
fi
}
install_schema() {
local SCHEMA_DIR="$1"
mkdir -p "${SCHEMA_DIR}"
cp resources/com.keyman.gschema.xml "${SCHEMA_DIR}"/
glib-compile-schemas "${SCHEMA_DIR}"
}
execute_with_temp_schema() {
local TEMP_DATA_DIR SCHEMA_DIR
TEMP_DATA_DIR=$(mktemp -d)
SCHEMA_DIR="${TEMP_DATA_DIR}/glib-2.0/schemas"
export XDG_DATA_DIRS="${TEMP_DATA_DIR}":${XDG_DATA_DIRS-}
export GSETTINGS_SCHEMA_DIR="${SCHEMA_DIR}:/usr/share/glib-2.0/schemas/:${GSETTINGS_SCHEMA_DIR-}"
mkdir -p "${SCHEMA_DIR}"
cp resources/com.keyman.gschema.xml "${SCHEMA_DIR}"/
glib-compile-schemas "${SCHEMA_DIR}"
install_schema "${SCHEMA_DIR}"
"$@"
export XDG_DATA_DIRS=${XDG_DATA_DIRS#*:}
unset GSETTINGS_SCHEMA_DIR
export XDG_DATA_DIRS=${XDG_DATA_DIRS#*:}
rm -rf "${TEMP_DATA_DIR}"
}
@ -105,25 +112,42 @@ test_action() {
if builder_has_option --report; then
builder_echo "Creating coverage report"
python3 -m coverage html --directory="$THIS_SCRIPT_PATH/build/coveragereport/" --data-file=build/.coverage
python3 -m coverage html --directory="${THIS_SCRIPT_PATH}/build/coveragereport/" --data-file=build/.coverage
fi
}
install_action() {
if [ -n "${SUDO_USER:-}" ]; then
# with sudo install into /usr/local
pip3 install qrcode sentry-sdk
if [[ -v VIRTUAL_ENV ]]; then
# Running in a virtual environment
pip3 install qrcode sentry-sdk fonttools setuptools
pip3 install .
# install icons
mkdir -p "${VIRTUAL_ENV}/share/keyman/icons"
cp keyman_config/icons/* "${VIRTUAL_ENV}/share/keyman/icons"
# install man pages
mkdir -p "${VIRTUAL_ENV}/share/man/man1"
cp ../../debian/man/*.1 "${VIRTUAL_ENV}/share/man/man1"
# install schema
install_schema "${VIRTUAL_ENV}/share/glib-2.0/schemas"
elif [[ -n "${SUDO_USER:-}" ]] || [[ "$(id -u)" == "0" ]]; then
# as root install into /usr/local
pip3 install qrcode sentry-sdk
pip3 install --prefix /usr/local .
# install icons
mkdir -p /usr/local/share/keyman/icons
cp keyman_config/icons/* /usr/local/share/keyman/icons
# install man pages
mkdir -p /usr/local/share/man/man1
cp ../../debian/man/*.1 /usr/local/share/man/man1
# install schema
install_schema "/usr/local/share/glib-2.0/schemas"
else
# without sudo install into /tmp/keyman (or $DESTDIR)
mkdir -p "/tmp/keyman/$(python3 -c 'import sys;import os;pythonver="python%d.%d" % (sys.version_info[0], sys.version_info[1]);sitedir = os.path.join("lib", pythonver, "site-packages");print(sitedir)')"
python_version=$(python3 -c 'import sys;import os;pythonver="python%d.%d" % (sys.version_info[0], sys.version_info[1]);sitedir = os.path.join("lib", pythonver, "site-packages");print(sitedir)')
mkdir -p "/tmp/keyman/${python_version}"
pip3 install --prefix /tmp/keyman .
# install schema
install_schema "/tmp/keyman/local/share/glib-2.0/schemas"
fi
}
@ -132,6 +156,7 @@ uninstall_action() {
clean_action
rm -rf /usr/local/share/keyman/icons
rm -f /usr/local/share/man/man1/km-*.1
rm -f /usr/local/share/glib-2.0/schemas/com.keyman.gschema.xml
pip3 uninstall keyman_config
rm -f /usr/local/bin/km-config
rm -f /usr/local/bin/km-kvk2ldml

View file

@ -111,6 +111,14 @@ def _set_dbus_started_for_session(value):
def verify_dbus_running():
if os.environ.get('SUDO_USER'):
# If running with sudo we won't try to start dbus since it will
# not always work. The main reason we need dbus is so that we can
# notify other km-config processes that the keyboard list has changed.
# If running with sudo we call dbus-send instead of instantiating the
# KeymanConfigServiceManager class.
return
if 'DBUS_SESSION_BUS_ADDRESS' in os.environ:
# already running - nothing to do
return

View file

@ -2,6 +2,7 @@
import contextlib
import logging
import os
import subprocess
import dbus
import dbus.bus
@ -12,6 +13,9 @@ from dbus import DBusException
BUS_NAME = 'com.Keyman.Config'
OBJECT_PATH = '/com/Keyman/Config'
DBusSessionBusAddressValue = f'unix:path=/run/user/{os.environ.get("SUDO_UID")}/bus'
DBusSessionBusAddress = f'DBUS_SESSION_BUS_ADDRESS={DBusSessionBusAddressValue}'
XdgRuntimeDir = f'XDG_RUNTIME_DIR=/run/user/{os.environ.get("SUDO_UID")}'
__keyman_config_service = None
@ -36,6 +40,16 @@ class KeymanConfigService(dbus.service.Object):
self.KeyboardListChangedSignal()
def keyboard_list_changed():
if os.environ.get('SUDO_USER'):
args = ['sudo', '-u', os.environ.get('SUDO_USER'), 'dbus-send',
f'--bus={DBusSessionBusAddressValue}',
'/com/Keyman/Config', 'com.Keyman.Config.keyboard_list_changed']
subprocess.run(args)
else:
get_keyman_config_service().keyboard_list_changed()
class KeymanConfigServiceManager:
def __init__(self, handler):
self.__name_owner_watch = None

View file

@ -22,7 +22,12 @@ class InstallLocation(GObject.GEnum):
User = 3
Unknown = 99
can_use_cache = True
# When running under sudo, we don't want to use the cache
# since that stores files with root permissions so that they
# can't be deleted/replaced by the user later.
can_use_cache = not os.environ.get('SUDO_USER')
def get_install_area_string(area):
if area == InstallLocation.OS:

View file

@ -8,6 +8,8 @@ import sys
from gi.repository import Gio # needs to come before gi.overrides.GLib!
from gi.overrides.GLib import Variant
from keyman_config.dbus_util import DBusSessionBusAddress, XdgRuntimeDir
# The xdg module got renamed to xdg_base_dirs, but the package
# (python3-xdg) on Ubuntu still ships xdg. So we support both.
try:
@ -71,29 +73,19 @@ class GSettings():
def get(self, key):
if self.is_sudo:
args = ['sudo', '-H', '-u', os.environ.get('SUDO_USER'),
DBusSessionBusAddress, XdgRuntimeDir,
'gsettings', 'get', self.schema_id, key]
if sys.version_info.major <= 3 and sys.version_info.minor < 7:
# capture_output got added in Python 3.7
try:
output = subprocess.check_output(args)
value = eval(output)
except subprocess.CalledProcessError:
value = None
logging.warning('Could not convert to sources')
else:
process = subprocess.run(args, capture_output=True)
if process.returncode == 0:
output = process.stdout
if output.decode('utf-8').startswith('['):
value = eval(output)
return value
process = subprocess.run(args, capture_output=True)
if process.returncode == 0:
output = process.stdout
if output.decode('utf-8').startswith('['):
return eval(output)
logging.debug('Could not convert to sources')
return None
logging.debug('Could not convert to sources')
return None
else:
variant = self.schema.get_value(key)
value = self._convert_variant_to_array(variant)
return value
return self._convert_variant_to_array(variant)
def _get_schema_path(self):
return self.schema_id.replace('.', '/')
@ -121,9 +113,10 @@ class GSettings():
def set(self, key, value, type_string) -> None:
if self.is_sudo:
variant = str(value)
subprocess.run(
['sudo', '-H', '-u', os.environ.get('SUDO_USER'),
'gsettings', 'set', self.schema_id, key, variant], check=False)
args = ['sudo', '-H', '-u', os.environ.get('SUDO_USER'),
DBusSessionBusAddress, XdgRuntimeDir,
'gsettings', 'set', self.schema_id, key, variant]
subprocess.run(args, check=False)
elif get_dbus_started_for_session():
self._set_key_file(key, value, type_string)
else:

View file

@ -10,9 +10,11 @@ import time
import gi
from keyman_config.dbus_util import DBusSessionBusAddress, XdgRuntimeDir
gi.require_version('IBus', '1.0')
from gi.repository import IBus
from pkg_resources import parse_version
from packaging import version
from keyman_config.gnome_keyboards_util import is_gnome_desktop
from keyman_config.gsettings import GSettings
@ -114,10 +116,8 @@ def verify_ibus_daemon(start):
return IbusDaemon.ERROR_USER
try:
ps_output = subprocess.run(('ps', '--user', user, '-o', 's=', '-o', 'cmd'),
check=False, stdout=subprocess.PIPE).stdout
ibus_daemons = re.findall(r'^[^ZT] (/[^ ]+/|)ibus-daemon( .*|$)',
ps_output.decode('utf-8'), re.MULTILINE)
ibus_daemons = subprocess.run(('pgrep', '-u', user, 'ibus-daemon'),
check=False, stdout=subprocess.PIPE).stdout.split()
if len(ibus_daemons) <= 0:
if start:
_start_ibus_daemon(realuser)
@ -156,7 +156,7 @@ def _get_ibus_version():
def _start_ibus_daemon(realuser):
try:
if parse_version(_get_ibus_version()) >= parse_version('1.5.28'):
if version.parse(_get_ibus_version()) >= version.parse('1.5.28'):
# IBus ~1.5.28 added the `start` command, so we use that if possible
# and let IBus deal with the necessary parameters
args = ['ibus', 'start', '-d']
@ -171,36 +171,28 @@ def _start_ibus_daemon(realuser):
if realuser:
# we have been called with `sudo`. Start ibus-daemon for the real user.
logging.info('starting ibus-daemon for user %s', realuser)
subprocess.run(['sudo', '-u', realuser].extend(args), check=True)
args = ['sudo', '-u', realuser,
DBusSessionBusAddress, XdgRuntimeDir, *args]
logging.info(f'starting ibus-daemon for user {realuser} with args: {args}')
else:
logging.info('ibus-daemon not running. Starting it...')
subprocess.run(args, check=True)
except Exception:
logging.warning('Failed to start ibus-daemon')
logging.info(f'ibus-daemon not running. Starting it with args: {args}...')
subprocess.run(args, check=True)
logging.info('ibus-daemon started successfully')
except Exception as e:
logging.warning(f'Failed to start ibus-daemon: {e}')
def restart_ibus(bus=None):
verify_ibus_daemon(False)
if realuser := os.environ.get('SUDO_USER'):
# we have been called with `sudo`. Restart ibus for the real user.
logging.info('restarting IBus by subprocess for user %s', realuser)
subprocess.run(['sudo', '-u', realuser, 'ibus', 'restart'], check=False)
logging.info(f'restarting IBus by subprocess for user {realuser}')
subprocess.run(['sudo', '-u', realuser,
DBusSessionBusAddress, XdgRuntimeDir,
'ibus', 'restart'], check=False)
else:
logging.info('restarting IBus through API')
try:
if not bus:
bus = get_ibus_bus()
if bus:
logging.info("restarting IBus")
# we no longer try to restart since we sometimes ended up with more than one
# ibus-daemon process (#6237). Instead we only exit ibus here, and start
# ibus again below.
bus.exit(False)
bus.destroy()
except Exception as e:
logging.warning("Failed to restart IBus")
logging.warning(e)
logging.info('restarting IBus by subprocess')
subprocess.run(['ibus', 'restart'], check=False)
# give ibus a chance to shutdown (#6237)
time.sleep(1) # 1s
verify_ibus_daemon(True)

View file

@ -12,7 +12,7 @@ from keyman_config import _, __version__, secure_lookup
from keyman_config.canonical_language_code_utils import CanonicalLanguageCodeUtils
from keyman_config.convertico import checkandsaveico, extractico
from keyman_config.custom_keyboards import CustomKeyboards
from keyman_config.dbus_util import get_keyman_config_service
from keyman_config.dbus_util import keyboard_list_changed
from keyman_config.fcitx_util import is_fcitx_running
from keyman_config.get_kmp import (InstallLocation, get_keyboard_data,
get_keyboard_dir, get_keyman_doc_dir,
@ -351,5 +351,5 @@ def install_kmp(inputfile, sharedarea=False, language=None):
else:
return_value = InstallKmp().install_kmp_user(inputfile, language)
get_keyman_config_service().keyboard_list_changed()
keyboard_list_changed()
return return_value

View file

@ -40,7 +40,8 @@ class SentryErrorHandling:
def initialize_sentry(self):
(enabled, reason) = self.is_sentry_enabled()
if not enabled:
print(reason, file=sys.stderr)
if not self._is_running_tests():
print(reason, file=sys.stderr)
logging.info(reason)
return (enabled, reason)
else:
@ -58,8 +59,8 @@ class SentryErrorHandling:
return (True, '')
def is_sentry_enabled(self):
if self._is_unit_test():
return (False, 'Running unit tests, not reporting to Sentry')
if self._is_running_tests():
return (False, 'Running tests, not reporting to Sentry')
elif self._get_environ_nosentry():
return (False, 'Not reporting to Sentry because KEYMAN_NOSENTRY environment variable set')
elif not __uploadsentry__:
@ -94,12 +95,16 @@ class SentryErrorHandling:
keyman_nosentry = os.environ.get('KEYMAN_NOSENTRY')
return keyman_nosentry and (int(keyman_nosentry) == 1)
def _is_unit_test(self): # sourcery skip: use-any, use-next
def _is_running_tests(self): # sourcery skip: use-any, use-next
# The suggested refactorings (using any() or next()) don't work
# when testing on Ubuntu 20.04
for line in traceback.format_stack():
if '/unittest/' in line:
return True
if platform.node() == 'autopkgtest':
return True
return False
def _handle_enabled(self, enabled):

View file

@ -7,7 +7,7 @@ from shutil import rmtree
from keyman_config import _
from keyman_config.custom_keyboards import CustomKeyboards
from keyman_config.dbus_util import get_keyman_config_service
from keyman_config.dbus_util import keyboard_list_changed
from keyman_config.fcitx_util import is_fcitx_running
from keyman_config.get_kmp import (InstallLocation, get_keyboard_dir,
get_keyman_doc_dir, get_keyman_font_dir)
@ -152,5 +152,5 @@ def uninstall_kmp(packageID, sharedarea=False, removeLanguages=True):
else:
msg = _uninstall_kmp_common(InstallLocation.User, packageID, removeLanguages)
get_keyman_config_service().keyboard_list_changed()
keyboard_list_changed()
return msg

View file

@ -109,6 +109,78 @@ def list_languages_for_keyboard(packageId, packageDir):
print(result)
def try_install_kmp(inputfile, arg, language=None, sharedarea=False):
try:
install_kmp(inputfile, sharedarea, language)
except InstallError as e:
if e.status == InstallStatus.Abort:
logging.error("km-package-install: error: Failed to install %s", arg)
logging.error(e.message)
sys.exit(3)
else:
logging.warning(e.message)
def install_package(args):
if args.shared:
if get_kmp_version_user(args.package):
logging.warning(
f'km-package-install: Warning: Keyboard {args.package} is also installed for the current user.'
)
installed_kmp_ver = get_kmp_version_shared(args.package)
else:
if get_kmp_version_shared(args.package):
logging.warning(
f'km-package-install: Warning: Keyboard {args.package} is already installed in the shared area.'
)
installed_kmp_ver = get_kmp_version_user(args.package)
pkgdata = get_keyboard_data(args.package)
if not pkgdata:
logging.error("km-package-install: error: Could not download keyboard data for %s", args.package)
sys.exit(3)
pkg_version = secure_lookup(pkgdata, 'version')
if installed_kmp_ver and pkg_version:
if packaging.version.parse(installed_kmp_ver) > packaging.version.parse(pkg_version):
if args.force:
logging.warning(f'km-package-install: warning: The {args.package} keyboard ' +
f'is already installed with a newer version {installed_kmp_ver}. ' +
f'Downgrading to version {pkg_version}.')
else:
logging.error(f'km-package-install: error: The {args.package} keyboard is ' +
f'already installed with a newer version {installed_kmp_ver}. ' +
f'Not installing version {pkg_version}. Use `--force` to downgrade.')
sys.exit(1)
if args.force:
uninstall_kmp(args.package, args.shared, False)
if kmpfile := get_kmp(args.package):
try_install_kmp(kmpfile, f"keyboard package {args.package}", args.bcp47, args.shared)
else:
logging.error("km-package-install: error: Could not download keyboard package %s", args.package)
sys.exit(2)
def install_file(parser, args):
name, ext = os.path.splitext(args.file)
if ext != ".kmp" or not is_zipfile(args.file):
print_usage_and_exit(
parser,
f"km-package-install: Input file ${args.file} is not a kmp file."
)
if not os.path.isfile(args.file):
print_usage_and_exit(
parser,
f"km-package-install: Keyman kmp file {args.file} not found."
)
try_install_kmp(args.file, f"file {args.file}", args.bcp47, args.shared)
def print_usage_and_exit(parser, msg):
parser.print_usage()
logging.error(msg)
sys.exit(2)
def main():
parser = argparse.ArgumentParser(
description='Install a Keyman keyboard package, either a local .kmp file or specify a ' +
@ -134,80 +206,24 @@ def main():
sys.exit(4)
if args.package and args.file:
parser.print_usage()
logging.error(
"km-package-install: error: too many arguments: either install a local kmp file or " +
"specify a keyboard id to download and install.")
sys.exit(2)
print_usage_and_exit(
parser,
"km-package-install: error: too many arguments: either install a local kmp file or " +
"specify a keyboard id to download and install.",
)
if os.path.exists(os.path.join(keyman_cache_dir(), 'kmpdirlist')):
os.remove(os.path.join(keyman_cache_dir(), 'kmpdirlist'))
def try_install_kmp(inputfile, arg, language=None, sharedarea=False):
try:
install_kmp(inputfile, sharedarea, language)
except InstallError as e:
if e.status == InstallStatus.Abort:
logging.error("km-package-install: error: Failed to install %s", arg)
logging.error(e.message)
sys.exit(3)
else:
logging.warning(e.message)
if args.file:
name, ext = os.path.splitext(args.file)
if ext != ".kmp" or not is_zipfile(args.file):
logging.error("km-package-install: Input file %s is not a kmp file.", args.file)
logging.error("km-package-install -f <kmpfile>")
sys.exit(2)
if not os.path.isfile(args.file):
logging.error("km-package-install: Keyman kmp file %s not found.", args.file)
logging.error("km-package-install -f <kmpfile>")
sys.exit(2)
try_install_kmp(args.file, f"file {args.file}", args.bcp47, args.shared)
install_file(parser, args)
elif args.package:
if args.shared:
if get_kmp_version_user(args.package):
logging.warning(f'km-package-install: Warning: Keyboard {args.package} is ' +
'also installed for the current user.')
installed_kmp_ver = get_kmp_version_shared(args.package)
else:
if get_kmp_version_shared(args.package):
logging.warning(f'km-package-install: Warning: Keyboard {args.package} is ' +
'already installed in the shared area.')
installed_kmp_ver = get_kmp_version_user(args.package)
pkgdata = get_keyboard_data(args.package)
if not pkgdata:
logging.error("km-package-install: error: Could not download keyboard data for %s", args.package)
sys.exit(3)
pkg_version = secure_lookup(pkgdata, 'version')
if installed_kmp_ver and pkg_version:
if packaging.version.parse(installed_kmp_ver) > packaging.version.parse(pkg_version):
if args.force:
logging.warning(f'km-package-install: warning: The {args.package} keyboard ' +
f'is already installed with a newer version {installed_kmp_ver}. ' +
f'Downgrading to version {pkg_version}.')
else:
logging.error(f'km-package-install: error: The {args.package} keyboard is ' +
f'already installed with a newer version {installed_kmp_ver}. ' +
f'Not installing version {pkg_version}. Use `--force` to downgrade.')
sys.exit(1)
if args.force:
uninstall_kmp(args.package, args.shared, False)
kmpfile = get_kmp(args.package)
if kmpfile:
try_install_kmp(kmpfile, f"keyboard package {args.package}", args.bcp47, args.shared)
else:
logging.error("km-package-install: error: Could not download keyboard package %s", args.package)
sys.exit(2)
install_package(args)
else:
parser.print_usage()
logging.error(
"km-package-install: error: no arguments: either install a local kmp file " +
"or specify a keyboard package id to download and install.")
sys.exit(2)
print_usage_and_exit(
parser,
"km-package-install: error: no arguments: either install a local kmp file " +
"or specify a keyboard package id to download and install.",
)
if __name__ == "__main__":

View file

@ -325,9 +325,7 @@ class UninstallKmpTests(unittest.TestCase):
self.mockUninstallDir = self._setupMock('keyman_config.uninstall_kmp._uninstall_dir')
self.mockGetMetaData = self._setupMock('keyman_config.uninstall_kmp.get_metadata')
self.mockCustomKeyboardsClass = self._setupMock('keyman_config.uninstall_kmp.CustomKeyboards')
self.mockGetKeymanConfigService = self._setupMock('keyman_config.uninstall_kmp.get_keyman_config_service')
self.mockKeymanConfigServiceManager = Mock()
self.mockGetKeymanConfigService.return_value = self.mockKeymanConfigServiceManager
self.mockKeyboardListChanged = self._setupMock('keyman_config.uninstall_kmp.keyboard_list_changed')
def _setupMock(self, arg0):
patcher = patch(arg0)
@ -341,9 +339,6 @@ class UninstallKmpTests(unittest.TestCase):
keyboards = [{'id': 'foo1', 'languages': [{'id': 'en'}]}, {'id': 'bar2', 'languages': [{'id': 'fr'}]}]
self.mockGetMetaData.return_value = (None, None, None, keyboards, None)
keyboardListChangedMock = Mock()
self.mockKeymanConfigServiceManager.keyboard_list_changed = keyboardListChangedMock
# Execute
uninstall_kmp('foo', False, True)
@ -351,5 +346,4 @@ class UninstallKmpTests(unittest.TestCase):
self.mockUninstallKeyboardsFromIbus.assert_called_once_with(keyboards, '/tmp/foo')
self.mockUninstallDir.assert_called()
self.assertEqual(mockCustomKeyboardsInstance.remove.call_count, 2)
self.assertEqual(keyboardListChangedMock.call_count, 1)
self.mockKeyboardListChanged.assert_called_once()

View file

@ -365,7 +365,8 @@ NSString *const kKMLegacyApps = @"KMLegacyApps";
[clientAppId isEqual: @"org.sil.app.builder.scripture.ScriptureAppBuilder"] ||
[clientAppId isEqual: @"org.sil.app.builder.reading.ReadingAppBuilder"] ||
[clientAppId isEqual: @"org.sil.app.builder.dictionary.DictionaryAppBuilder"] ||
//[clientAppId isEqual: @"com.microsoft.Word"] || // 2020-11-24[mcd]: Appears to work well in Word 16.43, disable legacy by default
[clientAppId isEqual: @"com.microsoft.Word"] ||
// Microsoft Word removed on 2020-11-24 but added back on 2025-10-10 for issue #14160
[clientAppId isEqual: @"org.openoffice.script"] ||
// 2023-12-13[sgs]: Adobe apps automatically detected as non-compliant
//[clientAppId isEqual: @"com.adobe.illustrator"] ||

View file

@ -22,7 +22,7 @@ Read only
### Return Value
Presently set to [`"http://help.keymanweb.com/go"`]().
Presently set to [`"https://help.keyman.com/go"`]().
## Description

View file

@ -130,7 +130,7 @@ if(!keymanweb) {
this.outerDiv = util.createElement('div'); // Container for UI (visible when KeymanWeb is active)
this.innerDiv = util.createElement('div'); // inner div for UI
this.kbdIcon = util.createElement('img');
this.outerDiv.innerHTML = "<a href='http://keyman.com/web/' target='KeymanWebHelp'>"
this.outerDiv.innerHTML = "<a href='https://keyman.com/developer/keymanweb/' target='KeymanWebHelp'>"
+ "<img src='"+imgPath+"kmicon.gif' border='0' style='padding: 0px 2px 0 1px; margin:0px;' title='KeymanWeb' alt='KeymanWeb' /></a>"; /* I2081 */
let s=this.outerDiv.style;

View file

@ -1,18 +1,18 @@
(*
Name: UfrmOSKOnScreenKeyboard
Copyright: Copyright (C) SIL International.
Documentation:
Description:
Documentation:
Description:
Create Date: 14 Sep 2006
Modified Date: 8 Apr 2015
Authors: mcdurdin
Related Files:
Dependencies:
Related Files:
Dependencies:
Bugs:
Todo:
Notes:
Bugs:
Todo:
Notes:
History: 14 Sep 2006 - mcdurdin - Initial version
04 Dec 2006 - mcdurdin - Support exporting and printing
04 Dec 2006 - mcdurdin - Fix shift states when pressing keys
@ -72,6 +72,7 @@ type
VKI: TVisualKeyboardInfo;
VisualKeyboards: TVisualKeyboardInfoList;
FCachedShiftState: TExtShiftState;
IsSimulatedLControlDown: Boolean;
function GetAsyncShiftState: TExtShiftState;
procedure UpdateKeyboard(FLoading: Boolean);
@ -89,6 +90,7 @@ type
procedure PrintKeyboard;
procedure ResetShiftStates; // I1144
procedure OskModifierEvent(VKCode, Flags: DWORD);
function HasVisualKeyboard: Boolean; overload;
function HasVisualKeyboard(KeymanID: Integer): Boolean; overload;
@ -222,6 +224,58 @@ begin
ShiftStateChange(fkcss, ass);
end;
(**
* Handles OSK modifier events received from
* keyman32:PostVisualKeyboardModifierEvent in k32_visualkeyboardinterface.cpp.
*
* This takes the chiral VK_LCONTROL / VK_RCONTROL / VK_LMENU / VK_RMENU virtual
* key codes.
*
* This currently deals only with one situation: when Windows posts a simulated
* LControl key when AltGr is pressed on a European layout. See
* serialkeyeventserver.cpp:WndProc for a deeper discussion of the key events
* that are generated in this scenario.
*
* @param VKCode virtual key code of the modifier key, chiral for Alt and Ctrl
* @param Flags as follows:
* KEYEVENTF_EXTENDEDKEY 0x00000001 - extended bit is set on the scan code
* KEYEVENTF_KEYUP 0x00000002 - key is being released
* scan code mask 0x0FFF0000 - 12 bits for scan code
* all other bits reserved
*)
procedure TfrmOSKOnScreenKeyboard.OskModifierEvent(VKCode, Flags: DWORD);
var
isUp: Boolean;
scanCode: DWORD;
const
/// The normal scan code for left Control key
SCAN_LEFT_CONTROL = $1D;
/// The scan code generated by Windows when it simulates pressing left
/// Control, when AltGr is pressed on a European layout; this is generated by
/// Windows for compatibility reasons.
SCAN_LEFT_CONTROL_SIMULATED = $21D;
begin
// not used: extended := (Flags and KEYEVENTF_EXTENDEDKEY) <> 0;
isUp := (Flags and KEYEVENTF_KEYUP) <> 0;
// In order to identify the Windows simulated left control scan code, we need
// to match on more bits than the normal 8 bit scan code.
scanCode := (Flags and $0FFF0000) shr 16;
// If the Left Control key is simulated by Windows, then we want to ignore it
// because in reality, AltGr has been pressed on a European keyboard. However,
// if the Left Control key is being released, then we should reset the flag
// whether it comes from the Windows simulation or from another source (real
// or injected), so that we don't end up with a stuck left control key
if (scanCode = SCAN_LEFT_CONTROL_SIMULATED) and not isUp then
IsSimulatedLControlDown := True
else if ((scanCode = SCAN_LEFT_CONTROL) or (scanCode = SCAN_LEFT_CONTROL_SIMULATED)) and isUp then
IsSimulatedLControlDown := False;
// TODO: in the future, we might be able to eliminate tmrCheck and make all
// modifier updates go through the WH_KEYBOARD_LL hook.
end;
procedure TfrmOSKOnScreenKeyboard.ShiftStateChange(kbdShift, asyncShift: TExtShiftState);
procedure PrepState(fkcss, ass: TExtShiftState; shift: TExtShiftStateValue; vk: Integer);
var
@ -317,10 +371,10 @@ begin
FNewShiftState := FNewShiftState - [essLCtrl, essRCtrl];
FChanged := True;
end;
if FChanged then
ShiftStateChange(FNewShiftState, FShiftState);
FShiftState := FNewShiftState;
kbd.ShiftState := FShiftState;
UpdateKeyboard(False);
@ -357,36 +411,28 @@ begin
end;
function TfrmOSKOnScreenKeyboard.GetAsyncShiftState: TExtShiftState;
var
lctrl: SHORT;
ralt: SHORT;
begin
Result := [];
if (GetAsyncKeyState(VK_SHIFT) and $8000) = $8000 then Result := Result + [essShift];
if kbd.LRShift then
begin
if (GetAsyncKeyState(VK_LCONTROL) and $8000) = $8000 then Result := Result + [essLCtrl];
lctrl := GetAsyncKeyState(VK_LCONTROL);
ralt := GetAsyncKeyState(VK_RMENU);
if not IsSimulatedLControlDown and ((lctrl and $8000) = $8000) then
Result := Result + [essLCtrl];
if (GetAsyncKeyState(VK_RCONTROL) and $8000) = $8000 then Result := Result + [essRCtrl];
if (GetAsyncKeyState(VK_LMENU) and $8000) = $8000 then Result := Result + [essLAlt];
if (GetAsyncKeyState(VK_RMENU) and $8000) = $8000 then Result := Result + [essRAlt];
if (ralt and $8000) = $8000 then Result := Result + [essRAlt];
end
else
begin
if (GetAsyncKeyState(VK_CONTROL) and $8000) = $8000 then Result := Result + [essCtrl];
if (GetAsyncKeyState(VK_MENU) and $8000) = $8000 then Result := Result + [essAlt];
end;
(*Result := [];
if (GetAsyncKeyState(VK_SHIFT) and $8000) = $8000 then Result := Result + [essShift];
if kbd.LRShift then
begin
if (GetAsyncKeyState(VK_LCONTROL) and $8000) = $8000 then Result := Result + [essLCtrl];
if (GetAsyncKeyState(VK_RCONTROL) and $8000) = $8000 then Result := Result + [essRCtrl];
if (GetAsyncKeyState(VK_LMENU) and $8000) = $8000 then Result := Result + [essLAlt];
if (GetAsyncKeyState(VK_RMENU) and $8000) = $8000 then Result := Result + [essRAlt];
end
else
begin
if (GetAsyncKeyState(VK_CONTROL) and $8000) = $8000 then Result := Result + [essCtrl];
if (GetAsyncKeyState(VK_MENU) and $8000) = $8000 then Result := Result + [essAlt];
end;*)
end;
function TfrmOSKOnScreenKeyboard.HasVisualKeyboard(KeymanID: Integer): Boolean;
@ -474,7 +520,7 @@ begin
ss := ExtShiftStateToVkShiftState(kbd.ShiftState);
ssi := ValidExtShiftStateIndex(kbd.ShiftState);
end;
for i := 0 to VKI.Keyboard.Keys.Count - 1 do
begin
if not FPositional then

View file

@ -203,6 +203,7 @@ type
procedure WMNCHitTest(var Message: TWMNCHitTest); message WM_NCHITTEST;
procedure WMUserFormShown(var Message: TMessage); message WM_USER_FormShown; // I4225
procedure WMKeymanOskModifierEvent(var Message: TMessage); message WM_KEYMAN_OSK_MODIFIER_EVENT;
procedure UpdatePanels;
procedure SetActivePage(Page: TOSKActivePage);
@ -998,6 +999,12 @@ begin
inherited;
end;
procedure TfrmVisualKeyboard.WMKeymanOskModifierEvent(var Message: TMessage);
begin
if Assigned(FOnScreenKeyboard) then
FOnScreenKeyboard.OskModifierEvent(Message.WParam, Message.LParam);
end;
procedure TfrmVisualKeyboard.AdjustToClosestMonitor(var x, y, cx, cy: Integer);
var
m: TMonitor;

View file

@ -176,10 +176,15 @@ LRESULT _kmnLowLevelKeyboardProc(
if (GetKeyState(VK_LSHIFT) < 0) {
FHotkeyShiftState |= HK_SHIFT;
}
if (GetKeyState(VK_RSHIFT) < 0) {
FHotkeyShiftState |= AllowRightModifierHotkey ? HK_SHIFT : HK_RSHIFT_INVALID;
}
if(isModifierKey(hs->vkCode)) {
PostVisualKeyboardModifierEvent(hs->vkCode, LLKHFFlagstoWMKeymanKeyEventFlags(hs));
}
//TODO: #8064. Can remove debug message once issue #8064 is resolved
SendDebugMessageFormat("[FHotkeyShiftState:%x]", FHotkeyShiftState);

View file

@ -0,0 +1,65 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by mcdurdin on 2025-10-08
*
* Interface between keyman32.dll and the visual keyboard module hosted in
* keyman.exe
*/
#include "pch.h"
// These messages should correspond to the UfrmOSKOnScreenKeyboard section in
// UserMessages.pas. Only messages used in keyman32 are currently defined here.
#define WM_KEYMAN_OSK_MODIFIER_EVENT (WM_USER + 141)
/**
* Tell the visual keyboard module that modifier keys on the physical keyboard
* have been pressed or released, in order to update the display of the visual
* keyboard. This is sent from the low level keyboard hook, so is an in-process
* single thread event.
*
* This takes the chiral VK_LCONTROL / VK_RCONTROL / VK_LMENU / VK_RMENU virtual
* key codes.
*
* This function exists primarily to support workaround of the Windows hack of
* generating LCtrl+RAlt when AltGr is pressed on European layouts. The visual
* keyboard module includes special handling for the custom scan code of 0x21D
* for the fudged LCtrl key.
*
* @param vkCode virtual key code of the modifier key, chiral for Alt and Ctrl
* @param flags as follows:
* KEYEVENTF_EXTENDEDKEY 0x00000001 - extended bit is set on the scan code
* KEYEVENTF_KEYUP 0x00000002 - key is being released
* scan code mask 0x0FFF0000 - 12 bits for scan code
* all other bits reserved
*/
void PostVisualKeyboardModifierEvent(UINT vkCode, DWORD flags) {
PostMessageToVisualKeyboardWindowIfExists(WM_KEYMAN_OSK_MODIFIER_EVENT, vkCode, flags);
}
/**
* Find the handle to the Visual Keyboard window in keyman.exe, if it is open,
* and returns NULL if not found.
*
* @return HWND Handle to the Visual Keyboard window
*/
HWND GetVisualKeyboardWindow() {
return FindWindow("TfrmVisualKeyboard", NULL);
}
/**
* Post a message to the Visual Keyboard window in keyman.exe, if it is open.
*
* @param msg Windows message
* @param wParam WPARAM
* @param lParam LPARAM
* @return BOOL FALSE if window does exist or on PostMessage failure
*/
BOOL PostMessageToVisualKeyboardWindowIfExists(UINT msg, WPARAM wParam, LPARAM lParam) {
HWND hwnd = GetVisualKeyboardWindow();
if(hwnd == NULL) {
return FALSE;
}
return PostMessage(hwnd, msg, wParam, lParam);
}

View file

@ -0,0 +1,14 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by mcdurdin on 2025-10-08
*
* Interface between keyman32.dll and the visual keyboard module hosted in
* keyman.exe
*/
#pragma once
HWND GetVisualKeyboardWindow();
void PostVisualKeyboardModifierEvent(UINT vkCode, DWORD flags);
BOOL PostMessageToVisualKeyboardWindowIfExists(UINT msg, WPARAM wParam, LPARAM lParam);

View file

@ -349,6 +349,7 @@
<ClCompile Include="K32_load.cpp" />
<ClCompile Include="k32_lowlevelkeyboardhook.cpp" />
<ClCompile Include="k32_tsf.cpp" />
<ClCompile Include="k32_visualkeyboardinterface.cpp" />
<ClCompile Include="keybd_shift.cpp" />
<ClCompile Include="keyboardoptions.cpp" />
<ClCompile Include="keyman32.cpp" />
@ -441,6 +442,7 @@
<ClInclude Include="pch.h" />
<ClInclude Include="hotkeys.h" />
<ClInclude Include="k32_tsf.h" />
<ClInclude Include="k32_visualkeyboardinterface.h" />
<ClInclude Include="KBD.H" />
<ClInclude Include="keyboardoptions.h" />
<ClInclude Include="keyman64.h" />

View file

@ -262,6 +262,7 @@ void keybd_shift(LPINPUT pInputs, int* n, BOOL isReset, LPBYTE const kbd);
#include "..\..\..\..\common\windows\cpp\include\crc32.h"
#include "k32_tsf.h"
#include "k32_visualkeyboardinterface.h"
void ReportActiveKeyboard(WORD wCommand); // I3933 // I3949
void SelectKeyboardHKL(DWORD hkl, BOOL foreground); // I3933 // I3949 // I4271

View file

@ -170,10 +170,11 @@ LRESULT _kmnGetMessageProc(int nCode, WPARAM wParam, LPARAM lParam)
}
// I1337 - move mousewheel message checking before the PM_REMOVE handling - otherwise we can't pre-modify the message
// TODO: Move to WH_MOUSE_LL proc in main process
// TODO: Move to WH_MOUSE_LL proc in main process?
if(mp->message == WM_MOUSEWHEEL)
{
HWND hwndMouseWheel = FindWindow("TfrmVisualKeyboard", NULL);
// TODO: move most of this logic to k32_visualkeyboardinterface.cpp
HWND hwndMouseWheel = GetVisualKeyboardWindow();
if(hwndMouseWheel && GetWindowThreadProcessId(mp->hwnd, NULL) != GetWindowThreadProcessId(hwndMouseWheel, NULL))
{
RECT rcMouseWheel;

View file

@ -475,6 +475,10 @@ private:
There is a possibility that some apps may try and sniff that 0x21D scan code and get confused because Keyman
doesn't emit it. Hopefully this is rare.
See also:
* PostVisualKeyboardModifierEvent in k32_visualkeyboardinterface.cpp
* TfrmOSKOnScreenKeyboard.OskModifierEvent in UfrmOSKOnScreenKeyboard.cpp
*/
INPUT input[2];
input[0].type = INPUT_KEYBOARD;

View file

@ -0,0 +1,9 @@
# keyboard_ll_identifier
This test app allows us to view details of key events as they are received by
the low level keyboard hook. There are subtle differences between these key
events and those received in WM_KEY* messages, e.g. see discussions on
simulated left control in k32_visualkeyboardinterface.cpp.
The app currently only reports on modifier keys, but it's easy to tweak for
other reporting as needed.

View file

@ -0,0 +1,14 @@
program keyboard_ll_identifier;
uses
Vcl.Forms,
keyboard_ll_identifier_unit in 'keyboard_ll_identifier_unit.pas' {Form1};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.

View file

@ -0,0 +1,970 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ProjectGuid>{C945C654-1D70-4248-8513-09FBD3715ACE}</ProjectGuid>
<ProjectVersion>18.8</ProjectVersion>
<FrameworkType>VCL</FrameworkType>
<MainSource>keyboard_ll_identifier.dpr</MainSource>
<Base>True</Base>
<Config Condition="'$(Config)'==''">Debug</Config>
<Platform Condition="'$(Platform)'==''">Win32</Platform>
<TargetedPlatforms>1</TargetedPlatforms>
<AppType>Application</AppType>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Base' or '$(Base)'!=''">
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Base)'=='true') or '$(Base_Win32)'!=''">
<Base_Win32>true</Base_Win32>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win64' and '$(Base)'=='true') or '$(Base_Win64)'!=''">
<Base_Win64>true</Base_Win64>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Debug' or '$(Cfg_1)'!=''">
<Cfg_1>true</Cfg_1>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Cfg_1)'=='true') or '$(Cfg_1_Win32)'!=''">
<Cfg_1_Win32>true</Cfg_1_Win32>
<CfgParent>Cfg_1</CfgParent>
<Cfg_1>true</Cfg_1>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Config)'=='Release' or '$(Cfg_2)'!=''">
<Cfg_2>true</Cfg_2>
<CfgParent>Base</CfgParent>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="('$(Platform)'=='Win32' and '$(Cfg_2)'=='true') or '$(Cfg_2_Win32)'!=''">
<Cfg_2_Win32>true</Cfg_2_Win32>
<CfgParent>Cfg_2</CfgParent>
<Cfg_2>true</Cfg_2>
<Base>true</Base>
</PropertyGroup>
<PropertyGroup Condition="'$(Base)'!=''">
<DCC_DcuOutput>.\$(Platform)\$(Config)</DCC_DcuOutput>
<DCC_ExeOutput>.\$(Platform)\$(Config)</DCC_ExeOutput>
<DCC_E>false</DCC_E>
<DCC_N>false</DCC_N>
<DCC_S>false</DCC_S>
<DCC_F>false</DCC_F>
<DCC_K>false</DCC_K>
<DCC_Namespace>System;Xml;Data;Datasnap;Web;Soap;Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;$(DCC_Namespace)</DCC_Namespace>
<Icon_MainIcon>$(BDS)\bin\delphi_PROJECTICON.ico</Icon_MainIcon>
<UWP_DelphiLogo44>$(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png</UWP_DelphiLogo44>
<UWP_DelphiLogo150>$(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png</UWP_DelphiLogo150>
<SanitizedProjectName>keyboard_ll_identifier</SanitizedProjectName>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win32)'!=''">
<DCC_UsePackage>DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;vcl;IndyIPServer;vclactnband;vclFireDAC;IndySystem;tethering;svnui;mbColorLibD10;dsnapcon;FireDACADSDriver;scFontCombo;FireDACMSAccDriver;fmxFireDAC;vclimg;TeeDB;FireDAC;Jcl;vcltouch;JvCore;vcldb;bindcompfmx;svn;FireDACSqliteDriver;FireDACPgDriver;inetdb;CEF4Delphi;FMXTee;soaprtl;DbxCommonDriver;FmxTeeUI;FireDACIBDriver;fmx;fmxdae;xmlrtl;soapmidas;Tee;fmxobj;vclwinx;common_components;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dbexpress;IndyCore;vclx;browse4folder;bindcomp;appanalytics;dsnap;FireDACCommon;IndyIPClient;bindcompvcl;RESTBackendComponents;TeeUI;VCLRESTComponents;soapserver;dbxcds;VclSmp;JvDocking;adortl;developer_components;JclVcl;vclie;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;inetdbxpress;IndyProtocols;keyman_components;FireDACCommonODBC;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage)</DCC_UsePackage>
<DCC_Namespace>Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace)</DCC_Namespace>
<BT_BuildType>Debug</BT_BuildType>
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
<VerInfo_Locale>1033</VerInfo_Locale>
<Manifest_File>$(BDS)\bin\default_app.manifest</Manifest_File>
</PropertyGroup>
<PropertyGroup Condition="'$(Base_Win64)'!=''">
<DCC_UsePackage>DBXSqliteDriver;bindcompdbx;IndyIPCommon;RESTComponents;DBXInterBaseDriver;vcl;IndyIPServer;vclactnband;vclFireDAC;IndySystem;tethering;dsnapcon;FireDACADSDriver;FireDACMSAccDriver;fmxFireDAC;vclimg;TeeDB;FireDAC;Jcl;vcltouch;vcldb;bindcompfmx;FireDACSqliteDriver;FireDACPgDriver;inetdb;CEF4Delphi;FMXTee;soaprtl;DbxCommonDriver;FmxTeeUI;FireDACIBDriver;fmx;fmxdae;xmlrtl;soapmidas;Tee;fmxobj;vclwinx;rtl;DbxClientDriver;CustomIPTransport;vcldsnap;dbexpress;IndyCore;vclx;bindcomp;appanalytics;dsnap;FireDACCommon;IndyIPClient;bindcompvcl;RESTBackendComponents;TeeUI;VCLRESTComponents;soapserver;dbxcds;VclSmp;adortl;JclVcl;vclie;bindengine;DBXMySQLDriver;CloudService;dsnapxml;FireDACMySQLDriver;dbrtl;inetdbxpress;IndyProtocols;FireDACCommonODBC;FireDACCommonDriver;inet;fmxase;$(DCC_UsePackage)</DCC_UsePackage>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1)'!=''">
<DCC_Define>DEBUG;$(DCC_Define)</DCC_Define>
<DCC_DebugDCUs>true</DCC_DebugDCUs>
<DCC_Optimize>false</DCC_Optimize>
<DCC_GenerateStackFrames>true</DCC_GenerateStackFrames>
<DCC_DebugInfoInExe>true</DCC_DebugInfoInExe>
<DCC_RemoteDebug>true</DCC_RemoteDebug>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
<DCC_RemoteDebug>false</DCC_RemoteDebug>
<AppEnableRuntimeThemes>true</AppEnableRuntimeThemes>
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
<DCC_DebugInformation>0</DCC_DebugInformation>
</PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2_Win32)'!=''">
<AppEnableRuntimeThemes>true</AppEnableRuntimeThemes>
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
</PropertyGroup>
<ItemGroup>
<DelphiCompile Include="$(MainSource)">
<MainSource>MainSource</MainSource>
</DelphiCompile>
<DCCReference Include="keyboard_ll_identifier_unit.pas">
<Form>Form1</Form>
<FormType>dfm</FormType>
</DCCReference>
<BuildConfiguration Include="Release">
<Key>Cfg_2</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
<BuildConfiguration Include="Debug">
<Key>Cfg_1</Key>
<CfgParent>Base</CfgParent>
</BuildConfiguration>
</ItemGroup>
<ProjectExtensions>
<Borland.Personality>Delphi.Personality.12</Borland.Personality>
<Borland.ProjectType>Application</Borland.ProjectType>
<BorlandProject>
<Delphi.Personality>
<Source>
<Source Name="MainSource">keyboard_ll_identifier.dpr</Source>
</Source>
</Delphi.Personality>
<Deployment Version="3">
<DeployFile LocalName="Win32\Debug\keyboard_ll_identifier.exe" Configuration="Debug" Class="ProjectOutput">
<Platform Name="Win32">
<RemoteName>keyboard_ll_identifier.exe</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployClass Name="AdditionalDebugSymbols">
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidClassesDexFile">
<Platform Name="Android">
<RemoteDir>classes</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>classes</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidFileProvider">
<Platform Name="Android">
<RemoteDir>res\xml</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\xml</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidGDBServer">
<Platform Name="Android">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidLibnativeArmeabiFile">
<Platform Name="Android">
<RemoteDir>library\lib\armeabi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>library\lib\armeabi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidLibnativeArmeabiv7aFile">
<Platform Name="Android64">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidLibnativeMipsFile">
<Platform Name="Android">
<RemoteDir>library\lib\mips</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>library\lib\mips</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidServiceOutput">
<Platform Name="Android">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>library\lib\arm64-v8a</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidServiceOutput_Android32">
<Platform Name="Android64">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidSplashImageDef">
<Platform Name="Android">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidSplashStyles">
<Platform Name="Android">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="AndroidSplashStylesV21">
<Platform Name="Android">
<RemoteDir>res\values-v21</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values-v21</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_Colors">
<Platform Name="Android">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_DefaultAppIcon">
<Platform Name="Android">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon144">
<Platform Name="Android">
<RemoteDir>res\drawable-xxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon36">
<Platform Name="Android">
<RemoteDir>res\drawable-ldpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-ldpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon48">
<Platform Name="Android">
<RemoteDir>res\drawable-mdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-mdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon72">
<Platform Name="Android">
<RemoteDir>res\drawable-hdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-hdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_LauncherIcon96">
<Platform Name="Android">
<RemoteDir>res\drawable-xhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_NotificationIcon24">
<Platform Name="Android">
<RemoteDir>res\drawable-mdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-mdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_NotificationIcon36">
<Platform Name="Android">
<RemoteDir>res\drawable-hdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-hdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_NotificationIcon48">
<Platform Name="Android">
<RemoteDir>res\drawable-xhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_NotificationIcon72">
<Platform Name="Android">
<RemoteDir>res\drawable-xxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_NotificationIcon96">
<Platform Name="Android">
<RemoteDir>res\drawable-xxxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xxxhdpi</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_SplashImage426">
<Platform Name="Android">
<RemoteDir>res\drawable-small</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-small</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_SplashImage470">
<Platform Name="Android">
<RemoteDir>res\drawable-normal</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-normal</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_SplashImage640">
<Platform Name="Android">
<RemoteDir>res\drawable-large</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-large</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_SplashImage960">
<Platform Name="Android">
<RemoteDir>res\drawable-xlarge</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\drawable-xlarge</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="Android_Strings">
<Platform Name="Android">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>res\values</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="DebugSymbols">
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
</Platform>
</DeployClass>
<DeployClass Name="DependencyFramework">
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.framework</Extensions>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.framework</Extensions>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
</Platform>
</DeployClass>
<DeployClass Name="DependencyModule">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
<Extensions>.dll;.bpl</Extensions>
</Platform>
</DeployClass>
<DeployClass Required="true" Name="DependencyPackage">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
<Extensions>.bpl</Extensions>
</Platform>
</DeployClass>
<DeployClass Name="File">
<Platform Name="Android">
<Operation>0</Operation>
</Platform>
<Platform Name="Android64">
<Operation>0</Operation>
</Platform>
<Platform Name="iOSDevice32">
<Operation>0</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>0</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>0</Operation>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\Resources\StartUp\</RemoteDir>
<Operation>0</Operation>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\Resources\StartUp\</RemoteDir>
<Operation>0</Operation>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch1024x768">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch1536x2048">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch1668">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch1668x2388">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch2048x1536">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch2048x2732">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch2224">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch2388x1668">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch2732x2048">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPad_Launch768x1024">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch1125">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch1136x640">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch1242">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch1242x2688">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch1334">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch1792">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch2208">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch2436">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch2688x1242">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch320">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch640">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch640x1136">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch750">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="iPhone_Launch828">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectAndroidManifest">
<Platform Name="Android">
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSDeviceDebug">
<Platform Name="iOSDevice32">
<RemoteDir>..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<RemoteDir>..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSDeviceResourceRules">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSEntitlements">
<Platform Name="iOSDevice32">
<RemoteDir>..\</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<RemoteDir>..\</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSInfoPList">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSResource">
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectOSXDebug">
<Platform Name="OSX64">
<RemoteDir>..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectOSXEntitlements">
<Platform Name="OSX32">
<RemoteDir>..\</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSX64">
<RemoteDir>..\</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectOSXInfoPList">
<Platform Name="OSX32">
<RemoteDir>Contents</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectOSXResource">
<Platform Name="OSX32">
<RemoteDir>Contents\Resources</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\Resources</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Required="true" Name="ProjectOutput">
<Platform Name="Android">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Android64">
<RemoteDir>library\lib\arm64-v8a</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice32">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSDevice64">
<Operation>1</Operation>
</Platform>
<Platform Name="iOSSimulator">
<Operation>1</Operation>
</Platform>
<Platform Name="Linux64">
<Operation>1</Operation>
</Platform>
<Platform Name="OSX32">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="OSX64">
<RemoteDir>Contents\MacOS</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Win32">
<Operation>0</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectOutput_Android32">
<Platform Name="Android64">
<RemoteDir>library\lib\armeabi-v7a</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectUWPManifest">
<Platform Name="Win32">
<Operation>1</Operation>
</Platform>
<Platform Name="Win64">
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="UWP_DelphiLogo150">
<Platform Name="Win32">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Win64">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="UWP_DelphiLogo44">
<Platform Name="Win32">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
<Platform Name="Win64">
<RemoteDir>Assets</RemoteDir>
<Operation>1</Operation>
</Platform>
</DeployClass>
<ProjectRoot Platform="iOSDevice64" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="Win64" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="iOSDevice32" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="Linux64" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="Win32" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="OSX32" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="Android" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="OSX64" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="iOSSimulator" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="Android64" Name="$(PROJECTNAME)"/>
</Deployment>
<Platforms>
<Platform value="Win32">True</Platform>
<Platform value="Win64">False</Platform>
</Platforms>
</BorlandProject>
<ProjectFileVersion>12</ProjectFileVersion>
</ProjectExtensions>
<Import Project="$(BDS)\Bin\CodeGear.Delphi.Targets" Condition="Exists('$(BDS)\Bin\CodeGear.Delphi.Targets')"/>
<Import Project="$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj" Condition="Exists('$(APPDATA)\Embarcadero\$(BDSAPPDATABASEDIR)\$(PRODUCTVERSION)\UserTools.proj')"/>
<Import Project="$(MSBuildProjectName).deployproj" Condition="Exists('$(MSBuildProjectName).deployproj')"/>
</Project>

View file

@ -0,0 +1,27 @@
object Form1: TForm1
Left = 0
Top = 0
Caption = 'Form1'
ClientHeight = 347
ClientWidth = 728
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'Tahoma'
Font.Style = []
OldCreateOrder = False
OnCreate = FormCreate
OnDestroy = FormDestroy
PixelsPerInch = 96
TextHeight = 13
object Memo1: TMemo
Left = 8
Top = 8
Width = 697
Height = 321
Lines.Strings = (
'Memo1')
TabOrder = 0
end
end

View file

@ -0,0 +1,85 @@
unit keyboard_ll_identifier_unit;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class(TForm)
Memo1: TMemo;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
procedure AppOnMessage(var Msg: TMsg; var Handled: Boolean);
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
var
hh: Cardinal;
type
TKbdLlHookStruct = record
vkCode, scanCode, flags, time: DWORD;
dwExtraInfo: ULONG_PTR;
end;
PKbdLlHookStruct = ^TKbdLlHookStruct;
function kbdll(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
s: PKbdllhookstruct;
begin
if nCode < 0 then
begin
Exit(CallNextHookEx(hh, nCode, wParam, lParam));
end;
s := PkbdllHookStruct(lParam);
if s.vkCode in [VK_LCONTROL, VK_RCONTROL, VK_LMENU, VK_RMENU, VK_LSHIFT, VK_RSHIFT, VK_CONTROL, VK_SHIFT, VK_MENU] then
if Assigned(Form1) then
begin
Form1.memo1.Lines.Add(Format('%08.8x %08.8x %08.8x', [s.vkCode, s.scanCode, s.flags]));
end;
Exit(CallNextHookEx(hh, nCode, wParam, lParam));
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
// Uncomment to monitor WM_KEY* and WM_SYSKEY* messages
// Application.OnMessage := AppOnMessage;
hh := SetWindowsHookEx(WH_KEYBOARD_LL, kbdll, GetModuleHandle(nil), 0);
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
Form1 := nil;
UnhookWindowsHookEx(hh);
end;
procedure TForm1.AppOnMessage(var Msg: TMsg; var Handled: Boolean);
begin
if (Msg.message = WM_KEYDOWN) or (Msg.message = WM_SYSKEYDOWN) or (Msg.message = WM_KEYUP) or (Msg.message = WM_SYSKEYUP) then
begin
if Msg.wParam in [VK_CONTROL, VK_SHIFT, VK_MENU] then
if (Msg.LParam and $FFFF) = 1 then
begin
memo1.Lines.Add(Format('%08.8x %08.8x', [Msg.wParam, Msg.lParam]));
end;
end;
end;
end.