Files
OneCAD-Draft/src/ui/window/sheet_set.rs
T

969 lines
36 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Sheet Set Manager (SHEETSET): the open sheet sets as a tree of subsets
//! and sheets, the New Sheet Set wizard (NEWSHEETSET), the Sheet Set /
//! Subset / Sheet Properties dialog and the small New Sheet, New Subset,
//! Import Layout and Rename & Renumber forms. The tree is read from the
//! sheet set database (`.dst`) itself on every view.
use crate::app::Message;
use crate::t;
use crate::ui::dock::PanelId;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{button_style, dialog_button, field_style};
use crate::ui::window::pdf_dialogs::{accent_text, card_style};
use codec::sheet_set::{self as ss, ComponentKind, Element as SsElement, SheetSetDatabase};
use iced::widget::{
button, checkbox, column, container, mouse_area, pick_list, row, scrollable, text, text_input, Space,
};
use iced::{Background, Border, Element, Fill, Length, Theme};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
const SET_ICON: &[u8] = include_bytes!("../../../assets/icons/sheetset.svg");
/// SSMAUTOOPEN, SSLOCATE, SSMPOLLTIME and SSMSHEETSTATUS: profile settings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct SheetSetSettings {
pub auto_open: u8,
pub locate: u8,
pub poll_time: u16,
pub sheet_status: u8,
}
impl Default for SheetSetSettings {
fn default() -> Self {
Self { auto_open: 1, locate: 1, poll_time: 60, sheet_status: 2 }
}
}
/// The palette's three pages.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SsmTab {
#[default]
SheetList,
SheetViews,
ModelViews,
}
/// The palette and the open sheet sets.
#[derive(Debug, Default)]
pub struct SheetSetManager {
pub show: bool,
pub settings: SheetSetSettings,
/// Open sheet sets (each read from its `.dst`).
pub sets: Vec<SheetSetDatabase>,
/// The set the palette shows.
pub current: Option<usize>,
pub tab: SsmTab,
/// Folded subset / set ids.
pub collapsed: HashSet<String>,
/// Highlighted component id.
pub selected: Option<String>,
/// SSFOUND: the `.dst` the last opened sheet drawing named, when found.
pub found: String,
/// The open dialog (shown in the `SheetSet` modal).
pub dialog: Option<SsDialog>,
}
impl SheetSetManager {
pub fn db(&self) -> Option<&SheetSetDatabase> {
self.sets.get(self.current?)
}
pub fn db_mut(&mut self) -> Option<&mut SheetSetDatabase> {
self.sets.get_mut(self.current?)
}
}
/// Right-click commands of a tree row.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MenuAction {
Open,
NewSheet,
NewSubset,
ImportLayout,
Rename,
Remove,
Properties,
Close,
}
#[derive(Debug, Clone)]
pub enum SheetSetMsg {
/// The set combo: a set by index, or `None` for "Open...".
PickSet(Option<usize>),
Tab(SsmTab),
Expand(String),
Select(String),
/// Double-click: open the sheet's drawing at its layout.
Activate(String),
Menu(String, MenuAction),
Refresh,
/// A `.dst` picked by Open (or given by automation).
OpenPicked(Option<std::path::PathBuf>),
/// A drawing picked by Import Layout as Sheet.
ImportPicked(Option<std::path::PathBuf>),
/// A folder picked for a dialog field.
FolderPicked(FieldId, Option<std::path::PathBuf>),
/// A template drawing picked for a sheet creation template.
TemplatePicked(Option<std::path::PathBuf>),
Browse(FieldId),
BrowseTemplate,
/// Edit a text field of the open dialog.
Input(FieldId, String),
Toggle(FieldId, bool),
Choose(FieldId, usize),
Wizard(WizardMsg),
/// Open the Properties dialog from the wizard.
WizardProperties,
AddCustom,
AddCustomOk,
AddCustomCancel,
/// A custom property row's value.
CustomValue(usize, String),
Ok,
Cancel,
Help,
}
#[derive(Debug, Clone)]
pub enum WizardMsg {
Step(u8),
Existing(bool),
/// Toggle one drawing layout of the "existing drawings" page.
LayoutOn(usize, bool),
RemoveFolder(usize),
}
/// Text fields, toggles and lists of the dialogs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FieldId {
/// A Properties row by index.
Row(usize),
Name,
Description,
Folder,
Hierarchy,
Number,
Title,
FileName,
/// The layout list of Import Layout as Sheet.
Layout,
/// Add Custom Property: name, default value, owner (0 set, 1 sheet).
CustomName,
CustomDefault,
CustomOwner,
/// A wizard "existing drawings" folder to add.
AddFolder,
/// Prompt for template (Properties).
PromptTemplate,
}
/// What a Properties row edits.
#[derive(Debug, Clone, PartialEq)]
pub enum RowKey {
/// An `AcSmProp` of the component.
Prop(&'static str),
/// A file reference property (`NewSheetLocation`).
Folder(&'static str),
/// The sheet creation template (`DefDwtLayout`): the value shows
/// `Layout (file)`; `template` holds the picked drawing and layout.
Template,
/// Yes / No: prompt for template.
PromptTemplate,
/// Shown only.
ReadOnly,
}
#[derive(Debug, Clone)]
pub struct PropRow {
pub group: &'static str,
pub label: &'static str,
pub key: RowKey,
pub value: String,
}
/// The Sheet Set / Subset / Sheet Properties dialog.
#[derive(Debug, Clone)]
pub struct Properties {
/// `None` edits the wizard's draft set.
pub set: Option<usize>,
pub component: String,
pub kind: ComponentKind,
pub title: String,
pub rows: Vec<PropRow>,
/// Name, value, flags.
pub custom: Vec<(String, String, i32)>,
/// Add Custom Property being filled: name, default value, owner is sheet.
pub adding: Option<(String, String, bool)>,
/// The template picked in this dialog: drawing and layout.
pub template: Option<(String, String)>,
/// Back to the wizard on OK / Cancel.
pub wizard: Option<Box<Wizard>>,
pub error: Option<String>,
}
/// One layout found by the "existing drawings" wizard page.
#[derive(Debug, Clone)]
pub struct FoundLayout {
pub folder: String,
pub file: String,
pub layout: String,
pub handle: String,
pub on: bool,
}
/// NEWSHEETSET.
#[derive(Debug, Clone)]
pub struct Wizard {
/// 0 Begin, 1 Sheet Set Details, 2 Choose Layouts (existing drawings), 3 Confirm.
pub step: u8,
pub existing: bool,
pub name: String,
pub description: String,
pub folder: String,
pub hierarchy: bool,
pub folders: Vec<String>,
pub layouts: Vec<FoundLayout>,
/// The set being built: name, description, properties and custom
/// properties go into it; Finish writes it.
pub draft: SheetSetDatabase,
pub error: Option<String>,
}
/// New Sheet, New Subset, Rename & Renumber and Import Layout as Sheet.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormKind {
NewSheet,
NewSubset,
Rename,
ImportLayout,
}
#[derive(Debug, Clone)]
pub struct Form {
pub kind: FormKind,
pub set: usize,
/// Parent (new sheet / subset / import) or the sheet / subset renamed.
pub component: String,
pub number: String,
pub title: String,
pub file_name: String,
pub folder: String,
/// Import: the drawing and its layouts (name, handle).
pub drawing: String,
pub layouts: Vec<(String, String)>,
pub layout: usize,
pub error: Option<String>,
}
#[derive(Debug, Clone)]
pub enum SsDialog {
Wizard(Wizard),
Properties(Properties),
Form(Form),
}
impl SsDialog {
pub fn title(&self) -> String {
match self {
SsDialog::Wizard(_) => t!("Create Sheet Set").into_owned(),
SsDialog::Properties(p) => p.title.clone(),
SsDialog::Form(f) => match f.kind {
FormKind::NewSheet => t!("New Sheet").into_owned(),
FormKind::NewSubset => t!("Subset Properties").into_owned(),
FormKind::Rename => t!("Rename & Renumber Sheet").into_owned(),
FormKind::ImportLayout => t!("Import Layout as Sheet").into_owned(),
},
}
}
}
fn msg(m: SheetSetMsg) -> Message {
Message::SheetSet(m)
}
// ── palette ──────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, PartialEq, Eq)]
struct Pick(Option<usize>, String);
impl std::fmt::Display for Pick {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.1)
}
}
fn menu_entry(label: String, m: Option<Message>) -> Element<'static, Message> {
button(text(label).size(12))
.width(Fill)
.padding([5, 12])
.on_press_maybe(m)
.style(|theme: &Theme, status| {
let mut s = button::text(theme, status);
if matches!(status, button::Status::Hovered) {
s.background = Some(Background::Color(theme.palette().primary.base.color));
s.text_color = theme.palette().primary.base.text;
}
s
})
.into()
}
fn menu_separator() -> Element<'static, Message> {
container(Space::new())
.height(1)
.width(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.neutral.color)),
..Default::default()
})
.into()
}
/// A row's right-click menu.
fn row_menu(id: String, kind: ComponentKind) -> Element<'static, Message> {
let entry = |label: &str, action: MenuAction, on: bool| {
menu_entry(t!(label).into_owned(), on.then(|| msg(SheetSetMsg::Menu(id.clone(), action))))
};
let sheet = kind == ComponentKind::Sheet;
let mut items = vec![
entry("Open", MenuAction::Open, sheet),
menu_separator(),
entry("New Sheet...", MenuAction::NewSheet, !sheet),
entry("New Subset...", MenuAction::NewSubset, !sheet),
entry("Import Layout as Sheet...", MenuAction::ImportLayout, !sheet),
menu_separator(),
entry("Rename & Renumber...", MenuAction::Rename, sheet),
entry(if kind == ComponentKind::Subset { "Remove Subset" } else { "Remove Sheet" }, MenuAction::Remove, kind != ComponentKind::SheetSet),
];
if kind == ComponentKind::SheetSet {
items.push(entry("Close Sheet Set", MenuAction::Close, true));
}
items.push(menu_separator());
items.push(entry("Properties...", MenuAction::Properties, true));
container(iced::widget::Column::with_children(items).spacing(1))
.padding(4)
.width(Length::Fixed(220.0))
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
})
.into()
}
fn tree_row<'a>(
state: &SheetSetManager,
el: &SsElement,
depth: u16,
icon: &'static [u8],
label: String,
fold: Option<bool>,
) -> Element<'a, Message> {
let id = el.id().to_string();
let kind = ComponentKind::of(el).unwrap_or(ComponentKind::Sheet);
let arrow: Element<'a, Message> = match fold {
Some(open) => button(if open {
crate::ui::icons::themed_arrow_down(10.0)
} else {
crate::ui::icons::themed_arrow_right(10.0)
})
.on_press(msg(SheetSetMsg::Expand(id.clone())))
.style(button::text)
.padding(2)
.into(),
None => Space::new().width(14).into(),
};
let selected = state.selected.as_deref() == Some(id.as_str());
let cells = row![
Space::new().width(Length::Fixed(f32::from(depth) * 16.0)),
arrow,
crate::ui::icons::semantic(icon, 14.0),
text(label).size(12).width(Fill),
]
.spacing(5)
.align_y(iced::Center);
let area = mouse_area(container(cells).width(Fill).padding([3, 6]).style(move |theme: &Theme| container::Style {
background: selected.then(|| Background::Color(theme.palette().primary.weak.color)),
text_color: selected.then(|| theme.palette().primary.weak.text),
..Default::default()
}))
.on_press(msg(SheetSetMsg::Select(id.clone())))
.on_double_click(msg(SheetSetMsg::Activate(id.clone())));
iced_aw::ContextMenu::new(area, move || row_menu(id.clone(), kind)).into()
}
fn push_tree<'a>(
state: &SheetSetManager,
el: &SsElement,
depth: u16,
out: &mut Vec<Element<'a, Message>>,
) {
for c in &el.children {
match ComponentKind::of(c) {
Some(ComponentKind::Subset) => {
let open = !state.collapsed.contains(c.id());
out.push(tree_row(state, c, depth, crate::ui::icons::FOLDER_OPEN, c.prop("Name").unwrap_or("").to_string(), Some(open)));
if open {
push_tree(state, c, depth + 1, out);
}
}
Some(ComponentKind::Sheet) => {
out.push(tree_row(state, c, depth, crate::ui::icons::DOC, ss::number_and_title(c), None));
}
_ => {}
}
}
}
/// Sheet views of the set: each sheet's `AcSmSheetView` children.
fn sheet_views(db: &SheetSetDatabase) -> Vec<String> {
db.sheets()
.into_iter()
.flat_map(|sheet| {
sheet
.named("SheetViews")
.map(|v| v.children.iter().filter(|c| c.name == "AcSmSheetView").collect::<Vec<_>>())
.unwrap_or_default()
.into_iter()
.map(|v| {
let number = v.prop("Number").unwrap_or("");
let title = v.prop("Title").unwrap_or("");
if number.is_empty() { title.to_string() } else { format!("{number} - {title}") }
})
})
.collect()
}
/// Model view locations (`Resources`) of the set.
fn model_views(db: &SheetSetDatabase) -> Vec<String> {
db.sheet_set()
.named("Resources")
.map(|r| r.children.iter().map(|c| db.resolve_file(c)).collect())
.unwrap_or_default()
}
fn heading(label: &str) -> Element<'static, Message> {
text(t!(label).into_owned())
.size(11)
.font(iced::Font { weight: iced::font::Weight::Bold, ..iced::Font::DEFAULT })
.style(|theme: &Theme| text::Style { color: Some(theme.palette().primary.base.color) })
.width(Fill)
.into()
}
pub fn view<'a>(state: &'a SheetSetManager, width: f32, auto_collapse: bool) -> Element<'a, Message> {
let title_bar =
crate::ui::dock::title_bar(PanelId::SheetSetManager, t!("Sheet Set Manager").into_owned(), auto_collapse);
let mut picks: Vec<Pick> =
state.sets.iter().enumerate().map(|(i, db)| Pick(Some(i), db.name().to_string())).collect();
picks.push(Pick(None, t!("Open...").into_owned()));
let current = state.current.and_then(|i| picks.get(i).cloned());
let combo = pick_list(current, picks, |p: &Pick| p.1.clone())
.placeholder(t!("Open...").into_owned())
.on_select(|p: Pick| msg(SheetSetMsg::PickSet(p.0)))
.text_size(12)
.padding([5, 8])
.width(Fill);
let tab = |label: &str, t: SsmTab| {
button(text(t!(label).into_owned()).size(11).center().width(Fill))
.on_press(msg(SheetSetMsg::Tab(t)))
.style(button_style(state.tab == t))
.padding([5, 4])
.width(Fill)
};
let tabs = row![
tab("Sheet List", SsmTab::SheetList),
tab("Sheet Views", SsmTab::SheetViews),
tab("Model Views", SsmTab::ModelViews),
]
.spacing(2);
let body: Element<'a, Message> = match (state.db(), state.tab) {
(None, _) => text(t!("Open a sheet set or create one with NEWSHEETSET.").into_owned())
.size(12)
.style(muted_style)
.into(),
(Some(db), SsmTab::SheetList) => {
let set = db.sheet_set();
let open = !state.collapsed.contains(set.id());
let mut rows = vec![tree_row(state, set, 0, SET_ICON, db.name().to_string(), Some(open))];
if open {
push_tree(state, set, 1, &mut rows);
}
let header = row![
heading("Sheets"),
button(text("↻").size(13))
.on_press(msg(SheetSetMsg::Refresh))
.style(button::subtle)
.padding([2, 4]),
]
.align_y(iced::Center);
column![header, scrollable(iced::widget::Column::with_children(rows).spacing(1)).height(Fill)]
.spacing(6)
.into()
}
(Some(db), SsmTab::SheetViews) => {
let views = sheet_views(db);
let list: Element<'a, Message> = if views.is_empty() {
text(t!("No sheet views.").into_owned()).size(12).style(muted_style).into()
} else {
iced::widget::Column::with_children(views.into_iter().map(|v| text(v).size(12).into())).spacing(3).into()
};
column![heading("Views by sheet"), list].spacing(6).into()
}
(Some(db), SsmTab::ModelViews) => {
let locations = model_views(db);
let list: Element<'a, Message> = if locations.is_empty() {
text(t!("No locations.").into_owned()).size(12).style(muted_style).into()
} else {
iced::widget::Column::with_children(locations.into_iter().map(|v| text(v).size(12).into()))
.spacing(3)
.into()
};
column![heading("Locations"), list].spacing(6).into()
}
};
let card = container(body).padding(6).width(Fill).height(Fill).style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().background.weak.color)),
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
});
let hint = text(t!("Double-click opens a sheet. Right-click for more commands.").into_owned())
.size(10)
.style(muted_style);
crate::ui::dock::frame(column![title_bar, combo, tabs, card, hint].spacing(6), width)
}
// ── dialogs ──────────────────────────────────────────────────────────────────
fn card<'a>(title: &str, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
container(column![text(t!(title).to_uppercase()).size(10).style(accent_text), content.into()].spacing(8))
.padding([10, 12])
.width(Fill)
.style(card_style)
.into()
}
fn input<'a>(value: &str, field: FieldId) -> Element<'a, Message> {
text_input("", value)
.size(12)
.padding([5, 8])
.style(field_style)
.on_input(move |v| msg(SheetSetMsg::Input(field, v)))
.into()
}
fn labeled<'a>(label: &str, control: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
row![text(t!(label).into_owned()).size(12).width(Length::Fixed(170.0)), control.into()]
.spacing(8)
.align_y(iced::Center)
.into()
}
fn browse<'a>(m: SheetSetMsg) -> Element<'a, Message> {
button(text("...").size(12)).on_press(msg(m)).style(button_style(false)).padding([5, 10]).into()
}
fn error_band<'a>(error: &Option<String>) -> Option<Element<'a, Message>> {
error.as_ref().map(|e| {
container(text(e.clone()).size(12))
.padding([6, 10])
.width(Fill)
.style(|theme: &Theme| container::Style {
background: Some(Background::Color(theme.palette().danger.weak.color)),
text_color: Some(theme.palette().danger.weak.text),
border: Border { radius: 4.0.into(), ..Default::default() },
..Default::default()
})
.into()
})
}
fn footer<'a>(left: Vec<Element<'a, Message>>, right: Vec<Element<'a, Message>>) -> Element<'a, Message> {
let mut r = row![button(text("?").size(12)).on_press(msg(SheetSetMsg::Help)).style(button_style(false)).padding([5, 11])]
.spacing(6)
.align_y(iced::Center);
for e in left {
r = r.push(e);
}
r = r.push(Space::new().width(Fill));
for e in right {
r = r.push(e);
}
r.into()
}
fn wizard_view<'a>(w: &'a Wizard) -> Element<'a, Message> {
let steps: Vec<(u8, &str)> = if w.existing {
vec![(0, "Begin"), (1, "Details"), (2, "Choose Layouts"), (3, "Confirm")]
} else {
vec![(0, "Begin"), (1, "Details"), (3, "Confirm")]
};
let chips = iced::widget::Row::with_children(steps.iter().enumerate().map(|(n, (s, label))| {
button(text(format!("{} {}", n + 1, t!(*label))).size(12))
.on_press(msg(SheetSetMsg::Wizard(WizardMsg::Step(*s))))
.style(button_style(w.step == *s))
.padding([4, 10])
.into()
}))
.spacing(6);
let page: Element<'a, Message> = match w.step {
0 => card(
"Create a sheet set using",
column![
checkbox(!w.existing)
.label(t!("An empty sheet set").into_owned())
.text_size(12)
.size(14)
.on_toggle(|_| msg(SheetSetMsg::Wizard(WizardMsg::Existing(false)))),
checkbox(w.existing)
.label(t!("Existing drawings").into_owned())
.text_size(12)
.size(14)
.on_toggle(|_| msg(SheetSetMsg::Wizard(WizardMsg::Existing(true)))),
text(
t!(if w.existing {
"Lets you specify one or more folders containing drawings. The layouts from these drawings can be automatically imported into the sheet set."
} else {
"Creates a sheet set with no subsets or sheets."
})
.into_owned()
)
.size(11)
.style(muted_style),
]
.spacing(8),
),
1 => card(
"Sheet Set",
column![
labeled("Name of new sheet set:", input(&w.name, FieldId::Name)),
labeled("Description (optional):", input(&w.description, FieldId::Description)),
labeled(
"Store sheet set data file (.dst) here:",
row![input(&w.folder, FieldId::Folder), browse(SheetSetMsg::Browse(FieldId::Folder))].spacing(6)
),
checkbox(w.hierarchy)
.label(t!("Create a folder hierarchy based on subsets").into_owned())
.text_size(12)
.size(14)
.on_toggle(|v| msg(SheetSetMsg::Toggle(FieldId::Hierarchy, v))),
row![
Space::new().width(Fill),
button(text(t!("Sheet Set Properties...").into_owned()).size(12))
.on_press(msg(SheetSetMsg::WizardProperties))
.style(button_style(false))
.padding([5, 12]),
],
]
.spacing(8),
),
2 => {
let rows = w.layouts.iter().enumerate().map(|(i, l)| {
checkbox(l.on)
.label(format!("{} — {}", l.file, l.layout))
.text_size(12)
.size(14)
.on_toggle(move |v| msg(SheetSetMsg::Wizard(WizardMsg::LayoutOn(i, v))))
.into()
});
let folders = w.folders.iter().enumerate().map(|(i, f)| {
row![
text(f.clone()).size(12).width(Fill),
button(text("×").size(12))
.on_press(msg(SheetSetMsg::Wizard(WizardMsg::RemoveFolder(i))))
.style(button::text)
.padding([0, 6]),
]
.into()
});
card(
"Choose Layouts",
column![
row![
text(t!("Select folders containing drawings. Layouts in the drawings can be added to the sheet set.").into_owned())
.size(11)
.style(muted_style)
.width(Fill),
button(text(t!("Browse...").into_owned()).size(12))
.on_press(msg(SheetSetMsg::Browse(FieldId::AddFolder)))
.style(button_style(false))
.padding([5, 12]),
]
.spacing(8)
.align_y(iced::Center),
iced::widget::Column::with_children(folders.collect::<Vec<_>>()).spacing(2),
container(scrollable(iced::widget::Column::with_children(rows.collect::<Vec<_>>()).spacing(3)))
.height(Length::Fixed(180.0)),
]
.spacing(8),
)
}
_ => {
let mut lines = vec![
format!("{}: {}", t!("Sheet Set"), w.name),
format!("{}: {}", t!("Sheet set data file"), wizard_dst_path(w)),
format!("{}: {}", t!("Description"), w.description),
];
if w.existing {
let n = w.layouts.iter().filter(|l| l.on).count();
lines.push(format!("{}: {n}", t!("Sheets to import")));
}
for (name, value, _) in ss::custom_properties(w.draft.sheet_set()) {
lines.push(format!("{name}: {value}"));
}
card(
"Confirm",
iced::widget::Column::with_children(lines.into_iter().map(|l| text(l).size(12).into())).spacing(4),
)
}
};
let next_step = match (w.step, w.existing) {
(0, _) => Some(1),
(1, true) => Some(2),
(1, false) | (2, _) => Some(3),
_ => None,
};
let back_step = match (w.step, w.existing) {
(3, true) => Some(2),
(3, false) | (2, _) => Some(1),
(1, _) => Some(0),
_ => None,
};
let back = button(text(format!("‹ {}", t!("Back"))).size(12))
.on_press_maybe(back_step.map(|s| msg(SheetSetMsg::Wizard(WizardMsg::Step(s)))))
.style(button_style(false))
.padding([6, 14]);
let next: Element<'a, Message> = match next_step {
Some(s) => button(text(format!("{} ›", t!("Next"))).size(12))
.on_press(msg(SheetSetMsg::Wizard(WizardMsg::Step(s))))
.style(button_style(true))
.padding([6, 14])
.into(),
None => dialog_button(t!("Finish"), msg(SheetSetMsg::Ok), true).into(),
};
let mut col = column![chips, page].spacing(10);
if let Some(e) = error_band(&w.error) {
col = col.push(e);
}
col.push(footer(vec![], vec![back.into(), next, dialog_button(t!("Cancel"), msg(SheetSetMsg::Cancel), false).into()]))
.into()
}
/// Where the wizard writes the `.dst`.
pub fn wizard_dst_path(w: &Wizard) -> String {
std::path::Path::new(&w.folder).join(format!("{}.dst", w.name.trim())).to_string_lossy().to_string()
}
fn properties_view<'a>(p: &'a Properties) -> Element<'a, Message> {
let mut groups: Vec<Element<'a, Message>> = Vec::new();
let mut current: Option<&'static str> = None;
let mut rows: Vec<Element<'a, Message>> = Vec::new();
let flush = |group: Option<&'static str>, rows: &mut Vec<Element<'a, Message>>, groups: &mut Vec<Element<'a, Message>>| {
if let Some(g) = group {
groups.push(card(g, iced::widget::Column::with_children(std::mem::take(rows)).spacing(6)));
}
};
for (i, r) in p.rows.iter().enumerate() {
if current != Some(r.group) {
flush(current, &mut rows, &mut groups);
current = Some(r.group);
}
let control: Element<'a, Message> = match r.key {
RowKey::ReadOnly => container(text(r.value.clone()).size(12).style(muted_style))
.padding([5, 8])
.width(Fill)
.into(),
RowKey::Folder(_) => row![input(&r.value, FieldId::Row(i)), browse(SheetSetMsg::Browse(FieldId::Row(i)))]
.spacing(6)
.into(),
RowKey::Template => row![
container(text(r.value.clone()).size(12)).padding([5, 8]).width(Fill),
browse(SheetSetMsg::BrowseTemplate)
]
.spacing(6)
.align_y(iced::Center)
.into(),
RowKey::PromptTemplate => {
let yes = r.value == "Yes";
let opts = vec![t!("Yes").into_owned(), t!("No").into_owned()];
pick_list(Some(opts[if yes { 0 } else { 1 }].clone()), opts.clone(), |s: &String| s.clone())
.on_select(move |s: String| msg(SheetSetMsg::Choose(FieldId::Row(i), usize::from(s != t!("Yes")))))
.text_size(12)
.padding([5, 8])
.width(Fill)
.into()
}
RowKey::Prop(_) => input(&r.value, FieldId::Row(i)),
};
rows.push(labeled(r.label, control));
}
flush(current, &mut rows, &mut groups);
if p.kind != ComponentKind::Subset {
let owner = |f: i32| if f & ss::CUSTOM_SHEET_PROP != 0 { t!("Sheet") } else { t!("Sheet Set") };
let mut custom: Vec<Element<'a, Message>> = p
.custom
.iter()
.enumerate()
.map(|(i, (name, value, flags))| {
let label = if p.kind == ComponentKind::SheetSet { format!("{name} ({})", owner(*flags)) } else { name.clone() };
row![
text(label).size(12).width(Length::Fixed(170.0)),
text_input("", value)
.size(12)
.padding([5, 8])
.style(field_style)
.on_input(move |v| msg(SheetSetMsg::CustomValue(i, v))),
]
.spacing(8)
.align_y(iced::Center)
.into()
})
.collect();
if p.kind == ComponentKind::SheetSet {
match &p.adding {
Some((name, value, sheet)) => custom.push(
container(
column![
labeled("Name:", input(name, FieldId::CustomName)),
labeled("Default value:", input(value, FieldId::CustomDefault)),
labeled(
"Owner",
row![
checkbox(!*sheet)
.label(t!("Sheet Set").into_owned())
.text_size(12)
.size(14)
.on_toggle(|_| msg(SheetSetMsg::Choose(FieldId::CustomOwner, 0))),
checkbox(*sheet)
.label(t!("Sheet").into_owned())
.text_size(12)
.size(14)
.on_toggle(|_| msg(SheetSetMsg::Choose(FieldId::CustomOwner, 1))),
]
.spacing(12)
),
row![
Space::new().width(Fill),
dialog_button(t!("Cancel"), msg(SheetSetMsg::AddCustomCancel), false),
dialog_button(t!("OK"), msg(SheetSetMsg::AddCustomOk), true),
]
.spacing(6),
]
.spacing(6),
)
.padding(8)
.style(|theme: &Theme| container::Style {
border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 4.0.into() },
..Default::default()
})
.into(),
),
None => custom.push(
row![
Space::new().width(Fill),
button(text(t!("Add Custom Property...").into_owned()).size(12))
.on_press(msg(SheetSetMsg::AddCustom))
.style(button_style(false))
.padding([5, 12]),
]
.into(),
),
}
}
groups.push(card("Custom Properties", iced::widget::Column::with_children(custom).spacing(6)));
}
let mut col = column![scrollable(iced::widget::Column::with_children(groups).spacing(10)).height(Fill)].spacing(10);
if let Some(e) = error_band(&p.error) {
col = col.push(e);
}
col.push(footer(
vec![],
vec![
dialog_button(t!("Cancel"), msg(SheetSetMsg::Cancel), false).into(),
dialog_button(t!("OK"), msg(SheetSetMsg::Ok), true).into(),
],
))
.into()
}
fn form_view<'a>(f: &'a Form) -> Element<'a, Message> {
let body: Element<'a, Message> = match f.kind {
FormKind::NewSheet => column![
labeled("Number", input(&f.number, FieldId::Number)),
labeled("Sheet title", input(&f.title, FieldId::Title)),
labeled("File name", input(&f.file_name, FieldId::FileName)),
labeled("Folder path", container(text(f.folder.clone()).size(12).style(muted_style)).padding([5, 8])),
labeled("Sheet template", container(text(f.drawing.clone()).size(12).style(muted_style)).padding([5, 8])),
]
.spacing(8)
.into(),
FormKind::NewSubset => column![
labeled("Subset name", input(&f.title, FieldId::Title)),
labeled(
"New sheet location",
row![input(&f.folder, FieldId::Folder), browse(SheetSetMsg::Browse(FieldId::Folder))].spacing(6)
),
]
.spacing(8)
.into(),
FormKind::Rename => column![
labeled("Number", input(&f.number, FieldId::Number)),
labeled("Sheet title", input(&f.title, FieldId::Title)),
]
.spacing(8)
.into(),
FormKind::ImportLayout => {
let rows = f.layouts.iter().enumerate().map(|(i, (name, _))| {
button(text(name.clone()).size(12))
.on_press(msg(SheetSetMsg::Choose(FieldId::Layout, i)))
.style(button_style(f.layout == i))
.padding([3, 8])
.width(Fill)
.into()
});
column![
labeled("Drawing", container(text(f.drawing.clone()).size(12).style(muted_style)).padding([5, 8])),
text(t!("Layouts").into_owned()).size(12),
container(scrollable(iced::widget::Column::with_children(rows.collect::<Vec<_>>()).spacing(1)))
.height(Length::Fixed(160.0)),
]
.spacing(8)
.into()
}
};
let mut col = column![card(
match f.kind {
FormKind::NewSheet => "Sheet",
FormKind::NewSubset => "Subset",
FormKind::Rename => "Sheet",
FormKind::ImportLayout => "Import Layout as Sheet",
},
body
)]
.spacing(10);
if let Some(e) = error_band(&f.error) {
col = col.push(e);
}
col.push(footer(
vec![],
vec![
dialog_button(t!("Cancel"), msg(SheetSetMsg::Cancel), false).into(),
dialog_button(t!("OK"), msg(SheetSetMsg::Ok), true).into(),
],
))
.into()
}
pub fn dialog_view<'a>(state: &'a SheetSetManager, sizing: crate::ui::modal::ModalSizing) -> Element<'a, Message> {
let body = match state.dialog.as_ref() {
Some(SsDialog::Wizard(w)) => wizard_view(w),
Some(SsDialog::Properties(p)) => properties_view(p),
Some(SsDialog::Form(f)) => form_view(f),
None => Space::new().into(),
};
container(body).padding([10, 12]).width(sizing.width).into()
}