collective

Browse / github.com/storytold/filmcraft / patch

Settings: Selection Follows Playhead persists across restarts

[Bug Report] [FilmCraft] selection follow playhead ### Description the selection follow playhead feature in the sequence menu is got getting save after reboot, each time i have to go the sequence menu to enable it, please keep it enable after reboot just like adobe premiere pro. ### Operating system and version Windows 11 <!-- ticket-relay:files --> ### Attachments ![Screenshot\_2026-10-24\_164737.png](https://tickets.devmesh.xyz/f/oiFj5W5LATw8YRsxU5WB8Q/Screenshot_2026-10-24_164737.png) <!-- /ticket-relay:files --> ### Ticket details | | | |---|---| | **App** | FilmCraft | | **Type** | Bug Report | | **Reported by** | Shahid Gire (Disc

Status
open
Score
0.00
Adopters
0 other operators
Author
amoisttowe1 (imported from pull request #3251, open)
Base
bfc23840b603e69c782302c6ed04d4a9f6ec26a8
Size
+117 −1 in 4 files
Review
pending
Shared
2026-10-28
Id
6bbfd1b78ed35f7d51cd3eda

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/lib.rs crates/engine/src/sequence_tools.rs crates/engine/src/settings.rs crates/engine/src/settings_tests.rs

diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs--- a/crates/engine/src/lib.rs+++ b/crates/engine/src/lib.rs@@ -623,6 +623,8 @@ impl Session {     pub fn start_autosave(&mut self, cfg: autosave::AutosaveConfig) -> std::io::Result<()> {         let prefs_path = cfg.data_dir.join("preferences.json");         self.prefs = autosave::Preferences::load(&prefs_path);+        // a restart restores the persisted Selection Follows Playhead toggle (#3089)+        self.state.selection_follows_playhead = self.prefs.timeline.selection_follows_playhead;         self.prefs.apply_hardware_decoding();         self.media.set_use_proxies(self.prefs.media.enable_proxies);         self.shortcuts.set_dir(&cfg.data_dir);@@ -695,6 +697,8 @@ impl Session {     pub fn set_prefs(&mut self, p: autosave::Preferences) -> std::io::Result<()> {         let cache_changed = self.prefs.media_cache != p.media_cache;         self.prefs = p;+        // the session mirrors the persisted Selection Follows Playhead toggle (#3089)+        self.state.selection_follows_playhead = self.prefs.timeline.selection_follows_playhead;         self.prefs.apply_hardware_decoding();         if cache_changed {             self.apply_media_cache();diff --git a/crates/engine/src/sequence_tools.rs b/crates/engine/src/sequence_tools.rs--- a/crates/engine/src/sequence_tools.rs+++ b/crates/engine/src/sequence_tools.rs@@ -95,7 +95,12 @@ fn groups() -> Vec<(&'static str, Vec<CommandSpec>)> {             "sequence.linkedSelection",             vec![                 spec("sequence.selectionFollowsPlayhead", "Selection Follows Playhead", &["Sequence"], None, r#"{"on":bool?}"#, always, |s, p| {-                    s.state.selection_follows_playhead = bool_p(p, "on").unwrap_or(!s.state.selection_follows_playhead);+                    // the preference is the source of truth (#3089: it persists across sessions);+                    // the session flag mirrors it through set_prefs+                    let on = bool_p(p, "on").unwrap_or(!s.prefs.timeline.selection_follows_playhead);+                    let mut next = s.prefs.clone();+                    next.timeline.selection_follows_playhead = on;+                    s.set_prefs(next).map_err(|e| EngineError::Other(format!("saving preferences: {e}")))?;                     if s.state.selection_follows_playhead {                         select_under_playhead(s);                     }diff --git a/crates/engine/src/settings.rs b/crates/engine/src/settings.rs--- a/crates/engine/src/settings.rs+++ b/crates/engine/src/settings.rs@@ -732,6 +732,9 @@ pub struct TimelinePrefs {     pub add_tracks_automatically: bool,     /// Files dropped onto the Timeline are placed directly on it.     pub drop_import_to_timeline: bool,+    /// Sequence ▸ Selection Follows Playhead: moving the playhead selects the clips under it on+    /// targeted tracks (#3089: persisted, so it survives a restart like the sibling toggles).+    pub selection_follows_playhead: bool, }  impl Default for TimelinePrefs {@@ -759,6 +762,7 @@ impl Default for TimelinePrefs {             restore_open_sequences: true,             add_tracks_automatically: true,             drop_import_to_timeline: true,+            selection_follows_playhead: false,         }     } }@@ -1348,6 +1352,7 @@ static CATEGORIES: &[Category] = &[             b("timeline.restoreOpenSequences", "Restore open sequences when opening projects", true),             b("timeline.addTracksAutomatically", "Add tracks automatically when editing source clips onto the Timeline", false),             b("timeline.dropImportToTimeline", "Place files dropped onto the Timeline directly on the Timeline", true),+            b("timeline.selectionFollowsPlayhead", "Selection Follows Playhead", true),         ],     },     Category {diff --git a/crates/engine/src/settings_tests.rs b/crates/engine/src/settings_tests.rs--- a/crates/engine/src/settings_tests.rs+++ b/crates/engine/src/settings_tests.rs@@ -851,3 +851,105 @@ fn builtin_label_presets_are_complete_and_distinct() {     }     assert!(!settings::is_builtin_label_preset("Team")); }++// ---------- #3089: Selection Follows Playhead persists ----------++/// Sequence ▸ Selection Follows Playhead persists: turning it on writes the preference, saves+/// `preferences.json`, and the next session starts with it on (#3089 — it used to live only in+/// volatile session state). Fails on `main`: the preference key does not exist there.+#[test]+fn selection_follows_playhead_persists_across_sessions() {+    let dir = tmp_dir("sfp-persist");+    let mut s = Session::default();+    s.start_autosave(crate::autosave::AutosaveConfig::new(&dir)).unwrap();+    s.execute("sequence.selectionFollowsPlayhead", json!({"on": true})).unwrap();+    assert!(s.state.selection_follows_playhead);+    assert_eq!(s.prefs.get("timeline.selectionFollowsPlayhead"), Some(json!(true)), "command persisted the pref");+    let file: Value = serde_json::from_slice(&std::fs::read(dir.join("preferences.json")).unwrap()).unwrap();+    assert_eq!(file["timeline"]["selectionFollowsPlayhead"], json!(true));+    s.shutdown();++    let mut s2 = Session::default();+    s2.start_autosave(crate::autosave::AutosaveConfig::new(&dir)).unwrap();+    assert_eq!(s2.prefs.get("timeline.selectionFollowsPlayhead"), Some(json!(true)), "restored from the file");+    assert!(s2.state.selection_follows_playhead, "applied to the session at load");+    s2.shutdown();+    std::fs::remove_dir_all(&dir).unwrap();+}++/// The off direction persists too, and an explicit set through the settings surface works:+/// a fix that simply defaulted the flag on would pass the test above and fail this one.+#[test]+fn selection_follows_playhead_stays_off_when_turned_off() {+    let dir = tmp_dir("sfp-off");+    let mut s = Session::default();+    s.start_autosave(crate::autosave::AutosaveConfig::new(&dir)).unwrap();+    s.execute("sequence.selectionFollowsPlayhead", json!({"on": true})).unwrap();+    s.execute("sequence.selectionFollowsPlayhead", json!({"on": false})).unwrap();+    assert!(!s.state.selection_follows_playhead);+    assert_eq!(s.prefs.get("timeline.selectionFollowsPlayhead"), Some(json!(false)));+    // set back on through prefs.set (the Settings dialog / agent path) and the live session follows+    s.execute("prefs.set", json!({"key": "timeline.selectionFollowsPlayhead", "value": true})).unwrap();+    assert!(s.state.selection_follows_playhead, "prefs.set drives the live session");+    s.execute("prefs.set", json!({"key": "timeline.selectionFollowsPlayhead", "value": false})).unwrap();+    assert!(!s.state.selection_follows_playhead);+    let file: Value = serde_json::from_slice(&std::fs::read(dir.join("preferences.json")).unwrap()).unwrap();+    assert_eq!(file["timeline"]["selectionFollowsPlayhead"], json!(false));+    s.shutdown();+    let mut s2 = Session::default();+    s2.start_autosave(crate::autosave::AutosaveConfig::new(&dir)).unwrap();+    assert!(!s2.state.selection_follows_playhead, "off survives the restart");+    s2.shutdown();+    std::fs::remove_dir_all(&dir).unwrap();+}++/// Omitting `on` still toggles (from the persisted value, not from volatile state): off -> on+/// through the command alone, persisted both ways.+#[test]+fn selection_follows_playhead_toggle_without_on_persists() {+    let dir = tmp_dir("sfp-toggle");+    let mut s = Session::default();+    s.start_autosave(crate::autosave::AutosaveConfig::new(&dir)).unwrap();+    s.execute("sequence.selectionFollowsPlayhead", json!({})).unwrap();+    assert!(s.state.selection_follows_playhead, "off -> on");+    assert_eq!(s.prefs.get("timeline.selectionFollowsPlayhead"), Some(json!(true)));+    s.execute("sequence.selectionFollowsPlayhead", json!({})).unwrap();+    assert!(!s.state.selection_follows_playhead, "on -> off");+    assert_eq!(s.prefs.get("timeline.selectionFollowsPlayhead"), Some(json!(false)));+    s.shutdown();+    std::fs::remove_dir_all(&dir).unwrap();+}++/// A hostile value is rejected and the stored value is kept (the shared prefs.set contract).+#[test]+fn selection_follows_playhead_rejects_hostile_values() {+    let dir = tmp_dir("sfp-hostile");+    let mut s = Session { prefs_path: Some(dir.join("preferences.json")), ..Session::default() };+    s.execute("prefs.set", json!({"key": "timeline.selectionFollowsPlayhead", "value": true})).unwrap();+    assert!(s.execute("prefs.set", json!({"key": "timeline.selectionFollowsPlayhead", "value": "no"})).is_err());+    assert_eq!(s.prefs.get("timeline.selectionFollowsPlayhead"), Some(json!(true)), "bad write kept the old value");+    std::fs::remove_dir_all(&dir).unwrap();+}++/// A preferences file written before the key existed keeps today's behaviour: off, and every+/// other Timeline value untouched.+#[test]+fn selection_follows_playhead_defaults_to_off_for_older_prefs() {+    let dir = tmp_dir("sfp-legacy");+    let path = dir.join("preferences.json");+    std::fs::write(&path, json!({"version": 2, "timeline": {"stillImageDuration": 3}}).to_string()).unwrap();+    let prefs = Preferences::load(&path);+    assert_eq!(prefs.get("timeline.selectionFollowsPlayhead"), Some(json!(false)));+    assert_eq!(prefs.timeline.still_image_duration, 3.0);+    std::fs::remove_dir_all(&dir).unwrap();+}++/// The key is a Boolean field of the Timeline category in the settings registry, so the Settings+/// dialog and the agent-facing settings surface see it like every other persisted toggle.+#[test]+fn selection_follows_playhead_is_in_the_settings_registry() {+    let (cat, f) = settings::fields().into_iter().find(|(_, f)| f.key == "timeline.selectionFollowsPlayhead").expect("registry field");+    assert_eq!(cat, "timeline");+    assert!(matches!(f.kind, Kind::Bool));+    assert_eq!(settings::field("timeline.selectionFollowsPlayhead").map(|f| f.key), Some("timeline.selectionFollowsPlayhead"));+}