Compare commits

..

1 Commits

81 changed files with 1655 additions and 2722 deletions

3
.gitignore vendored
View File

@ -1,6 +1,5 @@
.zanata-cache/
_build
po/squeekboard.pot
po/*.mo
TAGS
tags
vgdump

View File

@ -4,6 +4,10 @@ stages:
- build
- test
.tags: &tags
tags:
- librem5
before_script:
- apt-get -y update
- apt-get -y install wget ca-certificates gnupg
@ -12,6 +16,7 @@ before_script:
- apt-get -y update
build_docs:
<<: *tags
stage: build
artifacts:
paths:
@ -20,11 +25,10 @@ build_docs:
- apt-get -y install python3-pip python3-sphinx
- pip3 install recommonmark
- ./doc/build.sh _build
except:
variables:
- $PKG_ONLY == "1"
build_meson:
tags:
- librem5
stage: build
artifacts:
paths:
@ -34,91 +38,115 @@ build_meson:
- apt-get -y build-dep .
- meson . _build/ -Ddepdatadir=/usr/share --werror
- ninja -C _build install
except:
variables:
- $PKG_ONLY == "1"
build_deb:
tags:
- librem5
stage: build
artifacts:
paths:
- '*.deb'
- "*.deb"
script:
- rm -f ../*.deb
- apt-get -y build-dep .
- apt-get -y install devscripts
- REV=$(git log -1 --format=%h)
- VER=$(dpkg-parsechangelog -SVersion)
- DEBFULLNAME="Librem5 CI"
- EMAIL="librem5-builds@lists.community.puri.sm"
- dch -v"$VER+librem5ci$CI_PIPELINE_ID.$REV" "$MSG"
- debuild -i -us -uc -b
- cp ../*.deb .
build_deb:arm64:
build_deb:amber:
image: pureos/amber
tags:
- aarch64
- librem5
stage: build
artifacts:
paths:
- '*.deb'
- "*.deb"
script:
- echo "deb http://ci.puri.sm/ scratch librem5" > /etc/apt/sources.list.d/ci.list
- apt-get -y update
- rm -f ../*.deb
- apt-get -y build-dep .
- apt-get -y install devscripts
- debuild -i -us -uc -b
- cp ../*.deb .
build_deb:buster:
image: "debian:buster"
tags:
- librem5
stage: build
artifacts:
paths:
- "*.deb"
script:
- echo "deb http://ci.puri.sm/ scratch librem5" > /etc/apt/sources.list.d/ci.list
- apt-get -y update
- rm -f ../*.deb
- apt-get -y build-dep .
- apt-get -y install devscripts
- debuild -i -us -uc -b
- cp ../*.deb .
build_deb:arm64:
tags:
- librem5:arm64
stage: build
artifacts:
paths:
- "*.deb"
script:
- rm -f ../*.deb
- apt-get -y build-dep .
- apt-get -y install devscripts
- REV=$(git log -1 --format=%h)
- VER=$(dpkg-parsechangelog -SVersion)
- DEBFULLNAME="Librem5 CI"
- EMAIL="librem5-builds@lists.community.puri.sm"
- dch -v"$VER+librem5ci$CI_PIPELINE_ID.$REV" "$MSG"
- debuild -i -us -uc -b
- cp ../*.deb .
build_deb:arm64_buster:
image: "debian:buster"
tags:
- librem5:arm64
stage: build
artifacts:
paths:
- "*.deb"
script:
- echo "deb http://ci.puri.sm/ scratch librem5" > /etc/apt/sources.list.d/ci.list
- apt-get -y update
- rm -f ../*.deb
- apt-get -y build-dep .
- apt-get -y install devscripts
- debuild -i -us -uc -b
- cp ../*.deb .
test_lintian:
<<: *tags
stage: test
needs:
- job: build_deb
artifacts: true
dependencies:
- build_deb
script:
- apt-get -y install lintian
- lintian *.deb
except:
variables:
- $PKG_ONLY == "1"
test:
tags:
- librem5
stage: test
needs:
- job: build_meson
artifacts: true
- build_meson
script:
- apt-get -y build-dep .
- apt-get -y install clang-tidy
- ninja -C _build test
- tools/style-check_build _build
except:
variables:
- $PKG_ONLY == "1"
test_style:
stage: test
needs: []
script:
- apt-get -y build-dep .
- tools/style-check_source
except:
variables:
- $PKG_ONLY == "1"
- cd _build
- clang-tidy --checks=-clang-diagnostic-missing-braces,readability-braces-around-statements, --warnings-as-errors=readability-braces-around-statements -extra-arg=-Wno-unknown-warning-option ../src/*.c ../eek/*.c ../eekboard/*.c
check_release:
<<: *tags
stage: test
needs: []
only:
refs:
- master
script:
- apt-get -y install git python3
- (head -n 1 ./debian/changelog && git tag) | ./debian/check_release.py
except:
variables:
- $PKG_ONLY == "1"

66
Cargo.lock generated
View File

@ -1,7 +1,5 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "atk"
version = "0.7.0"
@ -28,12 +26,6 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitflags"
version = "1.2.1"
@ -67,9 +59,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.72"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
[[package]]
name = "clap"
@ -265,22 +257,6 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "indexmap"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -289,9 +265,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.108"
version = "0.2.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
[[package]]
name = "linked-hash-map"
@ -344,24 +320,24 @@ dependencies = [
[[package]]
name = "pkg-config"
version = "0.3.22"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
[[package]]
name = "proc-macro2"
version = "1.0.32"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.10"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
@ -404,18 +380,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.130"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.130"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
dependencies = [
"proc-macro2",
"quote",
@ -424,21 +400,21 @@ dependencies = [
[[package]]
name = "serde_yaml"
version = "0.8.21"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af"
checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23"
dependencies = [
"dtoa",
"indexmap",
"linked-hash-map",
"serde",
"yaml-rust",
]
[[package]]
name = "syn"
version = "1.0.82"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
dependencies = [
"proc-macro2",
"quote",
@ -456,9 +432,9 @@ dependencies = [
[[package]]
name = "unicode-width"
version = "0.1.9"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]]
name = "unicode-xid"

View File

@ -23,14 +23,14 @@ rustc_less_1_36 = []
# Dependencies which don't change based on build flags
[dependencies.cairo-sys-rs]
version = "*"
version = ""
[dependencies.glib-sys]
version = "*"
version = ""
features = ["v2_44"]
[dependencies.gtk-sys]
version = "*"
version = ""
features = ["v3_22"]
[dependencies]

View File

@ -1,7 +1,7 @@
*squeekboard* - a Wayland on-screen keyboard
*squeekboard* - a Wayland virtual keyboard
========================================
*Squeekboard* is a keyboard-shaped input method supporting Wayland, built primarily for the *Librem 5* phone.
*Squeekboard* is a virtual keyboard supporting Wayland, built primarily for the *Librem 5* phone.
It squeaks because some Rust got inside.
@ -11,24 +11,20 @@ Features
### Present
- GTK3
- Custom keyboard layouts defined in yaml
- Input purpose dependent keyboard layouts
- Custom yaml-defined keyboards
- DBus interface to show and hide
- Use Wayland input method protocol to submit text
- Use Wayland input method protocol to show and hide
- Use Wayland virtual keyboard protocol
### Temporarily dropped
- A settings interface
### TODO
- Text prediction/correction
- Use preedit
- Submit actions like "next field" using a future Wayland protocol
- Use Wayland input method protocol
- Pick up DBus interface files from /usr/share
Creating layouts
-------------------
If you want to work on layouts, check out the [guide](doc/tutorial.md).
Building
--------
@ -39,7 +35,7 @@ See `.gitlab-ci.yml` or run `apt-get build-dep .`
### Build from git repo
```bash
$ git clone https://gitlab.gnome.org/World/Phosh/squeekboard.git
$ git clone https://source.puri.sm/Librem5/squeekboard.git
$ cd squeekboard
$ mkdir _build
$ meson _build/
@ -58,31 +54,18 @@ $ cd ../build/
$ src/squeekboard
```
Squeekboard's panel will appear whenever a compatible application requests an input method. Click a text field in any GTK application, like `python3 ./tools/entry.py`.
Squeekboard honors the gnome "screen-keyboard-enabled" setting. Either enable this through gnome-settings under accessibility or run:
```bash
$ gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true
```
Alternatively, force panel visibility manually with:
To make the keyboard show you can use either an application that does so automatically, like a text editor or `python3 ./tests/entry.py`, or you can manually trigger it with:
```bash
busctl call --user sm.puri.OSK0 /sm/puri/OSK0 sm.puri.OSK0 SetVisible b true
```
### What the compositor has to support
A compatible compositor has to support the protocols:
- layer-shell
- virtual-keyboard-v1
It's strongly recommended to support:
- input-method-v2
Developing
----------

View File

@ -1,6 +0,0 @@
/* Theme independent style */
sq_view.pin sq_button {
border-radius: 0px;
margin: 1px 1px 1px 1px;
}

View File

@ -1,84 +0,0 @@
# Armenian layout created by Norayr Chilingarian
# Yerevan
# Oct 2021
---
outlines:
default: { width: 35.33, height: 52 }
altline: { width: 52.67, height: 52 }
wide: { width: 32, height: 32 }
spaceline: { width: 142, height: 52 }
special: { width: 44, height: 52 }
views:
base:
- "՝ է թ փ ձ ջ ւ և ռ չ ճ ֊ ժ"
- "ք ո ե ր տ ը ւ ի օ պ խ ծ շ"
- "ա ս դ ֆ գ հ յ կ լ "
- "Shift_L զ ղ ց վ բ ն մ ՛ BackSpace"
- "show_numbers preferences space period Return"
upper:
- "՝ Է Թ Փ Ձ Ջ Ւ և Ռ Չ Ճ — Ժ"
- "Ք Ո Ե Ր Տ Ը Ւ Ի Օ Պ Խ Ծ Շ"
- Ս Դ Ֆ Գ Հ Յ Կ Լ ։"
- "Shift_L Զ Ղ Ց Վ Բ Ն Մ ՞ BackSpace"
- "show_numbers preferences space period Return"
numbers:
- "show_symbols , \" ' colon ; ! ? BackSpace"
- "ﬓ ﬔ ﬕ ﬖ ﬗ ՟ և"
- "1 2 3 4 5 6 7 8 9 0"
- "show_letters preferences space period Return"
symbols:
- "show_numbers_from_symbols \\ % < > = [ ] BackSpace"
- "* # $ / & - _ + ( )"
- "© ® £ € ¥ ^ ° @ { }"
- "~ ` | · √ π τ ÷ × ¶"
- "show_letters preferences space period Return"
buttons:
Shift_L:
action:
locking:
lock_view: "upper"
unlock_view: "base"
outline: "altline"
icon: "key-shift"
BackSpace:
outline: "altline"
icon: "edit-clear-symbolic"
action: "erase"
preferences:
action: "show_prefs"
outline: "special"
icon: "keyboard-mode-symbolic"
show_numbers:
action:
set_view: "numbers"
outline: "wide"
label: "123"
show_numbers_from_symbols:
action:
set_view: "numbers"
outline: "altline"
label: "123"
show_letters:
action:
set_view: "base"
outline: "wide"
label: "ԱԲԳ"
show_symbols:
action:
set_view: "symbols"
outline: "altline"
label: "*/="
period:
outline: "special"
text: "."
space:
outline: "spaceline"
text: " "
Return:
outline: "wide"
icon: "key-enter"
keysym: "Return"
colon:
text: ":"

View File

@ -1,84 +0,0 @@
# Armenian layout created by Norayr Chilingarian
# Yerevan
# Oct 2021
---
outlines:
default: { width: 35.33, height: 52 }
altline: { width: 52.67, height: 52 }
wide: { width: 32, height: 32 }
spaceline: { width: 142, height: 52 }
special: { width: 44, height: 52 }
views:
base:
- "՝ ֆ ձ ֊ , ։ ՞ ՛ ) օ է ղ"
- "ճ փ բ ս մ ո ւ կ ը թ ծ ց »"
- "ջ վ գ ե ա ն ի տ հ պ ր"
- "Shift_L ժ դ չ յ զ լ ք խ շ ռ BackSpace"
- "show_numbers preferences space period Return"
upper:
- "՜ Ֆ Ձ — $ … ՟ և ՚ ( Օ Է Ղ"
- "Ճ Փ Բ Ս Մ Ո Ւ Կ Ը Թ Ծ Ց «"
- "Ջ Վ Գ Ե Ա Ն Ի Տ Հ Պ Պ Ր"
- "Shift_L Ժ Դ Չ Յ Զ Լ Ք Խ Շ Ռ BackSpace"
- "show_numbers preferences space period Return"
numbers:
- "show_symbols , \" ' colon ; ! ? BackSpace"
- "ﬓ ﬔ ﬕ ﬖ ﬗ ՟ և"
- "1 2 3 4 5 6 7 8 9 0"
- "show_letters preferences space period Return"
symbols:
- "show_numbers_from_symbols \\ % < > = [ ] BackSpace"
- "* # $ / & - _ + ( )"
- "© ® £ € ¥ ^ ° @ { }"
- "~ ` | · √ π τ ÷ × ¶"
- "show_letters preferences space period Return"
buttons:
Shift_L:
action:
locking:
lock_view: "upper"
unlock_view: "base"
outline: "altline"
icon: "key-shift"
BackSpace:
outline: "altline"
icon: "edit-clear-symbolic"
action: "erase"
preferences:
action: "show_prefs"
outline: "special"
icon: "keyboard-mode-symbolic"
show_numbers:
action:
set_view: "numbers"
outline: "wide"
label: "123"
show_numbers_from_symbols:
action:
set_view: "numbers"
outline: "altline"
label: "123"
show_letters:
action:
set_view: "base"
outline: "wide"
label: "ԱԲԳ"
show_symbols:
action:
set_view: "symbols"
outline: "altline"
label: "*/="
period:
outline: "special"
text: "."
space:
outline: "spaceline"
text: " "
Return:
outline: "wide"
icon: "key-enter"
keysym: "Return"
colon:
text: ":"

View File

@ -1,73 +0,0 @@
# Maintained by Patrick Jörg <patrickjoerg@gmx.ch>. No Copyright, enjoy!
---
outlines:
default: { width: 35.33, height: 52 }
altline: { width: 48, height: 52 }
wide: { width: 59, height: 52 }
spaceline: { width: 70, height: 52 }
special: { width: 28, height: 52 }
views:
base:
- "q w e r t z u i o p ü"
- "a s d f g h j k l ö ä"
- "Shift_L y x c v b n m BackSpace"
- "show_numbers ? ! preferences ' space , . Return"
upper:
- "Q W E R T Z U I O P Ü"
- "A S D F G H J K L Ö Ä"
- "Shift_L Y X C V B N M BackSpace"
- "show_numbers - _ preferences \" space , . Return"
numbers:
- "1 2 3 4 5 6 7 8 9 0"
- "@ * + - = ( ) ~ < >"
- "show_symbols # & / \\ √ ; : BackSpace"
- "show_letters ? ! preferences _ space , . Return"
symbols:
- "€ $ £ ¥ % | § µ [ ]"
- "© ® § ` ^ { } · ¡ ¿"
- "show_numbers « » ÷ × “ ” „ BackSpace"
- "show_letters preferences - space , . Return"
buttons:
Shift_L:
action:
locking:
lock_view: "upper"
unlock_view: "base"
outline: "altline"
icon: "key-shift"
BackSpace:
outline: "altline"
icon: "edit-clear-symbolic"
action: erase
preferences:
action: "show_prefs"
outline: "special"
icon: "keyboard-mode-symbolic"
show_numbers:
action:
set_view: "numbers"
outline: "altline"
label: "123"
show_letters:
action:
set_view: "base"
outline: "altline"
label: "abc"
show_symbols:
action:
set_view: "symbols"
outline: "altline"
label: "*/="
space:
outline: "spaceline"
label: " "
text: " "
Return:
outline: "wide"
icon: "key-enter"
keysym: "Return"
"\"":
keysym: "quotedbl"

View File

@ -1,93 +0,0 @@
# Maintained by: Jordy Bossy <jordi@bossy.space>
# and Patrick Jörg <patrickjoerg@gmx.ch>. No Copyright, enjoy!
---
outlines:
default: { width: 35.33, height: 52 }
altline: { width: 48, height: 52 }
wide: { width: 59, height: 52 }
spaceline: { width: 70, height: 52 }
special: { width: 28, height: 52 }
views:
base:
- "q w e r t z u i o p ü"
- "a s d f g h j k l ö ä"
- "Shift_L y x c v b n m BackSpace"
- "show_numbers show_eschars preferences ' space , . Return"
upper:
- "Q W E R T Z U I O P Ü"
- "A S D F G H J K L Ö Ä"
- "Shift_L Y X C V B N M BackSpace"
- "show_numbers show_eschars preferences \" space , . Return"
numbers:
- "1 2 3 4 5 6 7 8 9 0"
- "@ * + - = ( ) ~ < > ? !"
- "show_symbols # & / \\ √ ; : BackSpace"
- "show_letters show_eschars preferences _ space , . Return"
symbols:
- "€ $ £ ¥ % | § µ [ ]"
- "© ® § ` ^ { } · ¡ ¿"
- "show_numbers « » ÷ × “ ” „ BackSpace"
- "show_letters show_eschars preferences - space , . Return"
eschars:
- "à â ç é è ê î ô ù û"
- "À Â Ç É È Ê Î Ô Ù Û"
- "show_numbers æ œ ä ë ï ö ü BackSpace"
- "show_letters_from_accents preferences ñ Ñ space ° ß Return"
buttons:
Shift_L:
action:
locking:
lock_view: "upper"
unlock_view: "base"
outline: "altline"
icon: "key-shift"
BackSpace:
outline: "altline"
icon: "edit-clear-symbolic"
action: erase
preferences:
action: "show_prefs"
outline: "special"
icon: "keyboard-mode-symbolic"
show_numbers:
action:
set_view: "numbers"
outline: "altline"
label: "123"
show_letters:
action:
set_view: "base"
outline: "altline"
label: "abc"
show_symbols:
action:
set_view: "symbols"
outline: "altline"
label: "*/="
show_eschars:
action:
locking:
lock_view: "eschars"
unlock_view: "base"
outline: "altline"
label: "âÂ"
show_letters_from_accents:
action:
locking:
lock_view: "eschars"
unlock_view: "base"
outline: "altline"
label: "âÂ"
space:
outline: "spaceline"
label: " "
text: " "
Return:
outline: "wide"
icon: "key-enter"
keysym: "Return"
"\"":
keysym: "quotedbl"

View File

@ -1,93 +0,0 @@
# Maintained by: Jordy Bossy <jordy@bossy.space>
# and Patrick Jörg <patrickjoerg@gmx.ch>. No Copyright, enjoy!
---
outlines:
default: { width: 48, height: 42 }
altline: { width: 81, height: 42 }
wide: { width: 108, height: 42 }
spaceline: { width: 216, height: 42 }
special: { width: 48, height: 42 }
views:
base:
- "q w e r t z u i o p ü"
- "a s d f g h j k l ö ä"
- "Shift_L y x c v b n m BackSpace"
- "show_numbers show_eschars preferences ' space , . Return"
upper:
- "Q W E R T Z U I O P Ü"
- "A S D F G H J K L Ö Ä"
- "Shift_L Y X C V B N M BackSpace"
- "show_numbers show_eschars preferences \" space , . Return"
numbers:
- "1 2 3 4 5 6 7 8 9 0"
- "@ * + - = ( ) ~ < > ? !"
- "show_symbols # & / \\ √ ; : BackSpace"
- "show_letters show_eschars preferences _ space , . Return"
symbols:
- "€ $ £ ¥ % | § µ [ ]"
- "© ® § ` ^ { } · ¡ ¿"
- "show_numbers « » ÷ × “ ” „ BackSpace"
- "show_letters show_eschars preferences - space , . Return"
eschars:
- "à â ç é è ê î ô ù û"
- "À Â Ç É È Ê Î Ô Ù Û"
- "show_numbers æ œ ä ë ï ö ü BackSpace"
- "show_letters_from_accents preferences ñ Ñ space ° ß Return"
buttons:
Shift_L:
action:
locking:
lock_view: "upper"
unlock_view: "base"
outline: "altline"
icon: "key-shift"
BackSpace:
outline: "altline"
icon: "edit-clear-symbolic"
action: "erase"
preferences:
action: "show_prefs"
outline: "special"
icon: "keyboard-mode-symbolic"
show_numbers:
action:
set_view: "numbers"
outline: "altline"
label: "123"
show_letters:
action:
set_view: "base"
outline: "altline"
label: "abc"
show_symbols:
action:
set_view: "symbols"
outline: "altline"
label: "*/="
show_eschars:
action:
locking:
lock_view: "eschars"
unlock_view: "base"
outline: "altline"
label: "äÄ"
show_letters_from_accents:
action:
locking:
lock_view: "eschars"
unlock_view: "base"
outline: "altline"
label: "âÂ"
space:
outline: "spaceline"
label: " "
text: " "
Return:
outline: "wide"
icon: "key-enter"
keysym: "Return"
"\"":
keysym: "quotedbl"

