mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 21:57:12 -06:00
continued work; wip
This commit is contained in:
@@ -17,6 +17,7 @@ mod paint;
|
||||
mod panelcmds;
|
||||
mod path;
|
||||
mod pathops;
|
||||
pub mod prefscmds;
|
||||
mod recolor;
|
||||
mod select;
|
||||
mod textedit;
|
||||
@@ -143,6 +144,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(panelcmds::specs());
|
||||
v.extend(buildcmds::specs());
|
||||
v.extend(brushsym::specs());
|
||||
v.extend(prefscmds::specs());
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
//! Preferences (Edit → Preferences): `prefs.get`, `prefs.set`, `prefs.reset`, `prefs.list`.
|
||||
//!
|
||||
//! [`PREF_SPECS`] is the single source of truth for every preference: its category, section,
|
||||
//! label and value kind. The Preferences dialog renders from it and `prefs.set` validates with it.
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use super::*;
|
||||
use crate::Prefs;
|
||||
|
||||
/// What kind of value a preference holds.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum PrefKind {
|
||||
Bool,
|
||||
/// A number in `min..=max` shown with `unit` ("pt", "px", "°", "%", "min", "").
|
||||
Num { min: f64, max: f64, unit: &'static str },
|
||||
/// A whole number in `min..=max`.
|
||||
Int { min: i64, max: i64 },
|
||||
/// One of (value, label).
|
||||
Choice(&'static [(&'static str, &'static str)]),
|
||||
/// `#rrggbb`.
|
||||
Color,
|
||||
Text,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PrefSpec {
|
||||
pub key: &'static str,
|
||||
pub category: &'static str,
|
||||
/// Group heading inside the category ("" = none).
|
||||
pub section: &'static str,
|
||||
pub label: &'static str,
|
||||
pub kind: PrefKind,
|
||||
}
|
||||
|
||||
/// Categories in Illustrator's order (Plug-ins & Scratch Disks is "Performance & Storage" here).
|
||||
pub const PREF_CATEGORIES: &[&str] = &[
|
||||
"General",
|
||||
"Selection & Anchor Display",
|
||||
"Type",
|
||||
"Units",
|
||||
"Guides & Grid",
|
||||
"Smart Guides",
|
||||
"Slices",
|
||||
"Hyphenation",
|
||||
"Performance & Storage",
|
||||
"User Interface",
|
||||
"Performance",
|
||||
"File Handling",
|
||||
"Clipboard Handling",
|
||||
"Appearance of Black",
|
||||
"Devices",
|
||||
];
|
||||
|
||||
pub const UNITS: &[(&str, &str)] = &[
|
||||
("points", "Points"),
|
||||
("picas", "Picas"),
|
||||
("inches", "Inches"),
|
||||
("millimeters", "Millimeters"),
|
||||
("centimeters", "Centimeters"),
|
||||
("pixels", "Pixels"),
|
||||
];
|
||||
const LINE_STYLE: &[(&str, &str)] = &[("lines", "Lines"), ("dots", "Dots")];
|
||||
const BLACK: &[(&str, &str)] = &[("accurate", "Display All Blacks Accurately"), ("rich", "Display All Blacks as Rich Black")];
|
||||
const BLACK_OUT: &[(&str, &str)] = &[("accurate", "Output All Blacks Accurately"), ("rich", "Output All Blacks as Rich Black")];
|
||||
|
||||
macro_rules! p {
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, bool) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Bool }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, num($min:expr, $max:expr, $unit:literal)) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Num { min: $min, max: $max, unit: $unit } }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, int($min:expr, $max:expr)) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Int { min: $min, max: $max } }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, choice($c:expr)) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Choice($c) }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, color) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Color }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, text) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Text }
|
||||
};
|
||||
}
|
||||
|
||||
pub const PREF_SPECS: &[PrefSpec] = &[
|
||||
// General
|
||||
p!("keyboardIncrement", "General", "", "Keyboard Increment", num(0.001, 1296.0, "pt")),
|
||||
p!("constrainAngle", "General", "", "Constrain Angle", num(-360.0, 360.0, "°")),
|
||||
p!("cornerRadius", "General", "", "Corner Radius", num(0.0, 1296.0, "pt")),
|
||||
p!("pasteOffset", "General", "", "Paste Offset", num(0.0, 1296.0, "pt")),
|
||||
p!("disableAutoAddDelete", "General", "Options", "Disable Auto Add/Delete", bool),
|
||||
p!("usePreciseCursors", "General", "Options", "Use Precise Cursors", bool),
|
||||
p!("showToolTips", "General", "Options", "Show Tool Tips", bool),
|
||||
p!("antiAliasedArtwork", "General", "Options", "Anti-aliased Artwork", bool),
|
||||
p!("selectSameTintPercent", "General", "Options", "Select Same Tint %", bool),
|
||||
p!("showHomeScreen", "General", "Options", "Show The Home Screen When No Documents Are Open", bool),
|
||||
p!("usePreviewBounds", "General", "Options", "Use Preview Bounds", bool),
|
||||
p!("displayPrintSize", "General", "Options", "Display Print Size at 100% Zoom", bool),
|
||||
p!("doubleClickToIsolate", "General", "Options", "Double Click To Isolate", bool),
|
||||
p!("transformPatternTiles", "General", "Options", "Transform Pattern Tiles", bool),
|
||||
p!("scaleCorners", "General", "Options", "Scale Corners", bool),
|
||||
p!("scaleStrokes", "General", "Options", "Scale Strokes & Effects", bool),
|
||||
p!("zoomWithMouseWheel", "General", "Options", "Zoom with Mouse Wheel", bool),
|
||||
// Selection & Anchor Display
|
||||
p!("selectionTolerance", "Selection & Anchor Display", "Selection", "Tolerance", num(1.0, 8.0, "px")),
|
||||
p!("objectSelectionByPathOnly", "Selection & Anchor Display", "Selection", "Object Selection by Path Only", bool),
|
||||
p!("snapToPointTolerance", "Selection & Anchor Display", "Selection", "Snap to Point", num(1.0, 8.0, "px")),
|
||||
p!("ctrlClickSelectsBehind", "Selection & Anchor Display", "Selection", "Command Click to Select Objects Behind", bool),
|
||||
p!("zoomToSelection", "Selection & Anchor Display", "Selection", "Zoom to Selection", bool),
|
||||
p!("moveLockedWithArtboard", "Selection & Anchor Display", "Selection", "Move Locked and Hidden Artwork with Artboard", bool),
|
||||
p!("anchorSize", "Selection & Anchor Display", "Anchor Points, Handle, and Bounding Box Display", "Size", int(1, 7)),
|
||||
p!(
|
||||
"handleStyle",
|
||||
"Selection & Anchor Display",
|
||||
"Anchor Points, Handle, and Bounding Box Display",
|
||||
"Handles",
|
||||
choice(&[("solid", "Solid"), ("hollow", "Hollow"), ("large", "Large")])
|
||||
),
|
||||
p!("highlightAnchorsOnHover", "Selection & Anchor Display", "Anchor Points, Handle, and Bounding Box Display", "Highlight anchors on mouse over", bool),
|
||||
p!(
|
||||
"showHandlesMultipleAnchors",
|
||||
"Selection & Anchor Display",
|
||||
"Anchor Points, Handle, and Bounding Box Display",
|
||||
"Show handles when multiple anchors are selected",
|
||||
bool
|
||||
),
|
||||
p!("hideCornerWidgetAbove", "Selection & Anchor Display", "Anchor Points, Handle, and Bounding Box Display", "Hide Corner Widget for angles greater than", num(0.0, 180.0, "°")),
|
||||
p!("penRubberBand", "Selection & Anchor Display", "Enable Rubber Band for", "Pen Tool", bool),
|
||||
p!("curvatureRubberBand", "Selection & Anchor Display", "Enable Rubber Band for", "Curvature Tool", bool),
|
||||
// Type
|
||||
p!("typeSizeIncrement", "Type", "", "Size/Leading", num(0.001, 1296.0, "pt")),
|
||||
p!("trackingIncrement", "Type", "", "Tracking", num(1.0, 1000.0, "/1000 em")),
|
||||
p!("baselineShiftIncrement", "Type", "", "Baseline Shift", num(0.001, 1296.0, "pt")),
|
||||
p!("showEastAsianOptions", "Type", "Language Options", "Show East Asian Options", bool),
|
||||
p!("showIndicOptions", "Type", "Language Options", "Show Indic Options", bool),
|
||||
p!("typeSelectionByPathOnly", "Type", "Options", "Type Object Selection by Path Only", bool),
|
||||
p!("fontNamesInEnglish", "Type", "Options", "Show Font Names in English", bool),
|
||||
p!("autoSizeAreaType", "Type", "Options", "Auto Size New Area Type", bool),
|
||||
p!("fontPreview", "Type", "Options", "Enable in-menu font previews", bool),
|
||||
p!("fontPreviewSize", "Type", "Options", "Font Preview Size", choice(&[("small", "Small"), ("medium", "Medium"), ("large", "Large")])),
|
||||
p!("recentFontsCount", "Type", "Options", "Number of Recent Fonts", int(1, 15)),
|
||||
p!("missingGlyphProtection", "Type", "Options", "Enable Missing Glyph Protection", bool),
|
||||
p!("highlightAlternateGlyphs", "Type", "Options", "Highlight Alternate Glyphs", bool),
|
||||
p!("placeholderText", "Type", "Options", "Fill New Type Objects With Placeholder Text", bool),
|
||||
// Units
|
||||
p!("unitsGeneral", "Units", "", "General", choice(UNITS)),
|
||||
p!("unitsStroke", "Units", "", "Stroke", choice(UNITS)),
|
||||
p!("unitsType", "Units", "", "Type", choice(UNITS)),
|
||||
p!("unitsAsianType", "Units", "", "East Asian Type", choice(UNITS)),
|
||||
p!("numbersWithoutUnitsArePoints", "Units", "", "Numbers Without Units Are Points", bool),
|
||||
p!("identifyObjectsBy", "Units", "", "Identify Objects By", choice(&[("objectName", "Object Name"), ("xmlId", "XML ID")])),
|
||||
// Guides & Grid
|
||||
p!("guideColor", "Guides & Grid", "Guides", "Color", color),
|
||||
p!("guideStyle", "Guides & Grid", "Guides", "Style", choice(LINE_STYLE)),
|
||||
p!("gridColor", "Guides & Grid", "Grid", "Color", color),
|
||||
p!("gridStyle", "Guides & Grid", "Grid", "Style", choice(LINE_STYLE)),
|
||||
p!("gridlineEvery", "Guides & Grid", "Grid", "Gridline every", num(1.0, 16383.0, "pt")),
|
||||
p!("gridSubdivisions", "Guides & Grid", "Grid", "Subdivisions", int(1, 1000)),
|
||||
p!("gridsInBack", "Guides & Grid", "Grid", "Grids In Back", bool),
|
||||
p!("showPixelGrid", "Guides & Grid", "Grid", "Show Pixel Grid (Above 600% Zoom)", bool),
|
||||
// Smart Guides
|
||||
p!("smartGuideColor", "Smart Guides", "Display Options", "Color", color),
|
||||
p!("alignmentGuides", "Smart Guides", "Display Options", "Alignment Guides", bool),
|
||||
p!("objectHighlighting", "Smart Guides", "Display Options", "Object Highlighting", bool),
|
||||
p!("transformToolsGuides", "Smart Guides", "Display Options", "Transform Tools", bool),
|
||||
p!("constructionGuides", "Smart Guides", "Display Options", "Construction Guides", bool),
|
||||
p!(
|
||||
"constructionAngles",
|
||||
"Smart Guides",
|
||||
"Display Options",
|
||||
"Angles",
|
||||
choice(&[
|
||||
("90° & 45° Angles", "90° & 45° Angles"),
|
||||
("30° Angles", "30° Angles"),
|
||||
("45° Angles", "45° Angles"),
|
||||
("60° Angles", "60° Angles"),
|
||||
("90° Angles", "90° Angles"),
|
||||
("90° & 45° & 30° Angles", "90° & 45° & 30° Angles"),
|
||||
])
|
||||
),
|
||||
p!("anchorPathLabels", "Smart Guides", "Display Options", "Anchor/Path Labels", bool),
|
||||
p!("measurementLabels", "Smart Guides", "Display Options", "Measurement Labels", bool),
|
||||
p!("spacingGuides", "Smart Guides", "Display Options", "Spacing Guides", bool),
|
||||
p!("snappingTolerance", "Smart Guides", "", "Snapping Tolerance", num(0.0, 40.0, "pt")),
|
||||
// Slices
|
||||
p!("showSliceNumbers", "Slices", "", "Show Slice Numbers", bool),
|
||||
p!("sliceLineColor", "Slices", "", "Line Color", color),
|
||||
// Hyphenation
|
||||
p!(
|
||||
"hyphenationLanguage",
|
||||
"Hyphenation",
|
||||
"",
|
||||
"Default Language",
|
||||
choice(&[
|
||||
("English: USA", "English: USA"),
|
||||
("English: UK", "English: UK"),
|
||||
("French", "French"),
|
||||
("German", "German"),
|
||||
("Spanish", "Spanish"),
|
||||
("Italian", "Italian"),
|
||||
("Dutch", "Dutch"),
|
||||
("Portuguese", "Portuguese")
|
||||
])
|
||||
),
|
||||
p!("hyphenationExceptions", "Hyphenation", "", "Exceptions (comma separated)", text),
|
||||
// Performance & Storage
|
||||
p!("pluginsFolder", "Performance & Storage", "Plug-ins", "Additional Plug-ins Folder", text),
|
||||
p!("scratchPrimary", "Performance & Storage", "Scratch Disks", "Primary", text),
|
||||
p!("scratchSecondary", "Performance & Storage", "Scratch Disks", "Secondary", text),
|
||||
// User Interface
|
||||
p!(
|
||||
"uiBrightness",
|
||||
"User Interface",
|
||||
"",
|
||||
"Brightness",
|
||||
choice(&[("dark", "Dark"), ("mediumDark", "Medium Dark"), ("mediumLight", "Medium Light"), ("light", "Light")])
|
||||
),
|
||||
p!("canvasColor", "User Interface", "", "Canvas Color", choice(&[("matchUi", "Match User Interface Brightness"), ("white", "White")])),
|
||||
p!("autoCollapseIconPanels", "User Interface", "", "Auto-Collapse Iconic Panels", bool),
|
||||
p!("openDocumentsAsTabs", "User Interface", "", "Open Documents As Tabs", bool),
|
||||
p!("largeTabs", "User Interface", "", "Large Tabs", bool),
|
||||
p!("uiScaling", "User Interface", "UI Scaling", "Scale", num(0.75, 2.0, "×")),
|
||||
p!("scaleCursorWithUi", "User Interface", "UI Scaling", "Scale Cursor Proportional to UI", bool),
|
||||
// Performance
|
||||
p!("gpuPerformance", "Performance", "GPU Performance", "GPU Performance", bool),
|
||||
p!("animatedZoom", "Performance", "GPU Performance", "Animated Zoom", bool),
|
||||
p!("historyStates", "Performance", "", "History States", int(5, 1000)),
|
||||
p!("realTimeDrawing", "Performance", "", "Real-time Drawing and Editing", bool),
|
||||
p!("renderThreads", "Performance", "", "Render Threads (-1 = Automatic)", int(-1, 64)),
|
||||
// File Handling
|
||||
p!("backgroundSave", "File Handling", "", "Enable Background Save", bool),
|
||||
p!("backgroundExport", "File Handling", "", "Enable Background Export", bool),
|
||||
p!("autosaveRecovery", "File Handling", "Data Recovery", "Automatically Save Recovery Data", bool),
|
||||
p!("autosaveInterval", "File Handling", "Data Recovery", "Every (minutes)", int(1, 60)),
|
||||
p!("recoveryFolder", "File Handling", "Data Recovery", "Folder", text),
|
||||
p!("recoveryOffForComplex", "File Handling", "Data Recovery", "Turn off Data Recovery for complex documents", bool),
|
||||
p!("recentFilesCount", "File Handling", "Files", "Number of Recent Files to Display", int(0, 30)),
|
||||
p!("lowResProxyEps", "File Handling", "Files", "Use Low Resolution Proxy for Linked EPS", bool),
|
||||
p!("antiAliasedBitmaps", "File Handling", "Files", "Display Bitmaps as Anti-aliased Images in Pixel Preview", bool),
|
||||
p!(
|
||||
"updateLinks",
|
||||
"File Handling",
|
||||
"Files",
|
||||
"Update Links",
|
||||
choice(&[("automatically", "Automatically"), ("manually", "Manually"), ("askWhenModified", "Ask When Modified")])
|
||||
),
|
||||
// Clipboard Handling
|
||||
p!("copyAsSvg", "Clipboard Handling", "On Copy", "Include SVG Code", bool),
|
||||
p!("copyAsPdf", "Clipboard Handling", "On Copy", "PDF", bool),
|
||||
p!("copyAicb", "Clipboard Handling", "On Copy", "AICB (no transparency support)", bool),
|
||||
p!("aicbMode", "Clipboard Handling", "On Copy", "AICB", choice(&[("preservePaths", "Preserve Paths"), ("preserveAppearance", "Preserve Appearance and Overprints")])),
|
||||
p!("pasteTextFormatting", "Clipboard Handling", "On Paste", "When pasting text", choice(&[("keep", "Keep Formatting"), ("plain", "Keep Plain Text")])),
|
||||
// Appearance of Black
|
||||
p!("blackOnScreen", "Appearance of Black", "", "On Screen", choice(BLACK)),
|
||||
p!("blackOutput", "Appearance of Black", "", "Printing / Exporting", choice(BLACK_OUT)),
|
||||
// Devices
|
||||
p!("touchWorkspace", "Devices", "", "Enable Touch Workspace", bool),
|
||||
p!("touchGestures", "Devices", "", "Enable Touch Gestures", bool),
|
||||
];
|
||||
|
||||
pub fn spec(key: &str) -> Option<&'static PrefSpec> {
|
||||
PREF_SPECS.iter().find(|s| s.key == key)
|
||||
}
|
||||
|
||||
/// Validate (and normalize) a value for `key`. Numbers may be given as strings; booleans as
|
||||
/// "true"/"false"; choices by value or label (case-insensitive).
|
||||
pub fn validate(key: &str, v: &Value) -> std::result::Result<Value, String> {
|
||||
let sp = spec(key).ok_or_else(|| format!("unknown preference `{key}`"))?;
|
||||
let num = || v.as_f64().or_else(|| v.as_str().and_then(|s| s.trim().trim_end_matches(|c: char| c.is_alphabetic() || c == '°').trim().parse().ok()));
|
||||
match sp.kind {
|
||||
PrefKind::Bool => match v {
|
||||
Value::Bool(b) => Ok(json!(b)),
|
||||
Value::String(s) if s == "true" || s == "false" => Ok(json!(s == "true")),
|
||||
_ => Err(format!("`{key}` must be true or false")),
|
||||
},
|
||||
PrefKind::Num { min, max, .. } => match num() {
|
||||
Some(x) if x.is_finite() && (min..=max).contains(&x) => Ok(json!(x)),
|
||||
Some(x) => Err(format!("`{key}` must be between {min} and {max} (got {x})")),
|
||||
None => Err(format!("`{key}` must be a number")),
|
||||
},
|
||||
PrefKind::Int { min, max } => match num() {
|
||||
Some(x) if x.fract() == 0.0 && (min as f64..=max as f64).contains(&x) => Ok(json!(x as i64)),
|
||||
Some(x) => Err(format!("`{key}` must be a whole number between {min} and {max} (got {x})")),
|
||||
None => Err(format!("`{key}` must be a whole number")),
|
||||
},
|
||||
PrefKind::Choice(opts) => {
|
||||
let s = v.as_str().ok_or_else(|| format!("`{key}` must be a string"))?;
|
||||
opts.iter()
|
||||
.find(|(val, label)| val.eq_ignore_ascii_case(s) || label.eq_ignore_ascii_case(s))
|
||||
.map(|(val, _)| json!(val))
|
||||
.ok_or_else(|| format!("`{key}` must be one of {}", opts.iter().map(|o| o.0).collect::<Vec<_>>().join(", ")))
|
||||
}
|
||||
PrefKind::Color => {
|
||||
let s = v.as_str().unwrap_or("");
|
||||
let hex = s.trim_start_matches('#');
|
||||
if hex.len() == 6 && hex.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||
Ok(json!(format!("#{}", hex.to_ascii_lowercase())))
|
||||
} else {
|
||||
Err(format!("`{key}` must be a #rrggbb colour"))
|
||||
}
|
||||
}
|
||||
PrefKind::Text => match v {
|
||||
Value::String(s) => Ok(json!(s)),
|
||||
_ => Err(format!("`{key}` must be a string")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
impl Prefs {
|
||||
pub fn to_json(&self) -> Value {
|
||||
serde_json::to_value(self).unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
/// Set several validated values at once; on any error nothing changes.
|
||||
pub fn set_values(&mut self, values: &Map<String, Value>) -> std::result::Result<(), String> {
|
||||
let mut obj = self.to_json();
|
||||
for (k, v) in values {
|
||||
obj[k.as_str()] = validate(k, v)?;
|
||||
}
|
||||
*self = serde_json::from_value(obj).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Values of one category (`None` = all) reset to defaults.
|
||||
pub fn reset(&mut self, category: Option<&str>) {
|
||||
let d = Prefs::default().to_json();
|
||||
let mut obj = self.to_json();
|
||||
for sp in PREF_SPECS.iter().filter(|s| category.is_none_or(|c| c.eq_ignore_ascii_case(s.category))) {
|
||||
obj[sp.key] = d[sp.key].clone();
|
||||
}
|
||||
if let Ok(p) = serde_json::from_value(obj) {
|
||||
*self = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Replace the preferences and push the ones with engine-side consumers into open documents
|
||||
/// and the renderer (grid, history depth, render threads).
|
||||
pub fn apply_prefs(&mut self, p: Prefs) {
|
||||
let grid_changed = p.gridline_every != self.prefs.gridline_every || p.grid_subdivisions != self.prefs.grid_subdivisions;
|
||||
let history_changed = p.history_states != self.prefs.history_states;
|
||||
self.prefs = p;
|
||||
drawcraft_render::set_default_threads(u16::try_from(self.prefs.render_threads).ok());
|
||||
for st in &mut self.docs {
|
||||
if history_changed {
|
||||
st.history.limit = self.prefs.history_states as usize;
|
||||
let over = st.history.undo.len().saturating_sub(st.history.limit);
|
||||
st.history.undo.drain(..over);
|
||||
}
|
||||
if grid_changed {
|
||||
let d = std::sync::Arc::make_mut(&mut st.doc);
|
||||
d.grid.spacing = self.prefs.gridline_every;
|
||||
d.grid.subdivisions = self.prefs.grid_subdivisions;
|
||||
st.revision += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(query "prefs.get", "Get Preferences", [], None, "{key?} → the value of one preference, or {prefs: {…all…}}", always, get),
|
||||
cmd!(
|
||||
query "prefs.set",
|
||||
"Set Preference",
|
||||
[],
|
||||
None,
|
||||
"{key, value} or {values: {key: value, …}} set preferences (validated; see prefs.list)",
|
||||
always,
|
||||
set
|
||||
),
|
||||
cmd!(query "prefs.reset", "Reset Preferences", [], None, "{category?} reset one category (or everything) to defaults", always, reset),
|
||||
cmd!(query "prefs.list", "List Preferences", [], None, "{} → [{key, category, section, label, kind, value, …}] every preference", always, list),
|
||||
]
|
||||
}
|
||||
|
||||
fn get(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let all = s.prefs.to_json();
|
||||
match str_param(p, "key") {
|
||||
Some(k) if spec(k).is_some() => Ok(all[k].clone()),
|
||||
Some(k) => Err(bad("prefs.get", format!("unknown preference `{k}`"))),
|
||||
None => Ok(json!({ "prefs": all })),
|
||||
}
|
||||
}
|
||||
|
||||
fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut values = p.get("values").and_then(Value::as_object).cloned().unwrap_or_default();
|
||||
if let Some(k) = str_param(p, "key") {
|
||||
values.insert(k.to_string(), p.get("value").cloned().ok_or_else(|| bad("prefs.set", "missing `value`"))?);
|
||||
}
|
||||
if values.is_empty() {
|
||||
return Err(bad("prefs.set", "give {key, value} or {values}"));
|
||||
}
|
||||
let mut next = s.prefs.clone();
|
||||
next.set_values(&values).map_err(|e| bad("prefs.set", e))?;
|
||||
s.apply_prefs(next);
|
||||
let all = s.prefs.to_json();
|
||||
Ok(Value::Object(values.keys().map(|k| (k.clone(), all[k.as_str()].clone())).collect()))
|
||||
}
|
||||
|
||||
fn reset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cat = str_param(p, "category");
|
||||
if let Some(c) = cat
|
||||
&& !PREF_CATEGORIES.iter().any(|x| x.eq_ignore_ascii_case(c))
|
||||
{
|
||||
return Err(bad("prefs.reset", format!("unknown category `{c}`")));
|
||||
}
|
||||
let mut next = s.prefs.clone();
|
||||
next.reset(cat);
|
||||
s.apply_prefs(next);
|
||||
Ok(json!({ "prefs": s.prefs.to_json() }))
|
||||
}
|
||||
|
||||
fn list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let all = s.prefs.to_json();
|
||||
Ok(Value::Array(
|
||||
PREF_SPECS
|
||||
.iter()
|
||||
.map(|sp| {
|
||||
let mut o = json!({"key": sp.key, "category": sp.category, "section": sp.section, "label": sp.label, "value": all[sp.key]});
|
||||
match sp.kind {
|
||||
PrefKind::Bool => o["kind"] = json!("bool"),
|
||||
PrefKind::Num { min, max, unit } => {
|
||||
o["kind"] = json!("number");
|
||||
o["min"] = json!(min);
|
||||
o["max"] = json!(max);
|
||||
o["unit"] = json!(unit);
|
||||
}
|
||||
PrefKind::Int { min, max } => {
|
||||
o["kind"] = json!("integer");
|
||||
o["min"] = json!(min);
|
||||
o["max"] = json!(max);
|
||||
}
|
||||
PrefKind::Choice(c) => {
|
||||
o["kind"] = json!("choice");
|
||||
o["options"] = json!(c.iter().map(|x| x.0).collect::<Vec<_>>());
|
||||
}
|
||||
PrefKind::Color => o["kind"] = json!("color"),
|
||||
PrefKind::Text => o["kind"] = json!("text"),
|
||||
}
|
||||
o
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user