Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package org.jetbrains.kotlinconf

actual fun getPlatformId(): String = "android"


internal actual val isMacPlatform: Boolean = false
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package org.jetbrains.kotlinconf

internal expect fun getPlatformId(): String

internal expect val isMacPlatform: Boolean
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.jetbrains.kotlinconf

import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import org.jetbrains.kotlinconf.ui.components.MainHeaderContainerState

/**
* It handles Ctrl/Cmd + F key events when the component is focused.
*/
@Composable
expect fun Modifier.searchShortcut(onTriggered: () -> Unit): Modifier
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.liveRegion
Expand All @@ -46,6 +47,7 @@ import org.jetbrains.kotlinconf.ScrollToTopHandler
import org.jetbrains.kotlinconf.SessionCardView
import org.jetbrains.kotlinconf.SessionId
import org.jetbrains.kotlinconf.SessionState
import org.jetbrains.kotlinconf.searchShortcut
import org.jetbrains.kotlinconf.generated.resources.Res
import org.jetbrains.kotlinconf.generated.resources.nav_destination_schedule
import org.jetbrains.kotlinconf.generated.resources.schedule_action_filter_bookmarked
Expand Down Expand Up @@ -144,11 +146,19 @@ fun ScheduleScreen(
viewModel.setSearchParams(params)
}

val searchBarFocusRequester = remember { FocusRequester() }
Column(
modifier = Modifier
.fillMaxSize()
.background(color = KotlinConfTheme.colors.mainBackground)
.padding(topInsetPadding())
.searchShortcut {
if (headerState != MainHeaderContainerState.Search) {
headerState = MainHeaderContainerState.Search
}
// ensure the field is focused
searchBarFocusRequester.requestFocus()
}
) {
Header(
startContent = {
Expand All @@ -163,7 +173,8 @@ fun ScheduleScreen(
searchQuery = searchQuery,
onSearchQueryChange = { searchQuery = it },
onClearSearch = { viewModel.resetFilters() },
viewModel = viewModel
searchBarFocusRequester = searchBarFocusRequester,
viewModel = viewModel,
)
HorizontalDivider(
thickness = 1.dp,
Expand Down Expand Up @@ -308,6 +319,7 @@ private fun Header(
searchQuery: String,
onSearchQueryChange: (String) -> Unit,
onClearSearch: () -> Unit,
searchBarFocusRequester: FocusRequester,
viewModel: ScheduleViewModel,
) {
MainHeaderContainer(
Expand Down Expand Up @@ -354,6 +366,7 @@ private fun Header(
},
onClear = onClearSearch,
hasAdditionalInputs = filterItems.any { it.isSelected },
searchBarFocusRequester = searchBarFocusRequester,
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
Expand All @@ -31,6 +33,7 @@ import org.jetbrains.compose.resources.stringResource
import org.jetbrains.kotlinconf.HideKeyboardOnDragHandler
import org.jetbrains.kotlinconf.ScrollToTopHandler
import org.jetbrains.kotlinconf.SpeakerId
import org.jetbrains.kotlinconf.searchShortcut
import org.jetbrains.kotlinconf.generated.resources.Res
import org.jetbrains.kotlinconf.generated.resources.speakers_error_no_data
import org.jetbrains.kotlinconf.generated.resources.speakers_number_of_results
Expand Down Expand Up @@ -74,10 +77,18 @@ fun SpeakersScreen(
viewModel.setSearchText(searchText)
}

val searchBarFocusRequester = remember { FocusRequester() }
Column(
Modifier.fillMaxSize()
.background(color = KotlinConfTheme.colors.mainBackground)
.padding(topInsetPadding())
.searchShortcut {
if (searchState != MainHeaderContainerState.Search) {
searchState = MainHeaderContainerState.Search
}
// ensure the field is focused
searchBarFocusRequester.requestFocus()
}
) {
MainHeaderContainer(
state = searchState,
Expand Down Expand Up @@ -111,6 +122,7 @@ fun SpeakersScreen(
searchText = ""
},
onClear = { searchText = "" },
searchBarFocusRequester = searchBarFocusRequester,
)
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
Expand All @@ -41,6 +43,7 @@ import org.jetbrains.compose.resources.stringResource
import org.jetbrains.kotlinconf.HideKeyboardOnDragHandler
import org.jetbrains.kotlinconf.ScreenWithTitle
import org.jetbrains.kotlinconf.ScrollToTopHandler
import org.jetbrains.kotlinconf.searchShortcut
import org.jetbrains.kotlinconf.generated.resources.Res
import org.jetbrains.kotlinconf.generated.resources.licenses_number_of_results
import org.jetbrains.kotlinconf.generated.resources.licenses_title
Expand Down Expand Up @@ -87,11 +90,19 @@ fun LicensesScreen(
viewModel.setSearchText(searchText)
}

val searchBarFocusRequester = remember { FocusRequester() }
Column(
modifier = Modifier
.fillMaxSize()
.background(color = KotlinConfTheme.colors.mainBackground)
.padding(topInsetPadding())
.searchShortcut {
if (searchState != MainHeaderContainerState.Search) {
searchState = MainHeaderContainerState.Search
}
// ensure the field is focused
searchBarFocusRequester.requestFocus()
}
) {
MainHeaderContainer(
state = searchState,
Expand Down Expand Up @@ -132,6 +143,7 @@ fun LicensesScreen(
searchText = ""
},
onClear = { searchText = "" },
searchBarFocusRequester = searchBarFocusRequester,
)
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.jetbrains.kotlinconf

import org.jetbrains.skiko.hostOs

internal actual val isMacPlatform: Boolean by lazy {
hostOs.isMacOS
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.jetbrains.kotlinconf

import androidx.compose.foundation.focusable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isMetaPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type

@Composable
actual fun Modifier.searchShortcut(onTriggered: () -> Unit): Modifier {
val focusRequester = remember { FocusRequester() }
var hasFocus by remember { mutableStateOf(false) }

LaunchedEffect(hasFocus) {
if (!hasFocus) {
runCatching { focusRequester.requestFocus() }
}
}

return this
.onFocusChanged { hasFocus = it.hasFocus }
.focusRequester(focusRequester)
.focusable()
.onPreviewKeyEvent { event ->
val modifierPressed = if (isMacPlatform) {
event.isMetaPressed && !event.isCtrlPressed
} else {
event.isCtrlPressed && !event.isMetaPressed
}
val isFindShortcut = event.type == KeyEventType.KeyDown
&& event.key == Key.F
&& modifierPressed
if (isFindShortcut) {
onTriggered()
true
} else {
false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.jetbrains.kotlinconf

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import kotlinx.browser.document
import org.w3c.dom.events.Event
import org.w3c.dom.events.KeyboardEvent

// Web needs its own implementation, because to handle the key events on canvas it has to be focused,
// but it's not always the case.
// To be improved in Compose Multiplatform for web - https://youtrack.jetbrains.com/issue/CMP-9948
@Composable
actual fun Modifier.searchShortcut(onTriggered: () -> Unit): Modifier {
val currentTrigger by rememberUpdatedState(onTriggered)
DisposableEffect(Unit) {
val listener: (Event) -> Unit = { event ->
val keyboardEvent = event as KeyboardEvent
val modifierPressed = if (isMacPlatform) {
keyboardEvent.metaKey && !keyboardEvent.ctrlKey
} else {
keyboardEvent.ctrlKey && !keyboardEvent.metaKey
}
val isFindShortcut = modifierPressed && (keyboardEvent.key == "f" || keyboardEvent.key == "F")
if (isFindShortcut) {
event.preventDefault()
currentTrigger()
}
}
document.addEventListener("keydown", listener)
onDispose { document.removeEventListener("keydown", listener) }
}
return this
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
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.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.PreviewLightDark
Expand All @@ -48,12 +54,26 @@ fun MainHeaderSearchBar(
onClose: () -> Unit,
modifier: Modifier = Modifier,
hasAdditionalInputs: Boolean = false,
searchBarFocusRequester: FocusRequester? = null,
) {

fun KeyEvent.isEscPressed(): Boolean =
this.type == KeyEventType.KeyDown && this.key == Key.Escape

Row(
modifier = modifier
.height(48.dp)
.fillMaxWidth()
.background(KotlinConfTheme.colors.mainBackground),
.background(KotlinConfTheme.colors.mainBackground)
.onPreviewKeyEvent { event ->
if (event.isEscPressed()) {
onClose()
onSearchValueChange("")
true
} else {
false
}
},
verticalAlignment = Alignment.CenterVertically,
) {
TopMenuButton(
Expand All @@ -66,7 +86,7 @@ fun MainHeaderSearchBar(
)

var focusRequested by rememberSaveable { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
val focusRequester = searchBarFocusRequester ?: remember { FocusRequester() }
if (!focusRequested) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
Expand Down
Loading