diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt index 5d4aa0d6..51068118 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -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). diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarChrome.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarChrome.kt index 95e9ab33..ef620f4b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarChrome.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarChrome.kt @@ -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 diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventSelectionBar.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventSelectionBar.kt index 14e66c46..3e96f28b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventSelectionBar.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventSelectionBar.kt @@ -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, ) { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventSelectionHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventSelectionHost.kt index 60b82fe0..0c9d27f4 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventSelectionHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventSelectionHost.kt @@ -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(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(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, ) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourGutter.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourGutter.kt index 80b5fa61..afa19499 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourGutter.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/HourGutter.kt @@ -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) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt index c280ed27..643925af 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt @@ -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 { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt index 8b1a4a24..5d8a1e71 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt @@ -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, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1be5a65..27b0d1ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,6 +100,7 @@ Changed to %1$s Change recurring event Event actions + Hide event actions Couldn\'t open the event Undo Move undone