Skip to content

Fix #13113: add default frame rate setting to options - #13281

Open
Ironship wants to merge 1 commit into
SubtitleEdit:mainfrom
Ironship:fix/issue-13113
Open

Fix #13113: add default frame rate setting to options#13281
Ironship wants to merge 1 commit into
SubtitleEdit:mainfrom
Ironship:fix/issue-13113

Conversation

@Ironship

@Ironship Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Problem

Fixes #13113 — on macOS (and other platforms) SubtitleEdit 5.x always opens files at the default 23.976 fps and there is no way to change the default frame rate in the UI.

SE (5.2 beta 2, macOS x64) defaults to opening files in 23.976 fps, and I cannot find a way to change this. It would be great if there was a setting to change the default frame rate, or if SE always remembered the last set frame rate between startups.

The setting value exists (Se.Settings.General.DefaultFrameRate, src/ui/Logic/Config/SeGeneral.cs:40, default 23.976) but no UI control reads or writes it in SE 5.x — a comment in src/ui/Features/Tools/BatchConvert/BatchConverter.cs:2112 confirms: "nothing in the UI ever assigns it". SE 4.0.16 had a "Default frame rate" combo box in Options (src/ui/Forms/Options/Settings.Designer.cs in tag 4.0.16, saving gs.DefaultFrameRate = outFrameRate); the control was lost in the 5.x rewrite.

Fix

Restore the missing UI control: add a "Default frame rate" combo box to Options → General (next to "Use frame mode"), wired to Se.Settings.General.DefaultFrameRate:

  • src/ui/Features/Options/Settings/SettingsPage.cs — new SettingsItem with a ComboBox bound to FrameRates / SelectedDefaultFrameRate (same pattern as the existing DefaultEncoding combo).
  • src/ui/Features/Options/Settings/SettingsViewModel.csFrameRates list (23.976/24/25/29.97/30/50/59.94/60/120), load of the current value (inserted into the list when it is not one of the presets), and save back to general.DefaultFrameRate.
  • src/ui/Logic/Config/Se.csLoadSettings() re-derives the UI-side CurrentFrameRate = DefaultFrameRate, bridges the default into libse, and then seeds libse's live CurrentFrameRate once at startup. SaveSettings() deliberately does not copy the UI-side current rate, so later settings saves cannot overwrite a frame rate supplied by a video parser.
  • src/ui/Logic/Config/Language/Options/LanguageSettings.cs — "Default frame rate" resource string (the generated English.json file is not hand-edited).

CurrentFrameRate semantics: at startup it now comes from the configurable default; opening a video still overrides it with the video's actual frame rate (MainViewModel.cs:19224), and the toolbar combo still overrides it for the session.

Verification

  • dotnet build src/ui/UI.csproj -v q -nologo — EXIT:0, 0 warnings, 0 errors (build.log attached in run)
  • Permanent regression tests (committed, tests/UI/Logic/Config/FrameRateSettingsTests.cs):
    • LoadSettings_SeedsCurrentFrameRateFromDefault — a persisted stale 23.976 current rate is replaced by the configured default (25) on load, in both the UI settings store and libse;
    • SaveSettings_DoesNotOverwriteVideoDerivedFrameRate — a parser-provided 29.97 in libse survives a later SaveSettings(temp).
    • 2/2 PASS, plus the neighboring ContinuationStyle round-trip tests 4/4 PASS.
  • Manual verification path:
    1. Start SE 5.x, open Options → General.
    2. "Default frame rate" combo is present next to "Use frame mode" and shows 23.976.
    3. Change it to 25 and close Options — with no video loaded, the toolbar and live frame-rate setting update to 25 immediately.
    4. Restart SE and open an SRT file without loading a video — the frame rate remains 25 (not 23.976), e.g. visible in the status bar / when inserting time codes in frame mode.
    5. Open a video file — the video's actual frame rate still takes over as before.

Notes

  • Interpretation: the issue asks for a way to change the default frame rate; implementing the explicit setting (the first option the reporter suggests) is the smallest change. Remembering the last-used frame rate across startups would change CurrentFrameRate semantics, which is deliberately NOT done.
  • The combo is intentionally not editable: a rate that is not one of the presets (e.g. 23.98) can only appear if it is already present in Settings.json; FrameRates.Insert(0, …) displays such a value but there is no way to type a custom one. Deliberate, matching the same pattern as the other frame-rate combos in the app.
  • Deliberate non-change: the hard-coded 23.976 fallback in GetMediaInformation (MainViewModel.cs:19224) is only hit when a video is loaded but its frame rate cannot be read; left untouched to keep CurrentFrameRate semantics stable.
  • This PR was created with AI assistance (per repo AI contributor guidelines).

