Add keyboard control (WIP)
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package ch.dissem.yaep.ui.common
|
package ch.dissem.yaep.ui.common
|
||||||
|
|
||||||
|
import SelectionManager
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -15,6 +16,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.TextUnit
|
import androidx.compose.ui.unit.TextUnit
|
||||||
import androidx.compose.ui.unit.TextUnitType
|
import androidx.compose.ui.unit.TextUnitType
|
||||||
import ch.dissem.yaep.domain.Game
|
import ch.dissem.yaep.domain.Game
|
||||||
|
import focus
|
||||||
import ch.dissem.yaep.domain.HorizontalClue
|
import ch.dissem.yaep.domain.HorizontalClue
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -25,6 +27,7 @@ import kotlin.time.ExperimentalTime
|
|||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
fun App(
|
fun App(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
selectionManager: SelectionManager,
|
||||||
selectDirectly: Boolean,
|
selectDirectly: Boolean,
|
||||||
spacing: Dp,
|
spacing: Dp,
|
||||||
game: Game,
|
game: Game,
|
||||||
@@ -56,6 +59,8 @@ fun App(
|
|||||||
modifier = Modifier.blurOnFinished(isSolved),
|
modifier = Modifier.blurOnFinished(isSolved),
|
||||||
grid = {
|
grid = {
|
||||||
PuzzleGrid(
|
PuzzleGrid(
|
||||||
|
modifier = Modifier
|
||||||
|
.focus(remember { selectionManager.add() }),
|
||||||
grid = game.grid,
|
grid = game.grid,
|
||||||
spacing = spacing,
|
spacing = spacing,
|
||||||
selectDirectly = selectDirectly,
|
selectDirectly = selectDirectly,
|
||||||
@@ -66,9 +71,12 @@ fun App(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
horizontalClues = {
|
horizontalClues = {
|
||||||
|
val horizontalClueSelection = remember { selectionManager.add() }
|
||||||
for (clue in horizontalClues) {
|
for (clue in horizontalClues) {
|
||||||
HorizontalClue(
|
HorizontalClue(
|
||||||
modifier = Modifier.forClue(clue, spacing),
|
modifier = Modifier
|
||||||
|
.focus(horizontalClueSelection)
|
||||||
|
.forClue(clue, spacing),
|
||||||
spacing = spacing,
|
spacing = spacing,
|
||||||
clue = clue.clue,
|
clue = clue.clue,
|
||||||
isClueViolated = clue.isViolated
|
isClueViolated = clue.isViolated
|
||||||
@@ -76,9 +84,12 @@ fun App(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
verticalClues = {
|
verticalClues = {
|
||||||
|
val verticalClueSelection = remember { selectionManager.add() }
|
||||||
for (clue in verticalClues) {
|
for (clue in verticalClues) {
|
||||||
VerticalClue(
|
VerticalClue(
|
||||||
modifier = Modifier.forClue(clue, spacing),
|
modifier = Modifier
|
||||||
|
.focus(verticalClueSelection)
|
||||||
|
.forClue(clue, spacing),
|
||||||
spacing = spacing,
|
spacing = spacing,
|
||||||
clue = clue.clue,
|
clue = clue.clue,
|
||||||
isClueViolated = clue.isViolated
|
isClueViolated = clue.isViolated
|
||||||
|
|||||||
@@ -1,39 +1,118 @@
|
|||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEvent
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.isShiftPressed
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlin.IllegalStateException
|
||||||
|
|
||||||
class StatusManager {
|
class SelectionManager(
|
||||||
lateinit var focused: StatusHolder
|
val keyNext: Key,
|
||||||
private set
|
val keyPrevious: Key? = null
|
||||||
|
) {
|
||||||
|
var isActiveFlow = MutableStateFlow<Boolean>(false)
|
||||||
|
var isActive: Boolean
|
||||||
|
get() = isActiveFlow.value
|
||||||
|
set(value) {
|
||||||
|
isActiveFlow.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var focusedFlow = MutableStateFlow<Focusable?>(null)
|
||||||
|
|
||||||
|
var focused: Focusable?
|
||||||
|
get() = focusedFlow.value
|
||||||
|
set(value) {
|
||||||
|
focusedFlow.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
val last: Focusable
|
||||||
|
get() = focused?.previous ?: throw IllegalStateException("not initialized")
|
||||||
|
|
||||||
|
val child: SelectionManager?
|
||||||
|
get() = focused?.child
|
||||||
|
|
||||||
fun focusNext() {
|
fun focusNext() {
|
||||||
focused = focused.next
|
focused = focused?.next
|
||||||
}
|
}
|
||||||
|
|
||||||
fun focusPrevious() {
|
fun focusPrevious() {
|
||||||
focused = focused.previous
|
focused = focused?.previous
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(): Focusable {
|
||||||
|
val new = Focusable(this)
|
||||||
|
if (focused != null) {
|
||||||
|
new.next = focused!!
|
||||||
|
} else {
|
||||||
|
focused = new
|
||||||
|
}
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
if (event.type != KeyEventType.KeyUp) return false
|
||||||
|
|
||||||
|
if (event.key == keyNext) {
|
||||||
|
if (keyPrevious == null && event.isShiftPressed) {
|
||||||
|
focusPrevious()
|
||||||
|
} else {
|
||||||
|
focusNext()
|
||||||
|
}
|
||||||
|
} else if (event.key == keyPrevious) {
|
||||||
|
focusPrevious()
|
||||||
|
} else {
|
||||||
|
return child?.onKeyEvent(event) == true
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StatusHolder(private val manager: StatusManager) {
|
class Focusable(
|
||||||
val hasFocus: Boolean
|
manager: SelectionManager,
|
||||||
get() = manager.focused == this
|
) {
|
||||||
var isSelected: Boolean = false
|
val hasFocus: Flow<Boolean> =
|
||||||
|
combine(manager.isActiveFlow, manager.focusedFlow) { isActive, focused ->
|
||||||
|
isActive && focused == this
|
||||||
|
}
|
||||||
|
|
||||||
var previous: StatusHolder = this
|
var previous: Focusable = this
|
||||||
private set
|
private set
|
||||||
var next: StatusHolder = this
|
|
||||||
|
private var _next: Focusable = this
|
||||||
|
var next: Focusable
|
||||||
|
get() = _next
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
previous = value.previous
|
||||||
|
previous._next = this
|
||||||
value.previous = this
|
value.previous = this
|
||||||
|
_next = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var child: SelectionManager? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun createChild(
|
||||||
|
keyNext: Key,
|
||||||
|
keyPrevious: Key? = null
|
||||||
|
): SelectionManager {
|
||||||
|
child = SelectionManager(keyNext, keyPrevious)
|
||||||
|
child!!.isActive = false
|
||||||
|
return child!!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Modifier.status(holder: StatusHolder): Modifier = if (holder.hasFocus) {
|
fun Modifier.focus(holder: Focusable): Modifier = if (holder.hasFocus.collectAsState(false).value) {
|
||||||
border(
|
border(
|
||||||
width = 2.dp,
|
width = 2.dp,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package ch.dissem.yaep.ui.desktop
|
package ch.dissem.yaep.ui.desktop
|
||||||
|
|
||||||
|
import SelectionManager
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -16,6 +17,8 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.WindowPlacement
|
import androidx.compose.ui.window.WindowPlacement
|
||||||
import androidx.compose.ui.window.WindowScope
|
import androidx.compose.ui.window.WindowScope
|
||||||
@@ -40,11 +43,13 @@ import yaep.desktop.generated.resources.Res as DRes
|
|||||||
@Composable
|
@Composable
|
||||||
fun WindowScope.DesktopWindow(
|
fun WindowScope.DesktopWindow(
|
||||||
useDarkMode: Boolean,
|
useDarkMode: Boolean,
|
||||||
|
selectionManager: SelectionManager,
|
||||||
topBar: @Composable () -> Unit,
|
topBar: @Composable () -> Unit,
|
||||||
content: @Composable (PaddingValues) -> Unit
|
content: @Composable (PaddingValues) -> Unit
|
||||||
) {
|
) {
|
||||||
AppTheme(darkTheme = useDarkMode) {
|
AppTheme(darkTheme = useDarkMode) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = Modifier.onKeyEvent { event -> selectionManager.onKeyEvent(event) },
|
||||||
topBar = {
|
topBar = {
|
||||||
WindowDraggableArea {
|
WindowDraggableArea {
|
||||||
topBar()
|
topBar()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -47,10 +48,12 @@ fun main(): Unit = application {
|
|||||||
state = windowState,
|
state = windowState,
|
||||||
icon = painterResource(DRes.drawable.ic_launcher)
|
icon = painterResource(DRes.drawable.ic_launcher)
|
||||||
) {
|
) {
|
||||||
|
val selectionManager = remember { SelectionManager(Key.Tab).apply { isActive = true } }
|
||||||
var useDarkMode by remember { mutableStateOf(true) }
|
var useDarkMode by remember { mutableStateOf(true) }
|
||||||
var resetCluesBeacon by remember { mutableStateOf(Any()) }
|
var resetCluesBeacon by remember { mutableStateOf(Any()) }
|
||||||
DesktopWindow(
|
DesktopWindow(
|
||||||
useDarkMode = useDarkMode,
|
useDarkMode = useDarkMode,
|
||||||
|
selectionManager = selectionManager,
|
||||||
topBar = {
|
topBar = {
|
||||||
AppBar(
|
AppBar(
|
||||||
useDarkMode = useDarkMode,
|
useDarkMode = useDarkMode,
|
||||||
@@ -67,6 +70,7 @@ fun main(): Unit = application {
|
|||||||
) {
|
) {
|
||||||
App(
|
App(
|
||||||
modifier = Modifier.padding(it),
|
modifier = Modifier.padding(it),
|
||||||
|
selectionManager = selectionManager,
|
||||||
spacing = 8.dp,
|
spacing = 8.dp,
|
||||||
selectDirectly = true,
|
selectDirectly = true,
|
||||||
game = game,
|
game = game,
|
||||||
|
|||||||
Reference in New Issue
Block a user