forked from 1b-agent/opencadstudio-upstream
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
30 files changed
+222
-61
No files matched your search
+11
-5
@@ -965,9 +965,10 @@ pub(super) struct OpenCADStudio {
|
||||
// ── Floating panel windows ────────────────────────────────────────────
|
||||
/// The open Select Color panel, if any.
|
||||
color_picker: Option<crate::ui::color_select::ColorPicker>,
|
||||
/// Colour books loaded this session, read lazily from `color_book_paths`.
|
||||
/// The built-in colour books and those in `color_book_dirs`, read
|
||||
/// lazily when the Select Color panel opens.
|
||||
color_books: std::sync::Arc<Vec<crate::ui::color_select::book::ColorBook>>,
|
||||
color_book_paths: Vec<String>,
|
||||
color_book_dirs: Vec<String>,
|
||||
color_books_read: bool,
|
||||
/// Recently committed real colours, newest first.
|
||||
/// ACI and True Color remain semantically distinct even when their RGB matches.
|
||||
@@ -4174,8 +4175,13 @@ pub enum Message {
|
||||
/// Commit the colour chosen in the shared picker, with its `BOOK$NAME`
|
||||
/// when it comes from a colour book.
|
||||
ColorWindowPick(codec::types::Color, Option<String>),
|
||||
/// An `.acb` file was read: the book and, on desktop, where it lives.
|
||||
ColorBookLoaded(Result<(crate::ui::color_select::book::ColorBook, Option<String>), String>),
|
||||
/// An `.acb` file was read (and, on desktop, copied into the first
|
||||
/// colour book folder).
|
||||
ColorBookLoaded(Result<crate::ui::color_select::book::ColorBook, String>),
|
||||
/// Options › Files: pick a folder to search for colour books.
|
||||
ColorBookDirBrowse,
|
||||
ColorBookDirAdd(String),
|
||||
ColorBookDirRemove(usize),
|
||||
/// Set a block/linetype Handle field on the selected dim style from a
|
||||
/// dropdown of available block-records / linetypes (by name).
|
||||
DsSetHandle {
|
||||
@@ -4608,7 +4614,7 @@ impl OpenCADStudio {
|
||||
web_autosave_clear_after: Vec::new(),
|
||||
color_picker: None,
|
||||
color_books: Default::default(),
|
||||
color_book_paths: Vec::new(),
|
||||
color_book_dirs: Vec::new(),
|
||||
color_books_read: false,
|
||||
active_modal: None,
|
||||
hyperlink_editor_handles: Vec::new(),
|
||||
|
||||
+14
-4
@@ -619,9 +619,10 @@ pub struct UserSettings {
|
||||
/// Blocks palette: library folders or drawings, last used first.
|
||||
#[serde(default)]
|
||||
pub block_libraries: Vec<String>,
|
||||
/// Colour book (.acb) files loaded in the Select Color panel.
|
||||
#[serde(default)]
|
||||
pub color_books: Vec<String>,
|
||||
/// Folders searched for `.acb` colour books. Books loaded in the Select
|
||||
/// Color panel are copied into the first one.
|
||||
#[serde(default = "default_color_book_dirs")]
|
||||
pub color_book_dirs: Vec<String>,
|
||||
/// Blocks palette view: 0 extra large, 1 large, 2 medium, 3 small icons,
|
||||
/// 4 details, 5 list.
|
||||
#[serde(default = "default_block_view")]
|
||||
@@ -749,6 +750,15 @@ fn default_grid_major() -> u32 {
|
||||
5
|
||||
}
|
||||
|
||||
/// One folder in the config directory; none on the web, which has no folders.
|
||||
fn default_color_book_dirs() -> Vec<String> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(dir) = crate::config::config_dir() {
|
||||
return vec![dir.join("color_books").display().to_string()];
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -904,7 +914,7 @@ impl Default for UserSettings {
|
||||
block_recent: Vec::new(),
|
||||
block_favorites: Vec::new(),
|
||||
block_libraries: Vec::new(),
|
||||
color_books: Vec::new(),
|
||||
color_book_dirs: default_color_book_dirs(),
|
||||
block_palette_view: 1,
|
||||
block_insert: BlockInsertOptions::default(),
|
||||
block_mru_list: 50,
|
||||
|
||||
@@ -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
@@ -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())
|
||||
{
|
||||
|
||||
@@ -367,6 +367,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
},
|
||||
&self.qnew_template,
|
||||
&self.color_book_dirs,
|
||||
self.double_click_block_refedit,
|
||||
self.double_click_block_attedit,
|
||||
self.cursor_type,
|
||||
|
||||
@@ -4736,6 +4736,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Not a color book" => Some(("common", "color-not-a-book")),
|
||||
"This color book is encrypted and cannot be read" => Some(("common", "color-book-encrypted")),
|
||||
"The color book has no colors" => Some(("common", "color-book-empty")),
|
||||
"Color book locations" => Some(("common", "color-book-locations")),
|
||||
"Folders searched for .acb color books. A book loaded in Select Color is copied into the first folder." => Some(("common", "color-book-locations-hint")),
|
||||
"CAD Color Index (ACI) 10–249" => Some(("common", "cad-color-index-aci-10-249")),
|
||||
"Grayscale 250–255" => Some(("common", "grayscale-250-255")),
|
||||
"Isometric drafting" => Some(("common", "isometric-drafting")),
|
||||
|
||||
@@ -672,8 +672,9 @@ impl ColorPicker {
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Grid or list toggle for the Document tab: dark on the accent colour
|
||||
/// when active, like the OK button.
|
||||
/// Grid or list toggle for the Document tab, styled like the app's other
|
||||
/// toggles: light accent when active, its icon in that accent's text
|
||||
/// colour.
|
||||
fn view_button(&self, icon: &'static [u8], view: DocView) -> Element<'_, Message> {
|
||||
let active = self.doc_view == view;
|
||||
button(
|
||||
@@ -683,7 +684,7 @@ impl ColorPicker {
|
||||
.height(18)
|
||||
.style(move |theme: &Theme, _status| svg::Style {
|
||||
color: Some(if active {
|
||||
theme.palette().primary.base.text
|
||||
theme.palette().primary.weak.text
|
||||
} else {
|
||||
theme.palette().background.base.text
|
||||
}),
|
||||
@@ -695,13 +696,7 @@ impl ColorPicker {
|
||||
.width(CONTROL_HEIGHT)
|
||||
.height(CONTROL_HEIGHT)
|
||||
.padding(0)
|
||||
.style(move |theme: &Theme, status| {
|
||||
if active {
|
||||
button::primary(theme, status)
|
||||
} else {
|
||||
toggle_style(theme, false, status)
|
||||
}
|
||||
})
|
||||
.style(move |theme: &Theme, status| toggle_style(theme, active, status))
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -750,21 +745,20 @@ impl ColorPicker {
|
||||
.center(Length::Fill)
|
||||
.into();
|
||||
};
|
||||
// The book list as wide as the colour bars; the search box and the
|
||||
// load button over the page strip and the RGB values.
|
||||
// The book list and its load button over the colour bars; the
|
||||
// search box over the page strip and the RGB values, to the end.
|
||||
let books = Arc::clone(&self.books);
|
||||
let top = row![
|
||||
pick_list(
|
||||
Some(self.book),
|
||||
(0..self.books.len()).collect::<Vec<_>>(),
|
||||
move |&i| books[i].name.clone(),
|
||||
)
|
||||
.on_select(|i| picker_msg(ColorPickerMsg::Book(i)))
|
||||
.text_size(12)
|
||||
.padding([6, 8])
|
||||
.width(Length::Fill),
|
||||
row![
|
||||
search_box(&self.book_query, |query| ColorPickerMsg::BookQuery(query)),
|
||||
pick_list(
|
||||
Some(self.book),
|
||||
(0..self.books.len()).collect::<Vec<_>>(),
|
||||
move |&i| books[i].name.clone(),
|
||||
)
|
||||
.on_select(|i| picker_msg(ColorPickerMsg::Book(i)))
|
||||
.text_size(12)
|
||||
.padding([6, 8])
|
||||
.width(Length::Fill),
|
||||
load_button(
|
||||
container(
|
||||
secondary_icon(crate::ui::icons::themed_handle(
|
||||
@@ -781,6 +775,11 @@ impl ColorPicker {
|
||||
.padding(0),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(iced::Center)
|
||||
.width(Length::Fill),
|
||||
container(search_box(&self.book_query, |query| {
|
||||
ColorPickerMsg::BookQuery(query)
|
||||
}))
|
||||
.width(SIDE_WIDTH),
|
||||
]
|
||||
.spacing(10)
|
||||
|
||||
@@ -204,6 +204,7 @@ pub fn view_window<'a>(
|
||||
drawing_prefs: DrawingPrefs,
|
||||
folders: Folders,
|
||||
qnew_template: &'a str,
|
||||
color_book_dirs: &'a [String],
|
||||
double_click_block_refedit: bool,
|
||||
double_click_block_attedit: bool,
|
||||
cursor_type: CursorType,
|
||||
@@ -1697,6 +1698,49 @@ pub fn view_window<'a>(
|
||||
.align_y(iced::Center)
|
||||
};
|
||||
|
||||
// Searched for `.acb` colour books; editable, unlike the folders above.
|
||||
// Browsers have no folders, so the web build leaves it out.
|
||||
let color_book_locations = |dirs: &'a [String]| -> Element<'a, Message> {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
return Space::new().into();
|
||||
}
|
||||
let list = dirs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(column![].spacing(6), |list, (i, dir)| {
|
||||
list.push(
|
||||
row![
|
||||
text(dir.as_str()).size(11).width(Fill),
|
||||
button(text(crate::t!("Open folder")).size(11))
|
||||
.padding([4, 10])
|
||||
.style(button::secondary)
|
||||
.on_press(Message::OpenFolder(dir.clone())),
|
||||
button(text(crate::t!("Remove")).size(11))
|
||||
.padding([4, 10])
|
||||
.style(button::secondary)
|
||||
.on_press(Message::ColorBookDirRemove(i)),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
});
|
||||
column![
|
||||
text(crate::t!("Color book locations")).size(12),
|
||||
text(crate::t!(
|
||||
"Folders searched for .acb color books. A book loaded in Select Color is copied into the first folder."
|
||||
))
|
||||
.size(11),
|
||||
list,
|
||||
button(text(crate::t!("Add folder…")).size(11))
|
||||
.padding([4, 10])
|
||||
.style(button::secondary)
|
||||
.on_press(Message::ColorBookDirBrowse),
|
||||
Space::new().height(16),
|
||||
]
|
||||
.spacing(6)
|
||||
.into()
|
||||
};
|
||||
|
||||
let files = column![
|
||||
text(crate::t!("Files")).size(15),
|
||||
Space::new().height(6),
|
||||
@@ -1716,6 +1760,7 @@ pub fn view_window<'a>(
|
||||
Space::new().height(12),
|
||||
folder_row(crate::t!("Fonts"), folders.fonts.clone()),
|
||||
Space::new().height(16),
|
||||
color_book_locations(color_book_dirs),
|
||||
text(crate::t!("Default template for new drawings")).size(12),
|
||||
Space::new().height(4),
|
||||
row![
|
||||
|
||||
Reference in new issue
Block a user