Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 77 additions & 3 deletions crates/reco-gui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,21 @@ impl AppState {
Ok(())
}

/// Whether the calibration currently loaded is the one configured in
/// preferences as the Default Calibration - i.e. saving now would
/// overwrite the fallback future sessions rely on, not just this
/// session's own file. `None == None` deliberately doesn't count (no
/// default configured means nothing to protect).
fn is_default_calibration(&self) -> bool {
match (
&self.calibration_path,
&self.user_settings.default_calibration_path,
) {
(Some(current), Some(default)) => current == default,
_ => false,
}
}

/// Restore Topology to the values loaded at init (or after auto-cal).
fn reset_calibration(&mut self) {
if let Some(base) = self.cal_baseline.clone() {
Expand Down Expand Up @@ -2386,6 +2401,14 @@ fn main() -> anyhow::Result<()> {
.unwrap_or_default()
.into(),
);
app.set_prefs_default_calibration_path(
s.user_settings
.default_calibration_path
.as_ref()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default()
.into(),
);
app.set_recording_codec(s.user_settings.recording_codec.clone().into());
app.set_recording_quality(s.user_settings.recording_quality.clone().into());
app.set_recording_folder(
Expand Down Expand Up @@ -2417,6 +2440,12 @@ fn main() -> anyhow::Result<()> {
} else {
Some(PathBuf::from(model_path))
};
let default_cal_path = app.get_prefs_default_calibration_path().to_string();
s.user_settings.default_calibration_path = if default_cal_path.is_empty() {
None
} else {
Some(PathBuf::from(default_cal_path))
};
s.user_settings.recording_codec = app.get_recording_codec().to_string();
s.user_settings.recording_quality = app.get_recording_quality().to_string();
let rec_folder = app.get_recording_folder().to_string();
Expand Down Expand Up @@ -2470,6 +2499,18 @@ fn main() -> anyhow::Result<()> {
}
});

let app_weak = app.as_weak();
app.on_pick_prefs_default_calibration(move || {
let dialog = rfd::FileDialog::new()
.set_title("Select default calibration")
.add_filter("Calibration JSON", &["json"]);
if let Some(path) = dialog.pick_file()
&& let Some(app) = app_weak.upgrade()
{
app.set_prefs_default_calibration_path(path.to_string_lossy().to_string().into());
}
});

let app_weak = app.as_weak();
app.on_pick_recording_folder(move || {
let dialog = rfd::FileDialog::new().set_title("Select default recording folder");
Expand Down Expand Up @@ -2858,9 +2899,10 @@ fn main() -> anyhow::Result<()> {
}
});

let app_weak = app.as_weak();
let state_ref = Rc::clone(&state);
app.on_save_calibration(move || {
// Shared by both `save-calibration` (after the default-calibration
// check passes) and `confirm-save-calibration` (user already said
// "yes, overwrite the default" in the warning modal).
fn do_save_calibration(state_ref: &Rc<RefCell<AppState>>, app_weak: &slint::Weak<RecoApp>) {
let save_result = state_ref.borrow().save_calibration();
match save_result {
Err(e) => {
Expand All @@ -2886,6 +2928,24 @@ fn main() -> anyhow::Result<()> {
}
}
}
}

let app_weak = app.as_weak();
let state_ref = Rc::clone(&state);
app.on_save_calibration(move || {
if state_ref.borrow().is_default_calibration() {
if let Some(app) = app_weak.upgrade() {
app.set_overwrite_default_cal_warning_open(true);
}
return;
}
do_save_calibration(&state_ref, &app_weak);
});

let app_weak = app.as_weak();
let state_ref = Rc::clone(&state);
app.on_confirm_save_calibration(move || {
do_save_calibration(&state_ref, &app_weak);
});

let app_weak = app.as_weak();
Expand Down Expand Up @@ -4225,6 +4285,20 @@ fn try_init_and_update(state: &Rc<RefCell<AppState>>, app_weak: &slint::Weak<Rec
if let Some(app) = app_weak.upgrade() {
sync_segments(&s, &app);
}
// No calibration explicitly picked yet: fall back to the user's
// configured Default Calibration (preferences), if any and if it
// still exists on disk - this is the one place every left/right/
// calibration pick path converges before `try_init`, so it covers
// "no calibration otherwise available" generically instead of
// duplicating the fallback at each pick site.
if s.calibration_path.is_none()
&& let Some(default_cal) = s.user_settings.default_calibration()
{
if let Some(app) = app_weak.upgrade() {
app.set_calibration_path(display_name(&default_cal).into());
}
s.calibration_path = Some(default_cal);
}
// Capture the pre-init clip length so we can distinguish an input
// change (new load / appended segments) from a calibration-only
// reload further down.
Expand Down
47 changes: 47 additions & 0 deletions crates/reco-gui/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ pub struct GuiSettings {
#[serde(default)]
pub ai_model_path: Option<PathBuf>,

/// Calibration file to fall back to whenever no calibration is
/// otherwise loaded/picked for a session (see `try_init_and_update`'s
/// fallback). `None` means no default is configured - the app
/// behaves as it always has, requiring an explicit pick.
#[serde(default)]
pub default_calibration_path: Option<PathBuf>,

/// Last window size, remembered across restarts. `None` means
/// "use Slint's preferred-width / preferred-height defaults".
#[serde(default)]
Expand Down Expand Up @@ -116,6 +123,7 @@ impl Default for GuiSettings {
default_quality: default_quality(),
default_blend_width: default_blend_width(),
ai_model_path: None,
default_calibration_path: None,
window_size: None,
window_maximized: false,
recording_codec: default_codec(),
Expand Down Expand Up @@ -168,6 +176,17 @@ impl GuiSettings {
self.recent_calibration.push(path);
self.save();
}

/// The configured default-calibration fallback, if set and if it
/// still exists on disk - a deleted/moved default should silently
/// stop applying rather than pointing `calibration_path` at a dead
/// file (see `try_init_and_update`).
pub fn default_calibration(&self) -> Option<PathBuf> {
self.default_calibration_path
.as_ref()
.filter(|p| p.exists())
.cloned()
}
}

#[cfg(test)]
Expand All @@ -193,4 +212,32 @@ mod tests {
assert_eq!(s.default_quality, "balanced");
assert!(s.recent_left.is_empty());
}

#[test]
fn default_calibration_none_when_unset() {
let s = GuiSettings::default();
assert!(s.default_calibration().is_none());
}

#[test]
fn default_calibration_none_when_file_no_longer_exists() {
let mut s = GuiSettings::default();
s.default_calibration_path = Some(PathBuf::from("does-not-exist.json"));
assert!(s.default_calibration().is_none());
}

#[test]
fn default_calibration_returns_path_when_it_exists() {
let dir = std::env::temp_dir().join(format!(
"reco-gui-settings-test-default-cal-{}",
std::process::id(),
));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("default.json");
std::fs::write(&path, b"{}").unwrap();

let mut s = GuiSettings::default();
s.default_calibration_path = Some(path.clone());
assert_eq!(s.default_calibration(), Some(path));
}
}
75 changes: 75 additions & 0 deletions crates/reco-gui/ui/main.slint
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,16 @@ export component RecoApp inherits Window {
in-out property <string> prefs-default-quality: "balanced";
in-out property <float> prefs-default-blend-width: 0.05;
in-out property <string> prefs-ai-model-path: "";
in-out property <string> prefs-default-calibration-path: "";
// Confirmation shown when Save Calibration would overwrite whichever
// file is configured above as the Default Calibration - that file is
// meant to be a stable fallback for future sessions, so overwriting
// it in-place needs an explicit "yes really" rather than silently
// changing what every session with no calibration falls back to.
// Rust decides whether to open this (it knows both paths); confirming
// calls `confirm-save-calibration` instead of `save-calibration` to
// skip the check on the second click.
in-out property <bool> overwrite-default-cal-warning-open: false;

// Toast notification list (Tier 3a).
in-out property <[Toast]> toasts: [];
Expand Down Expand Up @@ -713,6 +723,10 @@ export component RecoApp inherits Window {
callback changed-cal-camera-axis-offset(float);
callback changed-cal-x-ty(float);
callback save-calibration();
// Confirmed overwrite of the Default Calibration (see
// `overwrite-default-cal-warning-open`) - performs the actual save
// that `save-calibration` held back pending confirmation.
callback confirm-save-calibration();
callback reset-calibration();
// Live lens parameter editing. Fires on any of the 8 sliders per
// camera; Rust reads the property values for the currently-selected
Expand Down Expand Up @@ -753,6 +767,7 @@ export component RecoApp inherits Window {
callback open-prefs-dialog();
callback save-prefs();
callback pick-prefs-model();
callback pick-prefs-default-calibration();
callback open-website();
callback open-forum();
in-out property <bool> prefs-telemetry-enabled: false;
Expand Down Expand Up @@ -2099,6 +2114,20 @@ export component RecoApp inherits Window {
}
}

Text { text: "Default calibration"; color: #ccc; font-size: 12px; }
HorizontalLayout {
spacing: 6px;
LineEdit {
text <=> root.prefs-default-calibration-path;
placeholder-text: "Path to .json, or leave empty";
horizontal-stretch: 1;
}
Button {
text: "Browse…";
clicked => { root.pick-prefs-default-calibration(); }
}
}

Rectangle { height: 1px; background: root.border-subtle; }

Text { text: "Recording defaults"; color: #ccc; font-size: 12px; font-weight: 600; }
Expand Down Expand Up @@ -2188,6 +2217,52 @@ export component RecoApp inherits Window {
}
}

// ── Overwrite-default-calibration confirmation (modal) ──
if root.overwrite-default-cal-warning-open: Rectangle {
background: root.bg-scrim;
TouchArea { clicked => { root.overwrite-default-cal-warning-open = false; } }

Rectangle {
width: 420px;
height: min(parent.height - 60px, 240px);
background: root.bg-surface;
border-radius: 12px;
border-width: 1px;
border-color: root.border-mid;
TouchArea {}

VerticalLayout {
padding: 24px;
spacing: 14px;

Text { text: "Overwrite default calibration?"; color: root.text-heading; font-size: 18px; font-weight: 600; }

Text {
text: "This is your configured Default Calibration - the fallback used whenever no calibration is otherwise loaded. Saving now will overwrite it with the current session's changes.";
color: root.text-muted;
font-size: 12px;
wrap: word-wrap;
}

Rectangle { vertical-stretch: 1; }

HorizontalLayout {
spacing: 8px;
alignment: end;
Button { text: "Cancel"; clicked => { root.overwrite-default-cal-warning-open = false; } }
Button {
text: "Overwrite Default";
primary: true;
clicked => {
root.overwrite-default-cal-warning-open = false;
root.confirm-save-calibration();
}
}
}
}
}
}

// ── Bug report dialog ──
if root.bug-dialog-open: Rectangle {
background: root.bg-scrim;
Expand Down
Loading