mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
* View: remember Extras, Show and Snap To across restarts (#3762) View › Extras, the View › Show items (Pixel Grid, Grid, Guides, …) and the View › Snap To targets reset to their defaults on every launch. They are now saved in Preferences (`viewExtras`) as they change and restored at startup, as Photoshop keeps them between sessions. Only items that differ from their defaults are stored, so a default changed in a later release still reaches users who never touched it. A hand-edited file can't break them: unknown keys and non-boolean values keep their defaults. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * View: rustfmt the view-extras restart test (#3762) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -994,6 +994,10 @@ pub struct Preferences {
|
||||
/// differ from their defaults, saved as the user changes them and restored at launch, as
|
||||
/// Photoshop remembers each tool's options between sessions. JSON owned by the shell.
|
||||
pub tool_options: Value,
|
||||
/// View › Extras, the View › Show items (Pixel Grid, Grid, Guides, …) and the View › Snap To
|
||||
/// targets that differ from their defaults, saved as the user toggles them and restored at
|
||||
/// launch, as Photoshop keeps them between sessions (#3762). JSON owned by the shell.
|
||||
pub view_extras: Value,
|
||||
/// File › Scripts › Script Events Manager: event → script bindings.
|
||||
pub script_events: crate::automate_cmds::ScriptEvents,
|
||||
}
|
||||
|
||||
@@ -1725,6 +1725,8 @@ impl eframe::App for PhotocraftApp {
|
||||
brush_picker::persist(self, &ctx);
|
||||
// Tool options are remembered between launches the same way (#2814).
|
||||
tool_memory::persist(self, &ctx);
|
||||
// So are View › Extras, Show and Snap To (#3762).
|
||||
view_cmds::persist(self, &ctx);
|
||||
self.automation_input = false;
|
||||
shape_stroke_ui::finish_frame(self, &ctx);
|
||||
native_menu::sync(self, &ctx);
|
||||
|
||||
@@ -218,6 +218,7 @@ pub fn load(app: &mut PhotocraftApp) {
|
||||
crate::brush_picker::restore(app);
|
||||
sync_rulers(app);
|
||||
crate::tool_memory::restore(app);
|
||||
crate::view_cmds::restore(app);
|
||||
app.sync_recent();
|
||||
app.prefs_rt.saved_rev = app.session.prefs.rev();
|
||||
app.prefs_rt.saved_value = Some(app.session.prefs_value());
|
||||
@@ -2649,6 +2650,39 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_show_items_survive_a_restart() {
|
||||
let (mut app, store) = app_with_store();
|
||||
let ctx = egui::Context::default();
|
||||
tick(&mut app, &ctx);
|
||||
// Untouched: nothing is stored, so later default changes still reach the user.
|
||||
crate::view_cmds::persist(&mut app, &ctx);
|
||||
tick(&mut app, &ctx);
|
||||
assert!(stored(&store).get("viewExtras").is_none_or(Value::is_null));
|
||||
// The menu commands, as a user toggles them (#3762: Pixel Grid off).
|
||||
for id in ["view.show.pixelGrid", "view.show.grid", "view.snapTo.layers", "view.extras"] {
|
||||
crate::menus::invoke(&mut app, &ctx, id, json!({})).unwrap();
|
||||
}
|
||||
crate::view_cmds::persist(&mut app, &ctx);
|
||||
tick(&mut app, &ctx);
|
||||
assert_eq!(stored(&store)["viewExtras"], json!({"extras": false, "grid": true, "show": {"pixelGrid": false}, "snapTo": {"layers": false}}));
|
||||
// A restart gets them back.
|
||||
let saved = store.lock().unwrap().clone().unwrap();
|
||||
let (mut app2, _) = app_with_saved(Some(saved));
|
||||
tick(&mut app2, &ctx);
|
||||
let v = &app2.ui.view;
|
||||
assert!(!v.extras && !v.show.pixel_grid && !v.snap_to.layers && app2.ui.extras.grid);
|
||||
assert!(v.show.selection_edges && v.snap_to.guides && app2.ui.extras.guides, "the rest keep their defaults");
|
||||
// A hand-edited file: unknown keys and non-booleans keep the defaults, good ones still load.
|
||||
for bad in [json!("junk"), json!({"extras": 0, "show": {"pixelGrid": "no", "mesh": true, "bogus": true}, "snapTo": [1]})] {
|
||||
let (mut app3, _) = app_with_saved(Some(json!({"viewExtras": bad}).to_string()));
|
||||
tick(&mut app3, &ctx);
|
||||
let v = &app3.ui.view;
|
||||
assert!(v.extras && v.show.pixel_grid && v.snap_to.layers, "{bad}");
|
||||
assert_eq!(v.show.mesh, bad.is_object());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_tooltips_preferences_turn_tooltips_off() {
|
||||
let (mut app, _) = app_with_store();
|
||||
|
||||
@@ -214,6 +214,106 @@ fn snap_slot<'a>(s: &'a mut SnapTo, key: &str) -> Option<&'a mut bool> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Every [`show_slot`] key.
|
||||
const SHOW_KEYS: [&str; 13] = [
|
||||
"layerEdges",
|
||||
"selectionEdges",
|
||||
"targetPath",
|
||||
"pixelGrid",
|
||||
"smartGuides",
|
||||
"brushPreview",
|
||||
"slices",
|
||||
"notes",
|
||||
"mesh",
|
||||
"editPins",
|
||||
"count",
|
||||
"canvasGuides",
|
||||
"artboardGuides",
|
||||
];
|
||||
/// Every [`snap_slot`] key.
|
||||
const SNAP_KEYS: [&str; 5] = ["guides", "grid", "layers", "slices", "documentBounds"];
|
||||
|
||||
/// The flags of `now` that differ from `def`, by key (`None` when none do).
|
||||
fn changed_flags<T: Copy>(now: T, def: T, keys: &[&str], slot: impl for<'a> Fn(&'a mut T, &str) -> Option<&'a mut bool>) -> Option<Value> {
|
||||
let (mut now, mut def) = (now, def);
|
||||
let diff: Map<String, Value> = keys
|
||||
.iter()
|
||||
.filter_map(|k| {
|
||||
let v = *slot(&mut now, k)?;
|
||||
(Some(v) != slot(&mut def, k).copied()).then(|| ((*k).to_string(), Value::Bool(v)))
|
||||
})
|
||||
.collect();
|
||||
(!diff.is_empty()).then_some(Value::Object(diff))
|
||||
}
|
||||
|
||||
/// View › Extras, Show and Snap To as remembered across restarts (`Preferences::view_extras`):
|
||||
/// only what differs from the defaults, so a default changed in a later release still reaches
|
||||
/// users who never touched that item. `Null` when nothing differs.
|
||||
fn remembered(app: &PhotocraftApp) -> Value {
|
||||
let (v, e, def) = (&app.ui.view, &app.ui.extras, crate::state::Extras::default());
|
||||
let mut m = Map::new();
|
||||
if v.extras != ViewOptions::default().extras {
|
||||
m.insert("extras".into(), Value::Bool(v.extras));
|
||||
}
|
||||
// View › Show › Grid and Guides live in `UiState::extras` beside the rulers and snapping.
|
||||
if e.grid != def.grid {
|
||||
m.insert("grid".into(), Value::Bool(e.grid));
|
||||
}
|
||||
if e.guides != def.guides {
|
||||
m.insert("guides".into(), Value::Bool(e.guides));
|
||||
}
|
||||
if let Some(show) = changed_flags(v.show, Show::default(), &SHOW_KEYS, show_slot) {
|
||||
m.insert("show".into(), show);
|
||||
}
|
||||
if let Some(snap) = changed_flags(v.snap_to, SnapTo::default(), &SNAP_KEYS, snap_slot) {
|
||||
m.insert("snapTo".into(), snap);
|
||||
}
|
||||
if m.is_empty() { Value::Null } else { Value::Object(m) }
|
||||
}
|
||||
|
||||
/// Remember View › Extras, Show and Snap To in the preferences once the user lets go of the
|
||||
/// pointer (#3762). Cheap: a small JSON compare per frame. Run every frame, so items set by the
|
||||
/// control channel or the Show Extras Options dialog are remembered too.
|
||||
pub fn persist(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
if ctx.input(|i| i.pointer.any_down()) {
|
||||
return;
|
||||
}
|
||||
let now = remembered(app);
|
||||
if app.session.prefs().view_extras != now {
|
||||
app.session.prefs.edit(|p| p.view_extras = now);
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore the remembered View › Extras, Show and Snap To items at launch. A hand-edited file
|
||||
/// can't break them: an unknown key or a value that isn't a boolean keeps its default.
|
||||
pub fn restore(app: &mut PhotocraftApp) {
|
||||
let saved = app.session.prefs().view_extras.clone();
|
||||
if !saved.is_null() && !saved.is_object() {
|
||||
log::warn!("Preferences: the remembered View › Show items are not an object; using the defaults");
|
||||
return;
|
||||
}
|
||||
let flag = |k: &str| saved.get(k).and_then(Value::as_bool);
|
||||
if let Some(on) = flag("extras") {
|
||||
app.ui.view.extras = on;
|
||||
}
|
||||
if let Some(on) = flag("grid") {
|
||||
app.ui.extras.grid = on;
|
||||
}
|
||||
if let Some(on) = flag("guides") {
|
||||
app.ui.extras.guides = on;
|
||||
}
|
||||
for (k, v) in saved.get("show").and_then(Value::as_object).into_iter().flatten() {
|
||||
if let (Some(slot), Some(on)) = (show_slot(&mut app.ui.view.show, k), v.as_bool()) {
|
||||
*slot = on;
|
||||
}
|
||||
}
|
||||
for (k, v) in saved.get("snapTo").and_then(Value::as_object).into_iter().flatten() {
|
||||
if let (Some(slot), Some(on)) = (snap_slot(&mut app.ui.view.snap_to, k), v.as_bool()) {
|
||||
*slot = on;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Window › <panel>: (panel, dock tab) for panels that live as tabs of a dock card.
|
||||
fn panel_tab(app: &PhotocraftApp, id: &str) -> Option<(&'static str, usize)> {
|
||||
let pro = app.ui.theme.is_pro();
|
||||
|
||||
Reference in New Issue
Block a user