forked from 1b-agent/opencadstudio-upstream
feat(block-editor): add functional Visibility ribbon controls
Add the current-state selector, Visibility States dialog and mode/show/hide controls to the contextual ribbon, using shared icons, tooltips, modal UI and adaptive flyouts. Disable the controls until a Visibility parameter exists. Stage state creation, rename, deletion, current-state changes and reordering until OK. Support copied, show-all and hide-all new states in one undo step, preserve native graph metadata, and restore current-state identity on undo/redo. Implement nonsaved BVMODE and display-only reveal during BVSHOW selection. Cancelling selection leaves saved visibility flags and history unchanged. Keep the existing command-line state workflows available. Validation: 6 targeted tests; 277 block, 31 app dock, 35 ribbon and 12 history checks passed (overlapping filters). All 17 local DWG fixtures passed in-memory DWG/DXF preservation checks with unchanged original checksums.
This commit is contained in:
1 parent
308990e672
commit
daa26007f9
24 files changed
+1341
-34
No files matched your search
@@ -250,6 +250,7 @@ impl OpenCADStudio {
|
||||
return None;
|
||||
}
|
||||
Some(DeltaSnapshot {
|
||||
visibility_states: None,
|
||||
entities,
|
||||
current_layout_before: session.return_layout.clone(),
|
||||
current_layout_after: session.return_layout.clone(),
|
||||
|
||||
@@ -59,6 +59,239 @@ fn feed(app: &mut OpenCADStudio, text: &str) {
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
}
|
||||
|
||||
fn visibility_editor() -> (OpenCADStudio, crate::ui::window::block_visibility::Target) {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let _ = app.run_command_line("BPARAMETER VISIBILITY 0,0");
|
||||
let target = app
|
||||
.block_visibility_context()
|
||||
.expect("placed parameter")
|
||||
.target;
|
||||
(app, target)
|
||||
}
|
||||
|
||||
fn visibility_action(app: &mut OpenCADStudio, action: crate::ui::window::block_visibility::Action) {
|
||||
let _ = app.update(Message::BlockVisibility(action));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_ribbon_manager_cancel_validation_and_single_undo() {
|
||||
use crate::ui::window::block_visibility::Action as A;
|
||||
let (mut app, target) = visibility_editor();
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
visibility_action(&mut app, A::Open(target));
|
||||
visibility_action(&mut app, A::Name("Alternate".into()));
|
||||
visibility_action(&mut app, A::New);
|
||||
assert_eq!(app.tabs[i].scene.document, before, "dialog is staged");
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert!(app.block_visibility.is_none());
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
visibility_action(&mut app, A::Open(target));
|
||||
visibility_action(&mut app, A::Name("visibilitystate0".into()));
|
||||
visibility_action(&mut app, A::New);
|
||||
assert!(app.block_visibility.as_ref().unwrap().error.is_some());
|
||||
visibility_action(&mut app, A::Delete);
|
||||
assert_eq!(
|
||||
app.block_visibility.as_ref().unwrap().edited.states.len(),
|
||||
1
|
||||
);
|
||||
visibility_action(&mut app, A::Name("Alternate".into()));
|
||||
visibility_action(&mut app, A::New);
|
||||
visibility_action(&mut app, A::SetCurrent);
|
||||
visibility_action(&mut app, A::Name("Alternate renamed".into()));
|
||||
visibility_action(&mut app, A::Rename);
|
||||
visibility_action(&mut app, A::Move(true));
|
||||
visibility_action(&mut app, A::Apply);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
|
||||
assert_eq!(
|
||||
app.block_visibility_context().unwrap().current.as_deref(),
|
||||
Some("Alternate renamed")
|
||||
);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(
|
||||
app.block_visibility_context().unwrap().names,
|
||||
["VisibilityState0"]
|
||||
);
|
||||
app.redo_steps(1);
|
||||
assert_eq!(
|
||||
app.block_visibility_context().unwrap().names,
|
||||
["Alternate renamed", "VisibilityState0"]
|
||||
);
|
||||
assert_eq!(
|
||||
app.block_visibility_context().unwrap().current.as_deref(),
|
||||
Some("Alternate renamed")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_ribbon_new_modes_state_selection_and_native_roundtrip() {
|
||||
use crate::ui::window::block_visibility::{Action as A, NewVisibility as V};
|
||||
let (mut app, target) = visibility_editor();
|
||||
let i = app.active_tab;
|
||||
let member = members(&app)[0];
|
||||
visibility_action(&mut app, A::Open(target));
|
||||
for (name, mode) in [("Hidden", V::Hide), ("Shown", V::Show), ("Copy", V::Keep)] {
|
||||
visibility_action(&mut app, A::Name(name.into()));
|
||||
visibility_action(&mut app, A::NewVisibility(mode));
|
||||
visibility_action(&mut app, A::New);
|
||||
}
|
||||
visibility_action(&mut app, A::Apply);
|
||||
let parameter =
|
||||
crate::scene::block_visibility::parameter(&app.tabs[i].scene.document, target.definition)
|
||||
.unwrap();
|
||||
assert!(parameter.states[0].visible_blocks.contains(&member));
|
||||
assert!(parameter.states[1].visible_blocks.is_empty());
|
||||
assert!(parameter.states[2].visible_blocks.contains(&member));
|
||||
assert!(parameter.states[3].visible_blocks.contains(&member));
|
||||
visibility_action(&mut app, A::Current(target, "Hidden".into()));
|
||||
assert!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(member)
|
||||
.unwrap()
|
||||
.common()
|
||||
.invisible
|
||||
);
|
||||
visibility_action(&mut app, A::Current(target, "Shown".into()));
|
||||
visibility_action(&mut app, A::Current(target, "Copy".into()));
|
||||
app.undo_steps(1);
|
||||
assert_eq!(
|
||||
app.block_visibility_context().unwrap().current.as_deref(),
|
||||
Some("Shown"),
|
||||
"identical states retain their identity in history"
|
||||
);
|
||||
app.redo_steps(1);
|
||||
assert_eq!(
|
||||
app.block_visibility_context().unwrap().current.as_deref(),
|
||||
Some("Copy")
|
||||
);
|
||||
app.save_block_edit(i);
|
||||
for ext in ["dwg", "dxf"] {
|
||||
let document = &app.tabs[i].scene.document;
|
||||
let bytes = crate::io::save_to_bytes(document, ext, document.version).unwrap();
|
||||
let loaded = crate::io::load_bytes(&format!("visibility.{ext}"), bytes).unwrap();
|
||||
let parameter =
|
||||
crate::scene::block_visibility::parameter(&loaded, target.definition).unwrap();
|
||||
assert_eq!(
|
||||
parameter
|
||||
.states
|
||||
.iter()
|
||||
.map(|state| state.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["VisibilityState0", "Hidden", "Shown", "Copy"]
|
||||
);
|
||||
assert!(parameter.states[1].visible_blocks.is_empty());
|
||||
assert!(parameter.states[2].visible_blocks.contains(&member));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_ribbon_mode_and_make_visible_cancel_do_not_change_saved_flags() {
|
||||
use crate::ui::window::block_visibility::Action as A;
|
||||
let (mut app, target) = visibility_editor();
|
||||
let i = app.active_tab;
|
||||
let member = members(&app)[0];
|
||||
app.tabs[i].scene.select_entity(member, true);
|
||||
let _ = app.run_command_line("BVHIDE");
|
||||
app.finish_pending_history(i);
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
let dirty = app.tabs[i].dirty;
|
||||
let wires = |app: &OpenCADStudio| {
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.entity_wires()
|
||||
.iter()
|
||||
.filter(|wire| crate::scene::Scene::handle_from_wire_name(&wire.name) == Some(member))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert!(wires(&app).is_empty());
|
||||
visibility_action(&mut app, A::Mode(target));
|
||||
assert!(
|
||||
!wires(&app).is_empty(),
|
||||
"hidden member is rendered for picking"
|
||||
);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
visibility_action(&mut app, A::Mode(target));
|
||||
assert!(wires(&app).is_empty());
|
||||
let _ = app.run_command_line("BVSHOW");
|
||||
app.sync_block_visibility_display();
|
||||
assert!(!wires(&app).is_empty());
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert!(wires(&app).is_empty());
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
|
||||
assert_eq!(app.tabs[i].dirty, dirty);
|
||||
let _ = app.run_command_line("BVMODE 1");
|
||||
assert!(app.block_visibility_mode);
|
||||
let _ = app.run_command_line("SETVAR BVMODE 2");
|
||||
assert!(app.block_visibility_mode);
|
||||
let _ = app.run_command_line("BVMODE 0");
|
||||
assert!(!app.block_visibility_mode);
|
||||
assert!(
|
||||
app.pending_setvar.is_none(),
|
||||
"inline variable must close its probe prompt"
|
||||
);
|
||||
let _ = app.run_command_line("BVSHOW");
|
||||
app.tabs[i].scene.select_entity(member, true);
|
||||
let result = app.tabs[i]
|
||||
.active_cmd
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_selection_complete(vec![member]);
|
||||
let _ = app.apply_cmd_result(result);
|
||||
feed(&mut app, "");
|
||||
assert!(
|
||||
!app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(member)
|
||||
.unwrap()
|
||||
.common()
|
||||
.invisible
|
||||
);
|
||||
app.undo_steps(1);
|
||||
assert!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(member)
|
||||
.unwrap()
|
||||
.common()
|
||||
.invisible,
|
||||
"undo uses the hidden flags from before picking"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_ribbon_manager_rejects_stale_target_and_stale_document() {
|
||||
use crate::ui::window::block_visibility::Action as A;
|
||||
let (mut app, target) = visibility_editor();
|
||||
visibility_action(&mut app, A::Open(target));
|
||||
visibility_action(&mut app, A::Name("Alternate".into()));
|
||||
visibility_action(&mut app, A::New);
|
||||
app.tabs[app.active_tab].scene.bump_geometry();
|
||||
visibility_action(&mut app, A::Apply);
|
||||
assert!(app.block_visibility.as_ref().unwrap().error.is_some());
|
||||
assert_eq!(
|
||||
app.block_visibility_context().unwrap().names,
|
||||
["VisibilityState0"]
|
||||
);
|
||||
let _ = app.update(Message::CloseModal);
|
||||
let mut stale = target;
|
||||
stale.document += 1;
|
||||
visibility_action(&mut app, A::Open(stale));
|
||||
assert!(app.block_visibility.is_none());
|
||||
assert!(app.active_modal.is_none());
|
||||
let _ = app.run_command_line("BVSTATE DIALOG");
|
||||
assert_eq!(app.active_modal, Some(ModalKind::BlockVisibility));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_undo_preserves_imported_native_networks_when_constraints_are_unchanged() {
|
||||
use codec::objects::{
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
use super::{Message, ModalKind, OpenCADStudio};
|
||||
use crate::scene::block_visibility as visibility;
|
||||
use crate::ui::window::block_visibility::{Action, NewVisibility, RibbonContext, State, Target};
|
||||
use iced::Task;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn block_visibility_context(&self) -> Option<RibbonContext> {
|
||||
let tab = self.tabs.get(self.active_tab)?;
|
||||
let definition = tab.active_block_edit_session()?.br_handle;
|
||||
let parameter = visibility::parameter(&tab.scene.document, definition)?;
|
||||
if parameter.states.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let current = self.visibility_current(self.active_tab, definition);
|
||||
Some(RibbonContext {
|
||||
target: Target {
|
||||
document: tab.id,
|
||||
definition,
|
||||
parameter: parameter.handle,
|
||||
},
|
||||
names: parameter
|
||||
.states
|
||||
.iter()
|
||||
.map(|state| state.name.clone())
|
||||
.collect(),
|
||||
current: parameter
|
||||
.states
|
||||
.get(current)
|
||||
.map(|state| state.name.clone()),
|
||||
mode: self.block_visibility_mode,
|
||||
})
|
||||
}
|
||||
|
||||
fn visibility_target_valid(&self, target: Target) -> bool {
|
||||
self.block_visibility_context()
|
||||
.is_some_and(|context| context.target == target)
|
||||
}
|
||||
|
||||
pub(super) fn sync_block_visibility_display(&mut self) {
|
||||
for tab in &mut self.tabs {
|
||||
let definition = tab
|
||||
.active_block_edit_session()
|
||||
.map(|session| session.br_handle);
|
||||
// Resolve imported state flags once when an editor opens, not on
|
||||
// every view refresh. Subsequent state identity follows history.
|
||||
if let Some(definition) = definition {
|
||||
if !tab.scene.visibility_states.contains_key(&definition) {
|
||||
if let Some(parameter) = visibility::parameter(&tab.scene.document, definition)
|
||||
{
|
||||
let current =
|
||||
visibility::shown_state(&tab.scene.document, parameter.handle);
|
||||
tab.scene.visibility_states.insert(definition, current);
|
||||
}
|
||||
}
|
||||
}
|
||||
let editing = definition.is_some();
|
||||
let picking = tab
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.reveals_hidden_block_objects());
|
||||
tab.scene
|
||||
.set_block_visibility_reveal(editing && (self.block_visibility_mode || picking));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn handle_block_visibility(&mut self, action: Action) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
match action {
|
||||
Action::Open(target) => {
|
||||
if !self.visibility_target_valid(target) {
|
||||
return Task::none();
|
||||
}
|
||||
// A ribbon click can interrupt a running authoring command.
|
||||
let cancellation =
|
||||
if self.tabs[i].active_cmd.is_some() || self.pending_setvar.is_some() {
|
||||
self.on_command_escape()
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
self.sync_block_visibility_display();
|
||||
let original =
|
||||
visibility::parameter(&self.tabs[i].scene.document, target.definition)
|
||||
.unwrap()
|
||||
.clone();
|
||||
let current = self
|
||||
.visibility_current(i, target.definition)
|
||||
.min(original.states.len().saturating_sub(1));
|
||||
let name = original
|
||||
.states
|
||||
.get(current)
|
||||
.map(|state| state.name.clone())
|
||||
.unwrap_or_default();
|
||||
self.block_visibility = Some(State {
|
||||
target,
|
||||
edited: original.clone(),
|
||||
original,
|
||||
current,
|
||||
selected: current,
|
||||
name,
|
||||
new_visibility: NewVisibility::Keep,
|
||||
error: None,
|
||||
epoch: self.tabs[i].scene.geometry_epoch,
|
||||
});
|
||||
self.active_modal = Some(ModalKind::BlockVisibility);
|
||||
return cancellation;
|
||||
}
|
||||
Action::Current(target, name) => {
|
||||
if !self.visibility_target_valid(target) {
|
||||
return Task::none();
|
||||
}
|
||||
let parameter =
|
||||
visibility::parameter(&self.tabs[i].scene.document, target.definition).unwrap();
|
||||
let Some(index) = parameter.states.iter().position(|state| state.name == name)
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
if self.visibility_current(i, target.definition) != index {
|
||||
let cancellation =
|
||||
if self.tabs[i].active_cmd.is_some() || self.pending_setvar.is_some() {
|
||||
self.on_command_escape()
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
self.push_undo_snapshot(i, "Visibility state");
|
||||
self.show_visibility_state(i, target.definition, target.parameter, index);
|
||||
self.finish_pending_history(i);
|
||||
return cancellation;
|
||||
}
|
||||
}
|
||||
Action::Mode(target) => {
|
||||
if self.visibility_target_valid(target) {
|
||||
self.block_visibility_mode = !self.block_visibility_mode;
|
||||
self.sync_block_visibility_display();
|
||||
}
|
||||
}
|
||||
Action::Apply => {
|
||||
let Some(state) = self.block_visibility.as_ref() else {
|
||||
return Task::none();
|
||||
};
|
||||
if !self.visibility_target_valid(state.target)
|
||||
|| self.tabs[i].scene.geometry_epoch != state.epoch
|
||||
|| visibility::parameter(&self.tabs[i].scene.document, state.target.definition)
|
||||
!= Some(&state.original)
|
||||
{
|
||||
self.block_visibility.as_mut().unwrap().error = Some(
|
||||
crate::t!("The block changed. Cancel and reopen Visibility States.")
|
||||
.into_owned(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let state = self.block_visibility.take().unwrap();
|
||||
if state.edited != state.original
|
||||
|| state.current != self.visibility_current(i, state.target.definition)
|
||||
{
|
||||
self.push_undo_snapshot(i, "Visibility states");
|
||||
visibility::replace_states(
|
||||
&mut self.tabs[i].scene.document,
|
||||
state.target.definition,
|
||||
state.edited,
|
||||
);
|
||||
self.show_visibility_state(
|
||||
i,
|
||||
state.target.definition,
|
||||
state.target.parameter,
|
||||
state.current,
|
||||
);
|
||||
self.finish_pending_history(i);
|
||||
self.refresh_properties();
|
||||
}
|
||||
self.active_modal = None;
|
||||
}
|
||||
action => {
|
||||
let Some(state) = self.block_visibility.as_mut() else {
|
||||
return Task::none();
|
||||
};
|
||||
state.error = None;
|
||||
match action {
|
||||
Action::Select(index) => state.select(index),
|
||||
Action::Name(name) => state.name = name,
|
||||
Action::NewVisibility(mode) => state.new_visibility = mode,
|
||||
Action::New | Action::Rename => {
|
||||
let rename = matches!(action, Action::Rename);
|
||||
if let Some(error) = state.name_error(rename) {
|
||||
state.error = Some(error);
|
||||
return Task::none();
|
||||
}
|
||||
let name = state.name.trim().to_string();
|
||||
if rename {
|
||||
state.edited.states[state.selected].name = name;
|
||||
} else {
|
||||
let mut new = state.edited.states[state.current].clone();
|
||||
new.name = name;
|
||||
if state.new_visibility != NewVisibility::Keep {
|
||||
// Newly controlled geometry was visible in all existing states.
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let members = document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|record| record.handle == state.target.definition)
|
||||
.map(|record| record.entity_handles.clone())
|
||||
.unwrap_or_default();
|
||||
for handle in members {
|
||||
if !state.edited.all_blocks.contains(&handle) {
|
||||
state.edited.all_blocks.push(handle);
|
||||
if document
|
||||
.get_entity(handle)
|
||||
.is_some_and(|entity| !entity.common().invisible)
|
||||
{
|
||||
for old in &mut state.edited.states {
|
||||
old.visible_blocks.push(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
new.visible_blocks = if state.new_visibility == NewVisibility::Show
|
||||
{
|
||||
state.edited.all_blocks.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
}
|
||||
state.edited.states.push(new);
|
||||
state.select(state.edited.states.len() - 1);
|
||||
}
|
||||
}
|
||||
Action::Delete if state.edited.states.len() > 1 => {
|
||||
let removed = state.selected;
|
||||
state.edited.states.remove(removed);
|
||||
if state.current == removed {
|
||||
state.current = 0;
|
||||
} else if state.current > removed {
|
||||
state.current -= 1;
|
||||
}
|
||||
state.select(removed.min(state.edited.states.len() - 1));
|
||||
}
|
||||
Action::SetCurrent => state.current = state.selected,
|
||||
Action::Move(up) => {
|
||||
let other = if up {
|
||||
state.selected.checked_sub(1)
|
||||
} else {
|
||||
Some(state.selected + 1)
|
||||
};
|
||||
if let Some(other) =
|
||||
other.filter(|other| *other < state.edited.states.len())
|
||||
{
|
||||
state.edited.states.swap(state.selected, other);
|
||||
if state.current == state.selected {
|
||||
state.current = other;
|
||||
} else if state.current == other {
|
||||
state.current = state.selected;
|
||||
}
|
||||
state.select(other);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
@@ -97,12 +97,16 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
/// The state the block shows in the editor.
|
||||
fn visibility_current(&self, i: usize, block: Handle) -> usize {
|
||||
pub(in crate::app) fn visibility_current(&self, i: usize, block: Handle) -> usize {
|
||||
let scene = &self.tabs[i].scene;
|
||||
scene
|
||||
.visibility_states
|
||||
.get(&block)
|
||||
.copied()
|
||||
.filter(|index| {
|
||||
visibility::parameter(&scene.document, block)
|
||||
.is_some_and(|parameter| *index < parameter.states.len())
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
visibility::parameter(&scene.document, block).map_or(0, |parameter| {
|
||||
visibility::shown_state(&scene.document, parameter.handle)
|
||||
@@ -110,7 +114,13 @@ impl OpenCADStudio {
|
||||
})
|
||||
}
|
||||
|
||||
fn show_visibility_state(&mut self, i: usize, block: Handle, parameter: Handle, state: usize) {
|
||||
pub(in crate::app) fn show_visibility_state(
|
||||
&mut self,
|
||||
i: usize,
|
||||
block: Handle,
|
||||
parameter: Handle,
|
||||
state: usize,
|
||||
) {
|
||||
visibility::apply_state(&mut self.tabs[i].scene.document, parameter, state);
|
||||
self.tabs[i].scene.visibility_states.insert(block, state);
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
@@ -143,6 +153,40 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
let variable = cmd.strip_prefix("SETVAR ").unwrap_or(cmd);
|
||||
if variable == "BVMODE" || variable.starts_with("BVMODE ") {
|
||||
let value = variable.strip_prefix("BVMODE").unwrap().trim();
|
||||
// Inline command probing may have opened the bare variable prompt.
|
||||
if !value.is_empty() && self.pending_setvar.as_deref() == Some("BVMODE") {
|
||||
self.pending_setvar = None;
|
||||
}
|
||||
match value {
|
||||
"" => {
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"BVMODE: enter 0 (hide) or 1 (show dimmed) <{}>:",
|
||||
u8::from(self.block_visibility_mode)
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
self.pending_setvar = Some("BVMODE".into());
|
||||
}
|
||||
"TOGGLE" => {
|
||||
self.block_visibility_mode = !self.block_visibility_mode;
|
||||
self.sync_block_visibility_display();
|
||||
}
|
||||
value @ ("0" | "1") => {
|
||||
self.block_visibility_mode = value == "1";
|
||||
self.sync_block_visibility_display();
|
||||
self.command_line
|
||||
.push_output(crate::tf!("BVMODE = {}", value).as_ref());
|
||||
}
|
||||
_ => self
|
||||
.command_line
|
||||
.push_error(crate::t!("BVMODE: only 0 or 1 is allowed.").as_ref()),
|
||||
}
|
||||
return Some(Task::none());
|
||||
}
|
||||
match cmd {
|
||||
cmd if cmd == "BPARAMETER" || cmd.starts_with("BPARAMETER ") => {
|
||||
let Some(block) = self.visibility_block(i, "BPARAMETER") else {
|
||||
@@ -165,6 +209,17 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
Some(Task::none())
|
||||
}
|
||||
"BVSTATE DIALOG" => {
|
||||
if let Some(context) = self.block_visibility_context() {
|
||||
return Some(self.handle_block_visibility(
|
||||
crate::ui::window::block_visibility::Action::Open(context.target),
|
||||
));
|
||||
}
|
||||
self.command_line.push_error(
|
||||
crate::t!("Place a Visibility parameter in the Block Editor first.").as_ref(),
|
||||
);
|
||||
Some(Task::none())
|
||||
}
|
||||
"BVSTATE" => {
|
||||
let block = self.visibility_block(i, "BVSTATE")?;
|
||||
let document = &self.tabs[i].scene.document;
|
||||
@@ -221,25 +276,8 @@ impl OpenCADStudio {
|
||||
Some(Task::none())
|
||||
}
|
||||
"BVHIDE" | "BVSHOW" => {
|
||||
let block = self.visibility_block(i, cmd)?;
|
||||
self.visibility_block(i, cmd)?;
|
||||
let hide = cmd == "BVHIDE";
|
||||
if !hide {
|
||||
// Every object of the block shows while they are picked;
|
||||
// the state is put back once the pick ends.
|
||||
let document = &mut self.tabs[i].scene.document;
|
||||
let members: Vec<Handle> = document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|record| record.handle == block)
|
||||
.map(|record| record.entity_handles.clone())
|
||||
.unwrap_or_default();
|
||||
for handle in members {
|
||||
if let Some(entity) = document.get_entity_mut(handle) {
|
||||
entity.common_mut().invisible = false;
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
}
|
||||
let pending = if hide { "_BVHIDE" } else { "_BVSHOW" };
|
||||
if !self.tabs[i].scene.selected.is_empty() {
|
||||
return Some(self.dispatch_command(pending));
|
||||
|
||||
@@ -692,6 +692,97 @@ fn floating_menu_docks_and_allow_docking_keeps_a_drag_floating() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_ribbon_real_controls_disable_enable_and_open_manager() {
|
||||
with_stack(|| {
|
||||
use crate::ui::window::block_visibility::Action as A;
|
||||
let mut app = authoring_app();
|
||||
app.ribbon.set_block_editing(true);
|
||||
app.ribbon
|
||||
.set_collapse_mode(crate::ui::ribbon::CollapseMode::Compact);
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let point = authoring_control(&app, "BVSTATE_MANAGER");
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.block_visibility.is_none());
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
authoring_capture(&app, "visibility-ribbon-disabled");
|
||||
let _ = app.run_command_line("BPARAMETER VISIBILITY 0.5,0.5");
|
||||
let target = app.block_visibility_context().unwrap().target;
|
||||
let point = authoring_control(&app, "BVSTATE_MANAGER");
|
||||
authoring_click(&mut app, point);
|
||||
assert_eq!(
|
||||
app.active_modal,
|
||||
Some(crate::app::ModalKind::BlockVisibility)
|
||||
);
|
||||
let _ = app.update(Message::BlockVisibility(A::Name("Alternate".into())));
|
||||
let _ = app.update(Message::BlockVisibility(A::New));
|
||||
authoring_capture(&app, "visibility-states-dialog");
|
||||
let point = find_in_view(&app, "OK");
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.block_visibility.is_none());
|
||||
assert_eq!(app.block_visibility_context().unwrap().names.len(), 2);
|
||||
let messages = {
|
||||
let mut simulator = iced_test::Simulator::with_size(
|
||||
iced::Settings::default(),
|
||||
VIEW_SIZE,
|
||||
app.view_main(),
|
||||
);
|
||||
let target = simulator
|
||||
.click(iced::widget::Id::new("BVSTATE_CURRENT"))
|
||||
.expect("current-state selector");
|
||||
let bounds = target.visible_bounds().unwrap();
|
||||
authoring_snapshot(
|
||||
&mut simulator,
|
||||
&app.active_theme,
|
||||
"visibility-state-dropdown",
|
||||
);
|
||||
// Native pick-list options are painted text, not Text widgets.
|
||||
// Pick the second row using the measured selector/row height.
|
||||
let position = iced::Point::new(bounds.center_x(), bounds.y + bounds.height * 2.5);
|
||||
simulator.point_at(position);
|
||||
simulator.simulate([
|
||||
iced::Event::Mouse(iced::mouse::Event::CursorMoved { position }),
|
||||
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left)),
|
||||
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(
|
||||
iced::mouse::Button::Left,
|
||||
)),
|
||||
]);
|
||||
simulator.into_messages().collect::<Vec<_>>()
|
||||
};
|
||||
for message in messages {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert_eq!(
|
||||
app.block_visibility_context().unwrap().current.as_deref(),
|
||||
Some("Alternate")
|
||||
);
|
||||
let point = authoring_control(&app, "BVMODE_TOGGLE");
|
||||
authoring_click(&mut app, point);
|
||||
assert!(app.block_visibility_mode);
|
||||
authoring_capture(&app, "visibility-ribbon-enabled");
|
||||
app.ribbon
|
||||
.set_collapse_mode(crate::ui::ribbon::CollapseMode::Collapsed);
|
||||
let point = find_in_view(&app, "Visibility ▾");
|
||||
authoring_click(&mut app, point);
|
||||
let messages = {
|
||||
let mut simulator = iced_test::Simulator::with_size(
|
||||
iced::Settings::default(),
|
||||
VIEW_SIZE,
|
||||
app.view_main(),
|
||||
);
|
||||
simulator
|
||||
.click("Visibility States")
|
||||
.expect("manager in flyout");
|
||||
simulator.into_messages().collect::<Vec<_>>()
|
||||
};
|
||||
for message in messages {
|
||||
let _ = app.update(message);
|
||||
}
|
||||
assert_eq!(app.block_visibility.as_ref().unwrap().target, target);
|
||||
});
|
||||
}
|
||||
|
||||
fn authoring_app() -> OpenCADStudio {
|
||||
use codec::{
|
||||
entities::{Insert, Line},
|
||||
|
||||
@@ -797,6 +797,9 @@ impl HistorySnapshot {
|
||||
.saturating_add(d.named_parameters.as_ref().map_or(0, |(before, after)| {
|
||||
before.len().saturating_add(after.len()).saturating_mul(128)
|
||||
}))
|
||||
.saturating_add(d.visibility_states.as_ref().map_or(0, |(before, after)| {
|
||||
(before.len() + after.len()).saturating_mul(32)
|
||||
}))
|
||||
.saturating_add(d.reference_designations.len().saturating_mul(16))
|
||||
.saturating_add(d.label.len()),
|
||||
HistorySnapshot::ObjectVisibility(v) => v
|
||||
@@ -833,6 +836,11 @@ pub(super) struct ObjectVisibilitySnapshot {
|
||||
/// cloning the flat entity store.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct DeltaSnapshot {
|
||||
/// Editor current-state indices must follow undo even when two states show identical objects.
|
||||
pub(super) visibility_states: Option<(
|
||||
rustc_hash::FxHashMap<Handle, usize>,
|
||||
rustc_hash::FxHashMap<Handle, usize>,
|
||||
)>,
|
||||
pub(super) entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)>,
|
||||
pub(super) current_layout_before: String,
|
||||
pub(super) current_layout_after: String,
|
||||
@@ -949,6 +957,7 @@ pub(super) struct HistoryState {
|
||||
}
|
||||
|
||||
pub(super) struct PendingHistorySnapshot {
|
||||
pub(super) visibility_states_before: rustc_hash::FxHashMap<Handle, usize>,
|
||||
pub(super) label: String,
|
||||
pub(super) current_layout: String,
|
||||
pub(super) active_layer: String,
|
||||
|
||||
@@ -216,6 +216,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
let selected: Vec<Handle> = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let delta = DeltaSnapshot {
|
||||
visibility_states: None,
|
||||
entities,
|
||||
current_layout_before: self.tabs[i].scene.current_layout.clone(),
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
@@ -346,7 +347,11 @@ impl OpenCADStudio {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let visibility_after = self.tabs[i].scene.visibility_states.clone();
|
||||
let visibility_states = (pending.visibility_states_before != visibility_after)
|
||||
.then_some((pending.visibility_states_before, visibility_after));
|
||||
if !pending.keep_empty
|
||||
&& visibility_states.is_none()
|
||||
&& entities.is_empty()
|
||||
&& reference_designations.is_empty()
|
||||
&& !structure_changed
|
||||
@@ -357,6 +362,7 @@ impl OpenCADStudio {
|
||||
return;
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
visibility_states,
|
||||
entities,
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
@@ -403,6 +409,7 @@ impl OpenCADStudio {
|
||||
let structure_before = self.tabs[i].scene.document.snapshot_structure();
|
||||
let recorder = self.tabs[i].scene.document.begin_entity_change_recording();
|
||||
self.tabs[i].history.pending = Some(PendingHistorySnapshot {
|
||||
visibility_states_before: self.tabs[i].scene.visibility_states.clone(),
|
||||
label,
|
||||
current_layout,
|
||||
active_layer: self.tabs[i].active_layer.clone(),
|
||||
@@ -514,6 +521,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let delta = DeltaSnapshot {
|
||||
visibility_states: None,
|
||||
entities: Vec::new(),
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
@@ -572,6 +580,7 @@ impl OpenCADStudio {
|
||||
return;
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
visibility_states: None,
|
||||
entities: Vec::new(),
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
@@ -630,6 +639,7 @@ impl OpenCADStudio {
|
||||
return;
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
visibility_states: None,
|
||||
entities: Vec::new(),
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
@@ -696,6 +706,7 @@ impl OpenCADStudio {
|
||||
return;
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
visibility_states: None,
|
||||
entities: Vec::new(),
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
@@ -726,6 +737,7 @@ impl OpenCADStudio {
|
||||
self.finish_pending_history(i);
|
||||
let (text_names, dim_names, object_handles) = after.changed_keys(&before);
|
||||
let delta = DeltaSnapshot {
|
||||
visibility_states: None,
|
||||
entities: Vec::new(),
|
||||
current_layout_before: self.tabs[i].scene.current_layout.clone(),
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
@@ -932,6 +944,7 @@ impl OpenCADStudio {
|
||||
structure = Some(StructureSnapshot::Objects(objects));
|
||||
}
|
||||
let delta = DeltaSnapshot {
|
||||
visibility_states: None,
|
||||
entities,
|
||||
current_layout_before: pending.current_layout,
|
||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||
@@ -1131,6 +1144,10 @@ impl OpenCADStudio {
|
||||
let value = if undo { &entry.before } else { &entry.after };
|
||||
*scene.parametric_constraint_set_mut(entry.scope) = value.clone();
|
||||
}
|
||||
if let Some((before, after)) = &d.visibility_states {
|
||||
scene.visibility_states = if undo { before } else { after }.clone();
|
||||
scene.bump_geometry();
|
||||
}
|
||||
if let Some((before, after)) = &d.named_parameters {
|
||||
scene.named_parameters = if undo { before } else { after }.clone();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ mod block_editor;
|
||||
#[cfg(test)]
|
||||
mod block_editor_tests;
|
||||
mod block_save_as;
|
||||
mod block_visibility;
|
||||
mod block_properties;
|
||||
#[cfg(test)]
|
||||
mod block_properties_tests;
|
||||
@@ -589,6 +590,8 @@ pub(super) struct OpenCADStudio {
|
||||
block_definition_open_editor: Option<String>,
|
||||
block_edit_picker: Option<crate::ui::window::block_edit_picker::State>,
|
||||
block_save_as: Option<crate::ui::window::block_save_as::State>,
|
||||
block_visibility: Option<crate::ui::window::block_visibility::State>,
|
||||
block_visibility_mode: bool,
|
||||
/// PDF dialogs' working copies; `None` while closed.
|
||||
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
|
||||
point_cloud_attach: Option<crate::ui::window::pdf_dialogs::PointCloudAttachState>,
|
||||
@@ -2239,6 +2242,7 @@ pub enum ModalKind {
|
||||
BlockEditChanges,
|
||||
BlockEditPicker,
|
||||
BlockSaveAs,
|
||||
BlockVisibility,
|
||||
SaveDialog,
|
||||
Recovery,
|
||||
/// Fonts referenced by the drawing are missing on this machine.
|
||||
@@ -2884,6 +2888,7 @@ pub enum Message {
|
||||
BlockEditChangesCancel,
|
||||
BlockSaveAsName(String),
|
||||
BlockSaveAsApply,
|
||||
BlockVisibility(crate::ui::window::block_visibility::Action),
|
||||
BlockEditPickerName(String),
|
||||
BlockEditPickerSelect(String),
|
||||
BlockEditPickerApply,
|
||||
@@ -4658,6 +4663,8 @@ impl OpenCADStudio {
|
||||
block_definition_open_editor: None,
|
||||
block_edit_picker: None,
|
||||
block_save_as: None,
|
||||
block_visibility: None,
|
||||
block_visibility_mode: false,
|
||||
pdf_attach: None,
|
||||
point_cloud_attach: None,
|
||||
attdef_dialog: None,
|
||||
|
||||
@@ -1254,7 +1254,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_command_escape(&mut self) -> Task<Message> {
|
||||
pub(in crate::app) fn on_command_escape(&mut self) -> Task<Message> {
|
||||
self.tabs[self.active_tab].measurement.clear();
|
||||
// Escape answers a pending client getpoint request first: no point.
|
||||
if self.control.get_point.is_some() {
|
||||
|
||||
@@ -236,6 +236,7 @@ impl OpenCADStudio {
|
||||
Some(AttMan) => self.attman = None,
|
||||
Some(BlockEditPicker) => self.block_edit_picker = None,
|
||||
Some(BlockSaveAs) => self.block_save_as = None,
|
||||
Some(BlockVisibility) => self.block_visibility = None,
|
||||
Some(PdfAttach) => self.pdf_attach = None,
|
||||
Some(UnderlayLayers) => self.underlay_layers = None,
|
||||
Some(PdfImportSettings) => self.pdf_import_settings = None,
|
||||
@@ -390,6 +391,7 @@ impl OpenCADStudio {
|
||||
let before = self.tabs.get(self.active_tab).map(|tab| tab.id);
|
||||
let task = self.update_message(msg);
|
||||
self.restore_dynamic_ucs_after_command();
|
||||
self.sync_block_visibility_display();
|
||||
if self.tabs.get(self.active_tab).map(|tab| tab.id) != before {
|
||||
self.sync_underlay_tab();
|
||||
}
|
||||
@@ -9658,6 +9660,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
Message::CloseModal => {
|
||||
if self.active_modal == Some(super::ModalKind::BlockVisibility) {
|
||||
self.block_visibility = None;
|
||||
}
|
||||
// The Invalid Area dialog's ✕ undoes the boundary change, as its Cancel does.
|
||||
if self.active_modal == Some(super::ModalKind::CountInvalidArea) {
|
||||
return self.update(Message::Count(
|
||||
@@ -10670,6 +10675,7 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockSaveAsApply => self.apply_block_save_as_dialog(),
|
||||
Message::BlockVisibility(action) => self.handle_block_visibility(action),
|
||||
Message::BlockEditPickerName(name) => {
|
||||
if let Some(state) = self.block_edit_picker.as_mut() {
|
||||
state.name = name.clone();
|
||||
|
||||
@@ -2218,6 +2218,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
self.tabs[self.active_tab].history.undo_stack.len(),
|
||||
self.tabs[self.active_tab].history.redo_stack.len(),
|
||||
self.show_block_palette,
|
||||
self.block_visibility_context(),
|
||||
));
|
||||
}
|
||||
// Split the `chrome` bucket so live `view-detail` traces show
|
||||
|
||||
@@ -86,6 +86,7 @@ impl OpenCADStudio {
|
||||
Some(K::Unsaved) => crate::tr!("modal", "unsaved-changes"),
|
||||
Some(K::BlockEditChanges) => crate::t!("Block Editor").into_owned(),
|
||||
Some(K::BlockEditPicker) => crate::t!("Edit Block Definition").into_owned(),
|
||||
Some(K::BlockVisibility) => crate::t!("Visibility States").into_owned(),
|
||||
Some(K::BlockSaveAs) => crate::t!("Save Block As").into_owned(),
|
||||
Some(K::PointStyle) => crate::tr!("modal", "point-style"),
|
||||
Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"),
|
||||
@@ -520,6 +521,12 @@ impl OpenCADStudio {
|
||||
crate::ui::window::block_edit_picker::view(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::BlockVisibility => {
|
||||
let state = self.block_visibility.as_ref()?;
|
||||
sized_flow(ex, 580, 470, |flow| {
|
||||
crate::ui::window::block_visibility::view(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::BlockSaveAs => {
|
||||
let state = self.block_save_as.as_ref()?;
|
||||
sized_flow(ex, 430, 370, |flow| {
|
||||
|
||||
@@ -2991,6 +2991,12 @@ pub trait CadCommand: Send {
|
||||
/// While true, viewport clicks are routed through the normal selection
|
||||
/// system (single / box / polygon) instead of the command's point-pick path.
|
||||
/// After each completed selection action the host calls `on_selection_complete`.
|
||||
/// A selection command may reveal state-hidden BEDIT members for picking.
|
||||
/// This is a display request only; the document visibility flags stay intact.
|
||||
fn reveals_hidden_block_objects(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -94,6 +94,41 @@ impl CadModule for BlockEditorModule {
|
||||
),
|
||||
);
|
||||
}
|
||||
groups.push(RibbonGroup {
|
||||
title: "Visibility",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(ToolDef {
|
||||
id: "BVSTATE_MANAGER",
|
||||
label: "Visibility States",
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/ui/list.svg")),
|
||||
event: ModuleEvent::Command("BVSTATE DIALOG".into()),
|
||||
}),
|
||||
RibbonItem::Tool(ToolDef {
|
||||
id: "BVMODE_TOGGLE",
|
||||
label: "Visibility Mode",
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/constrain/show_all.svg"
|
||||
)),
|
||||
event: ModuleEvent::Command("BVMODE TOGGLE".into()),
|
||||
}),
|
||||
RibbonItem::Tool(ToolDef {
|
||||
id: "BVSHOW",
|
||||
label: "Make Visible",
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/constrain/show.svg"
|
||||
)),
|
||||
event: ModuleEvent::Command("BVSHOW".into()),
|
||||
}),
|
||||
RibbonItem::Tool(ToolDef {
|
||||
id: "BVHIDE",
|
||||
label: "Make Invisible",
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/constrain/hide_all.svg"
|
||||
)),
|
||||
event: ModuleEvent::Command("BVHIDE".into()),
|
||||
}),
|
||||
],
|
||||
});
|
||||
groups
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,7 +66,8 @@ impl CadCommand for BvStateCommand {
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Option => {
|
||||
crate::t!("BVSTATE Enter an option [New/Set/Rename/Delete] <Set>:").into_owned()
|
||||
crate::t!("BVSTATE Enter an option [New/Set/Rename/Delete/Dialog] <Set>:")
|
||||
.into_owned()
|
||||
}
|
||||
Step::NewName => crate::t!("BVSTATE Enter name of new visibility state:").into_owned(),
|
||||
Step::SetName => crate::tf!(
|
||||
@@ -93,6 +94,7 @@ impl CadCommand for BvStateCommand {
|
||||
CmdOption::new(crate::t!("Set").as_ref(), "S"),
|
||||
CmdOption::new(crate::t!("Rename").as_ref(), "R"),
|
||||
CmdOption::new(crate::t!("Delete").as_ref(), "D"),
|
||||
CmdOption::new(crate::t!("Dialog").as_ref(), "DIALOG"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
@@ -114,6 +116,7 @@ impl CadCommand for BvStateCommand {
|
||||
match &self.step {
|
||||
Step::Option => {
|
||||
self.step = match text.trim_start_matches('_').to_ascii_uppercase().as_str() {
|
||||
"DIALOG" => return Some(CmdResult::Dispatch("BVSTATE DIALOG".into())),
|
||||
"N" | "NEW" => Step::NewName,
|
||||
"" | "S" | "SET" => Step::SetName,
|
||||
"R" | "RENAME" => Step::RenameFrom,
|
||||
@@ -196,5 +199,5 @@ impl CadCommand for BvStateCommand {
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["BVSTATE", "BVHIDE", "BVSHOW"]
|
||||
names: &["BVSTATE", "BVHIDE", "BVSHOW", "BVMODE"]
|
||||
});
|
||||
@@ -177,6 +177,10 @@ impl SelectObjectsCommand {
|
||||
}
|
||||
|
||||
impl CadCommand for SelectObjectsCommand {
|
||||
fn reveals_hidden_block_objects(&self) -> bool {
|
||||
self.pending_cmd == "_BVSHOW"
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"SELECT"
|
||||
}
|
||||
|
||||
@@ -32,6 +32,46 @@ const MINOR: i32 = 329;
|
||||
/// Value code of an expression that holds no value.
|
||||
const NO_VALUE: i16 = -9999;
|
||||
|
||||
impl Scene {
|
||||
/// BVMODE and BVSHOW picking reveal only state-controlled members of BEDIT.
|
||||
pub(crate) fn visibility_revealed(&self, handle: Handle) -> bool {
|
||||
self.block_visibility_reveal
|
||||
&& self.block_edit_block.is_some_and(|definition| {
|
||||
self.document.get_entity(handle).is_some_and(|entity| {
|
||||
entity.common().owner_handle == definition && entity.common().invisible
|
||||
}) && parameter(&self.document, definition)
|
||||
.is_some_and(|parameter| parameter.all_blocks.contains(&handle))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn visibility_reveal_handles(&self) -> rustc_hash::FxHashSet<Handle> {
|
||||
self.block_edit_block
|
||||
.and_then(|definition| parameter(&self.document, definition))
|
||||
.filter(|_| self.block_visibility_reveal)
|
||||
.map(|parameter| {
|
||||
parameter
|
||||
.all_blocks
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| self.visibility_revealed(*handle))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn display_entity_invisible(&self, common: &codec::entities::EntityCommon) -> bool {
|
||||
common.invisible && !self.visibility_revealed(common.handle)
|
||||
}
|
||||
|
||||
pub(crate) fn set_block_visibility_reveal(&mut self, reveal: bool) {
|
||||
if self.block_visibility_reveal != reveal {
|
||||
self.block_visibility_reveal = reveal;
|
||||
self.recolor_meshes();
|
||||
self.bump_geometry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// (DXF name, class, DWG maintenance version) of the classes used here.
|
||||
const CLASSES: &[(&str, &str, i32)] = &[
|
||||
("ACAD_EVALUATION_GRAPH", "AcDbEvalGraph", 55),
|
||||
@@ -131,6 +171,20 @@ fn store(document: &mut CadDocument, parameter: BlockVisibilityParameter) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Commit a staged state list while retaining the parameter's graph and identity.
|
||||
pub(crate) fn replace_states(
|
||||
document: &mut CadDocument,
|
||||
definition: Handle,
|
||||
edited: BlockVisibilityParameter,
|
||||
) {
|
||||
debug_assert_eq!(
|
||||
parameter(document, definition).map(|p| p.handle),
|
||||
Some(edited.handle)
|
||||
);
|
||||
store(document, edited);
|
||||
tag_members(document, definition);
|
||||
}
|
||||
|
||||
/// The objects of a definition, block begin and end left out.
|
||||
fn members(document: &CadDocument, definition: Handle) -> Vec<Handle> {
|
||||
document
|
||||
@@ -784,6 +838,79 @@ mod tests {
|
||||
use super::*;
|
||||
use codec::entities::{Circle, Insert, Line};
|
||||
|
||||
#[test]
|
||||
fn visibility_ribbon_reveal_respects_layers_uncontrolled_flags_and_nested_states() {
|
||||
let mut scene = Scene::new();
|
||||
scene.define_block_raw(
|
||||
"nested",
|
||||
Vector3::ZERO,
|
||||
vec![EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
))],
|
||||
);
|
||||
let nested = scene.document.block_records.get("nested").unwrap().handle;
|
||||
let child = members(&scene.document, nested)[0];
|
||||
scene
|
||||
.document
|
||||
.get_entity_mut(child)
|
||||
.unwrap()
|
||||
.common_mut()
|
||||
.invisible = true;
|
||||
scene.define_block_raw(
|
||||
"editor",
|
||||
Vector3::ZERO,
|
||||
vec![
|
||||
EntityType::Line(Line::from_points(Vector3::ZERO, Vector3::UNIT_X)),
|
||||
EntityType::Line(Line::from_points(
|
||||
Vector3::UNIT_Y,
|
||||
Vector3::UNIT_X + Vector3::UNIT_Y,
|
||||
)),
|
||||
EntityType::Insert(Insert::new("nested", Vector3::ZERO)),
|
||||
],
|
||||
);
|
||||
let definition = scene.document.block_records.get("editor").unwrap().handle;
|
||||
let controlled = members(&scene.document, definition)[0];
|
||||
let independent = members(&scene.document, definition)[1];
|
||||
scene
|
||||
.document
|
||||
.get_entity_mut(independent)
|
||||
.unwrap()
|
||||
.common_mut()
|
||||
.invisible = true;
|
||||
let parameter = create(&mut scene.document, definition, Vector3::ZERO).unwrap();
|
||||
set_visible(&mut scene.document, parameter, 0, &[controlled], false);
|
||||
apply_state(&mut scene.document, parameter, 0);
|
||||
scene.block_edit_block = Some(definition);
|
||||
let before = scene.document.clone();
|
||||
scene.set_block_visibility_reveal(true);
|
||||
let wires = scene.entity_wires();
|
||||
assert!(wires
|
||||
.iter()
|
||||
.any(|wire| Scene::handle_from_wire_name(&wire.name) == Some(controlled)));
|
||||
assert!(!scene.visibility_revealed(independent));
|
||||
assert!(!scene.visibility_revealed(child));
|
||||
assert_eq!(scene.visibility_reveal_handles().len(), 1);
|
||||
assert_eq!(
|
||||
scene.select_all_visible(),
|
||||
2,
|
||||
"only revealed line and the insert can be selected"
|
||||
);
|
||||
let color = scene.count_color(controlled, [1.0; 4], [0.0, 0.0, 0.0, 1.0]);
|
||||
assert_eq!(color, [0.5, 0.5, 0.5, 1.0]);
|
||||
scene.document.layers.get_mut("0").unwrap().flags.off = true;
|
||||
scene.bump_geometry();
|
||||
assert_eq!(
|
||||
scene.select_all_visible(),
|
||||
0,
|
||||
"mode never overrides a hidden layer"
|
||||
);
|
||||
scene.document.layers.get_mut("0").unwrap().flags.off = false;
|
||||
scene.set_block_visibility_reveal(false);
|
||||
assert_eq!(scene.document, before);
|
||||
assert!(!scene.visibility_revealed(controlled));
|
||||
}
|
||||
|
||||
/// A block given visibility states round-trips them, and a reference
|
||||
/// given its own copy still finds the definition and its states.
|
||||
#[test]
|
||||
|
||||
+13
-1
@@ -1510,7 +1510,7 @@ impl Scene {
|
||||
return false;
|
||||
}
|
||||
let c = entity.common();
|
||||
if c.invisible
|
||||
if self.display_entity_invisible(c)
|
||||
|| self.entity_temporarily_hidden(handle)
|
||||
|| layer_hidden(&c.layer)
|
||||
|| crate::scene::annotative::annotative_offscale_for(
|
||||
@@ -1811,6 +1811,11 @@ impl Scene {
|
||||
plot_only: bool,
|
||||
) -> Vec<HatchModel> {
|
||||
let depth_map = self.draw_depth_map();
|
||||
let visibility_reveal = if plot_only {
|
||||
rustc_hash::FxHashSet::default()
|
||||
} else {
|
||||
self.visibility_reveal_handles()
|
||||
};
|
||||
let graph = crate::scene::render_graph::RenderSceneGraph::new(
|
||||
&self.document,
|
||||
frozen,
|
||||
@@ -1818,6 +1823,7 @@ impl Scene {
|
||||
all_visible,
|
||||
depth_map.as_ref(),
|
||||
)
|
||||
.with_visibility_reveal(&visibility_reveal)
|
||||
.with_annotation_scale(self.annotation_scale)
|
||||
.with_viewport(viewport);
|
||||
let mut hatch_block_memo = std::collections::HashMap::new();
|
||||
@@ -2022,6 +2028,11 @@ impl Scene {
|
||||
plot_only: bool,
|
||||
) -> Vec<HatchModel> {
|
||||
let depth_map = self.draw_depth_map();
|
||||
let visibility_reveal = if plot_only {
|
||||
rustc_hash::FxHashSet::default()
|
||||
} else {
|
||||
self.visibility_reveal_handles()
|
||||
};
|
||||
let graph = crate::scene::render_graph::RenderSceneGraph::new(
|
||||
&self.document,
|
||||
frozen,
|
||||
@@ -2029,6 +2040,7 @@ impl Scene {
|
||||
all_visible,
|
||||
depth_map.as_ref(),
|
||||
)
|
||||
.with_visibility_reveal(&visibility_reveal)
|
||||
.with_annotation_scale(self.annotation_scale);
|
||||
let mut models = Vec::new();
|
||||
let mut wipeout_sources = rustc_hash::FxHashMap::default();
|
||||
|
||||
+67
-8
@@ -2149,6 +2149,7 @@ pub struct Scene {
|
||||
/// The visibility state each dynamic block shows in the Block Editor,
|
||||
/// by block record.
|
||||
pub visibility_states: HashMap<Handle, usize>,
|
||||
pub(crate) block_visibility_reveal: bool,
|
||||
/// Currently selected entity handles.
|
||||
pub selected: HashSet<Handle>,
|
||||
/// Polylines only part of which is selected (Ctrl+click): the picked
|
||||
@@ -2871,6 +2872,7 @@ impl Scene {
|
||||
paper_projected_cache: RefCell::new(HashMap::default()),
|
||||
current_layout: "Model".to_string(),
|
||||
block_edit_block: None,
|
||||
block_visibility_reveal: false,
|
||||
viewcube_ucs: glam::Mat4::IDENTITY,
|
||||
hatches: HashMap::default(),
|
||||
meshes: HashMap::default(),
|
||||
@@ -4558,6 +4560,11 @@ impl Scene {
|
||||
/// top-level entity): the count colours, else faded; unchanged outside
|
||||
/// count mode.
|
||||
pub(crate) fn count_color(&self, handle: Handle, color: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
|
||||
let color = if self.visibility_revealed(handle) {
|
||||
crate::scene::cache::block_cache::fade_toward_bg(color, bg)
|
||||
} else {
|
||||
color
|
||||
};
|
||||
let Some(count) = &self.count_display else {
|
||||
return color;
|
||||
};
|
||||
@@ -4578,7 +4585,9 @@ impl Scene {
|
||||
fn apply_refedit_fade(&self, wires: &mut Vec<WireModel>, bg: [f32; 4]) {
|
||||
// Block copies kept as stubs draw in their prototype's colour; while
|
||||
// copies are recoloured one by one they are expanded instead.
|
||||
if (self.count_display.is_some() || self.refedit_keep.is_some())
|
||||
if (self.count_display.is_some()
|
||||
|| self.refedit_keep.is_some()
|
||||
|| self.block_visibility_reveal)
|
||||
&& wires.iter().any(|wire| wire.stub.is_some())
|
||||
{
|
||||
*wires = crate::scene::model::instance_model::expand_stubs(wires).into_owned();
|
||||
@@ -4603,6 +4612,19 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.block_visibility_reveal {
|
||||
for wire in wires.iter_mut() {
|
||||
if Self::handle_from_wire_name(&wire.name)
|
||||
.is_some_and(|handle| self.visibility_revealed(handle))
|
||||
{
|
||||
wire.color = crate::scene::cache::block_cache::fade_toward_bg(wire.color, bg);
|
||||
for vertex in &mut wire.text_verts {
|
||||
vertex.color =
|
||||
crate::scene::cache::block_cache::fade_toward_bg(vertex.color, bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(keep) = &self.refedit_keep else {
|
||||
return;
|
||||
};
|
||||
@@ -6665,6 +6687,7 @@ impl Scene {
|
||||
/// Persisted `EntityCommon::invisible` values stay untouched and continue
|
||||
/// to serve file/dynamic-block visibility.
|
||||
pub fn reset_transient_visibility(&mut self) {
|
||||
self.block_visibility_reveal = false;
|
||||
self.object_isolation = ObjectIsolationState::default();
|
||||
self.preview_hidden.clear();
|
||||
self.command_preview_hidden.clear();
|
||||
@@ -7398,6 +7421,7 @@ impl Scene {
|
||||
fn resident_runs_match_memo(&self, block: Handle) -> bool {
|
||||
self.count_display.is_none()
|
||||
&& self.refedit_keep.is_none()
|
||||
&& !self.block_visibility_reveal
|
||||
&& self.active_live_section(block).is_none()
|
||||
}
|
||||
|
||||
@@ -8597,6 +8621,11 @@ impl Scene {
|
||||
return Vec::new();
|
||||
}
|
||||
let depth_map = self.draw_depth_map();
|
||||
let visibility_reveal = if plotting {
|
||||
HashSet::default()
|
||||
} else {
|
||||
self.visibility_reveal_handles()
|
||||
};
|
||||
let graph = render_graph::RenderSceneGraph::new(
|
||||
&self.document,
|
||||
frozen,
|
||||
@@ -8604,6 +8633,7 @@ impl Scene {
|
||||
all_visible,
|
||||
depth_map.as_ref(),
|
||||
)
|
||||
.with_visibility_reveal(&visibility_reveal)
|
||||
.with_annotation_scale(self.annotation_scale)
|
||||
.with_viewport(viewport);
|
||||
let mut models = Vec::new();
|
||||
@@ -8686,6 +8716,15 @@ impl Scene {
|
||||
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
|
||||
});
|
||||
}
|
||||
if !plotting
|
||||
&& self.visibility_revealed(if context.is_instanced() {
|
||||
context.root_handle
|
||||
} else {
|
||||
handle
|
||||
})
|
||||
{
|
||||
placed.opacity *= 0.3;
|
||||
}
|
||||
placed.draw_depth = context.draw_depth(handle, depth_map.as_ref());
|
||||
models.push(collect(placed, context));
|
||||
},
|
||||
@@ -8968,6 +9007,20 @@ impl Scene {
|
||||
material.diffuse[3] = style.0[3];
|
||||
}
|
||||
}
|
||||
if self.visibility_revealed(handle) {
|
||||
for mesh in &mut set.lods {
|
||||
mesh.color = crate::scene::cache::block_cache::fade_toward_bg(
|
||||
mesh.color,
|
||||
self.current_bg(),
|
||||
);
|
||||
}
|
||||
if let Some(material) = set.material.as_mut() {
|
||||
material.diffuse = crate::scene::cache::block_cache::fade_toward_bg(
|
||||
material.diffuse,
|
||||
self.current_bg(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(set)
|
||||
})
|
||||
.collect();
|
||||
@@ -9390,13 +9443,15 @@ impl Scene {
|
||||
}
|
||||
let arc = if point_cloud::any_definition(&self.document) {
|
||||
let depths = rustc_hash::FxHashMap::default();
|
||||
let visibility_reveal = self.visibility_reveal_handles();
|
||||
let graph = render_graph::RenderSceneGraph::new(
|
||||
&self.document,
|
||||
(!frozen.is_empty()).then_some(frozen),
|
||||
None,
|
||||
true,
|
||||
&depths,
|
||||
);
|
||||
)
|
||||
.with_visibility_reveal(&visibility_reveal);
|
||||
let mut clouds = Vec::new();
|
||||
graph.walk_root(
|
||||
self.render_scene_root(target_block),
|
||||
@@ -9473,7 +9528,7 @@ impl Scene {
|
||||
continue;
|
||||
};
|
||||
let common = entity.common();
|
||||
if common.invisible
|
||||
if self.display_entity_invisible(common)
|
||||
|| self.entity_temporarily_hidden(common.handle)
|
||||
|| self
|
||||
.document
|
||||
@@ -9639,7 +9694,7 @@ impl Scene {
|
||||
let Some(c) = self.document.get_entity(handle).map(|e| e.common()) else {
|
||||
return false;
|
||||
};
|
||||
if c.invisible {
|
||||
if self.display_entity_invisible(c) {
|
||||
return false;
|
||||
}
|
||||
!self.layer_hidden(&c.layer)
|
||||
@@ -9673,6 +9728,7 @@ impl Scene {
|
||||
return Vec::new();
|
||||
}
|
||||
let depth_map = self.draw_depth_map();
|
||||
let visibility_reveal = self.visibility_reveal_handles();
|
||||
let graph = render_graph::RenderSceneGraph::new(
|
||||
&self.document,
|
||||
frozen,
|
||||
@@ -9680,6 +9736,7 @@ impl Scene {
|
||||
all_visible,
|
||||
depth_map.as_ref(),
|
||||
)
|
||||
.with_visibility_reveal(&visibility_reveal)
|
||||
.with_annotation_scale(self.annotation_scale)
|
||||
.with_viewport(viewport);
|
||||
let mut out = Vec::new();
|
||||
@@ -9744,7 +9801,7 @@ impl Scene {
|
||||
self.material_base_dir.as_deref(),
|
||||
);
|
||||
}
|
||||
if self.count_display.is_some() {
|
||||
if self.count_display.is_some() || self.visibility_revealed(context.root_handle) {
|
||||
let own = transformed
|
||||
.instance_color
|
||||
.or(set.display_color())
|
||||
@@ -9973,7 +10030,7 @@ impl Scene {
|
||||
return false;
|
||||
}
|
||||
let common = entity.common();
|
||||
if common.invisible
|
||||
if self.display_entity_invisible(common)
|
||||
|| self.entity_temporarily_hidden(handle)
|
||||
|| self.layer_hidden(&common.layer)
|
||||
|| self.interaction_layer_frozen(&common.layer)
|
||||
@@ -10037,6 +10094,7 @@ impl Scene {
|
||||
.flatten()
|
||||
.copied()
|
||||
.collect();
|
||||
let visibility_reveal = self.visibility_reveal_handles();
|
||||
let graph = render_graph::RenderSceneGraph::new(
|
||||
&self.document,
|
||||
(!frozen.is_empty()).then_some(&frozen),
|
||||
@@ -10044,6 +10102,7 @@ impl Scene {
|
||||
all_visible,
|
||||
depth_map.as_ref(),
|
||||
)
|
||||
.with_visibility_reveal(&visibility_reveal)
|
||||
.with_annotation_scale(self.annotation_scale);
|
||||
for entity in self.document.entities() {
|
||||
let contextual = crate::scene::annotative::entity_for_annotation_context(
|
||||
@@ -10053,7 +10112,7 @@ impl Scene {
|
||||
);
|
||||
let host = contextual.as_ref();
|
||||
let common = host.common();
|
||||
if common.invisible
|
||||
if self.display_entity_invisible(common)
|
||||
|| self.entity_temporarily_hidden(common.handle)
|
||||
|| layer_hidden(&common.layer)
|
||||
|| crate::scene::annotative::annotative_offscale_for(
|
||||
@@ -11638,7 +11697,7 @@ impl Scene {
|
||||
layer: Option<&codec::tables::layer::Layer>,
|
||||
) -> bool {
|
||||
let c = e.common();
|
||||
if c.invisible {
|
||||
if self.display_entity_invisible(c) {
|
||||
return false;
|
||||
}
|
||||
// Session-only Isolate / Hide and interactive replacement previews.
|
||||
|
||||
@@ -280,6 +280,7 @@ pub struct RenderSceneGraph<'a> {
|
||||
all_visible: bool,
|
||||
depths: &'a FxHashMap<u64, [f32; 2]>,
|
||||
viewport: Option<Handle>,
|
||||
visibility_reveal: Option<&'a FxHashSet<Handle>>,
|
||||
}
|
||||
|
||||
impl<'a> RenderSceneGraph<'a> {
|
||||
@@ -298,9 +299,15 @@ impl<'a> RenderSceneGraph<'a> {
|
||||
all_visible,
|
||||
depths,
|
||||
viewport: None,
|
||||
visibility_reveal: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_visibility_reveal(mut self, handles: &'a FxHashSet<Handle>) -> Self {
|
||||
self.visibility_reveal = Some(handles);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_viewport(mut self, viewport: Option<Handle>) -> Self {
|
||||
self.viewport = viewport.filter(|handle| handle.is_valid());
|
||||
self
|
||||
@@ -579,7 +586,11 @@ impl<'a> RenderSceneGraph<'a> {
|
||||
|
||||
fn document_visible(&self, entity: &EntityType) -> bool {
|
||||
let common = entity.common();
|
||||
if common.invisible {
|
||||
if common.invisible
|
||||
&& !self
|
||||
.visibility_reveal
|
||||
.is_some_and(|handles| handles.contains(&common.handle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let layer = self.document.layers.get(&common.layer);
|
||||
|
||||
+11
-1
@@ -26,6 +26,7 @@ pub(crate) use draw_panel::tools as panel_tools;
|
||||
mod color_dropdown;
|
||||
mod context_tools;
|
||||
mod modify_panel;
|
||||
mod visibility_panel;
|
||||
pub use context_tools::{pdf_underlay_tools, point_cloud_tools, xref_tools, UnderlayContext};
|
||||
use widgets::{StyleContext, *};
|
||||
pub(crate) use widgets::{REDO_HISTORY_ID, UNDO_HISTORY_ID};
|
||||
@@ -630,6 +631,7 @@ impl Ribbon {
|
||||
undo_count: usize,
|
||||
redo_count: usize,
|
||||
show_block_palette: bool,
|
||||
visibility: Option<crate::ui::window::block_visibility::RibbonContext>,
|
||||
) -> Element<'_, Message> {
|
||||
// ── Quick-access file commands + undo/redo, one merged flow ────────
|
||||
let lead = iced::widget::Row::with_children(vec![
|
||||
@@ -825,7 +827,15 @@ impl Ribbon {
|
||||
// button. See `CollapsePanels`.
|
||||
let ts = self.toggle_state(show_block_palette);
|
||||
let panels: Vec<Panel<'_>> = groups
|
||||
.map(|g| self.panel(g, ts, &style_ctx, &|_| Vec::new()))
|
||||
.map(|g| {
|
||||
if self.modules[effective_active].id() == "block_editor"
|
||||
&& g.title == "Visibility"
|
||||
{
|
||||
visibility_panel::panel(visibility.clone())
|
||||
} else {
|
||||
self.panel(g, ts, &style_ctx, &|_| Vec::new())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H)
|
||||
.report_height(self.tool_bar_h.clone())
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
//! The contextual visibility controls share the ribbon's buttons, tips and collapse layout.
|
||||
use iced::widget::{button, column, container, pick_list, row, text, tooltip};
|
||||
use iced::{Element, Fill, Theme};
|
||||
|
||||
use super::collapse::Panel;
|
||||
use super::widgets::{make_icon, tool_btn_style, LARGE_ICON, LARGE_W, SMALL_ICON, TOOL_BAR_H};
|
||||
use crate::app::Message;
|
||||
use crate::modules::IconKind;
|
||||
use crate::ui::window::block_visibility::{Action, RibbonContext};
|
||||
use crate::ui::ROW_H;
|
||||
|
||||
fn tool(
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
icon: &'static [u8],
|
||||
large: bool,
|
||||
active: bool,
|
||||
message: Option<Message>,
|
||||
) -> Element<'static, Message> {
|
||||
let enabled = message.is_some();
|
||||
let size = if large { LARGE_ICON } else { SMALL_ICON };
|
||||
let image = if enabled {
|
||||
crate::ui::icons::semantic(icon, size)
|
||||
} else {
|
||||
crate::ui::icons::semantic_disabled(icon, size)
|
||||
};
|
||||
let face: Element<'static, Message> = if large {
|
||||
column![
|
||||
image,
|
||||
text(crate::t!(label).into_owned())
|
||||
.size(10)
|
||||
.align_x(iced::Center)
|
||||
]
|
||||
.align_x(iced::Center)
|
||||
.spacing(3)
|
||||
.into()
|
||||
} else {
|
||||
image
|
||||
};
|
||||
let mut control = button(face)
|
||||
.on_press_maybe(message)
|
||||
.style(move |theme: &Theme, status| {
|
||||
let mut style = tool_btn_style(theme, active, status);
|
||||
if !enabled {
|
||||
style.text_color = style.text_color.scale_alpha(0.42);
|
||||
style.background = None;
|
||||
}
|
||||
style
|
||||
})
|
||||
.padding([3, 5]);
|
||||
control = if large {
|
||||
control.width(LARGE_W + 8.0).height(3.0 * ROW_H)
|
||||
} else {
|
||||
control.width(ROW_H + 4.0).height(ROW_H)
|
||||
};
|
||||
tooltip(
|
||||
super::keytips::anchor(id, container(control).id(id)),
|
||||
super::tooltip_content(crate::t!(label).into_owned()),
|
||||
iced::widget::tooltip::Position::Right,
|
||||
)
|
||||
.gap(6)
|
||||
.delay(std::time::Duration::from_millis(400))
|
||||
.style(super::tooltip_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view(context: Option<RibbonContext>, compact: bool) -> Element<'static, Message> {
|
||||
let target = context.as_ref().map(|context| context.target);
|
||||
let mode = context.as_ref().is_some_and(|context| context.mode);
|
||||
let manager = tool(
|
||||
"BVSTATE_MANAGER",
|
||||
"Visibility States",
|
||||
include_bytes!("../../../assets/icons/ui/list.svg"),
|
||||
!compact,
|
||||
false,
|
||||
target.map(|target| Message::BlockVisibility(Action::Open(target))),
|
||||
);
|
||||
let toggles = row![
|
||||
tool(
|
||||
"BVMODE_TOGGLE",
|
||||
"Visibility Mode",
|
||||
include_bytes!("../../../assets/icons/constrain/show_all.svg"),
|
||||
false,
|
||||
mode,
|
||||
target.map(|target| Message::BlockVisibility(Action::Mode(target)))
|
||||
),
|
||||
tool(
|
||||
"BVSHOW",
|
||||
"Make Visible",
|
||||
include_bytes!("../../../assets/icons/constrain/show.svg"),
|
||||
false,
|
||||
false,
|
||||
target.map(|_| Message::Command("BVSHOW".into()))
|
||||
),
|
||||
tool(
|
||||
"BVHIDE",
|
||||
"Make Invisible",
|
||||
include_bytes!("../../../assets/icons/constrain/hide_all.svg"),
|
||||
false,
|
||||
false,
|
||||
target.map(|_| Message::Command("BVHIDE".into()))
|
||||
),
|
||||
]
|
||||
.spacing(2);
|
||||
let names = context
|
||||
.as_ref()
|
||||
.map(|context| context.names.clone())
|
||||
.unwrap_or_default();
|
||||
let current = context.as_ref().and_then(|context| context.current.clone());
|
||||
let mut selector = pick_list(current, names, |name: &String| name.clone())
|
||||
.placeholder(crate::t!("Visibility state").into_owned())
|
||||
.text_size(11)
|
||||
.padding([3, 6])
|
||||
.width(Fill);
|
||||
if let Some(target) = target {
|
||||
selector =
|
||||
selector.on_select(move |name| Message::BlockVisibility(Action::Current(target, name)));
|
||||
}
|
||||
let selector = container(selector).id("BVSTATE_CURRENT").width(Fill);
|
||||
let controls: Element<'static, Message> = if compact {
|
||||
column![row![manager, toggles].spacing(3), selector]
|
||||
.spacing(4)
|
||||
.width(160)
|
||||
.height(Fill)
|
||||
.into()
|
||||
} else {
|
||||
row![manager, column![toggles, selector].spacing(6).width(170)]
|
||||
.spacing(4)
|
||||
.height(Fill)
|
||||
.into()
|
||||
};
|
||||
column![controls, text(crate::t!("Visibility").into_owned()).size(9)]
|
||||
.align_x(iced::Center)
|
||||
.padding([3, 4])
|
||||
.height(TOOL_BAR_H)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn collapsed(tight: bool) -> Element<'static, Message> {
|
||||
let icon = make_icon(
|
||||
IconKind::Svg(include_bytes!("../../../assets/icons/ui/list.svg")),
|
||||
if tight { SMALL_ICON } else { LARGE_ICON },
|
||||
);
|
||||
button(
|
||||
column![icon, text(crate::t!("Visibility ▾").into_owned()).size(10)]
|
||||
.align_x(iced::Center)
|
||||
.spacing(4),
|
||||
)
|
||||
.on_press(Message::ToggleRibbonPanel("Visibility".into()))
|
||||
.style(|theme, status| tool_btn_style(theme, false, status))
|
||||
.padding([4, 6])
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(super) fn panel(context: Option<RibbonContext>) -> Panel<'static> {
|
||||
Panel {
|
||||
id: "Visibility".into(),
|
||||
elements: [
|
||||
view(context.clone(), false),
|
||||
view(context, true),
|
||||
collapsed(false),
|
||||
collapsed(true),
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
use codec::objects::BlockVisibilityParameter;
|
||||
use codec::Handle;
|
||||
use iced::widget::{
|
||||
button, column, container, pick_list, row, scrollable, text, text_input, Space,
|
||||
};
|
||||
use iced::{Element, Fill};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Target {
|
||||
pub document: u64,
|
||||
pub definition: Handle,
|
||||
pub parameter: Handle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RibbonContext {
|
||||
pub target: Target,
|
||||
pub names: Vec<String>,
|
||||
pub current: Option<String>,
|
||||
pub mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NewVisibility {
|
||||
Keep,
|
||||
Show,
|
||||
Hide,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NewVisibility {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(
|
||||
crate::t!(match self {
|
||||
Self::Keep => "Keep current visibility",
|
||||
Self::Show => "Show all objects",
|
||||
Self::Hide => "Hide all objects",
|
||||
})
|
||||
.as_ref(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Action {
|
||||
Open(Target),
|
||||
Current(Target, String),
|
||||
Mode(Target),
|
||||
Select(usize),
|
||||
Name(String),
|
||||
NewVisibility(NewVisibility),
|
||||
New,
|
||||
Rename,
|
||||
Delete,
|
||||
SetCurrent,
|
||||
Move(bool),
|
||||
Apply,
|
||||
}
|
||||
|
||||
pub(crate) struct State {
|
||||
pub target: Target,
|
||||
pub original: BlockVisibilityParameter,
|
||||
pub edited: BlockVisibilityParameter,
|
||||
pub current: usize,
|
||||
pub selected: usize,
|
||||
pub name: String,
|
||||
pub new_visibility: NewVisibility,
|
||||
pub error: Option<String>,
|
||||
/// A dialog cannot overwrite changes made by another entry point.
|
||||
pub epoch: u64,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn select(&mut self, index: usize) {
|
||||
if let Some(state) = self.edited.states.get(index) {
|
||||
self.selected = index;
|
||||
self.name = state.name.clone();
|
||||
self.error = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name_error(&self, renaming: bool) -> Option<String> {
|
||||
if self.name.trim().is_empty() || self.name.chars().any(char::is_control) {
|
||||
return Some(
|
||||
crate::t!("Enter a nonempty state name without control characters.").into_owned(),
|
||||
);
|
||||
}
|
||||
self.edited
|
||||
.states
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(index, state)| {
|
||||
(!renaming || index != self.selected)
|
||||
&& state.name.eq_ignore_ascii_case(self.name.trim())
|
||||
})
|
||||
.then(|| crate::t!("A visibility state with that name already exists.").into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn view(state: &State, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
|
||||
let send = |action| Message::BlockVisibility(action);
|
||||
let mut states = column![].spacing(2);
|
||||
for (index, item) in state.edited.states.iter().enumerate() {
|
||||
let label = if index == state.current {
|
||||
crate::tf!("{} (current)", item.name).into_owned()
|
||||
} else {
|
||||
item.name.clone()
|
||||
};
|
||||
states = states.push(
|
||||
button(text(label).size(13))
|
||||
.width(Fill)
|
||||
.on_press(send(Action::Select(index)))
|
||||
.style(if index == state.selected {
|
||||
button::primary
|
||||
} else {
|
||||
button::text
|
||||
}),
|
||||
);
|
||||
}
|
||||
let control = |label: &'static str, action, enabled: bool| {
|
||||
button(text(crate::t!(label).into_owned()).size(13))
|
||||
.width(Fill)
|
||||
.on_press_maybe(enabled.then_some(send(action)))
|
||||
};
|
||||
container(
|
||||
column![
|
||||
row![
|
||||
container(scrollable(states))
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.style(container::bordered_box),
|
||||
column![
|
||||
control(
|
||||
"Set Current",
|
||||
Action::SetCurrent,
|
||||
state.selected != state.current
|
||||
),
|
||||
control("Delete", Action::Delete, state.edited.states.len() > 1),
|
||||
control("Move Up", Action::Move(true), state.selected > 0),
|
||||
control(
|
||||
"Move Down",
|
||||
Action::Move(false),
|
||||
state.selected + 1 < state.edited.states.len()
|
||||
),
|
||||
]
|
||||
.spacing(8)
|
||||
.width(130),
|
||||
]
|
||||
.spacing(12)
|
||||
.height(Fill),
|
||||
text(crate::t!("State name").into_owned()).size(13),
|
||||
row![
|
||||
text_input(crate::t!("State name").as_ref(), &state.name)
|
||||
.on_input(move |name| send(Action::Name(name)))
|
||||
.style(crate::ui::style::form::field_style),
|
||||
button(text(crate::t!("Rename").into_owned())).on_press_maybe(
|
||||
state
|
||||
.name_error(true)
|
||||
.is_none()
|
||||
.then_some(send(Action::Rename))
|
||||
),
|
||||
]
|
||||
.spacing(8),
|
||||
row![
|
||||
pick_list(
|
||||
Some(state.new_visibility),
|
||||
[
|
||||
NewVisibility::Keep,
|
||||
NewVisibility::Show,
|
||||
NewVisibility::Hide
|
||||
],
|
||||
|mode: &NewVisibility| mode.to_string()
|
||||
)
|
||||
.on_select(move |mode| send(Action::NewVisibility(mode)))
|
||||
.width(Fill),
|
||||
button(text(crate::t!("New").into_owned())).on_press_maybe(
|
||||
state
|
||||
.name_error(false)
|
||||
.is_none()
|
||||
.then_some(send(Action::New))
|
||||
),
|
||||
]
|
||||
.spacing(8),
|
||||
text(state.error.clone().unwrap_or_default())
|
||||
.size(12)
|
||||
.style(text::danger),
|
||||
row![
|
||||
Space::new().width(Fill),
|
||||
button(text(crate::t!("OK").into_owned()))
|
||||
.on_press(send(Action::Apply))
|
||||
.style(button::primary),
|
||||
button(text(crate::t!("Cancel").into_owned())).on_press(Message::CloseModal),
|
||||
]
|
||||
.spacing(8),
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
.padding(16)
|
||||
.width(sizing.width)
|
||||
.height(sizing.height)
|
||||
.into()
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub mod block_definition;
|
||||
pub mod block_edit_picker;
|
||||
pub mod block_palette;
|
||||
pub mod block_save_as;
|
||||
pub mod block_visibility;
|
||||
pub mod browser;
|
||||
pub mod count_palette;
|
||||
pub mod drafting_settings;
|
||||
|
||||
Reference in new issue
Block a user