Fold the selection bar into its own button and drop the extra scroll room (#141)
This commit is contained in:
@@ -588,7 +588,7 @@ fun CalendarHost(
|
|||||||
EventMoveHost(
|
EventMoveHost(
|
||||||
reschedule,
|
reschedule,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
raised = chromeHost.selectionBarShown && !chromeHost.selectionCompact,
|
raised = chromeHost.selectionBarShown && chromeHost.selectionBarExpanded,
|
||||||
)
|
)
|
||||||
|
|
||||||
// The bar a long-pressed event brings up in place of the FAB (#141).
|
// The bar a long-pressed event brings up in place of the FAB (#141).
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ class ChromeHost {
|
|||||||
var selectionCompact by mutableStateOf(false)
|
var selectionCompact by mutableStateOf(false)
|
||||||
internal set
|
internal set
|
||||||
|
|
||||||
|
/** Whether the selection's bar is unfolded, rather than folded into its button. */
|
||||||
|
var selectionBarExpanded by mutableStateOf(false)
|
||||||
|
internal set
|
||||||
|
|
||||||
/** Whether the selection's action bar is showing — it steps aside mid-gesture. */
|
/** Whether the selection's action bar is showing — it steps aside mid-gesture. */
|
||||||
val selectionBarShown: Boolean get() = selection != null && !selectionBusy
|
val selectionBarShown: Boolean get() = selection != null && !selectionBusy
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.MoreHoriz
|
||||||
import androidx.compose.material.icons.filled.Share
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.FloatingToolbarDefaults
|
import androidx.compose.material3.FloatingToolbarDefaults
|
||||||
|
import androidx.compose.material3.FloatingToolbarHorizontalFabPosition
|
||||||
import androidx.compose.material3.HorizontalFloatingToolbar
|
import androidx.compose.material3.HorizontalFloatingToolbar
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
@@ -17,13 +20,16 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The action bar a selected event brings up in place of the FAB (#141). Edit and
|
* The action bar a selected event brings up in the FAB's corner (#141): a
|
||||||
* Delete are offered only where the event's calendar allows them; a read-only
|
* toolbar that folds into its own button and back out of it. Edit and Delete are
|
||||||
* event can still be duplicated into a writable calendar, or shared.
|
* offered only where the event's calendar allows them; a read-only event can
|
||||||
|
* still be duplicated into a writable calendar, or shared.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun EventSelectionBar(
|
fun EventSelectionBar(
|
||||||
|
expanded: Boolean,
|
||||||
|
onToggle: () -> Unit,
|
||||||
canEdit: Boolean,
|
canEdit: Boolean,
|
||||||
canDelete: Boolean,
|
canDelete: Boolean,
|
||||||
onEdit: () -> Unit,
|
onEdit: () -> Unit,
|
||||||
@@ -33,7 +39,22 @@ fun EventSelectionBar(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
HorizontalFloatingToolbar(
|
HorizontalFloatingToolbar(
|
||||||
expanded = true,
|
expanded = expanded,
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingToolbarDefaults.VibrantFloatingActionButton(onClick = onToggle) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (expanded) {
|
||||||
|
Icons.AutoMirrored.Filled.KeyboardArrowRight
|
||||||
|
} else {
|
||||||
|
Icons.Default.MoreHoriz
|
||||||
|
},
|
||||||
|
contentDescription = stringResource(
|
||||||
|
if (expanded) R.string.event_selection_hide else R.string.event_selection_actions,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
floatingActionButtonPosition = FloatingToolbarHorizontalFabPosition.End,
|
||||||
colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors(),
|
colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors(),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -11,13 +11,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.MoreHoriz
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -25,7 +22,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -66,28 +66,40 @@ fun EventSelectionHost(
|
|||||||
// Held past the selection clearing, so the bar has something to draw on its way out.
|
// Held past the selection clearing, so the bar has something to draw on its way out.
|
||||||
val held = remember { mutableStateOf<EventSelection?>(null) }
|
val held = remember { mutableStateOf<EventSelection?>(null) }
|
||||||
host.selection?.let { held.value = it }
|
host.selection?.let { held.value = it }
|
||||||
// Over the selected event's bottom handle the bar shrinks to one button in
|
// Folded into its button while it would cover the selected event's bottom
|
||||||
// the FAB's corner; tapping it brings the whole bar back for this selection.
|
// handle; the button opens and folds it either way, for this selection.
|
||||||
var opened by remember(host.selection?.event, host.selectionCompact) { mutableStateOf(false) }
|
var chosen by remember(host.selection?.event) { mutableStateOf<Boolean?>(null) }
|
||||||
val compact = host.selectionCompact && !opened
|
val expanded = chosen ?: !host.selectionCompact
|
||||||
|
SideEffect { host.selectionBarExpanded = expanded }
|
||||||
|
|
||||||
|
val cornerOrigin = if (LocalLayoutDirection.current == LayoutDirection.Rtl) {
|
||||||
|
TransformOrigin(0f, 1f)
|
||||||
|
} else {
|
||||||
|
TransformOrigin(1f, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
// Out of the way while the event is dragged or resized, so the
|
// Out of the way while the event is dragged or resized, so the
|
||||||
// bottom of the timeline is there to drop on.
|
// bottom of the timeline is there to drop on.
|
||||||
visible = host.selectionBarShown && !compact,
|
visible = host.selectionBarShown,
|
||||||
enter = scaleIn(MaterialTheme.motionScheme.fastSpatialSpec()) + fadeIn(),
|
// From the FAB's corner it stands in for.
|
||||||
exit = scaleOut(MaterialTheme.motionScheme.fastSpatialSpec()) + fadeOut(),
|
enter = scaleIn(MaterialTheme.motionScheme.fastSpatialSpec(), transformOrigin = cornerOrigin) +
|
||||||
|
fadeIn(),
|
||||||
|
exit = scaleOut(MaterialTheme.motionScheme.fastSpatialSpec(), transformOrigin = cornerOrigin) +
|
||||||
|
fadeOut(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomEnd)
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.padding(bottom = SnackChipMargin),
|
.padding(end = SnackChipMargin, bottom = SnackChipMargin),
|
||||||
) {
|
) {
|
||||||
val selection = held.value ?: return@AnimatedVisibility
|
val selection = held.value ?: return@AnimatedVisibility
|
||||||
val event = selection.event
|
val event = selection.event
|
||||||
val calendar = calendars[event.calendarId]
|
val calendar = calendars[event.calendarId]
|
||||||
val canEdit = calendar?.canModifyContents == true
|
val canEdit = calendar?.canModifyContents == true
|
||||||
EventSelectionBar(
|
EventSelectionBar(
|
||||||
|
expanded = expanded,
|
||||||
|
onToggle = { chosen = !expanded },
|
||||||
canEdit = canEdit,
|
canEdit = canEdit,
|
||||||
canDelete = canEdit && !calendar.isManaged,
|
canDelete = canEdit && !calendar.isManaged,
|
||||||
onEdit = {
|
onEdit = {
|
||||||
@@ -115,26 +127,6 @@ fun EventSelectionHost(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = host.selectionBarShown && compact,
|
|
||||||
enter = scaleIn(MaterialTheme.motionScheme.fastSpatialSpec()) + fadeIn(),
|
|
||||||
exit = scaleOut(MaterialTheme.motionScheme.fastSpatialSpec()) + fadeOut(),
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomEnd)
|
|
||||||
.navigationBarsPadding()
|
|
||||||
.padding(end = SnackChipMargin, bottom = SnackChipMargin),
|
|
||||||
) {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = { opened = true },
|
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.MoreHoriz,
|
|
||||||
contentDescription = stringResource(R.string.event_selection_actions),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val message = when (failure) {
|
val message = when (failure) {
|
||||||
null -> null
|
null -> null
|
||||||
EventActionFailure.LoadFailed -> stringResource(R.string.event_action_load_failed)
|
EventActionFailure.LoadFailed -> stringResource(R.string.event_action_load_failed)
|
||||||
@@ -158,7 +150,7 @@ fun EventSelectionHost(
|
|||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.padding(
|
.padding(
|
||||||
start = SnackChipMargin,
|
start = SnackChipMargin,
|
||||||
bottom = SnackChipMargin + if (host.selectionBarShown && !compact) SelectionBarClearance else 0.dp,
|
bottom = SnackChipMargin + if (host.selectionBarShown && expanded) SelectionBarClearance else 0.dp,
|
||||||
)
|
)
|
||||||
.height(SnackChipHeight),
|
.height(SnackChipHeight),
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
@@ -166,8 +158,8 @@ fun EventSelectionHost(
|
|||||||
SnackChip(
|
SnackChip(
|
||||||
visible = failure != null,
|
visible = failure != null,
|
||||||
message = shownMessage.value,
|
message = shownMessage.value,
|
||||||
// Clear of the compact button in the FAB's corner.
|
// Clear of the folded bar's button in the FAB's corner.
|
||||||
maxWidth = chipMaxWidth - if (compact) SelectionBarClearance else 0.dp,
|
maxWidth = chipMaxWidth - if (host.selectionBarShown) SelectionBarClearance else 0.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import androidx.compose.animation.core.animateFloatAsState
|
|||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -107,7 +106,6 @@ fun HourGutter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(dragController.bottomRoom()))
|
|
||||||
}
|
}
|
||||||
dragStartMin?.let { startMin ->
|
dragStartMin?.let { startMin ->
|
||||||
val top = (hourHeight * (startMin / 60f) - BADGE_HEIGHT / 2).coerceAtLeast(0.dp)
|
val top = (hourHeight * (startMin / 60f) - BADGE_HEIGHT / 2).coerceAtLeast(0.dp)
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import androidx.compose.ui.layout.positionInRoot
|
|||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
@@ -1191,13 +1190,6 @@ private fun DragCopy(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extra scroll room under the hours while an event is selected, so the last of
|
|
||||||
* the day can scroll clear of the action bar (#141). The columns and the gutter
|
|
||||||
* share one scroll state, so both have to add exactly this.
|
|
||||||
*/
|
|
||||||
fun TimelineDragController.bottomRoom(): Dp = if (selection != null) SelectionBarClearance else 0.dp
|
|
||||||
|
|
||||||
/** How far a selected block is lifted, animated between 0 and 1 (#141). */
|
/** How far a selected block is lifted, animated between 0 and 1 (#141). */
|
||||||
@Composable
|
@Composable
|
||||||
fun selectionLift(selected: Boolean): Float {
|
fun selectionLift(selected: Boolean): Float {
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ import androidx.compose.ui.zIndex
|
|||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import de.jeanlucmakiola.calendula.ui.common.PickUp
|
import de.jeanlucmakiola.calendula.ui.common.PickUp
|
||||||
import de.jeanlucmakiola.calendula.ui.common.SelectionBarClearance
|
import de.jeanlucmakiola.calendula.ui.common.SelectionBarClearance
|
||||||
import de.jeanlucmakiola.calendula.ui.common.bottomRoom
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ResizeEdge
|
import de.jeanlucmakiola.calendula.ui.common.ResizeEdge
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ResizeHandle
|
import de.jeanlucmakiola.calendula.ui.common.ResizeHandle
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ResizeHandleTarget
|
import de.jeanlucmakiola.calendula.ui.common.ResizeHandleTarget
|
||||||
@@ -1279,9 +1278,6 @@ private fun Timeline(
|
|||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
.onGloballyPositioned { if (active) dragController.geometry.viewport = it },
|
.onGloballyPositioned { if (active) dragController.geometry.viewport = it },
|
||||||
) {
|
) {
|
||||||
// Room past midnight to scroll the day's end clear of the action
|
|
||||||
// bar, matched by the gutter on the same scroll state (#141).
|
|
||||||
Spacer(Modifier.height(totalHeight + dragController.bottomRoom()))
|
|
||||||
TimelineColumns(
|
TimelineColumns(
|
||||||
geometry = geometry,
|
geometry = geometry,
|
||||||
gap = COLUMN_GAP,
|
gap = COLUMN_GAP,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
<string name="event_resize_done">Changed to %1$s</string>
|
<string name="event_resize_done">Changed to %1$s</string>
|
||||||
<string name="event_resize_recurring_title">Change recurring event</string>
|
<string name="event_resize_recurring_title">Change recurring event</string>
|
||||||
<string name="event_selection_actions">Event actions</string>
|
<string name="event_selection_actions">Event actions</string>
|
||||||
|
<string name="event_selection_hide">Hide event actions</string>
|
||||||
<string name="event_action_load_failed">Couldn\'t open the event</string>
|
<string name="event_action_load_failed">Couldn\'t open the event</string>
|
||||||
<string name="event_move_undo">Undo</string>
|
<string name="event_move_undo">Undo</string>
|
||||||
<string name="event_move_undone">Move undone</string>
|
<string name="event_move_undone">Move undone</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user