Library panels: a persistent panel whose file is gone stays listed as missing

At startup, a persistent swatch or graphic style library panel whose
file could not be read was closed and left out of the preferences,
with only a status-bar note. The next preferences save dropped it, so
moving or renaming the file once lost the panel and its settings.

Such a panel now reopens where the saved layout had it and stays in
the preferences. Its tab shows the file's name without the extension,
and its body shows "Missing:" with the file's path and a Locate
Library… button. The new command ui.libraryPanel.locate {panel,
path?} loads the library from the file given, or from one picked in
an open dialog, and shows it in the same panel, which keeps its view,
Show Find Field and Persistent. When the file's library is open in
another panel, the missing panel closes and that panel is brought
forward and made persistent.

A persistent panel whose User Defined library file is deleted while
the app runs shows the library as missing the same way; other panels
still close. window.swatchLibrary and window.graphicStyleLibrary take
a missing library's id, so a closed missing panel opens again.
Previous Library and Next Library are dimmed in a missing panel's
menu.

Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-21 23:14:32 +07:00
committed by bflatastic
parent 7d4ae444b6
commit b4fef5dc98
30 changed files with 306 additions and 26 deletions
+1
View File
@@ -5230,6 +5230,7 @@ theme Appearance Външен вид
@msg “{name}” is not a file „{name}“ не е файл
Enable Guides for 9-Slice Scaling Включване на водачи за 9-slice мащабиране
Persistent Постоянна
Locate Library… Намиране на библиотека…
Library Panel Options Опции на панела с библиотека
Artboard {n} of {total} Работна област {n} от {total}
Encoding and writing the file… Кодиране и записване на файла…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5269,6 +5269,7 @@ mark-style Japanese Japonské
@msg the image's pixels can't be read pixely obrazu nelze přečíst
Persistent Trvalé
Library Panel Options Možnosti panelu knihovny
Locate Library… Najít knihovnu…
@msg Persistent libraries not found: {files} Trvalé knihovny nebyly nalezeny: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` není panel knihovny (viz window.swatchLibrary)
@msg library panel `{given}` isn't open panel knihovny `{given}` není otevřený
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5229,6 +5229,7 @@ swatch Bright Violet Kraftig violet
Thread Text Sammenkæd tekst
Typing Indtastning
Persistent Vedvarende
Locate Library… Lokalisér bibliotek…
Library Panel Options Indstillinger for bibliotekspanel
Artboard {n} of {total} Tegneflade {n} af {total}
Encoding and writing the file… Koder og skriver filen…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4850,6 +4850,7 @@ effect Soft Weich
@msg the image's pixels can't be read die Pixel des Bildes können nicht gelesen werden
Persistent Dauerhaft
Library Panel Options Optionen für das Bibliotheksbedienfeld
Locate Library… Bibliothek ausfindig machen…
@msg Persistent libraries not found: {files} Dauerhafte Bibliotheken nicht gefunden: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` ist kein Bibliotheksbedienfeld (siehe window.swatchLibrary)
@msg library panel `{given}` isn't open Bibliotheksbedienfeld `{given}` ist nicht geöffnet
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5221,6 +5221,7 @@ effect Points Σημεία
@msg the image's pixels can't be read τα εικονοστοιχεία της εικόνας δεν μπορούν να διαβαστούν
Persistent Μόνιμη
Library Panel Options Επιλογές πίνακα βιβλιοθήκης
Locate Library… Εντοπισμός βιβλιοθήκης…
@msg Persistent libraries not found: {files} Δεν βρέθηκαν μόνιμες βιβλιοθήκες: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) Το `{given}` δεν είναι πίνακας βιβλιοθήκης (δείτε window.swatchLibrary)
@msg library panel `{given}` isn't open ο πίνακας βιβλιοθήκης `{given}` δεν είναι ανοιχτός
Can't render this file because it contains an unexpected character in line 2021 and column 19.
+1
View File
@@ -5211,6 +5211,7 @@ mark-style Japanese Japonesas
@msg the image's pixels can't be read no se pueden leer los píxeles de la imagen
Persistent Persistente
Library Panel Options Opciones del panel de biblioteca
Locate Library… Localizar biblioteca…
@msg Persistent libraries not found: {files} No se encontraron bibliotecas persistentes: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` no es un panel de biblioteca (consulte window.swatchLibrary)
@msg library panel `{given}` isn't open el panel de biblioteca `{given}` no está abierto
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5233,6 +5233,7 @@ mark-style Japanese Japanilainen
@msg the image's pixels can't be read kuvan pikseleitä ei voi lukea
Persistent Pysyvä
Library Panel Options Kirjastopaneelin asetukset
Locate Library… Paikanna kirjasto…
@msg Persistent libraries not found: {files} Pysyviä kirjastoja ei löytynyt: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` ei ole kirjastopaneeli (katso window.swatchLibrary)
@msg library panel `{given}` isn't open kirjastopaneeli `{given}` ei ole auki
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5268,6 +5268,7 @@ theme Appearance Apparence
@msg the image's pixels can't be read impossible de lire les pixels de l’image
Persistent Persistante
Library Panel Options Options du panneau de bibliothèque
Locate Library… Localiser la bibliothèque…
@msg Persistent libraries not found: {files} Bibliothèques persistantes introuvables : {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` n’est pas un panneau de bibliothèque (voir window.swatchLibrary)
@msg library panel `{given}` isn't open le panneau de bibliothèque `{given}` n’est pas ouvert
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5255,6 +5255,7 @@ imageTrace Outline Körvonal
@msg the image's pixels can't be read a kép képpontjai nem olvashatók
Persistent Állandó
Library Panel Options Könyvtárpanel beállításai
Locate Library… Könyvtár megkeresése…
@msg Persistent libraries not found: {files} Nem találhatók állandó könyvtárak: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) A(z) `{given}` nem könyvtárpanel (lásd: window.swatchLibrary)
@msg library panel `{given}` isn't open a(z) `{given}` könyvtárpanel nincs megnyitva
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5229,6 +5229,7 @@ swatch Bright Violet Violet cerah
Thread Text Untai teks
Typing Mengetik
Persistent Persisten
Locate Library… Temukan pustaka…
Library Panel Options Opsi panel pustaka
Artboard {n} of {total} Artboard {n} dari {total}
Encoding and writing the file… Mengodekan dan menulis berkas…
Can't render this file because it contains an unexpected character in line 3781 and column 19.
+1
View File
@@ -4883,6 +4883,7 @@ lighting Light Luce
@msg the image's pixels can't be read impossibile leggere i pixel dell’immagine
Persistent Persistente
Library Panel Options Opzioni pannello libreria
Locate Library… Individua libreria…
@msg Persistent libraries not found: {files} Librerie persistenti non trovate: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` non è un pannello libreria (vedi window.swatchLibrary)
@msg library panel `{given}` isn't open il pannello libreria `{given}` non è aperto
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4923,6 +4923,7 @@ lighting Light ライト
@msg the image's pixels can't be read 画像のピクセルを読み取れません
Persistent 永続
Library Panel Options ライブラリパネルオプション
Locate Library… ライブラリを検索…
@msg Persistent libraries not found: {files} 永続ライブラリが見つかりません: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` はライブラリパネルではありません (window.swatchLibrary を参照)
@msg library panel `{given}` isn't open ライブラリパネル `{given}` は開いていません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5027,6 +5027,7 @@ imageTrace Outline კონტური
Last Artboard ბოლო სამუშაო დაფა
Levels დონეები
Library Panel Options ბიბლიოთეკის პანელის პარამეტრები
Locate Library… ბიბლიოთეკის მოძებნა…
Lights სინათლის წყაროები
Link JavaScript Files… JavaScript ფაილების დაკავშირება…
Live Shape ცოცხალი ფიგურა
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4846,6 +4846,7 @@ lighting Light 광원
@msg the image's pixels can't be read 이미지의 픽셀을 읽을 수 없습니다
Persistent 지속
Library Panel Options 라이브러리 패널 옵션
Locate Library… 라이브러리 찾기…
@msg Persistent libraries not found: {files} 지속 라이브러리를 찾을 수 없습니다: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}`은(는) 라이브러리 패널이 아닙니다(window.swatchLibrary 참조)
@msg library panel `{given}` isn't open 라이브러리 패널 `{given}`이(가) 열려 있지 않습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5242,6 +5242,7 @@ swatch Bright Violet Felviolet
Typing Typen
Persistent Permanent
Library Panel Options Opties voor bibliotheekpaneel
Locate Library… Bibliotheek lokaliseren…
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Opslaan schrijft {_1}, niet {_2}: gebruik file.saveAsTemplate om het te schrijven
@msg `{key}` must be a #rrggbb color `{key}` moet een #rrggbb-kleur zijn
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5121,6 +5121,7 @@ mark-style Japanese Japońskie
@msg the image's pixels can't be read nie można odczytać pikseli obrazu
Persistent Trwały
Library Panel Options Opcje panelu biblioteki
Locate Library… Znajdź bibliotekę…
@msg Persistent libraries not found: {files} Nie znaleziono trwałych bibliotek: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` nie jest panelem biblioteki (zobacz window.swatchLibrary)
@msg library panel `{given}` isn't open panel biblioteki `{given}` nie jest otwarty
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5186,6 +5186,7 @@ mark-style Japanese Japonesas
@msg the image's pixels can't be read não é possível ler os pixels da imagem
Persistent Persistente
Library Panel Options Opções do Painel de Biblioteca
Locate Library… Localizar Biblioteca…
@msg Persistent libraries not found: {files} Bibliotecas persistentes não encontradas: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` não é um painel de biblioteca (consulte window.swatchLibrary)
@msg library panel `{given}` isn't open o painel de biblioteca `{given}` não está aberto
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -4879,6 +4879,7 @@ lighting Light Свет
@msg the image's pixels can't be read не удаётся прочитать пиксели изображения
Persistent Постоянная
Library Panel Options Параметры панели библиотеки
Locate Library… Найти библиотеку…
@msg Persistent libraries not found: {files} Постоянные библиотеки не найдены: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` — не панель библиотеки (см. window.swatchLibrary)
@msg library panel `{given}` isn't open панель библиотеки `{given}` не открыта
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5225,6 +5225,7 @@ theme Appearance Utseende
@msg the image's pixels can't be read bildens pixlar kan inte läsas
Persistent Beständig
Library Panel Options Alternativ för bibliotekspanelen
Locate Library… Hitta bibliotek…
@msg Persistent libraries not found: {files} Beständiga bibliotek hittades inte: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` är inte en bibliotekspanel (se window.swatchLibrary)
@msg library panel `{given}` isn't open bibliotekspanelen `{given}` är inte öppen
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -4863,6 +4863,7 @@ lighting Light Світло
@msg the image's pixels can't be read не вдається прочитати пікселі зображення
Persistent Постійна
Library Panel Options Параметри панелі бібліотеки
Locate Library… Знайти бібліотеку…
@msg Persistent libraries not found: {files} Постійні бібліотеки не знайдено: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` — не панель бібліотеки (див. window.swatchLibrary)
@msg library panel `{given}` isn't open панель бібліотеки `{given}` не відкрита
Can't render this file because it contains an unexpected character in line 726 and column 40.
+1
View File
@@ -4863,6 +4863,7 @@ theme Appearance Giao diện
@msg the image's pixels can't be read không đọc được các điểm ảnh của ảnh
Persistent Lưu giữ
Library Panel Options Tùy chọn bảng thư viện
Locate Library… Xác định vị trí thư viện…
@msg Persistent libraries not found: {files} Không tìm thấy thư viện lưu giữ: {files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` không phải là bảng thư viện (xem window.swatchLibrary)
@msg library panel `{given}` isn't open bảng thư viện `{given}` chưa được mở
Can't render this file because it contains an unexpected character in line 3981 and column 10.
+1
View File
@@ -5101,6 +5101,7 @@ lighting Light 光源
@msg the image's pixels can't be read 无法读取图像的像素
Persistent 持久
Library Panel Options 库面板选项
Locate Library… 查找库…
@msg Persistent libraries not found: {files} 找不到持久库:{files}
@msg `{given}` isn't a library panel (see window.swatchLibrary) `{given}` 不是库面板 (请参阅 window.swatchLibrary)
@msg library panel `{given}` isn't open 库面板 `{given}` 未打开
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4901,6 +4901,7 @@ lighting Light 光源
Light/Dark Balance 明暗平衡
Enable Guides for 9-Slice Scaling 啟用 9-slice 縮放參考線
Persistent 持久
Locate Library… 尋找資料庫…
Library Panel Options 資料庫面板選項
Artboard {n} of {total} 工作區域 {n} / {total}
Encoding and writing the file… 正在編碼並寫入檔案…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+9 -2
View File
@@ -508,7 +508,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"window.swatchLibrary",
"Swatch Library",
"",
"{library: id or name (see swatch.library.list) | null (close every swatch library panel)} open the swatch library in a read-only panel of its own, floating (panel id `library:swatches:<id>`, which window.panel.* moves, docks and closes), or bring its panel forward when it is open; clicking a swatch there runs swatch.library.add with apply. ui.libraryPanel sets its view, find field and Persistent (ui.inspect: ui.library_panels) → {open: id, panel, name, count}",
"{library: id or name (see swatch.library.list), or the id of a persistent panel's missing library | null (close every swatch library panel)} open the swatch library in a read-only panel of its own, floating (panel id `library:swatches:<id>`, which window.panel.* moves, docks and closes), or bring its panel forward when it is open; clicking a swatch there runs swatch.library.add with apply. ui.libraryPanel sets its view, find field and Persistent (ui.inspect: ui.library_panels) → {open: id, panel, name, count}",
),
(
"window.swatchLibrary.other",
@@ -522,6 +522,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"",
"{panel: a library panel's id (`library:swatches:pastels`, as window.swatchLibrary answers), library?: id or name of another library of its kind to show in the panel (Previous / Next Library; one open in a panel of its own is brought forward instead), view?: smallThumb|mediumThumb|largeThumb|smallList|largeList, showFind?: bool, persistent?: bool (reopen the panel when the app starts)} set an open library panel, or a closed persistent one's view, Show Find Field and Persistent; kept with the preferences (ui.inspect: ui.library_panels) → {panel, kind, library, view, showFind, persistent, open}",
),
(
"ui.libraryPanel.locate",
"Locate Library…",
"",
"{panel: an open library panel's id, path?} (default: pick a file) Locate Library… of a persistent library panel whose library's file is missing: load the library from the file, as Other Library… does, and show it in the panel, which keeps its view, Show Find Field and Persistent. When the file's library has a panel open already, the panel closes and that one is brought forward, made persistent when the closed one was → what ui.libraryPanel answers",
),
(
"ui.saveSwatchLibrary",
"Save Swatch Library…",
@@ -608,7 +614,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"window.graphicStyleLibrary",
"Graphic Style Library",
"",
"{library: id or name (see graphicStyle.libraries) | null (close every graphic style library panel)} open the graphic style library in a read-only panel of its own, floating (panel id `library:graphicStyles:<id>`), or bring its panel forward when it is open; clicking a style there runs graphicStyle.addFromLibrary with apply (Alt: add). ui.libraryPanel sets its view, find field and Persistent → {open: id, panel, name, count}",
"{library: id or name (see graphicStyle.libraries), or the id of a persistent panel's missing library | null (close every graphic style library panel)} open the graphic style library in a read-only panel of its own, floating (panel id `library:graphicStyles:<id>`), or bring its panel forward when it is open; clicking a style there runs graphicStyle.addFromLibrary with apply (Alt: add). ui.libraryPanel sets its view, find field and Persistent → {open: id, panel, name, count}",
),
(
"window.graphicStyleLibrary.other",
@@ -1610,6 +1616,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
"ui.flattenerPreview" => crate::panels::flattener_preview::command(app, p),
"window.graphicStyleLibrary" => crate::panels::graphic_styles::open_library(app, p),
"ui.libraryPanel" => crate::panels::library_panel::command(app, p),
"ui.libraryPanel.locate" => crate::panels::library_panel::locate(app, p),
"window.graphicStyleLibrary.other" => crate::panels::graphic_styles::other_library(app, s("path")),
"ui.saveGraphicStyleLibrary" => {
let names = p.get("names").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect());
+231 -20
View File
@@ -8,8 +8,9 @@
//! ([`panel_id`]), that floats, tabs, docks and closes as any panel of the shared docking system:
//! `window.swatchLibrary` and `window.graphicStyleLibrary` open one, or bring it forward when it is
//! open. `UiState::library_panels` keeps each one's view, Show Find Field and Persistent
//! (`ui.libraryPanel`); a persistent panel reopens when the app starts ([`restore`]). The find text
//! and the selection are panel state.
//! (`ui.libraryPanel`); a persistent panel reopens when the app starts ([`restore`]). A persistent
//! panel whose library can't be found shows the library as missing, with its file and Locate
//! Library… (`ui.libraryPanel.locate`). The find text and the selection are panel state.
use std::sync::{Mutex, PoisonError};
@@ -233,10 +234,28 @@ fn settings_mut<'a>(app: &'a mut VectorcraftApp, k: Kind, id: &str) -> Option<&'
app.ui.library_panels.iter_mut().find(|p| p.kind == k.kind && p.id == id)
}
/// Library panel `panel`'s tab label: its library's name (a built-in one's in the UI language).
/// The file of library `id` of `k` when the library is missing: its panel is persistent and the
/// library can't be found (its file was gone at startup, or a User Defined library's file was
/// deleted). The file is the path the library was read from, else the library's id.
fn missing_file(app: &VectorcraftApp, k: Kind, id: &str) -> Option<String> {
let s = settings(&app.ui, k.kind, id).filter(|s| s.persistent)?;
(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.
fn missing_name(app: &VectorcraftApp, k: Kind, id: &str) -> Option<String> {
let file = 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()))
}
/// Library panel `panel`'s tab label: its library's name (a built-in one's in the UI language), or
/// a missing library's [`missing_name`].
pub(crate) fn tab_label(app: &VectorcraftApp, panel: &str) -> Option<String> {
let (k, id) = parse(panel)?;
(k.name)(app, id).map(|name| library_name(id, &name).to_string())
match (k.name)(app, id) {
Some(name) => Some(library_name(id, &name).to_string()),
None => missing_name(app, k, id),
}
}
/// Library panel `panel`'s contents (the dock or its floating group draws its tab and menu button).
@@ -254,10 +273,11 @@ pub(crate) fn menu(app: &mut VectorcraftApp, ui: &mut Ui, panel: &str) {
}
/// Per frame, before the panels are drawn: close the panels of libraries that are gone (a User
/// Defined library whose file was deleted).
/// Defined library whose file was deleted), except persistent ones, which show the library as
/// missing ([`missing_file`]).
pub fn tidy(app: &mut VectorcraftApp) {
for panel in open_panels(&app.ui) {
if parse(&panel).is_some_and(|(k, id)| (k.name)(app, id).is_none()) {
if parse(&panel).is_some_and(|(k, id)| (k.name)(app, id).is_none() && missing_file(app, k, id).is_none()) {
close(app, &panel);
}
}
@@ -270,8 +290,9 @@ fn close(app: &mut VectorcraftApp, panel: &str) {
}
/// At startup, after the preferences are restored: the persistent library panels reopen, where the
/// saved layout has them or floating, their libraries read again (a loaded one from its file; one
/// that can't be found is left out with a note in the status bar); the others closed with the app.
/// saved layout has them or floating, their libraries read again (a loaded one from its file); the
/// others closed with the app. A panel whose library can't be found reopens showing the library
/// as missing ([`missing_file`]) and stays in the preferences, and the status bar names its file.
pub fn restore(app: &mut VectorcraftApp) {
let saved = std::mem::take(&mut app.ui.library_panels);
let mut missing = vec![];
@@ -286,10 +307,10 @@ pub fn restore(app: &mut VectorcraftApp) {
{
p.id = id;
}
match (k.name)(app, &p.id) {
Some(_) => app.ui.library_panels.push(p),
None => missing.push(p.path.unwrap_or(p.id)),
if (k.name)(app, &p.id).is_none() {
missing.push(p.path.clone().unwrap_or_else(|| p.id.clone()));
}
app.ui.library_panels.push(p);
}
let kept: Vec<String> = app.ui.library_panels.iter().map(LibraryPanel::panel).collect();
for panel in open_panels(&app.ui).into_iter().filter(|p| !kept.contains(p)) {
@@ -370,8 +391,9 @@ 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) 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}
/// (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)
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)) {
@@ -381,7 +403,8 @@ pub(crate) fn open_command<K: LibraryKind>(app: &mut VectorcraftApp, p: &Value)
};
let list = K::LIST;
let key = key.as_str().ok_or_else(|| format!("`library` is a library id or name (see {list})"))?;
let (id, name, count) = K::find(app, key).ok_or_else(|| format!("no library `{key}` (see {list})"))?;
let found = K::find(app, key).or_else(|| missing_name(app, kind_of::<K>(), key).map(|name| (key.to_string(), name, 0)));
let (id, name, count) = found.ok_or_else(|| format!("no library `{key}` (see {list})"))?;
let panel = panel_id(K::KIND, &id);
registered(&panel)?;
// Brought forward, it is the one opened last.
@@ -463,6 +486,37 @@ 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. 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 is forgotten, and that panel is brought
/// forward, made persistent when `panel` was. → what `ui.libraryPanel` answers
pub(crate) fn locate(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
let given = p.get("panel").and_then(Value::as_str).unwrap_or_default();
let (k, id) = parse(given).ok_or_else(|| format!("`{given}` isn't a library panel (see window.swatchLibrary)"))?;
if !is_open(&app.ui, given) {
return Err(format!("library panel `{given}` isn't open"));
}
let path = p.get("path").and_then(Value::as_str).map(str::to_string);
let Some(path) = pick_library_file(app, path)? else { return Ok(Value::Null) };
let r = app.run(k.load, json!({ "path": path }))?;
let next = r["library"].as_str().unwrap_or_default().to_string();
let to = panel_id(k.kind, &next);
if to != given && is_open(&app.ui, &to) {
let persistent = settings(&app.ui, k.kind, id).is_some_and(|s| s.persistent);
app.run("window.panel.close", json!({ "panel": given }))?;
app.ui.library_panels.retain(|q| q.panel() != given);
app.run("window.panel.activate", json!({ "panel": to }))?;
let mut params = json!({ "panel": to });
if persistent {
params["persistent"] = json!(true);
}
return command(app, &params);
}
command(app, &json!({ "panel": given, "library": next }))
}
/// Run `ui.libraryPanel` on panel `panel` with `params`.
fn set(app: &mut VectorcraftApp, panel: &str, mut params: Value) {
params["panel"] = json!(panel);
@@ -508,9 +562,13 @@ fn selection(ctx: &egui::Context, panel: &str) -> Vec<String> {
pstate(ctx, &key(panel, "selected"))
}
/// Find field, thumbnails or list rows, and the bottom bar of library `id`'s panel.
/// Find field, thumbnails or list rows, and the bottom bar of library `id`'s panel; for a missing
/// library, [`missing`].
fn body<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
let Some((_, lib)) = K::get(app, id) else { return };
let Some((_, lib)) = K::get(app, id) else {
missing::<K>(app, ui, id);
return;
};
let panel = panel_id(K::KIND, id);
let (view, hide_find) = settings(&app.ui, K::KIND, id).map_or((K::VIEW, false), |s| (s.view.unwrap_or(K::VIEW), s.hide_find));
let query = if hide_find { String::new() } else { widgets::search_field(ui, egui::Id::new(key(&panel, "find")), tl!("Find")) };
@@ -569,6 +627,25 @@ fn body<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
bottom::<K>(app, ui, id);
}
/// The body of the panel of library `id` when the library is missing ([`missing_file`]): its file,
/// and Locate Library… where the app has an open dialog.
fn missing<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
let Some(file) = missing_file(app, kind_of::<K>(), id) else { return };
widgets::list_box(ui, |ui| {
ui.set_width(ui.available_width());
ui.add_space(4.0);
widgets::dim_name(ui, &crate::i18n::fmt(tl!("Missing: {path}"), &[("path", &file)]));
ui.add_space(4.0);
});
ui.add_space(8.0);
let can = crate::picks::can(app, &crate::picks::PickRequest::Open(crate::FilePick::default()));
if ui.add_enabled(can, egui::Button::new(tl!("Locate Library…"))).clicked()
&& let Err(e) = app.run("ui.libraryPanel.locate", json!({ "panel": panel_id(K::KIND, id) }))
{
app.status(e);
}
}
/// The library `step` places after (or before, when negative) `id` in menu order, wrapping.
fn neighbour<K: LibraryKind>(app: &VectorcraftApp, id: &str, step: isize) -> Option<String> {
let all = K::list(app);
@@ -632,8 +709,10 @@ fn panel_menu<K: LibraryKind>(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
set(app, &panel, json!({ "showFind": hide_find }));
}
ui.separator();
// Previous Library and Next Library are dimmed for a missing library ([`missing_file`]).
let listed = K::get(app, id).is_some();
for (label, s) in [(tl!("Previous Library"), -1), (tl!("Next Library"), 1)] {
if menu_item(ui, label, true, false) {
if menu_item(ui, label, listed, false) {
step::<K>(app, id, s);
}
}
@@ -1043,6 +1122,44 @@ mod tests {
assert!(app.run("ui.libraryPanel", json!({"panel": styles, "view": "huge"})).is_err());
}
/// A context whose frames report their accessibility tree.
fn accessible() -> egui::Context {
let ctx = context();
ctx.enable_accesskit();
ctx
}
/// One frame of `draw` with `events`, after the frames `ctx` ran.
fn draw_frame(
app: &mut VectorcraftApp,
ctx: &egui::Context,
draw: &dyn Fn(&mut VectorcraftApp, &mut Ui),
events: Vec<Event>,
) -> egui::FullOutput {
let screen_rect = Some(Rect::from_min_size(Pos2::ZERO, vec2(1200.0, 800.0)));
let time = Some(ctx.input(|i| i.time) + 0.1);
let mut out = ctx.run_ui(egui::RawInput { events, time, screen_rect, ..Default::default() }, |ui| draw(app, ui));
out.textures_delta.clear();
out
}
/// Whether the control `draw` shows that screen readers name `label` is enabled, and its center.
fn control(app: &mut VectorcraftApp, ctx: &egui::Context, draw: &dyn Fn(&mut VectorcraftApp, &mut Ui), label: &str) -> (bool, Pos2) {
draw_frame(app, ctx, draw, vec![]);
let out = draw_frame(app, ctx, draw, vec![]);
let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes;
let (_, node) = nodes.iter().find(|(_, n)| n.label().map(str::trim) == Some(label)).unwrap_or_else(|| panic!("no control named {label:?}"));
let b = node.bounds().expect("bounds");
(!node.is_disabled(), Pos2::new((b.x0 + b.x1) as f32 * 0.5, (b.y0 + b.y1) as f32 * 0.5))
}
/// A click at `at` in a frame of `draw`.
fn click_at(app: &mut VectorcraftApp, ctx: &egui::Context, draw: &dyn Fn(&mut VectorcraftApp, &mut Ui), at: Pos2) {
let button = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
draw_frame(app, ctx, draw, vec![Event::PointerMoved(at)]);
draw_frame(app, ctx, draw, vec![button(true), button(false)]);
}
/// The app restarted with the preferences `app` saves.
fn restarted(app: &VectorcraftApp) -> VectorcraftApp {
let saved: crate::UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap();
@@ -1080,14 +1197,99 @@ mod tests {
assert!(SwatchLibraries::get(&next, id).is_some_and(|(_, lib)| lib.swatch("Red").is_some()), "read again from its file");
assert!(next.ui.status.is_empty(), "{}", next.ui.status);
assert!(next.ui.docking_hidden.keys().all(|p| p != &brights));
// Unchecked, a panel no longer reopens; a file that is gone is left out with a note.
// Unchecked, a panel no longer reopens; one whose file is gone reopens as missing, with a note.
let mut next = next;
next.run("ui.libraryPanel", json!({"panel": pastels, "persistent": false})).unwrap();
std::fs::remove_file(&gpl).unwrap();
let last = restarted(&next);
assert!(!is_open(&last.ui, &pastels) && !is_open(&last.ui, &brand));
assert!(!is_open(&last.ui, &pastels) && is_open(&last.ui, &brand));
assert!(last.ui.status.contains("Brand.gpl"), "{}", last.ui.status);
assert!(last.ui.library_panels.is_empty());
assert_eq!(last.ui.library_panels.iter().map(LibraryPanel::panel).collect::<Vec<_>>(), [brand]);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn a_persistent_panel_whose_file_is_gone_is_listed_as_missing_until_located() {
let dir = std::env::temp_dir().join(format!("vc-missing-library-{}", 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, "view": "smallList"})).unwrap();
std::fs::remove_file(&gpl).unwrap();
// The panel reopens showing its library as missing, and the preferences keep it.
let mut next = restarted(&app);
tidy(&mut next);
assert!(is_open(&next.ui, &brand), "{}", next.ui.status);
assert!(next.ui.status.contains("Brand.gpl"), "{}", next.ui.status);
let (_, gone) = parse(&brand).unwrap();
let s = settings(&next.ui, "swatches", gone).cloned().unwrap();
assert_eq!((s.path, s.persistent, s.view), (Some(gpl.to_string_lossy().into_owned()), true, Some(View::SmallList)));
assert_eq!(tab_label(&next, &brand).as_deref(), Some("Brand"));
// Closed, it opens again by its library's id.
next.run("window.panel.close", json!({"panel": 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));
let text = crate::tests_labels::painted_text(&mut next, |app, ui| show(app, ui, &brand));
let shown = format!("Missing: {}", gpl.to_string_lossy());
assert!(text.contains(&shown), "{text}");
let ctx = accessible();
let body = |app: &mut VectorcraftApp, ui: &mut Ui| show(app, ui, &brand);
assert!(!control(&mut next, &ctx, &body, "Locate Library…").0, "dimmed without an open dialog");
let panel_menu = |app: &mut VectorcraftApp, ui: &mut Ui| menu(app, ui, &brand);
assert!(!control(&mut next, &ctx, &panel_menu, "Next Library").0, "no library to step from");
let mut next = restarted(&next);
assert!(is_open(&next.ui, &brand), "still listed after another restart");
// Locate Library… reads the file where it is now and shows it in the same panel.
assert!(next.run("ui.libraryPanel.locate", json!({"panel": "layers"})).is_err());
let moved = dir.join("moved").join("Brand.gpl");
std::fs::write(&moved, palette).unwrap();
let picked = moved.to_string_lossy().into_owned();
next.services.pick_open = Some(Box::new(move |_: &crate::FilePick| Some(picked.clone())));
let (enabled, at) = control(&mut next, &ctx, &body, "Locate Library…");
assert!(enabled);
click_at(&mut next, &ctx, &body, at);
let located = panel_id("swatches", &format!("loaded/{}", moved.to_string_lossy()));
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()));
let s = settings(&next.ui, "swatches", id).cloned().unwrap();
assert_eq!((s.path, s.persistent, s.view), (Some(moved.to_string_lossy().into_owned()), true, Some(View::SmallList)));
assert!(settings(&next.ui, "swatches", gone).is_none());
let located_menu = |app: &mut VectorcraftApp, ui: &mut Ui| menu(app, ui, &located);
assert!(control(&mut next, &ctx, &located_menu, "Next Library").0);
let last = restarted(&next);
assert!(is_open(&last.ui, &located) && last.ui.status.is_empty(), "{}", last.ui.status);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn locating_a_library_open_in_its_own_panel_brings_that_panel_forward() {
let dir = std::env::temp_dir().join(format!("vc-locate-open-library-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).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);
let copy = dir.join("Copy.gpl");
std::fs::write(&copy, palette).unwrap();
let other = panel_of(next.run("window.swatchLibrary.other", json!({"path": copy.to_string_lossy()})).unwrap());
next.run("window.panel.close", json!({"panel": brand})).unwrap();
assert!(next.run("ui.libraryPanel.locate", json!({"panel": brand, "path": copy.to_string_lossy()})).is_err(), "a closed panel");
next.run("window.panel", json!({"panel": brand})).unwrap();
let r = next.run("ui.libraryPanel.locate", json!({"panel": brand, "path": copy.to_string_lossy()})).unwrap();
assert_eq!((r["panel"].as_str(), r["persistent"].as_bool()), (Some(other.as_str()), Some(true)));
assert!(!is_open(&next.ui, &brand) && is_open(&next.ui, &other));
assert!(next.ui.library_panels.iter().all(|p| p.panel() != brand), "the missing panel is forgotten");
let _ = std::fs::remove_dir_all(dir);
}
@@ -1119,15 +1321,24 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("Gone.gpl"), "GIMP Palette\nName: Gone\n#\n0 0 0\tInk\n").unwrap();
std::fs::write(dir.join("Kept.gpl"), "GIMP Palette\nName: Kept\n#\n0 0 0\tInk\n").unwrap();
let mut app = app();
app.session.swatch_libraries.set_user_dir(Some(dir.to_string_lossy().to_string()));
let panel = panel_of(app.run("window.swatchLibrary", json!({"library": "user/Gone.gpl"})).unwrap());
let kept = panel_of(app.run("window.swatchLibrary", json!({"library": "user/Kept.gpl"})).unwrap());
app.run("ui.libraryPanel", json!({"panel": kept, "persistent": true})).unwrap();
tidy(&mut app);
assert!(is_open(&app.ui, &panel));
std::fs::remove_file(dir.join("Gone.gpl")).unwrap();
std::fs::remove_file(dir.join("Kept.gpl")).unwrap();
app.session.swatch_libraries.rescan();
tidy(&mut app);
assert!(!is_open(&app.ui, &panel));
// A persistent panel stays open and shows its library as missing.
assert!(is_open(&app.ui, &kept));
assert_eq!(tab_label(&app, &kept).as_deref(), Some("Kept"));
let text = crate::tests_labels::painted_text(&mut app, |app, ui| show(app, ui, &kept));
assert!(text.contains("Missing: user/Kept.gpl"), "{text}");
let _ = std::fs::remove_dir_all(dir);
}
+1
View File
@@ -66,6 +66,7 @@ const DOCUMENT_FREE: &[&str] = &[
"ui.checkScript",
"window.swatchLibrary.other",
"window.graphicStyleLibrary.other",
"ui.libraryPanel.locate",
"shortcuts.import",
"shortcuts.export",
"ui.shortcutImportDialog",
+27
View File
@@ -623,6 +623,33 @@ fn an_import_shortcut_set_answer_goes_on_after_the_active_document_closes() {
let _ = std::fs::remove_dir_all(dir);
}
/// Locate Library… acts on no document either: its answer shows the picked file's library in the
/// missing library's panel even when the document active at the time closed meanwhile (Linux).
#[test]
fn a_locate_library_answer_goes_on_after_the_active_document_closes() {
let (mut app, shown, _) = with_dialogs(false);
let dir = std::env::temp_dir().join(format!("vc-picks-locate-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (gone, found) = (dir.join("Gone.gpl"), dir.join("Found.gpl"));
std::fs::write(&found, "GIMP Palette\nName: Found\n#\n255 0 0\tRed\n").unwrap();
let id = format!("loaded/{}", gone.to_string_lossy());
let path = Some(gone.to_string_lossy().into_owned());
let missing = library_panel::LibraryPanel { kind: "swatches".into(), id: id.clone(), path, view: None, hide_find: false, persistent: true };
app.ui.library_panels.push(missing);
let panel = library_panel::panel_id("swatches", &id);
app.run("window.panel", json!({"panel": panel})).unwrap();
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
assert!(app.run("ui.libraryPanel.locate", json!({"panel": panel})).is_err());
app.run("file.close", json!({})).unwrap();
assert!(app.session.documents().is_empty());
shown.answer(&[&found.to_string_lossy()]);
poll(&mut app);
let located = library_panel::panel_id("swatches", &format!("loaded/{}", found.to_string_lossy()));
assert!(library_panel::is_open(&app.ui, &located) && !library_panel::is_open(&app.ui, &panel), "{}", app.ui.status);
assert_ne!(app.ui.status, CLOSED);
let _ = std::fs::remove_dir_all(dir);
}
/// Save Changes' Save, answered by its Save dialog, saves and closes the document it asked about,
/// even when an earlier document closed meanwhile (Linux), then asks about the next one.
#[test]