View File

@ -1,81 +0,0 @@
---
outlines:
default: { width: 35.33, height: 52 }
altline: { width: 52.67, height: 52 }
wide: { width: 62, height: 52 }
spaceline: { width: 106.67, height: 52 }
special: { width: 44, height: 52 }
views:
base:
- "q w e r t y u i o p"
- "a s d f g h j k l"
- "Shift_L z x c v b n m BackSpace"
- "show_numbers preferences space at period Return"
upper:
- "Q W E R T Y U I O P"
- "A S D F G H J K L"
- "Shift_L Z X C V B N M BackSpace"
- "show_numbers preferences space at period Return"
numbers:
- "1 2 3 4 5 6 7 8 9 0"
- "@ # $ % & - _ + ( )"
- "show_symbols , \" ' colon ; ! ? BackSpace"
- "show_letters preferences space at period Return"
symbols:
- "~ ` | · √ π τ ÷ × ¶"
- "© ® £ € ¥ ^ ° * { }"
- "show_numbers_from_symbols \\ / < > = [ ] BackSpace"
- "show_letters preferences space at period Return"
buttons:
Shift_L:
action:
locking:
lock_view: "upper"
unlock_view: "base"
outline: "altline"
icon: "key-shift"
BackSpace:
outline: "altline"
icon: "edit-clear-symbolic"
action: erase
at:
outline: "special"
text: "@"
preferences:
action: show_prefs
outline: "special"
icon: "keyboard-mode-symbolic"
show_numbers:
action:
set_view: "numbers"
outline: "wide"
label: "123"
show_numbers_from_symbols:
action:
set_view: "numbers"
outline: "altline"
label: "123"
show_letters:
action:
set_view: "base"
outline: "wide"
label: "ABC"
show_symbols:
action:
set_view: "symbols"
outline: "altline"
label: "*/="
period:
outline: "special"
text: "."
space:
outline: "spaceline"
text: " "
Return:
outline: "wide"
icon: "key-enter"
keysym: "Return"
colon:
text: ":"

View File

@ -1,20 +0,0 @@
---
margins: { top: 4, side: 0, bottom: 4 }
outlines:
default: { width: 120, height: 52 }
views:
base:
- "1 2 3"
- "4 5 6"
- "7 8 9"
- "BackSpace 0 Return"
buttons:
BackSpace:
icon: "edit-clear-symbolic"
action: erase
Return:
icon: "key-enter"
keysym: "Return"

View File

@ -1,81 +0,0 @@
---
outlines:
default: { width: 35.33, height: 52 }
altline: { width: 52.67, height: 52 }
wide: { width: 62, height: 52 }
spaceline: { width: 106.67, height: 52 }
special: { width: 44, height: 52 }
views:
base:
- "q w e r t y u i o p"
- "a s d f g h j k l"
- "Shift_L z x c v b n m BackSpace"
- "show_numbers preferences space slash period Return"
upper:
- "Q W E R T Y U I O P"
- "A S D F G H J K L"
- "Shift_L Z X C V B N M BackSpace"
- "show_numbers preferences space slash period Return"
numbers:
- "1 2 3 4 5 6 7 8 9 0"
- "@ # $ % & - _ + ( )"
- "show_symbols , \" ' colon ; ! ? BackSpace"
- "show_letters preferences space slash period Return"
symbols:
- "~ ` | · √ π τ ÷ × ¶"
- "© ® £ € ¥ ^ ° * { }"
- "show_numbers_from_symbols \\ / < > = [ ] BackSpace"
- "show_letters preferences space slash period Return"
buttons:
Shift_L:
action:
locking:
lock_view: "upper"
unlock_view: "base"
outline: "altline"
icon: "key-shift"
BackSpace:
outline: "altline"
icon: "edit-clear-symbolic"
action: erase
preferences:
action: show_prefs
outline: "special"
icon: "keyboard-mode-symbolic"
show_numbers:
action:
set_view: "numbers"
outline: "wide"
label: "123"
show_numbers_from_symbols:
action:
set_view: "numbers"
outline: "altline"
label: "123"
show_letters:
action:
set_view: "base"
outline: "wide"
label: "ABC"
show_symbols:
action:
set_view: "symbols"
outline: "altline"
label: "*/="
period:
outline: "special"
text: "."
slash:
outline: "special"
text: "/"
space:
outline: "spaceline"
text: " "
Return:
outline: "wide"
icon: "key-enter"
keysym: "Return"
colon:
text: ":"

22
data/langs/bg-BG.txt Normal file
View File

@ -0,0 +1,22 @@
ara Арабски
be Белгийски
bg Български
br Бразилски
cz Чешки
de Немски
dk Датски
es Испански
emoji Емоджи
fi Фински
fr Френски
gr Гръцки
it Италиански
jp Японски
no Норвежки
pl Полски
ru Руски
se Шведски
th Тайски
ua Украински
terminal Терминал
us Английски (САЩ)

21
data/langs/cs-CZ.txt Normal file
View File

@ -0,0 +1,21 @@
be Belgická
cz Česká
cz+qwerty Česká (QWERTY)
de Německá
dk Dánská
emoji Emoji
es Španělská
fi Finská
fr Francouzská
gr Řecká
it Italská
jp Japonská
jp+kana Japonská (Kana)
no Norská
pl Polská
ru Ruská
se Švédská
terminal Terminál
th Thajská
ua Ukrajinská
us Anglická (USA)

0
data/langs/de-DE.txt Normal file
View File

2
data/langs/en-US.txt Normal file
View File

@ -0,0 +1,2 @@
emoji Emoji
terminal Terminal

7
data/langs/es-ES.txt Normal file
View File

@ -0,0 +1,7 @@
us Inglés (EE.UU.)
de Alemán
el Griego
es Español
it Italiano
jp+kana Japonés (Kana)
no Noruego

2
data/langs/fa-IR.txt Normal file
View File

@ -0,0 +1,2 @@
emoji ایموجی
terminal ترمینال

18
data/langs/fur-IT.txt Normal file
View File

@ -0,0 +1,18 @@
be Belgjic
br Brasilian
de Todesc
dk Danês
es Spagnûl
fi Finlandês
fr Francês
it+fur Furlan
gr Grêc
it Talian
jp+kana Gjaponês (Kana)
no Norvegjês
pl Polac
ru Rus
se Svedês
terminal Terminâl
ua Ucrain
us American (USA)

19
data/langs/he-IL.txt Normal file
View File

