diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000000..a6797deab4 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,144 @@ +# api-verification.yml: checks Core API for incompatible changes + +This action automatically verifies that the API of Keyman Core did not +change in incompatible ways without incrementing the API version number. +It applies to all platforms that use Core, even though we only run it on +Linux. + +- The action gets triggered by the completion of the Ubuntu packaging + workflow (`deb-packaging.yml`). +- The API version is defined in `core/CORE_API_VERSION.md`. +- The baseline file for comparing the API is + `linux/debian/libkeymancore${API_VERSION}.symbols`. The action will + re-create this file and and makes it available as an artifact. +- The API checks are implemented in `linux/scripts/verify_api.inc.sh`. + +More information about the API verification can be found in +`docs/core-api-verification.md`. + +# auto-merge-keyman-server-pr.yml +TODO + +# build-test-publish-docker.yml +TODO + +# cleanup-ghcr.yml +TODO + +# close-linked-issues-for-merged-prs.yml +TODO + +# core-arm64-windows-test.yml: Test Keyman Core on Windows/ARM64 + +This action allows us to test Keyman Core for Windows on ARM64 architecture with +a GitHub agent, as we do not have any TeamCity Windows build agents with ARM64 +arch. + +It will normally be triggered as part of a test build from the +resources/teamcity/triggers/trigger-test-builds.sh script. + +You can manually trigger a core-arm64-windows-test action on GitHub on your fork +of keymanapp/keyman. This is normally only needed to test changes to the action +itself: + +- push the changes to your fork of keymanapp/keyman: `git push myfork HEAD:master` +- make sure you have GHA enabled in the settings of your fork +- seet `GITHUB_TOKEN=` +- trigger a build with: + + ```bash + curl --write-out '\n' --request POST \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: token $GITHUB_TOKEN" \ + --data "{ \"event_type\": \"core-arm64-windows-test: master\", \ + \"client_payload\": { \ + \"buildSha\": \"$(git rev-parse refs/heads/master)\", \ + \"branch\": \"master\", \ + \"user\": \"${USER}\", \ + \"isTestBuild\": \"true\", \ + \"force\": \"true\"}" \ + https://api.github.com/repos//keyman/dispatches + ``` + + To trigger a build for PR #1234 in a branch `my-pr-branch` this would look similar + to this: + + ```bash + curl --write-out '\n' --request POST \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: token $GITHUB_TOKEN" \ + --data "{ \"event_type\": \"core-arm64-windows-test: PR #1234\", \ + \"client_payload\": { \ + \"buildSha\": \"$(git rev-parse refs/heads/my-pr-branch)\", \ + \"branch\": \"my-pr-branch\", \ + \"user\": \"${USER}\", \ + \"isTestBuild\": \"true\", \ + \"force\": \"true\"}" \ + https://api.github.com/repos//keyman/dispatches + ``` + +.... + +# crowdin.yml: Upload translation sources to [Crowdin](translate.keyman.com) + +This action automatically updates the English source strings on Crowdin +https://crowdin.com/project/keyman/sources/files + +based on the root [crowdin.yml](../../crowdin.yml) configuration file. The daily cron job currently runs at 6:00AM. + +This should match the behavior of locally using the [Crowdin CLI to upload sources](https://crowdin.github.io/crowdin-cli/commands/crowdin-upload-sources). + +# deb-packaging.yml: Ubuntu Packaging GitHub Action + +You can manually trigger a deb-packaging action on GitHub on your fork +of keymanapp/keyman. This is normally only needed to test changes to the action +itself: + +- push the changes to your fork of keymanapp/keyman: `git push myfork HEAD:master` +- make sure you have GHA enabled in the settings of your fork +- seet `GITHUB_TOKEN=` +- trigger a build with: + + ```bash + curl --write-out '\n' --request POST \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: token $GITHUB_TOKEN" \ + --data "{ \"event_type\": \"deb-pr-packaging: master\", \ + \"client_payload\": { \ + \"buildSha\": \"$(git rev-parse refs/heads/master)\", \ + \"branch\": \"master\", \ + \"baseBranch\": \"master\", \ + \"baseRef\": \"$(git rev-parse refs/heads/master^)\", \ + \"user\": \"${USER}\", \ + \"isTestBuild\": \"true\", \ + \"force\": \"true\"}" \ + https://api.github.com/repos//keyman/dispatches + ``` + + To trigger a build for PR #1234 in a branch `pr-1234` this would look similar + to this: + + ```bash + curl --write-out '\n' --request POST \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: token $GITHUB_TOKEN" \ + --data "{ \"event_type\": \"deb-pr-packaging: PR #1234\", \ + \"client_payload\": { \ + \"buildSha\": \"$(git rev-parse refs/heads/pr-1234)\", \ + \"branch\": \"pr-1234\", \ + \"baseBranch\": \"master\", \ + \"baseRef\": \"$(git rev-parse refs/heads/master)\", \ + \"user\": \"${USER}\", \ + \"isTestBuild\": \"true\", \ + \"force\": \"true\"}" \ + https://api.github.com/repos//keyman/dispatches + ``` + +# labeler.yml +TODO + +# npm-publish.yml +TODO + +# pr-build-status.yml +TODO diff --git a/.github/workflows/core-arm64-windows-test.yml b/.github/workflows/core-arm64-windows-test.yml new file mode 100644 index 0000000000..237e60d4ef --- /dev/null +++ b/.github/workflows/core-arm64-windows-test.yml @@ -0,0 +1,167 @@ +# +# This workflow tests Keyman Core on ARM64/Windows; we cannot use our TeamCity +# agents because we have no ARM64 Windows agents. +# +# It is triggered by a repository_dispatch event with the type +# `core-arm64-windows-test:*`. +# + +name: "Keyman Core - ARM64/Windows test" +run-name: "Keyman Core - ARM64/Windows test - ${{ github.event.client_payload.branch }} by @${{ github.event.client_payload.user }}" +on: + repository_dispatch: + types: ['core-arm64-windows-test:*'] + +# Inputs: +# +# buildSha: The SHA of the commit to build, e.g. of the branch or +# refs/pull/1234/head for PR +# isTestBuild: true for PR and test builds, false for release builds +# force: true to run this action on a fork of keymanapp/keyman +# +# Descriptive inputs: +# +# branch: Name of the branch to build, generally, `master`, `beta`, +# `stable-x.y`, or for a PR `#1234`. Used only in the action +# title +# user: The user that triggered the build or created the PR. Used +# only in the action title + +env: + GH_TOKEN: ${{ github.token }} + STATUS_CONTEXT: 'Keyman Core - ARM64 test' + _builder_timings: false + +jobs: + core-arm64-windows-test: + name: Keyman Core ARM64/Windows test + if: github.repository == 'keymanapp/keyman' || github.event.client_payload.force + runs-on: windows-11-arm + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 + with: + ref: '${{ github.event.client_payload.buildSha }}' + + - name: Set pending status on PR builds + id: set_status + if: github.event.client_payload.isTestBuild == 'true' + shell: bash + run: | + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \ + -f state='pending' \ + -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + -f description='Keyman Core - ARM64 test started' \ + -f context="$STATUS_CONTEXT" + + - name: Report initial npm/node versions + run: | + echo "Initial npm/node versions" + npm -v + node -v + + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - uses: actions/setup-node@v6 + with: + # This needs to be kept in sync with minimum-versions.sh + node-version: '20.19' + # As of Dec 2025, ngrok not yet supported on ARM Windows, + # google-closure-compiler (and others?) does not support arm64 yet, so + # use the x64 version of node. This is needed to avoid `npm ci` failure, + # even though we don't use ngrok directly; google-closure-compiler is + # used by emscripten + architecture: x64 + + - name: Report on current environment + shell: bash + run: | + echo "-- Environment --" + echo "pythonLocation=$pythonLocation" + echo "-- Current npm/node versions --" + npm -v + node -v + + - name: Setup environment + shell: bash + run: | + # As of Dec 2025, Emscripten does not support Win11 ARM natively + echo "EMSDK_ARCH=x86_64" >> $GITHUB_ENV + echo "EMSDK_KEEP_DOWNLOADS=1" >> $GITHUB_ENV + echo "KEYMAN_USE_EMSDK=1" >> $GITHUB_ENV + + echo "PATH=$PATH:$pythonLocation/Scripts" >> $GITHUB_ENV + + echo "KEYMAN_CI_SKIP_NVM=1" >> $GITHUB_ENV + + + - name: Install emscripten + shell: bash + run: | + source resources/build/minimum-versions.inc.sh + mkdir ../emsdk + pushd ../emsdk + git clone https://github.com/emscripten-core/emsdk.git + cd emsdk + ./emsdk install "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" + ./emsdk activate "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" + cd upstream/emscripten + npm install + echo "EMSCRIPTEN_BASE=$(pwd)" >> $GITHUB_ENV + popd + + - name: Install meson + shell: bash + run: | + pip3 install --user meson + + - name: Run test + shell: bash + run: | + # Run the test + ./core/build.sh configure,build,test:arm64 + + set_status: + name: Set result status on PR builds + needs: [core-arm64-windows-test] + runs-on: windows-11-arm + if: ${{ always() && github.event.client_payload.isTestBuild == 'true' }} + steps: + - name: Set success + if: needs.core-arm64-windows-test.result == 'success' + shell: bash + run: | + echo "RESULT=success" >> $GITHUB_ENV + echo "MSG=Keyman Core ARM64 test succeeded" >> $GITHUB_ENV + + - name: Set cancelled + if: needs.core-arm64-windows-test.result == 'cancelled' + shell: bash + run: | + echo "RESULT=error" >> $GITHUB_ENV + echo "MSG=Keyman Core ARM64 test cancelled" >> $GITHUB_ENV + + - name: Set failure + if: needs.core-arm64-windows-test.result == 'failure' + shell: bash + run: | + echo "RESULT=failure" >> $GITHUB_ENV + echo "MSG=Keyman Core ARM64 test failed" >> $GITHUB_ENV + + - name: Set final status + shell: bash + run: | + gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \ + -f state="$RESULT" \ + -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ + -f description="$MSG" \ + -f context="$STATUS_CONTEXT" diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index d5a25aacb6..7503d51d21 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: crowdin action - uses: crowdin/github-action@2.7.0 + uses: crowdin/github-action@v2.7.0 with: upload_sources: true diff --git a/.github/workflows/deb-packaging.README.md b/.github/workflows/deb-packaging.README.md deleted file mode 100644 index 662526a875..0000000000 --- a/.github/workflows/deb-packaging.README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Ubuntu Packaging GitHub Action - -You can manually trigger a deb-packaging action on GitHub, e.g. to test changes: - -- push the changes to your fork of keymanapp/keyman: git push myfork HEAD:master -- make sure you have GHA enabled in the settings of your fork and created an - access token on GitHub -- trigger a build with: - - ```bash - curl --write-out '\n' --request POST \ - --header "Accept: application/vnd.github+json" \ - --header "Authorization: token $GITHUB_TOKEN" \ - --data "{ \"event_type\": \"deb-pr-packaging: master\", \ - \"client_payload\": { \ - \"buildSha\": \"$(git rev-parse refs/heads/master)\", \ - \"branch\": \"master\", \ - \"baseBranch\": \"master\", - \"baseRef\": \"$(git rev-parse refs/heads/master^)\", - \"user\": \"${USER}\", - \"isTestBuild\": \"true\", - \"force\": \"true\"}" \ - https://api.github.com/repos//keyman/dispatches - ``` - - To trigger a build for PR #1234 in a branch `pr-1234` this would look similar - to this: - - ```bash - curl --write-out '\n' --request POST \ - --header "Accept: application/vnd.github+json" \ - --header "Authorization: token $GITHUB_TOKEN" \ - --data "{ \"event_type\": \"deb-pr-packaging: PR #1234\", \ - \"client_payload\": { \ - \"buildSha\": \"$(git rev-parse refs/heads/pr-1234)\", \ - \"branch\": \"pr-1234\", \ - \"baseBranch\": \"master\", - \"baseRef\": \"$(git rev-parse refs/heads/master)\", - \"user\": \"${USER}\", - \"isTestBuild\": \"true\", - \"force\": \"true\"}" \ - https://api.github.com/repos//keyman/dispatches - ``` diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 34fd04509e..36bfd0d0ce 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -136,7 +136,7 @@ jobs: strategy: fail-fast: true matrix: - dist: [jammy, noble, plucky, questing] + dist: [jammy, noble, questing] steps: - name: Checkout diff --git a/.github/workflows/pr-build-status.yml b/.github/workflows/pr-build-status.yml index 407dc23ef1..978a2b0995 100644 --- a/.github/workflows/pr-build-status.yml +++ b/.github/workflows/pr-build-status.yml @@ -82,6 +82,8 @@ jobs: summary += addStatus(o, 'build', status.context, status.state); } else if(status.context == 'npm pack/publish') { summary += addStatus(o, 'build', status.context, status.state); + } else if(status.context == 'Keyman Core - ARM64 test') { + summary += addStatus(o, 'build', status.context, status.state); } else if(status.context == 'check/web/file-size') { // Ignore check/web/file-size -- we won't block automerge for this at this point summary += addLog(`Skipping ${status.context}`); @@ -109,9 +111,10 @@ jobs: const { state, type } = filtered_statuses[context]; if(type == 'unknown') { // We special-case for unknown status checks, and never permit them - console.error(`An unknown context ${context} was found, cannot calculate build status.`); + // Note: function reduceStatuses() is where new status checks can be added + console.error(`An unknown context "${context}" was found, cannot calculate build status.`); return [ - 'error', `An unknown context ${context} was found, cannot calculate build status.`, summary + 'error', `An unknown context "${context}" was found, cannot calculate build status.`, summary ]; } if(type == 'build') { diff --git a/.gitignore b/.gitignore index 97c28ac83b..7b87219588 100644 --- a/.gitignore +++ b/.gitignore @@ -103,8 +103,8 @@ /windows/src/engine/keyman32/*.cod /windows/src/engine/keyman32/*.pdb -# /engine/keymanx64/ -/windows/src/engine/keymanx64/x64 +# /engine/keymanhp/ +/windows/src/engine/keymanhp/x64 # /engine/kmcomapi/ /windows/src/engine/kmcomapi/kmcomapi.tlb diff --git a/HISTORY.md b/HISTORY.md index 0e32221064..d71ea9780d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,117 @@ # Keyman Version History +## 19.0.192 alpha 2026-01-15 + +* fix(linux): issues when running ./build.sh test:linux (#15434) + +## 19.0.191 alpha 2026-01-14 + +* maint(linux): remove EOL Ubuntu 25.04 Plucky (#15423) +* maint(common): fix build when using different default node version (#15422) + +## 19.0.190 alpha 2026-01-14 + +* Update link to PrimerPrep in step-3.md (#15426) +* chore(common): Update Crowdin strings for Italian (#15418) +* maint(common): Handle custom languages in Crowdin CLI v4 (#15420) + +## 19.0.189 alpha 2026-01-12 + +* maint(common): Update `crowdin.yml` to work with Crowdin CLI 4.0+ (#15407) + +## 19.0.188 alpha 2026-01-09 + +* fix(mac): correct memory width mismatch in keyCodeForChar (#15393) +* docs(common): document `api-verification` GHA (#15410) + +## 19.0.187 alpha 2026-01-08 + +* maint(resources): add new ARM64 test status check to PR build status check (#15409) +* docs(windows): update OS support to include Arm64 Win11 (#15396) +* chore(windows,android): Update Crowdin strings for Portuguese (#15406) + +## 19.0.186 alpha 2026-01-07 + +* maint: add script to report on recent changes to stable release (#15389) + +## 19.0.185 alpha 2026-01-06 + +* chore(web): escape some additional values in TC service messages (#15384) + +## 19.0.184 alpha 2025-12-23 + +* maint(resources): set shell for steps in core-arm64-windows-test.yml (#15387) +* feat(developer): write UTF-8 .kmn file in kmdecomp (#15390) + +## 19.0.183 alpha 2025-12-22 + +* maint(core): remove leading slash on gh api call parameter (#15381) +* maint(core): fix test runner runs-on (#15382) + +## 19.0.182 alpha 2025-12-22 + +* maint(common): Fix crowdin sync version to v2.7.0 (#15373) +* chore(linux): Update keyman-config.pot strings (#15366) +* chore(windows): show architecture in support texteditor (#15378) + +## 19.0.181 alpha 2025-12-20 + +* fix(developer): clear message when selecting custom layer name in touch layout editor (#15371) +* fix(developer): prevent duplicate layer name in Layer Properties dialog (#15372) + +## 19.0.180 alpha 2025-12-19 + +* fix(mac): ensure retry for mac disk detach actually repeats (#15330) +* fix(developer): handle Enter key in dialogs in Touch Layout Editor (#15338) +* chore(windows): fix typo in error message (#15362) +* chore(common): Update crowdin strings for PT (#15368) + +## 19.0.179 alpha 2025-12-18 + +* fix(linux): fix persisting options (#15364) + +## 19.0.178 alpha 2025-12-17 + +* chore(windows): correct kbda1 to kbdth0 in comment in unit test (#15351) + +## 19.0.177 alpha 2025-12-16 + +* feat(windows): report a sentry event when low level hook is reinstalled (#15335) +* chore(linux): add manual test for localization (#15356) +* feat(developer): show keyboard version in web debugger (#15339) + +## 19.0.176 alpha 2025-12-13 + +* chore(windows): remove unused variable (#15334) + +## 19.0.175 alpha 2025-12-12 + +* fix(windows): use Thai Kedmanee instead of Arabic 101 in RightAltEmulationCheck.tests.cpp (#15346) + +## 19.0.174 alpha 2025-12-10 + +* chore(windows): simple bisect utility for checking setup-redist.exe file size (#15322) + +## 19.0.173 alpha 2025-12-09 + +* docs(developer): Clarify keystrokes in Step-1 of tutorial (#15326) +* feat(android): Add Grabbable Bar and settings status for keyboard resizing (#15267) +* chore(linux): Update debian changelog (#15282) +* docs(web): fix typo in osk documentation (#15313) + +## 19.0.172 alpha 2025-12-08 + +* docs(web): add links to next/previous page to web guide (#15311) +* fix(developer): verify project file loaded successfully in analyze (#15318) + +## 19.0.171 alpha 2025-12-08 + +* maint(resources): verify order of builder_describe_internal_dependency (#15257) +* maint(common): use `builder_launch` for child builds (#15258) +* change(windows): Revert "change(windows): include map2pdb generation for relevant Delphi projects" (#15276) +* change(windows): add shortcuts to 'Update Available' dialog (#15280) +* maint(resources): roll back builder_launch for trigger-release-builds.sh (#15316) + ## 19.0.170 alpha 2025-12-03 * chore(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 in /resources/build/pr-build-status (#15173) diff --git a/VERSION.md b/VERSION.md index 430c06443e..6668346379 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.171 \ No newline at end of file +19.0.193 \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java index 0afd3e5b28..4bc419ced6 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java @@ -22,32 +22,28 @@ import com.keyman.engine.BaseActivity; import com.keyman.engine.KMManager; /** - * Settings menu for adjusting the keyboard height. The value for the current device orientation - * is saved in shared preferences. + * Settings menu for adjusting the keyboard height. + * The value for the current device orientation is saved via KMManager. */ public class AdjustKeyboardHeightActivity extends BaseActivity { - private static final String TAG = "AdjustKbdHeight"; public static final String adjustKeyboardHeightKey = "AdjustKeyboardHeight"; - private static Button resetButton = null; - private static ImageView sampleKeyboard = null; - - // Keeps track of the adjusted keyboard height for saving - private static int currentHeight = 0; - private static ViewGroup.LayoutParams layoutParams; + private Button resetButton; + private ImageView sampleKeyboard; + private TextView percentageDisplay; + private int currentHeight; + private ViewGroup.LayoutParams layoutParams; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final Context context = this; - setContentView(R.layout.activity_adjust_keyboard_height); setupEdgeToEdge(R.id.adjust_keyboard_layout); setupStatusBarColors(R.color.keyman_blue, android.R.color.white); - Toolbar toolbar = (Toolbar) findViewById(R.id.titlebar); + Toolbar toolbar = findViewById(R.id.titlebar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { @@ -60,76 +56,162 @@ public class AdjustKeyboardHeightActivity extends BaseActivity { actionBar.setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.keyman_blue))); } - TextView adjustKeyboardHeightActivityTitle = (TextView) findViewById(R.id.bar_title); - - String titleStr = getString(R.string.adjust_keyboard_height); + TextView adjustKeyboardHeightActivityTitle = findViewById(R.id.bar_title); adjustKeyboardHeightActivityTitle.setTextColor(ContextCompat.getColor(this, R.color.ms_white)); - adjustKeyboardHeightActivityTitle.setText(titleStr); + adjustKeyboardHeightActivityTitle.setText(getString(R.string.adjust_keyboard_height)); + + sampleKeyboard = findViewById(R.id.sample_keyboard); + percentageDisplay = findViewById(R.id.keyboard_height_percentage); + View resizeHandle = findViewById(R.id.resize_handle); - sampleKeyboard = (ImageView) findViewById(R.id.sample_keyboard); layoutParams = sampleKeyboard.getLayoutParams(); - currentHeight = KMManager.getKeyboardHeight(context); - refreshSampleKeyboard(context); + currentHeight = KMManager.getKeyboardHeight(this); + refreshSampleKeyboard(); - resetButton = (Button) findViewById(R.id.reset_to_defaults); - resetButton.setOnClickListener(new View.OnClickListener() { - public void onClick(View v) { - // Clear the keyboard height preferences to reset - KMManager.applyKeyboardHeight(context, KMManager.KeyboardHeight_Reset); - - // Restore default height - currentHeight = KMManager.getKeyboardHeight(context); - refreshSampleKeyboard(context); - } + resetButton = findViewById(R.id.reset_to_defaults); + resetButton.setOnClickListener(v -> { + KMManager.applyKeyboardHeight(this, KMManager.KeyboardHeight_Reset); + currentHeight = KMManager.getKeyboardHeight(this); + refreshSampleKeyboard(); }); - sampleKeyboard.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View view, MotionEvent event) { - int y = (int)event.getY(); - - switch(event.getAction()) { - case MotionEvent.ACTION_MOVE: - // Update currentHeight as the user drags (moves) - // Increasing the keyboard height is a negative y - currentHeight -= y; - - // Apply lower and upper bounds on currentHeight - int minKeyboardHeight = KMManager.getKeyboardHeightMin(context); - int maxKeyboardHeight = KMManager.getKeyboardHeightMax(context); - currentHeight = Math.max(minKeyboardHeight, currentHeight); - currentHeight = Math.min(maxKeyboardHeight, currentHeight); - - refreshSampleKeyboard(context); - break; - case MotionEvent.ACTION_UP: - // Save the currentHeight when the user releases - KMManager.applyKeyboardHeight(context, currentHeight); - break; - } - return true; + View.OnTouchListener touchListener = (view, event) -> { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + view.setPressed(true); + break; + case MotionEvent.ACTION_MOVE: + int[] location = new int[2]; + sampleKeyboard.getLocationOnScreen(location); + int viewBottom = location[1] + sampleKeyboard.getHeight(); + int touchY = (int) event.getRawY(); + currentHeight = calculateKeyboardHeightFromTouch(touchY, viewBottom); + refreshSampleKeyboard(); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + view.setPressed(false); + KMManager.applyKeyboardHeight(this, currentHeight); + break; } - }); + return true; + }; + + resizeHandle.setOnTouchListener(touchListener); + sampleKeyboard.setOnTouchListener(touchListener); } /** - * Refresh the layout for the sample keyboard - * @param context + * Refresh the layout for the sample keyboard and update percentage display. */ - private void refreshSampleKeyboard(Context context) { - layoutParams.height = currentHeight; - sampleKeyboard.setLayoutParams(layoutParams); + private void refreshSampleKeyboard() { + if (layoutParams != null && sampleKeyboard != null) { + layoutParams.height = currentHeight; + sampleKeyboard.requestLayout(); + + if (percentageDisplay != null) { + percentageDisplay.setText(createLiveKeyboardHeightString()); + } + } + } + + /** + * Calculate percentage from height and default height. + * @param height Current height + * @param defaultHeight Default height + * @return Percentage rounded to nearest integer + */ + private static int calculatePercentage(int height, int defaultHeight) { + if (defaultHeight == 0 || height == 0) { + return 100; + } + return (int) Math.round((double) height / defaultHeight * 100); + } + + /** + * Create a string showing the keyboard height as percentages for both orientations. + * @param context Context + * @return String in format "100% Portrait | 100% Landscape" + */ + public static String createKeyboardHeightString(Context context) { + int portraitHeight = KMManager.getKeyboardHeight(context, Configuration.ORIENTATION_PORTRAIT); + int landscapeHeight = KMManager.getKeyboardHeight(context, Configuration.ORIENTATION_LANDSCAPE); + + int portraitDefault = KMManager.getDefaultKeyboardHeight(Configuration.ORIENTATION_PORTRAIT); + int landscapeDefault = KMManager.getDefaultKeyboardHeight(Configuration.ORIENTATION_LANDSCAPE); + + int portraitPercent = calculatePercentage(portraitHeight, portraitDefault); + int landscapePercent = calculatePercentage(landscapeHeight, landscapeDefault); + + return context.getString(R.string.keyboard_height_format, portraitPercent, landscapePercent); + } + + /** + * Calculate keyboard height from touch Y coordinate and clamp + * within min-max bounds. + * @param touchY Touch Y coordinate in screen coordinates + * @param viewBottom Bottom Y coordinate of keyboard view + * @return Keyboard height in pixels, clamped to min/max bounds + */ + private int calculateKeyboardHeightFromTouch(int touchY, int viewBottom) { + int height = viewBottom - touchY; + int minKeyboardHeight = KMManager.getKeyboardHeightMin(this); + int maxKeyboardHeight = KMManager.getKeyboardHeightMax(this); + return Math.max(minKeyboardHeight, Math.min(maxKeyboardHeight, height)); + } + + /** + * Create a string showing keyboard height percentages for both orientations, + * using the current (unsaved) height for the active orientation. + * @return String in format "100% Portrait | 100% Landscape" + */ + private String createLiveKeyboardHeightString() { + int currentOrientation = KMManager.getOrientation(this); + + int portraitDefault = KMManager.getDefaultKeyboardHeight(Configuration.ORIENTATION_PORTRAIT); + int landscapeDefault = KMManager.getDefaultKeyboardHeight(Configuration.ORIENTATION_LANDSCAPE); + + int portraitHeight; + int landscapeHeight; + if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) { + portraitHeight = currentHeight; + landscapeHeight = KMManager.getKeyboardHeight(this, Configuration.ORIENTATION_LANDSCAPE); + } else { + landscapeHeight = currentHeight; + portraitHeight = KMManager.getKeyboardHeight(this, Configuration.ORIENTATION_PORTRAIT); + } + + int portraitPercent = calculatePercentage(portraitHeight, portraitDefault); + int landscapePercent = calculatePercentage(landscapeHeight, landscapeDefault); + + return getString(R.string.keyboard_height_format, portraitPercent, landscapePercent); + } + + @Override + protected void onPause() { + super.onPause(); + // Ensure height is saved even if user switches tasks or navigates away abnormally + // This prevents the touch zone from being out of sync with the keyboard visual + if (currentHeight > 0) { + KMManager.applyKeyboardHeight(this, currentHeight); + } } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); + // IMPORTANT: Apply current height BEFORE switching orientations + // This ensures the old orientation's height is saved before loading the new one + if (currentHeight > 0) { + KMManager.applyKeyboardHeight(this, currentHeight); + } + layoutParams = sampleKeyboard.getLayoutParams(); - // When the user rotates the device, restore currentHeight + // When the user rotates the device, restore currentHeight for the new orientation currentHeight = KMManager.getKeyboardHeight(this); - refreshSampleKeyboard(this); + refreshSampleKeyboard(); } @Override diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java index 4629336635..3aa149f477 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java @@ -106,6 +106,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { adjustKeyboardHeight.setKey(AdjustKeyboardHeightActivity.adjustKeyboardHeightKey); adjustKeyboardHeight.setTitle(getString(R.string.adjust_keyboard_height)); adjustKeyboardHeight.setWidgetLayoutResource(R.layout.preference_height_icon_layout); + adjustKeyboardHeight.setSummary(AdjustKeyboardHeightActivity.createKeyboardHeightString(context)); Intent adjustKeyboardHeightIntent = new Intent(context, AdjustKeyboardHeightActivity.class); adjustKeyboardHeight.setIntent(adjustKeyboardHeightIntent); @@ -268,5 +269,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { // Update the language / keyboard count when we return to this menu from deeper levels. languagesPreference.setTitle(getInstalledLanguagesText()); + + adjustKeyboardHeight.setSummary(AdjustKeyboardHeightActivity.createKeyboardHeightString(context)); } } diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_arrow_down.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 0000000000..f8a99a03b9 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,10 @@ + + + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_arrow_up.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_arrow_up.xml new file mode 100644 index 0000000000..646c7b0a70 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_arrow_up.xml @@ -0,0 +1,10 @@ + + + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/resize_handle_background.xml b/android/KMAPro/kMAPro/src/main/res/drawable/resize_handle_background.xml new file mode 100644 index 0000000000..3f54ca25ca --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/resize_handle_background.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/resize_handle_bar.xml b/android/KMAPro/kMAPro/src/main/res/drawable/resize_handle_bar.xml new file mode 100644 index 0000000000..f6882731a1 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/resize_handle_bar.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml b/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml index 0cb7a262d2..8541398a57 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml @@ -10,32 +10,22 @@ - - + app:layout_constraintTop_toBottomOf="@id/adjust_instructions">