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
+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