const { Gtk } = imports.gi; import App from 'resource:///com/github/Aylur/ags/app.js'; import Widget from 'resource:///com/github/Aylur/ags/widget.js'; import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'; const { Box, EventBox, Button, Revealer } = Widget; const { execAsync } = Utils; import { MaterialIcon } from '../.commonwidgets/materialicon.js'; import { DEFAULT_OSK_LAYOUT, oskLayouts } from './data_keyboardlayouts.js'; import { setupCursorHoverGrab } from '../.widgetutils/cursorhover.js'; const keyboardLayout = oskLayouts[userOptions.onScreenKeyboard.layout] ? userOptions.onScreenKeyboard.layout : DEFAULT_OSK_LAYOUT; const keyboardJson = oskLayouts[keyboardLayout]; async function startYdotoolIfNeeded() { const running = exec('pidof ydotool') if (!running) execAsync(['ydotoold']).catch(print); } function releaseAllKeys() { const keycodes = Array.from(Array(249).keys()); execAsync([`ydotool`, `key`, ...keycodes.map(keycode => `${keycode}:0`)]) .then(console.log('[OSK] Released all keys')) .catch(print); } class ShiftMode { static Off = new ShiftMode('Off'); static Normal = new ShiftMode('Normal'); static Locked = new ShiftMode('Locked'); constructor(name) { this.name = name; } toString() { return `ShiftMode.${this.name}`; } } var modsPressed = false; const TopDecor = () => Box({ vertical: true, children: [ Box({ hpack: 'center', className: 'osk-dragline', homogeneous: true, children: [EventBox({ setup: setupCursorHoverGrab, })] }) ] }); const KeyboardControlButton = (icon, text, runFunction) => Button({ className: 'osk-control-button spacing-h-10', onClicked: () => runFunction(), child: Widget.Box({ children: [ MaterialIcon(icon, 'norm'), Widget.Label({ label: `${text}`, }), ] }) }) const KeyboardControls = () => Box({ vertical: true, className: 'spacing-v-5', children: [ Button({ className: 'osk-control-button txt-norm icon-material', onClicked: () => { releaseAllKeys(); toggleWindowOnAllMonitors('osk'); }, label: 'keyboard_hide', }), Button({ className: 'osk-control-button txt-norm', label: `${keyboardJson['name_short']}`, }), Button({ className: 'osk-control-button txt-norm icon-material', onClicked: () => { // TODO: Proper clipboard widget, since fuzzel doesn't receive mouse inputs execAsync([`bash`, `-c`, "pkill fuzzel || cliphist list | fuzzel --no-fuzzy --dmenu | cliphist decode | wl-copy"]).catch(print); }, label: 'assignment', }), ] }) var shiftMode = ShiftMode.Off; var shiftButton; var rightShiftButton; var allButtons = []; const KeyboardItself = (kbJson) => { return Box({ vertical: true, className: 'spacing-v-5', children: kbJson.keys.map(row => Box({ vertical: false, className: 'spacing-h-5', children: row.map(key => { return Button({ className: `osk-key osk-key-${key.shape}`, hexpand: ["space", "expand"].includes(key.shape), label: key.label, attribute: { key: key }, setup: (button) => { let pressed = false; allButtons = allButtons.concat(button); if (key.keytype == "normal") { button.connect('pressed', () => { // mouse down execAsync(`ydotool key ${key.keycode}:1`).catch(print); }); button.connect('clicked', () => { // release execAsync(`ydotool key ${key.keycode}:0`).catch(print); if (shiftMode == ShiftMode.Normal) { shiftMode = ShiftMode.Off; if (typeof shiftButton !== 'undefined') { execAsync(`ydotool key 42:0`).catch(print); shiftButton.toggleClassName('osk-key-active', false); } if (typeof rightShiftButton !== 'undefined') { execAsync(`ydotool key 54:0`).catch(print); rightShiftButton.toggleClassName('osk-key-active', false); } allButtons.forEach(button => { if (typeof button.attribute.key.labelShift !== 'undefined') button.label = button.attribute.key.label; }) } }); } else if (key.keytype == "modkey") { button.connect('pressed', () => { // release if (pressed) { execAsync(`ydotool key ${key.keycode}:0`).catch(print); button.toggleClassName('osk-key-active', false); pressed = false; if (key.keycode == 100) { // Alt Gr button allButtons.forEach(button => { if (typeof button.attribute.key.labelAlt !== 'undefined') button.label = button.attribute.key.label; }); } } else { execAsync(`ydotool key ${key.keycode}:1`).catch(print); button.toggleClassName('osk-key-active', true); if (!(key.keycode == 42 || key.keycode == 54)) pressed = true; else switch (shiftMode.name) { // This toggles the shift button state case "Off": { shiftMode = ShiftMode.Normal; allButtons.forEach(button => { if (typeof button.attribute.key.labelShift !== 'undefined') button.label = button.attribute.key.labelShift; }) if (typeof shiftButton !== 'undefined') { shiftButton.toggleClassName('osk-key-active', true); } if (typeof rightShiftButton !== 'undefined') { rightShiftButton.toggleClassName('osk-key-active', true); } } break; case "Normal": { shiftMode = ShiftMode.Locked; if (typeof shiftButton !== 'undefined') shiftButton.label = key.labelCaps; if (typeof rightShiftButton !== 'undefined') rightShiftButton.label = key.labelCaps; } break; case "Locked": { shiftMode = ShiftMode.Off; if (typeof shiftButton !== 'undefined') { shiftButton.label = key.label; shiftButton.toggleClassName('osk-key-active', false); } if (typeof rightShiftButton !== 'undefined') { rightShiftButton.label = key.label; rightShiftButton.toggleClassName('osk-key-active', false); } execAsync(`ydotool key ${key.keycode}:0`).catch(print); allButtons.forEach(button => { if (typeof button.attribute.key.labelShift !== 'undefined') button.label = button.attribute.key.label; } ) }; } if (key.keycode == 100) { // Alt Gr button allButtons.forEach(button => { if (typeof button.attribute.key.labelAlt !== 'undefined') button.label = button.attribute.key.labelAlt; }); } modsPressed = true; } }); if (key.keycode == 42) shiftButton = button; else if (key.keycode == 54) rightShiftButton = button; } } }) }) })) }) } const KeyboardWindow = () => Box({ vexpand: true, hexpand: true, vertical: true, className: 'osk-window spacing-v-5', children: [ TopDecor(), Box({ className: 'osk-body spacing-h-10', children: [ KeyboardControls(), Widget.Box({ className: 'separator-line' }), KeyboardItself(keyboardJson), ], }) ], setup: (self) => self.hook(App, (self, name, visible) => { // Update on open if (!name) return; if (name.startsWith('osk') && visible) { self.setCss(`margin-bottom: -0px;`); } }), }); export default ({ id }) => { const kbWindow = KeyboardWindow(); const gestureEvBox = EventBox({ child: kbWindow }) const gesture = Gtk.GestureDrag.new(gestureEvBox); gesture.connect('drag-begin', async () => { try { const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default; Hyprland.messageAsync('j/cursorpos').then((out) => { gesture.startY = JSON.parse(out).y; }).catch(print); } catch { return; } }); gesture.connect('drag-update', async () => { try { const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default; Hyprland.messageAsync('j/cursorpos').then((out) => { const currentY = JSON.parse(out).y; const offset = gesture.startY - currentY; if (offset > 0) return; kbWindow.setCss(` margin-bottom: ${offset}px; `); }).catch(print); } catch { return; } }); gesture.connect('drag-end', () => { var offset = gesture.get_offset()[2]; if (offset > 50) { App.closeWindow(`osk${id}`); } else { kbWindow.setCss(` transition: margin-bottom 170ms cubic-bezier(0.05, 0.7, 0.1, 1); margin-bottom: 0px; `); } }) return gestureEvBox; };