ソースを参照

global key handler

milan jurkulák 2 年 前
親
コミット
d20d330a20

+ 24 - 25
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt

@@ -33,9 +33,9 @@ import eu.mjdev.desktop.extensions.Compose.rememberComputed
 import eu.mjdev.desktop.extensions.Compose.rememberState
 import eu.mjdev.desktop.extensions.Compose.removeLast
 import eu.mjdev.desktop.extensions.Modifier.dropShadow
-import eu.mjdev.desktop.helpers.internal.KeyEventHandler.Companion.keyEventHandler
 import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuEnterAnimation
 import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuExitAnimation
+import eu.mjdev.desktop.helpers.internal.KeyEventHandler.Companion.globalKeyEventHandler
 import eu.mjdev.desktop.provider.DesktopScope
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 import eu.mjdev.desktop.windows.ChromeWindow
@@ -83,35 +83,34 @@ fun AppsMenu(
         )
     }
     val windowState: ChromeWindowState = rememberChromeWindowState(position = position)
+    globalKeyEventHandler(menuState.isVisible && menuState.enabled) {
+        onEscape {
+            menuState.hide()
+            true
+        }
+        onBack {
+            searchTextState.clear()
+            true
+        }
+        onBackSpace {
+            searchTextState.removeLast()
+            true
+        }
+        onDelete {
+            searchTextState.clear()
+            true
+        }
+        onChar { char ->
+            searchTextState + char
+            true
+        }
+    }
     ChromeWindow(
         visible = menuState.isVisible,
         enterAnimation = enterAnimation,
         exitAnimation = exitAnimation,
         windowState = windowState,
-        onFocusChange = { focused ->
-            menuState.onFocusChange(focused)
-            onFocusChange(focused)
-        },
-        onKeyEvent = { event ->
-            keyEventHandler(event) {
-                onEscape {
-                    menuState.hide()
-                }
-                onBack {
-                    searchTextState.clear()
-                }
-                onBackSpace {
-                    searchTextState.removeLast()
-                }
-                onDelete {
-                    searchTextState.clear()
-                }
-                onChar { char ->
-                    searchTextState + char
-                }
-            }
-            true
-        }
+        onFocusChange = onFocusChange
     ) {
         Box(
             modifier = Modifier

+ 9 - 8
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt

@@ -32,6 +32,7 @@ import eu.mjdev.desktop.extensions.Compose.rememberComputed
 import eu.mjdev.desktop.extensions.Compose.rememberState
 import eu.mjdev.desktop.helpers.animation.Animations.ControlCenterEnterAnimation
 import eu.mjdev.desktop.helpers.animation.Animations.ControlCenterExitAnimation
+import eu.mjdev.desktop.helpers.internal.KeyEventHandler.Companion.globalKeyEventHandler
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 import eu.mjdev.desktop.windows.ChromeWindow
 import eu.mjdev.desktop.windows.ChromeWindowState
@@ -52,7 +53,7 @@ fun ControlCenter(
 ) = withDesktopScope {
     // todo remove those
     val backgroundAlpha by remember { theme.controlCenterBackgroundAlphaState }
-    val controlCenterExpandedWidth : Dp by rememberComputed {
+    val controlCenterExpandedWidth: Dp by rememberComputed {
         (containerSize.width / 100f) * theme.controlCenterExpandedWidth
     }
     val controlCenterDividerWidth by remember { theme.controlCenterDividerWidthState }
@@ -69,15 +70,18 @@ fun ControlCenter(
         }
     }
     val windowState: ChromeWindowState = rememberChromeWindowState(position = position, size = size)
+    globalKeyEventHandler(controlCenterState.isVisible && controlCenterState.enabled) {
+        onEscape {
+            controlCenterState.hide()
+            true
+        }
+    }
     ChromeWindow(
         visible = true,
         windowState = windowState,
         enterAnimation = enterAnimation,
         exitAnimation = exitAnimation,
-        onFocusChange = { focused ->
-            controlCenterState.onFocusChange(focused)
-            onFocusChange(focused)
-        }
+        onFocusChange = onFocusChange
     ) {
         SlidingMenu(
             modifier = Modifier.fillMaxHeight(),
@@ -86,9 +90,6 @@ fun ControlCenter(
             onPointerEnter = {
                 controlCenterState.show()
                 windowState.requestFocus()
-            },
-            onPointerLeave = {
-                // controlCenterState.hide()
             }
         ) { isVisible ->
             if (!isVisible) {

+ 9 - 4
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanel.kt

@@ -35,6 +35,7 @@ import eu.mjdev.desktop.extensions.Compose.rememberState
 import eu.mjdev.desktop.extensions.Modifier.topShadow
 import eu.mjdev.desktop.helpers.animation.Animations.DesktopPanelEnterAnimation
 import eu.mjdev.desktop.helpers.animation.Animations.DesktopPanelExitAnimation
+import eu.mjdev.desktop.helpers.internal.KeyEventHandler.Companion.globalKeyEventHandler
 import eu.mjdev.desktop.provider.DesktopScope
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 import eu.mjdev.desktop.windows.ChromeWindow
@@ -83,16 +84,20 @@ fun DesktopPanel(
         size = panelSize
     )
     panelState.updateSize(panelSize - DpSize(0.dp, tooltipHeight))
+    globalKeyEventHandler(panelState.isVisible && panelState.enabled) {
+        onEscape {
+            panelState.hide()
+            true
+        }
+    }
     ChromeWindow(
         windowState = windowState,
         visible = true,
         alwaysOnTop = panelState.enabled,
         enterAnimation = enterAnimation,
         exitAnimation = exitAnimation,
-        onFocusChange = { focused ->
-            panelState.onFocusChange(focused)
-            onFocusChange(focused)
-        }) {
+        onFocusChange = onFocusChange
+    ) {
         SlidingMenu(
             modifier = Modifier.fillMaxWidth(),
             orientation = Vertical,

+ 3 - 9
src/main/kotlin/eu/mjdev/desktop/components/main/MainWindow.kt

@@ -40,10 +40,8 @@ fun MainWindow() = withDesktopScope {
             menuState = menuState,
             onMenuIconClicked = { if (!menuState.isVisible) menuState.show() },
             onFocusChange = { focus ->
-                if (!focus) {
-                    if (!menuState.isVisible) {
-                        panelState.hide()
-                    }
+                if (panelState.enabled && !focus && !menuState.isVisible) {
+                    panelState.hide()
                 }
             }
         )
@@ -51,12 +49,8 @@ fun MainWindow() = withDesktopScope {
             menuState = menuState,
             panelState = panelState,
             onFocusChange = { focus ->
-                if (!menuState.isWindowFocus && !focus && !controlCenterState.isWindowFocus) {
+                if (!focus) {
                     menuState.hide()
-                } else {
-                    if(menuState.isVisible) {
-                        requestFocus()
-                    }
                 }
             }
         )

+ 0 - 8
src/main/kotlin/eu/mjdev/desktop/components/sliding/base/VisibilityState.kt

@@ -16,7 +16,6 @@ class VisibilityState(
 ) {
     private val visibleState: MutableState<Boolean> = mutableStateOf(startState)
     private val boundsState: MutableState<DpBounds> = mutableStateOf(DpBounds.Zero)
-    private val focusState: MutableState<Boolean> = mutableStateOf(false)
 
     var bounds: DpBounds
         get() = boundsState.value
@@ -35,9 +34,6 @@ class VisibilityState(
             }
         }
 
-    val isWindowFocus
-        get() = focusState.value
-
     fun show() {
         if (!isVisible) {
             if (enabled) {
@@ -62,10 +58,6 @@ class VisibilityState(
         bounds = lc.toDpBounds()
     }
 
-    fun onFocusChange(focus: Boolean) {
-        focusState.value = focus
-    }
-
     fun updateSize(panelSize: DpSize) {
         bounds.height = panelSize.height
         bounds.width = panelSize.width

+ 5 - 1
src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt

@@ -249,13 +249,17 @@ object Compose {
     }
 
     fun MutableState<String>.removeLast() {
-        value.take(value.length - 1)
+        if (value.isNotEmpty()) value = value.take(value.length - 1)
     }
 
     operator fun MutableState<String>.plus(text: String) {
         value += text
     }
 
+    operator fun MutableState<String>.plus(char: Char) {
+        value += char
+    }
+
     @Composable
     fun Modifier.requestFocusOnTouch(
         focusRequester: FocusRequester,

+ 9 - 0
src/main/kotlin/eu/mjdev/desktop/extensions/Custom.kt

@@ -48,6 +48,15 @@ object Custom {
         block: () -> T
     ) = if (this == null) block() else this as T
 
+    val Char.isPrintable: Boolean
+        get() {
+            val block = Character.UnicodeBlock.of(this)
+            return (!Character.isISOControl(this)) &&
+                    this != java.awt.event.KeyEvent.CHAR_UNDEFINED &&
+                    block != null &&
+                    block != Character.UnicodeBlock.SPECIALS
+        }
+
     fun ParsedList(
         value: String?,
         delimiter: String = ";"

+ 77 - 25
src/main/kotlin/eu/mjdev/desktop/helpers/internal/KeyEventHandler.kt

@@ -8,55 +8,107 @@
 
 package eu.mjdev.desktop.helpers.internal
 
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.input.key.*
+import eu.mjdev.desktop.extensions.Custom.isPrintable
+import java.awt.AWTEvent
+import java.awt.Toolkit
+import java.awt.event.AWTEventListener
+import java.awt.event.KeyEvent as AwtKeyEvent
 
 @Suppress("CanBeParameter", "unused", "MemberVisibilityCanBePrivate")
 class KeyEventHandler(
-    private val event: KeyEvent,
+    private val event: AwtKeyEvent,
     private val block: KeyEventHandler.() -> Unit
 ) {
     private val listeners = mutableListOf<KeyEventListener>()
 
-    fun addListener(key: Key?, block: (String) -> Unit) {
+    fun addListener(key: Key?, block: (Char) -> Boolean) {
         listeners.add(KeyEventListener(key, block))
     }
 
     init {
         block()
-        if (event.type == KeyEventType.KeyDown) {
-            val char = Char(event.key.keyCode.toInt())
-            when {
-                char.isPrintable -> listeners.filter { l -> l.key == null }.forEach { l -> l.block(parseCharacter(event)) }
-                else -> listeners.filter { l -> l.key == event.key }.forEach { l -> l.block(parseCharacter(event)) }
+        val code = event.keyCode
+        val char = parseCharacter(event)
+        val isKeyUp = event.id == AwtKeyEvent.KEY_RELEASED
+        when {
+            isKeyUp && char.isPrintable -> {
+                var consumed = false
+                listeners.filter { l ->
+                    l.key == null
+                }.forEach { l ->
+                    consumed = consumed || l.block(char)
+                }
+                if (consumed) event.consume()
             }
+
+            isKeyUp -> {
+                var consumed = false
+                listeners.filter { l ->
+                    l.key?.keyCode?.toInt() == code
+                }.forEach { l ->
+                    consumed = consumed || l.block(char)
+                }
+                if (consumed) event.consume()
+            }
+
+            else -> Unit
         }
     }
 
-    private val Char.isPrintable : Boolean get() {
-        val block = Character.UnicodeBlock.of(this)
-        return (!Character.isISOControl(this)) &&
-                this != java.awt.event.KeyEvent.CHAR_UNDEFINED &&
-                block != null &&
-                block != Character.UnicodeBlock.SPECIALS
+    private fun parseCharacter(event: AwtKeyEvent): Char = Char(event.keyCode).let { c ->
+        if (event.isShiftDown) c.uppercaseChar() else c.lowercaseChar()
     }
 
-    private fun parseCharacter(event: KeyEvent) = Char(event.key.keyCode.toInt()).let { c ->
-        if (event.isShiftPressed) c.uppercase() else c.lowercase()
-    }
+    fun onChar(block: (Char) -> Boolean) = addListener(null, block)
+    fun onEscape(block: (Char) -> Boolean) = addListener(Key.Escape, block)
+    fun onDelete(block: (Char) -> Boolean) = addListener(Key.Delete, block)
+    fun onEnter(block: (Char) -> Boolean) = addListener(Key.Enter, block)
+    fun onBack(block: (Char) -> Boolean) = addListener(Key.Back, block)
+    fun onBackSpace(block: (Char) -> Boolean) = addListener(Key.Backspace, block)
 
-    fun onChar(block: (String) -> Unit) = addListener(null, block)
-    fun onEscape(block: (String) -> Unit) = addListener(Key.Escape, block)
-    fun onDelete(block: (String) -> Unit) = addListener(Key.Delete, block)
-    fun onEnter(block: (String) -> Unit) = addListener(Key.Enter, block)
-    fun onBack(block: (String) -> Unit) = addListener(Key.Back, block)
-    fun onBackSpace(block: (String) -> Unit) = addListener(Key.Back, block)
+    class KeyEventListener(val key: Key?, val block: (Char) -> Boolean)
+
+    class GlobalKeyListener(
+        val enabled: Boolean = true,
+        val onKey: (event: AwtKeyEvent) -> Unit
+    ) : AWTEventListener {
+        init {
+            Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.KEY_EVENT_MASK)
+        }
+
+        fun dispose() {
+            Toolkit.getDefaultToolkit().removeAWTEventListener(this)
+        }
 
-    class KeyEventListener(val key: Key?, val block: (String) -> Unit)
+        override fun eventDispatched(event: AWTEvent?) {
+            (event as? AwtKeyEvent)?.also { kev ->
+                if (enabled && !event.isConsumed) onKey(kev)
+            }
+        }
+    }
 
     companion object {
         fun keyEventHandler(
-            event: KeyEvent,
+            event: AwtKeyEvent,
             block: KeyEventHandler.() -> Unit
         ) = KeyEventHandler(event, block)
+
+        @Composable
+        fun globalKeyEventHandler(
+            enabled: Boolean = true,
+            block: KeyEventHandler.() -> Unit
+        ) {
+            val globalHandler = GlobalKeyListener(enabled) { event ->
+                keyEventHandler(event, block)
+            }
+            DisposableEffect(Unit) {
+                onDispose {
+                    globalHandler.dispose()
+                }
+            }
+        }
     }
-}
+}