diff --git a/data/layouts/ansi.xml b/data/layouts/ansi.xml
new file mode 100644
index 00000000..39e31f0c
--- /dev/null
+++ b/data/layouts/ansi.xml
@@ -0,0 +1,489 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/data/layouts/emoji.xml b/data/layouts/emoji.xml
new file mode 100644
index 00000000..afcf5bed
--- /dev/null
+++ b/data/layouts/emoji.xml
@@ -0,0 +1,2158 @@
+
+
+
+
+
+
+ true
+ true
+
+
+ start
+ 10
+ 5
+ none
+ 4
+ 4
+
+
+ 😀
+ 50
+ 50
+
+
+
+
+
+ 😁
+ 50
+ 50
+
+
+
+
+
+ 😂
+ 50
+ 50
+
+
+
+
+
+ 😃
+ 50
+ 50
+
+
+
+
+
+ 😄
+ 50
+ 50
+
+
+
+
+
+ 😅
+ 50
+ 50
+
+
+
+
+
+ 😆
+ 50
+ 50
+
+
+
+
+
+ 😇
+ 50
+ 50
+
+
+
+
+
+ 😈
+ 50
+ 50
+
+
+
+
+
+ 😉
+ 50
+ 50
+
+
+
+
+
+ 😊
+ 50
+ 50
+
+
+
+
+
+ 😋
+ 50
+ 50
+
+
+
+
+
+ 😌
+ 50
+ 50
+
+
+
+
+
+ 😍
+ 50
+ 50
+
+
+
+
+
+ 😎
+ 50
+ 50
+
+
+
+
+
+ 😏
+ 50
+ 50
+
+
+
+
+
+ 😐
+ 50
+ 50
+
+
+
+
+
+ 😑
+ 50
+ 50
+
+
+
+
+
+ 😒
+ 50
+ 50
+
+
+
+
+
+ 😓
+ 50
+ 50
+
+
+
+
+
+ 😔
+ 50
+ 50
+
+
+
+
+
+ 😕
+ 50
+ 50
+
+
+
+
+
+ 😖
+ 50
+ 50
+
+
+
+
+
+ 😗
+ 50
+ 50
+
+
+
+
+
+ 😘
+ 50
+ 50
+
+
+
+
+
+ 😙
+ 50
+ 50
+
+
+
+
+
+ 😚
+ 50
+ 50
+
+
+
+
+
+ 😛
+ 50
+ 50
+
+
+
+
+
+ 😜
+ 50
+ 50
+
+
+
+
+
+ 😝
+ 50
+ 50
+
+
+
+
+
+ 😞
+ 50
+ 50
+
+
+
+
+
+ 😟
+ 50
+ 50
+
+
+
+
+
+ 😠
+ 50
+ 50
+
+
+
+
+
+ 😡
+ 50
+ 50
+
+
+
+
+
+ 😢
+ 50
+ 50
+
+
+
+
+
+ 😣
+ 50
+ 50
+
+
+
+
+
+ 😤
+ 50
+ 50
+
+
+
+
+
+ 😥
+ 50
+ 50
+
+
+
+
+
+ 😦
+ 50
+ 50
+
+
+
+
+
+ 😧
+ 50
+ 50
+
+
+
+
+
+ 😨
+ 50
+ 50
+
+
+
+
+
+ 😩
+ 50
+ 50
+
+
+
+
+
+ 😪
+ 50
+ 50
+
+
+
+
+
+ 😫
+ 50
+ 50
+
+
+
+
+
+ 😬
+ 50
+ 50
+
+
+
+
+
+ 😭
+ 50
+ 50
+
+
+
+
+
+ 😮
+ 50
+ 50
+
+
+
+
+
+ 😯
+ 50
+ 50
+
+
+
+
+
+ 😰
+ 50
+ 50
+
+
+
+
+
+ 😱
+ 50
+ 50
+
+
+
+
+
+ 😲
+ 50
+ 50
+
+
+
+
+
+ 😳
+ 50
+ 50
+
+
+
+
+
+ 😴
+ 50
+ 50
+
+
+
+
+
+ 😵
+ 50
+ 50
+
+
+
+
+
+ 😶
+ 50
+ 50
+
+
+
+
+
+ 😷
+ 50
+ 50
+
+
+
+
+
+ 😸
+ 50
+ 50
+
+
+
+
+
+ 😹
+ 50
+ 50
+
+
+
+
+
+ 😺
+ 50
+ 50
+
+
+
+
+
+ 😻
+ 50
+ 50
+
+
+
+
+
+ 😼
+ 50
+ 50
+
+
+
+
+
+ 😽
+ 50
+ 50
+
+
+
+
+
+ 😾
+ 50
+ 50
+
+
+
+
+
+ 😿
+ 50
+ 50
+
+
+
+
+
+ 🙀
+ 50
+ 50
+
+
+
+
+
+ 🙁
+ 50
+ 50
+
+
+
+
+
+ 🙂
+ 50
+ 50
+
+
+
+
+
+ 🙃
+ 50
+ 50
+
+
+
+
+
+ 🙄
+ 50
+ 50
+
+
+
+
+
+ 🙅
+ 50
+ 50
+
+
+
+
+
+ 🙆
+ 50
+ 50
+
+
+
+
+
+ 🙎
+ 50
+ 50
+
+
+
+
+
+ 🌑
+ 50
+ 50
+
+
+
+
+
+ 🌒
+ 50
+ 50
+
+
+
+
+
+ 🌓
+ 50
+ 50
+
+
+
+
+
+ 🌔
+ 50
+ 50
+
+
+
+
+
+ 🌕
+ 50
+ 50
+
+
+
+
+
+ 🌖
+ 50
+ 50
+
+
+
+
+
+ 🌗
+ 50
+ 50
+
+
+
+
+
+ 🌘
+ 50
+ 50
+
+
+
+
+
+ 🌚
+ 50
+ 50
+
+
+
+
+
+ 🌛
+ 50
+ 50
+
+
+
+
+
+ 🌜
+ 50
+ 50
+
+
+
+
+
+ 🌝
+ 50
+ 50
+
+
+
+
+
+ 🌞
+ 50
+ 50
+
+
+
+
+
+ 🌬
+ 50
+ 50
+
+
+
+
+
+ 🎔
+ 50
+ 50
+
+
+
+
+
+ 🏻
+ 50
+ 50
+
+
+
+
+
+ 🏼
+ 50
+ 50
+
+
+
+
+
+ 🏽
+ 50
+ 50
+
+
+
+
+
+ 🏾
+ 50
+ 50
+
+
+
+
+
+ 🏿
+ 50
+ 50
+
+
+
+
+
+ 🐭
+ 50
+ 50
+
+
+
+
+
+ 🐮
+ 50
+ 50
+
+
+
+
+
+ 🐯
+ 50
+ 50
+
+
+
+
+
+ 🐰
+ 50
+ 50
+
+
+
+
+
+ 🐱
+ 50
+ 50
+
+
+
+
+
+ 🐲
+ 50
+ 50
+
+
+
+
+
+ 🐴
+ 50
+ 50
+
+
+
+
+
+ 🐵
+ 50
+ 50
+
+
+
+
+
+ 🐶
+ 50
+ 50
+
+
+
+
+
+ 🐷
+ 50
+ 50
+
+
+
+
+
+ 🐸
+ 50
+ 50
+
+
+
+
+
+ 🐹
+ 50
+ 50
+
+
+
+
+
+ 🐺
+ 50
+ 50
+
+
+
+
+
+ 🐻
+ 50
+ 50
+
+
+
+
+
+ 🐼
+ 50
+ 50
+
+
+
+
+
+ 💆
+ 50
+ 50
+
+
+
+
+
+ 💑
+ 50
+ 50
+
+
+
+
+
+ 💓
+ 50
+ 50
+
+
+
+
+
+ 💔
+ 50
+ 50
+
+
+
+
+
+ 💕
+ 50
+ 50
+
+
+
+
+
+ 💖
+ 50
+ 50
+
+
+
+
+
+ 💗
+ 50
+ 50
+
+
+
+
+
+ 💘
+ 50
+ 50
+
+
+
+
+
+ 💙
+ 50
+ 50
+
+
+
+
+
+ 💚
+ 50
+ 50
+
+
+
+
+
+ 💛
+ 50
+ 50
+
+
+
+
+
+ 💜
+ 50
+ 50
+
+
+
+
+
+ 💝
+ 50
+ 50
+
+
+
+
+
+ 💞
+ 50
+ 50
+
+
+
+
+
+ 💟
+ 50
+ 50
+
+
+
+
+
+ 💢
+ 50
+ 50
+
+
+
+
+
+ 💤
+ 50
+ 50
+
+
+
+
+
+ 💥
+ 50
+ 50
+
+
+
+
+
+ 💦
+ 50
+ 50
+
+
+
+
+
+ 💨
+ 50
+ 50
+
+
+
+
+
+ 💫
+ 50
+ 50
+
+
+
+
+
+ 💯
+ 50
+ 50
+
+
+
+
+
+ 📧
+ 50
+ 50
+
+
+
+
+
+ 🔅
+ 50
+ 50
+
+
+
+
+
+ 🔆
+ 50
+ 50
+
+
+
+
+
+ 🔗
+ 50
+ 50
+
+
+
+
+
+ 🔞
+ 50
+ 50
+
+
+
+
+
+ 🔠
+ 50
+ 50
+
+
+
+
+
+ 🔡
+ 50
+ 50
+
+
+
+
+
+ 🔢
+ 50
+ 50
+
+
+
+
+
+ 🔣
+ 50
+ 50
+
+
+
+
+
+ 🔤
+ 50
+ 50
+
+
+
+
+
+ 🔰
+ 50
+ 50
+
+
+
+
+
+ 🕅
+ 50
+ 50
+
+
+
+
+
+ 🕉
+ 50
+ 50
+
+
+
+
+
+ 🕐
+ 50
+ 50
+
+
+
+
+
+ 🕑
+ 50
+ 50
+
+
+
+
+
+ 🕒
+ 50
+ 50
+
+
+
+
+
+ 🕓
+ 50
+ 50
+
+
+
+
+
+ 🕔
+ 50
+ 50
+
+
+
+
+
+ 🕕
+ 50
+ 50
+
+
+
+
+
+ 🕖
+ 50
+ 50
+
+
+
+
+
+ 🕗
+ 50
+ 50
+
+
+
+
+
+ 🕘
+ 50
+ 50
+
+
+
+
+
+ 🕙
+ 50
+ 50
+
+
+
+
+
+ 🕚
+ 50
+ 50
+
+
+
+
+
+ 🕛
+ 50
+ 50
+
+
+
+
+
+ 🕜
+ 50
+ 50
+
+
+
+
+
+ 🕝
+ 50
+ 50
+
+
+
+
+
+ 🕞
+ 50
+ 50
+
+
+
+
+
+ 🕟
+ 50
+ 50
+
+
+
+
+
+ 🕠
+ 50
+ 50
+
+
+
+
+
+ 🕡
+ 50
+ 50
+
+
+
+
+
+ 🕢
+ 50
+ 50
+
+
+
+
+
+ 🕣
+ 50
+ 50
+
+
+
+
+
+ 🕤
+ 50
+ 50
+
+
+
+
+
+ 🕥
+ 50
+ 50
+
+
+
+
+
+ 🕦
+ 50
+ 50
+
+
+
+
+
+ 🕧
+ 50
+ 50
+
+
+
+
+
+ 🖤
+ 50
+ 50
+
+
+
+
+
+ 🗚
+ 50
+ 50
+
+
+
+
+
+ 🗛
+ 50
+ 50
+
+
+
+
+
+ 🚬
+ 50
+ 50
+
+
+
+
+
+ 🚭
+ 50
+ 50
+
+
+
+
+
+ 🚮
+ 50
+ 50
+
+
+
+
+
+ 🚯
+ 50
+ 50
+
+
+
+
+
+ 🚰
+ 50
+ 50
+
+
+
+
+
+ 🚱
+ 50
+ 50
+
+
+
+
+
+ 🚹
+ 50
+ 50
+
+
+
+
+
+ 🚺
+ 50
+ 50
+
+
+
+
+
+ 🚼
+ 50
+ 50
+
+
+
+
+
+ 🛉
+ 50
+ 50
+
+
+
+
+
+ 🛊
+ 50
+ 50
+
+
+
+
+
+ 🤍
+ 50
+ 50
+
+
+
+
+
+ 🤎
+ 50
+ 50
+
+
+
+
+
+ 🤐
+ 50
+ 50
+
+
+
+
+
+ 🤑
+ 50
+ 50
+
+
+
+
+
+ 🤒
+ 50
+ 50
+
+
+
+
+
+ 🤓
+ 50
+ 50
+
+
+
+
+
+ 🤔
+ 50
+ 50
+
+
+
+
+
+ 🤕
+ 50
+ 50
+
+
+
+
+
+ 🤖
+ 50
+ 50
+
+
+
+
+
+ 🤗
+ 50
+ 50
+
+
+
+
+
+ 🤠
+ 50
+ 50
+
+
+
+
+
+ 🤡
+ 50
+ 50
+
+
+
+
+
+ 🤢
+ 50
+ 50
+
+
+
+
+
+ 🤤
+ 50
+ 50
+
+
+
+
+
+ 🤥
+ 50
+ 50
+
+
+
+
+
+ 🤦
+ 50
+ 50
+
+
+
+
+
+ 🤧
+ 50
+ 50
+
+
+
+
+
+ 🤨
+ 50
+ 50
+
+
+
+
+
+ 🤩
+ 50
+ 50
+
+
+
+
+
+ 🤪
+ 50
+ 50
+
+
+
+
+
+ 🤫
+ 50
+ 50
+
+
+
+
+
+ 🤬
+ 50
+ 50
+
+
+
+
+
+ 🤭
+ 50
+ 50
+
+
+
+
+
+ 🤮
+ 50
+ 50
+
+
+
+
+
+ 🤯
+ 50
+ 50
+
+
+
+
+
+ 🥰
+ 50
+ 50
+
+
+
+
+
+ 🥱
+ 50
+ 50
+
+
+
+
+
+ 🥲
+ 50
+ 50
+
+
+
+
+
+ 🥳
+ 50
+ 50
+
+
+
+
+
+ 🥴
+ 50
+ 50
+
+
+
+
+
+ 🥵
+ 50
+ 50
+
+
+
+
+
+ 🥶
+ 50
+ 50
+
+
+
+
+
+ 🥸
+ 50
+ 50
+
+
+
+
+
+ 🥹
+ 50
+ 50
+
+
+
+
+
+ 🥺
+ 50
+ 50
+
+
+
+
+
+ 🦁
+ 50
+ 50
+
+
+
+
+
+ 🦄
+ 50
+ 50
+
+
+
+
+
+ 🦊
+ 50
+ 50
+
+
+
+
+
+ 🦒
+ 50
+ 50
+
+
+
+
+
+ 🦓
+ 50
+ 50
+
+
+
+
+
+ 🦰
+ 50
+ 50
+
+
+
+
+
+ 🦱
+ 50
+ 50
+
+
+
+
+
+ 🦲
+ 50
+ 50
+
+
+
+
+
+ 🦳
+ 50
+ 50
+
+
+
+
+
+ 🧐
+ 50
+ 50
+
+
+
+
+
+ 🧡
+ 50
+ 50
+
+
+
+
+
+ ☙
+ 50
+ 50
+
+
+
+
+
+ ☫
+ 50
+ 50
+
+
+
+
+
+ ☮
+ 50
+ 50
+
+
+
+
+
+ ☹
+ 50
+ 50
+
+
+
+
+
+ ☺
+ 50
+ 50
+
+
+
+
+
+ ☻
+ 50
+ 50
+
+
+
+
+
+ ♡
+ 50
+ 50
+
+
+
+
+
+ ♥
+ 50
+ 50
+
+
+
+
+
+ ♲
+ 50
+ 50
+
+
+
+
+
+ ♳
+ 50
+ 50
+
+
+
+
+
+ ♴
+ 50
+ 50
+
+
+
+
+
+ ♵
+ 50
+ 50
+
+
+
+
+
+ ♶
+ 50
+ 50
+
+
+
+
+
+ ♷
+ 50
+ 50
+
+
+
+
+
+ ♸
+ 50
+ 50
+
+
+
+
+
+ ♹
+ 50
+ 50
+
+
+
+
+
+ ♺
+ 50
+ 50
+
+
+
+
+
+ ♻
+ 50
+ 50
+
+
+
+
+
+ ♼
+ 50
+ 50
+
+
+
+
+
+ ♽
+ 50
+ 50
+
+
+
+
+
+ ♿
+ 50
+ 50
+
+
+
+
+
+ ⚀
+ 50
+ 50
+
+
+
+
+
+ ⚁
+ 50
+ 50
+
+
+
+
+
+ ⚂
+ 50
+ 50
+
+
+
+
+
+ ⚃
+ 50
+ 50
+
+
+
+
+
+ ⚄
+ 50
+ 50
+
+
+
+
+
+ ⚅
+ 50
+ 50
+
+
+
+
+
+ ⚛
+ 50
+ 50
+
+
+
+
+
+ ⚭
+ 50
+ 50
+
+
+
+
+
+ ⚮
+ 50
+ 50
+
+
+
+
+
+ ⚯
+ 50
+ 50
+
+
+
+
+
+ ⛢
+ 50
+ 50
+
+
+
+
+
+ ⛯
+ 50
+ 50
+
+
+
+
+
+ ⛻
+ 50
+ 50
+
+
+
+
+
+ ⛼
+ 50
+ 50
+
+
+
+
+
+ ❗
+ 50
+ 50
+
+
+
+
+
+ ❣
+ 50
+ 50
+
+
+
+
+
+ ❤
+ 50
+ 50
+
+
+
+
+
+ ❥
+ 50
+ 50
+
+
+
+
+
+ ❦
+ 50
+ 50
+
+
+
+
+
+ ❧
+ 50
+ 50
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data/layouts/ether.xml b/data/layouts/ether.xml
new file mode 100644
index 00000000..cabb7152
--- /dev/null
+++ b/data/layouts/ether.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ true
+ true
+
+
+ start
+ 10
+ 5
+ none
+ 4
+ 4
+
+
+ ether
+ 50
+ 50
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data/layouts/iso.xml b/data/layouts/iso.xml
new file mode 100644
index 00000000..1a4b9a1a
--- /dev/null
+++ b/data/layouts/iso.xml
@@ -0,0 +1,480 @@
+
+
+
+
+ GTK_ORIENTATION_VERTICAL
+ 10
+ true
+ true
+
+
+ GTK_ORIENTATION_HORIZONTAL
+ true
+ true
+ 10
+
+
+ 50
+ 50
+ TLDE
+
+
+
+
+ 50
+ 50
+ AE01
+
+
+
+
+ 50
+ 50
+ AE02
+
+
+
+
+ 50
+ 50
+ AE03
+
+
+
+
+ 50
+ 50
+ AE04
+
+
+
+
+ 50
+ 50
+ AE05
+
+
+
+
+ 50
+ 50
+ AE06
+
+
+
+
+ 50
+ 50
+ AE07
+
+
+
+
+ 50
+ 50
+ AE08
+
+
+
+
+ 50
+ 50
+ AE09
+
+
+
+
+ 50
+ 50
+ AE10
+
+
+
+
+ 50
+ 50
+ AE11
+
+
+
+
+ 50
+ 50
+ AE12
+
+
+
+
+ 110
+ 50
+ BKSP
+
+
+
+
+
+
+ GTK_ORIENTATION_HORIZONTAL
+ true
+ true
+ 10
+
+
+ 95
+ 50
+ TAB
+
+
+
+
+ 50
+ 50
+ AD01
+
+
+
+
+ 50
+ 50
+ AD02
+
+
+
+
+ 50
+ 50
+ AD03
+
+
+
+
+ 50
+ 50
+ AD04
+
+
+
+
+ 50
+ 50
+ AD05
+
+
+
+
+ 50
+ 50
+ AD06
+
+
+
+
+ 50
+ 50
+ AD07
+
+
+
+
+ 50
+ 50
+ AD08
+
+
+
+
+ 50
+ 50
+ AD09
+
+
+
+
+ 50
+ 50
+ AD10
+
+
+
+
+ 50
+ 50
+ AD11
+
+
+
+
+ 50
+ 50
+ AD12
+
+
+
+
+
+
+ GTK_ORIENTATION_HORIZONTAL
+ true
+ true
+ 10
+
+
+ 105
+ 50
+ CAPS
+
+
+
+
+ 50
+ 50
+ AC01
+
+
+
+
+ 50
+ 50
+ AC02
+
+
+
+
+ 50
+ 50
+ AC03
+
+
+
+
+ 50
+ 50
+ AC04
+
+
+
+
+ 50
+ 50
+ AC05
+
+
+
+
+ 50
+ 50
+ AC06
+
+
+
+
+ 50
+ 50
+ AC07
+
+
+
+
+ 50
+ 50
+ AC08
+
+
+
+
+ 50
+ 50
+ AC09
+
+
+
+
+ 50
+ 50
+ AC10
+
+
+
+
+ 50
+ 50
+ AC11
+
+
+
+
+ 50
+ 50
+ BKSL
+
+
+
+
+ 70
+ 50
+ RTRN
+
+
+
+
+
+
+ GTK_ORIENTATION_HORIZONTAL
+ true
+ true
+ 10
+
+
+ 65
+ 50
+ LFSH
+
+
+
+
+ 50
+ 50
+ LSGT
+
+
+
+
+ 50
+ 50
+ AB01
+
+
+
+
+ 50
+ 50
+ AB02
+
+
+
+
+ 50
+ 50
+ AB03
+
+
+
+
+ 50
+ 50
+ AB04
+
+
+
+
+ 50
+ 50
+ AB05
+
+
+
+
+ 50
+ 50
+ AB06
+
+
+
+
+ 50
+ 50
+ AB07
+
+
+
+
+ 50
+ 50
+ AB08
+
+
+
+
+ 50
+ 50
+ AB09
+
+
+
+
+ 50
+ 50
+ AB10
+
+
+
+
+ 40
+ 65
+ 50
+ UP
+
+
+
+
+
+
+ GTK_ORIENTATION_HORIZONTAL
+ true
+ true
+ 10
+
+
+ 65
+ 50
+ LCTL
+
+
+
+
+ 65
+ 50
+ LWIN
+
+
+
+
+ 64
+ 50
+ LALT
+
+
+
+
+ 580
+ 50
+ SPCE
+
+
+
+
+ 65
+ 50
+ 40
+
+ LEFT
+
+
+
+
+ 65
+ 50
+ DOWN
+
+
+
+
+ 65
+ 50
+ RGHT
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data/layouts/numpad.xml b/data/layouts/numpad.xml
new file mode 100644
index 00000000..6f595634
--- /dev/null
+++ b/data/layouts/numpad.xml
@@ -0,0 +1,230 @@
+
+
+
+
+
+ vertical
+ true
+ true
+
+
+ center
+ center
+ 6
+ 6
+ true
+ True
+ True
+
+
+ 50
+ 50
+
+ 0
+ 0
+
+ KPNM
+
+
+
+
+ 50
+ 50
+
+ 1
+ 0
+
+ KPDV
+
+
+
+
+ 50
+ 50
+
+ 2
+ 0
+
+ KPMU
+
+
+
+
+ 50
+ 50
+
+ 3
+ 0
+
+ KPSU
+
+
+
+
+ 50
+ 50
+
+ 0
+ 1
+
+ KP7
+
+
+
+
+ 50
+ 50
+
+ 1
+ 1
+
+ KP8
+
+
+
+
+ 50
+ 50
+
+ 2
+ 1
+
+ KP9
+
+
+
+
+ 50
+ 106
+
+ 3
+ 1
+ 2
+
+ KPAD
+
+
+
+
+ 50
+ 50
+
+ 0
+ 2
+
+ KP4
+
+
+
+
+ 50
+ 50
+
+ 1
+ 2
+
+ KP5
+
+
+
+
+ 50
+ 50
+
+ 2
+ 2
+
+ KP6
+
+
+
+
+ 50
+ 50
+
+ 0
+ 3
+
+ KP1
+
+
+
+
+ 50
+ 50
+
+ 1
+ 3
+
+ KP2
+
+
+
+
+ 50
+ 50
+
+ 2
+ 3
+
+ KP3
+
+
+
+
+ 50
+ 106
+
+ 3
+ 3
+ 2
+
+ KPEN
+
+
+
+
+ 106
+ 50
+
+ 0
+ 4
+ 2
+
+ KP0
+
+
+
+
+ 50
+ 50
+
+ 2
+ 4
+
+ KPDL
+
+
+
+
+
+ 5
+ 3
+ 2
+
+ ESC
+
+
+
+
+
+ 5
+ 0
+
+ BKSP
+
+
+
+
+
+
\ No newline at end of file
diff --git a/data/meson.build b/data/meson.build
index a74bd76d..21d2636b 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -26,4 +26,10 @@ install_data('wf-locker', install_dir:'/etc/pam.d/')
install_data('xdpw/wayfire', install_dir: '/etc/xdg/xdg-desktop-portal-wlr/')
+install_data(join_paths('layouts', 'ansi.xml'), install_dir: layout_dir)
+install_data(join_paths('layouts', 'iso.xml'), install_dir: layout_dir)
+install_data(join_paths('layouts', 'emoji.xml'), install_dir: layout_dir)
+install_data(join_paths('layouts', 'ether.xml'), install_dir: layout_dir)
+install_data(join_paths('layouts', 'numpad.xml'), install_dir: layout_dir)
+
subdir('css')
diff --git a/meson.build b/meson.build
index da74da51..2ee2bb75 100644
--- a/meson.build
+++ b/meson.build
@@ -1,16 +1,16 @@
project(
- 'wf-shell',
- 'c',
- 'cpp',
- version: '0.11.0',
- license: 'MIT',
- meson_version: '>=0.51.0',
- default_options: [
- 'cpp_std=c++17',
- 'c_std=c11',
- 'warning_level=2',
- 'werror=false',
- ],
+ 'wf-shell',
+ 'c',
+ 'cpp',
+ version: '0.11.0',
+ license: 'MIT',
+ meson_version: '>=0.51.0',
+ default_options: [
+ 'cpp_std=c++17',
+ 'c_std=c11',
+ 'warning_level=2',
+ 'werror=false',
+ ],
)
wayfire = dependency('wayfire')
@@ -22,7 +22,7 @@ wfconfig = dependency('wf-config', version: '>=0.7.0') #TODO fallback submodule
epoxy = dependency('epoxy')
gtklayershell = dependency('gtk4-layer-shell-0', version: '>=1.3.0', required: false)
if not gtklayershell.found()
- gtklayershell = dependency('gtk4-layer-shell', fallback: ['gtk4-layer-shell'])
+ gtklayershell = dependency('gtk4-layer-shell', fallback: ['gtk4-layer-shell'])
endif
libpulse = dependency('libpulse', required: get_option('volume-widget'))
libgvc = subproject('gvc', default_options: ['static=true'], required: get_option('volume-widget'))
@@ -30,55 +30,68 @@ pipewire = dependency('libpipewire-0.3', required: get_option('wp-mixer-widget')
wireplumber = dependency('wireplumber-0.5', required: get_option('wp-mixer-widget'))
ddcutil = dependency('ddcutil', required: get_option('ddcutil'))
dbusmenu_gtk = dependency('dbusmenu-glib-0.4')
+xkb = dependency('xkbcommon')
xkbregistry = dependency('xkbregistry')
json = subproject('wf-json').get_variable('wfjson')
openssl = dependency('openssl')
gbm = dependency('gbm', required: get_option('live-previews-dmabuf'))
drm = dependency('libdrm', required: get_option('live-previews-dmabuf'))
+enchant = dependency('enchant-2', required: false)
+llama = dependency('llama', required: false)
if get_option('wayland-logout') == true
- wayland_logout = subproject('wayland-logout')
+ wayland_logout = subproject('wayland-logout')
endif
if get_option('weather') == true
- subproject('owf')
- add_project_arguments('-DHAVE_WEATHER=1', language: 'cpp')
+ subproject('owf')
+ add_project_arguments('-DHAVE_WEATHER=1', language: 'cpp')
endif
if gbm.found() and drm.found() and not get_option('live-previews-dmabuf').disabled()
- add_project_arguments('-DHAVE_DMABUF=1', language: 'cpp')
+ add_project_arguments('-DHAVE_DMABUF=1', language: 'cpp')
endif
if libpulse.found()
- libgvc = libgvc.get_variable('libgvc_dep')
- add_project_arguments('-DHAVE_PULSE=1', language: 'cpp')
+ libgvc = libgvc.get_variable('libgvc_dep')
+ add_project_arguments('-DHAVE_PULSE=1', language: 'cpp')
endif
if wireplumber.found()
- add_project_arguments('-DHAVE_WIREPLUMBER=1', language: 'cpp')
+ add_project_arguments('-DHAVE_WIREPLUMBER=1', language: 'cpp')
endif
if ddcutil.found()
- add_project_arguments('-DHAVE_DDCUTIL=1', language: 'cpp')
+ add_project_arguments('-DHAVE_DDCUTIL=1', language: 'cpp')
+endif
+
+if enchant.found()
+ add_project_arguments('-DHAVE_ENCHANT=1', language: 'cpp')
+endif
+
+if llama.found()
+ add_project_arguments('-DHAVE_LLAMA=1', language: 'cpp')
endif
needs_libinotify = ['freebsd', 'dragonfly'].contains(host_machine.system())
libinotify = dependency('libinotify', required: needs_libinotify)
add_project_arguments(
- ['-Wno-pedantic', '-Wno-unused-parameter', '-Wno-parentheses'],
- language: 'cpp',
+ ['-Wno-pedantic', '-Wno-unused-parameter', '-Wno-parentheses'],
+ language: 'cpp',
)
resource_dir = join_paths(get_option('prefix'), 'share', 'wf-shell')
metadata_dir = join_paths(resource_dir, 'metadata')
sysconf_dir = join_paths(get_option('prefix'), get_option('sysconfdir'))
+layout_dir = join_paths(get_option('prefix'), 'share', 'wf-osk', 'layouts')
icon_dir = join_paths(get_option('prefix'), 'share', 'wf-shell', 'icons')
add_project_arguments('-DICONDIR="' + icon_dir + '"', language: 'cpp')
add_project_arguments('-DRESOURCEDIR="' + resource_dir + '"', language: 'cpp')
add_project_arguments('-DMETADATA_DIR="' + metadata_dir + '"', language: 'cpp')
add_project_arguments('-DSYSCONF_DIR="' + sysconf_dir + '"', language: 'cpp')
+add_project_arguments('-DLAYOUT_DIR="' + layout_dir + '"', language: 'cpp')
subdir('metadata')
subdir('proto')
diff --git a/metadata/meson.build b/metadata/meson.build
index 329a2314..5910d26a 100644
--- a/metadata/meson.build
+++ b/metadata/meson.build
@@ -1,10 +1,11 @@
configure_file(input: 'background.xml.in', output: 'background.xml',
- configuration: {
- 'wallpaper': join_paths(resource_dir, 'backgrounds')
+ configuration: {
+ 'wallpaper': join_paths(resource_dir, 'backgrounds')
},
- install: true,
- install_dir: metadata_dir)
+ install: true,
+ install_dir: metadata_dir)
+install_data('osk.xml', install_dir: metadata_dir)
install_data('dock.xml', install_dir: metadata_dir)
install_data('panel.xml', install_dir: metadata_dir)
install_data('locker.xml', install_dir: metadata_dir)
diff --git a/metadata/osk.xml b/metadata/osk.xml
new file mode 100644
index 00000000..9d0cfc3e
--- /dev/null
+++ b/metadata/osk.xml
@@ -0,0 +1,81 @@
+
+
+
+ <_short>Onscreen Keyboard
+ Shell
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/proto/input-method-unstable-v2.xml b/proto/input-method-unstable-v2.xml
new file mode 100644
index 00000000..1853f69a
--- /dev/null
+++ b/proto/input-method-unstable-v2.xml
@@ -0,0 +1,494 @@
+
+
+
+
+ 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.
+ Copyright © 2018 Purism SPC
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice (including the next
+ paragraph) shall be included in all copies or substantial portions of the
+ Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+
+
+ This protocol allows applications to act as input methods for compositors.
+
+ An input method context is used to manage the state of the input method.
+
+ Text strings are UTF-8 encoded, their indices and lengths are in bytes.
+
+ This document adheres to the RFC 2119 when using words like "must",
+ "should", "may", etc.
+
+ Warning! The protocol described in this file is experimental and
+ backward incompatible changes may be made. Backward compatible changes
+ may be added together with the corresponding interface version bump.
+ Backward incompatible changes are done by bumping the version number in
+ the protocol and interface names and resetting the interface version.
+ Once the protocol is to be declared stable, the 'z' prefix and the
+ version number in the protocol and interface names are removed and the
+ interface version number is reset.
+
+
+
+
+ An input method object allows for clients to compose text.
+
+ The objects connects the client to a text input in an application, and
+ lets the client to serve as an input method for a seat.
+
+ The zwp_input_method_v2 object can occupy two distinct states: active and
+ inactive. In the active state, the object is associated to and
+ communicates with a text input. In the inactive state, there is no
+ associated text input, and the only communication is with the compositor.
+ Initially, the input method is in the inactive state.
+
+ Requests issued in the inactive state must be accepted by the compositor.
+ Because of the serial mechanism, and the state reset on activate event,
+ they will not have any effect on the state of the next text input.
+
+ There must be no more than one input method object per seat.
+
+
+
+
+
+
+
+
+ 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 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.
+
+ The surrounding_text, and content_type events 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.
+
+
+
+
+
+ Notification that no focused text input currently needs an active
+ input method on this seat.
+
+ 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.
+
+ 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.
+
+
+
+
+
+ Updates 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
+ purpose of this event.
+
+ The argument text is a buffer containing the preedit string, and must
+ include the cursor position, and the complete selection. It should
+ contain additional characters before and after these. There is a
+ maximum length of wayland messages, so text can not be longer than 4000
+ bytes.
+
+ cursor is the byte offset of the cursor within the text buffer.
+
+ anchor is the byte offset of the selection anchor within the text
+ 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
+ method may assume that the text input does not support this
+ functionality and ignore following surrounding_text events.
+
+ Values set with this event are double-buffered. They will get applied
+ and set to initial values on the next zwp_input_method_v2.done
+ event.
+
+ The initial state for affected fields is empty, meaning that the text
+ input does not support sending surrounding text. If the empty values
+ get applied, subsequent attempts to change them may have no effect.
+
+
+
+
+
+
+
+
+ Tells the input method why the text surrounding the cursor changed.
+
+ Whenever the client detects an external change in text, cursor, or
+ anchor position, it must issue this request to the compositor. This
+ request is intended to give the input method a chance to update the
+ preedit text in an appropriate way, e.g. by removing it when the user
+ starts typing with a keyboard.
+
+ cause describes the source of the change.
+
+ The value set with this event is double-buffered. It will get applied
+ and set to its initial value on the next zwp_input_method_v2.done
+ event.
+
+ The initial value of cause is input_method.
+
+
+
+
+
+
+ Indicates the content type and hint for the current
+ zwp_input_method_v2 instance.
+
+ Values set with this event are double-buffered. They will get applied
+ on the next zwp_input_method_v2.done event.
+
+ The initial value for hint is none, and the initial value for purpose
+ is normal.
+
+
+
+
+
+
+
+ Atomically applies state changes recently sent to the client.
+
+ The done event establishes and updates the state of the client, and
+ must be issued after any changes to apply them.
+
+ Text input state (content purpose, content hint, surrounding text, and
+ change cause) is conceptually double-buffered within an input method
+ context.
+
+ Events modify the pending state, as opposed to the current state in use
+ by the input method. A done event atomically applies all pending state,
+ replacing the current state. After done, the new pending state is as
+ documented for each related request.
+
+ Events must be applied in the order of arrival.
+
+ Neither current nor pending state are modified unless noted otherwise.
+
+
+
+
+
+ Send the commit string text for insertion to the application.
+
+ Inserts a string at current cursor position (see commit event
+ sequence). The string to commit could be either just a single character
+ after a key press or the result of some composing.
+
+ The argument text is a buffer containing the string to insert. There is
+ a maximum length of wayland messages, so text can not be longer than
+ 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.
+
+ The initial value of text is an empty string.
+
+
+
+
+
+
+ Send the pre-edit string text to the application text input.
+
+ Place a new composing text (pre-edit) at the current cursor position.
+ Any previously set composing text must be removed. Any previously
+ existing selected text must be removed. The cursor is moved to a new
+ position within the preedit string.
+
+ The argument text is a buffer containing the preedit string. There is
+ a maximum length of wayland messages, so text can not be longer than
+ 4000 bytes.
+
+ The arguments cursor_begin and cursor_end are counted in bytes relative
+ to the beginning of the submitted string buffer. Cursor should be
+ hidden by the text input when both are equal to -1.
+
+ cursor_begin indicates the beginning of the cursor. cursor_end
+ indicates the end of the cursor. It may be equal or different than
+ cursor_begin.
+
+ Values set with this event are double-buffered. They must be applied on
+ the next zwp_input_method_v2.commit event.
+
+ The initial value of text is an empty string. The initial value of
+ cursor_begin, and cursor_end are both 0.
+
+
+
+
+
+
+
+
+ Remove the surrounding text.
+
+ before_length and after_length are the number of bytes before and after
+ the current cursor index (excluding the preedit text) to delete.
+
+ If any preedit text is present, it is replaced with the cursor for the
+ purpose of this event. In effect before_length is counted from the
+ beginning of preedit text, and after_length from its end (see commit
+ event sequence).
+
+ Values set with this event are double-buffered. They must be applied
+ and reset to initial on the next zwp_input_method_v2.commit request.
+
+ The initial values of both before_length and after_length are 0.
+
+
+
+
+
+
+
+ Apply state changes from commit_string, set_preedit_string and
+ delete_surrounding_text requests.
+
+ The state relating to these events is double-buffered, and each one
+ modifies the pending state. This request replaces the current state
+ with the pending state.
+
+ The connected text input is expected to proceed by evaluating the
+ changes in the following order:
+
+ 1. Replace existing preedit string with the cursor.
+ 2. Delete requested surrounding text.
+ 3. Insert commit string with the cursor at its end.
+ 4. Calculate surrounding text to send.
+ 5. Insert new preedit text in cursor position.
+ 6. Place cursor inside preedit text.
+
+ 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.
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+ Allow an input method to receive hardware keyboard input and process
+ key events to generate text events (with pre-edit) over the wire. This
+ allows input methods which compose multiple key events for inputting
+ text like it is done for CJK languages.
+
+ The compositor should send all keyboard events on the seat to the grab
+ holder via the returned wl_keyboard object. Nevertheless, the
+ compositor may decide not to forward any particular event. The
+ compositor must not further process any event after it has been
+ forwarded to the grab holder.
+
+ Releasing the resulting wl_keyboard object releases the grab.
+
+
+
+
+
+
+ The input method ceased to be available.
+
+ The compositor must issue this event as the only event on the object if
+ there was another input_method object associated with the same seat at
+ the time of its creation.
+
+ The compositor must issue this request when the object is no longer
+ usable, e.g. due to seat removal.
+
+ The input method context becomes inert and should be destroyed after
+ deactivation is handled. Any further requests and events except for the
+ destroy request must be ignored.
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+ This interface marks a surface as 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.
+
+
+
+
+ Notify about the position of the area of the text input expressed as a
+ rectangle in surface local coordinates.
+
+ This is a hint to the input method telling it the relative position of
+ the text being entered.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The zwp_input_method_keyboard_grab_v2 interface represents an exclusive
+ grab of the wl_keyboard interface associated with the seat.
+
+
+
+
+ This event provides a file descriptor to the client which can be
+ memory-mapped to provide a keyboard mapping description.
+
+
+
+
+
+
+
+
+ A key was pressed or released.
+ The time argument is a timestamp with millisecond granularity, with an
+ undefined base.
+
+
+
+
+
+
+
+
+
+ Notifies clients that the modifier and/or group state has changed, and
+ it should update its local state.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+
+
+ The input method manager allows the client to become the input method on
+ a chosen seat.
+
+ No more than one input method must be associated with any seat at any
+ given time.
+
+
+
+
+ Request a new input zwp_input_method_v2 object associated with a given
+ seat.
+
+
+
+
+
+
+
+ Destroys the zwp_input_method_manager_v2 object.
+
+ The zwp_input_method_v2 objects originating from it remain valid.
+
+
+
+
\ No newline at end of file
diff --git a/proto/meson.build b/proto/meson.build
index 28d4037a..03180346 100644
--- a/proto/meson.build
+++ b/proto/meson.build
@@ -21,6 +21,8 @@ client_protocols = [
[wl_protocol_dir, 'staging/ext-image-copy-capture/ext-image-copy-capture-v1.xml'],
'wlr-foreign-toplevel-management-unstable-v1.xml',
'wlr-screencopy.xml',
+ 'virtual-keyboard-unstable-v1.xml',
+ 'input-method-unstable-v2.xml',
wayfire.get_pkgconfig_variable('pkgdatadir') / 'unstable' / 'wayfire-shell-unstable-v2.xml',
]
diff --git a/proto/virtual-keyboard-unstable-v1.xml b/proto/virtual-keyboard-unstable-v1.xml
new file mode 100644
index 00000000..5095c91b
--- /dev/null
+++ b/proto/virtual-keyboard-unstable-v1.xml
@@ -0,0 +1,113 @@
+
+
+
+ Copyright © 2008-2011 Kristian Høgsberg
+ Copyright © 2010-2013 Intel Corporation
+ Copyright © 2012-2013 Collabora, Ltd.
+ Copyright © 2018 Purism SPC
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice (including the next
+ paragraph) shall be included in all copies or substantial portions of the
+ Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+
+
+
+ The virtual keyboard provides an application with requests which emulate
+ the behaviour of a physical keyboard.
+
+ This interface can be used by clients on its own to provide raw input
+ events, or it can accompany the input method protocol.
+
+
+
+
+ Provide a file descriptor to the compositor which can be
+ memory-mapped to provide a keyboard mapping description.
+
+ Format carries a value from the keymap_format enumeration.
+
+
+
+
+
+
+
+
+
+
+
+
+ A key was pressed or released.
+ The time argument is a timestamp with millisecond granularity, with an
+ undefined base. All requests regarding a single object must share the
+ same clock.
+
+ Keymap must be set before issuing this request.
+
+ State carries a value from the key_state enumeration.
+
+
+
+
+
+
+
+
+ Notifies the compositor that the modifier and/or group state has
+ changed, and it should update state.
+
+ The client should use wl_keyboard.modifiers event to synchronize its
+ internal state with seat state.
+
+ Keymap must be set before issuing this request.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A virtual keyboard manager allows an application to provide keyboard
+ input events as if they came from a physical keyboard.
+
+
+
+
+
+
+
+
+ Creates a new virtual keyboard associated to a seat.
+
+ If the compositor enables a keyboard to perform arbitrary actions, it
+ should present an error when an untrusted client requests a new
+ keyboard.
+
+
+
+
+
+
diff --git a/src/meson.build b/src/meson.build
index 78774b7c..f95671a3 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -5,6 +5,7 @@ subdir('dock')
subdir('locker')
subdir('locker-pin')
subdir('stream-chooser')
+subdir('osk')
pkgconfig = import('pkgconfig')
pkgconfig.generate(
diff --git a/src/osk/complete/complete-enchant.cpp b/src/osk/complete/complete-enchant.cpp
new file mode 100644
index 00000000..9cebee20
--- /dev/null
+++ b/src/osk/complete/complete-enchant.cpp
@@ -0,0 +1,113 @@
+#include "complete/complete-enchant.hpp"
+#include "glibmm/main.h"
+#include "osk.hpp"
+#include
+#include
+
+WayfireOskCompleteEnchant::WayfireOskCompleteEnchant()
+{
+ broker = enchant_broker_init();
+}
+
+WayfireOskCompleteEnchant::~WayfireOskCompleteEnchant()
+{
+ if (current_dict)
+ {
+ enchant_broker_free_dict(broker, current_dict);
+ }
+
+ enchant_broker_free(broker);
+}
+
+void WayfireOskCompleteEnchant::switch_language(std::string short_lang, std::string long_lang)
+{
+ if (short_lang == current_locale)
+ {
+ return;
+ }
+
+ if (current_dict)
+ {
+ enchant_broker_free_dict(broker, current_dict);
+ current_dict = nullptr;
+ }
+
+ current_dict = enchant_broker_request_dict(broker, short_lang.c_str());
+ current_locale = short_lang;
+
+ if (!current_dict)
+ {
+ std::cerr << "Warning: System dictionary missing for locale: " << short_lang << "\n";
+ current_dict = nullptr;
+ return;
+ }
+
+ std::cout << "Successfully loaded dictionary pipeline for: " << short_lang << "\n";
+ return;
+}
+
+void WayfireOskCompleteEnchant::get_suggestions(const std::string surrounding, const std::string partial_word)
+{
+ auto layout = WayfireOsk::get().get_current_layout();
+ /* Only suggest on alpabetical */
+ if ((layout.compare("ansi") != 0) && (layout.compare("iso") != 0))
+ {
+ return;
+ }
+
+ std::thread worker([this, partial_word] ()
+ {
+ std::vector results;
+
+ if (current_dict == nullptr)
+ {
+ std::cout << "No dictionary set" << std::endl;
+ return;
+ }
+
+ int valid = enchant_dict_check(current_dict, partial_word.c_str(), partial_word.length());
+ if (valid == 0)
+ {
+ results.push_back(partial_word);
+ }
+
+ size_t total_suggestions = 0;
+ char **suggest_list = enchant_dict_suggest(
+ current_dict,
+ partial_word.c_str(),
+ partial_word.length(),
+ &total_suggestions);
+ if (suggest_list && (total_suggestions > 0))
+ {
+ /* Comparing uint & int, but suggestion_limit config xml says 0 <= suggestion_limit <= 100 */
+ for (size_t i = 0; i < total_suggestions && results.size() < suggestion_limit; ++i)
+ {
+ if (suggest_list[i] != nullptr)
+ {
+ auto suggest = std::string(suggest_list[i]);
+ if (suggest.length() > partial_word.length())
+ {
+ results.push_back(suggest);
+ }
+ }
+ }
+
+ enchant_dict_free_string_list(current_dict, suggest_list);
+ }
+
+ if (results.size() > 0)
+ {
+ auto current_seq_id = get_next_sequence_id();
+ Glib::signal_idle().connect([results, current_seq_id] ()
+ {
+ WayfireOsk::get().get_window().set_suggestions(results, current_seq_id);
+ return false;
+ });
+ } else
+ {
+ std::cout << "No suggestions" << std::endl;
+ }
+ });
+
+ worker.detach();
+}
diff --git a/src/osk/complete/complete-enchant.hpp b/src/osk/complete/complete-enchant.hpp
new file mode 100644
index 00000000..c3103ba1
--- /dev/null
+++ b/src/osk/complete/complete-enchant.hpp
@@ -0,0 +1,21 @@
+#pragma once
+#include "complete.hpp"
+#include "wf-option-wrap.hpp"
+#include
+#include
+
+class WayfireOskCompleteEnchant : public WayfireOskComplete
+{
+ private:
+ EnchantBroker *broker;
+ EnchantDict *current_dict = nullptr;
+ std::string current_locale = "";
+ WfOption suggestion_limit{"osk/suggestion_limit"};
+
+ public:
+ WayfireOskCompleteEnchant();
+ ~WayfireOskCompleteEnchant();
+
+ void switch_language(std::string short_lang, std::string long_lang);
+ void get_suggestions(const std::string surrounding, const std::string partial_word);
+};
diff --git a/src/osk/complete/complete-tiny.cpp b/src/osk/complete/complete-tiny.cpp
new file mode 100644
index 00000000..aff44270
--- /dev/null
+++ b/src/osk/complete/complete-tiny.cpp
@@ -0,0 +1,176 @@
+#include "complete-tiny.hpp"
+#include "glibmm/main.h"
+#include
+#include
+#include
+#include
+#include "osk.hpp"
+
+
+WayfireOskCompleteTinyLlama::WayfireOskCompleteTinyLlama()
+{
+ llama_backend_init();
+ std::string model_path = llama_file;
+
+ switch_language("en_US", "English (American)");
+
+ auto mparams = llama_model_default_params();
+ model = llama_model_load_from_file(model_path.c_str(), mparams);
+ if (!model)
+ {
+ std::cerr << "failed loading model " << model_path << "\n";
+ return;
+ }
+}
+
+WayfireOskCompleteTinyLlama::~WayfireOskCompleteTinyLlama()
+{
+ if (model)
+ {
+ llama_model_free(model);
+ model = nullptr;
+ }
+
+ llama_backend_free();
+}
+
+void WayfireOskCompleteTinyLlama::switch_language(std::string short_lang, std::string long_lang)
+{
+ system_prompt = "You are user facing spell checker and autocorrect. Language: " + long_lang +
+ ". Do not say ether";
+}
+
+void WayfireOskCompleteTinyLlama::get_suggestions(const std::string surrounding_text,
+ const std::string partial_word)
+{
+ std::thread worker([this, surrounding_text, partial_word] ()
+ {
+ std::vector suggestions;
+ if (!model)
+ {
+ return;
+ }
+
+ const llama_vocab *vocab = llama_model_get_vocab(model);
+ if (!vocab)
+ {
+ return;
+ }
+
+ auto cparams = llama_context_default_params();
+ cparams.n_ctx = 512;
+ cparams.n_batch = 512;
+ llama_context *local_ctx = llama_init_from_model(model, cparams);
+ if (!local_ctx)
+ {
+ return;
+ }
+
+ std::string prompt_instruction;
+ if (partial_word.empty())
+ {
+ prompt_instruction = "What is the next word in this text: " +
+ surrounding_text;
+ } else
+ {
+ prompt_instruction =
+ "Assuming the last word is not fully typed yet, what is the full last word of this text: " +
+ surrounding_text;
+ }
+
+ std::string full_prompt = "<|system|>\n" + system_prompt + "\n" +
+ "<|user|>\n" + prompt_instruction;
+
+ std::vector tokens;
+ tokens.resize(full_prompt.size() + 4);
+ int n_tokens =
+ llama_tokenize(vocab, full_prompt.c_str(), full_prompt.size(), tokens.data(), tokens.size(), true,
+ true);
+ tokens.resize(n_tokens);
+
+ if (tokens.empty())
+ {
+ llama_free(local_ctx);
+ Glib::signal_idle().connect([] ()
+ {
+ WayfireOsk::get().get_window().clear_suggestions();
+ return false;
+ });
+ return;
+ }
+
+ llama_batch batch = llama_batch_get_one(tokens.data(), tokens.size());
+
+ if (llama_decode(local_ctx, batch) == 0)
+ {
+ auto *logits = llama_get_logits_ith(local_ctx, batch.n_tokens - 1);
+ int n_vocab = llama_vocab_n_tokens(vocab);
+
+ std::vector> candidates;
+ candidates.reserve(n_vocab);
+ for (int id = 0; id < n_vocab; ++id)
+ {
+ candidates.push_back({logits[id], id});
+ }
+
+ std::sort(candidates.rbegin(), candidates.rend());
+ /* Comparing uint & int, but suggestion_limit config xml says 0 <= suggestion_limit <= 100 */
+ for (size_t i = 0; i < candidates.size() && suggestions.size() < suggestion_limit; ++i)
+ {
+ llama_token tok = candidates[i].second;
+ if ((tok == llama_vocab_eos(vocab)) || (tok == llama_vocab_nl(vocab)))
+ {
+ continue;
+ }
+
+ char buf[128] = {0};
+ int len = llama_token_to_piece(vocab, tok, buf, sizeof(buf), 0, true);
+ if (len > 0)
+ {
+ std::string word(buf, len);
+ word.erase(std::remove(word.begin(), word.end(), '\n'), word.end());
+
+ Glib::ustring word_utf8 = word;
+
+ bool valid = false;
+ for (auto c : word_utf8)
+ {
+ if (g_unichar_isalpha(c))
+ {
+ valid = true;
+ break;
+ }
+ }
+
+ if (!valid)
+ {
+ continue;
+ }
+
+ auto start = word.find_first_not_of(" \t");
+ auto end = word.find_last_not_of(" \t");
+ word = (start == std::string::npos) ? "" : word.substr(start, end - start + 1);
+
+ if (!word.empty() &&
+ (std::find(suggestions.begin(), suggestions.end(), word) == suggestions.end()))
+ {
+ suggestions.push_back(word);
+ }
+ }
+ }
+ }
+
+ llama_free(local_ctx);
+ if (suggestions.size() > 0)
+ {
+ uint64_t current_seq_id = get_next_sequence_id();
+
+ Glib::signal_idle().connect([suggestions, current_seq_id] ()
+ {
+ WayfireOsk::get().get_window().set_suggestions(suggestions, current_seq_id);
+ return false;
+ });
+ }
+ });
+ worker.detach();
+}
diff --git a/src/osk/complete/complete-tiny.hpp b/src/osk/complete/complete-tiny.hpp
new file mode 100644
index 00000000..ae82ee9d
--- /dev/null
+++ b/src/osk/complete/complete-tiny.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "complete/complete.hpp"
+#include "wf-option-wrap.hpp"
+#include
+#include
+class WayfireOskCompleteTinyLlama : public WayfireOskComplete
+{
+ private:
+ llama_model *model = nullptr;
+ std::string system_prompt = "";
+ WfOption llama_file{"osk/llama_file"};
+ WfOption suggestion_limit{"osk/suggestion_limit"};
+
+ public:
+ WayfireOskCompleteTinyLlama();
+ ~WayfireOskCompleteTinyLlama();
+
+ void switch_language(std::string short_land, std::string long_land) override;
+ void get_suggestions(const std::string surrounding_text, const std::string partial_word) override;
+};
diff --git a/src/osk/complete/complete.hpp b/src/osk/complete/complete.hpp
new file mode 100644
index 00000000..bd98e537
--- /dev/null
+++ b/src/osk/complete/complete.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include
+#include
+#include
+
+/* Abstract definition of an autocompleter
+ * get_suggestions takes text leading up to and a partial word.
+ * Both MAY be blank.
+ *
+ * get_suggestions MUST spawn a worker to process data off-thread.
+ *
+ * When it finishes processing it has two possible actions:
+ * - WayfireOsk::get().get_window().set_suggestions(results, current_seq_id);
+ * - WayfireOsk::get().get_window().clear_suggestions();
+ *
+ * Furthermore, if you are sending a current_seq_id you MUST increment in beforehand. Sending suggestions
+ * with a lower seq_id than
+ * once already accepted will automatically throw it away believing it to be out of date
+ */
+
+class WayfireOskComplete
+{
+ protected:
+ std::atomic sequence_counter{0};
+ uint64_t get_next_sequence_id()
+ {
+ return ++sequence_counter;
+ }
+
+ public:
+ WayfireOskComplete()
+ {}
+ ~WayfireOskComplete()
+ {}
+ virtual void switch_language(std::string short_lang, std::string long_lang) = 0;
+ virtual void get_suggestions(const std::string surrounding_text, const std::string partial_word) = 0;
+};
+
+class WayfireOskCompleteNull : public WayfireOskComplete
+{
+ void switch_language(std::string, std::string) override
+ {}
+ void get_suggestions(const std::string surrounding, const std::string partial_word) override
+ {}
+};
diff --git a/src/osk/display.cpp b/src/osk/display.cpp
new file mode 100644
index 00000000..82ad0c6b
--- /dev/null
+++ b/src/osk/display.cpp
@@ -0,0 +1,72 @@
+#include "display.hpp"
+#include "gdk/gdk.h"
+#include "gdk/wayland/gdkwayland.h"
+#include
+#include
+
+static void registry_add_object(void *data, struct wl_registry *registry,
+ uint32_t name, const char *interface, uint32_t version)
+{
+ auto display = static_cast(data);
+ if (strcmp(interface, zwf_shell_manager_v2_interface.name) == 0)
+ {
+ display->zwf_manager =
+ (zwf_shell_manager_v2*)wl_registry_bind(registry, name,
+ &zwf_shell_manager_v2_interface, std::min(version, 1u));
+ }
+
+ if (strcmp(interface, zwp_virtual_keyboard_manager_v1_interface.name) == 0)
+ {
+ display->vk_manager = (zwp_virtual_keyboard_manager_v1*)
+ wl_registry_bind(registry, name,
+ &zwp_virtual_keyboard_manager_v1_interface, 1u);
+ }
+
+ if (strcmp(interface, zwp_input_method_manager_v2_interface.name) == 0)
+ {
+ display->im_manager = (zwp_input_method_manager_v2*)wl_registry_bind(
+ registry, name, &zwp_input_method_manager_v2_interface, 1u);
+ }
+}
+
+static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name)
+{
+ /* no-op */
+}
+
+static struct wl_registry_listener registry_listener =
+{
+ ®istry_add_object,
+ ®istry_remove_object
+};
+
+WaylandDisplay::WaylandDisplay()
+{
+ auto gdk_display = gdk_display_get_default();
+ display = gdk_wayland_display_get_wl_display(gdk_display);
+
+ if (!display)
+ {
+ std::cerr << "Failed to connect to wayland display!" <<
+ " Are you sure you are running a wayland compositor?" << std::endl;
+ std::exit(-1);
+ }
+
+ wl_registry *registry = wl_display_get_registry(display);
+ wl_registry_add_listener(registry, ®istry_listener, this);
+ wl_display_dispatch(display);
+ wl_display_roundtrip(display);
+
+ if (!vk_manager)
+ {
+ std::cerr << "Compositor doesn't support the virtual-keyboard-v1 " <<
+ "protocol, exiting" << std::endl;
+ std::exit(-1);
+ }
+}
+
+WaylandDisplay& WaylandDisplay::get()
+{
+ static WaylandDisplay instance;
+ return instance;
+}
diff --git a/src/osk/display.hpp b/src/osk/display.hpp
new file mode 100644
index 00000000..f003d46d
--- /dev/null
+++ b/src/osk/display.hpp
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "input-method-unstable-v2-client-protocol.h"
+#include "virtual-keyboard-unstable-v1-client-protocol.h"
+#include "wayfire-shell-unstable-v2-client-protocol.h"
+#include
+class WaylandDisplay
+{
+ WaylandDisplay();
+
+ public:
+ static WaylandDisplay& get();
+
+ wl_display *display = nullptr;
+
+ zwf_shell_manager_v2 *zwf_manager = nullptr;
+ zwp_virtual_keyboard_manager_v1 *vk_manager = nullptr;
+ zwp_input_method_manager_v2 *im_manager = nullptr;
+};
diff --git a/src/osk/layout.cpp b/src/osk/layout.cpp
new file mode 100644
index 00000000..f70e0b34
--- /dev/null
+++ b/src/osk/layout.cpp
@@ -0,0 +1,341 @@
+#include "layout.hpp"
+#include "glibmm/ustring.h"
+#include "gtkmm/togglebutton.h"
+#include "osk.hpp"
+#include
+
+WayfireOskLayout::WayfireOskLayout(std::string xml_filepath)
+{
+ builder = Gtk::Builder::create_from_file(xml_filepath);
+ auto layout = builder->get_widget("keyboard_main");
+ if (!layout)
+ {
+ std::cerr << "Layout box null" << std::endl;
+ std::exit(-1);
+ }
+
+ append(*layout);
+ hook_up();
+}
+
+void WayfireOskLayout::hook_up()
+{
+ /* Iterate all objects */
+ GSList *all_objects = gtk_builder_get_objects(builder->gobj());
+
+ for (GSList *l = all_objects; l != nullptr; l = l->next)
+ {
+ GObject *gobj = G_OBJECT(l->data);
+
+ auto gobject = Glib::wrap(gobj, true);
+ if (!gobject)
+ {
+ continue;
+ }
+
+ auto toggle_obj = dynamic_cast(gobject.get());
+ if (toggle_obj)
+ {
+ /* Toggles can ONLY be scancode types */
+ std::string id = toggle_obj->get_name();
+ bind_toggle(toggle_obj, id);
+ } else
+ {
+ /* Buttons might be scancode based or string literal */
+ auto button_obj = dynamic_cast(gobject.get());
+ if (button_obj)
+ {
+ std::string id = button_obj->get_name();
+ if (id.rfind("gtkmm", 0) == 0)
+ {
+ /* Button without a scan-code name. Sends its label as content */
+ auto click_gesture = Gtk::GestureClick::create();
+
+ signals.push_back(click_gesture->signal_pressed().connect(
+ [=] (int button, double x, double y)
+ {
+ click_gesture->set_state(Gtk::EventSequenceState::CLAIMED);
+ }));
+ signals.push_back(click_gesture->signal_released().connect(
+ [=] (int button, double x, double y)
+ {
+ auto& keyboard = WayfireOsk::get();
+ keyboard.get_device().send_string(button_obj->get_label());
+ }));
+ button_obj->add_controller(click_gesture);
+ } else
+ {
+ /* Scancode mode*/
+ bind(button_obj, id);
+ }
+ }
+
+ auto label_obj = dynamic_cast(gobject.get());
+ if (label_obj)
+ {
+ std::string id = label_obj->get_name();
+ if (id.rfind("gtkmm", 0) == 0)
+ {
+ auto click_gesture = Gtk::GestureClick::create();
+
+ signals.push_back(click_gesture->signal_pressed().connect(
+ [=] (int button, double x, double y)
+ {
+ click_gesture->set_state(Gtk::EventSequenceState::CLAIMED);
+ }));
+ signals.push_back(click_gesture->signal_released().connect(
+ [=] (int button, double x, double y)
+ {
+ auto& keyboard = WayfireOsk::get();
+ keyboard.get_device().send_string(label_obj->get_label());
+ }));
+ label_obj->add_controller(click_gesture);
+ } else
+ {
+ /* For sanity sake we ARE NOT allowing labels as scancodes */
+ std::cerr << "error label with possible scancode '" << id << "'." << std::endl;
+ }
+ }
+ }
+ }
+
+ g_slist_free(all_objects);
+}
+
+WayfireOskLayout::~WayfireOskLayout()
+{
+ for (auto signal : signals)
+ {
+ signal.disconnect();
+ }
+}
+
+void WayfireOskLayout::bind(Gtk::Button *button, std::string id)
+{
+ auto keymap = WayfireOsk::get().get_device().get_keymap();
+ xkb_keycode_t kc = xkb_keymap_key_by_name(keymap, id.c_str());
+
+ auto update_label = [=] ()
+ {
+ auto keymap = WayfireOsk::get().get_device().get_keymap();
+ if ((kc == XKB_KEYCODE_INVALID) || (kc < 8))
+ {
+ return;
+ }
+
+ auto layout_idx = WayfireOsk::get().get_device().get_current_layout();
+
+ auto symbol = get_keycap_symbol(id);
+
+ if (id.compare("SPCE") == 0)
+ {
+ /* TODO Option*/
+ const char *layout_name = xkb_keymap_layout_get_name(
+ keymap, layout_idx);
+
+ button->set_label(layout_name);
+ return;
+ }
+
+ if (!symbol.empty())
+ {
+ button->set_label(symbol);
+ return;
+ }
+
+ struct xkb_state *temp_state = xkb_state_new(keymap);
+ if (!temp_state)
+ {
+ std::cout << "error, no temp_state" << std::endl;
+ return;
+ }
+
+ auto depressed = WayfireOsk::get().get_device().get_depressed_modifiers();
+
+ xkb_state_update_mask(
+ temp_state,
+ depressed,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0);
+ xkb_layout_index_t num_layouts = xkb_keymap_num_layouts(keymap);
+ if (layout_idx >= num_layouts)
+ {
+ std::cerr << "Invalid layout idx " << layout_idx << " : Max " << num_layouts << std::endl;
+ return;
+ }
+
+ xkb_level_index_t level = xkb_state_key_get_level(temp_state, kc, layout_idx);
+ if (level == XKB_LEVEL_INVALID)
+ {
+ return;
+ }
+
+ const xkb_keysym_t *syms;
+ int num_syms = xkb_keymap_key_get_syms_by_level(keymap, kc,
+ layout_idx, level, &syms);
+ if (num_syms > 0)
+ {
+ char buffer[7];
+
+ int bytes_written = xkb_keysym_to_utf8(*syms, buffer, sizeof(buffer));
+
+ if (bytes_written > 0)
+ {
+ std::string label = std::string(buffer, bytes_written);
+ if (!label.empty())
+ {
+ button->set_label(label);
+ }
+ }
+ }
+ };
+ /* Set a label and update every state change */
+ update_label();
+ signals.push_back(WayfireOsk::get().get_device().signal_modifiers_changed().connect([=] ()
+ {
+ update_label();
+ }));
+ signals.push_back(WayfireOsk::get().get_device().signal_layer_changed().connect([=] ()
+ {
+ update_label();
+ }));
+
+ /* A non-modifier key */
+ auto click_gesture = Gtk::GestureClick::create();
+
+ signals.push_back(click_gesture->signal_pressed().connect(
+ [=] (int button, double x, double y)
+ {
+ auto& keyboard = WayfireOsk::get();
+ auto code = kc - 8;
+
+ click_gesture->set_state(Gtk::EventSequenceState::CLAIMED);
+
+ keyboard.get_device().send_key(code,
+ WL_KEYBOARD_KEY_STATE_PRESSED);
+ }));
+ signals.push_back(click_gesture->signal_released().connect(
+ [=] (int button, double x, double y)
+ {
+ auto& keyboard = WayfireOsk::get();
+ auto code = kc - 8;
+
+ keyboard.get_device().send_key(code,
+ WL_KEYBOARD_KEY_STATE_RELEASED);
+ }));
+
+ button->add_controller(click_gesture);
+}
+
+void WayfireOskLayout::bind_toggle(Gtk::ToggleButton *button, std::string name)
+{
+ /* Code path for modifier keys. For now we latch them Only. Awaiting bug reports from people who
+ * expect to send a CTRl alone etc */
+ std::string modifier_name = "";
+ if ((name.compare("RCTL") == 0) || (name.compare("LCTL") == 0))
+ {
+ modifier_name = XKB_MOD_NAME_CTRL;
+ } else if ((name.compare("LFSH") == 0) || (name.compare("RFSH") == 0))
+ {
+ modifier_name = XKB_MOD_NAME_SHIFT;
+ } else if ((name.compare("LALT") == 0) || (name.compare("RALT") == 0))
+ {
+ modifier_name = XKB_MOD_NAME_ALT;
+ } else if ((name.compare("LWIN") == 0) || (name.compare("RWIN") == 0))
+ {
+ modifier_name = XKB_MOD_NAME_LOGO;
+ } else if (name.compare("CAPS") == 0)
+ {
+ modifier_name = XKB_MOD_NAME_CAPS;
+ } else if ((name.compare("NMLK") == 0) || ((name.compare("KPNM")) == 0))
+ {
+ modifier_name = XKB_MOD_NAME_NUM;
+ }
+
+ if (modifier_name.empty())
+ {
+ std::cout << "Not Modifier " << modifier_name << " " << name << std::endl;
+ return;
+ }
+
+ auto update_label = [=] ()
+ {
+ auto symbol = get_keycap_symbol(name);
+ button->set_label(symbol);
+ };
+ /* Set a label and update every state change */
+ update_label();
+ size_t toggle_index = signals.size();
+ auto toggle_sig = button->signal_toggled().connect([=] ()
+ {
+ WayfireOsk::get().get_device().toggle_modifier(modifier_name);
+ });
+
+ signals.push_back(toggle_sig);
+
+ signals.push_back(WayfireOsk::get().get_device().signal_modifiers_changed().connect([=] ()
+ {
+ /* This realistically is disconnected directly after the above, so this hack is fine */
+ signals[toggle_index].block();
+ button->set_active(WayfireOsk::get().get_device().is_modifier_pressed(modifier_name));
+ signals[toggle_index].unblock();
+ }));
+}
+
+std::string WayfireOskLayout::get_keycap_symbol(std::string id)
+{
+ if (id.compare("LEFT") == 0)
+ {
+ return "←";
+ } else if (id.compare("RGHT") == 0)
+ {
+ return "→";
+ } else if (id.compare("UP") == 0)
+ {
+ return "↑";
+ } else if (id.compare("DOWN") == 0)
+ {
+ return "↓";
+ } else if (id.compare("BKSP") == 0)
+ {
+ return "⌫";
+ } else if (id.compare("DEL") == 0)
+ {
+ return "⌦";
+ } else if ((id.compare("RTRN") == 0) || (id.compare("KPEN") == 0))
+ {
+ return "↵";
+ } else if (id.compare("TAB") == 0)
+ {
+ return "⇥";
+ } else if ((id.compare("LFSH") == 0) || (id.compare("RTSH") == 0))
+ {
+ return "⇧";
+ } else if ((id.compare("LCTL") == 0) || (id.compare("RCTL") == 0))
+ {
+ return "⌃";
+ } else if ((id.compare("LALT") == 0) || (id.compare("RALT") == 0))
+ {
+ return "⌥";
+ } else if (id.compare("CAPS") == 0)
+ {
+ return "⇪";
+ } else if ((id.compare("LWIN") == 0) || (id.compare("RWIN") == 0))
+ {
+ return "⌘";
+ } else if (id.compare("ESC") == 0)
+ {
+ return "ESC";
+ } else if (id.compare("SPCE") == 0)
+ {
+ return "␣";
+ } else if (id.compare("KPNM") == 0)
+ {
+ return "NUM";
+ }
+
+ return "";
+}
diff --git a/src/osk/layout.hpp b/src/osk/layout.hpp
new file mode 100644
index 00000000..26ec58fb
--- /dev/null
+++ b/src/osk/layout.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "gtkmm/box.h"
+#include "gtkmm/builder.h"
+#include "gtkmm/button.h"
+#include "gtkmm/togglebutton.h"
+#include
+#include
+class WayfireOskLayout : public Gtk::Box
+{
+ private:
+ Glib::RefPtr builder;
+ void hook_up();
+ std::vector signals;
+
+ void bind(Gtk::Button *button, std::string name);
+ void bind_toggle(Gtk::ToggleButton *button, std::string name);
+ static std::string get_keycap_symbol(std::string id);
+
+ public:
+ WayfireOskLayout(std::string file_name);
+ ~WayfireOskLayout();
+};
diff --git a/src/osk/meson.build b/src/osk/meson.build
new file mode 100644
index 00000000..831aad5b
--- /dev/null
+++ b/src/osk/meson.build
@@ -0,0 +1,33 @@
+sources = [
+ 'osk.cpp',
+ 'wayland-window.cpp',
+ 'virtual-keyboard.cpp',
+ 'layout.cpp',
+ 'display.cpp',
+]
+
+if enchant.found()
+ sources += 'complete/complete-enchant.cpp'
+endif
+
+if llama.found()
+ sources += 'complete/complete-tiny.cpp'
+endif
+
+executable(
+ 'wf-osk',
+ sources,
+ dependencies: [
+ gtkmm,
+ wf_protos,
+ gtklayershell,
+ xkb,
+ xkbregistry,
+ json,
+ wayfire,
+ libutil,
+ enchant,
+ llama,
+ ],
+ install: true,
+)
\ No newline at end of file
diff --git a/src/osk/osk.cpp b/src/osk/osk.cpp
new file mode 100644
index 00000000..15f5e42f
--- /dev/null
+++ b/src/osk/osk.cpp
@@ -0,0 +1,246 @@
+#ifdef HAVE_ENCHANT
+ #include "complete/complete-enchant.hpp"
+#endif
+#ifdef HAVE_LLAMA
+ #include "complete/complete-tiny.hpp"
+#endif
+#include "complete/complete.hpp"
+#include "css-config.hpp"
+#include "gtk/gtk.h"
+#include "gtkmm.h"
+#include "layout.hpp"
+#include "wayland-window.hpp"
+#include "wf-option-wrap.hpp"
+#include "wf-shell-app.hpp"
+#include "osk.hpp"
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+
+int spacing = OSK_SPACING;
+int default_width = 800;
+int default_height = 400;
+int headerbar_size = 60;
+
+std::string anchor = "bottom";
+
+void WayfireOsk::remove_layout()
+{
+ if (layout)
+ {
+ box->remove(*layout);
+ layout = nullptr;
+ for (auto signal : signals)
+ {
+ signal.disconnect();
+ }
+
+ signals.clear();
+ }
+}
+
+void WayfireOsk::init_layouts()
+{
+ std::cout << "init Layout" << std::endl;
+ std::string xml_filepath = std::string(LAYOUT_DIR) + "/" + get_current_layout() + ".xml";
+ remove_layout();
+
+ if ((vk == nullptr) || !vk->valid())
+ {
+ std::cout << "Invalid virtual keyboard state. No layout" << std::endl;
+ return;
+ }
+
+ try {
+ std::cout << "Loading layout '" << xml_filepath << "'" << std::endl;
+
+ layout = std::make_unique(xml_filepath);
+ box->append(*layout);
+ active_layout_path = xml_filepath;
+ } catch (const Glib::Error& ex)
+ {
+ std::cerr << "XML Parse Exception Encountered: " << ex.what() << std::endl;
+ }
+
+ window->set_widget(*box);
+}
+
+WayfireOsk::WayfireOsk()
+{}
+
+WayfireOsk::~WayfireOsk()
+{}
+
+void WayfireOsk::create(int argc, char **argv)
+{
+ if (instance)
+ {
+ throw std::logic_error("Creating keyboard twice!");
+ }
+
+ instance = std::unique_ptr(new WayfireOsk{});
+ instance->init_app();
+ instance->run(argc, argv);
+}
+
+WayfireOsk& WayfireOsk::get()
+{
+ if (!instance)
+ {
+ throw std::logic_error("Getting keyboard before creating it!");
+ }
+
+ return dynamic_cast(*instance.get());
+}
+
+VirtualKeyboardDevice& WayfireOsk::get_device()
+{
+ return *vk;
+}
+
+WaylandWindow& WayfireOsk::get_window()
+{
+ return *window;
+}
+
+WayfireOskComplete& WayfireOsk::get_complete()
+{
+ return *complete;
+}
+
+void WayfireOsk::activate()
+{
+ if (activate_show)
+ {
+ window->show();
+ }
+}
+
+void WayfireOsk::deactivate()
+{
+ if (deactivate_hide)
+ {
+ window->hide();
+ }
+}
+
+std::string WayfireOsk::get_application_name()
+{
+ return "org.wayfire.osk";
+}
+
+void WayfireOsk::set_completor()
+{
+ std::string complete_type = WfOption{"osk/suggest_engine"};
+
+ if (complete_type.compare("enchant") == 0)
+ {
+#ifdef HAVE_ENCHANT
+ complete = std::make_unique();
+ return;
+#else
+
+ std::cerr << "Enchant chosen but compiled out..." << std::endl;
+#endif
+ } else if (complete_type.compare("llama") == 0)
+ {
+#ifdef HAVE_LLAMA
+ complete = std::make_unique();
+ return;
+#else
+ std::cerr << "Llama chosen but compiled out..." << std::endl;
+#endif
+ }
+
+ std::cout << "No Auto complete" << std::endl;
+ complete = std::make_unique();
+}
+
+void WayfireOsk::on_activate()
+{
+ WayfireShellApp::on_activate();
+ box = new Gtk::Box();
+ window = std::make_unique(default_width, default_height, anchor, headerbar_size);
+ vk = std::make_unique();
+
+ set_completor();
+ signals.push_back(vk->signal_ready_changed().connect([=] (bool ready)
+ {
+ if (ready)
+ {
+ init_layouts();
+ } else
+ {
+ remove_layout();
+ }
+ }));
+ signals.push_back(vk->signal_keymap_changed().connect([=] ()
+ {
+ init_layouts();
+ }));
+ app->add_window(WayfireOsk::get().get_window());
+ app->hold();
+ auto css_provider = Gtk::CssProvider::create();
+ css_provider->load_from_data(
+ "button.depressed, button.depressed:hover { \
+ background-color: alpha(currentColor, 0.12); \
+ box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.25); \
+ background-image: none; \
+ }");
+ Gtk::StyleContext::add_provider_for_display(WayfireOsk::get().get_window().get_display(),
+ css_provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ new CssFromConfigInt("osk/emoji_size", "label.emoji{font-size:", "px;}");
+
+ if (start_hidden)
+ {
+ WayfireOsk::get().get_window().hide();
+ }
+}
+
+void WayfireOsk::on_config_reload()
+{
+ std::string new_layout_name = WfOption("osk/shape");
+ if (new_layout_name.compare(layout_name) != 0)
+ {
+ layout_name = new_layout_name;
+ init_layouts();
+ }
+
+ activate_show = WfOption("osk/activate_show");
+ deactivate_hide = WfOption("osk/deactivate_hide");
+ set_completor();
+}
+
+std::string WayfireOsk::get_current_layout()
+{
+ if (!user_chosen_layout.empty())
+ {
+ return user_chosen_layout;
+ }
+
+ if (vk && vk->is_numeric())
+ {
+ return "numpad";
+ }
+
+ return WfOption("osk/shape");
+}
+
+void WayfireOsk::user_selected_layout(std::string layout)
+{
+ user_chosen_layout = layout;
+ init_layouts();
+}
+
+int main(int argc, char **argv)
+{
+ WayfireOsk::create(argc, argv);
+ return 0;
+}
diff --git a/src/osk/osk.hpp b/src/osk/osk.hpp
new file mode 100644
index 00000000..4c260c35
--- /dev/null
+++ b/src/osk/osk.hpp
@@ -0,0 +1,73 @@
+#pragma once
+
+#include
+#include
+
+#include
+#include "complete/complete.hpp"
+#include "layout.hpp"
+#include "sigc++/connection.h"
+#include "virtual-keyboard.hpp"
+#include "wayland-window.hpp"
+#include "wf-shell-app.hpp"
+#include
+
+extern int spacing;
+
+class WayfireOsk : public WayfireShellApp
+{
+ void init_layouts();
+ void remove_layout();
+
+ std::vector signals;
+
+ std::unique_ptr window;
+ std::unique_ptr vk = nullptr;
+ std::unique_ptr complete;
+ WayfireOsk();
+
+ Gtk::Box *box = nullptr;
+ std::unique_ptr layout = nullptr;
+ std::string active_layout_path;
+ void refresh_labels_from_xkb();
+
+ std::string layout_name = "iso";
+
+
+ bool start_hidden = false;
+ bool activate_show = false, deactivate_hide = false;
+
+ void set_completor();
+
+ std::string user_chosen_layout = "";
+
+ public:
+ static WayfireOsk& get();
+ static void create(int argc, char **argv);
+
+ VirtualKeyboardDevice& get_device();
+ WaylandWindow& get_window();
+ WayfireOskComplete& get_complete();
+ ~WayfireOsk();
+
+ void activate();
+ void deactivate();
+ void on_config_reload() override;
+
+ Gio::Application::Flags get_extra_application_flags() override
+ {
+ return Gio::Application::Flags::NON_UNIQUE;
+ }
+
+ std::string get_application_name() override;
+
+ void user_selected_layout(std::string);
+ std::string get_current_layout();
+
+ protected:
+ void add_output(GMonitor monitor) override
+ {}
+ void rem_output(GMonitor monitor) override
+ {}
+ void on_activate() override;
+};
diff --git a/src/osk/virtual-keyboard.cpp b/src/osk/virtual-keyboard.cpp
new file mode 100644
index 00000000..f72f66fd
--- /dev/null
+++ b/src/osk/virtual-keyboard.cpp
@@ -0,0 +1,865 @@
+#include "virtual-keyboard.hpp"
+#include "gdk/gdk.h"
+#include "gdk/wayland/gdkwayland.h"
+#include "input-method-unstable-v2-client-protocol.h"
+#include "osk.hpp"
+#include "wf-ipc.hpp"
+#include "wayland-window.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "display.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+uint32_t get_current_time()
+{
+ timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ return ts.tv_sec * 1000ll + ts.tv_nsec / 1000000ll;
+}
+
+/* Keyboard callbacks */
+static void kbd_keymap(void *data,
+ struct wl_keyboard *wl_keyboard,
+ uint32_t format,
+ int32_t fd,
+ uint32_t size)
+{
+ auto instance = static_cast(data);
+ instance->handle_keymap(data, format, fd, size);
+}
+
+static void kbd_enter(void *data,
+ struct wl_keyboard *wl_keyboard,
+ uint32_t serial,
+ struct wl_surface *surface,
+ struct wl_array *keys)
+{
+ /* never called */
+}
+
+static void kbd_leave(void *data,
+ struct wl_keyboard *wl_keyboard,
+ uint32_t serial,
+ struct wl_surface *surface)
+{
+ /* never called */
+}
+
+static void kbd_key(void *data,
+ struct wl_keyboard *wl_keyboard,
+ uint32_t serial,
+ uint32_t time,
+ uint32_t key,
+ uint32_t state)
+{
+ /* never called */
+}
+
+static void kbd_modifiers(void *data,
+ struct wl_keyboard *wl_keyboard,
+ uint32_t serial,
+ uint32_t mods_depressed,
+ uint32_t mods_latched,
+ uint32_t mods_locked,
+ uint32_t group)
+{
+ /* never called */
+}
+
+static void kbd_repeat_info(void *data,
+ struct wl_keyboard *wl_keyboard,
+ int32_t rate,
+ int32_t delay)
+{
+ /* Don't care */
+}
+
+static const wl_keyboard_listener kbd_listener = {
+ kbd_keymap,
+ kbd_enter,
+ kbd_leave,
+ kbd_key,
+ kbd_modifiers,
+ kbd_repeat_info,
+};
+
+/* Input method callbacks */
+
+struct InputMethodState
+{
+ std::string surrounding_text = "";
+ uint32_t cursor_index = 0;
+ uint32_t anchor_index = 0;
+ uint32_t hint = 0;
+ uint32_t purpose = 0;
+ std::string active_word_fragment = "";
+ uint32_t serial = 0;
+ bool is_active;
+};
+
+static InputMethodState im_state;
+
+static std::pair extract_partial_word(const std::string& text, uint32_t cursor)
+{
+ if ((cursor == 0) || text.empty() || (cursor > text.length()))
+ {
+ return {"", ""};
+ }
+
+ size_t start_pos = text.find_last_of(" \t\n\r", cursor - 1);
+
+ if (start_pos == std::string::npos)
+ {
+ return {"", text.substr(0, cursor)}; /* Word starts at the very beginning of the buffer */
+ }
+
+ return {text.substr(0, start_pos), text.substr(start_pos + 1, cursor - (start_pos + 1))};
+}
+
+static void im_activate(void *data,
+ struct zwp_input_method_v2 *zwp_input_method_v2)
+{
+ im_state.is_active = true;
+ std::cout << "Activate " << std::endl;
+ auto instance = static_cast(data);
+ instance->handle_activate();
+}
+
+static void im_deactivate(void *data,
+ struct zwp_input_method_v2 *zwp_input_method_v2)
+{
+ im_state.is_active = false;
+ std::cout << "Deactivate " << std::endl;
+ auto instance = static_cast(data);
+ instance->handle_deactivate();
+}
+
+static void im_surrounding_text(void *data,
+ struct zwp_input_method_v2 *zwp_input_method_v2,
+ const char *text,
+ uint32_t cursor,
+ uint32_t anchor)
+{
+ if (text)
+ {
+ im_state.surrounding_text = text;
+ im_state.cursor_index = cursor;
+ im_state.anchor_index = anchor;
+ }
+}
+
+static void im_text_change_cause(void *data,
+ struct zwp_input_method_v2 *zwp_input_method_v2,
+ uint32_t cause)
+{}
+
+static void im_content_type(void *data,
+ struct zwp_input_method_v2 *zwp_input_method_v2,
+ uint32_t hint,
+ uint32_t purpose)
+{
+ im_state.hint = hint;
+ im_state.purpose = purpose;
+}
+
+static void im_done(void *data,
+ struct zwp_input_method_v2 *zwp_input_method_v2)
+{
+ auto instance = static_cast(data);
+
+ im_state.serial++;
+
+ instance->done();
+ std::cout << "done - serial: " << im_state.serial << " surrounding : '" << im_state.surrounding_text <<
+ "'" << std::endl;
+}
+
+static void im_unavailable(void *data,
+ struct zwp_input_method_v2 *zwp_input_method_v2)
+{
+ std::exit(-1);
+}
+
+static const zwp_input_method_v2_listener im_listener = {
+ im_activate,
+ im_deactivate,
+ im_surrounding_text,
+ im_text_change_cause,
+ im_content_type,
+ im_done,
+ im_unavailable,
+};
+
+VirtualKeyboardDevice::VirtualKeyboardDevice()
+{
+ auto& display = WaylandDisplay::get();
+ auto seat = Gdk::Display::get_default()->get_default_seat();
+ auto wl_seat = gdk_wayland_seat_get_wl_seat(seat->gobj());
+
+ auto wl_keyboard = wl_seat_get_keyboard(wl_seat);
+ wl_keyboard_add_listener(wl_keyboard, &kbd_listener, this);
+ vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(
+ display.vk_manager, wl_seat);
+ xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
+
+ if (display.im_manager)
+ {
+ im = zwp_input_method_manager_v2_get_input_method(display.im_manager, wl_seat);
+ zwp_input_method_v2_add_listener(im, &im_listener, this);
+ }
+
+ ipc_server = WayfireIPC::get_instance();
+ if (!ipc_server)
+ {
+ std::cerr << "Failed to connect to Wayfire IPC. exiting" << std::endl;
+ std::exit(-1);
+ }
+
+ ipc_client = ipc_server->create_client();
+
+ if (!ipc_client)
+ {
+ std::cerr << "Failed to connect to Wayfire IPC client. exiting" << std::endl;
+ std::exit(-1);
+ }
+
+ ipc_client->subscribe(this, {"keyboard-modifier-state-changed"});
+}
+
+VirtualKeyboardDevice::~VirtualKeyboardDevice()
+{
+ if (xkb_keymap)
+ {
+ xkb_keymap_unref(xkb_keymap);
+ }
+
+ if (xkb_context)
+ {
+ xkb_context_unref(xkb_context);
+ }
+}
+
+void VirtualKeyboardDevice::done()
+{
+ is_active = im_state.is_active;
+ if ((current_hint != im_state.hint) || (current_purpose != im_state.purpose))
+ {
+ current_hint = im_state.hint;
+ current_purpose = im_state.purpose;
+ hints_changed.emit();
+ }
+
+ auto [before, partial] = extract_partial_word(im_state.surrounding_text,
+ im_state.cursor_index);
+ im_state.active_word_fragment = partial;
+ if (!is_sensitive())
+ {
+ WayfireOsk::get().get_complete().get_suggestions(before, partial);
+ } else
+ {
+ WayfireOsk::get().get_window().clear_suggestions();
+ }
+}
+
+void VirtualKeyboardDevice::on_event(wf::json_t data)
+{
+ std::cout << data.serialize() << std::endl;
+ if (data["event"].as_string() == "keyboard-modifier-state-changed")
+ {
+ if (available_layouts.size() == 0)
+ {
+ set_available(data["state"]["possible-layouts"]);
+ }
+
+ auto state_layout = data["state"]["layout-index"].as_uint();
+ if (state_layout != current_layout)
+ {
+ current_layout = state_layout;
+ set_current(state_layout);
+ }
+ }
+}
+
+void VirtualKeyboardDevice::set_available(wf::json_t layouts)
+{
+ std::vector layouts_available;
+ std::map names;
+
+ for (size_t i = 0; i < layouts.size(); i++)
+ {
+ auto elem = layouts[i];
+ names[elem] = i;
+ layouts_available.push_back(Layout{
+ .Name = (std::string)elem,
+ .ID = "",
+ .Locale = "",
+ });
+ }
+
+ auto context = rxkb_context_new(RXKB_CONTEXT_NO_FLAGS);
+ rxkb_context_parse_default_ruleset(context);
+ auto rlayout = rxkb_layout_first(context);
+ for (; rlayout != NULL; rlayout = rxkb_layout_next(rlayout))
+ {
+ auto descr = rxkb_layout_get_description(rlayout);
+ auto name = names.find(descr);
+ if (name != names.end())
+ {
+ layouts_available[name->second].ID = rxkb_layout_get_brief(rlayout);
+
+ struct rxkb_iso3166_code *iso3166 = rxkb_layout_get_iso3166_first(rlayout);
+
+ const char *country = iso3166 ? rxkb_iso3166_code_get_code(iso3166) : nullptr;
+ if (country)
+ {
+ layouts_available[name->second].Locale = layouts_available[name->second].ID + "_" + country;
+ } else
+ {
+ layouts_available[name->second].Locale = layouts_available[name->second].ID;
+ }
+ }
+ }
+
+ available_layouts = layouts_available;
+}
+
+void VirtualKeyboardDevice::set_current(uint32_t index)
+{
+ current_layout = index;
+
+ layer_changed.emit();
+ WayfireOsk::get().get_complete().switch_language(available_layouts[current_layout].Locale,
+ available_layouts[current_layout].Name);
+}
+
+void VirtualKeyboardDevice::handle_keymap(void *data, uint32_t format,
+ int32_t fd, uint32_t size)
+{
+ std::cout << "HANDLE KEYMAP" << std::endl;
+ if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1)
+ {
+ close(fd);
+ return;
+ }
+
+ bool req_send_keymap = false;
+
+ char *map_str = static_cast(mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0));
+ if (map_str != MAP_FAILED)
+ {
+ auto next_keymap = std::string(map_str, size);
+ if (keymap != next_keymap)
+ {
+ req_send_keymap = true;
+ }
+
+ keymap = next_keymap;
+ if (xkb_keymap)
+ {
+ xkb_keymap_unref(xkb_keymap);
+ }
+
+ xkb_keymap = xkb_keymap_new_from_string(xkb_context, map_str,
+ XKB_KEYMAP_FORMAT_TEXT_V1,
+ XKB_KEYMAP_COMPILE_NO_FLAGS);
+ munmap(map_str, size);
+ ready_changed.emit(valid());
+ }
+
+ close(fd);
+ if (req_send_keymap)
+ {
+ send_keymap();
+ }
+
+ keymap_changed.emit();
+}
+
+int VirtualKeyboardDevice::create_shm_file(size_t size)
+{
+ int fd = memfd_create("vk_keymap", MFD_CLOEXEC);
+ if (fd < 0)
+ {
+ return -1;
+ }
+
+ if (ftruncate(fd, size) < 0)
+ {
+ close(fd);
+ return -1;
+ }
+
+ return fd;
+}
+
+void VirtualKeyboardDevice::send_keymap()
+{
+ if (!vk || keymap.empty())
+ {
+ return;
+ }
+
+ auto chars = keymap.c_str();
+ size_t size = strlen(chars) + 1;
+ int fd = create_shm_file(size);
+ if (fd < 0)
+ {
+ return;
+ }
+
+ void *data = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (data == MAP_FAILED)
+ {
+ close(fd);
+ return;
+ }
+
+ std::memcpy(data, chars, size);
+ munmap(data, size);
+
+ zwp_virtual_keyboard_v1_keymap(vk,
+ WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1,
+ fd, size);
+ close(fd);
+
+ keymap_changed.emit();
+}
+
+void VirtualKeyboardDevice::send_string(std::string message)
+{
+ bool insert_with_input_method = false;
+ if (insert_with_input_method)
+ {
+ /* Theoretically sound. Breaks often. Just like me */
+
+ if (!is_active)
+ {
+ std::cout << "Not active, skipped" << std::endl;
+ return;
+ }
+
+ if (im == nullptr)
+ {
+ std::cerr << "Cannot send arbitrary string without input_method_v2" << std::endl;
+ return;
+ }
+
+ std::cout << "Send '" << message << "' " << im_state.serial << std::endl;
+ zwp_input_method_v2_delete_surrounding_text(im, 0, 0);
+ zwp_input_method_v2_commit_string(im, message.c_str());
+ zwp_input_method_v2_commit(im, im_state.serial);
+ } else
+ {
+ /* I love this idea, it's so stupid.
+ *
+ * why does it work so well?
+ */
+ if (!vk || message.empty())
+ {
+ return;
+ }
+
+ std::vector codepoints = utf8_to_codepoints(message);
+ if (codepoints.empty())
+ {
+ return;
+ }
+
+ for (uint32_t codepoint : codepoints)
+ {
+ std::stringstream ss;
+ ss << std::hex << codepoint;
+ std::string hex_str = ss.str();
+
+ zwp_virtual_keyboard_v1_modifiers(vk, 5, 0, 0, current_layout);
+ send_key_raw(KEY_LEFTCTRL, true);
+ send_key_raw(KEY_LEFTSHIFT, true);
+ send_key_raw(KEY_U, true);
+ send_key_raw(KEY_U, false);
+ send_key_raw(KEY_LEFTCTRL, false);
+ send_key_raw(KEY_LEFTSHIFT, false);
+ zwp_virtual_keyboard_v1_modifiers(vk, 0, 0, 0, current_layout);
+
+ for (char c : hex_str)
+ {
+ uint32_t kc = hex_char_to_keycode(c);
+ if (kc != 0)
+ {
+ send_key_raw(kc, true);
+ send_key_raw(kc, false);
+ }
+ }
+
+ send_key_raw(KEY_ENTER, true);
+ send_key_raw(KEY_ENTER, false);
+ }
+ }
+}
+
+std::vector VirtualKeyboardDevice::utf8_to_codepoints(const std::string& str)
+{
+ /* Magic */
+ std::vector codepoints;
+ codepoints.reserve(str.size());
+
+ size_t i = 0;
+ while (i < str.size())
+ {
+ uint32_t point = 0;
+
+ auto c = static_cast(str[i]);
+
+ if (c < 0x80)
+ {
+ point = c;
+ i += 1;
+ } else if (((c & 0xE0) == 0xC0) && (i + 1 < str.size()))
+ {
+ point = ((c & 0x1F) << 6) |
+ (static_cast(str[i + 1]) & 0x3F);
+ i += 2;
+ } else if (((c & 0xF0) == 0xE0) && (i + 2 < str.size()))
+ {
+ point = ((c & 0x0F) << 12) |
+ ((static_cast(str[i + 1]) & 0x3F) << 6) |
+ (static_cast(str[i + 2]) & 0x3F);
+ i += 3;
+ } else if (((c & 0xF8) == 0xF0) && (i + 3 < str.size()))
+ {
+ point = ((c & 0x07) << 18) |
+ ((static_cast(str[i + 1]) & 0x3F) << 12) |
+ ((static_cast(str[i + 2]) & 0x3F) << 6) |
+ (static_cast(str[i + 3]) & 0x3F);
+ i += 4;
+ } else
+ {
+ /* Skip malformed individual UTF-8 bytes gracefully */
+ i += 1;
+ continue;
+ }
+
+ codepoints.push_back(point);
+ }
+
+ codepoints.shrink_to_fit();
+ return codepoints;
+}
+
+/*
+ * To emulate Hexcode keys, use scancodes
+ * FIXME: Will most likely not function on alternative layouts...
+ */
+uint32_t VirtualKeyboardDevice::hex_char_to_keycode(char c)
+{
+ switch (c)
+ {
+ case '0':
+ return KEY_0;
+
+ case '1':
+ return KEY_1;
+
+ case '2':
+ return KEY_2;
+
+ case '3':
+ return KEY_3;
+
+ case '4':
+ return KEY_4;
+
+ case '5':
+ return KEY_5;
+
+ case '6':
+ return KEY_6;
+
+ case '7':
+ return KEY_7;
+
+ case '8':
+ return KEY_8;
+
+ case '9':
+ return KEY_9;
+
+ case 'a':
+ case 'A':
+ return KEY_A;
+
+ case 'b':
+ case 'B':
+ return KEY_B;
+
+ case 'c':
+ case 'C':
+ return KEY_C;
+
+ case 'd':
+ case 'D':
+ return KEY_D;
+
+ case 'e':
+ case 'E':
+ return KEY_E;
+
+ case 'f':
+ case 'F':
+ return KEY_F;
+
+ default:
+ return 0;
+ }
+}
+
+/* Without changing modifiers send keys */
+void VirtualKeyboardDevice::send_key_raw(uint32_t key, uint32_t state) const
+{
+ zwp_virtual_keyboard_v1_key(vk, get_current_time(), key, state);
+}
+
+void VirtualKeyboardDevice::send_key(uint32_t key, uint32_t state) const
+{
+ /* TODO When we can globally track state, don't modify it here */
+ if ((state == 1) && (mods_depressed != 0))
+ {
+ zwp_virtual_keyboard_v1_modifiers(vk,
+ mods_depressed,
+ 0,
+ mods_locked,
+ current_layout);
+ }
+
+ send_key_raw(key, state);
+ if ((state == 0) && (mods_depressed != 0))
+ {
+ zwp_virtual_keyboard_v1_modifiers(vk, 0, 0, 0, current_layout);
+ }
+}
+
+void VirtualKeyboardDevice::toggle_modifier(std::string mod_name)
+{
+ if ((mod_name == "Lock") || (mod_name == "Caps") || (mod_name == "CapsLock") ||
+ (mod_name == "Mod2") || (mod_name == "NumLock") || (mod_name == "ScrollLock"))
+ {
+ uint32_t mask_bit = 0;
+ if ((mod_name == "Lock") || (mod_name == "Caps") || (mod_name == "CapsLock"))
+ {
+ mask_bit = (1U << 1);
+ } else if ((mod_name == "Mod2") || (mod_name == "NumLock"))
+ {
+ mask_bit = (1U << 4);
+ } else if (mod_name == "ScrollLock")
+ {
+ mask_bit = (1U << 5);
+ }
+
+ if (mask_bit != 0)
+ {
+ mods_locked ^= mask_bit;
+ modifiers_changed.emit();
+ }
+ } else
+ {
+ uint32_t mask_bit = 0;
+ if (mod_name == "Shift")
+ {
+ mask_bit = (1U << 0);
+ } else if ((mod_name == "Control") || (mod_name == "Ctrl"))
+ {
+ mask_bit = (1U << 2);
+ } else if ((mod_name == "Mod1") || (mod_name == "Alt"))
+ {
+ mask_bit = (1U << 3);
+ } else if ((mod_name == "Mod4") || (mod_name == "Super") || (mod_name == "Logo"))
+ {
+ mask_bit = (1U << 6);
+ }
+
+ if (mask_bit != 0)
+ {
+ mods_depressed ^= mask_bit;
+ modifiers_changed.emit();
+ }
+ }
+}
+
+void VirtualKeyboardDevice::set_modifier(std::string mod_name, bool val)
+{
+ if ((mod_name == "Lock") || (mod_name == "Caps") || (mod_name == "CapsLock") ||
+ (mod_name == "Mod2") || (mod_name == "NumLock") || (mod_name == "ScrollLock"))
+ {
+ uint32_t mask_bit = 0;
+ if ((mod_name == "Lock") || (mod_name == "Caps") || (mod_name == "CapsLock"))
+ {
+ mask_bit = (1U << 1);
+ } else if ((mod_name == "Mod2") || (mod_name == "NumLock"))
+ {
+ mask_bit = (1U << 4);
+ } else if (mod_name == "ScrollLock")
+ {
+ mask_bit = (1U << 5);
+ }
+
+ if (mask_bit != 0)
+ {
+ mods_locked = (mods_locked & ~mask_bit) | (val ? mask_bit : 0);
+ modifiers_changed.emit();
+ }
+ } else
+ {
+ uint32_t mask_bit = 0;
+ if (mod_name == "Shift")
+ {
+ mask_bit = (1U << 0);
+ } else if ((mod_name == "Control") || (mod_name == "Ctrl"))
+ {
+ mask_bit = (1U << 2);
+ } else if ((mod_name == "Mod1") || (mod_name == "Alt"))
+ {
+ mask_bit = (1U << 3);
+ } else if ((mod_name == "Mod4") || (mod_name == "Super") || (mod_name == "Logo"))
+ {
+ mask_bit = (1U << 6);
+ }
+
+ if (mask_bit != 0)
+ {
+ mods_depressed = (mods_depressed & ~mask_bit) | (val ? mask_bit : 0);
+ modifiers_changed.emit();
+ }
+ }
+}
+
+bool VirtualKeyboardDevice::is_modifier_pressed(const std::string mod_name)
+{
+ if ((mod_name == "Lock") || (mod_name == "Caps") || (mod_name == "CapsLock") ||
+ (mod_name == "Mod2") || (mod_name == "NumLock") || (mod_name == "ScrollLock"))
+ {
+ uint32_t mask_bit = 0;
+ if ((mod_name == "Lock") || (mod_name == "Caps") || (mod_name == "CapsLock"))
+ {
+ mask_bit = (1U << 1);
+ } else if ((mod_name == "Mod2") || (mod_name == "NumLock"))
+ {
+ mask_bit = (1U << 4);
+ } else if (mod_name == "ScrollLock")
+ {
+ mask_bit = (1U << 5);
+ }
+
+ return (mask_bit != 0) && ((mods_locked & mask_bit) != 0);
+ } else
+ {
+ uint32_t mask_bit = 0;
+ if (mod_name == "Shift")
+ {
+ mask_bit = (1U << 0);
+ } else if ((mod_name == "Control") || (mod_name == "Ctrl"))
+ {
+ mask_bit = (1U << 2);
+ } else if ((mod_name == "Mod1") || (mod_name == "Alt"))
+ {
+ mask_bit = (1U << 3);
+ } else if ((mod_name == "Mod4") || (mod_name == "Super") || (mod_name == "Logo"))
+ {
+ mask_bit = (1U << 6);
+ }
+
+ return (mask_bit != 0) && ((mods_depressed & mask_bit) != 0);
+ }
+}
+
+xkb_keymap*VirtualKeyboardDevice::get_keymap()
+{
+ return xkb_keymap;
+}
+
+void VirtualKeyboardDevice::handle_activate()
+{
+ WayfireOsk::get().activate();
+}
+
+void VirtualKeyboardDevice::handle_deactivate()
+{
+ WayfireOsk::get().deactivate();
+ WayfireOsk::get().get_window().clear_suggestions();
+}
+
+void VirtualKeyboardDevice::handle_modifiers(uint32_t mods_depressed, uint32_t mods_latched,
+ uint32_t mods_locked, uint32_t group)
+{
+ modifiers_changed.emit();
+}
+
+bool VirtualKeyboardDevice::valid()
+{
+ return xkb_keymap && xkb_context;
+}
+
+uint32_t VirtualKeyboardDevice::get_depressed_modifiers()
+{
+ return mods_depressed;
+}
+
+uint32_t VirtualKeyboardDevice::get_current_layout()
+{
+ return current_layout;
+}
+
+void VirtualKeyboardDevice::accept_suggestion(std::string value)
+{
+ if (!im)
+ {
+ return;
+ }
+
+ uint32_t bytes_to_delete = im_state.active_word_fragment.length();
+
+ if (bytes_to_delete > 0)
+ {
+ zwp_input_method_v2_delete_surrounding_text(im, bytes_to_delete, 0);
+ }
+
+ std::string text_to_insert = value + " ";
+
+ zwp_input_method_v2_commit_string(im, text_to_insert.c_str());
+
+ zwp_input_method_v2_commit(im, im_state.serial);
+
+ im_state.active_word_fragment = "";
+ WayfireOsk::get().get_window().clear_suggestions();
+}
+
+bool VirtualKeyboardDevice::is_sensitive()
+{
+ if (current_hint & HINT_SENSITIVE)
+ {
+ return true;
+ }
+
+ return current_purpose == InputType::PASSWORD || current_purpose == InputType::PIN;
+}
+
+bool VirtualKeyboardDevice::is_numeric()
+{
+ return current_purpose == InputType::DIGITS ||
+ current_purpose == InputType::NAME ||
+ current_purpose == InputType::PHONE ||
+ current_purpose == InputType::PIN ||
+ current_purpose == InputType::TIME ||
+ current_purpose == InputType::DATETIME;
+}
diff --git a/src/osk/virtual-keyboard.hpp b/src/osk/virtual-keyboard.hpp
new file mode 100644
index 00000000..a654c87e
--- /dev/null
+++ b/src/osk/virtual-keyboard.hpp
@@ -0,0 +1,142 @@
+#pragma once
+
+#include "input-method-unstable-v2-client-protocol.h"
+#include "sigc++/signal.h"
+#include "wf-ipc.hpp"
+#include
+#include
+#include
+#include
+#include
+
+#define HINT_COMPLETION 0x1
+#define HINT_SPELL_CHECK 0x2
+#define HINT_AUTO_CAPS 0x4
+#define HINT_LOWERCASE 0x8
+#define HINT_UPPERCASE 0x10
+#define HINT_TITLECASE 0x20
+#define HINT_HIDDEN_TEXT 0x40
+#define HINT_SENSITIVE 0x80
+#define HINT_LATIN 0x100
+#define HINT_MULTILINE 0x200
+
+/* https://wayland.app/protocols/text-input-unstable-v3#zwp_text_input_v3:enum:content_hint */
+enum InputType
+{
+ ANY = 0,
+ ALPHA = 1, /* Only allow alphabetic */
+ DIGITS = 1, /* Only digits */
+ NUMBER = 3, /* Digits, period and minus */
+ PHONE = 4,
+ URL = 5,
+ EMAIL = 6,
+ NAME = 7,
+ PASSWORD = 8,
+ PIN = 9,
+ TIME = 10,
+ DATETIME = 11,
+ TERMINAL = 12,
+};
+
+struct Layout
+{
+ std::string Name;
+ std::string ID;
+ std::string Locale;
+};
+
+class VirtualKeyboardDevice : public IIPCSubscriber
+{
+ sigc::signal keymap_changed, layer_changed, modifiers_changed, hints_changed;
+ sigc::signal ready_changed;
+ int shift_pressed_counter = 0;
+
+ zwp_virtual_keyboard_v1 *vk = nullptr;
+ zwp_input_method_v2 *im = nullptr;
+
+
+ std::shared_ptr ipc_client;
+ std::shared_ptr ipc_server;
+
+ uint32_t mods_depressed = 0, mods_locked = 0;
+ bool is_active = false;
+
+ struct xkb_context *xkb_context = nullptr;
+ struct xkb_keymap *xkb_keymap = nullptr;
+
+ std::string keymap;
+
+ void set_current(uint32_t index);
+ void set_available(wf::json_t layouts);
+ int create_shm_file(size_t size);
+ void send_keymap();
+
+ public:
+ uint32_t current_layout = 0;
+ std::vector available_layouts;
+ uint32_t current_hint = 0;
+ uint32_t current_purpose = 0;
+
+ VirtualKeyboardDevice();
+ ~VirtualKeyboardDevice();
+
+ void on_event(wf::json_t data) override;
+
+ void send_key(uint32_t key, uint32_t state) const;
+ void send_key_raw(uint32_t key, uint32_t state) const;
+
+ void send_string(std::string message);
+ struct xkb_keymap *get_keymap();
+ void set_keymap(std::string map);
+ void toggle_modifier(std::string mod_name);
+ void set_modifier(std::string mod_name, bool val);
+ uint32_t hex_char_to_keycode(char c);
+ std::vector utf8_to_codepoints(const std::string& str);
+
+
+ bool is_modifier_pressed(std::string mod_name);
+ bool valid();
+
+ void handle_keymap(void *data, uint32_t format,
+ int32_t fd, uint32_t size);
+
+ void handle_modifiers(uint32_t mods_depressed, uint32_t mods_latched,
+ uint32_t mods_locked, uint32_t group);
+
+ void handle_activate();
+ void handle_deactivate();
+
+ void accept_suggestion(std::string sugg);
+
+ uint32_t get_depressed_modifiers();
+ uint32_t get_current_layout();
+
+ sigc::signal signal_keymap_changed()
+ {
+ return keymap_changed;
+ }
+
+ sigc::signal signal_modifiers_changed()
+ {
+ return modifiers_changed;
+ }
+
+ sigc::signal signal_layer_changed()
+ {
+ return layer_changed;
+ }
+
+ sigc::signal signal_ready_changed()
+ {
+ return ready_changed;
+ }
+
+ sigc::signal signal_hints_changed()
+ {
+ return hints_changed;
+ }
+
+ bool is_sensitive();
+ bool is_numeric();
+ void done();
+};
diff --git a/src/osk/wayland-window.cpp b/src/osk/wayland-window.cpp
new file mode 100644
index 00000000..6544bae7
--- /dev/null
+++ b/src/osk/wayland-window.cpp
@@ -0,0 +1,241 @@
+#include "wayland-window.hpp"
+#include "gtkmm/enums.h"
+#include "osk.hpp"
+#include "virtual-keyboard-unstable-v1-client-protocol.h"
+#include "input-method-unstable-v2-client-protocol.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include "display.hpp"
+
+
+int32_t WaylandWindow::check_anchor(std::string anchor)
+{
+ std::transform(anchor.begin(), anchor.end(), anchor.begin(), ::tolower);
+
+ int32_t parsed_anchor = -1;
+ if (anchor.compare("top") == 0)
+ {
+ parsed_anchor = GTK_LAYER_SHELL_EDGE_TOP;
+ } else if (anchor.compare("bottom") == 0)
+ {
+ parsed_anchor = GTK_LAYER_SHELL_EDGE_BOTTOM;
+ } else if (anchor.compare("left") == 0)
+ {
+ parsed_anchor = GTK_LAYER_SHELL_EDGE_LEFT;
+ } else if (anchor.compare("right") == 0)
+ {
+ parsed_anchor = GTK_LAYER_SHELL_EDGE_RIGHT;
+ }
+
+ return parsed_anchor;
+}
+
+void WaylandWindow::init(int width, int height, std::string anchor)
+{
+ gtk_layer_init_for_window(this->gobj());
+ gtk_layer_set_layer(this->gobj(), GTK_LAYER_SHELL_LAYER_OVERLAY);
+ gtk_layer_set_namespace(this->gobj(), "keyboard");
+ gtk_layer_set_anchor(this->gobj(),
+ GTK_LAYER_SHELL_EDGE_BOTTOM, false);
+ gtk_layer_set_anchor(this->gobj(),
+ GTK_LAYER_SHELL_EDGE_TOP, false);
+ gtk_layer_set_anchor(this->gobj(),
+ GTK_LAYER_SHELL_EDGE_RIGHT, false);
+ gtk_layer_set_anchor(this->gobj(),
+ GTK_LAYER_SHELL_EDGE_LEFT, false);
+ auto layer_anchor = check_anchor(anchor);
+ if (layer_anchor > -1)
+ {
+ gtk_layer_set_anchor(this->gobj(),
+ (GtkLayerShellEdge)layer_anchor, true);
+ }
+
+ if (exclusion)
+ {
+ gtk_layer_auto_exclusive_zone_enable(this->gobj());
+ } else
+ {
+ gtk_layer_set_exclusive_zone(this->gobj(), -1);
+ }
+
+ this->set_size_request(width, height);
+ this->show();
+ auto gdk_window = this->get_surface()->gobj();
+ auto surface = gdk_wayland_surface_get_wl_surface(gdk_window);
+
+ if (surface && WaylandDisplay::get().zwf_manager)
+ {
+ this->wf_surface = zwf_shell_manager_v2_get_wf_surface(
+ WaylandDisplay::get().zwf_manager, surface);
+ }
+}
+
+void WaylandWindow::init_headerbar(int headerbar_size)
+{
+ std::vector buttons = {
+ &top_button, &bottom_button, &close_button
+ };
+
+ const int button_size = 0.8 * headerbar_size;
+ for (auto& button : buttons)
+ {
+ button->get_style_context()->add_class("image-button");
+ button->set_size_request(button_size, button_size);
+ button->set_margin_bottom(OSK_SPACING);
+ button->set_margin_top(OSK_SPACING);
+ button->set_margin_start(OSK_SPACING);
+ button->set_margin_end(OSK_SPACING);
+ }
+
+ close_button.set_image_from_icon_name("window-close-symbolic");
+ signals.push_back(close_button.signal_clicked().connect([=] ()
+ {
+ std::exit(0);
+ }, true));
+
+ top_button.set_image_from_icon_name("pan-up-symbolic");
+ signals.push_back(top_button.signal_clicked().connect([=] ()
+ {
+ gtk_layer_set_anchor(this->gobj(), GTK_LAYER_SHELL_EDGE_TOP, true);
+ gtk_layer_set_anchor(this->gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, false);
+ }, true));
+
+ bottom_button.set_image_from_icon_name("pan-down-symbolic");
+ signals.push_back(bottom_button.signal_clicked().connect([=] ()
+ {
+ gtk_layer_set_anchor(this->gobj(), GTK_LAYER_SHELL_EDGE_TOP, false);
+ gtk_layer_set_anchor(this->gobj(), GTK_LAYER_SHELL_EDGE_BOTTOM, true);
+ }, true));
+
+ layout_select.set_icon_name("input-keyboard-symbolic");
+
+ /* setup headerbar layout */
+ headerbar_box.set_size_request(-1, headerbar_size);
+ headerbar_box.append(top_button);
+ headerbar_box.append(bottom_button);
+ headerbar_box.append(suggestions);
+ headerbar_box.append(layout_select);
+ headerbar_box.append(close_button);
+
+ /* Init layout options */
+ std::filesystem::path build_dir = LAYOUT_DIR;
+ auto action_group = Gio::SimpleActionGroup::create();
+ auto select_action = Gio::SimpleAction::create("select_file", Glib::VARIANT_TYPE_STRING);
+ select_action->signal_activate().connect([=] (const Glib::VariantBase& parameter)
+ {
+ if (parameter && parameter.is_of_type(Glib::VARIANT_TYPE_STRING))
+ {
+ auto path_variant = Glib::VariantBase::cast_dynamic>(parameter);
+ Glib::ustring base_name = path_variant.get();
+ WayfireOsk::get().user_selected_layout(base_name);
+ }
+ });
+ action_group->add_action(select_action);
+ insert_action_group("ui_menu", action_group);
+
+ auto menu_model = Gio::Menu::create();
+
+ auto menu_item = Gio::MenuItem::create("Automatic", "ui_menu.select_file");
+
+ menu_item->set_action_and_target("ui_menu.select_file", Glib::Variant::create(
+ ""));
+
+ menu_model->append_item(menu_item);
+
+ if (std::filesystem::exists(build_dir) && std::filesystem::is_directory(build_dir))
+ {
+ for (const auto& entry : std::filesystem::directory_iterator(build_dir))
+ {
+ if (entry.is_regular_file() && (entry.path().extension() == ".xml"))
+ {
+ Glib::ustring base_name = entry.path().stem().string();
+
+ auto menu_item = Gio::MenuItem::create(base_name, "ui_menu.select_file");
+
+ menu_item->set_action_and_target("ui_menu.select_file", Glib::Variant::create(
+ base_name));
+
+ menu_model->append_item(menu_item);
+ }
+ }
+ }
+
+ layout_select.set_menu_model(menu_model);
+ layout_select.get_popover()->set_autohide(false);
+
+ suggestions.set_hexpand(true);
+
+ layout_box.set_orientation(Gtk::Orientation::VERTICAL);
+ layout_box.append(headerbar_box);
+ layout_box.set_spacing(OSK_SPACING);
+ this->set_child(layout_box);
+}
+
+WaylandWindow::WaylandWindow(int width, int height, std::string anchor, int headerbar_size) :
+ Gtk::Window()
+{
+ init_headerbar(headerbar_size);
+ /* setup gtk layer shell */
+ init(width, height, anchor);
+}
+
+WaylandWindow::~WaylandWindow()
+{
+ for (auto signal : signals)
+ {
+ signal.disconnect();
+ }
+}
+
+void WaylandWindow::set_widget(Gtk::Widget& w)
+{
+ if (current_widget)
+ {
+ this->layout_box.remove(*current_widget);
+ }
+
+ this->layout_box.append(w);
+ current_widget = &w;
+
+ w.set_margin_bottom(OSK_SPACING);
+ w.set_margin_start(OSK_SPACING);
+ w.set_margin_end(OSK_SPACING);
+ this->show();
+}
+
+void WaylandWindow::clear_suggestions()
+{
+ for (auto child : suggestions.get_children())
+ {
+ suggestions.remove(*child);
+ }
+}
+
+void WaylandWindow::set_suggestions(std::vector all, uint64_t seq_id)
+{
+ if (seq_id < last_suggestion)
+ {
+ return;
+ }
+
+ clear_suggestions();
+ for (auto sugg : all)
+ {
+ auto button = new Gtk::Button();
+ button->set_label(sugg);
+ button->signal_clicked().connect([=] ()
+ {
+ WayfireOsk::get().get_device().accept_suggestion(sugg);
+ });
+ suggestions.append(*button);
+ }
+}
diff --git a/src/osk/wayland-window.hpp b/src/osk/wayland-window.hpp
new file mode 100644
index 00000000..be623616
--- /dev/null
+++ b/src/osk/wayland-window.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include "gtkmm/menubutton.h"
+#include "wf-option-wrap.hpp"
+#include
+#include
+#include
+#include
+#include
+
+#define OSK_SPACING 8
+
+class WaylandWindow : public Gtk::Window
+{
+ std::vector signals;
+ zwf_surface_v2 *wf_surface = nullptr;
+
+ Gtk::Widget *current_widget = nullptr;
+ Gtk::Button close_button;
+ Gtk::Button top_button;
+ Gtk::Button bottom_button;
+ Gtk::MenuButton layout_select;
+
+ Gtk::Box suggestions;
+ Gtk::Box headerbar_box;
+ Gtk::Box layout_box;
+
+ std::atomic last_suggestion{0};
+
+ WfOption exclusion{"osk/exclusion"};
+
+ int32_t check_anchor(std::string anchor);
+ void init(int width, int height, std::string anchor);
+ void init_headerbar(int headerbar_size);
+
+ public:
+ WaylandWindow(int width, int height, std::string anchor, int headerbar_size);
+ ~WaylandWindow();
+ void set_widget(Gtk::Widget& w);
+ void clear_suggestions();
+ void set_suggestions(std::vector string_suggestions, uint64_t seq_id);
+};