@ -0,0 +1,19 @@
be בלגית
br פורטוגזית (ברזיל)
cz צ'כית
de גרמנית
dk דנית
es ספרדית
emoji אימוג'י
fi פינית
fr צרפתית
gr יוונית
il עברית
it איטלקית
no נורווגית
pl פולנית
ru רוסית
se שוודית
terminal טרמינל
ua אוקראינית
us אנגלית (ארה"ב)

0
data/langs/ja-JP.txt Normal file
View File

2
data/langs/pl-PL.txt Normal file
View File

@ -0,0 +1,2 @@
emoji emoji
terminal terminal

11
data/langs/ru-RU.txt Normal file
View File

@ -0,0 +1,11 @@
de Немецкий
es Испанский
fi Финский
gr Греческий
it Итальянский
no Норвежский
pl Польский
ru Русский
se Шведский
terminal Терминал
us Английский (США)

View File

@ -12,7 +12,7 @@ desktopconf.set('bindir', bindir)
desktop_file = 'sm.puri.Squeekboard.desktop'
i18n.merge_file(
i18n.merge_file('desktop',
input: configure_file(
input: desktop_file + '.in.in',
output: desktop_file + '.in',

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="app-menu">
<item>
<!-- translators: This is a emmoji keyboard layout -->
<attribute name="label" translatable="yes">Emoji</attribute>
<attribute name="action">layout</attribute>
<attribute name="target">emoji</attribute>
</item>
<item>
<!-- translators: This is a terminal keyboard layout -->
<attribute name="label" translatable="yes">Terminal</attribute>
<attribute name="action">layout</attribute>
<attribute name="target">terminal</attribute>
</item>
<section>
<item>
<attribute name="label" translatable="yes">Keyboard Settings</attribute>
<attribute name="action">settings</attribute>
</item>
</section>
</menu>
</interface>

19
data/popup.ui Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.16"/>
<object class="GtkPopoverMenu" id="main_menu">
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
</object>
<packing>
<property name="submenu">main</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

View File

@ -1,7 +1,7 @@
[Desktop Entry]
Name=Squeekboard
GenericName=On Screen Keyboard
Comment=An on screen virtual keyboard
GenericName=Squeekboard Virtual Keyboard
Comment=Virtual Keyboard
Exec=@bindir@/squeekboard
Terminal=false
Type=Application

View File

@ -1,10 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/sm/puri/squeekboard">
<file compressed="true">common.css</file>
<file compressed="true">style.css</file>
<file compressed="true">style-Adwaita:dark.css</file>
<file compressed="true" preprocess="xml-stripblanks">popover.ui</file>
<file compressed="true" preprocess="xml-stripblanks">popup.ui</file>
<file>icons/key-enter.svg</file>
<file>icons/key-shift.svg</file>
<file>icons/keyboard-mode-symbolic.svg</file>

View File

@ -58,5 +58,3 @@ sq_button.small {
background: #1c71d8;
border-color: #3584e4;
}
@import url("resource:///sm/puri/squeekboard/common.css");

View File

@ -61,5 +61,3 @@ sq_button.small {
background: mix(@theme_selected_bg_color, @theme_bg_color, 0.4); /*#1c71d8;*/
border-color: @borders; /*#3584e4;*/
}
@import url("resource:///sm/puri/squeekboard/common.css");

131
debian/changelog vendored
View File

@ -1,134 +1,3 @@
squeekboard (1.15.0-1) experimental; urgency=medium
[ Khaled Eldoheiri ]
* Introduce Arabic keyboard layout
[ Dorota Czaplejewicz ]
* Docs: Release procedure
* build: Fix "any" dependency versioning
* readme: Mention the layout guide
* dbus: Hint that maybe squeekboard is running
* readme: Change self-reference to repo to gnome
* docs: Move env vars section to debugging
* readme: Clarify basic running steps
* readme: Put emphasis on being an input method
* readme: Update features
* ci: Use cached artifacts in the test
* ci: Move release test to the start
* ci: Start lintian test right after deb build
* ci: Add git revision and CI pipeline number to .deb artifacts
* ci: Use bookworm image
* ci: Reformat yaml file
* ci: Include pre-build style check
* popover: Fix reentrancy problem
* submission: Wrap the structure in a safe wrapper
* util: Add ArcWrapped
* animation: Prototype a way of handling state and applying it separately
* state: Connect the animation state machine to the rest
* event_loop: Separate and use for physical keyboard presence
* Revert "util: Add ArcWrapped"
* Revert "ci: Use bookworm image"
* ci: Fix formatting
* ci: Make indentation close to original again
* cargo: version bump
[ Jordi ]
* Introduce Swiss French keyboard layout
* improve accents layout behavior and code cleaning
[ Plamen Stoev ]
* Rename bg to bg+phonetic
* Add 'bg' layout
* Translate more layout names in Bulgarian
[ William Wold ]
* Show error when Layer Shell is not supported
* Update entry.py file path in readme
* Update zwp_text_input_v3 (comment changes only)
* Update zwp_input_method_v2
[ Patrick Jörg ]
* Introduce Swiss German keyboard layout
* Introducing ch+de layout and modified ch.yaml fallback
* Added ch_wide
[ ZenWalker ]
* layersurface: avoid duplicate assignment
[ T. Zack Crawford ]
* Update tutorial.md to clarify steps in creating a custom layout
[ Guido Günther ]
* gitlab-ci: Adjust CI tags
* gitlab-ci: Drop build for outdated distributions
* data: Fix build with meson 0.60.0
* main: Remove trailing whitespace
* main: Honor --help and -h
* eek-renderer: Add log domain
* eek-renderer: Fix indentation
* eek-renderer: Honor theme changes (Closes: #296)
* main: Drop broken support G_BUS_TYPE_SYSTEM
* main: Avoid two error variables in the same function
* main: Use dark theme when run in a Phosh session (Closes: #242)
* gtk-keyboard: Don't set variable to NULL twice in a row
* renderer: Use `g_debug ()`
* main: Add debug flag to always show squeekboard on start
* renderer: Disconnect theme change signal handler
* main: Add debug flag to show GTK inspector
* README: Document SQUEEKBOARD_DEBUG environment variable
* Move style-check to separate script
* Honor input-purpose PIN
* entry: Use a scrolled window
* entry: Set a margin on the grids
* entry: Add a random text entry field
* imservice: Invoke eekboard_context_service_set_hint_purpose unconditionally
(Closes: #311)
* langs: Don't use empty translation file (Closes: #313)
* Initialize gettext
* Reuse the unused popover ui file for i18n (Closes: #315)
* po: Add German translation
* gresources: Drop popup.ui
* Revert "gresources: Drop popup.ui"
* gitlab-ci: Add PKG_ONLY
* layout: Drop trailing whitespace
* Use special pin keyboard
* layout: Keep content purpose around
* renderer: Set style class based on input purpose
* pin: Use less margin
* debian: Install translations
* debian: Switch to dh 13
* debian: Install desktop file
* eekboard-context-service: Don't translate property names
* server-context-servide: Don't translate application name
* data: Make generic name truly generic
* po: Add desktop file to translatable files
* Add URL and EMail keyboard variants for us (Closes: #65)
* gitignore: Drop zanata dir
* gitignore: Ignore generated po files
* popover: Move Emoji and Terminal to ui file
* popover: Add translator notes
* popover: Make the ui file match the code file name
* Remove emoji and terminal from translations
* popover: Don't complain about missing translations
* Drop custom translation handling
* Drop locale_config
* Remove custom translations
[ PhilProg ]
* Add documentation about compositors
[ Norayr Chilingarian ]
* armenian typewriter and phonetic keyboards.
* armenian layout also added to meson.build etc.
[ Arnaud Ferraris ]
* resources: add wide FR terminal keyboard
[ Sebastian Krzyszkowiak ]
* renderer: Take context scale into account when drawing icons (Closes: #139)
-- Dorota Czaplejewicz <dorota.czaplejewicz@puri.sm> Sun, 19 Dec 2021 14:11:06 +0000
squeekboard (1.14.0pureos0~amber0) amber-phone; urgency=medium
[ Dorota Czaplejewicz ]

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
10

3
debian/control vendored
View File

@ -4,7 +4,7 @@ Priority: optional
Maintainer: Dorota Czaplejewicz <dorota.czaplejewicz@puri.sm>
Build-Depends:
cargo,
debhelper-compat (= 13),
debhelper (>= 10),
meson (>=0.51.0),
ninja-build,
pkg-config,
@ -27,7 +27,6 @@ Build-Depends:
libwayland-dev (>= 1.16),
lsb-release,
python3,
python3-ruamel.yaml,
rustc,
wayland-protocols (>= 1.14),
Standards-Version: 4.1.3

View File

@ -1,4 +1,2 @@
tools/squeekboard-restyled usr/bin
usr/bin/squeekboard /usr/bin
usr/share/applications/
usr/share/locale/

View File

@ -90,15 +90,6 @@ Layouts can be selected using the GNOME Settings application.
$ gsettings set org.gnome.desktop.input-sources sources "[('xkb', 'us'), ('xkb', 'de')]"
```
### Environment Variables
Besides the environment variables supported by GTK and [GLib](https://docs.gtk.org/glib/running.html) applications
squeekboard honors the `SQUEEKBOARD_DEBUG` environment variable which can
contain a comma separated list of:
- `force-show` : Show squeekboard on startup independent of any gsettings or compositor requests
- `gtk-inspector`: Spawn [gtk-inspector](https://wiki.gnome.org/Projects/GTK/Inspector)
Coding
------
@ -120,16 +111,6 @@ User interface modules should:
### Style
Note that some portions, like the .gitlab-ci.yml file have accummulated enough style/whitespace conflicts that an enforced style checker is now applied.
To fix your contributions before submitting a change, use:
```
./tools/style-check_source --apply
```
* * *
Code submitted should roughly match the style of surrounding code. Things that will *not* be accepted are ones that often lead to errors:
- skipping brackets `{}` after every `if()`, `else`, and similar ([SCI CERT C: EXP19-C](https://wiki.sei.cmu.edu/confluence/display/c/EXP19-C.+Use+braces+for+the+body+of+an+if%2C+for%2C+or+while+statement))
@ -194,73 +175,14 @@ All Cargo dependencies must be selected in the version available in PureOS, and
Dependencies must be specified in `Cargo.toml` with 2 numbers: "major.minor". Since bugfix version number is meant to not affect the interface, this allows for safe updates.
Releases
----------
Squeekboard should get a new release every time something interesting comes in. Preferably when there are no known bugs too. People will rely on theose releases, after all.
### 1. Update `Cargo.toml`.
While the file is not actually used, it's a good idea to save the config in case some rare bug appears in dependencies.
`Cargo.lock` is used for remembering the revisions of all Rust dependencies. It must correspond to the default dependency configuration: without flags to use older or newer versions of dependencies. It should be updated often, preferably with each bugfix revision, and in a commit on its own:
```
cd squeekboard-build
cd build_dir
ninja ./Cargo.toml
sh /source_path/cargo.sh update
ninja test
cp ./Cargo.lock .../squeekboard-source
cp ./Cargo.lock /source_path/
```
Then commit the updated `Cargo.lock`.
### 2. Choose the version number
Squeekboard tries to use semantic versioning. It's 3 numbers separated by dots: "a.b.c". Releases which only fix bugs and nothing else are "a.b.c+1". Releases which add user-visible features in addition to bug fixes are "a.b+1.0". Releases which, in addition to the previous, change *the user contract* in incompatible ways are "a+1.0.0". "The user contract" means plugin APIs that are deemed stable, or the way language switching works, etc. In other words, incompatible changes to developers, or big changes to users bump "a" to the next natural number.
### 3. Update the number in `meson.build`
It's in the `project(version: xxx)` statement.
### 4. Update packaging
Packaging is in the `debian/` directory, and creates builds that can be quickly tested.
```
cd squeekboard-source
gbp dch --multimaint-merge --ignore-branch
```
Inspect `debian/changelog`, and make sure the first line contains the correct version number and suite. For example:
```
squeekboard (1.13.0pureos0~amber0) amber-phone; urgency=medium
```
Commit the updated `debian/changelog`. The commit message should contain the release version and a description of changes.
> Release 1.13.0 "Externality"
>
> Changes:
>
> - A system for latching and locking views
> ...
### 5. Create a signed tag for downstreams
The tag should be the version number with "v" in front of it. The tag message should be "squeekboard" and the tag name. Push it to the upstream repository:
```
git tag -s -u my_address@example.com v1.13.0 -m "squeekboard v1.13.0"
git push v1.13.0
```
### 5. Create a signed tag for packaging
Similar to the above, but format it for the PureOS downstream.
```
git tag -s -u my_address@example.com 'pureos/1.13.0pureos0_amber0' -m 'squeekboard 1.13.0pureos0_amber0'
git push 'pureos/1.13.0pureos0_amber0'
```
### 6. Rejoice
You released a new version of Squeekboard, and made it available on PureOS. Congratulations.
Since version 1.9.3, `Cargo.lock` is not actually used by the build system, due to `Cargo.toml` being generated at every build.

View File

@ -21,8 +21,7 @@ Creating a layout is easy. You don't need to recompile things, just edit and tes
### Creating the keyboard layout
* To be written: For the time being, take a look at [Using non-latin language on Librem 5](https://forums.puri.sm/t/using-non-latin-language-on-librem-5/7103/5)
* Select and enable the input source you would like to change from the Region & Language section of the device settings. Perhaps use "A user-defined custom layout" listed under Other.
* Find the correct name of the .yaml file associated with that input source. This can be found with the command
* The correct name of the .yaml file can be found with the command
```
gsettings get org.gnome.desktop.input-sources sources
@ -30,14 +29,12 @@ gsettings get org.gnome.desktop.input-sources sources
The output should be something like this: `[('xkb', 'us'), ('xkb', 'de')]`
So for example “de.yaml” would be the correct name for the German keyboard layout.
If the name of your layout is not translated correctly in the list, you can fix it by adding it and recompiling Squeekboard.
There is also associated files for that layout in landscape, terminal, number, emoji mode. They can be found at something analogous to `us_wide.yaml`, `terminal/us.yaml`, `number/us.yaml`, `emoji/us.yaml`, respectively.
If the name of your layout is not translated correctly in the list, you can fix it by adding it and recompiling Squeekboard.
### Testing the layout
Copy your yaml file to `~/.local/share/squeekboard/keyboards/` for testing purposes. From there it should get picked up by squeekboard automatically.
The yaml file will overwrite the default settings for that layout. If you want to go back to default, simply remove the file.
You can also use the `test_layout` tool from the -devel package to check it for errors:

View File

@ -316,6 +316,7 @@ eek_gtk_keyboard_dispose (GObject *object)
if (priv->renderer) {
eek_renderer_free(priv->renderer);
priv->renderer = NULL;
priv->renderer = NULL;
}
if (priv->keyboard) {

View File

@ -18,8 +18,6 @@
* 02110-1301 USA
*/
#define G_LOG_DOMAIN "squeekboard-eek-renderer"
#include <math.h>
#include <string.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
@ -56,13 +54,6 @@ render_outline (cairo_t *cr,
position.x, position.y, position.width, position.height);
}
float get_scale(cairo_t *cr) {
double width = 1;
double height = 1;
cairo_user_to_device_distance (cr, &width, &height);
return width;
}
/// Rust interface
void eek_render_button_in_context(uint32_t scale_factor,
cairo_t *cr,
@ -79,17 +70,16 @@ void eek_render_button_in_context(uint32_t scale_factor,
/* render icon (if any) */
if (icon_name) {
int context_scale = ceil (get_scale (cr));
cairo_surface_t *icon_surface =
eek_renderer_get_icon_surface (icon_name, 16, scale_factor * context_scale);
eek_renderer_get_icon_surface (icon_name, 16, scale_factor);
if (icon_surface) {
double width = cairo_image_surface_get_width (icon_surface);
double height = cairo_image_surface_get_height (icon_surface);
gint width = cairo_image_surface_get_width (icon_surface);
gint height = cairo_image_surface_get_height (icon_surface);
cairo_save (cr);
cairo_translate (cr,
(bounds.width - width / (scale_factor * context_scale)) / 2,
(bounds.height - height / (scale_factor * context_scale)) / 2);
(bounds.width - (double)width / scale_factor) / 2,
(bounds.height - (double)height / scale_factor) / 2);
cairo_rectangle (cr, 0, 0, width, height);
cairo_clip (cr);
/* Draw the shape of the icon using the foreground color */
@ -236,8 +226,6 @@ eek_renderer_free (EekRenderer *self)
g_object_unref(self->css_provider);
g_object_unref(self->view_context);
g_object_unref(self->button_context);
g_clear_signal_handler (&self->theme_name_id, gtk_settings_get_default());
// this is where renderer-specific surfaces would be released
free(self);
@ -269,49 +257,12 @@ static GType button_type(void) {
return type;
}
static void
on_gtk_theme_name_changed (GtkSettings *settings, gpointer foo, EekRenderer *self)
{
g_autofree char *name = NULL;
g_object_get (settings, "gtk-theme-name", &name, NULL);
g_debug ("GTK theme: %s", name);
gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (),
GTK_STYLE_PROVIDER (self->css_provider));
gtk_style_context_remove_provider (self->button_context,
GTK_STYLE_PROVIDER(self->css_provider));
gtk_style_context_remove_provider (self->view_context,
GTK_STYLE_PROVIDER(self->css_provider));
g_set_object (&self->css_provider, squeek_load_style());
gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
GTK_STYLE_PROVIDER (self->css_provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
gtk_style_context_add_provider (self->button_context,
GTK_STYLE_PROVIDER(self->css_provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
gtk_style_context_add_provider (self->view_context,
GTK_STYLE_PROVIDER(self->css_provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}
static void
renderer_init (EekRenderer *self)
{
self->pcontext = NULL;
self->scale_factor = 1;
GtkSettings *gtk_settings;
gtk_settings = gtk_settings_get_default ();
self->theme_name_id = g_signal_connect (gtk_settings, "notify::gtk-theme-name",
G_CALLBACK (on_gtk_theme_name_changed), self);
self->css_provider = squeek_load_style();
}
@ -323,7 +274,6 @@ eek_renderer_new (LevelKeyboard *keyboard,
renderer_init(renderer);
renderer->pcontext = pcontext;
g_object_ref (renderer->pcontext);
const char *purpose_class = "normal";
/* Create a style context for the layout */
GtkWidgetPath *path = gtk_widget_path_new();
@ -345,55 +295,6 @@ eek_renderer_new (LevelKeyboard *keyboard,
if (squeek_layout_get_kind(keyboard->layout) == ARRANGEMENT_KIND_WIDE) {
gtk_widget_path_iter_add_class(path, -1, "wide");
}
/* Add style classes based on purpose */
switch (squeek_layout_get_purpose (keyboard->layout)) {
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL:
purpose_class = "normal";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA:
purpose_class = "alpha";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS:
purpose_class = "digits";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER:
purpose_class = "number";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE:
purpose_class = "phone";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL:
purpose_class = "url";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL:
purpose_class = "email";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME:
purpose_class = "name";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD:
purpose_class = "password";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN:
purpose_class = "pin";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE:
purpose_class = "date";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME:
purpose_class = "time";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME:
purpose_class = "datetime";
break;
case ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL:
purpose_class = "terminal";
break;
default:
g_warning ("Unknown input purpose %d", squeek_layout_get_purpose(keyboard->layout));
}
gtk_widget_path_iter_add_class(path, -1, purpose_class);
gtk_widget_path_append_type(path, button_type());
renderer->button_context = gtk_style_context_new ();
gtk_style_context_set_path(renderer->button_context, path);

View File

@ -39,8 +39,6 @@ typedef struct EekRenderer
GtkStyleContext *button_context; // TODO: maybe move a copy to each button
/// Style class for rendering the view and button CSS.
gchar *extra_style; // owned
// Theme name change signal handler id
gulong theme_name_id;
// Mutable state
gint scale_factor; /* the outputs scale factor */

View File

@ -217,7 +217,7 @@ eekboard_context_service_class_init (EekboardContextServiceClass *klass)
* Emitted when @context is destroyed.
*/
signals[DESTROYED] =
g_signal_new ("destroyed",
g_signal_new (I_("destroyed"),
G_TYPE_FROM_CLASS(gobject_class),
G_SIGNAL_RUN_LAST,
0,

View File

@ -1,7 +1,7 @@
project(
'squeekboard',
'c', 'rust',
version: '1.15.0',
version: '1.14.0',
license: 'GPLv3',
meson_version: '>=0.51.0',
default_options: [
@ -36,6 +36,8 @@ add_project_arguments(
i18n = import('i18n')
conf_data = configuration_data()
if get_option('buildtype').startswith('debug')
add_project_arguments('-DDEBUG=1', language : 'c')
endif
@ -61,7 +63,6 @@ endif
prefix = get_option('prefix')
bindir = join_paths(prefix, get_option('bindir'))
datadir = join_paths(prefix, get_option('datadir'))
localedir = join_paths(prefix, get_option('localedir'))
desktopdir = join_paths(datadir, 'applications')
pkgdatadir = join_paths(datadir, meson.project_name())
if get_option('depdatadir') == ''
@ -71,10 +72,6 @@ else
endif
dbusdir = join_paths(depdatadir, 'dbus-1/interfaces')
conf_data = configuration_data()
conf_data.set_quoted('GETTEXT_PACKAGE', 'squeekboard')
conf_data.set_quoted('LOCALEDIR', localedir)
summary = [
'',
'------------------',
@ -117,7 +114,6 @@ cargo_script = find_program('cargo.sh')
cargo_build = find_program('cargo_build.py')
subdir('data')
subdir('po')
subdir('protocols')
subdir('src')
subdir('tools')

View File

@ -1 +0,0 @@
de

View File

@ -1,2 +0,0 @@
data/popup.ui
data/sm.puri.Squeekboard.desktop.in.in

View File

@ -1,22 +0,0 @@
# German translations for squeekboard package.
# Copyright (C) 2021 THE squeekboard'S COPYRIGHT HOLDER
# This file is distributed under the same license as the squeekboard package.
# Automatically generated, 2021.
#
msgid ""
msgstr ""
"Project-Id-Version: squeekboard\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-12-03 18:41+0100\n"
"PO-Revision-Date: 2021-12-03 18:41+0100\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=ASCII\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: data/popup.ui:15
msgid "Keyboard Settings"
msgstr "Tastatureinstellungen"

View File

@ -1,2 +0,0 @@
i18n = import('i18n')
i18n.gettext('squeekboard', preset : 'glib')

View File

@ -2,9 +2,6 @@
<protocol name="input_method_unstable_v2">
<copyright>
Copyright © 2008-2011 Kristian Høgsberg
Copyright © 2010-2011 Intel Corporation
Copyright © 2012-2013 Collabora, Ltd.
Copyright © 2012, 2013 Intel Corporation
Copyright © 2015, 2016 Jan Arne Petersen
Copyright © 2017, 2018 Red Hat, Inc.
@ -75,19 +72,18 @@
Notification that a text input focused on this seat requested the input
method to be activated.
This event serves the purpose of providing the compositor with an
active input method.
This request must be issued every time a text input requests an input
method.
This event resets all state associated with previous enable, disable,
surrounding_text, text_change_cause, and content_type events, as well
as the state associated with set_preedit_string, commit_string, and
delete_surrounding_text requests. In addition, it marks the
zwp_input_method_v2 object as active, and makes any existing
zwp_input_popup_surface_v2 objects visible.
This request resets all state associated with previous enable, disable,
set_surrounding_text, set_text_change_cause, set_content_type, and
set_cursor_rectangle requests, as well as the state associated with
preedit_string, commit_string, and delete_surrounding_text events. In
addition, it marks the input method object as active.
The surrounding_text, and content_type events must follow before the
next done event if the text input supports the respective
functionality.
The set_surrounding_text, set_content_type and set_cursor_rectangle
requests must follow before the next done event if the text input
supports the respective functionality.
State set with this event is double-buffered. It will get applied on
the next zwp_input_method_v2.done event, and stay valid until changed.
@ -96,12 +92,13 @@
<event name="deactivate">
<description summary="deactivate event">
Notification that no focused text input currently needs an active
input method on this seat.
Notification that this seat's current text input requested the input
method to be deactivated.
This event marks the zwp_input_method_v2 object as inactive. The
compositor must make all existing zwp_input_popup_surface_v2 objects
invisible until the next activate event.
This event mrks the zwp_input_method_v2 object as inactive.
When the seat has the keyboard capability the text-input focus follows
the keyboard focus.
State set with this event is double-buffered. It will get applied on
the next zwp_input_method_v2.done event, and stay valid until changed.
@ -110,7 +107,7 @@
<event name="surrounding_text">
<description summary="surrounding text event">
Updates the surrounding plain text around the cursor, excluding the
Sets the surrounding plain text around the cursor, excluding the
preedit text.
If any preedit text is present, it is replaced with the cursor for the
@ -128,7 +125,7 @@
buffer. If there is no selected text, anchor must be the same as
cursor.
If this event does not arrive before the first done event, the input
If this request does not arrive before the first done event, the input
method may assume that the text input does not support this
functionality and ignore following surrounding_text events.
@ -169,7 +166,7 @@
<event name="content_type">
<description summary="content purpose and hint">
Indicates the content type and hint for the current
zwp_input_method_v2 instance.
input_method_context instance.
Values set with this event are double-buffered. They will get applied
on the next zwp_input_method_v2.done event.
@ -216,14 +213,14 @@
4000 bytes.
Values set with this event are double-buffered. They must be applied
and reset to initial on the next zwp_text_input_v3.commit request.
and reset to initial on the next zwp_text_input_v3.done event.
The initial value of text is an empty string.
</description>
<arg name="text" type="string"/>
</request>
<request name="set_preedit_string">
<request name="preedit_string">
<description summary="pre-edit string">
Send the pre-edit string text to the application text input.
@ -278,7 +275,7 @@
<request name="commit">
<description summary="apply state">
Apply state changes from commit_string, set_preedit_string and
Apply state changes from commit_string, preedit_string and
delete_surrounding_text requests.
The state relating to these events is double-buffered, and each one
@ -297,10 +294,11 @@
The serial number reflects the last state of the zwp_input_method_v2
object known to the client. The value of the serial argument must be
equal to the number of done events already issued by that object. When
the compositor receives a commit request with a serial different than
the number of past done events, it must proceed as normal, except it
should not change the current state of the zwp_input_method_v2 object.
equal to the number of done events already issued on that object.
When the compositor receives a commit request with a serial different than
the number of past commit requests, it must proceed as normal, except
it should not change the current state of the zwp_input_method_v2
object.
</description>
<arg name="serial" type="uint"/>
</request>
@ -309,10 +307,6 @@
<description summary="create popup surface">
Creates a new zwp_input_popup_surface_v2 object wrapping a given
surface.
The surface gets assigned the "input_popup" role. If the surface
already has an assigned role, the compositor must issue a protocol
error.
</description>
<arg name="id" type="new_id" interface="zwp_input_popup_surface_v2"/>
<arg name="surface" type="object" interface="wl_surface"/>
@ -333,8 +327,7 @@
Releasing the resulting wl_keyboard object releases the grab.
</description>
<arg name="keyboard" type="new_id"
interface="zwp_input_method_keyboard_grab_v2"/>
<arg name="keyboard" type="new_id" interface="wl_keyboard"/>
</request>
<event name="unavailable">
@ -354,25 +347,15 @@
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the text input">
Destroys the zwp_text_input_v2 object and any associated child
objects, i.e. zwp_input_popup_surface_v2 and
zwp_input_method_keyboard_grab_v2.
</description>
</request>
<request name="destroy" type="destructor"/>
</interface>
<interface name="zwp_input_popup_surface_v2" version="1">
<description summary="popup surface">
This interface marks a surface as a popup for interacting with an input
method.
This surface is a popup for interacting with an input method.
The compositor should place it near the active text input area. It must
be visible if and only if the input method is in the active state.
The client must not destroy the underlying wl_surface while the
zwp_input_popup_surface_v2 object exists.
</description>
<event name="text_input_rectangle">
@ -392,75 +375,6 @@
<request name="destroy" type="destructor"/>
</interface>
<interface name="zwp_input_method_keyboard_grab_v2" version="1">
<!-- Closely follows wl_keyboard version 6 -->
<description summary="keyboard grab">
The zwp_input_method_keyboard_grab_v2 interface represents an exclusive
grab of the wl_keyboard interface associated with the seat.
</description>
<event name="keymap">
<description summary="keyboard mapping">
This event provides a file descriptor to the client which can be
memory-mapped to provide a keyboard mapping description.
</description>
<arg name="format" type="uint" enum="wl_keyboard.keymap_format"
summary="keymap format"/>
<arg name="fd" type="fd" summary="keymap file descriptor"/>
<arg name="size" type="uint" summary="keymap size, in bytes"/>
</event>
<event name="key">
<description summary="key event">
A key was pressed or released.
The time argument is a timestamp with millisecond granularity, with an
undefined base.
</description>
<arg name="serial" type="uint" summary="serial number of the key event"/>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="key" type="uint" summary="key that produced the event"/>
<arg name="state" type="uint" enum="wl_keyboard.key_state"
summary="physical state of the key"/>
</event>
<event name="modifiers">
<description summary="modifier and group state">
Notifies clients that the modifier and/or group state has changed, and
it should update its local state.
</description>
<arg name="serial" type="uint" summary="serial number of the modifiers event"/>
<arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
<arg name="mods_latched" type="uint" summary="latched modifiers"/>
<arg name="mods_locked" type="uint" summary="locked modifiers"/>
<arg name="group" type="uint" summary="keyboard layout"/>
</event>
<request name="release" type="destructor">
<description summary="release the grab object"/>
</request>
<event name="repeat_info">
<description summary="repeat rate and delay">
Informs the client about the keyboard's repeat rate and delay.
This event is sent as soon as the zwp_input_method_keyboard_grab_v2
object has been created, and is guaranteed to be received by the
client before any key press event.
Negative values for either rate or delay are illegal. A rate of zero
will disable any repeating (regardless of the value of delay).
This event can be sent later on as well with a new value if necessary,
so clients should continue listening for the event past the creation
of zwp_input_method_keyboard_grab_v2.
</description>
<arg name="rate" type="int"
summary="the rate of repeating keys in characters per second"/>
<arg name="delay" type="int"
summary="delay in milliseconds since key down until repeating starts"/>
</event>
</interface>
<interface name="zwp_input_method_manager_v2" version="1">
<description summary="input method manager">
The input method manager allows the client to become the input method on

View File

@ -94,12 +94,6 @@
zwp_text_input_v3.disable when there is no longer any input focus on
the current surface.
Clients must not enable more than one text input on the single seat
and should disable the current text input before enabling the new one.
At most one instance of text input may be in enabled state per instance,
Requests to enable the another text input when some text input is active
must be ignored by compositor.
This request resets all state associated with previous enable, disable,
set_surrounding_text, set_text_change_cause, set_content_type, and
set_cursor_rectangle requests, as well as the state associated with
@ -313,9 +307,6 @@
<description summary="enter event">
Notification that this seat's text-input focus is on a certain surface.
If client has created multiple text input objects, compositor must send
this event to all of them.
When the seat has the keyboard capability the text-input focus follows
the keyboard focus. This event sets the current surface for the
text-input object.
@ -330,9 +321,7 @@
set.
The leave notification clears the current surface. It is sent before
the enter notification for the new focus. After leave event, compositor
must ignore requests from any text input instances until next enter
event.
the enter notification for the new focus.
When the seat has the keyboard capability the text-input focus follows
the keyboard focus.

View File

@ -1,17 +0,0 @@
/* Copyright (C) 2020 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
/*! Animation details */
use std::time::Duration;
/// The keyboard should hide after this has elapsed to prevent flickering.
pub const HIDING_TIMEOUT: Duration = Duration::from_millis(200);
/// The outwardly visible state of visibility
#[derive(PartialEq, Debug, Clone)]
pub enum Outcome {
Visible,
Hidden,
}

View File

@ -2,9 +2,3 @@
* Autogenerated by the Meson build system.
* Do not edit, your changes will be lost.
*/
#pragma once
#mesondefine GETTEXT_PACKAGE
#mesondefine LOCALEDIR

View File

@ -61,7 +61,7 @@ pub mod c {
};
let (kind, layout) = load_layout_data_with_fallback(&name, type_, variant, overlay_str);
let layout = ::layout::Layout::new(layout, kind, variant);
let layout = ::layout::Layout::new(layout, kind);
Box::into_raw(Box::new(layout))
}
}
@ -166,13 +166,10 @@ fn get_directory_string(
let layout_purpose = match overlay {
None => match content_purpose {
ContentPurpose::Email => Special("email"),
ContentPurpose::Digits => Special("number"),
ContentPurpose::Number => Special("number"),
ContentPurpose::Digits => Special("number"),
ContentPurpose::Phone => Special("number"),
ContentPurpose::Pin => Special("pin"),
ContentPurpose::Terminal => Special("terminal"),
ContentPurpose::Url => Special("url"),
_ => Default,
},
Some(overlay) => Special(overlay),

View File

@ -19,9 +19,7 @@
#include "config.h"
#include "dbus.h"
#include "main.h"
#include <inttypes.h>
#include <stdio.h>
#include <gio/gio.h>
@ -46,6 +44,11 @@ dbus_handler_destroy(DBusHandler *service)
service->introspection_data = NULL;
}
if (service->context) {
g_signal_handlers_disconnect_by_data (service->context, service);
service->context = NULL;
}
free(service);
}
@ -54,25 +57,38 @@ handle_set_visible(SmPuriOSK0 *object, GDBusMethodInvocation *invocation,
gboolean arg_visible, gpointer user_data) {
DBusHandler *service = user_data;
if (service->context) {
if (arg_visible) {
squeek_state_send_force_visible (service->state_manager);
server_context_service_force_show_keyboard (service->context);
} else {
squeek_state_send_force_hidden(service->state_manager);
server_context_service_hide_keyboard (service->context);
}
}
sm_puri_osk0_complete_set_visible(object, invocation);
return TRUE;
}
static void on_visible(DBusHandler *service,
GParamSpec *pspec,
ServerContextService *context)
{
(void)pspec;
gboolean visible;
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (context));
g_object_get (context, "visible", &visible, NULL);
sm_puri_osk0_set_visible(service->dbus_interface, visible);
}
DBusHandler *
dbus_handler_new (GDBusConnection *connection,
const gchar *object_path,
struct squeek_state_manager *state_manager)
const gchar *object_path)
{
DBusHandler *self = calloc(1, sizeof(DBusHandler));
self->object_path = g_strdup(object_path);
self->connection = connection;
self->state_manager = state_manager;
self->dbus_interface = sm_puri_osk0_skeleton_new();
g_signal_connect(self->dbus_interface, "handle-set-visible",
@ -93,9 +109,16 @@ dbus_handler_new (GDBusConnection *connection,
return self;
}
// Exported to Rust
void dbus_handler_set_visible(DBusHandler *service,
uint8_t visible)
void
dbus_handler_set_ui_context(DBusHandler *service,
ServerContextService *context)
{
sm_puri_osk0_set_visible(service->dbus_interface, visible);
g_return_if_fail (!service->context);
service->context = context;
g_signal_connect_swapped (service->context,
"notify::visible",
G_CALLBACK(on_visible),
service);
}

View File

@ -19,20 +19,15 @@
#ifndef DBUS_H_
#define DBUS_H_ 1
#include "sm.puri.OSK0.h"
#include "server-context-service.h"
// From main.h
struct squeek_state_manager;
#include "sm.puri.OSK0.h"
G_BEGIN_DECLS
#define DBUS_SERVICE_PATH "/sm/puri/OSK0"
#define DBUS_SERVICE_INTERFACE "sm.puri.OSK0"
/// Two jobs: accept events, forwarding them to the visibility manager,
/// and get updated from inside to show internal state.
/// Updates are handled in the same loop as the UI.
/// See main.rs
typedef struct _DBusHandler
{
GDBusConnection *connection;
@ -41,14 +36,13 @@ typedef struct _DBusHandler
guint registration_id;
char *object_path;
/// Forward incoming events there
struct squeek_state_manager *state_manager; // shared reference
ServerContextService *context; // unowned reference
} DBusHandler;
DBusHandler * dbus_handler_new (GDBusConnection *connection,
const gchar *object_path,
struct squeek_state_manager *state_manager);
const gchar *object_path);
void dbus_handler_set_ui_context(DBusHandler *service,
ServerContextService *context);
void dbus_handler_destroy(DBusHandler*);
G_END_DECLS
#endif /* DBUS_H_ */

View File

@ -7,7 +7,7 @@ use ::action::{ Action, Modifier };
use ::keyboard;
use ::layout::{ Button, Label, LatchedState, Layout };
use ::layout::c::{ Bounds, EekGtkKeyboard, Point };
use ::submission::c::Submission as CSubmission;
use ::submission::Submission;
use glib::translate::FromGlibPtrNone;
use gtk::WidgetExt;
@ -76,11 +76,10 @@ mod c {
layout: *mut Layout,
renderer: EekRenderer,
cr: *mut cairo_sys::cairo_t,
submission: CSubmission,
submission: *const Submission,
) {
let layout = unsafe { &mut *layout };
let submission = submission.clone_ref();
let submission = submission.borrow();
let submission = unsafe { &*submission };
let cr = unsafe { cairo::Context::from_raw_none(cr) };
let active_modifiers = submission.get_active_modifiers();

View File

@ -1,141 +0,0 @@
/* Copyright (C) 2021 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
/*! This drives the loop from the `loop` module.
*
* The tracker loop needs to be driven somehow,
* and connected to the external world,
* both on the side of receiving and sending events.
*
* That's going to be implementation-dependent,
* connecting to some external mechanisms
* for time, messages, and threading/callbacks.
*
* This is the "imperative shell" part of the software,
* and no longer unit-testable.
*/
use crate::event_loop;
use crate::logging;
use crate::main::Commands;
use crate::state::{ Application, Event };
use glib;
use std::sync::mpsc;
use std::thread;
use std::time::Instant;
// Traits
use crate::logging::Warn;
type Sender = mpsc::Sender<Event>;
type UISender = glib::Sender<Commands>;
/// This loop driver spawns a new thread which updates the state in a loop,
/// in response to incoming events.
/// It sends outcomes to the glib main loop using a channel.
/// The outcomes are applied by the UI end of the channel in the `main` module.
// This could still be reasonably tested,
// by creating a glib::Sender and checking what messages it receives.
#[derive(Clone)]
pub struct Threaded {
thread: Sender,
}
impl Threaded {
pub fn new(ui: UISender, initial_state: Application) -> Self {
let (sender, receiver) = mpsc::channel();
let saved_sender = sender.clone();
thread::spawn(move || {
let mut state = event_loop::State::new(initial_state, Instant::now());
loop {
match receiver.recv() {
Ok(event) => {
state = Self::handle_loop_event(&sender, state, event, &ui);
},
Err(e) => {
logging::print(logging::Level::Bug, &format!("Senders hung up, aborting: {}", e));
return;
},
};
}
});
Self {
thread: saved_sender,
}
}
pub fn send(&self, event: Event) -> Result<(), mpsc::SendError<Event>> {
self.thread.send(event)
}
fn handle_loop_event(loop_sender: &Sender, state: event_loop::State, event: Event, ui: &UISender)
-> event_loop::State
{
let now = Instant::now();
let (new_state, commands) = event_loop::handle_event(state.clone(), event, now);
ui.send(commands)
.or_warn(&mut logging::Print, logging::Problem::Bug, "Can't send to UI");
if new_state.scheduled_wakeup != state.scheduled_wakeup {
if let Some(when) = new_state.scheduled_wakeup {
Self::schedule_timeout_wake(loop_sender, when);
}
}
new_state
}
fn schedule_timeout_wake(loop_sender: &Sender, when: Instant) {
let sender = loop_sender.clone();
thread::spawn(move || {
let now = Instant::now();
thread::sleep(when - now);
sender.send(Event::TimeoutReached(when))
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't wake visibility manager");
});
}
}
/// For calling in only
mod c {
use super::*;
use crate::state::Presence;
use crate::state::visibility;
use crate::util::c::Wrapped;
#[no_mangle]
pub extern "C"
fn squeek_state_send_force_visible(mgr: Wrapped<Threaded>) {
let sender = mgr.clone_ref();
let sender = sender.borrow();
sender.send(Event::Visibility(visibility::Event::ForceVisible))
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
}
#[no_mangle]
pub extern "C"
fn squeek_state_send_force_hidden(sender: Wrapped<Threaded>) {
let sender = sender.clone_ref();
let sender = sender.borrow();
sender.send(Event::Visibility(visibility::Event::ForceHidden))
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
}
#[no_mangle]
pub extern "C"
fn squeek_state_send_keyboard_present(sender: Wrapped<Threaded>, present: u32) {
let sender = sender.clone_ref();
let sender = sender.borrow();
let state =
if present == 0 { Presence::Missing }
else { Presence::Present };
sender.send(Event::PhysicalKeyboard(state))
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
}
}

View File

@ -1,186 +0,0 @@
/* Copyright (C) 2021 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
/*! The loop abstraction for driving state changes.
* It binds to the state tracker in `state::Application`,
* and actually gets driven by a driver in the `driver` module.
*
* * * *
*
* If we performed updates in a tight loop,
* the state tracker would have been all we need.
*
* ``
* loop {
* event = current_event()
* outcome = update_state(event)
* io.apply(outcome)
* }
* ``
*
* This is enough to process all events,
* and keep the window always in sync with the current state.
*
* However, we're trying to be conservative,
* and not waste time performing updates that don't change state,
* so we have to react to events that end up influencing the state.
*
* One complication from that is that animation steps
* are not a response to events coming from the owner of the loop,
* but are needed by the loop itself.
*
* This is where the rest of bugs hide:
* too few scheduled wakeups mean missed updates and wrong visible state.
* Too many wakeups can slow down the process, or make animation jittery.
* The loop iteration is kept as a pure function to stay testable.
*/
pub mod driver;
// This module is tightly coupled to the shape of data passed around in this project.
// That's not a problem as long as there's only one loop.
// They can still be abstracted into Traits,
// and the loop parametrized over them.
use crate::main::Commands;
use crate::state;
use crate::state::Event;
use std::cmp;
use std::time::{ Duration, Instant };
/// This keeps the state of the tracker loop between iterations
#[derive(Clone)]
struct State {
state: state::Application,
scheduled_wakeup: Option<Instant>,
last_update: Instant,
}
impl State {
fn new(initial_state: state::Application, now: Instant) -> Self {
Self {
state: initial_state,
scheduled_wakeup: None,
last_update: now,
}
}
}
/// A single iteration of the loop, updating its persistent state.
/// - updates tracker state,
/// - determines outcome,
/// - determines next scheduled animation wakeup,
/// and because this is a pure function, it's easily testable.
/// It returns the new state, and the message to send onwards.
fn handle_event(
mut loop_state: State,
event: Event,
now: Instant,
) -> (State, Commands) {
// Calculate changes to send to the consumer,
// based on publicly visible state.
// The internal state may change more often than the publicly visible one,
// so the resulting changes may be no-ops.
let old_state = loop_state.state.clone();
let last_update = loop_state.last_update;
loop_state.state = loop_state.state.apply_event(event.clone(), now);
loop_state.last_update = now;
let new_outcome = loop_state.state.get_outcome(now);
let commands = old_state.get_outcome(last_update)
.get_commands_to_reach(&new_outcome);
// Timeout events are special: they affect the scheduled timeout.
loop_state.scheduled_wakeup = match event {
Event::TimeoutReached(when) => {
if when > now {
// Special handling for scheduled events coming in early.
// Wait at least 10 ms to avoid Zeno's paradox.
// This is probably not needed though,
// if the `now` contains the desired time of the event.
// But then what about time "reversing"?
Some(cmp::max(
when,
now + Duration::from_millis(10),
))
} else {
// There's only one timeout in flight, and it's this one.
// It's about to complete, and then the tracker can be cleared.
// I'm not sure if this is strictly necessary.
None
}
},
_ => loop_state.scheduled_wakeup.clone(),
};
// Reschedule timeout if the new state calls for it.
let scheduled = &loop_state.scheduled_wakeup;
let desired = loop_state.state.get_next_wake(now);
loop_state.scheduled_wakeup = match (scheduled, desired) {
(&Some(scheduled), Some(next)) => {
if scheduled > next {
// State wants a wake to happen before the one which is already scheduled.
// The previous state is removed in order to only ever keep one in flight.
// That hopefully avoids pileups,
// e.g. because the system is busy
// and the user keeps doing something that queues more events.
Some(next)
} else {
// Not changing the case when the wanted wake is *after* scheduled,
// because wakes are not expensive as long as they don't pile up,
// and I can't see a pileup potential when it doesn't retrigger itself.
// Skipping an expected event is much more dangerous.
Some(scheduled)
}
},
(None, Some(next)) => Some(next),
// No need to change the unneeded wake - see above.
// (Some(_), None) => ...
(other, _) => other.clone(),
};
(loop_state, commands)
}
#[cfg(test)]
mod test {
use super::*;
use crate::animation;
use crate::imservice::{ ContentHint, ContentPurpose };
use crate::main::PanelCommand;
use crate::state::{ Application, InputMethod, InputMethodDetails, Presence, visibility };
fn imdetails_new() -> InputMethodDetails {
InputMethodDetails {
purpose: ContentPurpose::Normal,
hint: ContentHint::NONE,
}
}
#[test]
fn schedule_hide() {
let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
let mut now = start;
let state = Application {
im: InputMethod::Active(imdetails_new()),
physical_keyboard: Presence::Missing,
visibility_override: visibility::State::NotForced,
};
let l = State::new(state, now);
let (l, commands) = handle_event(l, InputMethod::InactiveSince(now).into(), now);
assert_eq!(commands.panel_visibility, Some(PanelCommand::Show));
assert_eq!(l.scheduled_wakeup, Some(now + animation::HIDING_TIMEOUT));
now += animation::HIDING_TIMEOUT;
let (l, commands) = handle_event(l, Event::TimeoutReached(now), now);
assert_eq!(commands.panel_visibility, Some(PanelCommand::Hide));
assert_eq!(l.scheduled_wakeup, None);
}
}

View File

@ -23,6 +23,22 @@ static const struct zwp_input_method_v2_listener input_method_listener = {
.unavailable = imservice_handle_unavailable,
};
struct submission* get_submission(struct zwp_input_method_manager_v2 *immanager,
struct zwp_virtual_keyboard_manager_v1 *vkmanager,
struct vis_manager *vis_manager,
struct wl_seat *seat,
EekboardContextService *state) {
struct zwp_input_method_v2 *im = NULL;
if (immanager) {
im = zwp_input_method_manager_v2_get_input_method(immanager, seat);
}
struct zwp_virtual_keyboard_v1 *vk = NULL;
if (vkmanager) {
vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmanager, seat);
}
return submission_new(im, vk, state, vis_manager);
}
/// Un-inlined
struct zwp_input_method_v2 *imservice_manager_get_input_method(struct zwp_input_method_manager_v2 *manager,
struct wl_seat *seat) {

View File

@ -8,11 +8,7 @@ use std::ffi::CString;
use std::fmt;
use std::num::Wrapping;
use std::string::String;
use std::time::Instant;
use crate::event_loop::driver;
use crate::state;
use crate::state::Event;
use ::logging;
use ::util::c::into_cstring;
@ -27,6 +23,9 @@ pub mod c {
use std::os::raw::{c_char, c_void};
pub use ::ui_manager::c::UIManager;
pub use ::submission::c::StateManager;
// The following defined in C
/// struct zwp_input_method_v2*
@ -41,6 +40,7 @@ pub mod c {
pub fn eek_input_method_commit_string(im: *mut InputMethod, text: *const c_char);
pub fn eek_input_method_delete_surrounding_text(im: *mut InputMethod, before: u32, after: u32);
pub fn eek_input_method_commit(im: *mut InputMethod, serial: u32);
fn eekboard_context_service_set_hint_purpose(state: *const StateManager, hint: u32, purpose: u32);
}
// The following defined in Rust. TODO: wrap naked pointers to Rust data inside RefCells to prevent multiple writers
@ -141,6 +141,7 @@ pub mod c {
im: *const InputMethod)
{
let imservice = check_imservice(imservice, im).unwrap();
let active_changed = imservice.current.active ^ imservice.pending.active;
imservice.current = imservice.pending.clone();
imservice.pending = IMProtocolState {
@ -149,7 +150,19 @@ pub mod c {
};
imservice.serial += Wrapping(1u32);
imservice.send_event();
if active_changed {
(imservice.active_callback)(imservice.current.active);
if imservice.current.active {
unsafe {
eekboard_context_service_set_hint_purpose(
imservice.state_manager,
imservice.current.content_hint.bits(),
imservice.current.content_purpose.clone() as u32,
);
}
}
}
}
// TODO: this is really untested
@ -165,7 +178,7 @@ pub mod c {
// the keyboard is already decommissioned
imservice.current.active = false;
imservice.send_event();
(imservice.active_callback)(imservice.current.active);
}
// FIXME: destroy and deallocate
@ -220,7 +233,7 @@ bitflags!{
/// use rs::imservice::ContentPurpose;
/// assert_eq!(ContentPurpose::Alpha as u32, 1);
/// ```
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Clone)]
pub enum ContentPurpose {
Normal = 0,
Alpha = 1,
@ -316,7 +329,9 @@ impl Default for IMProtocolState {
pub struct IMService {
/// Owned reference (still created and destroyed in C)
pub im: *mut c::InputMethod,
sender: driver::Threaded,
/// Unowned reference. Be careful, it's shared with C at large
state_manager: *const c::StateManager,
active_callback: Box<dyn Fn(bool)>,
pending: IMProtocolState,
current: IMProtocolState, // turn current into an idiomatic representation?
@ -332,13 +347,15 @@ pub enum SubmitError {
impl IMService {
pub fn new(
im: *mut c::InputMethod,
sender: driver::Threaded,
state_manager: *const c::StateManager,
active_callback: Box<dyn Fn(bool)>,
) -> Box<IMService> {
// IMService will be referenced to by C,
// so it needs to stay in the same place in memory via Box
let imservice = Box::new(IMService {
im,
sender,
active_callback,
state_manager,
pending: IMProtocolState::default(),
current: IMProtocolState::default(),
preedit_string: String::new(),
@ -398,21 +415,4 @@ impl IMService {
pub fn is_active(&self) -> bool {
self.current.active
}
fn send_event(&self) {
let state = &self.current;
let timestamp = Instant::now();
let message = if state.active {
state::InputMethod::Active(
state::InputMethodDetails {
hint: state.content_hint,
purpose: state.content_purpose,
}
)
} else {
state::InputMethod::InactiveSince(timestamp)
};
self.sender.send(Event::InputMethod(message))
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
}
}

View File

@ -33,7 +33,6 @@ struct transformation squeek_layout_calculate_transformation(
struct squeek_layout *squeek_load_layout(const char *name, uint32_t type, uint32_t variant_type, const char *overlay_name);
enum squeek_arrangement_kind squeek_layout_get_kind(const struct squeek_layout *);
uint32_t squeek_layout_get_purpose(const struct squeek_layout *);
void squeek_layout_free(struct squeek_layout*);
void squeek_layout_release(struct squeek_layout *layout,

View File

@ -32,8 +32,6 @@ use ::manager;
use ::submission::{ Submission, SubmitData, Timestamp };
use ::util::find_max_double;
use ::imservice::ContentPurpose;
// Traits
use std::borrow::Borrow;
use ::logging::Warn;
@ -44,7 +42,6 @@ pub mod c {
use gtk_sys;
use std::os::raw::c_void;
use crate::submission::c::Submission as CSubmission;
use std::ops::{ Add, Sub };
@ -186,13 +183,6 @@ pub mod c {
layout.kind.clone() as u32
}
#[no_mangle]
pub extern "C"
fn squeek_layout_get_purpose(layout: *const Layout) -> u32 {
let layout = unsafe { &*layout };
layout.purpose.clone() as u32
}
#[no_mangle]
pub extern "C"
fn squeek_layout_free(layout: *mut Layout) {
@ -208,7 +198,7 @@ pub mod c {
pub extern "C"
fn squeek_layout_release(
layout: *mut Layout,
submission: CSubmission,
submission: *mut Submission,
widget_to_layout: Transformation,
time: u32,
manager: manager::c::Manager,
@ -216,8 +206,7 @@ pub mod c {
) {
let time = Timestamp(time);
let layout = unsafe { &mut *layout };
let submission = submission.clone_ref();
let mut submission = submission.borrow_mut();
let submission = unsafe { &mut *submission };
let ui_backend = UIBackend {
widget_to_layout,
keyboard: ui_keyboard,
@ -229,7 +218,7 @@ pub mod c {
let key: &Rc<RefCell<KeyState>> = key.borrow();
seat::handle_release_key(
layout,
&mut submission,
submission,
Some(&ui_backend),
time,
Some(manager),
@ -244,19 +233,18 @@ pub mod c {
pub extern "C"
fn squeek_layout_release_all_only(
layout: *mut Layout,
submission: CSubmission,
submission: *mut Submission,
time: u32,
) {
let layout = unsafe { &mut *layout };
let submission = submission.clone_ref();
let mut submission = submission.borrow_mut();
let submission = unsafe { &mut *submission };
// The list must be copied,
// because it will be mutated in the loop
for key in layout.pressed_keys.clone() {
let key: &Rc<RefCell<KeyState>> = key.borrow();
seat::handle_release_key(
layout,
&mut submission,
submission,
None, // don't update UI
Timestamp(time),
None, // don't switch layouts
@ -269,15 +257,14 @@ pub mod c {
pub extern "C"
fn squeek_layout_depress(
layout: *mut Layout,
submission: CSubmission,
submission: *mut Submission,
x_widget: f64, y_widget: f64,
widget_to_layout: Transformation,
time: u32,
ui_keyboard: EekGtkKeyboard,
) {
let layout = unsafe { &mut *layout };
let submission = submission.clone_ref();
let mut submission = submission.borrow_mut();
let submission = unsafe { &mut *submission };
let point = widget_to_layout.forward(
Point { x: x_widget, y: y_widget }
);
@ -288,7 +275,7 @@ pub mod c {
if let Some(state) = state {
seat::handle_press_key(
layout,
&mut submission,
submission,
Timestamp(time),
&state,
);
@ -307,7 +294,7 @@ pub mod c {
pub extern "C"
fn squeek_layout_drag(
layout: *mut Layout,
submission: CSubmission,
submission: *mut Submission,
x_widget: f64, y_widget: f64,
widget_to_layout: Transformation,
time: u32,
@ -316,8 +303,7 @@ pub mod c {
) {
let time = Timestamp(time);
let layout = unsafe { &mut *layout };
let submission = submission.clone_ref();
let mut submission = submission.borrow_mut();
let submission = unsafe { &mut *submission };
let ui_backend = UIBackend {
widget_to_layout,
keyboard: ui_keyboard,
@ -345,7 +331,7 @@ pub mod c {
} else {
seat::handle_release_key(
layout,
&mut submission,
submission,
Some(&ui_backend),
time,
Some(manager),
@ -356,7 +342,7 @@ pub mod c {
if !found {
seat::handle_press_key(
layout,
&mut submission,
submission,
time,
&state,
);
@ -370,7 +356,7 @@ pub mod c {
let key: &Rc<RefCell<KeyState>> = wrapped_key.borrow();
seat::handle_release_key(
layout,
&mut submission,
submission,
Some(&ui_backend),
time,
Some(manager),
@ -641,7 +627,6 @@ pub enum LatchedState {
pub struct Layout {
pub margins: Margins,
pub kind: ArrangementKind,
pub purpose: ContentPurpose,
pub current_view: String,
// If current view is latched,
@ -691,7 +676,7 @@ impl fmt::Display for NoSuchView {
// The usage of &mut on Rc<RefCell<KeyState>> doesn't mean anything special.
// Cloning could also be used.
impl Layout {
pub fn new(data: LayoutData, kind: ArrangementKind, purpose: ContentPurpose) -> Layout {
pub fn new(data: LayoutData, kind: ArrangementKind) -> Layout {
Layout {
kind,
current_view: "base".to_owned(),
@ -700,7 +685,6 @@ impl Layout {
keymaps: data.keymaps,
pressed_keys: HashSet::new(),
margins: data.margins,
purpose,
}
}
@ -1210,7 +1194,6 @@ mod test {
"base".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
"locked".into() => (c::Point { x: 0.0, y: 0.0 }, view),
},
purpose: ContentPurpose::Normal,
};
// Basic cycle
@ -1287,7 +1270,6 @@ mod test {
"locked".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
"unlocked".into() => (c::Point { x: 0.0, y: 0.0 }, view),
},
purpose: ContentPurpose::Normal,
};
layout.apply_view_transition(&switch);
@ -1354,7 +1336,6 @@ mod test {
"locked".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
"ĄĘ".into() => (c::Point { x: 0.0, y: 0.0 }, view),
},
purpose: ContentPurpose::Normal,
};
// Latch twice, then Ąto-unlatch across 2 levels
@ -1455,7 +1436,6 @@ mod test {
views: hashmap! {
String::new() => (c::Point { x: 0.0, y: 0.0 }, view),
},
purpose: ContentPurpose::Normal,
};
assert_eq!(
layout.calculate_inner_size(),

View File

@ -19,21 +19,18 @@ extern crate xkbcommon;
mod logging;
mod action;
mod animation;
pub mod data;
mod drawing;
mod event_loop;
pub mod float_ord;
pub mod imservice;
mod keyboard;
mod layout;
mod locale;
mod main;
mod locale_config;
mod manager;
mod outputs;
mod popover;
mod resources;
mod state;
mod style;
mod submission;
pub mod tests;

View File

@ -88,6 +88,15 @@ impl Drop for XkbInfo {
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Translation<'a>(pub &'a str);
impl<'a> Translation<'a> {
pub fn to_owned(&'a self) -> OwnedTranslation {
OwnedTranslation(self.0.to_owned())
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct OwnedTranslation(pub String);

535
src/locale_config.rs Normal file
View File

@ -0,0 +1,535 @@
/*! Locale detection and management.
* Based on https://github.com/rust-locale/locale_config
*
* Ready for deletion/replacement once Debian starts packaging this,
* although this version doesn't need lazy_static.
*
* Copyright (c) 20162019 Jan Hudec <bulb@ucw.cz>
Copyright (c) 2016 A.J. Gardner <aaron.j.gardner@gmail.com>
Copyright (c) 2019, Bastien Orivel <eijebong@bananium.fr>
Copyright (c) 2019, Igor Gnatenko <i.gnatenko.brain@gmail.com>
Copyright (c) 2019, Sophie Tauchert <999eagle@999eagle.moe>
*/
use regex::Regex;
use std::borrow::Cow;
use std::env;
/// Errors that may be returned by `locale_config`.
#[derive(Copy,Clone,Debug,PartialEq,Eq)]
pub enum Error {
/// Provided definition was not well formed.
///
/// This is returned when provided configuration string does not match even the rather loose
/// definition for language range from [RFC4647] or the composition format used by `Locale`.
///
/// [RFC4647]: https://www.rfc-editor.org/rfc/rfc4647.txt
NotWellFormed,
/// Placeholder for adding more errors in future. **Do not match!**.
__NonExhaustive,
}
impl ::std::fmt::Display for Error {
fn fmt(&self, out: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
out.write_str(match self {
&Error::NotWellFormed => "Language tag is not well-formed.",
// this is exception: here we do want exhaustive match so we don't publish version with
// missing descriptions by mistake.
&Error::__NonExhaustive => panic!("Placeholder error must not be instantiated!"),
})
}
}
/// Convenience Result alias.
type Result<T> = ::std::result::Result<T, Error>;
/// Iterator over `LanguageRange`s for specific category in a `Locale`
///
/// Returns `LanguageRange`s in the `Locale` that are applicable to provided category. The tags
/// are returned in order of preference, which means the category-specific ones first and then
/// the generic ones.
///
/// The iterator is guaranteed to return at least one value.
pub struct TagsFor<'a, 'c> {
src: &'a str,
tags: std::str::Split<'a, &'static str>,
category: Option<&'c str>,
}
impl<'a, 'c> Iterator for TagsFor<'a, 'c> {
type Item = LanguageRange<'a>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(cat) = self.category {
while let Some(s) = self.tags.next() {
if s.starts_with(cat) && s[cat.len()..].starts_with("=") {
return Some(
LanguageRange { language: Cow::Borrowed(&s[cat.len()+1..]) });
}
}
self.category = None;
self.tags = self.src.split(",");
}
while let Some(s) = self.tags.next() {
if s.find('=').is_none() {
return Some(
LanguageRange{ language: Cow::Borrowed(s) });
}
}
return None;
}
}
/// Language and culture identifier.
///
/// This object holds a [RFC4647] extended language range.
///
/// The internal data may be owned or shared from object with lifetime `'a`. The lifetime can be
/// extended using the `into_static()` method, which internally clones the data as needed.
///
/// # Syntax
///
/// The range is composed of `-`-separated alphanumeric subtags, possibly replaced by `*`s. It
/// might be empty.
///
/// In agreement with [RFC4647], this object only requires that the tag matches:
///
/// ```ebnf
/// language_tag = (alpha{1,8} | "*")
/// ("-" (alphanum{1,8} | "*"))*
/// ```
///
/// The exact interpretation is up to the downstream localization provider, but it expected that
/// it will be matched against a normalized [RFC5646] language tag, which has the structure:
///
/// ```ebnf
/// language_tag = language
/// ("-" script)?
/// ("-" region)?
/// ("-" variant)*
/// ("-" extension)*
/// ("-" private)?
///
/// language = alpha{2,3} ("-" alpha{3}){0,3}
///
/// script = aplha{4}
///
/// region = alpha{2}
/// | digit{3}
///
/// variant = alphanum{5,8}
/// | digit alphanum{3}
///
/// extension = [0-9a-wyz] ("-" alphanum{2,8})+
///
/// private = "x" ("-" alphanum{1,8})+
/// ```
///
/// * `language` is an [ISO639] 2-letter or, where not defined, 3-letter code. A code for
/// macro-language might be followed by code of specific dialect.
/// * `script` is an [ISO15924] 4-letter code.
/// * `region` is either an [ISO3166] 2-letter code or, for areas other than countries, [UN M.49]
/// 3-digit numeric code.
/// * `variant` is a string indicating variant of the language.
/// * `extension` and `private` define additional options. The private part has same structure as
/// the Unicode [`-u-` extension][u_ext]. Available options are documented for the facets that
/// use them.
///
/// The values obtained by inspecting the system are normalized according to those rules.
///
/// The content will be case-normalized as recommended in [RFC5646] §2.1.1, namely:
///
/// * `language` is written in lowercase,
/// * `script` is written with first capital,
/// * `country` is written in uppercase and
/// * all other subtags are written in lowercase.
///
/// When detecting system configuration, additional options that may be generated under the
/// [`-u-` extension][u_ext] currently are:
///
/// * `cf` — Currency format (`account` for parenthesized negative values, `standard` for minus
/// sign).
/// * `fw` — First day of week (`mon` to `sun`).
/// * `hc` — Hour cycle (`h12` for 112, `h23` for 023).
/// * `ms` — Measurement system (`metric` or `ussystem`).
/// * `nu` — Numbering system—only decimal systems are currently used.
/// * `va` — Variant when locale is specified in Unix format and the tag after `@` does not
/// correspond to any variant defined in [Language subtag registry].
///
/// And under the `-x-` extension, following options are defined:
///
/// * `df` — Date format:
///
/// * `iso`: Short date should be in ISO format of `yyyy-MM-dd`.
///
/// For example `-df-iso`.
///
/// * `dm` — Decimal separator for monetary:
///
/// Followed by one or more Unicode codepoints in hexadecimal. For example `-dm-002d` means to
/// use comma.
///
/// * `ds` — Decimal separator for numbers:
///
/// Followed by one or more Unicode codepoints in hexadecimal. For example `-ds-002d` means to
/// use comma.
///
/// * `gm` — Group (thousand) separator for monetary:
///
/// Followed by one or more Unicode codepoints in hexadecimal. For example `-dm-00a0` means to
/// use non-breaking space.
///
/// * `gs` — Group (thousand) separator for numbers:
///
/// Followed by one or more Unicode codepoints in hexadecimal. For example `-ds-00a0` means to
/// use non-breaking space.
///
/// * `ls` — List separator:
///
/// Followed by one or more Unicode codepoints in hexadecimal. For example, `-ds-003b` means to
/// use a semicolon.
///
/// [RFC5646]: https://www.rfc-editor.org/rfc/rfc5646.txt
/// [RFC4647]: https://www.rfc-editor.org/rfc/rfc4647.txt
/// [ISO639]: https://en.wikipedia.org/wiki/ISO_639
/// [ISO15924]: https://en.wikipedia.org/wiki/ISO_15924
/// [ISO3166]: https://en.wikipedia.org/wiki/ISO_3166
/// [UN M.49]: https://en.wikipedia.org/wiki/UN_M.49
/// [u_ext]: http://www.unicode.org/reports/tr35/#u_Extension
/// [Language subtag registry]: https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
#[derive(Clone,Debug,Eq,Hash,PartialEq)]
pub struct LanguageRange<'a> {
language: Cow<'a, str>
}
impl<'a> LanguageRange<'a> {
/// Return LanguageRange for the invariant locale.
///
/// Invariant language is identified simply by empty string.
pub fn invariant() -> LanguageRange<'static> {
LanguageRange { language: Cow::Borrowed("") }
}
/// Create language tag from Unix/Linux/GNU locale tag.
///
/// Unix locale tags have the form
///
/// > *language* [ `_` *region* ] [ `.` *encoding* ] [ `@` *variant* ]
///
/// The *language* and *region* have the same format as RFC5646. *Encoding* is not relevant
/// here, since Rust always uses Utf-8. That leaves *variant*, which is unfortunately rather
/// free-form. So this function will translate known variants to corresponding RFC5646 subtags
/// and represent anything else with Unicode POSIX variant (`-u-va-`) extension.
///
/// Note: This function is public here for benefit of applications that may come across this
/// kind of tags from other sources than system configuration.
pub fn from_unix(s: &str) -> Result<LanguageRange<'static>> {
let unix_tag_regex = Regex::new(r"(?ix) ^
(?P<language> [[:alpha:]]{2,3} )
(?: _ (?P<region> [[:alpha:]]{2} | [[:digit:]]{3} ))?
(?: \. (?P<encoding> [0-9a-zA-Z-]{1,20} ))?
(?: @ (?P<variant> [[:alnum:]]{1,20} ))?
$ ").unwrap();
let unix_invariant_regex = Regex::new(r"(?ix) ^
(?: c | posix )
(?: \. (?: [0-9a-zA-Z-]{1,20} ))?
$ ").unwrap();
if let Some(caps) = unix_tag_regex.captures(s) {
let src_variant = caps.name("variant").map(|m| m.as_str()).unwrap_or("").to_ascii_lowercase();
let mut res = caps.name("language").map(|m| m.as_str()).unwrap().to_ascii_lowercase();
let region = caps.name("region").map(|m| m.as_str()).unwrap_or("");
let mut script = "";
let mut variant = "";
let mut uvariant = "";
match src_variant.as_ref() {
// Variants seen in the wild in GNU LibC (via http://lh.2xlibre.net/) or in Debian
// GNU/Linux Stretch system. Treatment of things not found in RFC5646 subtag registry
// (http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry)
// or CLDR according to notes at https://wiki.openoffice.org/wiki/LocaleMapping.
// Dialects:
// aa_ER@saaho - NOTE: Can't be found under that name in RFC5646 subtag registry,
// but there is language Saho with code ssy, which is likely that thing.
"saaho" if res == "aa" => res = String::from("ssy"),
// Scripts:
// @arabic
"arabic" => script = "Arab",
// @cyrillic
"cyrl" => script = "Cyrl",
"cyrillic" => script = "Cyrl",
// @devanagari
"devanagari" => script = "Deva",
// @hebrew
"hebrew" => script = "Hebr",
// tt@iqtelif
// Neither RFC5646 subtag registry nor CLDR knows anything about this, but as best
// as I can tell it is Tatar name for Latin (default is Cyrillic).
"iqtelif" => script = "Latn",
// @Latn
"latn" => script = "Latn",
// @latin
"latin" => script = "Latn",
// en@shaw
"shaw" => script = "Shaw",
// Variants:
// sr@ijekavianlatin
"ijekavianlatin" => {
script = "Latn";
variant = "ijekavsk";
},
// sr@ije
"ije" => variant = "ijekavsk",
// sr@ijekavian
"ijekavian" => variant = "ijekavsk",
// ca@valencia
"valencia" => variant = "valencia",
// Currencies:
// @euro - NOTE: We follow suite of Java and Openoffice and ignore it, because it
// is default for all locales where it sometimes appears now, and because we use
// explicit currency in monetary formatting anyway.
"euro" => {},
// Collation:
// gez@abegede - NOTE: This is collation, but CLDR does not have any code for it,
// so we for the moment leave it fall through as -u-va- instead of -u-co-.
// Anything else:
// en@boldquot, en@quot, en@piglatin - just randomish stuff
// @cjknarrow - beware, it's gonna end up as -u-va-cjknarro due to lenght limit
s if s.len() <= 8 => uvariant = &*s,
s => uvariant = &s[0..8], // the subtags are limited to 8 chars, but some are longer
};
if script != "" {
res.push('-');
res.push_str(script);
}
if region != "" {
res.push('-');
res.push_str(&*region.to_ascii_uppercase());
}
if variant != "" {
res.push('-');
res.push_str(variant);
}
if uvariant != "" {
res.push_str("-u-va-");
res.push_str(uvariant);
}
return Ok(LanguageRange {
language: Cow::Owned(res)
});
} else if unix_invariant_regex.is_match(s) {
return Ok(LanguageRange::invariant())
} else {
return Err(Error::NotWellFormed);
}
}
}
impl<'a> AsRef<str> for LanguageRange<'a> {
fn as_ref(&self) -> &str {
self.language.as_ref()
}
}
/// Locale configuration.
///
/// Users may accept several languages in some order of preference and may want to use rules from
/// different culture for some particular aspect of the program behaviour, and operating systems
/// allow them to specify this (to various extent).
///
/// The `Locale` objects represent the user configuration. They contain:
///
/// - The primary `LanguageRange`.
/// - Optional category-specific overrides.
/// - Optional fallbacks in case data (usually translations) for the primary language are not
/// available.
///
/// The set of categories is open-ended. The `locale` crate uses five well-known categories
/// `messages`, `numeric`, `time`, `collate` and `monetary`, but some systems define additional
/// ones (GNU Linux has additionally `paper`, `name`, `address`, `telephone` and `measurement`) and
/// these are provided in the user default `Locale` and other libraries can use them.
///
/// `Locale` is represented by a `,`-separated sequence of tags in `LanguageRange` syntax, where
/// all except the first one may be preceded by category name and `=` sign.
///
/// The first tag indicates the default locale, the tags prefixed by category names indicate
/// _overrides_ for those categories and the remaining tags indicate fallbacks.
///
/// Note that a syntactically valid value of HTTP `Accept-Language` header is a valid `Locale`. Not
/// the other way around though due to the presence of category selectors.
// TODO: Interning
#[derive(Clone,Debug,Eq,Hash,PartialEq)]
pub struct Locale {
// TODO: Intern the string for performance reasons
// XXX: Store pre-split to LanguageTags?
inner: String,
}
impl Locale {
/// Construct invariant locale.
///
/// Invariant locale is represented simply with empty string.
pub fn invariant() -> Locale {
Locale::from(LanguageRange::invariant())
}
/// Append fallback language tag.
///
/// Adds fallback to the end of the list.
pub fn add(&mut self, tag: &LanguageRange) {
for i in self.inner.split(',') {
if i == tag.as_ref() {
return; // don't add duplicates
}
}
self.inner.push_str(",");
self.inner.push_str(tag.as_ref());
}
/// Append category override.
///
/// Appending new override for a category that already has one will not replace the existing
/// override. This might change in future.
pub fn add_category(&mut self, category: &str, tag: &LanguageRange) {
if self.inner.split(',').next().unwrap() == tag.as_ref() {
return; // don't add useless override equal to the primary tag
}
for i in self.inner.split(',') {
if i.starts_with(category) &&
i[category.len()..].starts_with("=") &&
&i[category.len() + 1..] == tag.as_ref() {
return; // don't add duplicates
}
}
self.inner.push_str(",");
self.inner.push_str(category);
self.inner.push_str("=");
self.inner.push_str(tag.as_ref());
}
/// Iterate over `LanguageRange`s in this `Locale` applicable to given category.
///
/// Returns `LanguageRange`s in the `Locale` that are applicable to provided category. The tags
/// are returned in order of preference, which means the category-specific ones first and then
/// the generic ones.
///
/// The iterator is guaranteed to return at least one value.
pub fn tags_for<'a, 'c>(&'a self, category: &'c str) -> TagsFor<'a, 'c> {
let mut tags = self.inner.split(",");
while let Some(s) = tags.clone().next() {
if s.starts_with(category) && s[category.len()..].starts_with("=") {
return TagsFor {
src: self.inner.as_ref(),
tags: tags,
category: Some(category),
};
}
tags.next();
}
return TagsFor {
src: self.inner.as_ref(),
tags: self.inner.split(","),
category: None,
};
}
}
/// Locale is specified by a string tag. This is the way to access it.
// FIXME: Do we want to provide the full string representation? We would have it as single string
// then.
impl AsRef<str> for Locale {
fn as_ref(&self) -> &str {
self.inner.as_ref()
}
}
impl<'a> From<LanguageRange<'a>> for Locale {
fn from(t: LanguageRange<'a>) -> Locale {
Locale {
inner: t.language.into_owned(),
}
}
}
fn tag(s: &str) -> Result<LanguageRange> {
LanguageRange::from_unix(s)
}
// TODO: Read /etc/locale.alias
fn tag_inv(s: &str) -> LanguageRange {
tag(s).unwrap_or(LanguageRange::invariant())
}
pub fn system_locale() -> Option<Locale> {
// LC_ALL overrides everything
if let Ok(all) = env::var("LC_ALL") {
if let Ok(t) = tag(all.as_ref()) {
return Some(Locale::from(t));
}
}
// LANG is default
let mut loc =
if let Ok(lang) = env::var("LANG") {
Locale::from(tag_inv(lang.as_ref()))
} else {
Locale::invariant()
};
// category overrides
for &(cat, var) in [
("ctype", "LC_CTYPE"),
("numeric", "LC_NUMERIC"),
("time", "LC_TIME"),
("collate", "LC_COLLATE"),
("monetary", "LC_MONETARY"),
("messages", "LC_MESSAGES"),
("paper", "LC_PAPER"),
("name", "LC_NAME"),
("address", "LC_ADDRESS"),
("telephone", "LC_TELEPHONE"),
("measurement", "LC_MEASUREMENT"),
].iter() {
if let Ok(val) = env::var(var) {
if let Ok(tag) = tag(val.as_ref())
{
loc.add_category(cat, &tag);
}
}
}
// LANGUAGE defines fallbacks
if let Ok(langs) = env::var("LANGUAGE") {
for i in langs.split(':') {
if i != "" {
if let Ok(tag) = tag(i) {
loc.add(&tag);
}
}
}
}
if loc.as_ref() != "" {
return Some(loc);
} else {
return None;
}
}
#[cfg(test)]
mod test {
use super::LanguageRange;
#[test]
fn unix_tags() {
assert_eq!("cs-CZ", LanguageRange::from_unix("cs_CZ.UTF-8").unwrap().as_ref());
assert_eq!("sr-RS-ijekavsk", LanguageRange::from_unix("sr_RS@ijekavian").unwrap().as_ref());
assert_eq!("sr-Latn-ijekavsk", LanguageRange::from_unix("sr.UTF-8@ijekavianlatin").unwrap().as_ref());
assert_eq!("en-Arab", LanguageRange::from_unix("en@arabic").unwrap().as_ref());
assert_eq!("en-Arab", LanguageRange::from_unix("en.UTF-8@arabic").unwrap().as_ref());
assert_eq!("de-DE", LanguageRange::from_unix("DE_de.UTF-8@euro").unwrap().as_ref());
assert_eq!("ssy-ER", LanguageRange::from_unix("aa_ER@saaho").unwrap().as_ref());
assert!(LanguageRange::from_unix("foo_BAR").is_err());
assert!(LanguageRange::from_unix("en@arabic.UTF-8").is_err());
assert_eq!("", LanguageRange::from_unix("C").unwrap().as_ref());
assert_eq!("", LanguageRange::from_unix("C.UTF-8").unwrap().as_ref());
assert_eq!("", LanguageRange::from_unix("C.ISO-8859-1").unwrap().as_ref());
assert_eq!("", LanguageRange::from_unix("POSIX").unwrap().as_ref());
}
}

View File

@ -1,33 +0,0 @@
#pragma once
/// This all wraps https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.channel
#include <inttypes.h>
#include "input-method-unstable-v2-client-protocol.h"
#include "virtual-keyboard-unstable-v1-client-protocol.h"
#include "eek/eek-types.h"
#include "dbus.h"
struct receiver;
/// Wrapped<event_loop::driver::Threaded>
struct squeek_state_manager;
struct submission;
struct rsobjects {
struct receiver *receiver;
struct squeek_state_manager *state_manager;
struct submission *submission;
};
void register_ui_loop_handler(struct receiver *receiver, ServerContextService *ui, DBusHandler *dbus_handler);
struct rsobjects squeek_rsobjects_new(struct zwp_input_method_v2 *im, struct zwp_virtual_keyboard_v1 *vk);
void squeek_state_send_force_visible(struct squeek_state_manager *state);
void squeek_state_send_force_hidden(struct squeek_state_manager *state);
void squeek_state_send_keyboard_present(struct squeek_state_manager *state, uint32_t keyboard_present);

View File

@ -1,149 +0,0 @@
/* Copyright (C) 2020 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
/*! Glue for the main loop. */
use crate::state;
use glib::{Continue, MainContext, PRIORITY_DEFAULT, Receiver};
mod c {
use super::*;
use std::os::raw::c_void;
use std::rc::Rc;
use std::time::Instant;
use crate::event_loop::driver;
use crate::imservice::IMService;
use crate::imservice::c::InputMethod;
use crate::state;
use crate::submission::Submission;
use crate::util::c::Wrapped;
use crate::vkeyboard::c::ZwpVirtualKeyboardV1;
/// ServerContextService*
#[repr(transparent)]
pub struct UIManager(*const c_void);
/// DbusHandler*
#[repr(transparent)]
pub struct DBusHandler(*const c_void);
/// Holds the Rust structures that are interesting from C.
#[repr(C)]
pub struct RsObjects {
receiver: Wrapped<Receiver<Commands>>,
state_manager: Wrapped<driver::Threaded>,
submission: Wrapped<Submission>,
}
extern "C" {
fn server_context_service_real_show_keyboard(service: *const UIManager);
fn server_context_service_real_hide_keyboard(service: *const UIManager);
fn server_context_service_set_hint_purpose(service: *const UIManager, hint: u32, purpose: u32);
// This should probably only get called from the gtk main loop,
// given that dbus handler is using glib.
fn dbus_handler_set_visible(dbus: *const DBusHandler, visible: u8);
}
/// Creates what's possible in Rust to eliminate as many FFI calls as possible,
/// because types aren't getting checked across their boundaries,
/// and that leads to suffering.
#[no_mangle]
pub extern "C"
fn squeek_rsobjects_new(
im: *mut InputMethod,
vk: ZwpVirtualKeyboardV1,
) -> RsObjects {
let (sender, receiver) = MainContext::channel(PRIORITY_DEFAULT);
let now = Instant::now();
let state_manager = driver::Threaded::new(sender, state::Application::new(now));
let imservice = if im.is_null() {
None
} else {
Some(IMService::new(im, state_manager.clone()))
};
let submission = Submission::new(vk, imservice);
RsObjects {
submission: Wrapped::new(submission),
state_manager: Wrapped::new(state_manager),
receiver: Wrapped::new(receiver),
}
}
/// Places the UI loop callback in the glib main loop.
#[no_mangle]
pub extern "C"
fn register_ui_loop_handler(
receiver: Wrapped<Receiver<Commands>>,
ui_manager: *const UIManager,
dbus_handler: *const DBusHandler,
) {
let receiver = unsafe { receiver.unwrap() };
let receiver = Rc::try_unwrap(receiver).expect("References still present");
let receiver = receiver.into_inner();
let ctx = MainContext::default();
ctx.acquire();
receiver.attach(
Some(&ctx),
move |msg| {
main_loop_handle_message(msg, ui_manager, dbus_handler);
Continue(true)
},
);
ctx.release();
}
/// A single iteration of the UI loop.
/// Applies state outcomes to external portions of the program.
/// This is the outest layer of the imperative shell,
/// and doesn't lend itself to testing other than integration.
fn main_loop_handle_message(
msg: Commands,
ui_manager: *const UIManager,
dbus_handler: *const DBusHandler,
) {
match msg.panel_visibility {
Some(PanelCommand::Show) => unsafe {
server_context_service_real_show_keyboard(ui_manager);
},
Some(PanelCommand::Hide) => unsafe {
server_context_service_real_hide_keyboard(ui_manager);
},
None => {},
};
if let Some(visible) = msg.dbus_visible_set {
unsafe { dbus_handler_set_visible(dbus_handler, visible as u8) };
}
if let Some(hints) = msg.layout_hint_set {
unsafe {
server_context_service_set_hint_purpose(
ui_manager,
hints.hint.bits(),
hints.purpose.clone() as u32,
)
};
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub enum PanelCommand {
Show,
Hide,
}
/// The commands consumed by the main loop,
/// to be sent out to external components.
#[derive(Clone)]
pub struct Commands {
pub panel_visibility: Option<PanelCommand>,
pub layout_hint_set: Option<state::InputMethodDetails>,
pub dbus_visible_set: Option<bool>,
}

View File

@ -5,7 +5,9 @@ use gtk;
use std::ffi::CString;
use std::cmp::Ordering;
use ::layout::c::{ Bounds, EekGtkKeyboard };
use ::locale::{ OwnedTranslation, compare_current_locale };
use ::locale;
use ::locale::{ OwnedTranslation, Translation, compare_current_locale };
use ::locale_config::system_locale;
use ::logging;
use ::manager;
use ::resources;
@ -18,9 +20,10 @@ use gio::SimpleActionExt;
use glib::translate::FromGlibPtrNone;
use glib::variant::ToVariant;
#[cfg(not(feature = "gtk_v0_5"))]
use gtk::prelude::*;
use gtk::BuilderExtManual;
use gtk::PopoverExt;
use gtk::WidgetExt;
use std::io::Write;
use ::logging::Warn;
mod c {
@ -108,6 +111,46 @@ mod variants {
}
}
fn make_menu_builder(inputs: Vec<(&str, OwnedTranslation)>) -> gtk::Builder {
let mut xml: Vec<u8> = Vec::new();
writeln!(
xml,
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<interface>
<menu id=\"app-menu\">
<section>"
).unwrap();
for (input_name, translation) in inputs {
writeln!(
xml,
"
<item>
<attribute name=\"label\" translatable=\"yes\">{}</attribute>
<attribute name=\"action\">layout</attribute>
<attribute name=\"target\">{}</attribute>
</item>",
translation.0,
input_name,
).unwrap();
}
writeln!(
xml,
"
</section>
<section>
<item>
<attribute name=\"label\" translatable=\"yes\">Keyboard Settings</attribute>
<attribute name=\"action\">settings</attribute>
</item>
</section>
</menu>
</interface>"
).unwrap();
gtk::Builder::new_from_string(
&String::from_utf8(xml).expect("Bad menu definition")
)
}
fn get_settings(schema_name: &str) -> Option<gio::Settings> {
let mut error_handler = logging::Print{};
gio::SettingsSchemaSource::get_default()
@ -207,8 +250,13 @@ fn get_current_layout(
/// Translates all provided layout names according to current locale,
/// for the purpose of display (i.e. errors will be caught and reported)
fn translate_layout_names(layouts: &Vec<LayoutId>) -> Vec<OwnedTranslation> {
// `XkbInfo` being temporary means that its return values must be
// copied, forcing the use of `OwnedTranslation`.
// This procedure is rather ugly...
// Xkb lookup *must not* be applied to non-system layouts,
// so both translators can't be merged into one lookup table,
// therefore must be done in two steps.
// `XkbInfo` being temporary also means
// that its return values must be copied,
// forcing the use of `OwnedTranslation`.
enum Status {
/// xkb names should get all translated here
Translated(OwnedTranslation),
@ -217,7 +265,7 @@ fn translate_layout_names(layouts: &Vec<LayoutId>) -> Vec<OwnedTranslation> {
}
// Attempt to take all xkb names from gnome-desktop's xkb info.
let xkb_translator = ::locale::XkbInfo::new();
let xkb_translator = locale::XkbInfo::new();
let translated_names = layouts.iter()
.map(|id| match id {
@ -229,15 +277,49 @@ fn translate_layout_names(layouts: &Vec<LayoutId>) -> Vec<OwnedTranslation> {
&format!("No display name for xkb layout {}", name),
).unwrap_or_else(|| Status::Remaining(name.clone()))
},
LayoutId::Local (_) => unreachable!(),
LayoutId::Local(name) => Status::Remaining(name.clone()),
});
// Non-xkb layouts and weird xkb layouts
// still need to be looked up in the internal database.
let builtin_translations = system_locale()
.map(|locale|
locale.tags_for("messages")
.next().unwrap() // guaranteed to exist
.as_ref()
.to_owned()
)
.or_print(logging::Problem::Surprise, "No locale detected")
.and_then(|lang| {
resources::get_layout_names(lang.as_str())
.or_print(
logging::Problem::Surprise,
&format!("No translations for locale {}", lang),
)
});
match builtin_translations {
Some(translations) => {
translated_names
.map(|status| match status {
Status::Remaining(name) => {
translations.get(name.as_str())
.unwrap_or(&Translation(name.as_str()))
.to_owned()
},
Status::Translated(t) => t,
})
.collect()
},
None => {
translated_names
.map(|status| match status {
Status::Remaining(name) => OwnedTranslation(name),
Status::Translated(t) => t,
})
.collect()
},
}
}
pub fn show(
@ -268,12 +350,12 @@ pub fn show(
.chain(overlay_layouts)
.collect();
let translated_names = translate_layout_names(&system_layouts);
let translated_names = translate_layout_names(&all_layouts);
// sorted collection of language layouts
// sorted collection of human and machine names
let mut human_names: Vec<(OwnedTranslation, LayoutId)> = translated_names
.into_iter()
.zip(system_layouts.clone().into_iter())
.zip(all_layouts.clone().into_iter())
.collect();
human_names.sort_unstable_by(|(tr_a, layout_a), (tr_b, layout_b)| {
@ -285,14 +367,32 @@ pub fn show(
}
});
let builder = gtk::Builder::new_from_resource("/sm/puri/squeekboard/popover.ui");
let model: gio::Menu = builder.get_object("app-menu").unwrap();
// GVariant doesn't natively support `enum`s,
// so the `choices` vector will serve as a lookup table.
let choices_with_translations: Vec<(String, (OwnedTranslation, LayoutId))>
= human_names.into_iter()
.enumerate()
.map(|(i, human_entry)| {(
format!("{}_{}", i, human_entry.1.get_name()),
human_entry,
)}).collect();
for (tr, l) in human_names.iter().rev() {
let detailed_action = format!("layout::{}", l.get_name());
let item = gio::MenuItem::new(Some(&tr.0), Some(detailed_action.as_str()));
model.prepend_item (&item);
}
let builder = make_menu_builder(
choices_with_translations.iter()
.map(|(id, (translation, _))| (id.as_str(), (*translation).clone()))
.collect()
);
let choices: Vec<(String, LayoutId)>
= choices_with_translations.into_iter()
.map(|(id, (_tr, layout))| (id, layout))
.collect();
// Much more debuggable to populate the model & menu
// from a string representation
// than add items imperatively
let model: gio::MenuModel = builder.get_object("app-menu").unwrap();
let menu = gtk::Popover::new_from_model(Some(&window), &model);
menu.set_pointing_to(&gtk::Rectangle {
@ -303,36 +403,32 @@ pub fn show(
});
menu.set_constrain_to(gtk::PopoverConstraint::None);
let action_group = gio::SimpleActionGroup::new();
if let Some(current_layout) = get_current_layout(manager, &system_layouts) {
let current_layout_name = all_layouts.iter()
let current_name_variant = choices.iter()
.find(
|l| l.get_name() == current_layout.get_name()
|(_id, layout)| layout == &current_layout
).unwrap()
.get_name();
log_print!(logging::Level::Debug, "Current Layout {}", current_layout_name);
.0.to_variant();
let layout_action = gio::SimpleAction::new_stateful(
"layout",
Some(current_layout_name.to_variant().type_()),
&current_layout_name.to_variant()
Some(current_name_variant.type_()),
&current_name_variant,
);
let menu_inner = menu.clone();
layout_action.connect_change_state(move |_action, state| {
match state {
Some(v) => {
log_print!(logging::Level::Debug, "Selected layout {}", v);
v.get::<String>()
.or_print(
logging::Problem::Bug,
&format!("Variant is not string: {:?}", v)
)
.map(|state| {
let layout = all_layouts.iter()
let (_id, layout) = choices.iter()
.find(
|choices| state == choices.get_name()
|choices| state == choices.0
).unwrap();
set_visible_layout(
manager,
@ -347,21 +443,20 @@ pub fn show(
};
menu_inner.popdown();
});
action_group.add_action(&layout_action);
};
let settings_action = gio::SimpleAction::new("settings", None);
settings_action.connect_activate(move |_, _| {
let s = CString::new("region").unwrap();
unsafe { c::popover_open_settings_panel(s.as_ptr()) };
});
let action_group = gio::SimpleActionGroup::new();
action_group.add_action(&layout_action);
action_group.add_action(&settings_action);
menu.insert_action_group("popup", Some(&action_group));
};
menu.bind_model(Some(&model), Some("popup"));
glib::idle_add_local(move || {
menu.popup();
Continue(false)
});
}

View File

@ -2,6 +2,11 @@
* This could be done using GResource, but that would need additional work.
*/
use std::collections::HashMap;
use ::locale::Translation;
use std::iter::FromIterator;
// TODO: keep a list of what is a language layout,
// and what a convenience layout. "_wide" is not a layout,
// neither is "number"
@ -13,9 +18,6 @@ static KEYBOARDS: &[(&'static str, &'static str)] = &[
("us_wide", include_str!("../data/keyboards/us_wide.yaml")),
// Language layouts: keep alphabetical.
("am", include_str!("../data/keyboards/am.yaml")),
("am+phonetic", include_str!("../data/keyboards/am+phonetic.yaml")),
("ara", include_str!("../data/keyboards/ara.yaml")),
("ara_wide", include_str!("../data/keyboards/ara_wide.yaml")),
@ -28,9 +30,6 @@ static KEYBOARDS: &[(&'static str, &'static str)] = &[
("br", include_str!("../data/keyboards/br.yaml")),
("ch+fr", include_str!("../data/keyboards/ch+fr.yaml")),
("ch+de", include_str!("../data/keyboards/ch+de.yaml")),
("ch", include_str!("../data/keyboards/ch.yaml")),
("ch_wide", include_str!("../data/keyboards/ch_wide.yaml")),
("de", include_str!("../data/keyboards/de.yaml")),
("de_wide", include_str!("../data/keyboards/de_wide.yaml")),
@ -86,19 +85,11 @@ static KEYBOARDS: &[(&'static str, &'static str)] = &[
("us+dvorak", include_str!("../data/keyboards/us+dvorak.yaml")),
("us+dvorak_wide", include_str!("../data/keyboards/us+dvorak_wide.yaml")),
// Email
("email/us", include_str!("../data/keyboards/email/us.yaml")),
// URL
("url/us", include_str!("../data/keyboards/url/us.yaml")),
// Others
("number/us", include_str!("../data/keyboards/number/us.yaml")),
("pin/us", include_str!("../data/keyboards/pin/us.yaml")),
// Terminal
("terminal/fr", include_str!("../data/keyboards/terminal/fr.yaml")),
("terminal/fr_wide", include_str!("../data/keyboards/terminal/fr_wide.yaml")),
("terminal/us", include_str!("../data/keyboards/terminal/us.yaml")),
("terminal/us_wide", include_str!("../data/keyboards/terminal/us_wide.yaml")),
@ -120,6 +111,46 @@ pub fn get_overlays() -> Vec<&'static str> {
OVERLAY_NAMES.to_vec()
}
/// Translations of the layout identifier strings
static LAYOUT_NAMES: &[(&'static str, &'static str)] = &[
("de-DE", include_str!("../data/langs/de-DE.txt")),
("en-US", include_str!("../data/langs/en-US.txt")),
("es-ES", include_str!("../data/langs/es-ES.txt")),
("fur-IT", include_str!("../data/langs/fur-IT.txt")),
("he-IL", include_str!("../data/langs/he-IL.txt")),
("ja-JP", include_str!("../data/langs/ja-JP.txt")),
("pl-PL", include_str!("../data/langs/pl-PL.txt")),
("ru-RU", include_str!("../data/langs/ru-RU.txt")),
];
pub fn get_layout_names(lang: &str)
-> Option<HashMap<&'static str, Translation<'static>>>
{
let translations = LAYOUT_NAMES.iter()
.find(|(name, _data)| *name == lang)
.map(|(_name, data)| *data);
translations.map(make_mapping)
}
fn parse_line(line: &str) -> Option<(&str, Translation)> {
let comment = line.trim().starts_with("#");
if comment {
None
} else {
let mut iter = line.splitn(2, " ");
let name = iter.next().unwrap();
// will skip empty and unfinished lines
iter.next().map(|tr| (name, Translation(tr.trim())))
}
}
fn make_mapping(data: &str) -> HashMap<&str, Translation> {
HashMap::from_iter(
data.split("\n")
.filter_map(parse_line)
)
}
#[cfg(test)]
mod test {
use super::*;
@ -130,4 +161,32 @@ mod test {
assert!(get_keyboard(&format!("{}/us", name)).is_some());
}
}
#[test]
fn mapping_line() {
assert_eq!(
Some(("name", Translation("translation"))),
parse_line("name translation")
);
}
#[test]
fn mapping_bad() {
assert_eq!(None, parse_line("bad"));
}
#[test]
fn mapping_empty() {
assert_eq!(None, parse_line(""));
}
#[test]
fn mapping_comment() {
assert_eq!(None, parse_line("# comment"));
}
#[test]
fn mapping_comment_offset() {
assert_eq!(None, parse_line(" # comment"));
}
}

View File

@ -30,6 +30,7 @@
enum {
PROP_0,
PROP_VISIBLE,
PROP_ENABLED,
PROP_LAST
};
@ -42,10 +43,12 @@ struct _ServerContextService {
struct submission *submission; // unowned
struct squeek_layout_state *layout;
struct ui_manager *manager; // unowned
struct squeek_state_manager *state_manager; // shared reference
struct vis_manager *vis_manager; // owned
gboolean visible;
PhoshLayerSurface *window;
GtkWidget *widget; // nullable
guint hiding;
guint last_requested_height;
};
@ -64,6 +67,23 @@ on_destroy (ServerContextService *self, GtkWidget *widget)
//eekboard_context_service_destroy (EEKBOARD_CONTEXT_SERVICE (context));
}
static void
on_notify_map (ServerContextService *self, GtkWidget *widget)
{
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));
g_object_set (self, "visible", TRUE, NULL);
}
static void
on_notify_unmap (ServerContextService *self, GtkWidget *widget)
{
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));
g_object_set (self, "visible", FALSE, NULL);
}
static uint32_t
calculate_height(int32_t width, GdkRectangle *geometry)
{
@ -167,6 +187,8 @@ make_window (ServerContextService *self)
g_object_connect (self->window,
"swapped-signal::destroy", G_CALLBACK(on_destroy), self,
"swapped-signal::map", G_CALLBACK(on_notify_map), self,
"swapped-signal::unmap", G_CALLBACK(on_notify_unmap), self,
"swapped-signal::configured", G_CALLBACK(on_surface_configure), self,
NULL);
@ -177,7 +199,8 @@ make_window (ServerContextService *self)
// or for hacks with regular windows.
gtk_widget_set_can_focus (GTK_WIDGET(self->window), FALSE);
g_object_set (G_OBJECT(self->window), "accept_focus", FALSE, NULL);
gtk_window_set_title (GTK_WINDOW(self->window), "Squeekboard");
gtk_window_set_title (GTK_WINDOW(self->window),
_("Squeekboard"));
gtk_window_set_icon_name (GTK_WINDOW(self->window), "squeekboard");
gtk_window_set_keep_above (GTK_WINDOW(self->window), TRUE);
}
@ -203,8 +226,7 @@ make_widget (ServerContextService *self)
gtk_widget_show_all(self->widget);
}
// Called from rust
void
static void
server_context_service_real_show_keyboard (ServerContextService *self)
{
if (!self->window) {
@ -213,16 +235,99 @@ server_context_service_real_show_keyboard (ServerContextService *self)
if (!self->widget) {
make_widget (self);
}
self->visible = TRUE;
gtk_widget_show (GTK_WIDGET(self->window));
}
// Called from rust
void
static gboolean
show_keyboard_source_func(ServerContextService *context)
{
server_context_service_real_show_keyboard(context);
return G_SOURCE_REMOVE;
}
static void
server_context_service_real_hide_keyboard (ServerContextService *self)
{
if (self->window) {
gtk_widget_hide (GTK_WIDGET(self->window));
self->visible = FALSE;
}
static gboolean
hide_keyboard_source_func(ServerContextService *context)
{
server_context_service_real_hide_keyboard(context);
return G_SOURCE_REMOVE;
}
static gboolean
on_hide (ServerContextService *self)
{
server_context_service_real_hide_keyboard(self);
self->hiding = 0;
return G_SOURCE_REMOVE;
}
static void
server_context_service_show_keyboard (ServerContextService *self)
{
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE(self));
if (self->hiding) {
g_source_remove (self->hiding);
self->hiding = 0;
}
if (!self->visible) {
g_idle_add((GSourceFunc)show_keyboard_source_func, self);
}
}
void
server_context_service_force_show_keyboard (ServerContextService *self)
{
if (!submission_hint_available(self->submission)) {
eekboard_context_service_set_hint_purpose(
self->state,
ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE,
ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL
);
}
server_context_service_show_keyboard(self);
}
void
server_context_service_hide_keyboard (ServerContextService *self)
{
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE(self));
if (self->visible) {
g_idle_add((GSourceFunc)hide_keyboard_source_func, self);
}
}
/// Meant for use by the input-method handler:
/// the visible keyboard is no longer needed.
/// The implementation will delay it slightly,
/// because the release may be due to switching from one text field to another.
/// In this case, the user doesn't really need the keyboard surface
/// to disappear completely.
void
server_context_service_release_visibility (ServerContextService *self)
{
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE(self));
if (!self->hiding && self->visible) {
self->hiding = g_timeout_add (200, (GSourceFunc) on_hide, self);
}
}
static void
server_context_service_set_physical_keyboard_present (ServerContextService *self, gboolean physical_keyboard_present)
{
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));
squeek_visman_set_keyboard_present(self->vis_manager, physical_keyboard_present);
}
static void
@ -234,8 +339,11 @@ server_context_service_set_property (GObject *object,
ServerContextService *self = SERVER_CONTEXT_SERVICE(object);
switch (prop_id) {
case PROP_VISIBLE:
self->visible = g_value_get_boolean (value);
break;
case PROP_ENABLED:
squeek_state_send_keyboard_present(self->state_manager, !g_value_get_boolean (value));
server_context_service_set_physical_keyboard_present (self, !g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@ -249,7 +357,11 @@ server_context_service_get_property (GObject *object,
GValue *value,
GParamSpec *pspec)
{
ServerContextService *self = SERVER_CONTEXT_SERVICE(object);
switch (prop_id) {
case PROP_VISIBLE:
g_value_set_boolean (value, self->visible);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@ -277,6 +389,18 @@ server_context_service_class_init (ServerContextServiceClass *klass)
gobject_class->get_property = server_context_service_get_property;
gobject_class->dispose = server_context_service_dispose;
/**
* Flag to indicate if keyboard is visible or not.
*/
pspec = g_param_spec_boolean ("visible",
"Visible",
"Visible",
FALSE,
G_PARAM_READWRITE);
g_object_class_install_property (gobject_class,
PROP_VISIBLE,
pspec);
/**
* ServerContextServie:keyboard:
*
@ -318,20 +442,24 @@ init (ServerContextService *self) {
}
ServerContextService *
server_context_service_new (EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct squeek_state_manager *state_manager)
server_context_service_new (EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct vis_manager *visman)
{
ServerContextService *ui = g_object_new (SERVER_TYPE_CONTEXT_SERVICE, NULL);
ui->submission = submission;
ui->state = self;
ui->layout = layout;
ui->manager = uiman;
ui->state_manager = state_manager;
ui->vis_manager = visman;
init(ui);
return ui;
}
// Used from Rust
void server_context_service_set_hint_purpose(ServerContextService *self, uint32_t hint,
uint32_t purpose) {
eekboard_context_service_set_hint_purpose(self->state, hint, purpose);
void
server_context_service_update_visible (ServerContextService *self, gboolean visible) {
if (visible) {
server_context_service_show_keyboard(self);
} else {
server_context_service_hide_keyboard(self);
}
}

View File

@ -29,7 +29,7 @@ G_BEGIN_DECLS
/** Manages the lifecycle of the window displaying layouts. */
G_DECLARE_FINAL_TYPE (ServerContextService, server_context_service, SERVER, CONTEXT_SERVICE, GObject)
ServerContextService *server_context_service_new(EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct squeek_state_manager *state_manager);
ServerContextService *server_context_service_new(EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct vis_manager *visman);
enum squeek_arrangement_kind server_context_service_get_layout_type(ServerContextService *);
void server_context_service_force_show_keyboard (ServerContextService *self);
void server_context_service_hide_keyboard (ServerContextService *self);

View File

@ -29,7 +29,6 @@
#include "eekboard/eekboard-context-service.h"
#include "dbus.h"
#include "layout.h"
#include "main.h"
#include "outputs.h"
#include "submission.h"
#include "server-context-service.h"
@ -39,29 +38,21 @@
#include <gdk/gdkwayland.h>
typedef enum _SqueekboardDebugFlags {
SQUEEKBOARD_DEBUG_FLAG_NONE = 0,
SQUEEKBOARD_DEBUG_FLAG_FORCE_SHOW = 1 << 0,
SQUEEKBOARD_DEBUG_FLAG_GTK_INSPECTOR = 1 << 1,
} SqueekboardDebugFlags;
/// Some state, some IO components, all mixed together.
/// Better move what's possible to state::Application,
/// or secondary data structures of the same general shape.
/// Global application state
struct squeekboard {
struct squeek_wayland wayland; // Just hooks.
DBusHandler *dbus_handler; // Controls visibility of the OSK.
EekboardContextService *settings_context; // Gsettings hooks.
ServerContextService *ui_context; // mess, includes the entire UI
/// Currently wanted layout. TODO: merge into state::Application
struct squeek_layout_state layout_choice;
/// UI shape tracker/chooser. TODO: merge into state::Application
struct ui_manager *ui_manager;
struct submission *submission; // Wayland text input handling.
struct squeek_layout_state layout_choice; // Currently wanted layout.
struct ui_manager *ui_manager; // UI shape tracker/chooser. TODO: merge with layuot choice
};
GMainLoop *loop;
static gboolean opt_system = FALSE;
static gchar *opt_address = NULL;
static void
quit (void)
@ -86,16 +77,12 @@ on_name_lost (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
SqueekboardDebugFlags *flags = user_data;
// TODO: could conceivable continue working
// if intrnal changes stop sending dbus changes
(void)connection;
(void)name;
(void)user_data;
g_warning("DBus unavailable, unclear how to continue. Is Squeekboard already running?");
if ((*flags & SQUEEKBOARD_DEBUG_FLAG_FORCE_SHOW) == 0) {
exit (1);
}
g_error("DBus unavailable, unclear how to continue.");
}
// Wayland
@ -259,106 +246,16 @@ session_register(void) {
g_signal_connect (_client_proxy, "g-signal", G_CALLBACK (client_proxy_signal), NULL);
}
static void
phosh_theme_init (void)
{
GtkSettings *gtk_settings;
const char *desktop;
gboolean phosh_session;
g_auto (GStrv) components = NULL;
desktop = g_getenv ("XDG_CURRENT_DESKTOP");
if (!desktop) {
return;
}
components = g_strsplit (desktop, ":", -1);
phosh_session = g_strv_contains ((const char * const *)components, "Phosh");
if (!phosh_session) {
return;
}
gtk_settings = gtk_settings_get_default ();
g_object_set (G_OBJECT (gtk_settings), "gtk-application-prefer-dark-theme", TRUE, NULL);
}
/// Create Rust objects in one go,
/// to avoid crossing the language barrier and losing type information
static struct rsobjects create_rsobjects(struct zwp_input_method_manager_v2 *immanager,
struct zwp_virtual_keyboard_manager_v1 *vkmanager,
struct wl_seat *seat) {
struct zwp_input_method_v2 *im = NULL;
if (immanager) {
im = zwp_input_method_manager_v2_get_input_method(immanager, seat);
}
struct zwp_virtual_keyboard_v1 *vk = NULL;
if (vkmanager) {
vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmanager, seat);
}
return squeek_rsobjects_new(im, vk);
}
static GDebugKey debug_keys[] =
{
{ .key = "force-show",
.value = SQUEEKBOARD_DEBUG_FLAG_FORCE_SHOW,
},
{ .key = "gtk-inspector",
.value = SQUEEKBOARD_DEBUG_FLAG_GTK_INSPECTOR,
},
};
static SqueekboardDebugFlags
parse_debug_env (void)
{
const char *debugenv;
SqueekboardDebugFlags flags = SQUEEKBOARD_DEBUG_FLAG_NONE;
debugenv = g_getenv("SQUEEKBOARD_DEBUG");
if (!debugenv) {
return flags;
}
return g_parse_debug_string(debugenv, debug_keys, G_N_ELEMENTS (debug_keys));
}
int
main (int argc, char **argv)
{
SqueekboardDebugFlags debug_flags = SQUEEKBOARD_DEBUG_FLAG_NONE;
g_autoptr (GError) err = NULL;
g_autoptr(GOptionContext) opt_context = NULL;
const GOptionEntry options [] = {
{ NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL }
};
opt_context = g_option_context_new ("- A on screen keyboard");
g_option_context_add_main_entries (opt_context, options, NULL);
g_option_context_add_group (opt_context, gtk_get_option_group (TRUE));
if (!g_option_context_parse (opt_context, &argc, &argv, &err)) {
g_warning ("%s", err->message);
return 1;
}
textdomain (GETTEXT_PACKAGE);
bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
if (!gtk_init_check (&argc, &argv)) {
g_printerr ("Can't init GTK\n");
exit (1);
}
debug_flags = parse_debug_env ();
eek_init ();
phosh_theme_init ();
// Set up Wayland
gdk_set_allowed_backends ("wayland");
GdkDisplay *gdk_display = gdk_display_get_default ();
@ -366,7 +263,6 @@ main (int argc, char **argv)
if (display == NULL) {
g_error ("Failed to get display: %m\n");
exit(1);
}
@ -379,25 +275,18 @@ main (int argc, char **argv)
if (!instance.wayland.seat) {
g_error("No seat Wayland global available.");
exit(1);
}
if (!instance.wayland.virtual_keyboard_manager) {
g_error("No virtual keyboard manager Wayland global available.");
exit(1);
}
if (!instance.wayland.layer_shell) {
g_error("No layer shell global available.");
exit(1);
}
if (!instance.wayland.input_method_manager) {
g_warning("Wayland input method interface not available");
}
struct rsobjects rsobjects = create_rsobjects(instance.wayland.input_method_manager,
instance.wayland.virtual_keyboard_manager,
instance.wayland.seat);
instance.ui_manager = squeek_uiman_new();
instance.settings_context = eekboard_context_service_new(&instance.layout_choice);
@ -407,17 +296,51 @@ main (int argc, char **argv)
// TODO: make dbus errors non-always-fatal
// dbus is not strictly necessary for the useful operation
// if text-input is used, as it can bring the keyboard in and out
GBusType bus_type;
if (opt_system) {
bus_type = G_BUS_TYPE_SYSTEM;
} else if (opt_address) {
bus_type = G_BUS_TYPE_NONE;
} else {
bus_type = G_BUS_TYPE_SESSION;
}
GDBusConnection *connection = NULL;
connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &err);
GError *error = NULL;
switch (bus_type) {
case G_BUS_TYPE_SYSTEM:
case G_BUS_TYPE_SESSION:
error = NULL;
connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
if (connection == NULL) {
g_printerr ("Can't connect to the bus: %s. "
"Visibility switching unavailable.", err->message);
"Visibility switching unavailable.", error->message);
g_error_free (error);
}
break;
case G_BUS_TYPE_NONE:
error = NULL;
connection = g_dbus_connection_new_for_address_sync (opt_address,
0,
NULL,
NULL,
&error);
if (connection == NULL) {
g_printerr ("Can't connect to the bus at %s: %s\n",
opt_address,
error->message);
g_error_free (error);
exit (1);
}
break;
default:
g_assert_not_reached ();
break;
}
guint owner_id = 0;
DBusHandler *service = NULL;
if (connection) {
service = dbus_handler_new(connection, DBUS_SERVICE_PATH, rsobjects.state_manager);
service = dbus_handler_new(connection, DBUS_SERVICE_PATH);
if (service == NULL) {
g_printerr ("Can't create dbus server\n");
@ -430,7 +353,7 @@ main (int argc, char **argv)
G_BUS_NAME_OWNER_FLAGS_NONE,
on_name_acquired,
on_name_lost,
&debug_flags,
NULL,
NULL);
if (owner_id == 0) {
g_printerr ("Can't own the name\n");
@ -438,31 +361,35 @@ main (int argc, char **argv)
}
}
eekboard_context_service_set_submission(instance.settings_context, rsobjects.submission);
struct vis_manager *vis_manager = squeek_visman_new();
instance.submission = get_submission(instance.wayland.input_method_manager,
instance.wayland.virtual_keyboard_manager,
vis_manager,
instance.wayland.seat,
instance.settings_context);
eekboard_context_service_set_submission(instance.settings_context, instance.submission);
ServerContextService *ui_context = server_context_service_new(
instance.settings_context,
rsobjects.submission,
instance.submission,
&instance.layout_choice,
instance.ui_manager,
rsobjects.state_manager);
vis_manager);
if (!ui_context) {
g_error("Could not initialize GUI");
exit(1);
}
instance.ui_context = ui_context;
register_ui_loop_handler(rsobjects.receiver, instance.ui_context, instance.dbus_handler);
squeek_visman_set_ui(vis_manager, instance.ui_context);
if (instance.dbus_handler) {
dbus_handler_set_ui_context(instance.dbus_handler, instance.ui_context);
}
eekboard_context_service_set_ui(instance.settings_context, instance.ui_context);
session_register();
if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_GTK_INSPECTOR) {
gtk_window_set_interactive_debugging (TRUE);
}
if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_FORCE_SHOW) {
squeek_state_send_force_visible (rsobjects.state_manager);
}
loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run (loop);

View File

@ -1,417 +0,0 @@
/* Copyright (C) 2021 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
/*! Application-wide state is stored here.
* It's driven by the loop defined in the loop module. */
use crate::animation;
use crate::imservice::{ ContentHint, ContentPurpose };
use crate::main::{ Commands, PanelCommand };
use std::time::Instant;
#[derive(Clone, Copy)]
pub enum Presence {
Present,
Missing,
}
#[derive(Clone)]
pub struct InputMethodDetails {
pub hint: ContentHint,
pub purpose: ContentPurpose,
}
#[derive(Clone)]
pub enum InputMethod {
Active(InputMethodDetails),
InactiveSince(Instant),
}
/// Incoming events
#[derive(Clone)]
pub enum Event {
InputMethod(InputMethod),
Visibility(visibility::Event),
PhysicalKeyboard(Presence),
/// Event triggered because a moment in time passed.
/// Use to animate state transitions.
/// The value is the ideal arrival time.
TimeoutReached(Instant),
}
impl From<InputMethod> for Event {
fn from(im: InputMethod) -> Self {
Self::InputMethod(im)
}
}
pub mod visibility {
#[derive(Clone)]
pub enum Event {
/// User requested the panel to show
ForceVisible,
/// The user requested the panel to go down
ForceHidden,
}
#[derive(Clone, PartialEq, Debug, Copy)]
pub enum State {
/// Last interaction was user forcing the panel to go visible
ForcedVisible,
/// Last interaction was user forcing the panel to hide
ForcedHidden,
/// Last interaction was the input method changing active state
NotForced,
}
}
/// The outwardly visible state.
#[derive(Clone)]
pub struct Outcome {
pub visibility: animation::Outcome,
pub im: InputMethod,
}
impl Outcome {
/// Returns the commands needed to apply changes as required by the new state.
/// This implementation doesn't actually take the old state into account,
/// instead issuing all the commands as needed to reach the new state.
/// The receivers of the commands bear the burden
/// of checking if the commands end up being no-ops.
pub fn get_commands_to_reach(&self, new_state: &Self) -> Commands {
let layout_hint_set = match new_state {
Outcome {
visibility: animation::Outcome::Visible,
im: InputMethod::Active(hints),
} => Some(hints.clone()),
Outcome {
visibility: animation::Outcome::Visible,
im: InputMethod::InactiveSince(_),
} => Some(InputMethodDetails {
hint: ContentHint::NONE,
purpose: ContentPurpose::Normal,
}),
Outcome {
visibility: animation::Outcome::Hidden,
..
} => None,
};
let (dbus_visible_set, panel_visibility) = match new_state.visibility {
animation::Outcome::Visible => (Some(true), Some(PanelCommand::Show)),
animation::Outcome::Hidden => (Some(false), Some(PanelCommand::Hide)),
};
Commands {
panel_visibility,
layout_hint_set,
dbus_visible_set,
}
}
}
/// The actual logic of the program.
/// At this moment, limited to calculating visibility and IM hints.
///
/// It keeps the panel visible for a short time period after each hide request.
/// This prevents flickering on quick successive enable/disable events.
/// It does not treat user-driven hiding in a special way.
///
/// This is the "functional core".
/// All state changes return the next state and the optimal time for the next check.
///
/// This state tracker can be driven by any event loop.
#[derive(Clone)]
pub struct Application {
pub im: InputMethod,
pub visibility_override: visibility::State,
pub physical_keyboard: Presence,
}
impl Application {
/// A conservative default, ignoring the actual state of things.
/// It will initially show the keyboard for a blink.
// The ignorance might actually be desired,
// as it allows for startup without waiting for a system check.
// The downside is that adding actual state should not cause transitions.
// Another acceptable alternative is to allow explicitly uninitialized parts.
pub fn new(now: Instant) -> Self {
Self {
im: InputMethod::InactiveSince(now),
visibility_override: visibility::State::NotForced,
physical_keyboard: Presence::Missing,
}
}
pub fn apply_event(self, event: Event, _now: Instant) -> Self {
match event {
Event::TimeoutReached(_) => self,
Event::Visibility(visibility) => Self {
visibility_override: match visibility {
visibility::Event::ForceHidden => visibility::State::ForcedHidden,
visibility::Event::ForceVisible => visibility::State::ForcedVisible,
},
..self
},
Event::PhysicalKeyboard(presence) => Self {
physical_keyboard: presence,
..self
},
Event::InputMethod(new_im) => match (self.im.clone(), new_im) {
(InputMethod::Active(_old), InputMethod::Active(new_im))
=> Self {
im: InputMethod::Active(new_im),
..self
},
// For changes in active state, remove user's visibility override.
// Both cases spelled out explicitly, rather than by the wildcard,
// to not lose the notion that it's the opposition that matters
(InputMethod::InactiveSince(_old), InputMethod::Active(new_im))
=> Self {
im: InputMethod::Active(new_im),
visibility_override: visibility::State::NotForced,
..self
},
(InputMethod::Active(_old), InputMethod::InactiveSince(since))
=> Self {
im: InputMethod::InactiveSince(since),
visibility_override: visibility::State::NotForced,
..self
},
// This is a weird case, there's no need to update an inactive state.
// But it's not wrong, just superfluous.
(InputMethod::InactiveSince(old), InputMethod::InactiveSince(_new))
=> Self {
// New is going to be newer than old, so it can be ignored.
// It was already inactive at that moment.
im: InputMethod::InactiveSince(old),
..self
},
}
}
}
pub fn get_outcome(&self, now: Instant) -> Outcome {
// FIXME: include physical keyboard presence
Outcome {
visibility: match (self.physical_keyboard, self.visibility_override) {
(_, visibility::State::ForcedHidden) => animation::Outcome::Hidden,
(_, visibility::State::ForcedVisible) => animation::Outcome::Visible,
(Presence::Present, visibility::State::NotForced) => animation::Outcome::Hidden,
(Presence::Missing, visibility::State::NotForced) => match self.im {
InputMethod::Active(_) => animation::Outcome::Visible,
InputMethod::InactiveSince(since) => {
if now < since + animation::HIDING_TIMEOUT { animation::Outcome::Visible }
else { animation::Outcome::Hidden }
},
},
},
im: self.im.clone(),
}
}
/// Returns the next time to update the outcome.
pub fn get_next_wake(&self, now: Instant) -> Option<Instant> {
match self {
Self {
visibility_override: visibility::State::NotForced,
im: InputMethod::InactiveSince(since),
..
} => {
let anim_end = *since + animation::HIDING_TIMEOUT;
if now < anim_end { Some(anim_end) }
else { None }
}
_ => None,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use std::time::Duration;
fn imdetails_new() -> InputMethodDetails {
InputMethodDetails {
purpose: ContentPurpose::Normal,
hint: ContentHint::NONE,
}
}
/// Test the original delay scenario: no flicker on quick switches.
#[test]
fn avoid_hide() {
let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
let mut now = start;
let state = Application {
im: InputMethod::Active(imdetails_new()),
physical_keyboard: Presence::Missing,
visibility_override: visibility::State::NotForced,
};
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
// Check 100ms at 1ms intervals. It should remain visible.
for _i in 0..100 {
now += Duration::from_millis(1);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Visible,
"Hidden when it should remain visible: {:?}",
now.saturating_duration_since(start),
)
}
let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
assert_eq!(state.get_outcome(now).visibility, animation::Outcome::Visible);
}
/// Make sure that hiding works when input method goes away
#[test]
fn hide() {
let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
let mut now = start;
let state = Application {
im: InputMethod::Active(imdetails_new()),
physical_keyboard: Presence::Missing,
visibility_override: visibility::State::NotForced,
};
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
while let animation::Outcome::Visible = state.get_outcome(now).visibility {
now += Duration::from_millis(1);
assert!(
now < start + Duration::from_millis(250),
"Hiding too slow: {:?}",
now.saturating_duration_since(start),
);
}
}
/// Check against the false showing bug.
/// Expectation: it will get hidden and not appear again
#[test]
fn false_show() {
let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
let mut now = start;
let state = Application {
im: InputMethod::Active(imdetails_new()),
physical_keyboard: Presence::Missing,
visibility_override: visibility::State::NotForced,
};
// This reflects the sequence from Wayland:
// disable, disable, enable, disable
// all in a single batch.
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
while let animation::Outcome::Visible = state.get_outcome(now).visibility {
now += Duration::from_millis(1);
assert!(
now < start + Duration::from_millis(250),
"Still not hidden: {:?}",
now.saturating_duration_since(start),
);
}
// One second without appearing again
for _i in 0..1000 {
now += Duration::from_millis(1);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Hidden,
"Appeared unnecessarily: {:?}",
now.saturating_duration_since(start),
);
}
}
#[test]
fn force_visible() {
let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
let mut now = start;
let state = Application {
im: InputMethod::InactiveSince(now),
physical_keyboard: Presence::Missing,
visibility_override: visibility::State::NotForced,
};
now += Duration::from_secs(1);
let state = state.apply_event(Event::Visibility(visibility::Event::ForceVisible), now);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Visible,
"Failed to show: {:?}",
now.saturating_duration_since(start),
);
now += Duration::from_secs(1);
let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
now += Duration::from_secs(1);
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
now += Duration::from_secs(1);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Hidden,
"Failed to release forced visibility: {:?}",
now.saturating_duration_since(start),
);
}
#[test]
fn keyboard_present() {
let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
let mut now = start;
let state = Application {
im: InputMethod::Active(imdetails_new()),
physical_keyboard: Presence::Missing,
visibility_override: visibility::State::NotForced,
};
now += Duration::from_secs(1);
let state = state.apply_event(Event::PhysicalKeyboard(Presence::Present), now);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Hidden,
"Failed to hide: {:?}",
now.saturating_duration_since(start),
);
now += Duration::from_secs(1);
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
now += Duration::from_secs(1);
let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Hidden,
"Failed to remain hidden: {:?}",
now.saturating_duration_since(start),
);
now += Duration::from_secs(1);
let state = state.apply_event(Event::PhysicalKeyboard(Presence::Missing), now);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Visible,
"Failed to appear: {:?}",
now.saturating_duration_since(start),
);
}
}

View File

@ -4,12 +4,20 @@
#include "input-method-unstable-v2-client-protocol.h"
#include "virtual-keyboard-unstable-v1-client-protocol.h"
#include "eek/eek-types.h"
#include "main.h"
#include "src/ui_manager.h"
struct submission;
struct squeek_layout;
struct submission* get_submission(struct zwp_input_method_manager_v2 *immanager,
struct zwp_virtual_keyboard_manager_v1 *vkmanager,
struct vis_manager *vis_manager,
struct wl_seat *seat,
EekboardContextService *state);
// Defined in Rust
struct submission* submission_new(struct zwp_input_method_v2 *im, struct zwp_virtual_keyboard_v1 *vk, EekboardContextService *state, struct vis_manager *vis_manager);
uint8_t submission_hint_available(struct submission *self);
void submission_set_ui(struct submission *self, ServerContextService *ui_context);
void submission_use_layout(struct submission *self, struct squeek_layout *layout, uint32_t time);
#endif

View File

@ -19,13 +19,12 @@
use std::collections::HashSet;
use std::ffi::CString;
use crate::vkeyboard::c::ZwpVirtualKeyboardV1;
use ::action::Modifier;
use ::imservice;
use ::imservice::IMService;
use ::keyboard::{ KeyCode, KeyStateId, Modifiers, PressType };
use ::layout;
use ::ui_manager::VisibilityManager;
use ::util::vec_remove;
use ::vkeyboard;
use ::vkeyboard::VirtualKeyboard;
@ -37,29 +36,71 @@ use std::iter::FromIterator;
pub mod c {
use super::*;
use crate::util::c::Wrapped;
use std::os::raw::c_void;
pub type Submission = Wrapped<super::Submission>;
use ::imservice::c::InputMethod;
use ::util::c::Wrapped;
use ::vkeyboard::c::ZwpVirtualKeyboardV1;
// The following defined in C
/// EekboardContextService*
#[repr(transparent)]
pub struct StateManager(*const c_void);
#[no_mangle]
pub extern "C"
fn submission_new(
im: *mut InputMethod,
vk: ZwpVirtualKeyboardV1,
state_manager: *const StateManager,
visibility_manager: Wrapped<VisibilityManager>,
) -> *mut Submission {
let imservice = if im.is_null() {
None
} else {
let visibility_manager = visibility_manager.clone_ref();
Some(IMService::new(
im,
state_manager,
Box::new(move |active| visibility_manager.borrow_mut().set_im_active(active)),
))
};
// TODO: add vkeyboard too
Box::<Submission>::into_raw(Box::new(
Submission {
imservice,
modifiers_active: Vec::new(),
virtual_keyboard: VirtualKeyboard(vk),
pressed: Vec::new(),
keymap_fds: Vec::new(),
keymap_idx: None,
}
))
}
#[no_mangle]
pub extern "C"
fn submission_use_layout(
submission: Submission,
submission: *mut Submission,
layout: *const layout::Layout,
time: u32,
) {
let submission = submission.clone_ref();
let mut submission = submission.borrow_mut();
if submission.is_null() {
panic!("Null submission pointer");
}
let submission: &mut Submission = unsafe { &mut *submission };
let layout = unsafe { &*layout };
submission.use_layout(layout, Timestamp(time));
}
#[no_mangle]
pub extern "C"
fn submission_hint_available(submission: Submission) -> u8 {
let submission = submission.clone_ref();
let submission = submission.borrow();
fn submission_hint_available(submission: *mut Submission) -> u8 {
if submission.is_null() {
panic!("Null submission pointer");
}
let submission: &mut Submission = unsafe { &mut *submission };
let active = submission.imservice.as_ref()
.map(|imservice| imservice.is_active());
(Some(true) == active) as u8
@ -92,17 +133,6 @@ pub enum SubmitData<'a> {
}
impl Submission {
pub fn new(vk: ZwpVirtualKeyboardV1, imservice: Option<Box<IMService>>) -> Self {
Submission {
imservice,
modifiers_active: Vec::new(),
virtual_keyboard: VirtualKeyboard(vk),
pressed: Vec::new(),
keymap_fds: Vec::new(),
keymap_idx: None,
}
}
/// Sends a submit text event if possible;
/// otherwise sends key press and makes a note of it
pub fn handle_press(

View File

@ -5,7 +5,6 @@
#include "eek/eek-types.h"
#include "outputs.h"
#include "main.h"
struct ui_manager;
@ -15,5 +14,7 @@ uint32_t squeek_uiman_get_perceptual_height(struct ui_manager *uiman);
struct vis_manager;
struct vis_manager *squeek_visman_new(struct squeek_state_manager *state_manager);
struct vis_manager *squeek_visman_new(void);
void squeek_visman_set_ui(struct vis_manager *visman, ServerContextService *ui_context);
void squeek_visman_set_keyboard_present(struct vis_manager *visman, uint32_t keyboard_present);
#endif

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020, 2021 Purism SPC
/* Copyright (C) 2020 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
@ -10,11 +10,49 @@
use std::cmp::min;
use ::outputs::c::OutputHandle;
pub mod c {
use super::*;
use std::os::raw::c_void;
use ::util::c::Wrapped;
/// ServerContextService*
#[repr(transparent)]
pub struct UIManager(*const c_void);
extern "C" {
pub fn server_context_service_update_visible(imservice: *const UIManager, active: u32);
pub fn server_context_service_release_visibility(imservice: *const UIManager);
}
#[no_mangle]
pub extern "C"
fn squeek_visman_new() -> Wrapped<VisibilityManager> {
Wrapped::new(VisibilityManager {
ui_manager: None,
visibility_state: VisibilityFactors {
im_active: false,
physical_keyboard_present: false,
}
})
}
/// Use to initialize the UI reference
#[no_mangle]
pub extern "C"
fn squeek_visman_set_ui(visman: Wrapped<VisibilityManager>, ui_manager: *const UIManager) {
let visman = visman.clone_ref();
let mut visman = visman.borrow_mut();
visman.set_ui_manager(Some(ui_manager))
}
#[no_mangle]
pub extern "C"
fn squeek_visman_set_keyboard_present(visman: Wrapped<VisibilityManager>, present: u32) {
let visman = visman.clone_ref();
let mut visman = visman.borrow_mut();
visman.set_keyboard_present(present != 0)
}
#[no_mangle]
pub extern "C"
fn squeek_uiman_new() -> Wrapped<Manager> {
@ -80,3 +118,131 @@ impl Manager {
}
}
}
#[derive(PartialEq, Debug)]
enum Visibility {
Hidden,
Visible,
}
#[derive(Debug)]
enum VisibilityTransition {
/// Hide immediately
Hide,
/// Hide if no show request comes soon
Release,
/// Show instantly
Show,
/// Don't do anything
NoTransition,
}
/// Contains visibility policy
#[derive(Clone, Debug)]
struct VisibilityFactors {
im_active: bool,
physical_keyboard_present: bool,
}
impl VisibilityFactors {
/// Static policy.
/// Use when transitioning from an undefined state (e.g. no UI before).
fn desired(&self) -> Visibility {
match self {
VisibilityFactors {
im_active: true,
physical_keyboard_present: false,
} => Visibility::Visible,
_ => Visibility::Hidden,
}
}
/// Stateful policy
fn transition_to(&self, next: &Self) -> VisibilityTransition {
use self::Visibility::*;
let im_deactivation = self.im_active && !next.im_active;
match (self.desired(), next.desired(), im_deactivation) {
(Visible, Hidden, true) => VisibilityTransition::Release,
(Visible, Hidden, _) => VisibilityTransition::Hide,
(Hidden, Visible, _) => VisibilityTransition::Show,
_ => VisibilityTransition::NoTransition,
}
}
}
// Temporary struct for migration. Should be integrated with Manager eventually.
pub struct VisibilityManager {
/// Owned reference. Be careful, it's shared with C at large
ui_manager: Option<*const c::UIManager>,
visibility_state: VisibilityFactors,
}
impl VisibilityManager {
fn set_ui_manager(&mut self, ui_manager: Option<*const c::UIManager>) {
let new = VisibilityManager {
ui_manager,
..unsafe { self.clone() }
};
self.apply_changes(new);
}
fn apply_changes(&mut self, new: Self) {
if let Some(ui) = &new.ui_manager {
if self.ui_manager.is_none() {
// Previous state was never applied, so effectively undefined.
// Just apply the new one.
let new_state = new.visibility_state.desired();
unsafe {
c::server_context_service_update_visible(
*ui,
(new_state == Visibility::Visible) as u32,
);
}
} else {
match self.visibility_state.transition_to(&new.visibility_state) {
VisibilityTransition::Hide => unsafe {
c::server_context_service_update_visible(*ui, 0);
},
VisibilityTransition::Show => unsafe {
c::server_context_service_update_visible(*ui, 1);
},
VisibilityTransition::Release => unsafe {
c::server_context_service_release_visibility(*ui);
},
VisibilityTransition::NoTransition => {}
}
}
}
*self = new;
}
pub fn set_im_active(&mut self, im_active: bool) {
let new = VisibilityManager {
visibility_state: VisibilityFactors {
im_active,
..self.visibility_state.clone()
},
..unsafe { self.clone() }
};
self.apply_changes(new);
}
pub fn set_keyboard_present(&mut self, keyboard_present: bool) {
let new = VisibilityManager {
visibility_state: VisibilityFactors {
physical_keyboard_present: keyboard_present,
..self.visibility_state.clone()
},
..unsafe { self.clone() }
};
self.apply_changes(new);
}
/// The struct is not really safe to clone due to the ui_manager reference.
/// This is only a helper for getting desired visibility.
unsafe fn clone(&self) -> Self {
VisibilityManager {
ui_manager: self.ui_manager.clone(),
visibility_state: self.visibility_state.clone(),
}
}
}

View File

@ -58,15 +58,12 @@ foreach layout : [
'us', 'us_wide',
# Block: Languages
'am', 'am+phonetic',
'ara', 'ara_wide',
'be', 'be_wide',
'bg',
'bg+phonetic',
'br',
'ch+fr',
'ch+de',
'ch', 'ch_wide',
'cz', 'cz_wide',
'cz+qwerty', 'cz+qwerty_wide',
'de', 'de_wide',
@ -93,14 +90,12 @@ foreach layout : [
# Terminal keyboards
'terminal/fr',
'terminal/fr_wide',
'terminal/us',
'terminal/us_wide',
# Block: Not languages.
'emoji/us',
'number/us',
'pin/us',
]
extra = []
if layout.startswith('emoji/')

View File

@ -1,13 +1,10 @@
#!/usr/bin/env python3
import gi
import random
import sys
gi.require_version('Gtk', '3.0')
gi.require_version('GLib', '2.0')
from gi.repository import Gtk
from gi.repository import GLib
try:
terminal = [("Terminal", Gtk.InputPurpose.TERMINAL)]
@ -17,8 +14,6 @@ except AttributeError:
def new_grid(items, set_type):
grid = Gtk.Grid(orientation='vertical', column_spacing=8, row_spacing=8)
grid.props.margin = 6
i = 0
for text, value in items:
l = Gtk.Label(label=text)
@ -49,41 +44,17 @@ class App(Gtk.Application):
("OSK provided", Gtk.InputHints.INHIBIT_OSK)
]
def on_purpose_toggled(self, btn, entry):
purpose = Gtk.InputPurpose.PIN if btn.get_active() else Gtk.InputPurpose.PASSWORD
entry.set_input_purpose(purpose)
def on_timeout(self, e):
r = random.randint(0, len(self.purposes) - 1)
(_, purpose) = self.purposes[r]
print(f"Setting {purpose}")
e.set_input_purpose(purpose)
return True
def add_random (self, grid):
l = Gtk.Label(label="Random")
e = Gtk.Entry(hexpand=True)
e.set_input_purpose(Gtk.InputPurpose.FREE_FORM)
grid.attach(l, 0, len(self.purposes), 1, 1)
grid.attach(e, 1, len(self.purposes), 1, 1)
GLib.timeout_add_seconds (3, self.on_timeout, e)
def do_activate(self):
w = Gtk.ApplicationWindow(application=self)
w.set_default_size (300, 500)
notebook = Gtk.Notebook()
def add_purpose(entry, purpose):
entry.set_input_purpose(purpose)
def add_hint(entry, hint):
entry.set_input_hints(hint)
purpose_grid = new_grid(self.purposes, add_purpose)
self.add_random(purpose_grid)
hint_grid = new_grid(self.hints, add_hint)
purpose_scroll = Gtk.ScrolledWindow()
purpose_scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
purpose_scroll.add(purpose_grid)
notebook.append_page(purpose_scroll, Gtk.Label(label="Purposes"))
notebook.append_page(purpose_grid, Gtk.Label(label="Purposes"))
notebook.append_page(hint_grid, Gtk.Label(label="Hints"))
w.add(notebook)
w.show_all()

View File

@ -1,15 +0,0 @@
#!/bin/sh
# Enforces style check for the C parts of the project.
if [ -z "$1" ]; then
echo "Please pass build directory to check."
exit 1
fi
cd "$1"
clang-tidy --checks=-clang-diagnostic-missing-braces,readability-braces-around-statements, \
--warnings-as-errors=readability-braces-around-statements \
-extra-arg=-Wno-unknown-warning-option \
../src/*.c ../eek/*.c ../eekboard/*.c

View File

@ -1,12 +0,0 @@
#!/bin/sh
set -e
# Enforces style check for the project.
THIS=$(realpath $0)
TOOLS=$(dirname $THIS)
cd $TOOLS/..
# The CI file seems to be touched regularly, and causing problems often,
# unlike layout files.
./tools/yamlfmt ./.gitlab-ci.yml $1

View File

@ -1,44 +0,0 @@
#!/usr/bin/env python3
"""Checks YAML files for correct formatting.
Usage: yamlfmt.py [--apply] file.yaml
"""
import io
from ruamel.yaml import YAML
import sys
args = sys.argv[:]
try:
args.remove('--apply')
want_apply = True
except ValueError:
want_apply = False
path = args[1]
def dump(yaml, yml):
buf = io.BytesIO()
yaml.dump(yml, buf)
return buf.getvalue().decode('utf-8')
with open(path) as f:
contents = f.read()
yaml = YAML()
yaml.indent(offset=2, sequence=4)
yml = yaml.load(contents)
formatted = dump(yaml, yml)
well_formatted = formatted == contents
if not well_formatted:
print('The yaml file is not correctly formatted:', path)
if want_apply:
print('Correcting', path)
with open(path, 'w') as f:
f.write(formatted)
else:
print('Please use the following correction:')
print('----------corrected', path)
print(formatted)
print('----------end corrected', path)
sys.exit(1)