Bring the drag docs in line with what the code does (#68)
The realignment envelope is described as implemented: whole-day shifts, or a time-only shift whose anchor keeps its day. Also documents the one-write- at-a-time gate, and notes the drag as a third recurring-scope prompt.
This commit is contained in:
@@ -69,8 +69,10 @@ data class MoveRequest(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A recurring drop waiting for the user to pick how far it reaches.
|
* A recurring drop waiting for the user to pick how far it reaches.
|
||||||
* [occurrenceOnly] means the rule names days a single moved occurrence can't
|
* [occurrenceOnly] means a wider write would leave the rule and the series
|
||||||
* re-derive (`BYDAY=MO,WE`, `2TH`, …) — see [realignRecurrence].
|
* anchor disagreeing — either the rule names days one moved occurrence can't
|
||||||
|
* re-derive (`BYDAY=MO,WE`, `2TH`, …), or the shift wouldn't carry the anchor
|
||||||
|
* across the same midnights the occurrence crossed.
|
||||||
*/
|
*/
|
||||||
data class MoveScopePrompt(val occurrenceOnly: Boolean)
|
data class MoveScopePrompt(val occurrenceOnly: Boolean)
|
||||||
|
|
||||||
@@ -297,8 +299,9 @@ class RescheduleViewModel @Inject constructor(
|
|||||||
val isRecurring = original.rrule != null && !detail.isException
|
val isRecurring = original.rrule != null && !detail.isException
|
||||||
val movedDay = shifted.start.date != original.start.date
|
val movedDay = shifted.start.date != original.start.date
|
||||||
// The series anchor moves by the same *wall-clock* shift as the dragged
|
// The series anchor moves by the same *wall-clock* shift as the dragged
|
||||||
// occurrence, so only a whole-day shift moves it by a predictable number
|
// occurrence, so only a whole-day shift carries it across the same number
|
||||||
// of days; anything else can cross an extra midnight at the anchor.
|
// of midnights whatever time of day it sits at. A same-date drag has its
|
||||||
|
// own, narrower check — see anchorKeepsItsDay.
|
||||||
val wholeDayShift = original.isAllDay || shifted.start.time == original.start.time
|
val wholeDayShift = original.isAllDay || shifted.start.time == original.start.time
|
||||||
val realigned = if (isRecurring && movedDay) {
|
val realigned = if (isRecurring && movedDay) {
|
||||||
if (wholeDayShift) {
|
if (wholeDayShift) {
|
||||||
@@ -326,7 +329,8 @@ class RescheduleViewModel @Inject constructor(
|
|||||||
else -> anchorKeepsItsDay(detail, original, shifted, zone)
|
else -> anchorKeepsItsDay(detail, original, shifted, zone)
|
||||||
},
|
},
|
||||||
// Where the series row's own DTSTART lands, given the anchor moves by
|
// Where the series row's own DTSTART lands, given the anchor moves by
|
||||||
// the same shift — only meaningful under wholeDayShift.
|
// the same shift. Only read under canRealign, which is what
|
||||||
|
// guarantees the anchor really does travel this many days.
|
||||||
newAnchorDate = anchorDate(detail, original, zone)
|
newAnchorDate = anchorDate(detail, original, zone)
|
||||||
.plus(shifted.start.date.toEpochDays() - original.start.date.toEpochDays(), DateTimeUnit.DAY),
|
.plus(shifted.start.date.toEpochDays() - original.start.date.toEpochDays(), DateTimeUnit.DAY),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<!-- Drag an event to another time or day (#68) -->
|
<!-- Drag an event to another time or day (#68) -->
|
||||||
<string name="event_move_action">Move…</string>
|
<string name="event_move_action">Move…</string>
|
||||||
<string name="event_move_recurring_title">Move recurring event</string>
|
<string name="event_move_recurring_title">Move recurring event</string>
|
||||||
<string name="event_move_occurrence_only">This series picks its days in a way that can\'t be recalculated from one moved event, so only this event can move.</string>
|
<string name="event_move_occurrence_only">Moving the whole series would change which days it falls on, so only this event can move.</string>
|
||||||
<!-- %1$s is the new date and time, e.g. "Fri, 7 Aug, 09:00". -->
|
<!-- %1$s is the new date and time, e.g. "Fri, 7 Aug, 09:00". -->
|
||||||
<string name="event_move_done">Moved to %1$s</string>
|
<string name="event_move_done">Moved to %1$s</string>
|
||||||
<string name="event_move_undo">Undo</string>
|
<string name="event_move_undo">Undo</string>
|
||||||
|
|||||||
@@ -129,15 +129,29 @@ Two things the drag has to get right that the edit screen sidesteps:
|
|||||||
from the edit screen; wiring it there too is a separate change.
|
from the edit screen; wiring it there too is a separate change.
|
||||||
|
|
||||||
What the rule has to agree with is the **series anchor**, not the occurrence
|
What the rule has to agree with is the **series anchor**, not the occurrence
|
||||||
being dragged, and the anchor moves by the same *wall-clock* shift. Only two
|
being dragged, and `buildEventUpdateValues` moves that anchor by the same
|
||||||
things survive that intact, which is exactly the envelope the realigner accepts:
|
*wall-clock* shift. So the realigner only touches weekday, which is uniform mod
|
||||||
weekday (uniform mod 7, so every anchor time of day crosses the same number of
|
7 and therefore survives that shift whatever time of day the anchor sits at.
|
||||||
midnights) and a shift that is a whole number of days (otherwise a late-enough
|
Day-of-month is not uniform — `BYMONTHDAY=28` with a January anchor, occurrence
|
||||||
anchor crosses one midnight more than the occurrence did). Day-of-month is not
|
Feb 28 dragged to Mar 1, would leave a Jan 29 anchor under a `BYMONTHDAY=1`
|
||||||
uniform — `BYMONTHDAY=28` with a January anchor, occurrence Feb 28 dragged to
|
rule, a DTSTART that is not an instance of its own rule and a phantom
|
||||||
Mar 1, would leave a Jan 29 anchor under a `BYMONTHDAY=1` rule, a DTSTART that
|
occurrence on any client that trusts it.
|
||||||
is not an instance of its own rule and a phantom occurrence on any client that
|
|
||||||
trusts it. Those drags may only move the one occurrence.
|
Uniform mod 7 is not enough on its own: the anchor also has to cross the *same
|
||||||
|
number of midnights* as the occurrence did, or the rebuilt weekday is off by a
|
||||||
|
day. `RescheduleViewModel` requires that of every write wider than one
|
||||||
|
occurrence, in the two shapes a drag comes in:
|
||||||
|
|
||||||
|
- a drag that **changes the day** must be a whole number of days, so the two
|
||||||
|
move in step whatever the anchor's time of day;
|
||||||
|
- a **time-only** drag must leave the anchor on its own day. That is normally
|
||||||
|
free, since the anchor shares the occurrence's time of day — but a row with
|
||||||
|
no `EVENT_TIMEZONE` resolves anchor and occurrence in zones that can sit a
|
||||||
|
DST hour apart, and a near-midnight drag would then carry the anchor across a
|
||||||
|
midnight the occurrence never crossed.
|
||||||
|
|
||||||
|
Whatever falls outside — a rule the realigner won't rebuild, or a shift that
|
||||||
|
would move rule and anchor out of step — may only move the one occurrence.
|
||||||
|
|
||||||
The accepted parts are deliberately a **subset** of what `parseSimpleRecurrence`
|
The accepted parts are deliberately a **subset** of what `parseSimpleRecurrence`
|
||||||
understands, so anything realignable is also a rule whose `UNTIL` the guard
|
understands, so anything realignable is also a rule whose `UNTIL` the guard
|
||||||
@@ -157,6 +171,14 @@ occurrence, and a single occurrence becomes an exception row that no `UNTIL`
|
|||||||
constrains. Testing the occurrence in every case would refuse the perfectly
|
constrains. Testing the occurrence in every case would refuse the perfectly
|
||||||
ordinary drag of a bounded series' last occurrence.
|
ordinary drag of a bounded series' last occurrence.
|
||||||
|
|
||||||
|
**One drop is written at a time.** Two drops of the same recurring event landing
|
||||||
|
inside one write window would each compute their shift from the same pre-move
|
||||||
|
occurrence, while the data layer applies both to the re-read anchor — so the
|
||||||
|
shifts would compound. `RescheduleViewModel.move` therefore refuses while a write
|
||||||
|
(or its scope dialog) is outstanding, and says so in its return value: the view
|
||||||
|
that took the drop releases the block it was holding on the target instead of
|
||||||
|
waiting out a settle that will never arrive.
|
||||||
|
|
||||||
Two known limitations of a whole-series move, both shared with the edit screen's
|
Two known limitations of a whole-series move, both shared with the edit screen's
|
||||||
own *All events* time save rather than introduced here — a drag just makes them
|
own *All events* time save rather than introduced here — a drag just makes them
|
||||||
one gesture away: the series' `EXDATE` stamps and its exception rows are **not**
|
one gesture away: the series' `EXDATE` stamps and its exception rows are **not**
|
||||||
@@ -204,8 +226,8 @@ already bounded by the same dirty check — only `ALL_DAY`, `EVENT_TIMEZONE`,
|
|||||||
`DTSTART` and `DTEND`/`DURATION`/`RRULE` are written — so a concurrent remote
|
`DTSTART` and `DTEND`/`DURATION`/`RRULE` are written — so a concurrent remote
|
||||||
edit to the title, notes or guests survives untouched. What a drop *can* clobber
|
edit to the title, notes or guests survives untouched. What a drop *can* clobber
|
||||||
is a concurrent remote **time** change, and parking a one-gesture action behind a
|
is a concurrent remote **time** change, and parking a one-gesture action behind a
|
||||||
modal would cost more than that case is worth; the undo in the confirmation
|
modal would cost more than that case is worth; the undo on the confirmation chip
|
||||||
snackbar is the answer instead. Undo restores semantics, not the row's byte
|
is the answer instead. Undo restores semantics, not the row's byte
|
||||||
shape (`DURATION` normalises to `P<n>S`/`P<n>D`, `EVENT_TIMEZONE` is stamped
|
shape (`DURATION` normalises to `P<n>S`/`P<n>D`, `EVENT_TIMEZONE` is stamped
|
||||||
concrete), and it is offered only where the inverse is one symmetric write — a
|
concrete), and it is offered only where the inverse is one symmetric write — a
|
||||||
one-off event or a whole-series shift. *This event* leaves an exception row
|
one-off event or a whole-series shift. *This event* leaves an exception row
|
||||||
|
|||||||
Reference in New Issue
Block a user