From 2cf7d590bd2f662eaca7dfa281f979b18ad5fe1b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 18 Aug 2026 15:52:39 +0200 Subject: [PATCH] Hide the quick-switch button below two views (#150) The pill only appears with something to switch between, so one enabled view hides it just like zero. The floor that blocked disabling is gone from both the settings screen and the view model. --- .../calendula/ui/agenda/AgendaScreen.kt | 3 +++ .../calendula/ui/common/CalendarView.kt | 16 +++++++----- .../calendula/ui/common/ViewSwitcherPill.kt | 6 +++++ .../calendula/ui/day/DayScreen.kt | 3 +++ .../calendula/ui/month/MonthScreen.kt | 3 +++ .../ui/settings/SettingsViewModel.kt | 10 +++---- .../calendula/ui/settings/ViewsSettings.kt | 9 ++----- .../calendula/ui/week/WeekScreen.kt | 3 +++ app/src/main/res/values/strings.xml | 2 +- .../calendula/ui/common/ViewBackStackTest.kt | 26 +++++++++++++++++++ 10 files changed, 61 insertions(+), 20 deletions(-) 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) + } }