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 @@ + + + + + GTK_ORIENTATION_VERTICAL + 10 + true + true + + + GTK_ORIENTATION_HORIZONTAL + 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 + + + + + 95 + 50 + BKSP + + + + + + + GTK_ORIENTATION_HORIZONTAL + 10 + true + true + + + 80 + 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 + + + + + 80 + 50 + BKSL + + + + + + + GTK_ORIENTATION_HORIZONTAL + 10 + true + true + + + 95 + 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 + + + + + 115 + 50 + RTRN + + + + + + + GTK_ORIENTATION_HORIZONTAL + 10 + true + true + + + 125 + 50 + LFSH + + + + + 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 + + + + + 65 + 50 + UP + + + + + + + GTK_ORIENTATION_HORIZONTAL + true + true + 10 + + + 65 + 50 + LCTL + + + + + 65 + 50 + LWIN + + + + + 64 + 50 + LALT + + + + + 340 + 50 + SPCE + + + + + 65 + 50 + RALT + + + + + 65 + 50 + RWIN + + + + + 65 + 50 + RCTL + + + + + 65 + 50 + LEFT + + + + + 65 + 50 + DOWN + + + + + 65 + 50 + RGHT + + + + + + \ 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); +};