mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
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:
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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());
|
||||
|
||||
@@ -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, ¶ms);
|
||||
}
|
||||
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(©, 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
+13
-3
@@ -1574,11 +1574,21 @@ of its kind in that panel (what Previous Library and Next Library do; a library
|
||||
brought forward instead), `view` is `smallThumb`, `mediumThumb`, `largeThumb`, `smallList` or `largeList`,
|
||||
`showFind` shows or hides the find field, and `persistent: true` is the panel menu's Persistent: the panel
|
||||
reopens when the app starts, with its view and find field, where the saved layout had it (or floating). A
|
||||
library loaded from a file is read again from its path; one whose file is gone is left out, and the status bar
|
||||
says which. A closed persistent panel can still be set, so `persistent: false` stops it reopening. The reply
|
||||
is `{panel, kind, library, view, showFind, persistent, open}`, and `ui.inspect` lists every panel's settings
|
||||
library loaded from a file is read again from its path. When the file is gone, the panel still reopens and
|
||||
stays in the preferences: its tab shows the file's name, its body shows `Missing:` with the file's path and a
|
||||
Locate Library… button, and the status bar names the file. A persistent panel whose User Defined library's
|
||||
file is deleted while the app runs stays open with the same tab and `Missing:` body; any other panel closes.
|
||||
A closed persistent panel can still be set, so `persistent: false` stops it reopening. The reply is
|
||||
`{panel, kind, library, view, showFind, persistent, open}`, and `ui.inspect` lists every panel's settings
|
||||
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.
|
||||
`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`.
|
||||
|
||||
```json
|
||||
{"name":"invoke_menu","arguments":{"command":"window.swatchLibrary","params":{"library":"pastels"}}}
|
||||
{"name":"invoke_menu","arguments":{"command":"ui.libraryPanel","params":{"panel":"library:swatches:pastels","view":"smallList","persistent":true}}}
|
||||
|
||||
@@ -700,6 +700,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Paint and appearance (M3):**
|
||||
- **CSS swatch libraries:** fractional linear and radial gradient stop positions are preserved, so nearby stops such as 12.5% and 12.890625% remain distinct instead of collapsing to 13%. Reference: [CSS Images 3 color stops](https://www.w3.org/TR/css-images-3/#color-stop-syntax). The original synthetic regression is `palette_io::tests::css_preserves_fractional_gradient_stop_positions` in `crates/color/src/palette_io.rs`. Scores and hours are unchanged.
|
||||
- **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries and five gradient libraries in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate.
|
||||
- **Missing library files:** a persistent swatch or graphic style library panel whose file is gone at startup reopens and stays in the preferences, showing the file's path with Locate Library… (`ui.libraryPanel.locate`), which reads the file from where it is now into the same panel.
|
||||
- **Colour:** a Color Picker (colour field, H/S/B/R/G/B channel slider, HSB/RGB/Lab/CMYK fields, hex, web-only snapping, out-of-gamut correction); the Color panel shows a selected colour in its own model and, with nothing selected, the new-art defaults in the mode picked last, which it keeps between sessions; Alt-click paints the other proxy; proxies look through groups and show "?" for mixed paint; Invert, Complement, Apply Last Color (`,`) and Apply Last Gradient (`.`) keep the colour model; every paint command feeds the recent colours. The Color Guide has 21 harmony rules, Steps and Variation options and Limit to Library. Adjust Color Balance (its Global mode shifts tints) and Saturate preview live; Edit Colors and Recolor also reach meshes, images and patterns. Recolor Artwork has colour reduction, five recolour methods, presets, Limit to Library, a colour-wheel Edit tab and Edit Color Group; a local Color Themes panel saves five-colour themes. Fill/Stroke chips open swatch and mixer popovers (the Properties panel has the Control bar's Fill and Stroke chips, Stroke link and weight spinner, with nothing selected too), panels have Illustrator's Window menu keys (Align Shift+F7, Transform Shift+F8, Pathfinder Cmd+Shift+F9, Layers F7, Character Cmd+T…; every panel can be given one in Edit › Keyboard Shortcuts), and Eyedropper Options pick up and apply chosen attributes (Alt and Shift+Alt, image pixels). An ICC CMYK profile converts straight into the RGB working space (not through sRGB, so a wider space such as Wide Gamut RGB keeps colours sRGB can't hold) with the colour settings' black-point compensation, on the canvas and in RGB exports alike (the built-in Generic CMYK compensates both ways, so RGB black separated to a rich black displays as black again). Fills and strokes can overprint (Attributes panel, Overprint Black options), and CMYK documents blend in inks on screen, in PDF and when flattening. Document Color Mode converts the art, symbols, pattern tiles and swatches through the colour settings: Gray stays Gray, RGB greys separate into four-colour greys and a rich black as Illustrator does, or go on the black plate only with `grays: "black"` (also for `document.open {colorMode}`).
|
||||
View › Proof Setup › Color blindness – Tritanopia-type turns Proof Colors on and proofs the canvas with the tritanopia simulation of Brettel, Viénot and Mollon (1997). The Separations Preview panel's Proof Setup list offers the same target (`view.proofSetup {target: "tritanopia"}`). Grays keep their value.
|
||||
- **Color Picker layout (#2008):** a centered draggable header, equal-width actions above aligned HSB/RGB/Hex and Lab/CMYK fields, compact field and slider, and warnings below the preview. No horizontal scrolling; narrow windows stack the sections with vertical scrolling. Tests in `dialogs/color_picker.rs` cover actual pointer/key input, typed values before OK/Enter, Cancel/Escape rollback, one Undo step, mode-size stability, warnings and layout at two UI scales. The screen sampler sits above the field and the upstream two-grid Tab order is retained. AccessKit regressions check the action names and each channel slider's displayed value, units and range. German and enlarged English UI renders were reviewed locally; no reference images are shipped. This is a layout improvement, with no change to scores or remaining-hour estimates.
|
||||
|
||||
+1
-1
@@ -243,7 +243,7 @@ empty), without opening the dialog.
|
||||
| SVG Interactivity, Retype, Comments, Version History panels | missing | the last two are cloud features (out of scope) |
|
||||
| Workspaces (save, reset, switch) | done | |
|
||||
| Background Export: a progress dialog with the file name, the artboard count and a bar (busy for one artboard and for formats made in one go), Cancel (Esc) and Continue in Background; it opens after 0.4 s and never over another dialog. A cancelled export writes no file | done | #2564; on the shared `vectorcraft_doc::cancel` token (#2546), so a cancelled render keeps no half-drawn pattern tile or effect raster. The bar moves between artboards, not within one. Save As, Export for Screens and Export Selection don't show it yet |
|
||||
| Swatch and graphic style library panels: each library opens in a panel of its own, several at once, that floats, groups, docks and closes like any panel; opening an open library brings its panel forward; Previous / Next Library change the library in place; the panel menu's Persistent reopens the panel at startup with its view and find field (a library loaded from a file is read again, a missing file is skipped with a status-bar note) | done | #2603; panel ids `library:<kind>:<id>`, `ui.libraryPanel {panel, library?, view?, showFind?, persistent?}`; the library panels have no sort option, as before |
|
||||
| Swatch and graphic style library panels: each library opens in a panel of its own, several at once, that floats, groups, docks and closes like any panel; opening an open library brings its panel forward; Previous / Next Library change the library in place; the panel menu's Persistent reopens the panel at startup with its view and find field (a library loaded from a file is read again; when its file is missing the panel reopens showing the file's path with Locate Library…, and the status bar names the file) | done | #2603; panel ids `library:<kind>:<id>`, `ui.libraryPanel {panel, library?, view?, showFind?, persistent?}`, `ui.libraryPanel.locate {panel, path?}`; the library panels have no sort option, as before |
|
||||
| DXF export/import preserves intended curve closure | done | #1299; R13–2018 closed SPLINE bit set/read; an open curve ending at its start stays open; knots and control points unchanged; native CIRCLE/ARC export and CNC preset remain pending |
|
||||
| macOS save sheet: choose the format from a menu | done | #866; the save panel is VectorCraft's own `NSSavePanel` with its Format menu set before the sheet opens (`apps/vectorcraft/src/mac_save_format.rs`); #1855 had withdrawn the menu that was added after the sheet began, which made Save As, Save a Copy and the first Save show no sheet |
|
||||
| Preferences: every option acts | partial | ~16 stored but unused (#394) |
|
||||
|
||||
Reference in New Issue
Block a user