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(
|
||||
reschedule,
|
||||
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).
|
||||
|
||||
@@ -86,6 +86,10 @@ class ChromeHost {
|
||||
var selectionCompact by mutableStateOf(false)
|
||||
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. */
|
||||
val selectionBarShown: Boolean get() = selection != null && !selectionBusy
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package de.jeanlucmakiola.calendula.ui.common
|
||||
|
||||
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.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.MoreHoriz
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FloatingToolbarDefaults
|
||||
import androidx.compose.material3.FloatingToolbarHorizontalFabPosition
|
||||
import androidx.compose.material3.HorizontalFloatingToolbar
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -17,13 +20,16 @@ import androidx.compose.ui.res.stringResource
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
|
||||
/**
|
||||
* The action bar a selected event brings up in place of the FAB (#141). Edit and
|
||||
* Delete are offered only where the event's calendar allows them; a read-only
|
||||
* event can still be duplicated into a writable calendar, or shared.
|
||||
* The action bar a selected event brings up in the FAB's corner (#141): a
|
||||
* toolbar that folds into its own button and back out of it. Edit and Delete are
|
||||
* 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)
|
||||
@Composable
|
||||
fun EventSelectionBar(
|
||||
expanded: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
canEdit: Boolean,
|
||||
canDelete: Boolean,
|
||||
onEdit: () -> Unit,
|
||||
@@ -33,7 +39,22 @@ fun EventSelectionBar(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
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(),
|
||||
modifier = modifier,
|
||||
) {
|
||||
|
||||
@@ -11,13 +11,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -25,7 +22,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
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.unit.dp
|
||||
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.
|
||||
val held = remember { mutableStateOf<EventSelection?>(null) }
|
||||
host.selection?.let { held.value = it }
|
||||
// Over the selected event's bottom handle the bar shrinks to one button in
|
||||
// the FAB's corner; tapping it brings the whole bar back for this selection.
|
||||
var opened by remember(host.selection?.event, host.selectionCompact) { mutableStateOf(false) }
|
||||
val compact = host.selectionCompact && !opened
|
||||
// Folded into its button while it would cover the selected event's bottom
|
||||
// handle; the button opens and folds it either way, for this selection.
|
||||
var chosen by remember(host.selection?.event) { mutableStateOf<Boolean?>(null) }
|
||||
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()) {
|
||||
AnimatedVisibility(
|
||||
// Out of the way while the event is dragged or resized, so the
|
||||
// bottom of the timeline is there to drop on.
|
||||
visible = host.selectionBarShown && !compact,
|
||||
enter = scaleIn(MaterialTheme.motionScheme.fastSpatialSpec()) + fadeIn(),
|
||||
exit = scaleOut(MaterialTheme.motionScheme.fastSpatialSpec()) + fadeOut(),
|
||||
visible = host.selectionBarShown,
|
||||
// From the FAB's corner it stands in for.
|
||||
enter = scaleIn(MaterialTheme.motionScheme.fastSpatialSpec(), transformOrigin = cornerOrigin) +
|
||||
fadeIn(),
|
||||
exit = scaleOut(MaterialTheme.motionScheme.fastSpatialSpec(), transformOrigin = cornerOrigin) +
|
||||
fadeOut(),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.align(Alignment.BottomEnd)
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = SnackChipMargin),
|
||||
.padding(end = SnackChipMargin, bottom = SnackChipMargin),
|
||||
) {
|
||||
val selection = held.value ?: return@AnimatedVisibility
|
||||
val event = selection.event
|
||||
val calendar = calendars[event.calendarId]
|
||||
val canEdit = calendar?.canModifyContents == true
|
||||
EventSelectionBar(
|
||||
expanded = expanded,
|
||||
onToggle = { chosen = !expanded },
|
||||
canEdit = canEdit,
|
||||
canDelete = canEdit && !calendar.isManaged,
|
||||
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) {
|
||||
null -> null
|
||||
EventActionFailure.LoadFailed -> stringResource(R.string.event_action_load_failed)
|
||||
@@ -158,7 +150,7 @@ fun EventSelectionHost(
|
||||
.navigationBarsPadding()
|
||||
.padding(
|
||||
start = SnackChipMargin,
|
||||
bottom = SnackChipMargin + if (host.selectionBarShown && !compact) SelectionBarClearance else 0.dp,
|
||||
bottom = SnackChipMargin + if (host.selectionBarShown && expanded) SelectionBarClearance else 0.dp,
|
||||
)
|
||||
.height(SnackChipHeight),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
@@ -166,8 +158,8 @@ fun EventSelectionHost(
|
||||
SnackChip(
|
||||
visible = failure != null,
|
||||
message = shownMessage.value,
|
||||
// Clear of the compact button in the FAB's corner.
|
||||
maxWidth = chipMaxWidth - if (compact) SelectionBarClearance else 0.dp,
|
||||
// Clear of the folded bar's button in the FAB's corner.
|
||||
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.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -107,7 +106,6 @@ fun HourGutter(
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(dragController.bottomRoom()))
|
||||
}
|
||||
dragStartMin?.let { startMin ->
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
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). */
|
||||
@Composable
|
||||
fun selectionLift(selected: Boolean): Float {
|
||||
|
||||
@@ -66,7 +66,6 @@ import androidx.compose.ui.zIndex
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import de.jeanlucmakiola.calendula.ui.common.PickUp
|
||||
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.ResizeHandle
|
||||
import de.jeanlucmakiola.calendula.ui.common.ResizeHandleTarget
|
||||
@@ -1279,9 +1278,6 @@ private fun Timeline(
|
||||
.verticalScroll(scrollState)
|
||||
.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(
|
||||
geometry = geometry,
|
||||
gap = COLUMN_GAP,
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
<string name="event_resize_done">Changed to %1$s</string>
|
||||
<string name="event_resize_recurring_title">Change recurring event</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_move_undo">Undo</string>
|
||||
<string name="event_move_undone">Move undone</string>
|
||||
|
||||
Reference in New Issue
Block a user