From 3c66a3a6d7b59f94dcabe59032b75159a06a0f15 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 11 Aug 2026 17:48:45 +0200 Subject: [PATCH] Setting to turn drag-to-reschedule off (#173) (#174) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → Views → *Drag to reschedule*, on by default. Turned off, CalendarHost provides no EventMoveScope, which is the existing "moving is off" contract: no event block in month, week or day view registers a drag gesture, and the screen-reader Move… action goes away with it. Nothing changes for anyone who leaves it on. Closes #173 Co-authored-by: Jean-Luc Makiola Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/174 --- .../calendula/data/prefs/SettingsPrefs.kt | 14 +++++++++++++ .../calendula/ui/CalendarHost.kt | 6 +++++- .../calendula/ui/CalendarHostViewModel.kt | 14 +++++++++++++ .../calendula/ui/common/EventMoveScope.kt | 17 ++++++++++++++++ .../calendula/ui/day/DayScreen.kt | 3 ++- .../calendula/ui/month/MonthScreen.kt | 2 +- .../calendula/ui/settings/SettingsUiState.kt | 2 ++ .../ui/settings/SettingsViewModel.kt | 20 ++++++++++++++++--- .../calendula/ui/settings/ViewsSettings.kt | 14 ++++++++++++- .../calendula/ui/week/WeekScreen.kt | 3 ++- app/src/main/res/values/strings.xml | 2 ++ 11 files changed, 89 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index 5fa676d..6bc5849 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -301,6 +301,19 @@ class SettingsPrefs @Inject constructor( store.edit { it[TODAY_BUTTON_IN_TOOLBAR_KEY] = enabled } } + /** + * Whether events can be dragged to another slot in the calendar views (#68, + * #173). Defaults to ON. Off means no block registers a drag gesture at all; + * rescheduling then goes through the edit form. + */ + val dragToReschedule: Flow = store.data.map { prefs -> + prefs[DRAG_TO_RESCHEDULE_KEY] ?: true + } + + suspend fun setDragToReschedule(enabled: Boolean) { + store.edit { it[DRAG_TO_RESCHEDULE_KEY] = enabled } + } + /** * How far ahead the in-app Agenda screen shows events (v2.11). Defaults to * [AgendaRange.Month] — a month of upcoming events. Independent of the @@ -945,6 +958,7 @@ class SettingsPrefs @Inject constructor( internal val MONTH_VIEW_STYLE_KEY = stringPreferencesKey("month_view_style") internal val TIMELINE_SCALE_KEY = stringPreferencesKey("timeline_scale") internal val TODAY_BUTTON_IN_TOOLBAR_KEY = booleanPreferencesKey("today_button_in_toolbar") + internal val DRAG_TO_RESCHEDULE_KEY = booleanPreferencesKey("drag_to_reschedule") internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view") internal val QUICK_SWITCH_VIEWS_KEY = stringPreferencesKey("quick_switch_views") internal val DRAWER_VIEW_ORDER_KEY = stringPreferencesKey("drawer_view_order") 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 f2ef6e5..fa32b89 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -312,9 +312,13 @@ fun CalendarHost( heldEditKey = key editKey = key } - val moveScope = remember(movableCalendarIds, reschedule) { + // Off by preference (#173) switches off the gesture, not the scope: blocks + // register no drag, while the edit form and its TalkBack action stay put. + val dragToReschedule = viewModel.dragToReschedule.collectAsStateWithLifecycle().value + val moveScope = remember(movableCalendarIds, reschedule, dragToReschedule) { EventMoveScope( movableCalendarIds = movableCalendarIds, + dragEnabled = dragToReschedule, move = reschedule::move, inFlight = reschedule.inFlight, undoStarted = reschedule.undoStarted, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt index a5f3104..70cabf6 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt @@ -53,4 +53,18 @@ class CalendarHostViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5_000L), initialValue = false, ) + + /** + * Whether events may be dragged to another slot to reschedule them (#68, #173). + * Off until the stored value arrives, even though the preference itself defaults + * on: the wrong guess costs an enabled user a gesture for the few frames a cold + * start takes to read DataStore, but re-arms the one thing a disabled user + * switched off to be rid of. + */ + val dragToReschedule: StateFlow = prefs.dragToReschedule + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = false, + ) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventMoveScope.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventMoveScope.kt index 6fb0d8a..c338e40 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventMoveScope.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventMoveScope.kt @@ -29,6 +29,12 @@ class EventMoveScope( * the repository writes whatever it is handed — so this gate is load-bearing. */ val movableCalendarIds: Set, + /** + * Whether the *gesture* is on (#173). Off gates the drag alone: [edit] and the + * TalkBack action below stay, since invoking a menu item can't cause the + * accidental drags the setting exists to stop. + */ + val dragEnabled: Boolean, /** False when the drop was refused outright, so nothing will be written. */ val move: (MoveRequest) -> Boolean, /** @@ -91,6 +97,17 @@ fun ghostAlpha(lifted: Boolean): Float = animateFloatAsState( label = "ghost-alpha", ).value +/** + * Whether [event] can be picked up and dragged — its calendar allows moving and + * the gesture is switched on. Not the same question as [eventMoveAction], which + * survives the setting. + */ +@Composable +fun eventDragAllowed(event: EventInstance): Boolean { + val move = LocalEventMove.current ?: return false + return move.dragEnabled && move.allows(event) +} + /** * A TalkBack action that opens [event] in the edit form, so rescheduling isn't * pointer-only. Null when this event can't be moved. 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 05c2ef5..6b6d4fe 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 @@ -92,6 +92,7 @@ import de.jeanlucmakiola.calendula.ui.common.TimelineDragController import de.jeanlucmakiola.calendula.ui.common.TimelineDragOverlay import de.jeanlucmakiola.calendula.ui.common.TimelineDrop import de.jeanlucmakiola.calendula.ui.common.beginsOn +import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed import de.jeanlucmakiola.calendula.ui.common.eventMoveAction import de.jeanlucmakiola.calendula.ui.common.rememberEventDragSource import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController @@ -737,7 +738,7 @@ private fun EventBlock( val moveAction = eventMoveAction(block.event) // A block clipped at the top continues from the previous day: its top edge // is midnight, not the event's start, so dragging it would invent a time. - val draggable = moveAction != null && block.beginsOn(date, zone) + val draggable = eventDragAllowed(block.event) && block.beginsOn(date, zone) val dragModifier = rememberEventDragSource( enabled = draggable, key = block.event.instanceId, 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 14c7628..ffcccf6 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 @@ -2172,7 +2172,7 @@ private fun monthChipDragModifier( rowHeightPx: Float, isRtl: Boolean, ): Modifier = rememberDragSurface( - enabled = moveScope != null && controller != null, + enabled = moveScope?.dragEnabled == true && controller != null, key = week.days.first(), onPickUp = { local, pointerInRoot, nodeInRoot, size -> val bandTop = band[0]?.takeIf { it.isAttached }?.positionInRoot()?.y diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt index c3cf3fc..effeb71 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt @@ -42,6 +42,8 @@ data class SettingsUiState( val showWeekNumbers: Boolean = false, /** Whether the jump-to-today control sits in the top bar instead of the FAB (#60). */ val todayButtonInToolbar: Boolean = false, + /** Whether events can be dragged to another slot to reschedule them (#68, #173). */ + val dragToReschedule: Boolean = true, /** How far ahead the in-app Agenda screen shows events (v2.11). */ val agendaScreenRange: AgendaRange = AgendaRange.Month, /** How far ahead the agenda widget shows events (v2.11). */ 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 8b49d6e..d2b37fe 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 @@ -145,9 +145,15 @@ class SettingsViewModel @Inject constructor( prefs.showWeekNumbers, prefs.agendaShowToday, prefs.softenCalendarColors, - prefs.todayButtonInToolbar, - ) { hourLines, weekNumbers, showToday, soften, todayInToolbar -> - DisplayToggles(hourLines, weekNumbers, showToday, soften, todayInToolbar) + combine( + prefs.todayButtonInToolbar, + prefs.dragToReschedule, + ::Pair, + ), + ) { hourLines, weekNumbers, showToday, soften, (todayInToolbar, dragToMove) -> + DisplayToggles( + hourLines, weekNumbers, showToday, soften, todayInToolbar, dragToMove, + ) }, ) { view, screenRange, widgetRange, timeFormat, toggles -> ViewSettings( @@ -157,6 +163,7 @@ class SettingsViewModel @Inject constructor( agendaShowToday = toggles.agendaShowToday, softenColors = toggles.softenColors, todayButtonInToolbar = toggles.todayButtonInToolbar, + dragToReschedule = toggles.dragToReschedule, ) }, combine( @@ -189,6 +196,7 @@ class SettingsViewModel @Inject constructor( agendaShowToday = views.agendaShowToday, softenColors = views.softenColors, todayButtonInToolbar = views.todayButtonInToolbar, + dragToReschedule = views.dragToReschedule, agendaShowRangeBar = misc.showRangeBar, autofocusEventTitle = misc.autofocusEventTitle, pastEventDisplay = misc.pastEventDisplay, @@ -289,6 +297,7 @@ class SettingsViewModel @Inject constructor( val agendaShowToday: Boolean, val softenColors: Boolean, val todayButtonInToolbar: Boolean, + val dragToReschedule: Boolean, ) private data class DisplayToggles( @@ -297,6 +306,7 @@ class SettingsViewModel @Inject constructor( val agendaShowToday: Boolean, val softenColors: Boolean, val todayButtonInToolbar: Boolean, + val dragToReschedule: Boolean, ) private data class MiscSettings( @@ -534,6 +544,10 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { prefs.setTodayButtonInToolbar(enabled) } } + fun setDragToReschedule(enabled: Boolean) { + viewModelScope.launch { prefs.setDragToReschedule(enabled) } + } + /** * Switch the launcher label (#44). The card highlights at once, then settles * on what the component state reports; the writes are binder round-trips and 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 18bd68f..51943d9 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 @@ -114,7 +114,7 @@ internal fun ViewsScreen( GroupedRow( title = stringResource(R.string.settings_dim_completed), summary = stringResource(R.string.settings_dim_completed_summary), - position = Position.Bottom, + position = Position.Middle, trailing = { Switch( checked = state.dimCompletedEvents, @@ -123,6 +123,18 @@ internal fun ViewsScreen( }, onClick = { viewModel.setDimCompletedEvents(!state.dimCompletedEvents) }, ) + GroupedRow( + title = stringResource(R.string.settings_drag_to_reschedule), + summary = stringResource(R.string.settings_drag_to_reschedule_summary), + position = Position.Bottom, + trailing = { + Switch( + checked = state.dragToReschedule, + onCheckedChange = viewModel::setDragToReschedule, + ) + }, + onClick = { viewModel.setDragToReschedule(!state.dragToReschedule) }, + ) Spacer(Modifier.height(8.dp)) SectionHeader(stringResource(R.string.settings_month_header)) 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 b4debbe..869c3a2 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 @@ -100,6 +100,7 @@ import de.jeanlucmakiola.calendula.ui.common.TimelineDragController import de.jeanlucmakiola.calendula.ui.common.TimelineDragOverlay import de.jeanlucmakiola.calendula.ui.common.TimelineDrop import de.jeanlucmakiola.calendula.ui.common.beginsOn +import de.jeanlucmakiola.calendula.ui.common.eventDragAllowed import de.jeanlucmakiola.calendula.ui.common.eventMoveAction import de.jeanlucmakiola.calendula.ui.common.rememberEventDragSource import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController @@ -905,7 +906,7 @@ private fun EventBlock( val moveAction = eventMoveAction(block.event) // A block clipped at the top continues from the previous day: its top edge // is midnight, not the event's start, so dragging it would invent a time. - val draggable = moveAction != null && block.beginsOn(date, zone) + val draggable = eventDragAllowed(block.event) && block.beginsOn(date, zone) val dragModifier = rememberEventDragSource( enabled = draggable, key = block.event.instanceId, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cd92955..d8c06b4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -439,6 +439,8 @@ The height you pinched the timeline to Dim completed events Fade events that have already ended in month and week view + Drag to reschedule + Move an event by dragging it to another day or time Past events Show Dim