From c3eed7c4973e83e2949e29b79b0ef8fc7e4bb396 Mon Sep 17 00:00:00 2001 From: Maxim Devaev Date: Sun, 18 May 2025 22:07:47 +0300 Subject: [PATCH] pikvm/pikvm#1418: web: hold/lock key on keypad --- web/kvm/index.html | 73 ++++++-------- web/kvm/window-keyboard.pug | 4 +- web/kvm/window-stream.pug | 32 ++---- web/share/css/keypad.css | 65 ++++++++---- web/share/css/vars.css | 3 + web/share/css/x-mobile.css | 6 -- web/share/js/keypad.js | 188 +++++++++++++++++++++-------------- web/share/js/kvm/keyboard.js | 2 +- web/share/js/kvm/mouse.js | 2 +- web/share/js/tools.js | 2 +- 10 files changed, 207 insertions(+), 170 deletions(-) diff --git a/web/kvm/index.html b/web/kvm/index.html index 5c964d39..b8617d9c 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -1003,32 +1003,21 @@
-
-
Left
+
+

Left
-
-

Hold
+
+

Mid
-
-
-
Mid
-
-
-

Hold
-
-
-
-

Hold
-
-
-
Right
+
+

Right
-
-
Up
+
+

Up
-
-
Down
+
+

Down
@@ -1332,7 +1321,7 @@
-
+

Shift
@@ -1387,23 +1376,23 @@
-
+

Shift
-
+

Ctrl
-
+

Win
-
+

Alt
@@ -1413,12 +1402,12 @@
-
+

Alt
-
+

Win
@@ -1428,7 +1417,7 @@
-
+

Ctrl
@@ -1436,7 +1425,7 @@
-
+

Pt/Sq
@@ -1660,19 +1649,19 @@
-
+

Kana
-
+

N/Cnv
-
+

Cnv
@@ -1738,7 +1727,7 @@
-
+

Pt/Sq
@@ -1982,7 +1971,7 @@
-
+

Shift
@@ -2053,17 +2042,17 @@
-
+

Ctrl
-
+

Win
-
+

Alt
@@ -2073,12 +2062,12 @@
-
+

Alt
-
+

Win
@@ -2088,12 +2077,12 @@
-
+

Shift
-
+

