Skip to content

Make SubtitleLineViewModel.StartTime a plain setter; add StartTimeKeepDuration#11664

Merged
niksedk merged 3 commits into
mainfrom
fix/starttime-plain-setter
Jun 16, 2026
Merged

Make SubtitleLineViewModel.StartTime a plain setter; add StartTimeKeepDuration#11664
niksedk merged 3 commits into
mainfrom
fix/starttime-plain-setter

Conversation

@niksedk

@niksedk niksedk commented Jun 16, 2026

Copy link
Copy Markdown
Member

Background

SubtitleLineViewModel.StartTime's setter had a hidden side effect: assigning it also moved EndTime to preserve the duration ("drag the whole line"). This made the most natural-looking statement — line.StartTime = x — the dangerous one:

  • Programmatic code almost always wants the end to stay fixed, so the codebase had ~45 SetStartTimeOnly(...) calls to opt out of the drag.
  • Forgetting SetStartTimeOnly and writing StartTime = x silently shifted the end too, corrupting timing.

This is the design follow-up to #11661 (which routed Adjust through SetTimes). It inverts the default so the safe behavior is the obvious one.

Change

  • StartTime setter is now plain: it moves the start, keeps the end fixed, and recomputes the duration — identical to the existing SetStartTimeOnly, which is kept as an alias so the ~45 existing callers are unchanged.
  • New StartTimeKeepDuration property provides the old drag-the-line (preserve-duration) behavior, explicitly.
  • Grid binding (InitListViewAndEditBox): the start-time editor binds to StartTimeKeepDuration when no separate end-time editor is shown (preserving that UX), and to StartTimeOnly when one is.

Caller audit

I audited every direct SubtitleLineViewModel.StartTime = … assignment in src/ui (excluding Paragraph and other types). Only five relied on the drag and were converted to StartTimeKeepDuration:

  • AudioVisualizer.cs — waveform Alt-drag (move whole line)
  • MainViewModel.cs — insert subtitle at video position
  • MainViewModel.cs — transcribe split into new lines
  • MainViewModel.cs — ripple-delete trailing-line shift
  • PointSyncer.cs — single-point sync

All other direct assignments either set EndTime immediately after or wanted keep-end semantics (one, snap-start-to-shot-change, is actually fixed by the new plain setter).

Bonus fix

Converting single-point sync (AdjustViaShowEarlierLater) to StartTimeKeepDuration also fixes a real bug: it previously shifted only the start time of every line, leaving end times in place, which shrank/inverted durations. It now shifts the whole line by the offset.

Tests

  • SubtitleLineViewModelTimeTestsStartTime (plain, keep-end), SetStartTimeOnly (keep-end), and StartTimeKeepDuration (positive/negative shift, duration preserved).
  • PointSyncerTests — single-point sync shifts both start and end, preserving durations.

Full UI suite: 354/354 passing.

🤖 Generated with Claude Code

niksedk and others added 2 commits June 16, 2026 16:48
…pDuration

The StartTime setter had a hidden side effect: assigning it also moved
EndTime to preserve duration ("drag the whole line"). That made the
natural-looking `line.StartTime = x` the dangerous path - programmatic
code almost always wants the end to stay fixed, hence ~45 SetStartTimeOnly
calls, while forgetting it silently shifted timing.

Invert the default:
- StartTime setter is now plain: moves the start, keeps the end fixed,
  recomputes duration (identical to SetStartTimeOnly, which is kept as an
  alias so existing callers are unchanged).
- New StartTimeKeepDuration property provides the drag-the-line behavior.
- The start-time grid editor binds to StartTimeKeepDuration when no
  separate end-time editor is shown (preserving that UX), and to
  StartTimeOnly otherwise.

Converted the five call sites that relied on the old drag behavior to
StartTimeKeepDuration (waveform Alt-drag, insert-at-video-position,
transcribe-split, ripple-delete trailing shift, and single-point sync).

Converting single-point sync also fixes a bug where it shifted only the
start time, leaving end times in place (shrinking/inverting durations).

Add tests for the new StartTime / StartTimeKeepDuration / SetStartTimeOnly
semantics and for single-point sync shifting the whole line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code review found six sites that shift a line keeping its duration using the
compound form `StartTime += ...` (or `+ adjustment`), which the original
caller audit's `.StartTime = ` grep missed. After the StartTime-setter
inversion these would have moved only the start and corrupted durations:

- Adjust (all / selected / selected-and-forward) - the "Show earlier/later"
  and "Adjust all times" sync tool
- WaveformSetStartAndOffsetTheRest / WaveformSetEndAndOffsetTheRest
- AudioVisualizerSetStartAndOffsetTheRest

Convert them to StartTimeKeepDuration so the whole line shifts as before.

Also simplify StartTimeKeepDuration to delegate to SetTimes, so start/end
are applied atomically (no transient start > end) and the shift is computed
from the live EndTime - StartTime span rather than a possibly-stale Duration
field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@niksedk

niksedk commented Jun 16, 2026

Copy link
Copy Markdown
Member Author

Self-review follow-up

Ran a multi-angle review of this branch. It caught one significant miss in the original caller audit, now fixed in bd30cfe:

The audit grepped for .StartTime = and so never matched the compound-assignment form StartTime += …. Six whole-line-shift sites use exactly that and relied on the old end-follows-start behavior — after the inversion they would have moved only the start and corrupted durations:

  • Adjust(...) — all three modes (all / selected / selected-and-forward), i.e. the "Show earlier/later" / "Adjust all times" sync tool
  • WaveformSetStartAndOffsetTheRest / WaveformSetEndAndOffsetTheRest
  • AudioVisualizerSetStartAndOffsetTheRest

All converted to StartTimeKeepDuration.

Also simplified StartTimeKeepDuration to delegate to SetTimes(value, value + (EndTime - StartTime)) — applies start/end atomically (no transient start > end) and computes the span from live times instead of the Duration field.

Other review findings reviewed and not changed (not regressions from this PR):

  • Plain StartTime can now produce a negative duration if start is set past end — but this was already true via the pre-existing StartTimeOnly binding (end-editor mode) and SetStartTimeOnly; not introduced here.
  • _skipUpdate early-return dropping a two-way write-back — refuted: the suppression windows are synchronous on the UI thread, so user input can't interleave; StartTimeOnly already carried the identical guard.
  • Gap/neighbor brush staleness after a keep-duration drag — same as the previous drag setter (duration unchanged ⇒ no brush re-notify); gap coloring is refreshed by the slow timer.

Full UI suite: 354/354 green.

StartTimeKeepDuration must stay a property because the start-time editor
binds to it TwoWay, but assigning a side-effecting property from code is the
footgun this change set out to remove. Mirror the existing
StartTimeOnly/SetStartTimeOnly convention: keep a thin bindable property and
add an internal SetStartTimeKeepDuration(TimeSpan) method that the ~11
programmatic callers use, so the "move the whole line" side effect reads as
an explicit action. The property delegates to the method.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@niksedk niksedk merged commit d80908e into main Jun 16, 2026
2 of 3 checks passed
@niksedk niksedk deleted the fix/starttime-plain-setter branch June 16, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant