feat(ui): bulk layer on/off, lock, freeze, transparency + Ctrl+A (#236)
The Layer Manager already applied colour/linetype/lineweight in bulk; extend that to the rest: - Visibility, lock and freeze toggles now act on every selected layer, not just the clicked row. The clicked row's new state becomes the target for all. Transparency edits apply to the whole selection too. - Clicking a linetype/lineweight combo on a row that is already part of a multi-selection no longer collapses the selection (the whole row is one mouse_area whose plain-click select would otherwise reset it), so those edits stay bulk. - Ctrl/Cmd+A selects every layer row while the Layer Manager is open, and all drawing objects otherwise. Two now-unused Scene layer-toggle helpers are removed (the handlers set the flags directly and bump once). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5e4e7c1020
commit
818638d3aa
5 changed files with 135 additions and 63 deletions
|
|
@ -1097,6 +1097,9 @@ pub enum Message {
|
||||||
/// system-clipboard read, which is the only paste path that works on the
|
/// system-clipboard read, which is the only paste path that works on the
|
||||||
/// web) or, with no editor open, the entity paste command.
|
/// web) or, with no editor open, the entity paste command.
|
||||||
PasteShortcut,
|
PasteShortcut,
|
||||||
|
/// Ctrl/Cmd+A — select all layer rows when the Layer Manager is open, or all
|
||||||
|
/// drawing objects otherwise (#236).
|
||||||
|
SelectAllShortcut,
|
||||||
/// System-clipboard text read for the MText editor (`None` = empty/denied).
|
/// System-clipboard text read for the MText editor (`None` = empty/denied).
|
||||||
MTextPasteClip(Option<String>),
|
MTextPasteClip(Option<String>),
|
||||||
/// System-clipboard text read for the single-line TEXT editor.
|
/// System-clipboard text read for the single-line TEXT editor.
|
||||||
|
|
|
||||||
|
|
@ -709,6 +709,22 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Layers a Layer-manager row toggle (visibility / lock / freeze /
|
||||||
|
/// transparency) should affect: the whole multi-selection when the clicked
|
||||||
|
/// row is part of it, otherwise just the clicked row — so those toggles work
|
||||||
|
/// in bulk like the color / linetype / lineweight edits already do (#236).
|
||||||
|
pub(super) fn layer_row_action_targets(&self, i: usize, idx: usize) -> Vec<String> {
|
||||||
|
let Some(clicked) = self.tabs[i].layers.layers.get(idx).map(|l| l.name.clone()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let selected = self.selected_layer_names(i);
|
||||||
|
if selected.iter().any(|n| n == &clicked) {
|
||||||
|
selected
|
||||||
|
} else {
|
||||||
|
vec![clicked]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Layer names for the current Layer-manager selection — every row in the
|
/// Layer names for the current Layer-manager selection — every row in the
|
||||||
/// multi-selection, or the anchor row when the multi-set is empty. Bulk
|
/// multi-selection, or the anchor row when the multi-set is empty. Bulk
|
||||||
/// property edits and deletion act on these.
|
/// property edits and deletion act on these.
|
||||||
|
|
|
||||||
|
|
@ -858,19 +858,32 @@ impl OpenCADStudio {
|
||||||
// ── Layer panel messages ───────────────────────────────────────
|
// ── Layer panel messages ───────────────────────────────────────
|
||||||
Message::LayerToggleVisible(idx) => {
|
Message::LayerToggleVisible(idx) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if idx < self.tabs[i].layers.layers.len() {
|
// New state = toggle of the clicked row, applied to every target
|
||||||
self.push_undo_snapshot(i, "LAYER OFF/ON");
|
// (the whole selection when the clicked row is part of it) (#236).
|
||||||
let l = &mut self.tabs[i].layers.layers[idx];
|
let on = self.tabs[i].layers.layers.get(idx).map(|l| !l.visible);
|
||||||
l.visible = !l.visible;
|
let targets = self.layer_row_action_targets(i, idx);
|
||||||
let name = l.name.clone();
|
if let Some(on) = on {
|
||||||
let on = l.visible;
|
if !targets.is_empty() {
|
||||||
self.tabs[i].scene.toggle_layer_visibility(&name);
|
self.push_undo_snapshot(i, "LAYER OFF/ON");
|
||||||
self.command_line.push_output(&format!(
|
for name in &targets {
|
||||||
"Layer \"{}\" {}",
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
name,
|
dl.flags.off = !on;
|
||||||
if on { "on" } else { "off" }
|
}
|
||||||
));
|
if let Some(pl) =
|
||||||
self.sync_ribbon_layers();
|
self.tabs[i].layers.layers.iter_mut().find(|l| &l.name == name)
|
||||||
|
{
|
||||||
|
pl.visible = on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.bump_geometry();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"{} layer(s) turned {}",
|
||||||
|
targets.len(),
|
||||||
|
if on { "on" } else { "off" }
|
||||||
|
));
|
||||||
|
self.sync_ribbon_layers();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -886,41 +899,64 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::LayerToggleLock(idx) => {
|
Message::LayerToggleLock(idx) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if idx < self.tabs[i].layers.layers.len() {
|
let locked = self.tabs[i].layers.layers.get(idx).map(|l| !l.locked);
|
||||||
self.push_undo_snapshot(i, "LAYER LOCK/UNLOCK");
|
let targets = self.layer_row_action_targets(i, idx);
|
||||||
let l = &mut self.tabs[i].layers.layers[idx];
|
if let Some(locked) = locked {
|
||||||
l.locked = !l.locked;
|
if !targets.is_empty() {
|
||||||
let name = l.name.clone();
|
self.push_undo_snapshot(i, "LAYER LOCK/UNLOCK");
|
||||||
let locked = l.locked;
|
for name in &targets {
|
||||||
self.tabs[i].scene.toggle_layer_lock(&name);
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
self.command_line.push_output(&format!(
|
dl.flags.locked = locked;
|
||||||
"Layer \"{}\" {}",
|
}
|
||||||
name,
|
if let Some(pl) =
|
||||||
if locked { "locked" } else { "unlocked" }
|
self.tabs[i].layers.layers.iter_mut().find(|l| &l.name == name)
|
||||||
));
|
{
|
||||||
self.sync_ribbon_layers();
|
pl.locked = locked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.bump_geometry();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"{} layer(s) {}",
|
||||||
|
targets.len(),
|
||||||
|
if locked { "locked" } else { "unlocked" }
|
||||||
|
));
|
||||||
|
self.sync_ribbon_layers();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::LayerToggleFreeze(idx) => {
|
Message::LayerToggleFreeze(idx) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if idx < self.tabs[i].layers.layers.len() {
|
let frozen = self.tabs[i].layers.layers.get(idx).map(|l| !l.frozen);
|
||||||
self.push_undo_snapshot(i, "LAYER FREEZE");
|
let targets = self.layer_row_action_targets(i, idx);
|
||||||
let l = &mut self.tabs[i].layers.layers[idx];
|
if let Some(frozen) = frozen {
|
||||||
l.frozen = !l.frozen;
|
if !targets.is_empty() {
|
||||||
let name = l.name.clone();
|
self.push_undo_snapshot(i, "LAYER FREEZE");
|
||||||
let frozen = l.frozen;
|
for name in &targets {
|
||||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||||
if frozen {
|
if frozen {
|
||||||
dl.freeze();
|
dl.freeze();
|
||||||
} else {
|
} else {
|
||||||
dl.thaw();
|
dl.thaw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(pl) =
|
||||||
|
self.tabs[i].layers.layers.iter_mut().find(|l| &l.name == name)
|
||||||
|
{
|
||||||
|
pl.frozen = frozen;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
self.tabs[i].scene.bump_geometry();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_output(&format!(
|
||||||
|
"{} layer(s) {}",
|
||||||
|
targets.len(),
|
||||||
|
if frozen { "frozen" } else { "thawed" }
|
||||||
|
));
|
||||||
|
self.sync_ribbon_layers();
|
||||||
}
|
}
|
||||||
self.tabs[i].scene.bump_geometry();
|
|
||||||
self.tabs[i].dirty = true;
|
|
||||||
self.sync_ribbon_layers();
|
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -956,8 +992,11 @@ impl OpenCADStudio {
|
||||||
let anchor = panel.selected.unwrap_or(idx);
|
let anchor = panel.selected.unwrap_or(idx);
|
||||||
let (lo, hi) = (anchor.min(idx), anchor.max(idx));
|
let (lo, hi) = (anchor.min(idx), anchor.max(idx));
|
||||||
panel.selected_multi = (lo..=hi).collect();
|
panel.selected_multi = (lo..=hi).collect();
|
||||||
} else {
|
} else if !panel.selected_multi.contains(&idx) {
|
||||||
// Plain click selects just this row.
|
// Plain click collapses to this row — but NOT when it is
|
||||||
|
// already part of a multi-selection, so clicking a property
|
||||||
|
// combo (linetype / lineweight) on one of the selected rows
|
||||||
|
// keeps the selection and the edit stays bulk (#236).
|
||||||
panel.selected_multi = vec![idx];
|
panel.selected_multi = vec![idx];
|
||||||
}
|
}
|
||||||
panel.selected = Some(idx);
|
panel.selected = Some(idx);
|
||||||
|
|
@ -1082,11 +1121,22 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
Message::LayerTransparencyEdit(idx, s) => {
|
Message::LayerTransparencyEdit(idx, s) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) {
|
let val = if let Ok(v) = s.parse::<i32>() {
|
||||||
if let Ok(v) = s.parse::<i32>() {
|
Some(v.clamp(0, 90))
|
||||||
pl.transparency = v.clamp(0, 90);
|
} else if s.is_empty() {
|
||||||
} else if s.is_empty() {
|
Some(0)
|
||||||
pl.transparency = 0;
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
// Apply the edited transparency to every selected layer (#236).
|
||||||
|
if let Some(v) = val {
|
||||||
|
let targets = self.layer_row_action_targets(i, idx);
|
||||||
|
for name in &targets {
|
||||||
|
if let Some(pl) =
|
||||||
|
self.tabs[i].layers.layers.iter_mut().find(|l| &l.name == name)
|
||||||
|
{
|
||||||
|
pl.transparency = v;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -1792,6 +1842,19 @@ impl OpenCADStudio {
|
||||||
// text_input expects is empty. With no editor open it falls through
|
// text_input expects is empty. With no editor open it falls through
|
||||||
// to the entity paste command.
|
// to the entity paste command.
|
||||||
Message::PasteShortcut => self.on_paste_shortcut(),
|
Message::PasteShortcut => self.on_paste_shortcut(),
|
||||||
|
|
||||||
|
Message::SelectAllShortcut => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
if self.active_modal == Some(super::ModalKind::Layers) {
|
||||||
|
// Select every row in the Layer Manager (#236).
|
||||||
|
let n = self.tabs[i].layers.layers.len();
|
||||||
|
self.tabs[i].layers.selected_multi = (0..n).collect();
|
||||||
|
self.tabs[i].layers.selected = (n > 0).then_some(0);
|
||||||
|
Task::none()
|
||||||
|
} else {
|
||||||
|
self.dispatch_command("SELECTALL")
|
||||||
|
}
|
||||||
|
}
|
||||||
Message::MTextPasteClip(text) => {
|
Message::MTextPasteClip(text) => {
|
||||||
if let Some(text) = text.filter(|t| !t.is_empty()) {
|
if let Some(text) = text.filter(|t| !t.is_empty()) {
|
||||||
// CR/LF arrive as line breaks; MText keeps "\n", drop "\r".
|
// CR/LF arrive as line breaks; MText keeps "\n", drop "\r".
|
||||||
|
|
|
||||||
|
|
@ -1605,6 +1605,12 @@ impl OpenCADStudio {
|
||||||
"z" if !shift => Some(Message::Undo),
|
"z" if !shift => Some(Message::Undo),
|
||||||
"z" if shift => Some(Message::Redo),
|
"z" if shift => Some(Message::Redo),
|
||||||
"y" => Some(Message::Redo),
|
"y" => Some(Message::Redo),
|
||||||
|
// Ctrl/Cmd+A: select all layer rows when the
|
||||||
|
// Layer Manager is open, else all objects. The
|
||||||
|
// update handler branches on the active modal.
|
||||||
|
"a" if status == Status::Ignored => {
|
||||||
|
Some(Message::SelectAllShortcut)
|
||||||
|
}
|
||||||
// Clipboard accelerators defer to a focused
|
// Clipboard accelerators defer to a focused
|
||||||
// text widget: when the command-line history
|
// text widget: when the command-line history
|
||||||
// editor (or any text field) captures Ctrl+C/
|
// editor (or any text field) captures Ctrl+C/
|
||||||
|
|
|
||||||
|
|
@ -81,20 +81,4 @@ impl Scene {
|
||||||
self.bump_selection();
|
self.bump_selection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Layer helpers ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
pub fn toggle_layer_visibility(&mut self, name: &str) {
|
|
||||||
if let Some(layer) = self.document.layers.get_mut(name) {
|
|
||||||
layer.flags.off = !layer.flags.off;
|
|
||||||
}
|
|
||||||
self.bump_geometry();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn toggle_layer_lock(&mut self, name: &str) {
|
|
||||||
if let Some(layer) = self.document.layers.get_mut(name) {
|
|
||||||
layer.flags.locked = !layer.flags.locked;
|
|
||||||
}
|
|
||||||
// Re-tessellate so the locked-layer dim (fade) appears/clears at once.
|
|
||||||
self.bump_geometry();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue