diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt index 776e200..b7a18ba 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt @@ -135,6 +135,7 @@ fun AgendaScreen( AgendaTopBar( selectedView = selectedView, onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, + quickSwitchViews = quickSwitchViews, onOpenDrawer = { scope.launch { drawerState.open() } }, onOpenSearch = onOpenSearch, showTodayButton = todayInToolbar, @@ -409,6 +410,7 @@ private fun AgendaEmpty(modifier: Modifier = Modifier) { private fun AgendaTopBar( selectedView: CalendarView, onCycleView: () -> Unit, + quickSwitchViews: List, onOpenDrawer: () -> Unit, onOpenSearch: () -> Unit, showTodayButton: Boolean, @@ -440,6 +442,7 @@ private fun AgendaTopBar( } ViewSwitcherPill( current = selectedView, + cycle = quickSwitchViews, onCycle = onCycleView, modifier = Modifier.padding(end = 8.dp), ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt index b701d95..18bb606 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt @@ -57,7 +57,8 @@ fun CalendarView.next(available: List = IMPLEMENTED_VIEWS): Calend * implemented view — the settings screen reorders the whole set — while [cycle] * is the subset the pill actually steps through, in [order]. The navigation * drawer keeps its own separate order and always lists every view, so a view - * disabled here stays reachable there. + * disabled here stays reachable there — including when [cycle] is emptied and + * the pill disappears altogether. */ data class QuickSwitchConfig( val order: List, @@ -67,14 +68,15 @@ data class QuickSwitchConfig( val cycle: List get() = order.filter { it in enabled } companion object { + /** + * Fewest views that keep the switch meaningful. A single target is not a + * switch, so below this the pill is hidden rather than special-cased (#150); + * the drawer still reaches every view. + */ + const val MIN_CYCLE = 2 + /** All views, in default order, all enabled. */ val Default = QuickSwitchConfig(IMPLEMENTED_VIEWS, IMPLEMENTED_VIEWS.toSet()) - - /** - * Fewest views that keep the switch meaningful — a "switch" needs at - * least two targets, so the settings screen blocks disabling below this. - */ - const val MIN_ENABLED = 2 } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ViewSwitcherPill.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ViewSwitcherPill.kt index 18a1f3d..825e549 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ViewSwitcherPill.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ViewSwitcherPill.kt @@ -10,13 +10,19 @@ import androidx.compose.ui.res.stringResource /** * Top-bar pill that shows the current view and cycles to the next one on tap * (spec M1: Month → Week → Day → Month, restricted to [IMPLEMENTED_VIEWS]). + * + * Renders nothing when [cycle] holds fewer than [QuickSwitchConfig.MIN_CYCLE] + * views (#150), so it drops straight into an app bar's `actions` slot without a + * wrapping condition — the same way [TodayAction] handles being turned off. */ @Composable fun ViewSwitcherPill( current: CalendarView, + cycle: List, onCycle: () -> Unit, modifier: Modifier = Modifier, ) { + if (cycle.size < QuickSwitchConfig.MIN_CYCLE) return FilledTonalButton( onClick = onCycle, shape = MaterialTheme.shapes.large, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt index 6b6d4fe..768afbb 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt @@ -243,6 +243,7 @@ fun DayScreen( currentYear = currentYear, selectedView = selectedView, onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, + quickSwitchViews = quickSwitchViews, onOpenDrawer = { scope.launch { drawerState.open() } }, onOpenSearch = onOpenSearch, onJumpToDate = jumpToDate, @@ -411,6 +412,7 @@ private fun DayTopBar( currentYear: Int, selectedView: CalendarView, onCycleView: () -> Unit, + quickSwitchViews: List, onOpenDrawer: () -> Unit, onOpenSearch: () -> Unit, onJumpToDate: (LocalDate) -> Unit, @@ -445,6 +447,7 @@ private fun DayTopBar( } ViewSwitcherPill( current = selectedView, + cycle = quickSwitchViews, onCycle = onCycleView, modifier = Modifier.padding(end = 8.dp), ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index d207c51..c7fc85d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -386,6 +386,7 @@ fun MonthScreen( titleDate = LocalDate(titleMonth.year, titleMonth.month, 1), selectedView = selectedView, onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, + quickSwitchViews = quickSwitchViews, onOpenDrawer = { scope.launch { drawerState.open() } }, onOpenSearch = onOpenSearch, onJumpToDate = jumpToDate, @@ -650,6 +651,7 @@ private fun MonthTopBar( titleDate: LocalDate, selectedView: CalendarView, onCycleView: () -> Unit, + quickSwitchViews: List, onOpenDrawer: () -> Unit, onOpenSearch: () -> Unit, onJumpToDate: (LocalDate) -> Unit, @@ -683,6 +685,7 @@ private fun MonthTopBar( } ViewSwitcherPill( current = selectedView, + cycle = quickSwitchViews, onCycle = onCycleView, modifier = Modifier.padding(end = 8.dp), ) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index d2b37fe..edfbd4e 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -591,15 +591,15 @@ class SettingsViewModel @Inject constructor( /** * Enable or disable [view] in the quick-switch cycle, via an atomic - * read-modify-write so a concurrent reorder can't clobber it. The - * MIN_ENABLED floor is re-checked inside the transform, because the screen's - * own guard reads an async-echoed snapshot. + * read-modify-write so a concurrent reorder can't clobber it. Any number of + * views may be turned off; below two the pill hides itself (#150). */ fun setQuickSwitchViewEnabled(view: CalendarView, enabled: Boolean) { viewModelScope.launch { prefs.updateQuickSwitch { config -> - val next = if (enabled) config.enabled + view else config.enabled - view - if (next.size < QuickSwitchConfig.MIN_ENABLED) config else config.copy(enabled = next) + config.copy( + enabled = if (enabled) config.enabled + view else config.enabled - view, + ) } } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/ViewsSettings.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/ViewsSettings.kt index 51943d9..bdf44f8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/ViewsSettings.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/ViewsSettings.kt @@ -32,7 +32,6 @@ import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import de.jeanlucmakiola.calendula.ui.common.PickerDescription -import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig import de.jeanlucmakiola.calendula.ui.common.TimelineScale import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel import de.jeanlucmakiola.calendula.ui.common.descriptionRes @@ -55,8 +54,8 @@ import java.time.format.TextStyle as JavaTextStyle * it belongs to, plus the two cross-view ordering lists (#24, #69). * * The quick-switch cycle and the drawer list are independent orders — a view - * off in the cycle is still reachable from the drawer. The cycle needs at least - * [QuickSwitchConfig.MIN_ENABLED] targets. + * off in the cycle is still reachable from the drawer, including when the cycle + * is emptied and the pill disappears (#150). */ @Composable internal fun ViewsScreen( @@ -221,8 +220,6 @@ internal fun ViewsScreen( SectionHeader(stringResource(R.string.settings_quick_switch_header)) SettingsHint(stringResource(R.string.settings_quick_switch_hint)) Spacer(Modifier.height(8.dp)) - // Turning a view off is blocked once only the minimum remain enabled. - val canDisable = config.enabled.size > QuickSwitchConfig.MIN_ENABLED ReorderableColumn( items = config.order, keyOf = { it }, @@ -238,8 +235,6 @@ internal fun ViewsScreen( trailing = { Switch( checked = checked, - // Keep the last two on: with fewer, the pill can't switch. - enabled = !checked || canDisable, onCheckedChange = { on -> viewModel.setQuickSwitchViewEnabled(view, on) }, ) }, 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 869c3a2..1c6cd95 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 @@ -266,6 +266,7 @@ fun WeekScreen( currentYear = currentYear, selectedView = selectedView, onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, + quickSwitchViews = quickSwitchViews, onOpenDrawer = { scope.launch { drawerState.open() } }, onOpenSearch = onOpenSearch, onJumpToDate = jumpToDate, @@ -446,6 +447,7 @@ private fun WeekTopBar( currentYear: Int, selectedView: CalendarView, onCycleView: () -> Unit, + quickSwitchViews: List, onOpenDrawer: () -> Unit, onOpenSearch: () -> Unit, onJumpToDate: (LocalDate) -> Unit, @@ -480,6 +482,7 @@ private fun WeekTopBar( } ViewSwitcherPill( current = selectedView, + cycle = quickSwitchViews, onCycle = onCycleView, modifier = Modifier.padding(end = 8.dp), ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8c06b4..5fbdf01 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -496,7 +496,7 @@ Show the whole month Show the day\'s events Quick-switch button - Choose which views the top-right button cycles through, and drag to reorder them. Turned-off views stay reachable from the navigation menu. + Choose which views the top-right button cycles through, and drag to reorder them. With fewer than two views turned on the button is hidden. Turned-off views stay reachable from the navigation menu. Navigation menu Drag to reorder the views listed in the navigation menu. Drag to reorder diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt index 93d5028..fb73dc5 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt @@ -120,4 +120,30 @@ class ViewBackStackTest { ) assertThat(config.cycle).containsExactly(CalendarView.Agenda, CalendarView.Month).inOrder() } + + @Test + fun `a cycle can be emptied down to no views at all`() { + // #150: the settings screen no longer holds a floor, so every view may go. + val config = QuickSwitchConfig(order = IMPLEMENTED_VIEWS, enabled = emptySet()) + assertThat(config.cycle).isEmpty() + assertThat(config.cycle.size).isLessThan(QuickSwitchConfig.MIN_CYCLE) + } + + @Test + fun `one enabled view is below the cycle minimum, so the pill hides`() { + // A single target is not a switch — it hides rather than becoming a + // jump-to-one-view button, which would be dead once you were there. + val config = QuickSwitchConfig(order = IMPLEMENTED_VIEWS, enabled = setOf(CalendarView.Day)) + assertThat(config.cycle).containsExactly(CalendarView.Day) + assertThat(config.cycle.size).isLessThan(QuickSwitchConfig.MIN_CYCLE) + } + + @Test + fun `two enabled views are enough to show the pill`() { + val config = QuickSwitchConfig( + order = IMPLEMENTED_VIEWS, + enabled = setOf(CalendarView.Day, CalendarView.Month), + ) + assertThat(config.cycle.size).isAtLeast(QuickSwitchConfig.MIN_CYCLE) + } }