forked from 1b-agent/opencadstudio-upstream
Merge pull request #1613 from ramox81/codex/sheet-set-manager
Sheet Set Manager, NEWSHEETSET / OPENSHEETSET and sheet set fields
This commit is contained in:
commit
078306d8fc
50 files changed
+8974
-27
No files matched your search
@@ -48,6 +48,8 @@ pub enum PanelId {
|
||||
NodeGraph,
|
||||
/// Regions and scans of the drawing's point clouds.
|
||||
PointCloudManager,
|
||||
/// The open sheet sets: subsets and sheets.
|
||||
SheetSetManager,
|
||||
}
|
||||
|
||||
impl PanelId {
|
||||
@@ -60,6 +62,7 @@ impl PanelId {
|
||||
PanelId::Browser => "Browser",
|
||||
PanelId::NodeGraph => "Node Graph",
|
||||
PanelId::PointCloudManager => "Point Cloud Manager",
|
||||
PanelId::SheetSetManager => "Sheet Set Manager",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +75,7 @@ impl PanelId {
|
||||
PanelId::Browser => 230.0,
|
||||
PanelId::NodeGraph => 220.0,
|
||||
PanelId::PointCloudManager => 280.0,
|
||||
PanelId::SheetSetManager => 280.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +166,7 @@ impl DockState {
|
||||
PanelId::Browser,
|
||||
PanelId::NodeGraph,
|
||||
PanelId::PointCloudManager,
|
||||
PanelId::SheetSetManager,
|
||||
] {
|
||||
self.panels.entry(id).or_insert_with(|| DockPanel::for_id(id));
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ pub struct Ribbon {
|
||||
pub show_ucs_icon: bool,
|
||||
/// Properties panel (PROPERTIES) visibility — drives the Properties button highlight.
|
||||
pub show_properties: bool,
|
||||
/// Sheet Set Manager palette shown (SSMSTATE).
|
||||
pub show_sheet_set: bool,
|
||||
/// File tabs (FILETAB) visibility — drives the File Tabs button highlight.
|
||||
pub show_file_tabs: bool,
|
||||
/// Layout tabs (LAYOUTTAB) visibility — drives the Layout Tabs button highlight.
|
||||
@@ -174,6 +176,7 @@ impl Ribbon {
|
||||
show_viewcube: true,
|
||||
show_ucs_icon: true,
|
||||
show_properties: true,
|
||||
show_sheet_set: false,
|
||||
show_file_tabs: true,
|
||||
show_layout_tabs: true,
|
||||
open_dropdown: None,
|
||||
@@ -335,6 +338,9 @@ impl Ribbon {
|
||||
pub fn set_properties(&mut self, on: bool) {
|
||||
self.show_properties = on;
|
||||
}
|
||||
pub fn set_sheet_set(&mut self, on: bool) {
|
||||
self.show_sheet_set = on;
|
||||
}
|
||||
pub fn set_file_tabs(&mut self, on: bool) {
|
||||
self.show_file_tabs = on;
|
||||
}
|
||||
@@ -353,6 +359,7 @@ impl Ribbon {
|
||||
show_ucs_icon: self.show_ucs_icon,
|
||||
show_properties: self.show_properties,
|
||||
show_block_palette,
|
||||
show_sheet_set: self.show_sheet_set,
|
||||
show_file_tabs: self.show_file_tabs,
|
||||
show_layout_tabs: self.show_layout_tabs,
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ pub(super) struct ToggleState {
|
||||
pub show_ucs_icon: bool,
|
||||
pub show_properties: bool,
|
||||
pub show_block_palette: bool,
|
||||
pub show_sheet_set: bool,
|
||||
pub show_file_tabs: bool,
|
||||
pub show_layout_tabs: bool,
|
||||
}
|
||||
@@ -380,6 +381,7 @@ pub(super) fn is_active_tool(
|
||||
"UCSICON" => state.show_ucs_icon,
|
||||
"PROPERTIES" => state.show_properties,
|
||||
"BLOCKPALETTE" => state.show_block_palette,
|
||||
"SHEETSET" => state.show_sheet_set,
|
||||
"FILETAB" => state.show_file_tabs,
|
||||
"LAYOUTTAB" => state.show_layout_tabs,
|
||||
id => active_tool.as_deref() == Some(id),
|
||||
|
||||
@@ -378,6 +378,11 @@ impl<'a> Program<Message> for BlockPreviewCanvas<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A block preview (the palette's canvas) for other dialogs.
|
||||
pub(crate) fn preview_canvas<'a>(wires: &'a [WireModel], height: f32) -> Element<'a, Message> {
|
||||
canvas(BlockPreviewCanvas { wires }).width(Fill).height(Length::Fixed(height)).into()
|
||||
}
|
||||
|
||||
fn pm(m: BlockPaletteMsg) -> Message {
|
||||
Message::BlockPalette(m)
|
||||
}
|
||||
|
||||
@@ -38,8 +38,234 @@ pub const CATEGORIES: &[(&str, &[&str])] = &[
|
||||
"Plot",
|
||||
&["DeviceName", "Login", "PageSetupName", "PaperSize", "PlotDate", "PlotOrientation", "PlotScale", "PlotStyleTable"],
|
||||
),
|
||||
("SheetSet", SHEET_SET_FIELDS),
|
||||
];
|
||||
|
||||
/// The SheetSet category, in the reference's order.
|
||||
pub const SHEET_SET_FIELDS: &[&str] = &[
|
||||
"CurrentSheetCategory",
|
||||
"CurrentSheetCustom",
|
||||
"CurrentSheetDescription",
|
||||
"CurrentSheetIssuePurpose",
|
||||
"CurrentSheetNumber",
|
||||
"CurrentSheetNumberAndTitle",
|
||||
"CurrentSheetRevisionDate",
|
||||
"CurrentSheetRevisionNumber",
|
||||
"CurrentSheetSet",
|
||||
"CurrentSheetSetCustom",
|
||||
"CurrentSheetSetDescription",
|
||||
"CurrentSheetSetProjectMilestone",
|
||||
"CurrentSheetSetProjectName",
|
||||
"CurrentSheetSetProjectNumber",
|
||||
"CurrentSheetSetProjectPhase",
|
||||
"CurrentSheetSubSet",
|
||||
"CurrentSheetTitle",
|
||||
"SheetSet",
|
||||
"SheetSetPlaceholder",
|
||||
"SheetView",
|
||||
];
|
||||
|
||||
/// A sheet set field's code without format: `\AcSm[.16.2] Component.Property`.
|
||||
fn sheet_set_code(name: &str, custom: &str) -> String {
|
||||
let (version, target) = match name {
|
||||
"CurrentSheetCategory" => (".16.2", "Sheet.Category".to_string()),
|
||||
"CurrentSheetCustom" => ("", format!("Sheet.{custom}")),
|
||||
"CurrentSheetDescription" => ("", "Sheet.Description".into()),
|
||||
"CurrentSheetIssuePurpose" => (".16.2", "Sheet.IssuePurpose".into()),
|
||||
"CurrentSheetNumber" => ("", "Sheet.Number".into()),
|
||||
"CurrentSheetNumberAndTitle" => ("", "Sheet.NumberAndTitle".into()),
|
||||
"CurrentSheetRevisionDate" => (".16.2", "Sheet.RevisionDate".into()),
|
||||
"CurrentSheetRevisionNumber" => (".16.2", "Sheet.RevisionNumber".into()),
|
||||
"CurrentSheetSet" => ("", "SheetSet.Name".into()),
|
||||
"CurrentSheetSetCustom" => ("", format!("SheetSet.{custom}")),
|
||||
"CurrentSheetSetDescription" => ("", "SheetSet.Description".into()),
|
||||
"CurrentSheetSetProjectMilestone" => (".16.2", "SheetSet.ProjectMilestone".into()),
|
||||
"CurrentSheetSetProjectName" => (".16.2", "SheetSet.ProjectName".into()),
|
||||
"CurrentSheetSetProjectNumber" => (".16.2", "SheetSet.ProjectNumber".into()),
|
||||
"CurrentSheetSetProjectPhase" => (".16.2", "SheetSet.ProjectPhase".into()),
|
||||
"CurrentSheetSubSet" => ("", "Subset.Name".into()),
|
||||
"CurrentSheetTitle" => ("", "Sheet.Title".into()),
|
||||
_ => return "\\AcSm".into(),
|
||||
};
|
||||
format!("\\AcSm{version} {target}")
|
||||
}
|
||||
|
||||
/// The navigation tree of a SheetSet (set, subsets, sheets) or SheetView
|
||||
/// (set, view categories) field: depth, component id, label, is a sheet.
|
||||
pub fn nav_nodes(db: &codec::sheet_set::SheetSetDatabase, views: bool) -> Vec<(u16, String, String, bool)> {
|
||||
use codec::sheet_set::{number_and_title, ComponentKind, Element as SsElement};
|
||||
let set = db.sheet_set();
|
||||
let mut out = vec![(0, set.id().to_string(), db.name().to_string(), false)];
|
||||
if views {
|
||||
// Views of the unnamed (default) category sit under the set, then each
|
||||
// named category with its views.
|
||||
let all: Vec<&SsElement> = db.sheets().into_iter().flat_map(|s| db.sheet_views(s)).collect();
|
||||
let named: Vec<&SsElement> = db.view_categories();
|
||||
let in_named = |v: &SsElement| {
|
||||
codec::sheet_set::SheetSetDatabase::view_category_of(v).is_some_and(|c| named.iter().any(|n| n.id() == c))
|
||||
};
|
||||
for v in all.iter().filter(|v| !in_named(v)) {
|
||||
out.push((1, v.id().to_string(), number_and_title(v), true));
|
||||
}
|
||||
for c in &named {
|
||||
out.push((1, c.id().to_string(), c.prop("Name").unwrap_or("").to_string(), false));
|
||||
for v in all.iter().filter(|v| codec::sheet_set::SheetSetDatabase::view_category_of(v) == Some(c.id())) {
|
||||
out.push((2, v.id().to_string(), number_and_title(v), true));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
fn go(el: &SsElement, depth: u16, out: &mut Vec<(u16, String, String, bool)>) {
|
||||
for c in &el.children {
|
||||
match ComponentKind::of(c) {
|
||||
Some(ComponentKind::Subset) => {
|
||||
out.push((depth, c.id().to_string(), c.prop("Name").unwrap_or("").to_string(), false));
|
||||
go(c, depth + 1, out);
|
||||
}
|
||||
Some(ComponentKind::Sheet) => out.push((depth, c.id().to_string(), number_and_title(c), true)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
go(set, 1, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// The properties a navigation node offers: list label, code property, version.
|
||||
pub fn nav_props(db: &codec::sheet_set::SheetSetDatabase, id: &str) -> Vec<(String, String, &'static str)> {
|
||||
use codec::sheet_set::{custom_properties, ComponentKind, CUSTOM_SHEET_PROP, CUSTOM_SHEET_SET_PROP};
|
||||
let Some(el) = db.find(id) else { return Vec::new() };
|
||||
let fixed = |list: &[(&str, &str, &'static str)]| -> Vec<(String, String, &'static str)> {
|
||||
list.iter().map(|(l, p, v)| (l.to_string(), p.to_string(), *v)).collect()
|
||||
};
|
||||
let custom = |flag: i32| -> Vec<(String, String, &'static str)> {
|
||||
custom_properties(db.sheet_set()).into_iter().filter(|(_, _, f)| f & flag != 0).map(|(n, _, _)| (n.clone(), n, "")).collect()
|
||||
};
|
||||
match ComponentKind::of(el) {
|
||||
Some(ComponentKind::SheetSet) => {
|
||||
let mut v = fixed(&[
|
||||
("SheetSetName", "Name", ""),
|
||||
("SheetSetDescription", "Description", ""),
|
||||
("ProjectNumber", "ProjectNumber", ".16.2"),
|
||||
("ProjectName", "ProjectName", ".16.2"),
|
||||
("ProjectPhase", "ProjectPhase", ".16.2"),
|
||||
("ProjectMilestone", "ProjectMilestone", ".16.2"),
|
||||
]);
|
||||
v.extend(custom(CUSTOM_SHEET_SET_PROP));
|
||||
v
|
||||
}
|
||||
Some(ComponentKind::Subset) => fixed(&[("SheetSubsetName", "Name", "")]),
|
||||
Some(ComponentKind::Sheet) => {
|
||||
let mut v = fixed(&[
|
||||
("SheetNumberAndTitle", "NumberAndTitle", ""),
|
||||
("SheetTitle", "Title", ""),
|
||||
("SheetNumber", "Number", ""),
|
||||
("SheetDescription", "Description", ""),
|
||||
("RevisionNumber", "RevisionNumber", ".16.2"),
|
||||
("RevisionDate", "RevisionDate", ".16.2"),
|
||||
("IssuePurpose", "IssuePurpose", ".16.2"),
|
||||
("Category", "Category", ".16.2"),
|
||||
]);
|
||||
v.extend(custom(CUSTOM_SHEET_PROP));
|
||||
v
|
||||
}
|
||||
None if el.name == "AcSmSheetView" => fixed(&[
|
||||
("ViewNumberAndTitle", "NumberAndTitle", ""),
|
||||
("ViewTitle", "Title", ""),
|
||||
("ViewNumber", "Number", ""),
|
||||
("ViewportScale", "ViewportScale", ".16.2"),
|
||||
]),
|
||||
None => fixed(&[("ViewCategoryName", "Name", "")]),
|
||||
}
|
||||
}
|
||||
|
||||
/// The sheet drawing, named view and layout a sheet or sheet view node links to.
|
||||
pub fn nav_link(db: &codec::sheet_set::SheetSetDatabase, id: &str) -> Option<(String, String, String)> {
|
||||
let el = db.find(id)?;
|
||||
let (sheet, view) = if el.name == "AcSmSheetView" {
|
||||
let views = db.parent_of(id)?;
|
||||
let sheet = db.parent_of(views.id())?;
|
||||
(sheet.id().to_string(), el.named("NamedView").and_then(|v| v.prop("Name")).unwrap_or("").to_string())
|
||||
} else if codec::sheet_set::ComponentKind::of(el) == Some(codec::sheet_set::ComponentKind::Sheet) {
|
||||
(id.to_string(), String::new())
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let r = db.layout_reference(&sheet, "Layout")?;
|
||||
Some((r.file_name, view, r.name))
|
||||
}
|
||||
|
||||
impl FieldDialogState {
|
||||
/// `\AcSm Database("…").SheetSet("…")[.Component("…")].Property` of the
|
||||
/// picked node, with the text case and, on a sheet, the hyperlink to it.
|
||||
fn navigation_code(&self) -> String {
|
||||
let Some(db) = self.ss_sets.get(self.ss_set) else { return "\\AcSm".into() };
|
||||
let Some((_, prop, version)) = nav_props(db, &self.ss_node).into_iter().find(|(l, _, _)| *l == self.ss_prop) else {
|
||||
return "\\AcSm".into();
|
||||
};
|
||||
let set = db.sheet_set().id().to_string();
|
||||
let path = codec::sheet_set::native_path(db.path.as_deref().unwrap_or(""));
|
||||
// ViewportScale takes a scale format (and its version), not a text case.
|
||||
let scale = prop == "ViewportScale";
|
||||
let version = if scale {
|
||||
codec::fields::PLOT_SCALE_FORMATS
|
||||
.get(self.ss_scale_format)
|
||||
.map_or("", |(_, c)| if c.starts_with("\\AcVar.16.2") { ".16.2" } else { "" })
|
||||
} else {
|
||||
version
|
||||
};
|
||||
let mut code = format!("\\AcSm{version} Database(\"{path}\").SheetSet(\"{set}\")");
|
||||
if self.ss_node != set {
|
||||
code.push_str(&format!(".Component(\"{}\")", self.ss_node));
|
||||
}
|
||||
code.push('.');
|
||||
code.push_str(&prop);
|
||||
if scale {
|
||||
let f = viewport_scale_picture(self.ss_scale_format);
|
||||
if !f.is_empty() {
|
||||
code.push(' ');
|
||||
code.push_str(&f);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
if self.text_case != 0 {
|
||||
code.push_str(&format!(" \\f \"%tc{}\"", self.text_case));
|
||||
}
|
||||
// A sheet links to its layout, a view to its named view on the sheet.
|
||||
if self.ss_href {
|
||||
if let Some((file, view, layout)) = nav_link(db, &self.ss_node) {
|
||||
code.push_str(&format!(" \\href \"{file}#{view},{layout}##1\""));
|
||||
}
|
||||
}
|
||||
code
|
||||
}
|
||||
}
|
||||
|
||||
/// SheetSetPlaceholder types: list label, the `?` target, field version.
|
||||
pub const PLACEHOLDER_TYPES: &[(&str, &str, &str)] = &[
|
||||
("SheetNumberAndTitle", "?Sheet.NumberAndTitle", ""),
|
||||
("SheetTitle", "?Sheet.Title", ""),
|
||||
("SheetNumber", "?Sheet.Number", ""),
|
||||
("SheetDescription", "?Sheet.Description", ""),
|
||||
("RevisionNumber", "?Sheet.RevisionNumber", ".16.2"),
|
||||
("RevisionDate", "?Sheet.RevisionDate", ".16.2"),
|
||||
("IssuePurpose", "?Sheet.IssuePurpose", ".16.2"),
|
||||
("Category", "?Sheet.Category", ".16.2"),
|
||||
("Custom", "?Sheet.", ""),
|
||||
("ViewNumberAndTitle", "?View.NumberAndTitle", ""),
|
||||
("ViewTitle", "?View.Title", ""),
|
||||
("ViewNumber", "?View.Number", ""),
|
||||
("ViewportScale", "?View.ViewportScale", ".16.2"),
|
||||
];
|
||||
|
||||
/// ViewportScale placeholder formats: the PlotScale pictures.
|
||||
pub fn viewport_scale_picture(k: usize) -> String {
|
||||
codec::fields::PLOT_SCALE_FORMATS
|
||||
.get(k)
|
||||
.and_then(|(_, code)| code.find("\\f ").map(|p| code[p..].to_string()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The fields of a category; "All" lists every field once, sorted.
|
||||
pub fn fields_of(category: usize) -> Vec<&'static str> {
|
||||
if category == 0 {
|
||||
@@ -77,6 +303,13 @@ pub enum FieldKind {
|
||||
Formula,
|
||||
Hyperlink,
|
||||
PlotScale,
|
||||
/// A CurrentSheet* / CurrentSheetSet* field (text case).
|
||||
SheetSet,
|
||||
/// CurrentSheetCustom / CurrentSheetSetCustom (text case + property).
|
||||
SheetSetCustom,
|
||||
SheetSetPlaceholder,
|
||||
/// SheetSet / SheetView: a component picked in a sheet set.
|
||||
SheetSetNavigation,
|
||||
BlockPlaceholder,
|
||||
}
|
||||
|
||||
@@ -96,6 +329,10 @@ pub fn kind_of(name: &str) -> FieldKind {
|
||||
"Formula" => FieldKind::Formula,
|
||||
"Hyperlink" => FieldKind::Hyperlink,
|
||||
"PlotScale" => FieldKind::PlotScale,
|
||||
"CurrentSheetCustom" | "CurrentSheetSetCustom" => FieldKind::SheetSetCustom,
|
||||
"SheetSetPlaceholder" => FieldKind::SheetSetPlaceholder,
|
||||
"SheetSet" | "SheetView" => FieldKind::SheetSetNavigation,
|
||||
n if SHEET_SET_FIELDS.contains(&n) => FieldKind::SheetSet,
|
||||
"BlockPlaceholder" => FieldKind::BlockPlaceholder,
|
||||
_ => FieldKind::Text,
|
||||
}
|
||||
@@ -136,6 +373,23 @@ pub struct FieldDialogState {
|
||||
/// Index into codec BLOCK_PLACEHOLDER_PROPERTIES.
|
||||
pub placeholder_property: usize,
|
||||
pub preview: String,
|
||||
/// Sheet set fields: the open set's sheet (flags 2) and set (flags 1)
|
||||
/// custom property names, the chosen one, the placeholder type and the
|
||||
/// ViewportScale format.
|
||||
pub ss_sheet_custom: Vec<String>,
|
||||
pub ss_set_custom: Vec<String>,
|
||||
pub ss_custom: String,
|
||||
pub ss_placeholder: usize,
|
||||
pub ss_scale_format: usize,
|
||||
/// The open sheet set's name (SheetSet / SheetView fields).
|
||||
pub ss_set_name: String,
|
||||
/// SheetSet / SheetView: the sets offered (open ones and any picked with
|
||||
/// "..."), the one shown, the node and property picked, associate hyperlink.
|
||||
pub ss_sets: Vec<codec::sheet_set::SheetSetDatabase>,
|
||||
pub ss_set: usize,
|
||||
pub ss_node: String,
|
||||
pub ss_prop: String,
|
||||
pub ss_href: bool,
|
||||
/// Today in each of DATE_FORMATS, for the Examples list.
|
||||
pub examples: Vec<String>,
|
||||
}
|
||||
@@ -168,6 +422,17 @@ impl FieldDialogState {
|
||||
placeholder_block: None,
|
||||
placeholder_property: 7,
|
||||
preview: String::new(),
|
||||
ss_sheet_custom: Vec::new(),
|
||||
ss_set_custom: Vec::new(),
|
||||
ss_custom: String::new(),
|
||||
ss_placeholder: 0,
|
||||
ss_scale_format: 6,
|
||||
ss_set_name: String::new(),
|
||||
ss_sets: Vec::new(),
|
||||
ss_set: 0,
|
||||
ss_node: String::new(),
|
||||
ss_prop: String::new(),
|
||||
ss_href: true,
|
||||
examples: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -221,6 +486,25 @@ impl FieldDialogState {
|
||||
.map_or_else(|| "\\AcVar PlotScale".to_string(), |f| f.1.to_string()),
|
||||
vec![],
|
||||
),
|
||||
FieldKind::SheetSet | FieldKind::SheetSetCustom => (case(sheet_set_code(self.name, &self.ss_custom)), vec![]),
|
||||
FieldKind::SheetSetNavigation => (self.navigation_code(), vec![]),
|
||||
FieldKind::SheetSetPlaceholder => {
|
||||
let (label, target, version) = PLACEHOLDER_TYPES[self.ss_placeholder.min(PLACEHOLDER_TYPES.len() - 1)];
|
||||
let target = if label == "Custom" { format!("{target}{}", self.ss_custom) } else { target.to_string() };
|
||||
if label == "ViewportScale" {
|
||||
// The version follows the format, as for PlotScale.
|
||||
let version = codec::fields::PLOT_SCALE_FORMATS
|
||||
.get(self.ss_scale_format)
|
||||
.map_or("", |(_, code)| if code.starts_with("\\AcVar.16.2") { ".16.2" } else { "" });
|
||||
let base = format!("\\AcSm{version} {target}");
|
||||
match viewport_scale_picture(self.ss_scale_format) {
|
||||
f if f.is_empty() => (base, vec![]),
|
||||
f => (format!("{base} {f}"), vec![]),
|
||||
}
|
||||
} else {
|
||||
(case(format!("\\AcSm{version} {target}")), vec![])
|
||||
}
|
||||
}
|
||||
FieldKind::BlockPlaceholder => {
|
||||
let (_, property, format) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[self.placeholder_property];
|
||||
// Text properties take the chosen case; the others keep their own format.
|
||||
@@ -330,6 +614,15 @@ pub enum FieldDialogMsg {
|
||||
HyperlinkUrl(String),
|
||||
BrowseHyperlink,
|
||||
PlotScale(usize),
|
||||
SsCustom(String),
|
||||
SsPlaceholder(usize),
|
||||
SsScaleFormat(usize),
|
||||
SsSet(usize),
|
||||
SsNode(String),
|
||||
SsProp(String),
|
||||
SsHref(bool),
|
||||
SsBrowse,
|
||||
SsPicked(Option<std::path::PathBuf>),
|
||||
Help,
|
||||
Ok,
|
||||
}
|
||||
@@ -632,6 +925,123 @@ fn format_panel<'a>(state: &'a FieldDialogState) -> Element<'a, Message> {
|
||||
]
|
||||
.spacing(8)
|
||||
.into(),
|
||||
FieldKind::SheetSet => column![text(t!("Format")).size(12), case_list(state)].spacing(8).into(),
|
||||
FieldKind::SheetSetCustom => {
|
||||
let names = if state.name == "CurrentSheetCustom" { &state.ss_sheet_custom } else { &state.ss_set_custom };
|
||||
column![
|
||||
text(t!("Format")).size(12),
|
||||
case_list(state),
|
||||
text(t!("Custom property name")).size(12),
|
||||
pick_list(
|
||||
(!state.ss_custom.is_empty()).then(|| state.ss_custom.clone()),
|
||||
names.clone(),
|
||||
|s: &String| s.clone(),
|
||||
)
|
||||
.on_select(|s| msg(FieldDialogMsg::SsCustom(s)))
|
||||
.text_size(12)
|
||||
.padding([5, 8])
|
||||
.width(Fill),
|
||||
]
|
||||
.spacing(8)
|
||||
.into()
|
||||
}
|
||||
FieldKind::SheetSetPlaceholder => {
|
||||
let kind = PLACEHOLDER_TYPES[state.ss_placeholder.min(PLACEHOLDER_TYPES.len() - 1)].0;
|
||||
let types = list(
|
||||
PLACEHOLDER_TYPES
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (n, _, _))| (n.to_string(), state.ss_placeholder == i, FieldDialogMsg::SsPlaceholder(i)))
|
||||
.collect(),
|
||||
200.0,
|
||||
);
|
||||
let format: Element<'a, Message> = if kind == "ViewportScale" {
|
||||
list(
|
||||
codec::fields::PLOT_SCALE_FORMATS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (n, _))| (t!(*n).into_owned(), state.ss_scale_format == i, FieldDialogMsg::SsScaleFormat(i)))
|
||||
.collect(),
|
||||
170.0,
|
||||
)
|
||||
} else {
|
||||
case_list(state)
|
||||
};
|
||||
let mut col = column![text(t!("Placeholder type")).size(12), types, text(t!("Format")).size(12), format].spacing(8);
|
||||
if kind == "Custom" {
|
||||
col = col.push(text(t!("Custom property name")).size(12)).push(
|
||||
pick_list(
|
||||
(!state.ss_custom.is_empty()).then(|| state.ss_custom.clone()),
|
||||
state.ss_sheet_custom.clone(),
|
||||
|s: &String| s.clone(),
|
||||
)
|
||||
.on_select(|s| msg(FieldDialogMsg::SsCustom(s)))
|
||||
.text_size(12)
|
||||
.padding([5, 8])
|
||||
.width(Fill),
|
||||
);
|
||||
}
|
||||
col.into()
|
||||
}
|
||||
FieldKind::SheetSetNavigation => {
|
||||
let picks: Vec<Pick> = state.ss_sets.iter().enumerate().map(|(i, d)| Pick(i, d.name().to_string())).collect();
|
||||
let current = picks.get(state.ss_set).cloned();
|
||||
let set_row = row![
|
||||
pick_list(current, picks, |p: &Pick| p.1.clone())
|
||||
.on_select(|p: Pick| msg(FieldDialogMsg::SsSet(p.0)))
|
||||
.text_size(12)
|
||||
.padding([5, 8])
|
||||
.width(Fill),
|
||||
button(text("...").size(12)).on_press(msg(FieldDialogMsg::SsBrowse)).style(button_style(false)).padding([5, 10]),
|
||||
]
|
||||
.spacing(6);
|
||||
let db = state.ss_sets.get(state.ss_set);
|
||||
let nodes = db.map(|d| nav_nodes(d, state.name == "SheetView")).unwrap_or_default();
|
||||
let tree = list(
|
||||
nodes
|
||||
.iter()
|
||||
.map(|(depth, id, label, _)| {
|
||||
(format!("{}{label}", " ".repeat(usize::from(*depth))), *id == state.ss_node, FieldDialogMsg::SsNode(id.clone()))
|
||||
})
|
||||
.collect(),
|
||||
118.0,
|
||||
);
|
||||
let props = db.map(|d| nav_props(d, &state.ss_node)).unwrap_or_default();
|
||||
let prop_list = list(
|
||||
props.iter().map(|(l, _, _)| (l.clone(), *l == state.ss_prop, FieldDialogMsg::SsProp(l.clone()))).collect(),
|
||||
92.0,
|
||||
);
|
||||
let linkable = db.is_some_and(|d| nav_link(d, &state.ss_node).is_some()) && state.ss_prop != "ViewportScale";
|
||||
let mut href = checkbox(state.ss_href && linkable).label(t!("Associate hyperlink").into_owned()).text_size(12).size(14);
|
||||
if linkable {
|
||||
href = href.on_toggle(|v| msg(FieldDialogMsg::SsHref(v)));
|
||||
}
|
||||
let format: Element<'a, Message> = if state.ss_prop == "ViewportScale" {
|
||||
list(
|
||||
codec::fields::PLOT_SCALE_FORMATS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (n, _))| (t!(*n).into_owned(), state.ss_scale_format == i, FieldDialogMsg::SsScaleFormat(i)))
|
||||
.collect(),
|
||||
120.0,
|
||||
)
|
||||
} else {
|
||||
case_list(state)
|
||||
};
|
||||
column![
|
||||
text(t!("Sheet set")).size(12),
|
||||
set_row,
|
||||
text(t!("Sheet navigation tree")).size(12),
|
||||
tree,
|
||||
text(t!("Property")).size(12),
|
||||
prop_list,
|
||||
text(t!("Format")).size(12),
|
||||
format,
|
||||
href,
|
||||
]
|
||||
.spacing(8)
|
||||
.into()
|
||||
}
|
||||
FieldKind::Object => column![
|
||||
row![
|
||||
text(t!("Object type")).size(12).width(Length::Fixed(96.0)),
|
||||
@@ -728,6 +1138,7 @@ pub fn view<'a>(
|
||||
.spacing(10),
|
||||
FieldKind::Hyperlink => column![card(t!("Hyperlink").into_owned(), format_panel(state)), preview].spacing(10),
|
||||
FieldKind::BlockPlaceholder => column![card(t!("Block placeholder").into_owned(), format_panel(state)), preview].spacing(10),
|
||||
FieldKind::SheetSetNavigation => column![card(t!("Sheet set").into_owned(), format_panel(state)), preview].spacing(10),
|
||||
_ => column![card(t!("Format").into_owned(), format_panel(state)), preview].spacing(10),
|
||||
};
|
||||
let (code, _) = state.code();
|
||||
|
||||
@@ -27,6 +27,8 @@ pub mod alias_editor;
|
||||
pub mod find_replace;
|
||||
pub mod named_parameters;
|
||||
pub mod pc_manager;
|
||||
pub mod row_menu;
|
||||
pub mod sheet_set;
|
||||
pub mod pdf_dialogs;
|
||||
pub mod xref_attach;
|
||||
pub mod xref_help;
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
//! A right-click menu for palette rows with one level of fly-out submenus.
|
||||
//!
|
||||
//! The menu opens at the cursor (flipped at the window edges); an open fly-out
|
||||
//! is laid out beside it — on the right when it fits, otherwise on the left —
|
||||
//! so the menu itself never moves. Pressing a submenu entry keeps the menu open.
|
||||
|
||||
use iced::advanced::layout::{self, Layout};
|
||||
use iced::advanced::widget::{self, tree, Tree, Widget};
|
||||
use iced::advanced::{mouse, overlay, renderer, Shell};
|
||||
use iced::{keyboard, Element, Event, Length, Point, Rectangle, Renderer, Size, Theme, Vector};
|
||||
|
||||
use crate::app::Message;
|
||||
|
||||
const FLYOUT_GAP: f32 = 2.0;
|
||||
|
||||
pub(crate) struct RowMenu<'a> {
|
||||
underlay: Element<'a, Message>,
|
||||
main: Element<'a, Message>,
|
||||
/// Per child of the menu's column: a submenu entry or a separator (a click on it keeps the menu open).
|
||||
keep_open: Vec<bool>,
|
||||
/// The open fly-out: its entry's index in the column, and its content.
|
||||
flyout: Option<(usize, Element<'a, Message>)>,
|
||||
/// Published when the menu closes with a fly-out open.
|
||||
on_close: Message,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
show: bool,
|
||||
position: Point,
|
||||
}
|
||||
|
||||
impl<'a> RowMenu<'a> {
|
||||
pub(crate) fn new(
|
||||
underlay: impl Into<Element<'a, Message>>,
|
||||
main: Element<'a, Message>,
|
||||
keep_open: Vec<bool>,
|
||||
flyout: Option<(usize, Element<'a, Message>)>,
|
||||
on_close: Message,
|
||||
) -> Self {
|
||||
Self { underlay: underlay.into(), main, keep_open, flyout, on_close }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget<Message, Theme, Renderer> for RowMenu<'a> {
|
||||
fn size(&self) -> Size<Length> {
|
||||
self.underlay.as_widget().size()
|
||||
}
|
||||
|
||||
fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
|
||||
self.underlay.as_widget_mut().layout(&mut tree.children[0], renderer, limits)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.underlay.as_widget().draw(&tree.children[0], renderer, theme, style, layout, cursor, viewport);
|
||||
}
|
||||
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::default())
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
let mut refs: Vec<&mut Element<'a, Message>> = vec![&mut self.underlay, &mut self.main];
|
||||
if let Some((_, f)) = &mut self.flyout {
|
||||
refs.push(f);
|
||||
}
|
||||
tree.diff_children(&mut refs);
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = event {
|
||||
if let Some(p) = cursor.position_over(layout.bounds()) {
|
||||
let state: &mut State = tree.state.downcast_mut();
|
||||
state.show = true;
|
||||
state.position = p;
|
||||
if self.flyout.is_some() {
|
||||
shell.publish(self.on_close.clone());
|
||||
}
|
||||
shell.capture_event();
|
||||
shell.request_redraw();
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.underlay
|
||||
.as_widget_mut()
|
||||
.update(&mut tree.children[0], event, layout, cursor, renderer, shell, viewport);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.underlay.as_widget().mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.underlay.as_widget_mut().operate(&mut tree.children[0], layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'b>,
|
||||
renderer: &Renderer,
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
let state: &mut State = tree.state.downcast_mut();
|
||||
if !state.show {
|
||||
return self.underlay.as_widget_mut().overlay(&mut tree.children[0], layout, renderer, viewport, translation);
|
||||
}
|
||||
let position = state.position + translation;
|
||||
let (_, trees) = tree.children.split_at_mut(1);
|
||||
let (main_tree, fly_tree) = trees.split_at_mut(1);
|
||||
Some(overlay::Element::new(Box::new(MenuOverlay {
|
||||
state,
|
||||
position,
|
||||
main: &mut self.main,
|
||||
main_tree: &mut main_tree[0],
|
||||
keep_open: &self.keep_open,
|
||||
flyout: self.flyout.as_mut().map(|(i, f)| (*i, f)).zip(fly_tree.first_mut()),
|
||||
on_close: &self.on_close,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<RowMenu<'a>> for Element<'a, Message> {
|
||||
fn from(w: RowMenu<'a>) -> Self {
|
||||
Element::new(w)
|
||||
}
|
||||
}
|
||||
|
||||
struct MenuOverlay<'a, 'b> {
|
||||
state: &'b mut State,
|
||||
position: Point,
|
||||
main: &'b mut Element<'a, Message>,
|
||||
main_tree: &'b mut Tree,
|
||||
keep_open: &'b [bool],
|
||||
flyout: Option<((usize, &'b mut Element<'a, Message>), &'b mut Tree)>,
|
||||
on_close: &'b Message,
|
||||
}
|
||||
|
||||
/// The entries of a menu box (container › column › entries).
|
||||
fn entries(main: Layout<'_>) -> impl Iterator<Item = Layout<'_>> {
|
||||
main.children().next().into_iter().flat_map(|column| column.children())
|
||||
}
|
||||
|
||||
impl MenuOverlay<'_, '_> {
|
||||
fn close(&mut self, shell: &mut Shell<'_, Message>) {
|
||||
self.state.show = false;
|
||||
if self.flyout.is_some() {
|
||||
shell.publish(self.on_close.clone());
|
||||
}
|
||||
shell.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
impl overlay::Overlay<Message, Theme, Renderer> for MenuOverlay<'_, '_> {
|
||||
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
|
||||
let limits = layout::Limits::new(Size::ZERO, bounds);
|
||||
let main = self.main.as_widget_mut().layout(self.main_tree, renderer, &limits);
|
||||
let size = main.size();
|
||||
// At the cursor, flipped to the other side at the window edges.
|
||||
let mut p = self.position;
|
||||
if p.x + size.width > bounds.width {
|
||||
p.x = (p.x - size.width).max(0.0);
|
||||
}
|
||||
if p.y + size.height > bounds.height {
|
||||
p.y = (p.y - size.height).max(0.0);
|
||||
}
|
||||
let main = main.move_to(p);
|
||||
let mut nodes = Vec::with_capacity(2);
|
||||
if let Some(((index, flyout), tree)) = self.flyout.as_mut() {
|
||||
let node = flyout.as_widget_mut().layout(tree, renderer, &limits);
|
||||
let fly = node.size();
|
||||
// Its first item level with the entry (both boxes have the same inset).
|
||||
let entry_y = main.children().first().and_then(|column| column.children().get(*index)).map_or(0.0, |e| e.bounds().y);
|
||||
let y = (p.y + entry_y).min(bounds.height - fly.height).max(0.0);
|
||||
// On the right when it fits, otherwise on the left.
|
||||
let right = p.x + size.width + FLYOUT_GAP;
|
||||
let x = if right + fly.width <= bounds.width { right } else { (p.x - fly.width - FLYOUT_GAP).max(0.0) };
|
||||
nodes.push(main);
|
||||
nodes.push(node.move_to(Point::new(x, y)));
|
||||
} else {
|
||||
nodes.push(main);
|
||||
}
|
||||
layout::Node::with_children(bounds, nodes)
|
||||
}
|
||||
|
||||
fn draw(&self, renderer: &mut Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor) {
|
||||
let mut children = layout.children();
|
||||
if let Some(main) = children.next() {
|
||||
self.main.as_widget().draw(self.main_tree, renderer, theme, style, main, cursor, &layout.bounds());
|
||||
}
|
||||
if let (Some(((_, flyout), tree)), Some(fly)) = (self.flyout.as_ref(), children.next()) {
|
||||
flyout.as_widget().draw(tree, renderer, theme, style, fly, cursor, &layout.bounds());
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) {
|
||||
let mut children = layout.children();
|
||||
let Some(main) = children.next() else { return };
|
||||
let fly = children.next();
|
||||
let over = cursor.is_over(main.bounds()) || fly.is_some_and(|f| cursor.is_over(f.bounds()));
|
||||
match event {
|
||||
Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Escape), .. }) => {
|
||||
self.close(shell);
|
||||
shell.capture_event();
|
||||
return;
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonPressed(_)) if !over => {
|
||||
self.close(shell);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let viewport = layout.bounds();
|
||||
self.main.as_widget_mut().update(self.main_tree, event, main, cursor, renderer, shell, &viewport);
|
||||
if let (Some(((_, flyout), tree)), Some(fly)) = (self.flyout.as_mut(), fly) {
|
||||
flyout.as_widget_mut().update(tree, event, fly, cursor, renderer, shell, &viewport);
|
||||
}
|
||||
if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) = event {
|
||||
// Only a chosen entry closes the menu: a submenu entry opens its
|
||||
// fly-out, and separators, gaps and padding do nothing.
|
||||
let chosen = match entries(main).zip(self.keep_open.iter()).find(|(e, _)| cursor.is_over(e.bounds())) {
|
||||
Some((_, keep)) => !keep,
|
||||
None => fly.is_some_and(|f| entries(f).any(|e| cursor.is_over(e.bounds()))),
|
||||
};
|
||||
if chosen {
|
||||
self.close(shell);
|
||||
}
|
||||
}
|
||||
if over {
|
||||
shell.capture_event();
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(&self, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer) -> mouse::Interaction {
|
||||
let mut children = layout.children();
|
||||
let main = children.next().map(|m| {
|
||||
self.main.as_widget().mouse_interaction(self.main_tree, m, cursor, &m.bounds(), renderer)
|
||||
});
|
||||
let fly = match (self.flyout.as_ref(), children.next()) {
|
||||
(Some(((_, f), tree)), Some(l)) => Some(f.as_widget().mouse_interaction(tree, l, cursor, &l.bounds(), renderer)),
|
||||
_ => None,
|
||||
};
|
||||
fly.filter(|i| *i != mouse::Interaction::default()).or(main).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user