Ctrl
diff --git a/web/kvm/window-keyboard.pug b/web/kvm/window-keyboard.pug index e134e628..5c7fcc3e 100644 --- a/web/kvm/window-keyboard.pug +++ b/web/kvm/window-keyboard.pug @@ -13,7 +13,7 @@ mixin key(sp, code, classes="", width=0) mixin modifier(sp, code, classes="", width=0) - div(data-code=code class=`modifier ${classes}` style=(width ? `width: ${width}px` : "")) + div(data-code=code, data-allow-autohold, class=`key ${classes}`, style=(width ? `width: ${width}px` : "")) .label | #[b •]#[br] block @@ -21,7 +21,7 @@ mixin modifier(sp, code, classes="", width=0) mixin empty(sp, classes="", width=0) - div(class=`empty ${classes}` style=(width ? `width:${width}px` : "")) + div(class=`empty ${classes}`, style=(width ? `width:${width}px` : "")) .label   +spacer(sp) diff --git a/web/kvm/window-stream.pug b/web/kvm/window-stream.pug index 51bbc32f..8fc7d199 100644 --- a/web/kvm/window-stream.pug +++ b/web/kvm/window-stream.pug @@ -24,28 +24,16 @@ .keypad#stream-mouse-buttons(align="center") .keypad-block .keypad-row - .key.wide-3.left.rounded-left(data-code="left") - .label Left - .modifier.left.small.rounded-right(data-code="left") - .label #[b •]#[br]Hold - - .empty(style="width: 15px") - - .key.wide-1.left.rounded-left(data-code="middle") - .label Mid - .modifier.left.small.rounded-right(data-code="middle") - .label #[b •]#[br]Hold - - .empty(style="width: 15px") - - .modifier.right.small.rounded-left(data-code="right") - .label #[b •]#[br]Hold - .key.wide-3.right.rounded-right(data-code="right") - .label Right + .key.wide-3.left.rounded-left(data-code="left" data-allow-autohold) + .label #[b •]#[br]Left + .key.wide-1.rounded-none(data-code="middle" data-allow-autohold) + .label #[b •]#[br]Mid + .key.wide-3.right.rounded-right(data-code="right" data-allow-autohold) + .label #[b •]#[br]Right .empty(style="width: 30px") - .key.small.rounded-left(data-code="up") - .label Up - .key.small.rounded-right(data-code="down") - .label Down + .key.small.rounded-left(data-code="up" data-allow-autohold) + .label #[b •]#[br]Up + .key.small.rounded-right(data-code="down" data-allow-autohold) + .label #[b •]#[br]Down diff --git a/web/share/css/keypad.css b/web/share/css/keypad.css index 141ecf18..7456b8c5 100644 --- a/web/share/css/keypad.css +++ b/web/share/css/keypad.css @@ -52,7 +52,6 @@ div.keypad div.keypad-row div.spacer-fixed { } div.keypad div.key, -div.keypad div.modifier, div.keypad div.empty { vertical-align: top; font-size: 0.9em; @@ -65,8 +64,7 @@ div.keypad div.empty { div.keypad div.empty { border: thin solid transparent; } -div.keypad div.key, -div.keypad div.modifier { +div.keypad div.key { box-shadow: var(--shadow-micro); border: var(--border-key-thin); border-radius: 6px; @@ -74,33 +72,57 @@ div.keypad div.modifier { background-color: var(--cs-key-default-bg); cursor: pointer; } -div.keypad div.key:hover, -div.keypad div.modifier:hover { - color: var(--cs-key-hovered-fg); - background-color: var(--cs-key-hovered-bg); +@media (hover: hover) { + div.keypad div.key:not(div.holded):not(div.locked):hover { + color: var(--cs-key-hovered-fg); + background-color: var(--cs-key-hovered-bg); + } } + div.keypad div.rounded-left { - border-radius: 6px 0px 0px 6px !important; + border-radius: 6px 0px 0px 6px; } div.keypad div.rounded-right { - border-radius: 0px 6px 6px 0px !important; + border-radius: 0px 6px 6px 0px; } div.keypad div.rounded-none { - border-radius: 0px !important; + border-radius: 0px; } + div.keypad div.pressed { box-shadow: none; - color: var(--cs-key-pressed-fg) !important; - background-color: var(--cs-key-pressed-bg) !important; + color: var(--cs-key-pressed-fg); + background-color: var(--cs-key-pressed-bg); } + +div.keypad div.pressed:not(div.holded):not(div.locked):hover[data-allow-autohold] { + /* :active is not working on Firefox and iOS */ + background: linear-gradient(to top, var(--cs-key-holded-bg) 50%, var(--cs-key-pressed-bg) 0); + background-size: 100% 200%; + background-position: top; + animation: keypad-animate-holding 0.2s 0.3s forwards; +} +@keyframes keypad-animate-holding { + 100% { + background-position: bottom; + } +} + div.keypad div.holded { - box-shadow: none; - color: var(--cs-key-default-fg) !important; + /* Override animation end on iOS with !important */ + box-shadow: none !important; + color: var(--cs-key-holded-fg) !important; background-color: var(--cs-key-holded-bg) !important; } + +div.keypad div.locked { + box-shadow: none; + color: var(--cs-key-locked-fg); + background-color: var(--cs-key-locked-bg); +} + div.keypad div.key:last-child, -div.keypad div.empty:last-child, -div.keypad div.modifier:last-child { +div.keypad div.empty:last-child { margin-right: 0; } div.keypad div.wide-0 { @@ -120,12 +142,12 @@ div.keypad div.wide-4 { width: 288px; } div.keypad div.left { - text-align: left !important; - padding-left: 6px !important; + text-align: left; + padding-left: 6px; } div.keypad div.right { - text-align: right !important; - padding-right: 6px !important; + text-align: right; + padding-right: 6px; } div.keypad div.small { font-size: 0.7em; @@ -142,3 +164,6 @@ div.keypad div.label { div.keypad b { color: var(--cs-key-holded-bg); } +div.keypad div.locked b { + color: var(--cs-key-locked-bg); +} diff --git a/web/share/css/vars.css b/web/share/css/vars.css index 289c9dd2..cfa5a0b1 100644 --- a/web/share/css/vars.css +++ b/web/share/css/vars.css @@ -66,6 +66,9 @@ --cs-key-pressed-bg: #17191d; --cs-key-pressed-fg: #6c7481; --cs-key-holded-bg: #436a8a; + --cs-key-holded-fg: white; + --cs-key-locked-bg: #a80000; + --cs-key-locked-fg: white; --cs-marker-fg: #5b90bb; --cs-corner-bg: #5b90bb; diff --git a/web/share/css/x-mobile.css b/web/share/css/x-mobile.css index 46d0b5ea..7db2bfd1 100644 --- a/web/share/css/x-mobile.css +++ b/web/share/css/x-mobile.css @@ -113,9 +113,3 @@ ul#navbar li a.menu-button:hover:not(.active) { div.keypad { zoom: 1.28 !important; } - -div.keypad div.key:hover, -div.keypad div.modifier:hover { - color: var(--cs-key-default-fg); - background-color: var(--cs-key-default-bg); -} diff --git a/web/share/js/keypad.js b/web/share/js/keypad.js index 454bc405..a432e92d 100644 --- a/web/share/js/keypad.js +++ b/web/share/js/keypad.js @@ -23,73 +23,67 @@ "use strict"; -import {tools, $$$} from "./tools.js"; +import {tools} from "./tools.js"; -export function Keypad(__keys_parent, __sendKey, __apply_fixes) { +export function Keypad(__el_keypad, __sendKey, __apply_fixes) { var self = this; /************************************************************************/ - var __merged = {}; var __keys = {}; - var __modifiers = {}; + var __hold_timers = {}; var __fix_mac_cmd = false; var __fix_win_altgr = false; var __altgr_ctrl_timer = null; var __init__ = function() { + __el_keypad.addEventListener("contextmenu", (ev) => ev.preventDefault()); + if (__apply_fixes) { __fix_mac_cmd = tools.browser.is_mac; if (__fix_mac_cmd) { - tools.info(`Keymap at ${__keys_parent}: enabled Fix-Mac-CMD`); + tools.info(`Keymap at ${__el_keypad.id}: enabled Fix-Mac-CMD`); } __fix_win_altgr = tools.browser.is_win; if (__fix_win_altgr) { - tools.info(`Keymap at ${__keys_parent}: enabled Fix-Win-AltGr`); + tools.info(`Keymap at ${__el_keypad.id}: enabled Fix-Win-AltGr`); } } - for (let el_key of $$$(`${__keys_parent} div.key`)) { + for (let el_key of [].slice.call(__el_keypad.getElementsByClassName("key"))) { + if (el_key.hasAttribute("data-allow-autohold")) { + el_key.title = "Long left click or short right click for hold, middle for lock"; + } else { + el_key.title = "Right click for hold, middle for lock"; + } + let code = el_key.getAttribute("data-code"); tools.setDefault(__keys, code, []); __keys[code].push(el_key); - tools.setDefault(__merged, code, []); - __merged[code].push(el_key); - - tools.el.setOnDown(el_key, () => __clickHandler(el_key, true)); - tools.el.setOnUp(el_key, () => __clickHandler(el_key, false)); + tools.el.setOnDown(el_key, (ev) => __clickHandler(el_key, ev)); + tools.el.setOnUp(el_key, () => __clickHandler(el_key, null)); el_key.onmouseout = function() { - if (__isPressed(el_key)) { - __clickHandler(el_key, false); + if ( + __isActive(el_key, "pressed") + && !__isActive(el_key, "holded") + && !__isActive(el_key, "locked") + ) { + __clickHandler(el_key, null); } }; } - - for (let el_key of $$$(`${__keys_parent} div.modifier`)) { - let code = el_key.getAttribute("data-code"); - - tools.setDefault(__modifiers, code, []); - __modifiers[code].push(el_key); - - tools.setDefault(__merged, code, []); - __merged[code].push(el_key); - - tools.el.setOnDown(el_key, () => __toggleModifierHandler(el_key)); - } }; /************************************************************************/ self.releaseAll = function() { - for (let dict of [__keys, __modifiers]) { - for (let code in dict) { - if (__isActive(dict[code][0])) { - self.emitByCode(code, false); - } + for (let code in __keys) { + if (__isActive(__keys[code][0])) { + self.emitByCode(code, false); } } }; @@ -113,7 +107,10 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) { }; self.emitByCode = function(code, state, apply_fixes=true) { - if (code in __merged) { + if (code in __keys) { + let el_key = __keys[code][0]; + __stopHoldTimer(el_key); + if (__fix_win_altgr && apply_fixes) { if (!__fixWinAltgr(code, state)) { return; @@ -122,9 +119,17 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) { if (__fix_mac_cmd && apply_fixes) { __fixMacCmd(code, state); } - __commonHandler(__merged[code][0], state, false); - __unholdModifiers(); - } + + if (state && !__isActive(el_key)) { + __deactivate(el_key); + __activate(el_key, "pressed"); + __process(el_key, true); + } else { + __deactivate(el_key); + __process(el_key, false); + } + __unholdAll(); + }; }; var __fixMacCmd = function(code, state) { @@ -148,7 +153,7 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) { self.emitByCode("ControlLeft", true, false); } } - if (code === "ControlLeft" && !__isActive(__modifiers["ControlLeft"][0])) { + if (code === "ControlLeft" && !__isActive(__keys["ControlLeft"][0])) { __altgr_ctrl_timer = setTimeout(function() { __altgr_ctrl_timer = null; self.emitByCode("ControlLeft", true, false); @@ -165,61 +170,93 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) { return true; // Continue handling }; - var __clickHandler = function(el_key, state) { - __commonHandler(el_key, state, false); - __unholdModifiers(); - }; + var __clickHandler = function(el_key, ev) { + let state = false; + let act = "pressed"; + if (ev) { + state = (ev.type === "mousedown" || ev.type === "touchstart"); + if (ev.type === "mousedown") { + if (ev.button === 1) { + act = "locked"; + } else if (ev.button === 2) { + act = "holded"; + } + } + } - var __toggleModifierHandler = function(el_key) { - __commonHandler(el_key, !__isActive(el_key), true); - }; - - var __commonHandler = function(el_key, state, hold) { if (state && !__isActive(el_key)) { + __stopHoldTimer(el_key); __deactivate(el_key); - __activate(el_key, (hold ? "holded" : "pressed")); + __activate(el_key, act); __process(el_key, true); + __startHoldTimer(el_key); } else { - __deactivate(el_key); - __process(el_key, false); + let fixed = (__isActive(el_key, "holded") || __isActive(el_key, "locked")); + if (!state && fixed && __stopHoldTimer(el_key)) { + return; // Игнорировать первое отжатие сразу после нажатия + } + if (!state) { + __stopHoldTimer(el_key); + __deactivate(el_key); + __process(el_key, false); + if (!fixed) { + __unholdAll(); + } + } } }; - var __unholdModifiers = function() { - for (let code in __modifiers) { - let el_key = __modifiers[code][0]; - if (__isHolded(el_key)) { + var __startHoldTimer = function(el_key) { + __stopHoldTimer(el_key); + let code = el_key.getAttribute("data-code"); + __hold_timers[code] = setTimeout(function() { + // Помимо прямой функции, hold timer используется для детектирования факта + // нажатия в рамках одной сессии press/release, чтобы не отпустить сразу же + // зажатую или заблокированную клавишу. Поэтому таймер инициализируется всегда, + // но основную функцию выполняет только если у него есть атрибут data-allow-autohold. + if (el_key.hasAttribute("data-allow-autohold")) { + __deactivate(el_key); + __activate(el_key, "holded"); + } + }, 500); // Check keypad.css for the animation + }; + + var __stopHoldTimer = function(el_key) { + let code = el_key.getAttribute("data-code"); + if (!__hold_timers[code]) { + return false; + } + clearTimeout(__hold_timers[code]); + __hold_timers[code] = null; + return true; + }; + + var __unholdAll = function() { + for (let el_key of [].slice.call(__el_keypad.getElementsByClassName("key"))) { + __stopHoldTimer(el_key); + if (__isActive(el_key, "holded") && !__isActive(el_key, "locked")) { // Skip duplicating keys __deactivate(el_key); __process(el_key, false); } } }; - var __isPressed = function(el_key) { - let is_pressed = false; + var __isActive = function(el_key, cls=null) { let el_keys = __resolveKeys(el_key); for (el_key of el_keys) { - is_pressed = (is_pressed || el_key.classList.contains("pressed")); + if (cls) { + if (el_key.classList.contains(cls)) { + return true; + } + } else if ( + el_key.classList.contains("pressed") + || el_key.classList.contains("holded") + || el_key.classList.contains("locked") + ) { + return true; + } } - return is_pressed; - }; - - var __isHolded = function(el_key) { - let is_holded = false; - let el_keys = __resolveKeys(el_key); - for (el_key of el_keys) { - is_holded = (is_holded || el_key.classList.contains("holded")); - } - return is_holded; - }; - - var __isActive = function(el_key) { - let is_active = false; - let el_keys = __resolveKeys(el_key); - for (el_key of el_keys) { - is_active = (is_active || el_key.classList.contains("pressed") || el_key.classList.contains("holded")); - } - return is_active; + return false; }; var __activate = function(el_key, cls) { @@ -234,12 +271,13 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) { for (el_key of el_keys) { el_key.classList.remove("pressed"); el_key.classList.remove("holded"); + el_key.classList.remove("locked"); } }; var __resolveKeys = function(el_key) { let code = el_key.getAttribute("data-code"); - return __merged[code]; + return __keys[code]; }; var __process = function(el_key, state) { diff --git a/web/share/js/kvm/keyboard.js b/web/share/js/kvm/keyboard.js index 92218f09..3c30a900 100644 --- a/web/share/js/kvm/keyboard.js +++ b/web/share/js/kvm/keyboard.js @@ -35,7 +35,7 @@ export function Keyboard(__recordWsEvent) { var __keypad = null; var __init__ = function() { - __keypad = new Keypad("div#keyboard-window", __sendKey, true); + __keypad = new Keypad($("keyboard-window"), __sendKey, true); $("hid-keyboard-led").title = "Keyboard free"; diff --git a/web/share/js/kvm/mouse.js b/web/share/js/kvm/mouse.js index 73d59314..fd3c0c7f 100644 --- a/web/share/js/kvm/mouse.js +++ b/web/share/js/kvm/mouse.js @@ -55,7 +55,7 @@ export function Mouse(__getGeometry, __recordWsEvent) { var __stream_hovered = false; var __init__ = function() { - __keypad = new Keypad("div#stream-mouse-buttons", __sendButton, false); + __keypad = new Keypad($("stream-mouse-buttons"), __sendButton, false); $("hid-mouse-led").title = "Mouse free"; diff --git a/web/share/js/tools.js b/web/share/js/tools.js index 3436aba1..5c68fdb2 100644 --- a/web/share/js/tools.js +++ b/web/share/js/tools.js @@ -164,7 +164,7 @@ export var tools = new function() { if (prevent_default) { ev.preventDefault(); } - callback(); + callback(ev); }; }, "setOnUp": function(el, callback, prevent_default=true) {