Follow-up (audit-driven, commit deb99c6)

Independent audit found the setting was persisted but never applied at startup: Settings.json stores currentFrameRate, and it was restored over the constructor's CurrentFrameRate = DefaultFrameRate on every load. Fixed:

  • Se.cs — after settings load, CurrentFrameRate is re-seeded from DefaultFrameRate (the explicit setting now wins at startup; opening a video still overrides it with the video's real frame rate, and the toolbar combo still overrides it for the session).
  • MainViewModel.cs — the toolbar frame-rate combo now shows the configured default instead of a hardcoded 23.976 when no video is loaded (custom values are added to the combo list, same pattern as the Options combo).
  • BatchConverter.cs — comment updated (it claimed no UI ever assigns DefaultFrameRate; that is no longer true).

Deliberate behavior change: the last-used frame rate is no longer restored across restarts; the "Default frame rate" setting is authoritative at startup (the issue asked for exactly this).

Follow-up 2 + maintainer review (final commit 887a210)

The final deep audit found that copying CurrentFrameRate inside UpdateLibSeSettings() made every Se.SaveSettings() overwrite a parser-provided rate (for example 29.97) with stale UI session state (for example 24). The first follow-up removed that copy, but maintainer review correctly found that libse then stayed at its constructor default (23.976) at startup.

Final split of responsibilities:

  • LoadSettings() seeds both the UI-side and libse live current rate from the user's configured default exactly once at startup.
  • UpdateLibSeSettings() bridges DefaultFrameRate, but does not overwrite the live current rate during later settings saves.
  • Saving Options with no video loaded applies the new default immediately to both stores and refreshes the toolbar value. If a video is loaded, its parser-derived rate is preserved.
  • The hand-edited generated English.json hunk was removed; LanguageSettings.cs remains the source of truth.

Verification on 887a210 (real runs):

  • clean dotnet build src/ui/UI.csproj --no-incremental — EXIT:0;
  • focused startup/save probe — 2/2 PASS: startup 25 reaches UI + libse; parser 29.97 survives a later SaveSettings(temp).

@Ironship

Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

AUDIT 13281 (issue #13113): PASS-WITH-WARNINGS -> follow-up committed (49357e0)

  • A. Fixes issue: PASS - the missing 'Default frame rate' UI (lost in the 5.x rewrite; BatchConverter.cs:2112 'nothing in the UI ever assigns it') is restored and saved to Settings.json + propagated to libse.
  • WARN found: on EXISTING installs the persisted CurrentFrameRate (23.976) was restored at startup and never re-derived from the new default, so the setting appeared dead after restart. Fixed in follow-up commit 49357e0: LoadSettings() now re-derives CurrentFrameRate = DefaultFrameRate, and UpdateLibSeSettings() also syncs CurrentFrameRate to libse (frame-mode time codes). Save-time combo overrides are unaffected (re-derive is load-only).
  • B/C: PASS (surgical; combo wiring matches the page's existing pattern; invariant-culture round-trip). D: PASS per policy (UI-only, manual path documented, now accurate after the follow-up). E: build EXIT:0, 168/168 Language+Config UI tests pass on the amended branch. F: WARN cleared by the follow-up.
    VERDICT: mergeable.

@Ironship
Ironship marked this pull request as ready for review August 5, 2026 22:08
Copilot AI lite review requested due to automatic review settings August 5, 2026 22:08
@Ironship

Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

/copilot-review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Ironship

Ironship commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

AUDIT #13281 (issue #13113): FAIL on first pass → FIXED in deb99c6

  • A. Fixes issue: FAIL (first pass) — the new setting was persisted but never applied: Settings.json stores currentFrameRate and it was restored over the constructor's CurrentFrameRate = DefaultFrameRate on every load, so the reported symptom (always 23.976, no way to change) stayed.
  • Fix in deb99c6: after settings load, CurrentFrameRate is re-seeded from DefaultFrameRate; the toolbar combo now shows the configured default instead of hardcoded 23.976; stale BatchConverter comment updated.
  • B/C/D: PASS (surgical, style, minimal tests OK — UI-only, manual path documented). E: Compilation PASS — clean rebuild exit 0. F: low risk; one deliberate behavior change: the last-used frame rate no longer survives restarts — the explicit setting is authoritative at startup (this is exactly what the issue asked for). 1 commit (Ironship), DRAFT.

@Ironship

Ironship commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

/copilot-review

@Ironship

Ironship commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

FINAL AUDIT #13281 (issue #13113) — FAIL → FIXED in 4cc5fe6

  • The final deep audit (regression/dead-code/implementation-error) reproduced a real regression: after the deb99c6 startup sync, SaveSettings pushed the stale UI CurrentFrameRate over the parser's libse value (29.97 -> 24) via the UpdateLibSeSettings bridge.
  • Fix in 4cc5fe6: removed the CurrentFrameRate bridge — both legitimate writers (video open, toolbar combo) already write libse directly; startup coverage comes from libse's own load sync + the DefaultFrameRate bridge.
  • Verification (real runs): repro probe 2/2 PASS (parser 29.97 survives SaveSettings; default bridge 25 -> libse works); build --no-incremental EXIT:0; 1 commit, Ironship, CI re-running on 4cc5fe6.

@niksedk niksedk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes

Restoring the lost SE4 control is the right call and the Options wiring looks fine. Three things.

The libse bridge claim in Follow-up 2 is backwards

at startup libse's own load syncs CurrentFrameRate = DefaultFrameRate (GeneralSettings.cs:63) after the DefaultFrameRate bridge (line 622), so the default still applies

GeneralSettings.cs:60-63 is the constructor:

public GeneralSettings()
{
    DefaultFrameRate = 23.976;
    CurrentFrameRate = DefaultFrameRate;

It runs when Configuration.Settings is first constructed, which cannot be after UpdateLibSeSettings() — that method dereferences Configuration.Settings.General to do the bridging, so the ctor has already run by then. And even in the ordering the note assumes, the ctor seeds CurrentFrameRate from libse's own hard-coded 23.976, not from the user's value.

Net effect: with CurrentFrameRate removed from the bridge and nothing else writing it at startup, Configuration.Settings.General.CurrentFrameRate stays 23.976 no matter what the user picks. The libse consumers the PR itself names — TimedTextImscRosetta, TimeCode frame formatting — therefore ignore the new setting until a video is opened or the toolbar combo is touched. That is exactly the no-video-loaded case the reporter described.

The concern that motivated removing the line (a stale UI value clobbering the parser's rate on SaveSettings) is legitimate. The fix is to seed libse's CurrentFrameRate once at load rather than to stop bridging it at all — e.g. set it inside LoadSettings next to the existing UI-side seed, and leave SaveSettings alone.

The setting needs a restart to do anything

SettingsViewModel.SaveSettings writes general.DefaultFrameRate but not general.CurrentFrameRate, and Se.LoadSettings only runs at startup. So the user changes the option, closes Options, and nothing happens until they restart — which the manual verification path confirms (step 4 is "Restart SE"). Please also apply it to the live CurrentFrameRate on save when no video is loaded, or the setting looks broken on first use.

Don't hand-edit English.json

src/ui/Assets/Languages/English.json is generated from the Language*.cs classes — the class is the source of truth. LanguageSettings.cs is edited correctly here; the English.json hunk should be dropped and the file regenerated.

Minor

The toolbar combo reads Se.Settings.General.CurrentFrameRate once in the MainViewModel constructor, so changing the option mid-session leaves the toolbar showing the old value even after the underlying setting changes. Consistent with the restart-required behaviour above, and it would be fixed by the same change.

@Ironship

Ironship commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 887a210. You were right about the constructor order. The final behavior is now split clearly:\n\n- On startup, LoadSettings sets both UI and libse current frame rate from the user's default.\n- Later SaveSettings calls do not overwrite a rate read from a video.\n- Changing the option with no video loaded applies at once and updates the toolbar.\n- If a video is loaded, its rate stays unchanged.\n- The generated English.json hunk was removed.\n\nReal checks: clean UI build EXIT:0; focused startup/save probe 2/2 PASS (startup default 25 reaches both stores; parser 29.97 survives a later settings save). One commit, Ironship.

@Ironship

Ironship commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

FINAL AUDIT #13281 for issue #13113: PASS\n\nFresh head: 887a210; one Ironship commit; CI success. The final implementation now has explicit ownership of the live rate:\n- startup seeds UI and libse once from DefaultFrameRate;\n- Options updates UI/libse immediately only when no video is loaded and refreshes the toolbar;\n- video-open and toolbar paths remain the writers for video/session-derived rates;\n- UpdateLibSeSettings bridges DefaultFrameRate only, so later SaveSettings cannot clobber a parser-derived CurrentFrameRate.\n\nThe previously reproduced 29.97 -> 24 regression path is closed, the no-video startup path is covered, the generated English.json change is gone, and no duplicate/dead writer remains. Verification: clean UI build EXIT:0; startup/save focused probe 2/2 PASS; current CI success.

@Ironship

Ironship commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

/copilot-review

@Ironship

Ironship commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Rebased the single Ironship commit onto current main (e44a388). The only conflict was the BatchConverter frame-rate fallback comment: the upstream fixed/video/project-rate selection remains intact, and the final invalid-rate fallback now correctly refers to the restored Default frame rate option. No behavior was dropped. Fresh head: cded430. Verification: clean non-incremental UI build EXIT:0; BatchConverterBeautifyTimeCodesTests 3/3 PASS; full UITests 1483 passed, 0 failed, 1 skipped; current GitHub CI PASS.

@Ironship
Ironship requested a review from niksedk August 6, 2026 12:35

@niksedk niksedk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment — the code is right now; the remaining question is product, not code

All three of my earlier points are fixed, and the libse one is fixed the right way:

  • The startup seed is outside UpdateLibSeSettings. Configuration.Settings.General.CurrentFrameRate is now set once in LoadSettings after the bridge call, with a comment saying why it must not live in UpdateLibSeSettings (which SaveSettings also calls). That resolves the contradiction in the previous revision, where libse would have kept 23.976 regardless of the setting while the note claimed the constructor ran afterwards.
  • The setting applies without a restart. SaveSettings now writes general.CurrentFrameRate, Configuration.Settings.General.CurrentFrameRate and the toolbar's SelectedFrameRate when _mainViewModel is { IsVideoLoaded: false } — correctly gated so it cannot stomp a rate the parser derived from an open video, and null-safe via the pattern match.
  • English.json is no longer hand-edited; only LanguageSettings.cs.

The decision I would not make for you

Settings.General.CurrentFrameRate = Settings.General.DefaultFrameRate;

in LoadSettings means the frame rate a user picks from the toolbar is no longer remembered across restarts — the Options value always wins at startup. #13113 asked for either a configurable default or remembering the last used rate; this implements the first and deliberately removes the second. That is defensible and the PR is honest about it, but it is a behaviour change for every existing user, not just the reporter, and it is not something the diff can justify on its own.

Smaller

  1. No test. An earlier revision had a probe that reproduced the SaveSettings clobbering bug — exactly the regression this PR has already had once. Se.LoadSettings seeding libse, and SaveSettings not overwriting a video-derived rate, are both plain state checks against a temp settings file with no UI involved.
  2. The combo is not editable, so a rate like 23.98 can only appear if it is already in Settings.json. FrameRates.Insert(0, …) handles displaying such a value but there is no way to enter one. Fine if intentional, worth a line in the description if so.

@Ironship

Ironship commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in f7cb35c — both remaining points:

1. Tests are now committed (tests/UI/Logic/Config/FrameRateSettingsTests.cs), exactly the state checks you described, against a temp settings file with no UI involved:

  • LoadSettings_SeedsCurrentFrameRateFromDefault: writes a settings file with DefaultFrameRate = 25 and a stale persisted CurrentFrameRate = 23.976, loads it, and asserts both the UI store and libse end up at 25 — this is the regression the PR already had once.
  • SaveSettings_DoesNotOverwriteVideoDerivedFrameRate: sets libse's live rate to a parser-provided 29.97, calls SaveSettings(temp), and asserts 29.97 survives (the UpdateLibSeSettings bridge no longer copies CurrentFrameRate).

Both are RED on the pre-fix behavior (stale value survives load / save overwrites 29.97) and GREEN on the current head. 2/2 PASS; neighboring continuation-style round-trip tests 4/4 PASS.

2. Non-editable combo — deliberate; documented in the PR description: a rate outside the presets (e.g. 23.98) can only appear if already present in Settings.json; FrameRates.Insert(0, …) displays it but there is no way to type a custom one. Matches the other frame-rate combos in the app.

Verification: clean non-incremental UI build EXIT:0, fresh GitHub CI PASS on the new SHA.

@Ironship
Ironship requested a review from niksedk August 7, 2026 10:16
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.

Cannot set default frame rate

3 participants