@@ -21,6 +21,7 @@ package org.schabi.newpipe.ui.components.menu
2121import androidx.annotation.StringRes
2222import androidx.compose.foundation.BorderStroke
2323import androidx.compose.foundation.border
24+ import androidx.compose.foundation.layout.BoxWithConstraints
2425import androidx.compose.foundation.layout.Column
2526import androidx.compose.foundation.layout.fillMaxWidth
2627import androidx.compose.foundation.layout.offset
@@ -54,9 +55,15 @@ import androidx.compose.runtime.setValue
5455import androidx.compose.runtime.toMutableStateList
5556import androidx.compose.ui.Alignment
5657import androidx.compose.ui.Modifier
58+ import androidx.compose.ui.focus.focusTarget
5759import androidx.compose.ui.geometry.Offset
5860import androidx.compose.ui.graphics.Color
5961import androidx.compose.ui.graphics.vector.ImageVector
62+ import androidx.compose.ui.input.key.Key
63+ import androidx.compose.ui.input.key.KeyEventType
64+ import androidx.compose.ui.input.key.key
65+ import androidx.compose.ui.input.key.onKeyEvent
66+ import androidx.compose.ui.input.key.type
6067import androidx.compose.ui.platform.LocalDensity
6168import androidx.compose.ui.res.stringResource
6269import androidx.compose.ui.text.font.FontStyle
@@ -69,18 +76,19 @@ import androidx.compose.ui.unit.IntSize
6976import androidx.compose.ui.unit.dp
7077import androidx.compose.ui.unit.toSize
7178import org.schabi.newpipe.R
79+ import org.schabi.newpipe.ui.components.menu.LongPressAction.Type.Companion.DefaultEnabledActions
7280import org.schabi.newpipe.ui.detectDragGestures
7381import org.schabi.newpipe.ui.theme.AppTheme
7482import org.schabi.newpipe.util.text.FixedHeightCenteredText
83+ import kotlin.math.floor
7584import kotlin.math.min
7685
77- // TODO implement accessibility for this, to allow using this with a DPAD (e.g. Android TV)
7886@Composable
79- fun LongPressMenuEditor () {
87+ fun LongPressMenuEditor (modifier : Modifier = Modifier ) {
8088 // We get the current arrangement once and do not observe on purpose
8189 // TODO load from settings
82- val headerEnabled = remember { false } // true }
83- val actionArrangement = remember { LongPressAction . Type .entries } // DefaultEnabledActions }
90+ val headerEnabled = remember { true }
91+ val actionArrangement = remember { DefaultEnabledActions }
8492 val items = remember(headerEnabled, actionArrangement) {
8593 sequence {
8694 yield (ItemInList .EnabledCaption )
@@ -127,8 +135,8 @@ fun LongPressMenuEditor() {
127135 return closestItemInRow
128136 }
129137
130- fun beginDragGesture (pos : IntOffset ) {
131- val rawItem = findItemForOffsetOrClosestInRow(pos) ? : return
138+ fun beginDragGesture (pos : IntOffset , rawItem : LazyGridItemInfo ) {
139+ if (activeDragItem != null ) return
132140 val item = items.getOrNull(rawItem.index) ? : return
133141 if (item.isDraggable) {
134142 items[rawItem.index] = ItemInList .DragMarker (item.columnSpan)
@@ -138,34 +146,41 @@ fun LongPressMenuEditor() {
138146 }
139147 }
140148
141- fun handleDragGestureChange (pos : IntOffset , posChange : Offset ) {
142- val dragItem = activeDragItem
143- if (dragItem == null ) {
144- // when the user clicks outside of any draggable item, let the list be scrolled
145- gridState.dispatchRawDelta(- posChange.y)
146- return
147- }
148- activeDragPosition = pos
149+ fun beginDragGesture (pos : IntOffset ) {
149150 val rawItem = findItemForOffsetOrClosestInRow(pos) ? : return
151+ beginDragGesture(pos, rawItem)
152+ }
153+
154+ fun handleDragGestureChange (dragItem : ItemInList , rawItem : LazyGridItemInfo ) {
155+ val prevDragMarkerIndex = items.indexOfFirst { it is ItemInList .DragMarker }
156+ .takeIf { it >= 0 } ? : return // impossible situation, DragMarker is always in the list
150157
151158 // compute where the DragMarker will go (we need to do special logic to make sure the
152159 // HeaderBox always sticks right after EnabledCaption or HiddenCaption)
153160 val nextDragMarkerIndex = if (dragItem == ItemInList .HeaderBox ) {
154161 val hiddenCaptionIndex = items.indexOf(ItemInList .HiddenCaption )
155162 if (rawItem.index < hiddenCaptionIndex)
156163 1 // i.e. right after the EnabledCaption
164+ else if (prevDragMarkerIndex < hiddenCaptionIndex)
165+ hiddenCaptionIndex // i.e. right after the HiddenCaption
157166 else
158167 hiddenCaptionIndex + 1 // i.e. right after the HiddenCaption
159168 } else {
160169 var i = rawItem.index
161170 // make sure it is not possible to move items in between a *Caption and a HeaderBox
162- if (! items[i].isDraggable) i + = 1
163- if (items[i] == ItemInList .HeaderBox ) i + = 1
171+ val offsetForRemovingPrev = if (prevDragMarkerIndex < rawItem.index) 1 else 0
172+ if (! items[i - offsetForRemovingPrev].isDraggable) i + = 1
173+ if (items[i - offsetForRemovingPrev] == ItemInList .HeaderBox ) i + = 1
164174 i
165175 }
166176
177+ // no need to do anything if the DragMarker is already at the right place
178+ if (prevDragMarkerIndex == nextDragMarkerIndex) {
179+ return
180+ }
181+
167182 // adjust the position of the DragMarker
168- items.removeIf { it is ItemInList . DragMarker }
183+ items.removeAt(prevDragMarkerIndex)
169184 items.add(min(nextDragMarkerIndex, items.size), ItemInList .DragMarker (dragItem.columnSpan))
170185
171186 // add or remove NoneMarkers as needed
@@ -178,6 +193,18 @@ fun LongPressMenuEditor() {
178193 }
179194 }
180195
196+ fun handleDragGestureChange (pos : IntOffset , posChangeForScrolling : Offset ) {
197+ val dragItem = activeDragItem
198+ if (dragItem == null ) {
199+ // when the user clicks outside of any draggable item, let the list be scrolled
200+ gridState.dispatchRawDelta(- posChangeForScrolling.y)
201+ return
202+ }
203+ activeDragPosition = pos
204+ val rawItem = findItemForOffsetOrClosestInRow(pos) ? : return
205+ handleDragGestureChange(dragItem, rawItem)
206+ }
207+
181208 fun completeDragGestureAndCleanUp () {
182209 val dragItem = activeDragItem
183210 if (dragItem != null ) {
@@ -198,44 +225,153 @@ fun LongPressMenuEditor() {
198225 }
199226 }
200227
201- LazyVerticalGrid (
202- modifier = Modifier
203- .safeDrawingPadding()
204- .detectDragGestures(
205- beginDragGesture = ::beginDragGesture,
206- handleDragGestureChange = ::handleDragGestureChange,
207- endDragGesture = ::completeDragGestureAndCleanUp,
208- ),
209- // same width as the LongPressMenu
210- columns = GridCells .Adaptive (MinButtonWidth ),
211- userScrollEnabled = false ,
212- state = gridState,
213- ) {
214- itemsIndexed(
215- items,
216- key = { _, item -> item.stableUniqueKey() },
217- span = { _, item -> GridItemSpan (item.columnSpan ? : maxLineSpan) },
218- ) { i, item ->
228+ BoxWithConstraints (modifier) {
229+ // otherwise we wouldn't know the amount of columns to handle the Up/Down key events
230+ val columns = maxOf(1 , floor(this .maxWidth / MinButtonWidth ).toInt())
231+ LazyVerticalGrid (
232+ modifier = Modifier
233+ .safeDrawingPadding()
234+ .detectDragGestures(
235+ beginDragGesture = ::beginDragGesture,
236+ handleDragGestureChange = ::handleDragGestureChange,
237+ endDragGesture = ::completeDragGestureAndCleanUp,
238+ )
239+ .focusTarget()
240+ .onKeyEvent { event ->
241+ if (event.type != KeyEventType .KeyDown ) {
242+ if (event.type == KeyEventType .KeyUp &&
243+ event.key == Key .DirectionDown &&
244+ currentlyFocusedItem < 0
245+ ) {
246+ //
247+ currentlyFocusedItem = 0
248+ }
249+ return @onKeyEvent false
250+ }
251+ var focusedItem = currentlyFocusedItem
252+ when (event.key) {
253+ Key .DirectionUp -> {
254+ if (focusedItem < 0 ) {
255+ return @onKeyEvent false
256+ } else if (items[focusedItem].columnSpan == null ) {
257+ focusedItem - = 1
258+ } else {
259+ var remaining = columns
260+ while (true ) {
261+ focusedItem - = 1
262+ if (focusedItem < 0 ) {
263+ break
264+ }
265+ remaining - = items[focusedItem].columnSpan ? : columns
266+ if (remaining <= 0 ) {
267+ break
268+ }
269+ }
270+ }
271+ }
272+
273+ Key .DirectionDown -> {
274+ if (focusedItem >= items.size - 1 ) {
275+ return @onKeyEvent false
276+ } else if (items[focusedItem].columnSpan == null ) {
277+ focusedItem + = 1
278+ } else {
279+ var remaining = columns
280+ while (true ) {
281+ focusedItem + = 1
282+ if (focusedItem >= items.size - 1 ) {
283+ break
284+ }
285+ remaining - = items[focusedItem].columnSpan ? : columns
286+ if (remaining <= 0 ) {
287+ break
288+ }
289+ }
290+ }
291+ }
292+
293+ Key .DirectionLeft -> {
294+ if (focusedItem < 0 ) {
295+ return @onKeyEvent false
296+ } else {
297+ focusedItem - = 1
298+ }
299+ }
300+
301+ Key .DirectionRight -> {
302+ if (focusedItem >= items.size - 1 ) {
303+ return @onKeyEvent false
304+ } else {
305+ focusedItem + = 1
306+ }
307+ }
308+
309+ Key .Enter , Key .NumPadEnter , Key .DirectionCenter -> if (activeDragItem == null ) {
310+ val rawItem = gridState.layoutInfo.visibleItemsInfo
311+ .firstOrNull { it.index == focusedItem }
312+ ? : return @onKeyEvent false
313+ beginDragGesture(rawItem.offset, rawItem)
314+ return @onKeyEvent true
315+ } else {
316+ completeDragGestureAndCleanUp()
317+ return @onKeyEvent true
318+ }
319+
320+ else -> return @onKeyEvent false
321+ }
322+
323+ currentlyFocusedItem = focusedItem
324+ if (focusedItem < 0 ) {
325+ // not checking for focusedItem>=items.size because it's impossible for it
326+ // to reach that value, and that's because we assume that there is nothing
327+ // else focusable *after* this view. This way we don't need to cleanup the
328+ // drag gestures when the user reaches the end, which would be confusing as
329+ // then there would be no indication of the current cursor position at all.
330+ completeDragGestureAndCleanUp()
331+ return @onKeyEvent false
332+ }
333+
334+ val dragItem = activeDragItem
335+ if (dragItem != null ) {
336+ val rawItem = gridState.layoutInfo.visibleItemsInfo
337+ .firstOrNull { it.index == focusedItem }
338+ ? : return @onKeyEvent false
339+ activeDragPosition = rawItem.offset
340+ handleDragGestureChange(dragItem, rawItem)
341+ }
342+ return @onKeyEvent true
343+ },
344+ // same width as the LongPressMenu
345+ columns = GridCells .Adaptive (MinButtonWidth ),
346+ userScrollEnabled = false ,
347+ state = gridState,
348+ ) {
349+ itemsIndexed(
350+ items,
351+ key = { _, item -> item.stableUniqueKey() },
352+ span = { _, item -> GridItemSpan (item.columnSpan ? : maxLineSpan) },
353+ ) { i, item ->
354+ ItemInListUi (
355+ item = item,
356+ selected = currentlyFocusedItem == i,
357+ modifier = Modifier .animateItem()
358+ )
359+ }
360+ }
361+ if (activeDragItem != null ) {
362+ val size = with (LocalDensity .current) {
363+ remember(activeDragSize) { activeDragSize.toSize().toDpSize() }
364+ }
219365 ItemInListUi (
220- item = item,
221- selected = currentlyFocusedItem == i,
222- modifier = Modifier .animateItem()
366+ item = activeDragItem!! ,
367+ selected = false ,
368+ modifier = Modifier
369+ .size(size)
370+ .offset { activeDragPosition }
371+ .offset(- size.width / 2 , - size.height / 2 ),
223372 )
224373 }
225374 }
226- if (activeDragItem != null ) {
227- val size = with (LocalDensity .current) {
228- remember(activeDragSize) { activeDragSize.toSize().toDpSize() }
229- }
230- ItemInListUi (
231- item = activeDragItem!! ,
232- selected = true ,
233- modifier = Modifier
234- .size(size)
235- .offset { activeDragPosition }
236- .offset(- size.width / 2 , - size.height / 2 ),
237- )
238- }
239375}
240376
241377sealed class ItemInList (val isDraggable : Boolean , open val columnSpan : Int? = 1 ) {
@@ -305,7 +441,7 @@ private fun ActionOrHeaderBox(
305441 color = backgroundColor,
306442 contentColor = contentColor,
307443 shape = MaterialTheme .shapes.large,
308- border = BorderStroke (2 .dp, contentColor).takeIf { selected },
444+ border = BorderStroke (2 .dp, contentColor.copy(alpha = 1f ) ).takeIf { selected },
309445 modifier = modifier.padding(
310446 horizontal = horizontalPadding,
311447 vertical = 5 .dp,
0 commit comments