Library panels: Locate Library… reads a missing library whose panel isn't persistent

ui.libraryPanel.locate decided whether a panel's library was missing
through missing_file, which answers only for a persistent panel. A
command that turns Persistent off leaves the panel open on its missing
library until the next drawn frame closes it, and while the window is
hidden the app draws no frame between control requests. Locate
Library… on that panel answered "isn't missing its library" and read
no file.

The check now asks whether the panel's library can be found, the
lookup the tab label and the per-frame tidy use. A panel whose library
is there is still refused, persistent or not.

window.swatchLibrary and window.graphicStyleLibrary had the same
check: given the library id of such a panel, they answered "no
library". They now bring the open panel forward and answer its file's
name with count 0; the panel still closes at the next tidy.

Docs: docs/mcp.md (ui.libraryPanel.locate, window.swatchLibrary).
This commit is contained in:
dotCooCoo
2026-10-22 11:34:47 +07:00
committed by bflatastic
parent 0834be2989
commit de1182fbe3
2 changed files with 74 additions and 12 deletions
+69 -8
View File
@@ -244,9 +244,17 @@ fn missing_file(app: &VectorcraftApp, k: Kind, id: &str) -> Option<String> {
(k.name)(app, id).is_none().then(|| s.path.clone().unwrap_or_else(|| id.to_string()))
}
/// The name of missing library `id` of `k` ([`missing_file`]): its file's name without the extension.
/// The file of library `id` of `k` when its panel is open and the library can't be found,
/// Persistent or not (a panel whose Persistent a command turned off stays open until [`tidy`]).
fn open_missing_file(app: &VectorcraftApp, k: Kind, id: &str) -> Option<String> {
let s = settings(&app.ui, k.kind, id).filter(|_| is_open(&app.ui, &panel_id(k.kind, id)))?;
(k.name)(app, id).is_none().then(|| s.path.clone().unwrap_or_else(|| id.to_string()))
}
/// The name of missing library `id` of `k` ([`missing_file`], or [`open_missing_file`]): its file's
/// name without the extension.
fn missing_name(app: &VectorcraftApp, k: Kind, id: &str) -> Option<String> {
let file = missing_file(app, k, id)?;
let file = missing_file(app, k, id).or_else(|| open_missing_file(app, k, id))?;
Some(std::path::Path::new(&file).file_stem().map_or_else(|| file.clone(), |s| s.to_string_lossy().into_owned()))
}
@@ -393,9 +401,10 @@ pub(crate) fn library_name<'a>(id: &str, name: &'a str) -> &'a str {
}
/// The opener of library `K`'s libraries (`window.swatchLibrary {library}`): open library `library`
/// (an id or a name, or the id of a missing library's persistent panel, [`missing_file`]) in its
/// own panel, floating, or bring its panel forward when it is open; `null` closes every panel of
/// `K`'s libraries. → {open, panel, name, count} (a missing library: its [`missing_name`], count 0)
/// (an id or a name, or the id of a missing library's persistent or open panel, [`missing_name`])
/// in its own panel, floating, or bring its panel forward when it is open; `null` closes every
/// panel of `K`'s libraries. → {open, panel, name, count} (a missing library: its
/// [`missing_name`], count 0)
pub(crate) fn open_command<K: LibraryKind>(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let Some(key) = p.get("library").filter(|v| !v.is_null()) else {
for panel in open_panels(&app.ui).into_iter().filter(|p| parse(p).is_some_and(|(k, _)| k.kind == K::KIND)) {
@@ -488,8 +497,8 @@ pub(crate) fn command(app: &mut VectorcraftApp, p: &Value) -> Result<Value, Stri
}))
}
/// `ui.libraryPanel.locate {panel, path?}`: Locate Library… of a library panel whose library is
/// missing ([`missing_file`]; any other panel is an error). Load the library at `path`, else one
/// `ui.libraryPanel.locate {panel, path?}`: Locate Library… of a library panel whose library can't
/// be found, Persistent or not (any other panel is an error). Load the library at `path`, else one
/// picked in an open dialog (on the web the picked file arrives later and opens in a panel of its
/// own, as Other Library…'s does), and show it in open panel `panel`, which keeps its view, Show
/// Find Field and Persistent. When the file's library has a panel open already, `panel` closes and
@@ -501,7 +510,7 @@ pub(crate) fn locate(app: &mut VectorcraftApp, p: &Value) -> Result<Value, Strin
if !is_open(&app.ui, given) {
return Err(format!("library panel `{given}` isn't open"));
}
if missing_file(app, k, id).is_none() {
if (k.name)(app, id).is_some() {
return Err(format!("library panel `{given}` isn't missing its library"));
}
let path = p.get("path").and_then(Value::as_str).map(str::to_string);
@@ -1333,6 +1342,58 @@ mod tests {
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn locate_library_reads_a_missing_library_whose_panel_is_no_longer_persistent() {
let dir = std::env::temp_dir().join(format!("vc-locate-not-persistent-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("moved")).unwrap();
let palette = "GIMP Palette\nName: Brand\n#\n255 0 0\tRed\n";
let gpl = dir.join("Brand.gpl");
std::fs::write(&gpl, palette).unwrap();
let mut app = app();
let brand = panel_of(app.run("window.swatchLibrary.other", json!({"path": gpl.to_string_lossy()})).unwrap());
app.run("ui.libraryPanel", json!({"panel": brand, "persistent": true})).unwrap();
std::fs::remove_file(&gpl).unwrap();
let mut next = restarted(&app);
// After a command turns Persistent off, the panel stays open on its missing library until the next frame's tidy.
next.run("ui.libraryPanel", json!({"panel": brand, "persistent": false})).unwrap();
assert!(is_open(&next.ui, &brand));
let moved = dir.join("moved").join("Brand.gpl");
std::fs::write(&moved, palette).unwrap();
let r = next.run("ui.libraryPanel.locate", json!({"panel": brand, "path": moved.to_string_lossy()})).unwrap();
let located = panel_id("swatches", &format!("loaded/{}", moved.to_string_lossy()));
assert_eq!((r["panel"].as_str(), r["persistent"].as_bool()), (Some(located.as_str()), Some(false)));
assert!(!is_open(&next.ui, &brand) && is_open(&next.ui, &located), "{}", next.ui.status);
let (_, id) = parse(&located).unwrap();
assert!(SwatchLibraries::get(&next, id).is_some_and(|(_, lib)| lib.swatch("Red").is_some()));
tidy(&mut next);
assert!(is_open(&next.ui, &located), "its library is there now");
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn an_open_panel_whose_library_is_missing_is_brought_forward_by_its_id_when_not_persistent() {
let dir = std::env::temp_dir().join(format!("vc-open-not-persistent-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let gpl = dir.join("Brand.gpl");
std::fs::write(&gpl, "GIMP Palette\nName: Brand\n#\n255 0 0\tRed\n").unwrap();
let mut app = app();
let brand = panel_of(app.run("window.swatchLibrary.other", json!({"path": gpl.to_string_lossy()})).unwrap());
app.run("ui.libraryPanel", json!({"panel": brand, "persistent": true})).unwrap();
std::fs::remove_file(&gpl).unwrap();
let mut next = restarted(&app);
// After a command turns Persistent off, the panel stays open on its missing library until the next frame's tidy.
next.run("ui.libraryPanel", json!({"panel": brand, "persistent": false})).unwrap();
let (_, gone) = parse(&brand).unwrap();
let r = next.run("window.swatchLibrary", json!({"library": gone})).unwrap();
assert_eq!((r["panel"].as_str(), r["name"].as_str(), r["count"].as_u64()), (Some(brand.as_str()), Some("Brand"), Some(0)));
assert!(is_open(&next.ui, &brand));
tidy(&mut next);
assert!(!is_open(&next.ui, &brand), "a panel that isn't persistent still closes");
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn previous_and_next_show_the_neighbouring_library_in_the_same_panel() {
let mut app = app();
+5 -4
View File
@@ -1719,11 +1719,12 @@ under `ui.library_panels`.
`ui.libraryPanel.locate {panel, path?}` is Locate Library…: it loads the library at `path` (without one, it
asks for a file) as Other Library… does and shows it in the open panel `panel`, which keeps its view, find
field and Persistent, and answers as `ui.libraryPanel` does. When the file's library already has a panel open,
`panel` closes and is forgotten, and that panel is brought forward, made persistent when `panel` was. On a
panel whose library isn't missing, it answers an error without asking for or loading a file, and the panel
keeps its library.
`panel` closes and is forgotten, and that panel is brought forward, made persistent when `panel` was. It
runs on any open panel whose library is missing, with Persistent on or off. On a panel whose library isn't
missing, it answers an error without asking for or loading a file, and the panel keeps its library.
`window.swatchLibrary` and `window.graphicStyleLibrary` also take a missing library's id, so a closed missing
panel opens again; they answer its file's name without the extension and `count: 0`.
panel opens again, and an open panel whose library is missing is brought forward with Persistent on or off;
they answer its file's name without the extension and `count: 0`.
```json
{"name":"invoke_menu","arguments":{"command":"window.swatchLibrary","params":{"library":"pastels"}}}