Select Color: colour book folders, app-style view toggles, load button by the book list

- Colour books now come from folders, like AutoCAD's Color Book
  Locations: Options > Files lists them (add, open, remove); by default
  one color_books folder in the config directory. The Color Books tab
  shows the built-in books plus every .acb in those folders; Load copies
  the chosen file into the first folder. Web: session only, as before.
- Grid/list buttons use the app's toggle style (light accent, its text
  colour for the icon, same corners as inactive).
- Color Books: the load button sits right of the book list; the search
  box keeps its start over the page strip and runs to the end.
- Translations for the two new Options strings in all 22 locales.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
Claude committed 2026-10-14 18:51:02 +00:00
1 parent 322c1906f9
commit 666234f69e
30 files changed
+222 -61

No files matched your search

+5 -2
View File
@@ -860,7 +860,7 @@ impl OpenCADStudio {
block_recent: self.block_palette.recent.clone(),
block_favorites: self.block_palette.favorites.clone(),
block_libraries: self.block_palette.libraries.clone(),
color_books: self.color_book_paths.clone(),
color_book_dirs: self.color_book_dirs.clone(),
block_palette_view: self.block_palette.view as u8,
block_insert: self.block_palette.options.clone(),
block_mru_list: self.block_mru_list,
@@ -1026,7 +1026,10 @@ impl OpenCADStudio {
self.block_palette.recent = s.block_recent.clone();
self.block_palette.favorites = s.block_favorites.clone();
self.block_palette.libraries = s.block_libraries.clone();
self.color_book_paths = s.color_books.clone();
if self.color_book_dirs != s.color_book_dirs {
self.color_book_dirs = s.color_book_dirs.clone();
self.color_books_read = false;
}
self.block_palette.view =
crate::ui::window::block_palette::ViewMode::from_u8(s.block_palette_view);
self.block_palette.set_options(s.block_insert.clone());
+79 -28
View File
@@ -11596,25 +11596,38 @@ impl OpenCADStudio {
Some(crate::ui::color_select::Outcome::Commit(color, name)) => {
self.update(Message::ColorWindowPick(color, name))
}
Some(crate::ui::color_select::Outcome::LoadBook) => Task::perform(
async {
let file = crate::sys::file_dialog()
.add_filter(crate::t!("Color book").as_ref(), &["acb", "ACB"])
.pick_file()
.await
.ok_or_else(|| "Cancelled".to_string())?;
let book = crate::ui::color_select::book::parse(
&String::from_utf8_lossy(&file.read().await),
)?;
// The web build has no paths to read again later.
#[cfg(not(target_arch = "wasm32"))]
let path = Some(crate::sys::handle_path(&file).display().to_string());
#[cfg(target_arch = "wasm32")]
let path = None;
Ok((book, path))
},
Message::ColorBookLoaded,
),
Some(crate::ui::color_select::Outcome::LoadBook) => {
// Copied into the first colour book folder so it stays
// when the original moves; the web keeps it for the
// session only.
let folder = self.color_book_dirs.first().cloned();
Task::perform(
async move {
let file = crate::sys::file_dialog()
.add_filter(crate::t!("Color book").as_ref(), &["acb", "ACB"])
.pick_file()
.await
.ok_or_else(|| "Cancelled".to_string())?;
let bytes = file.read().await;
let book = crate::ui::color_select::book::parse(
&String::from_utf8_lossy(&bytes),
)?;
#[cfg(not(target_arch = "wasm32"))]
if let Some(folder) = folder {
let folder = std::path::PathBuf::from(folder);
std::fs::create_dir_all(&folder)
.and_then(|()| {
std::fs::write(folder.join(file.file_name()), &bytes)
})
.map_err(|e| e.to_string())?;
}
#[cfg(target_arch = "wasm32")]
let _ = folder;
Ok(book)
},
Message::ColorBookLoaded,
)
}
Some(crate::ui::color_select::Outcome::Cancel) => {
self.color_picker = None;
Task::none()
@@ -11651,7 +11664,7 @@ impl OpenCADStudio {
}
self.on_color_window_pick(color, name)
}
Message::ColorBookLoaded(Ok((book, path))) => {
Message::ColorBookLoaded(Ok(book)) => {
// A book loaded again replaces the earlier copy.
let books = std::sync::Arc::make_mut(&mut self.color_books);
let index = match books.iter().position(|b| b.name == book.name) {
@@ -11664,10 +11677,6 @@ impl OpenCADStudio {
books.len() - 1
}
};
if let Some(path) = path.filter(|path| !self.color_book_paths.contains(path)) {
self.color_book_paths.push(path);
self.save_config();
}
if let Some(picker) = self.color_picker.as_mut() {
picker.show_book(std::sync::Arc::clone(&self.color_books), index);
}
@@ -11679,6 +11688,35 @@ impl OpenCADStudio {
}
Task::none()
}
#[cfg(not(target_arch = "wasm32"))]
Message::ColorBookDirBrowse => Task::perform(
async {
crate::sys::file_dialog()
.pick_folder()
.await
.map(|handle| crate::sys::handle_path(&handle).display().to_string())
},
|dir| dir.map_or(Message::Noop, Message::ColorBookDirAdd),
),
// Browsers expose no folder picker.
#[cfg(target_arch = "wasm32")]
Message::ColorBookDirBrowse => Task::none(),
Message::ColorBookDirAdd(dir) => {
if !self.color_book_dirs.contains(&dir) {
self.color_book_dirs.push(dir);
self.color_books_read = false;
self.persist_settings_if_changed();
}
Task::none()
}
Message::ColorBookDirRemove(i) => {
if i < self.color_book_dirs.len() {
self.color_book_dirs.remove(i);
self.color_books_read = false;
self.persist_settings_if_changed();
}
Task::none()
}
Message::DsSetHandle { field, value } => self.on_ds_set_handle(field, value),
}
}
@@ -11759,9 +11797,9 @@ impl OpenCADStudio {
document.layers.get(layer).map(|layer| layer.color)
}
/// Read the built-in colour books and the ones remembered in the
/// settings, once per session. A loaded book replaces a built-in one of
/// the same name.
/// Read the built-in colour books and every `.acb` in the colour book
/// folders, once until the folders change. A book in a folder replaces a
/// built-in one of the same name.
fn read_color_books(&mut self) {
if std::mem::replace(&mut self.color_books_read, true) {
return;
@@ -11771,8 +11809,21 @@ impl OpenCADStudio {
let mut books: Vec<_> = book::builtin().collect();
#[cfg(not(target_arch = "wasm32"))]
for loaded in self
.color_book_paths
.color_book_dirs
.iter()
.flat_map(|dir| {
let mut files: Vec<_> = std::fs::read_dir(dir)
.into_iter()
.flatten()
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| {
path.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("acb"))
})
.collect();
files.sort();
files
})
.filter_map(|path| std::fs::read_to_string(path).ok())
.filter_map(|text| book::parse(&text).ok())
{