feat(mleaderstyle): add Multileader Style editor window

New dedicated style-manager window for MultiLeaderStyle objects (the
existing mlstyle window edits MLineStyle, not multileader). Opened via
the MLEADERSTYLE command / ribbon manager entry.

Editable across Leader Format / Structure / Content sections:
- path/content/text-angle/text-alignment enums (cycle buttons)
- line & text color (ACI), arrowhead & break-gap sizes
- landing distance/gap, max points, segment angles, scale factor
- default text, text height
- enable landing/dogleg, text frame, text-always-left, annotative
New/Delete/Set-Current style management.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-03 20:25:14 +03:00
1 parent 0dd86eb658
commit e0c5db9e0c
6 files changed
+796 -1

No files matched your search

+4 -1
View File
@@ -3983,7 +3983,10 @@ impl OpenCADStudio {
let parts: Vec<&str> = raw_rest.split_whitespace().collect();
let sub = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
match sub.as_str() {
"" | "LIST" | "?" => {
"" | "DIALOG" | "UI" => {
return Task::done(Message::MLeaderStyleDialogOpen);
}
"LIST" | "?" => {
let styles: Vec<String> = self.tabs[i]
.scene
.document
+49
View File
@@ -234,6 +234,7 @@ pub(super) struct OpenCADStudio {
textstyle_window: Option<window::Id>,
tablestyle_window: Option<window::Id>,
mlstyle_window: Option<window::Id>,
mleaderstyle_window: Option<window::Id>,
layout_manager_window: Option<window::Id>,
plotstyle_window: Option<window::Id>,
dimstyle_window: Option<window::Id>,
@@ -294,6 +295,21 @@ pub(super) struct OpenCADStudio {
// ── MLineStyle Dialog ─────────────────────────────────────────────────
mlstyle_selected: String,
// ── MLeaderStyle Dialog ───────────────────────────────────────────────
mleaderstyle_selected: String,
mls_landing_distance: String,
mls_landing_gap: String,
mls_arrowhead_size: String,
mls_text_height: String,
mls_scale_factor: String,
mls_break_gap: String,
mls_first_seg_angle: String,
mls_second_seg_angle: String,
mls_max_points: String,
mls_default_text: String,
mls_line_color: String,
mls_text_color: String,
// ── TableStyle Dialog ─────────────────────────────────────────────────
tablestyle_selected: String,
/// Edit buffers for the table style's general margins.
@@ -1005,6 +1021,21 @@ pub enum Message {
MlStyleDialogSetCurrent,
MlStyleDialogNew,
MlStyleDialogDelete,
// ── MLeaderStyle Dialog ───────────────────────────────────────────────
MLeaderStyleDialogOpen,
#[allow(dead_code)]
MLeaderStyleDialogClose,
MLeaderStyleDialogSelect(String),
MLeaderStyleDialogSetCurrent,
MLeaderStyleDialogNew,
MLeaderStyleDialogDelete,
MLeaderStyleEdit {
field: &'static str,
value: String,
},
MLeaderStyleToggle(&'static str),
MLeaderStyleCycle(&'static str),
MLeaderStyleApply,
// ── DimStyle Dialog ───────────────────────────────────────────────────
DimStyleDialogOpen,
DimStyleDialogClose,
@@ -1108,6 +1139,7 @@ impl OpenCADStudio {
textstyle_window: None,
tablestyle_window: None,
mlstyle_window: None,
mleaderstyle_window: None,
layout_manager_window: None,
plotstyle_window: None,
dimstyle_window: None,
@@ -1172,6 +1204,20 @@ impl OpenCADStudio {
ts_vmargin: "1.5".to_string(),
// MLineStyle dialog
mlstyle_selected: "Standard".to_string(),
// MLeaderStyle dialog
mleaderstyle_selected: "Standard".to_string(),
mls_landing_distance: String::new(),
mls_landing_gap: String::new(),
mls_arrowhead_size: String::new(),
mls_text_height: String::new(),
mls_scale_factor: String::new(),
mls_break_gap: String::new(),
mls_first_seg_angle: String::new(),
mls_second_seg_angle: String::new(),
mls_max_points: String::new(),
mls_default_text: String::new(),
mls_line_color: String::new(),
mls_text_color: String::new(),
// DimStyle dialog
dimstyle_selected: "Standard".to_string(),
dimstyle_tab: 0,
@@ -1308,6 +1354,9 @@ pub fn run() -> iced::Result {
if Some(window_id) == state.mlstyle_window {
return "Multiline Style".into();
}
if Some(window_id) == state.mleaderstyle_window {
return "Multileader Style".into();
}
if Some(window_id) == state.layout_manager_window {
return "Layout Manager".into();
}
+356
View File
@@ -1428,6 +1428,10 @@ impl OpenCADStudio {
self.mlstyle_window = None;
self.ribbon.deactivate_tool_if("MLSTYLE");
}
if self.mleaderstyle_window == Some(id) {
self.mleaderstyle_window = None;
self.ribbon.deactivate_tool_if("MLEADERSTYLE");
}
if self.layout_manager_window == Some(id) {
self.layout_manager_window = None;
self.ribbon.deactivate_tool_if("LAYOUTMANAGER");
@@ -6045,6 +6049,302 @@ impl OpenCADStudio {
Task::none()
}
// ── MLeaderStyle Dialog ───────────────────────────────────────────
Message::MLeaderStyleDialogOpen => {
use acadrust::objects::ObjectType;
let i = self.active_tab;
let cur = self.tabs[i].active_mleader_style.clone();
let exists = self.tabs[i]
.scene
.document
.objects
.values()
.any(|o| matches!(o, ObjectType::MultiLeaderStyle(s) if s.name == cur));
self.mleaderstyle_selected = if exists {
cur
} else {
self.tabs[i]
.scene
.document
.objects
.values()
.find_map(|o| {
if let ObjectType::MultiLeaderStyle(s) = o {
Some(s.name.clone())
} else {
None
}
})
.unwrap_or_else(|| "Standard".to_string())
};
self.load_mleaderstyle_bufs(i);
if let Some(id) = self.mleaderstyle_window {
return window::gain_focus(id);
}
let (id, task) = window::open(window::Settings {
size: iced::Size::new(560.0, 560.0),
resizable: true,
level: window::Level::AlwaysOnTop,
..Default::default()
});
self.mleaderstyle_window = Some(id);
task.map(|_| Message::Noop)
}
Message::MLeaderStyleDialogClose => {
if let Some(id) = self.mleaderstyle_window.take() {
window::close(id)
} else {
Task::none()
}
}
Message::MLeaderStyleDialogSelect(name) => {
self.mleaderstyle_selected = name;
let i = self.active_tab;
self.load_mleaderstyle_bufs(i);
Task::none()
}
Message::MLeaderStyleDialogSetCurrent => {
use acadrust::objects::ObjectType;
let i = self.active_tab;
let name = self.mleaderstyle_selected.clone();
let exists = self.tabs[i]
.scene
.document
.objects
.values()
.any(|o| matches!(o, ObjectType::MultiLeaderStyle(s) if s.name == name));
if exists {
self.tabs[i].active_mleader_style = name.clone();
self.ribbon.active_mleader_style = name.clone();
self.command_line
.push_output(&format!("Current multileader style: {}", name));
}
Task::none()
}
Message::MLeaderStyleDialogNew => {
use acadrust::objects::ObjectType;
let i = self.active_tab;
let doc = &self.tabs[i].scene.document;
let mut n = 1u32;
let new_name = loop {
let candidate = format!("MLeader{}", n);
let taken = doc.objects.values().any(|o| {
matches!(o, ObjectType::MultiLeaderStyle(s) if s.name.eq_ignore_ascii_case(&candidate))
});
if !taken {
break candidate;
}
n += 1;
};
self.push_undo_snapshot(i, "MLEADERSTYLE NEW");
let mut style = acadrust::objects::MultiLeaderStyle::new(&new_name);
let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle());
style.handle = nh;
self.tabs[i]
.scene
.document
.objects
.insert(nh, ObjectType::MultiLeaderStyle(style));
self.mleaderstyle_selected = new_name;
self.load_mleaderstyle_bufs(i);
self.tabs[i].dirty = true;
Task::none()
}
Message::MLeaderStyleDialogDelete => {
use acadrust::objects::ObjectType;
let i = self.active_tab;
let name = self.mleaderstyle_selected.clone();
if name.eq_ignore_ascii_case("Standard") {
self.command_line
.push_error("Cannot delete the Standard style.");
return Task::none();
}
let handle = self.tabs[i]
.scene
.document
.objects
.iter()
.find_map(|(&h, o)| match o {
ObjectType::MultiLeaderStyle(s) if s.name == name => Some(h),
_ => None,
});
if let Some(h) = handle {
self.push_undo_snapshot(i, "MLEADERSTYLE DEL");
self.tabs[i].scene.document.objects.remove(&h);
self.mleaderstyle_selected = self.tabs[i]
.scene
.document
.objects
.values()
.find_map(|o| {
if let ObjectType::MultiLeaderStyle(s) = o {
Some(s.name.clone())
} else {
None
}
})
.unwrap_or_else(|| "Standard".to_string());
self.load_mleaderstyle_bufs(i);
self.tabs[i].dirty = true;
}
Task::none()
}
Message::MLeaderStyleEdit { field, value } => {
match field {
"landing_distance" => self.mls_landing_distance = value,
"landing_gap" => self.mls_landing_gap = value,
"arrowhead_size" => self.mls_arrowhead_size = value,
"text_height" => self.mls_text_height = value,
"scale_factor" => self.mls_scale_factor = value,
"break_gap" => self.mls_break_gap = value,
"first_seg_angle" => self.mls_first_seg_angle = value,
"second_seg_angle" => self.mls_second_seg_angle = value,
"max_points" => self.mls_max_points = value,
"default_text" => self.mls_default_text = value,
"line_color" => self.mls_line_color = value,
"text_color" => self.mls_text_color = value,
_ => {}
}
Task::none()
}
Message::MLeaderStyleToggle(field) => {
let i = self.active_tab;
if let Some(s) = self.mleaderstyle_mut(i) {
match field {
"enable_landing" => s.enable_landing = !s.enable_landing,
"enable_dogleg" => s.enable_dogleg = !s.enable_dogleg,
"text_frame" => s.text_frame = !s.text_frame,
"text_always_left" => s.text_always_left = !s.text_always_left,
"annotative" => s.is_annotative = !s.is_annotative,
_ => {}
}
self.push_undo_snapshot(i, "MLEADERSTYLE EDIT");
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry();
}
Task::none()
}
Message::MLeaderStyleCycle(field) => {
use acadrust::objects::{
LeaderContentType, MultiLeaderPathType, TextAlignmentType, TextAngleType,
};
let i = self.active_tab;
if let Some(s) = self.mleaderstyle_mut(i) {
match field {
"path_type" => {
s.path_type = match s.path_type {
MultiLeaderPathType::Invisible => {
MultiLeaderPathType::StraightLineSegments
}
MultiLeaderPathType::StraightLineSegments => {
MultiLeaderPathType::Spline
}
MultiLeaderPathType::Spline => MultiLeaderPathType::Invisible,
};
}
"content_type" => {
s.content_type = match s.content_type {
LeaderContentType::None => LeaderContentType::Block,
LeaderContentType::Block => LeaderContentType::MText,
LeaderContentType::MText => LeaderContentType::Tolerance,
LeaderContentType::Tolerance => LeaderContentType::None,
};
}
"text_angle_type" => {
s.text_angle_type = match s.text_angle_type {
TextAngleType::ParallelToLastLeaderLine => TextAngleType::Horizontal,
TextAngleType::Horizontal => TextAngleType::Optimized,
TextAngleType::Optimized => {
TextAngleType::ParallelToLastLeaderLine
}
};
}
"text_alignment" => {
s.text_alignment = match s.text_alignment {
TextAlignmentType::Left => TextAlignmentType::Center,
TextAlignmentType::Center => TextAlignmentType::Right,
TextAlignmentType::Right => TextAlignmentType::Left,
};
}
_ => {}
}
self.push_undo_snapshot(i, "MLEADERSTYLE EDIT");
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry();
}
Task::none()
}
Message::MLeaderStyleApply => {
let i = self.active_tab;
let (
ld,
lg,
asz,
th,
sf,
bg,
fsa,
ssa,
mp,
dt,
lc,
tc,
) = (
self.mls_landing_distance.parse::<f64>().ok(),
self.mls_landing_gap.parse::<f64>().ok(),
self.mls_arrowhead_size.parse::<f64>().ok(),
self.mls_text_height.parse::<f64>().ok(),
self.mls_scale_factor.parse::<f64>().ok(),
self.mls_break_gap.parse::<f64>().ok(),
self.mls_first_seg_angle.parse::<f64>().ok(),
self.mls_second_seg_angle.parse::<f64>().ok(),
self.mls_max_points.parse::<i32>().ok(),
self.mls_default_text.clone(),
self.mls_line_color.parse::<i16>().ok(),
self.mls_text_color.parse::<i16>().ok(),
);
if let Some(s) = self.mleaderstyle_mut(i) {
if let Some(v) = ld {
s.landing_distance = v;
}
if let Some(v) = lg {
s.landing_gap = v;
}
if let Some(v) = asz {
s.arrowhead_size = v;
}
if let Some(v) = th {
s.text_height = v;
}
if let Some(v) = sf {
s.scale_factor = v;
}
if let Some(v) = bg {
s.break_gap_size = v;
}
if let Some(v) = fsa {
s.first_segment_angle = v;
}
if let Some(v) = ssa {
s.second_segment_angle = v;
}
if let Some(v) = mp {
s.max_leader_points = v;
}
s.default_text = dt;
if let Some(v) = lc {
s.line_color = acadrust::types::Color::from_index(v);
}
if let Some(v) = tc {
s.text_color = acadrust::types::Color::from_index(v);
}
self.push_undo_snapshot(i, "MLEADERSTYLE EDIT");
self.tabs[i].dirty = true;
self.tabs[i].scene.bump_geometry();
}
Task::none()
}
// ── DimStyle Dialog ───────────────────────────────────────────────
Message::DimStyleDialogOpen => {
let i = self.active_tab;
@@ -6544,6 +6844,62 @@ impl OpenCADStudio {
Some(task)
}
/// Mutable access to the currently selected multileader style.
fn mleaderstyle_mut(
&mut self,
tab: usize,
) -> Option<&mut acadrust::objects::MultiLeaderStyle> {
use acadrust::objects::ObjectType;
let name = self.mleaderstyle_selected.clone();
self.tabs[tab]
.scene
.document
.objects
.values_mut()
.find_map(|o| match o {
ObjectType::MultiLeaderStyle(s) if s.name == name => Some(s),
_ => None,
})
}
/// Populate all edit buffers from the currently selected multileader style.
fn load_mleaderstyle_bufs(&mut self, tab: usize) {
use acadrust::objects::ObjectType;
let name = self.mleaderstyle_selected.clone();
let Some(s) = self.tabs[tab]
.scene
.document
.objects
.values()
.find_map(|o| match o {
ObjectType::MultiLeaderStyle(s) if s.name == name => Some(s),
_ => None,
})
else {
return;
};
self.mls_landing_distance = format!("{:.4}", s.landing_distance);
self.mls_landing_gap = format!("{:.4}", s.landing_gap);
self.mls_arrowhead_size = format!("{:.4}", s.arrowhead_size);
self.mls_text_height = format!("{:.4}", s.text_height);
self.mls_scale_factor = format!("{:.4}", s.scale_factor);
self.mls_break_gap = format!("{:.4}", s.break_gap_size);
self.mls_first_seg_angle = format!("{:.4}", s.first_segment_angle);
self.mls_second_seg_angle = format!("{:.4}", s.second_segment_angle);
self.mls_max_points = s.max_leader_points.to_string();
self.mls_default_text = s.default_text.clone();
self.mls_line_color = s
.line_color
.index()
.map(|c| c.to_string())
.unwrap_or_default();
self.mls_text_color = s
.text_color
.index()
.map(|c| c.to_string())
.unwrap_or_default();
}
/// Populate all edit buffers from the currently selected dim style.
fn load_dimstyle_bufs(&mut self, tab: usize) {
let doc = &self.tabs[tab].scene.document;
+46
View File
@@ -155,6 +155,52 @@ impl OpenCADStudio {
tab.scene.document.header.multiline_style.clone(),
);
}
if Some(window_id) == self.mleaderstyle_window {
use acadrust::objects::ObjectType;
let tab = &self.tabs[self.active_tab];
let styles: Vec<String> = tab
.scene
.document
.objects
.values()
.filter_map(|o| {
if let ObjectType::MultiLeaderStyle(s) = o {
Some(s.name.clone())
} else {
None
}
})
.collect();
let selected_style = tab.scene.document.objects.values().find_map(|o| {
if let ObjectType::MultiLeaderStyle(s) = o {
if s.name == self.mleaderstyle_selected {
Some(s)
} else {
None
}
} else {
None
}
});
return crate::ui::mleaderstyle::view_window(crate::ui::mleaderstyle::MLeaderStyleView {
styles,
selected: &self.mleaderstyle_selected,
style: selected_style,
current: tab.active_mleader_style.clone(),
landing_distance: &self.mls_landing_distance,
landing_gap: &self.mls_landing_gap,
arrowhead_size: &self.mls_arrowhead_size,
text_height: &self.mls_text_height,
scale_factor: &self.mls_scale_factor,
break_gap: &self.mls_break_gap,
first_seg_angle: &self.mls_first_seg_angle,
second_seg_angle: &self.mls_second_seg_angle,
max_points: &self.mls_max_points,
default_text: &self.mls_default_text,
line_color: &self.mls_line_color,
text_color: &self.mls_text_color,
});
}
if Some(window_id) == self.layout_manager_window {
let i = self.active_tab;
let layouts = self.tabs[i].scene.layout_names();