forked from 1b-agent/opencadstudio-upstream
- SSM* / SS* sysvar messages and the Place on Sheet prompt go through t!/tf!, reusing the existing catalog entries where they exist; one new key (integer range) is translated in all 21 locales. - The SETVAR prefix is matched case-insensitively. - The stale "SHEETSET ... not yet implemented" key is removed from the catalog and every locale. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2746 lines
120 KiB
Rust
2746 lines
120 KiB
Rust
//! Sheet sets: SHEETSET / SHEETSETHIDE / NEWSHEETSET / OPENSHEETSET, the
|
||
//! SSM* / SS* system variables, the Sheet Set Manager palette and its
|
||
//! dialogs, the drawing's sheet link written on save and located on open.
|
||
|
||
use crate::app::{Message, OpenCADStudio};
|
||
use crate::ui::window::sheet_set::{
|
||
BlockList, Category, FieldId, Form, FormKind, FoundLayout, ImportRead, ImportRow, MenuAction, Placing, PropRow,
|
||
Properties, RowKey, SelectBlock, SheetSetMsg, SheetStatus, SsDialog, TemplatePick, ViewAction, Wizard, WizardMsg,
|
||
};
|
||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||
use codec::entities::Entity as _;
|
||
use crate::scene::model::wire_model::WireModel;
|
||
use glam::DVec3;
|
||
use codec::sheet_set::{self as ss, ComponentKind, LayoutReference, SheetSetData, SheetSetDatabase};
|
||
use iced::Task;
|
||
use std::path::{Path, PathBuf};
|
||
|
||
/// The variables this family answers for.
|
||
pub(super) const SHEET_SET_SYSVARS: &[&str] =
|
||
&["SSMAUTOOPEN", "SSLOCATE", "SSMPOLLTIME", "SSMSHEETSTATUS", "SSMSTATE", "SSFOUND"];
|
||
|
||
/// Paper-space layouts of a drawing in tab order: name and handle (hex).
|
||
fn paper_layouts(doc: &codec::CadDocument) -> Vec<(String, String)> {
|
||
let mut layouts: Vec<(i16, String, String)> = doc
|
||
.objects
|
||
.values()
|
||
.filter_map(|o| match o {
|
||
codec::objects::ObjectType::Layout(l) if !l.name.eq_ignore_ascii_case("Model") => {
|
||
Some((l.tab_order, l.name.clone(), format!("{:X}", l.handle.value())))
|
||
}
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
layouts.sort();
|
||
layouts.into_iter().map(|(_, n, h)| (n, h)).collect()
|
||
}
|
||
|
||
/// UTC now as the reference stamps `UpdateTime`: `yyyy/MM/dd HH:mm:ss.fff`.
|
||
fn utc_stamp() -> String {
|
||
let ms = iced::time::SystemTime::now()
|
||
.duration_since(iced::time::SystemTime::UNIX_EPOCH)
|
||
.map(|d| d.as_millis() as i64)
|
||
.unwrap_or(0);
|
||
let (days, rem) = (ms.div_euclid(86_400_000), ms.rem_euclid(86_400_000));
|
||
// Civil date from days since 1970-01-01.
|
||
let z = days + 719_468;
|
||
let era = z.div_euclid(146_097);
|
||
let doe = z - era * 146_097;
|
||
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||
let mp = (5 * doy + 2) / 153;
|
||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||
let y = yoe + era * 400 + i64::from(m <= 2);
|
||
format!(
|
||
"{y:04}/{m:02}/{d:02} {:02}:{:02}:{:02}.{:03}",
|
||
rem / 3_600_000,
|
||
rem / 60_000 % 60,
|
||
rem / 1000 % 60,
|
||
rem % 1000
|
||
)
|
||
}
|
||
|
||
fn folder_of(path: &str) -> String {
|
||
Path::new(path).parent().map(|p| p.to_string_lossy().to_string()).unwrap_or_default()
|
||
}
|
||
|
||
/// `.dst` access: native only (a browser has no file paths to read or write).
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
pub(crate) fn read_set(path: &str) -> Result<SheetSetDatabase, String> {
|
||
SheetSetDatabase::read(path)
|
||
}
|
||
|
||
#[cfg(target_arch = "wasm32")]
|
||
pub(crate) fn read_set(_path: &str) -> Result<SheetSetDatabase, String> {
|
||
Err(crate::t!("Not available").into_owned())
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn write_set(db: &mut SheetSetDatabase, path: &str) -> Result<(), String> {
|
||
db.write(path)
|
||
}
|
||
|
||
#[cfg(target_arch = "wasm32")]
|
||
fn write_set(_db: &mut SheetSetDatabase, _path: &str) -> Result<(), String> {
|
||
Err(crate::t!("Not available").into_owned())
|
||
}
|
||
|
||
/// The user and machine names a lock file carries.
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn lock_owner() -> (String, String) {
|
||
let user = std::env::var("USERNAME").or_else(|_| std::env::var("USER")).unwrap_or_default();
|
||
let machine = std::env::var("COMPUTERNAME")
|
||
.or_else(|_| std::env::var("HOSTNAME"))
|
||
.ok()
|
||
.or_else(|| std::fs::read_to_string("/etc/hostname").ok().map(|h| h.trim().to_string()))
|
||
.unwrap_or_default();
|
||
(user, machine)
|
||
}
|
||
|
||
/// The drawings whose lock files this application wrote, with the writing
|
||
/// process id, kept in the config folder so a crashed session's locks can
|
||
/// be told from someone else's and taken back.
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn lock_record() -> Option<PathBuf> {
|
||
Some(crate::config::config_dir()?.join("sheet_set_locks.txt"))
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn recorded_locks() -> Vec<(u32, PathBuf)> {
|
||
let text = lock_record().and_then(|p| std::fs::read_to_string(p).ok()).unwrap_or_default();
|
||
text.lines()
|
||
.filter_map(|l| {
|
||
let (pid, path) = l.split_once('\t')?;
|
||
Some((pid.parse().ok()?, PathBuf::from(path)))
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn record_lock(path: &Path, held: bool) {
|
||
let Some(record) = lock_record() else { return };
|
||
let mut locks = recorded_locks();
|
||
locks.retain(|(_, p)| p != path);
|
||
if held {
|
||
locks.push((std::process::id(), path.to_path_buf()));
|
||
}
|
||
let text: String = locks.iter().map(|(pid, p)| format!("{pid}\t{}\n", p.display())).collect();
|
||
if let Some(dir) = record.parent() {
|
||
let _ = std::fs::create_dir_all(dir);
|
||
}
|
||
let _ = std::fs::write(record, text);
|
||
}
|
||
|
||
/// Whether the `.dwl` next to `path` is one an earlier session of this
|
||
/// application left behind (it crashed): recorded by another process, and
|
||
/// still naming this user and machine.
|
||
// ponytail: the recorded process is assumed gone (the app is single
|
||
// instance); check it is alive if several instances ever share a profile.
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn stale_own_lock(path: &Path) -> bool {
|
||
let me = std::process::id();
|
||
if !recorded_locks().iter().any(|(pid, p)| p == path && *pid != me) {
|
||
return false;
|
||
}
|
||
let text = std::fs::read_to_string(path.with_extension("dwl")).unwrap_or_default();
|
||
let mut lines = text.lines().map(str::trim);
|
||
let (user, machine) = lock_owner();
|
||
lines.next() == Some(user.as_str()) && lines.next() == Some(machine.as_str())
|
||
}
|
||
|
||
/// `<drawing>.dwl` (user, machine, date lines) and `<drawing>.dwl2` (the same
|
||
/// as XML), as the reference writes them for a drawing it has open. The date
|
||
/// is the OS long date, two spaces and the 24-hour time.
|
||
// ponytail: .dwl is written as UTF-8; the reference's encoding of non-ASCII
|
||
// day names was not measured.
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn write_lock(path: &Path) -> std::io::Result<()> {
|
||
let (user, machine) = lock_owner();
|
||
let stamp = crate::entities::field::long_date_time_now();
|
||
std::fs::write(path.with_extension("dwl"), format!("{user}\n{machine} \n{stamp}"))?;
|
||
std::fs::write(
|
||
path.with_extension("dwl2"),
|
||
format!(
|
||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<whprops>\n<username>{user}</username>\n\
|
||
<machinename>{machine} </machinename>\n<fullname></fullname>\n<datetime>{stamp}</datetime>\n</whprops>"
|
||
),
|
||
)?;
|
||
record_lock(path, true);
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
pub(in crate::app) fn release_lock(path: &Path) {
|
||
let _ = std::fs::remove_file(path.with_extension("dwl"));
|
||
let _ = std::fs::remove_file(path.with_extension("dwl2"));
|
||
record_lock(path, false);
|
||
}
|
||
|
||
/// The insertion point of Place on Sheet (with the scale, picked from the
|
||
/// right-click list) or of a callout / view label block.
|
||
struct SheetSetPlaceCommand {
|
||
/// The view's size in model units (zero for a block).
|
||
size: (f64, f64),
|
||
scale: f64,
|
||
scales: Vec<(String, f64)>,
|
||
}
|
||
|
||
impl CadCommand for SheetSetPlaceCommand {
|
||
fn name(&self) -> &'static str {
|
||
"SSMPLACE"
|
||
}
|
||
|
||
fn prompt(&self) -> String {
|
||
crate::t!("Specify insertion point:").into_owned()
|
||
}
|
||
|
||
fn options(&self) -> Vec<CmdOption> {
|
||
self.scales.iter().map(|(name, _)| CmdOption::new(name, name)).collect()
|
||
}
|
||
|
||
fn wants_text_input(&self) -> bool {
|
||
!self.scales.is_empty()
|
||
}
|
||
|
||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||
let (_, scale) = self.scales.iter().find(|(name, _)| name.eq_ignore_ascii_case(text.trim()))?;
|
||
self.scale = *scale;
|
||
Some(CmdResult::NeedPoint)
|
||
}
|
||
|
||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||
CmdResult::Dispatch(format!("_SSMPLACEPOINT {} {} {}", pt.x, pt.y, self.scale))
|
||
}
|
||
|
||
fn on_enter(&mut self) -> CmdResult {
|
||
CmdResult::Cancel
|
||
}
|
||
|
||
fn on_escape(&mut self) -> CmdResult {
|
||
CmdResult::Cancel
|
||
}
|
||
|
||
/// The viewport's outline follows the cursor at its lower-left corner.
|
||
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
|
||
let (w, h) = (self.size.0 * self.scale, self.size.1 * self.scale);
|
||
if w <= 0.0 || h <= 0.0 {
|
||
return Vec::new();
|
||
}
|
||
let p = |x: f64, y: f64| [pt.x + x, pt.y + y, pt.z];
|
||
vec![WireModel::solid_f64(
|
||
"ssm_place".to_string(),
|
||
vec![p(0.0, 0.0), p(w, 0.0), p(w, h), p(0.0, h), p(0.0, 0.0)],
|
||
WireModel::CYAN,
|
||
false,
|
||
)]
|
||
}
|
||
}
|
||
|
||
/// A model view to place: centre (world), width, height, target, direction.
|
||
struct ModelView {
|
||
center: codec::types::Vector3,
|
||
width: f64,
|
||
height: f64,
|
||
target: codec::types::Vector3,
|
||
direction: codec::types::Vector3,
|
||
}
|
||
|
||
/// Named view `view` of a model drawing, or its extents.
|
||
fn model_view(doc: &codec::CadDocument, view: Option<&str>) -> Option<ModelView> {
|
||
use codec::types::Vector3;
|
||
if let Some(name) = view {
|
||
let v = doc.views.iter().find(|v| !v.paper_space && v.name.eq_ignore_ascii_case(name))?;
|
||
let c = crate::scene::named_view_center(v);
|
||
return Some(ModelView {
|
||
center: Vector3::new(c.x, c.y, c.z),
|
||
width: v.width.abs().max(1e-6),
|
||
height: v.height.abs().max(1e-6),
|
||
target: v.target.clone(),
|
||
direction: v.direction.clone(),
|
||
});
|
||
}
|
||
let (lo, hi) = (&doc.header.model_space_extents_min, &doc.header.model_space_extents_max);
|
||
let (w, h) = (hi.x - lo.x, hi.y - lo.y);
|
||
if !(w > 0.0 && h > 0.0 && w.is_finite() && h.is_finite()) {
|
||
return None;
|
||
}
|
||
Some(ModelView {
|
||
center: Vector3::new((lo.x + hi.x) / 2.0, (lo.y + hi.y) / 2.0, 0.0),
|
||
width: w,
|
||
height: h,
|
||
target: Vector3::ZERO,
|
||
direction: Vector3::new(0.0, 0.0, 1.0),
|
||
})
|
||
}
|
||
|
||
/// The `Database("…").SheetSet("…").Component("…").` prefix of a navigation field.
|
||
fn component_prefix(db: &SheetSetDatabase, id: &str) -> String {
|
||
format!(
|
||
"Database(\"{}\").SheetSet(\"{}\").Component(\"{id}\").",
|
||
ss::native_path(db.path.as_deref().unwrap_or("")),
|
||
db.sheet_set().id()
|
||
)
|
||
}
|
||
|
||
/// Whether new sheets of `component` prompt for their template: its own
|
||
/// "Prompt for template", else the nearest parent's.
|
||
fn prompts_for_template(db: &SheetSetDatabase, component: &str) -> bool {
|
||
let mut id = component.to_string();
|
||
for _ in 0..32 {
|
||
let Some(el) = db.find(&id) else { return false };
|
||
if let Some(v) = el.prop("PromptForDwt") {
|
||
return !matches!(v.trim(), "" | "0");
|
||
}
|
||
match db.parent_of(&id) {
|
||
Some(p) => id = p.id().to_string(),
|
||
None => return false,
|
||
}
|
||
}
|
||
false
|
||
}
|
||
|
||
/// A drawing's paper-space layouts and sheet link, for Import (run on a
|
||
/// worker thread).
|
||
fn read_import(path: &Path) -> ImportRead {
|
||
let doc = crate::io::load_file(path).map_err(|e| e.to_string())?;
|
||
Ok((paper_layouts(&doc), doc.sheet_set_data()))
|
||
}
|
||
|
||
/// Read the drawings picked for Import off the UI thread.
|
||
fn read_imports(paths: Vec<PathBuf>, new_form: bool) -> Task<Message> {
|
||
crate::app::update::background_task(
|
||
crate::t!("Import Layout as Sheet"),
|
||
move || {
|
||
paths
|
||
.into_iter()
|
||
.map(|p| {
|
||
let read = read_import(&p);
|
||
(p, read)
|
||
})
|
||
.collect()
|
||
},
|
||
move |read| Message::SheetSet(SheetSetMsg::ImportRead(new_form, read)),
|
||
)
|
||
}
|
||
|
||
/// The layouts of a drawing that can become sheets, each with the sheet set
|
||
/// it already belongs to: the drawing's sheet link, or this set.
|
||
fn import_rows(
|
||
path: &Path,
|
||
layouts: Vec<(String, String)>,
|
||
link: Option<&SheetSetData>,
|
||
db: &SheetSetDatabase,
|
||
) -> Vec<ImportRow> {
|
||
let drawing = path.to_string_lossy().to_string();
|
||
layouts
|
||
.into_iter()
|
||
.map(|(layout, handle)| {
|
||
let mine = db.sheet_for(&drawing, Some(&layout)).is_some_and(|s| {
|
||
s.named("Layout").and_then(|r| r.prop("Name")).is_some_and(|n| n.eq_ignore_ascii_case(&layout))
|
||
});
|
||
// A layout of this set is taken; one the drawing's sheet link
|
||
// names for another set is only warned about (still importable).
|
||
let warn = !mine
|
||
&& link.is_some_and(|l| {
|
||
l.layout_name.eq_ignore_ascii_case(&layout)
|
||
&& !l.sheet_set_file_name.is_empty()
|
||
&& db.path.as_deref().is_none_or(|p| ss::path_key(p) != ss::path_key(&l.sheet_set_file_name))
|
||
});
|
||
ImportRow { drawing: drawing.clone(), layout, handle, on: !mine, taken: mine, warn }
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// The layouts of the drawings in a wizard folder (run on a worker thread).
|
||
fn scan_folder(path: &Path) -> Vec<FoundLayout> {
|
||
let folder = path.to_string_lossy().to_string();
|
||
let mut files: Vec<PathBuf> = std::fs::read_dir(path)
|
||
.map(|rd| {
|
||
rd.filter_map(|e| e.ok().map(|e| e.path()))
|
||
.filter(|p| p.extension().is_some_and(|x| x.eq_ignore_ascii_case("dwg")))
|
||
.collect()
|
||
})
|
||
.unwrap_or_default();
|
||
files.sort();
|
||
let mut found = Vec::new();
|
||
for file in files {
|
||
let Ok(doc) = crate::io::load_file(&file) else {
|
||
continue;
|
||
};
|
||
for (layout, handle) in paper_layouts(&doc) {
|
||
found.push(FoundLayout {
|
||
folder: folder.clone(),
|
||
file: file.to_string_lossy().to_string(),
|
||
layout,
|
||
handle,
|
||
on: true,
|
||
});
|
||
}
|
||
}
|
||
found
|
||
}
|
||
|
||
/// Rename & Renumber: layout and file names follow the options.
|
||
fn follow_rename(f: &mut Form) {
|
||
let name = |prefix: bool| if prefix { format!("{} {}", f.number.trim(), f.title.trim()).trim().to_string() } else { f.title.trim().to_string() };
|
||
if f.rename[0] {
|
||
f.layout_name = name(f.rename[1]);
|
||
}
|
||
if f.rename[2] {
|
||
f.file_name = format!("{}.dwg", name(f.rename[3]));
|
||
}
|
||
}
|
||
|
||
fn view_form(db: &SheetSetDatabase, set: usize, id: &str) -> Form {
|
||
let el = db.find(id);
|
||
Form {
|
||
kind: FormKind::RenameView,
|
||
set,
|
||
component: id.to_string(),
|
||
number: el.and_then(|e| e.prop("Number")).unwrap_or("").to_string(),
|
||
title: el.and_then(|e| e.prop("Title")).unwrap_or("").to_string(),
|
||
..Form::default()
|
||
}
|
||
}
|
||
|
||
fn rename_form(db: &SheetSetDatabase, set: usize, id: &str) -> Form {
|
||
let el = db.find(id);
|
||
let reference = db.layout_reference(id, "Layout").unwrap_or_default();
|
||
let file = Path::new(&reference.file_name);
|
||
Form {
|
||
kind: FormKind::Rename,
|
||
set,
|
||
component: id.to_string(),
|
||
number: el.and_then(|e| e.prop("Number")).unwrap_or("").to_string(),
|
||
title: el.and_then(|e| e.prop("Title")).unwrap_or("").to_string(),
|
||
layout_name: reference.name.clone(),
|
||
file_name: file.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(),
|
||
folder: file.parent().map(|p| p.to_string_lossy().to_string()).unwrap_or_default(),
|
||
..Form::default()
|
||
}
|
||
}
|
||
|
||
/// Template of a component for new sheets: its own `DefDwtLayout`, else
|
||
/// the nearest parent's.
|
||
fn template_of(db: &SheetSetDatabase, component: &str) -> Option<LayoutReference> {
|
||
let mut id = component.to_string();
|
||
for _ in 0..32 {
|
||
if let Some(t) = db.layout_reference(&id, "DefDwtLayout") {
|
||
return Some(t);
|
||
}
|
||
id = db.parent_of(&id)?.id().to_string();
|
||
}
|
||
None
|
||
}
|
||
|
||
/// New sheet folder of a component: its `NewSheetLocation`, else the
|
||
/// nearest parent's, else the `.dst` folder.
|
||
fn sheet_folder_of(db: &SheetSetDatabase, component: &str) -> String {
|
||
let mut id = component.to_string();
|
||
for _ in 0..32 {
|
||
if let Some(f) = db.file_reference(&id, "NewSheetLocation").filter(|f| !f.is_empty()) {
|
||
return f;
|
||
}
|
||
match db.parent_of(&id) {
|
||
Some(p) => id = p.id().to_string(),
|
||
None => break,
|
||
}
|
||
}
|
||
db.path.as_deref().map(folder_of).unwrap_or_default()
|
||
}
|
||
|
||
impl OpenCADStudio {
|
||
pub(super) fn dispatch_sheet_set(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||
let upper = cmd.trim().to_ascii_uppercase();
|
||
let (verb, arg) = match upper.split_once(char::is_whitespace) {
|
||
Some((v, _)) => (v.to_string(), cmd.trim()[v.len()..].trim().to_string()),
|
||
None => (upper.clone(), String::new()),
|
||
};
|
||
match verb.as_str() {
|
||
"SHEETSET" | "SSM" => {
|
||
self.show_sheet_set_manager(true);
|
||
Some(Task::none())
|
||
}
|
||
"SHEETSETHIDE" => {
|
||
self.show_sheet_set_manager(false);
|
||
Some(Task::none())
|
||
}
|
||
// The ribbon toggle (its macro picks SHEETSET or SHEETSETHIDE by SSMSTATE).
|
||
"_SSMTOGGLE" => {
|
||
let on = !self.sheet_set.show;
|
||
self.show_sheet_set_manager(on);
|
||
Some(Task::none())
|
||
}
|
||
"NEWSHEETSET" => {
|
||
self.open_new_sheet_set_wizard();
|
||
Some(Task::none())
|
||
}
|
||
"_SSMPLACEPOINT" => {
|
||
let v: Vec<f64> = arg.split_whitespace().filter_map(|t| t.parse().ok()).collect();
|
||
if let [x, y, scale] = v[..] {
|
||
self.place_at(i, x, y, scale);
|
||
}
|
||
Some(Task::none())
|
||
}
|
||
"OPENSHEETSET" => {
|
||
if !arg.is_empty() {
|
||
return Some(self.update(Message::SheetSet(SheetSetMsg::OpenPicked(Some(PathBuf::from(
|
||
arg.trim_matches('"'),
|
||
))))));
|
||
}
|
||
Some(Task::perform(
|
||
async {
|
||
crate::sys::file_dialog()
|
||
.set_title(crate::t!("Open Sheet Set").as_ref())
|
||
.add_filter(crate::t!("Sheet Set (*.dst)").as_ref(), &["dst", "DST"])
|
||
.pick_file()
|
||
.await
|
||
.map(|h| crate::sys::handle_path(&h))
|
||
},
|
||
|p| Message::SheetSet(SheetSetMsg::OpenPicked(p)),
|
||
))
|
||
}
|
||
_ => self.dispatch_sheet_set_vars(cmd, i),
|
||
}
|
||
}
|
||
|
||
fn dispatch_sheet_set_vars(&mut self, cmd: &str, _i: usize) -> Option<Task<Message>> {
|
||
let cmd = cmd.trim();
|
||
let rest = match cmd.get(..7) {
|
||
Some(prefix) if prefix.eq_ignore_ascii_case("SETVAR ") => cmd[7..].trim(),
|
||
_ => cmd,
|
||
};
|
||
let mut parts = rest.splitn(2, char::is_whitespace);
|
||
let name = parts.next().unwrap_or("").to_ascii_uppercase();
|
||
if !SHEET_SET_SYSVARS.contains(&name.as_str()) {
|
||
return None;
|
||
}
|
||
let value = parts.next().map(str::trim).filter(|v| !v.is_empty());
|
||
let s = &mut self.sheet_set.settings;
|
||
match name.as_str() {
|
||
"SSMSTATE" => {
|
||
let v = u8::from(self.sheet_set.show);
|
||
self.command_line.push_output(&format!("SSMSTATE = {v} ({})", crate::t!("Read-only")));
|
||
}
|
||
"SSFOUND" => {
|
||
let v = self.sheet_set.found.clone();
|
||
self.command_line.push_output(&format!("SSFOUND = \"{v}\" ({})", crate::t!("Read-only")));
|
||
}
|
||
_ => {
|
||
let (current, min, max) = match name.as_str() {
|
||
"SSMAUTOOPEN" => (i32::from(s.auto_open), 0, 1),
|
||
"SSLOCATE" => (i32::from(s.locate), 0, 1),
|
||
"SSMPOLLTIME" => (i32::from(s.poll_time), 20, 600),
|
||
_ => (i32::from(s.sheet_status), 0, 2),
|
||
};
|
||
let ask = |app: &mut Self, name: String| {
|
||
app.command_line.push_output(&crate::tf!("Enter new value for {name} <{current}>:"));
|
||
app.pending_setvar = Some(name);
|
||
};
|
||
match value {
|
||
None => ask(self, name),
|
||
Some(v) => match v.parse::<i32>() {
|
||
Ok(n) if !(min..=max).contains(&n) => {
|
||
self.command_line.push_error(&if max == 1 {
|
||
crate::t!("Requires 0 or 1").into_owned()
|
||
} else {
|
||
crate::tf!("Requires an integer between {min} and {max}.").into_owned()
|
||
});
|
||
ask(self, name);
|
||
}
|
||
Err(_) => {
|
||
self.command_line.push_error(&crate::tf!("Requires an integer between {min} and {max}."));
|
||
ask(self, name);
|
||
}
|
||
Ok(n) => {
|
||
let s = &mut self.sheet_set.settings;
|
||
match name.as_str() {
|
||
"SSMAUTOOPEN" => s.auto_open = n as u8,
|
||
"SSLOCATE" => s.locate = n as u8,
|
||
"SSMPOLLTIME" => s.poll_time = n as u16,
|
||
_ => s.sheet_status = n as u8,
|
||
}
|
||
self.persist_settings_if_changed();
|
||
}
|
||
},
|
||
}
|
||
}
|
||
}
|
||
Some(Task::none())
|
||
}
|
||
|
||
/// Show or hide the palette (docked on the right like the other
|
||
/// managers).
|
||
pub(in crate::app) fn show_sheet_set_manager(&mut self, on: bool) {
|
||
let id = crate::ui::dock::PanelId::SheetSetManager;
|
||
self.sheet_set.show = on;
|
||
self.ribbon.set_sheet_set(on);
|
||
if on {
|
||
if self.dock.location(id).is_none() {
|
||
self.dock.dock(id, crate::app::config::DockSide::Right, usize::MAX);
|
||
}
|
||
self.dock_expanded = Some(id);
|
||
} else if self.dock_expanded == Some(id) {
|
||
self.dock_expanded = None;
|
||
}
|
||
}
|
||
|
||
/// Publish the open sets to the field engine and redraw every field.
|
||
fn sheet_sets_changed(&mut self) {
|
||
crate::entities::field::set_sheet_sets(self.sheet_set.sets.clone());
|
||
self.sync_drawing_locks();
|
||
self.refresh_locations();
|
||
if self.sheet_set.settings.sheet_status >= 1 {
|
||
self.refresh_sheet_status();
|
||
} else {
|
||
self.sheet_set.status.clear();
|
||
}
|
||
for tab in &mut self.tabs {
|
||
if refresh_sheet_fields(&mut tab.scene) {
|
||
tab.dirty = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Open (or bring forward) the sheet set in `path`.
|
||
pub(in crate::app) fn open_sheet_set(&mut self, path: &Path, show: bool) -> Result<(), String> {
|
||
let key = ss::path_key(&path.to_string_lossy());
|
||
if let Some(k) = self
|
||
.sheet_set
|
||
.sets
|
||
.iter()
|
||
.position(|db| db.path.as_deref().is_some_and(|p| ss::path_key(p) == key))
|
||
{
|
||
self.sheet_set.current = Some(k);
|
||
} else {
|
||
let db = read_set(&path.to_string_lossy())?;
|
||
self.sheet_set.sets.push(db);
|
||
self.sheet_set.current = Some(self.sheet_set.sets.len() - 1);
|
||
}
|
||
if show {
|
||
self.show_sheet_set_manager(true);
|
||
}
|
||
self.sheet_sets_changed();
|
||
Ok(())
|
||
}
|
||
|
||
/// Lock files for the sheets of the open sets that are open in tabs, as
|
||
/// other instances expect them (the sheet status shows such a sheet as
|
||
/// open). A drawing with someone else's lock file is left alone; a lock
|
||
/// a crashed session of this application left is taken back. With no set
|
||
/// open every lock is released.
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
fn sync_drawing_locks(&mut self) {
|
||
let sets = &self.sheet_set.sets;
|
||
let open: std::collections::HashSet<PathBuf> = self
|
||
.tabs
|
||
.iter()
|
||
.filter_map(|t| t.current_path.clone())
|
||
.filter(|p| {
|
||
let drawing = p.to_string_lossy();
|
||
sets.iter().any(|db| db.sheet_for(&drawing, None).is_some())
|
||
})
|
||
.filter(|p| p.exists())
|
||
.collect();
|
||
let gone: Vec<PathBuf> = self.sheet_set.locks.difference(&open).cloned().collect();
|
||
for p in gone {
|
||
release_lock(&p);
|
||
self.sheet_set.locks.remove(&p);
|
||
}
|
||
for p in open {
|
||
if self.sheet_set.locks.contains(&p) || (p.with_extension("dwl").exists() && !stale_own_lock(&p)) {
|
||
continue;
|
||
}
|
||
if write_lock(&p).is_ok() {
|
||
self.sheet_set.locks.insert(p);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(target_arch = "wasm32")]
|
||
fn sync_drawing_locks(&mut self) {}
|
||
|
||
/// Reload every open set whose `.dst` was changed by someone else.
|
||
fn poll_sheet_sets(&mut self) {
|
||
let mut changed = false;
|
||
for db in &mut self.sheet_set.sets {
|
||
let Some(path) = db.path.clone() else {
|
||
continue;
|
||
};
|
||
let Ok(modified) = std::fs::metadata(&path).and_then(|m| m.modified()) else {
|
||
continue;
|
||
};
|
||
let key = ss::path_key(&path);
|
||
if self.sheet_set.seen.insert(key, modified).is_none_or(|t| t == modified) {
|
||
continue;
|
||
}
|
||
if let Ok(fresh) = read_set(&path) {
|
||
if fresh.root != db.root {
|
||
*db = fresh;
|
||
changed = true;
|
||
}
|
||
}
|
||
}
|
||
if changed {
|
||
self.sheet_sets_changed();
|
||
}
|
||
}
|
||
|
||
/// Write the current set back to its `.dst`.
|
||
fn save_current_sheet_set(&mut self) {
|
||
let Some(db) = self.sheet_set.db_mut() else {
|
||
return;
|
||
};
|
||
let Some(path) = db.path.clone() else {
|
||
return;
|
||
};
|
||
if let Err(e) = write_set(db, &path) {
|
||
self.command_line.push_error(&format!("{path}: {e}"));
|
||
}
|
||
self.sheet_sets_changed();
|
||
}
|
||
|
||
fn open_new_sheet_set_wizard(&mut self) {
|
||
let mut n = 1;
|
||
let folder = dirs_next_documents();
|
||
let name = loop {
|
||
let name = format!("New Sheet Set ({n})");
|
||
if !Path::new(&folder).join(format!("{name}.dst")).exists() {
|
||
break name;
|
||
}
|
||
n += 1;
|
||
};
|
||
let draft = SheetSetDatabase::new(&name, "");
|
||
self.sheet_set.dialog = Some(SsDialog::Wizard(Wizard {
|
||
step: 0,
|
||
existing: false,
|
||
name,
|
||
description: String::new(),
|
||
folder,
|
||
hierarchy: false,
|
||
folders: Vec::new(),
|
||
layouts: Vec::new(),
|
||
draft,
|
||
error: None,
|
||
}));
|
||
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
||
}
|
||
|
||
/// Resolve a palette / automation target: a component id, a sheet
|
||
/// number or title, a subset name, or `set`.
|
||
pub(in crate::app) fn sheet_set_target(&self, spec: &str) -> Option<String> {
|
||
let db = self.sheet_set.db()?;
|
||
if spec.eq_ignore_ascii_case("set") {
|
||
return Some(db.sheet_set().id().to_string());
|
||
}
|
||
if db.find(spec).is_some() {
|
||
return Some(spec.to_string());
|
||
}
|
||
fn go(el: &ss::Element, spec: &str) -> Option<String> {
|
||
for c in &el.children {
|
||
match ComponentKind::of(c) {
|
||
Some(ComponentKind::Sheet)
|
||
if c.prop("Number") == Some(spec)
|
||
|| c.prop("Title") == Some(spec)
|
||
|| ss::number_and_title(c) == spec =>
|
||
{
|
||
return Some(c.id().to_string())
|
||
}
|
||
Some(ComponentKind::Subset) => {
|
||
if c.prop("Name") == Some(spec) {
|
||
return Some(c.id().to_string());
|
||
}
|
||
if let Some(id) = go(c, spec) {
|
||
return Some(id);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
None
|
||
}
|
||
go(db.sheet_set(), spec)
|
||
}
|
||
|
||
fn component_kind(&self, id: &str) -> Option<ComponentKind> {
|
||
ComponentKind::of(self.sheet_set.db()?.find(id)?)
|
||
}
|
||
|
||
pub(in crate::app) fn on_sheet_set(&mut self, m: SheetSetMsg) -> Task<Message> {
|
||
match m {
|
||
SheetSetMsg::PickSet(Some(k)) => {
|
||
if k < self.sheet_set.sets.len() {
|
||
self.sheet_set.current = Some(k);
|
||
self.sheet_sets_changed();
|
||
}
|
||
}
|
||
SheetSetMsg::PickSet(None) => return self.dispatch_command("OPENSHEETSET"),
|
||
SheetSetMsg::Tab(t) => self.sheet_set.tab = t,
|
||
SheetSetMsg::Expand(id) => {
|
||
if !self.sheet_set.collapsed.remove(&id) {
|
||
self.sheet_set.collapsed.insert(id);
|
||
}
|
||
}
|
||
SheetSetMsg::Select(id) => self.sheet_set.selected = Some(id),
|
||
SheetSetMsg::Activate(id) => {
|
||
self.sheet_set.selected = Some(id.clone());
|
||
if self.component_kind(&id) == Some(ComponentKind::Sheet) {
|
||
return self.open_sheet(&id);
|
||
}
|
||
if !self.sheet_set.collapsed.remove(&id) {
|
||
self.sheet_set.collapsed.insert(id);
|
||
}
|
||
}
|
||
SheetSetMsg::Poll => {
|
||
self.sync_drawing_locks();
|
||
self.poll_sheet_sets();
|
||
// SSMSHEETSTATUS 2: the status is taken again every SSMPOLLTIME seconds.
|
||
let every = iced::time::Duration::from_secs(u64::from(self.sheet_set.settings.poll_time.max(20)));
|
||
if self.sheet_set.settings.sheet_status == 2 && self.sheet_set.status_at.is_none_or(|t| t.elapsed() >= every) {
|
||
self.refresh_sheet_status();
|
||
}
|
||
}
|
||
SheetSetMsg::BrowseDrawings => {
|
||
return Task::perform(
|
||
async {
|
||
crate::sys::file_dialog()
|
||
.set_title(crate::t!("Select Drawing").as_ref())
|
||
.add_filter(crate::t!("Drawing (*.dwg)").as_ref(), &["dwg", "DWG"])
|
||
.pick_files()
|
||
.await
|
||
.map(|hs| hs.iter().map(crate::sys::handle_path).collect::<Vec<_>>())
|
||
.unwrap_or_default()
|
||
},
|
||
|p| Message::SheetSet(SheetSetMsg::ImportAdd(p)),
|
||
)
|
||
}
|
||
SheetSetMsg::ImportAdd(paths) if !paths.is_empty() => return read_imports(paths, false),
|
||
SheetSetMsg::ImportAdd(_) => {}
|
||
SheetSetMsg::ImportRead(new_form, read) => self.import_read(new_form, read),
|
||
SheetSetMsg::FolderScanned(path, found) => self.folder_scanned(path, found),
|
||
SheetSetMsg::DrawingRead(path, read, then) => match read.0 {
|
||
Ok((modified, doc)) => {
|
||
self.sheet_set.drawings.insert(&path, modified, doc);
|
||
return self.on_sheet_set(*then);
|
||
}
|
||
Err(e) => self.command_line.push_error(&format!("{path}: {e}")),
|
||
},
|
||
SheetSetMsg::Previous => self.rename_step(false),
|
||
SheetSetMsg::Next => self.rename_step(true),
|
||
SheetSetMsg::ViewsByCategory(v) => self.sheet_set.by_category = v,
|
||
SheetSetMsg::NewCategory => self.open_category(None),
|
||
SheetSetMsg::CategoryProperties(id) => self.open_category(Some(id)),
|
||
SheetSetMsg::CategoryRemove(id) => {
|
||
if let Some(db) = self.sheet_set.db_mut() {
|
||
db.remove(&id);
|
||
}
|
||
self.save_current_sheet_set();
|
||
}
|
||
SheetSetMsg::AddBlocks => {
|
||
if let Some(SsDialog::Category(c)) = self.sheet_set.dialog.take() {
|
||
let blocks = c.blocks.iter().map(|(id, label, _)| (id.clone(), label.clone())).collect();
|
||
self.sheet_set.dialog = Some(SsDialog::BlockList(BlockList { category: Box::new(c), blocks, selected: None, preview: None }));
|
||
}
|
||
}
|
||
SheetSetMsg::BlocksPicked(_) | SheetSetMsg::LocationPicked(None) => {}
|
||
SheetSetMsg::Submenu(key) => self.sheet_set.submenu = key,
|
||
SheetSetMsg::BlockListSelect(k) => {
|
||
let target = match self.sheet_set.dialog.as_ref() {
|
||
Some(SsDialog::BlockList(b)) => b.blocks.get(k).and_then(|(id, _)| self.listed_block(b.category.set, id)),
|
||
_ => None,
|
||
};
|
||
// A whole drawing taken as a block shows its model space.
|
||
let mut preview = None;
|
||
if let Some((file, name)) = target {
|
||
let whole = match self.drawing_or_read(&file, SheetSetMsg::BlockListSelect(k)) {
|
||
Ok(doc) => doc.block_records.get(&name).is_none(),
|
||
Err(task) => return task,
|
||
};
|
||
preview = self.block_preview(&file, if whole { "" } else { &name });
|
||
}
|
||
if let Some(SsDialog::BlockList(b)) = self.sheet_set.dialog.as_mut() {
|
||
b.selected = Some(k);
|
||
b.preview = preview;
|
||
}
|
||
}
|
||
SheetSetMsg::BlockListDelete => {
|
||
if let Some(SsDialog::BlockList(b)) = self.sheet_set.dialog.as_mut() {
|
||
if let Some(k) = b.selected.take().filter(|k| *k < b.blocks.len()) {
|
||
b.blocks.remove(k);
|
||
}
|
||
}
|
||
}
|
||
SheetSetMsg::BlockListAdd => {
|
||
if let Some(SsDialog::BlockList(b)) = self.sheet_set.dialog.take() {
|
||
self.sheet_set.dialog = Some(SsDialog::SelectBlock(SelectBlock {
|
||
list: Box::new(b),
|
||
file: String::new(),
|
||
whole: true,
|
||
names: Vec::new(),
|
||
preview: None,
|
||
error: None,
|
||
}));
|
||
}
|
||
}
|
||
SheetSetMsg::SelectBlockBrowse => {
|
||
return Task::perform(
|
||
async {
|
||
crate::sys::file_dialog()
|
||
.set_title(crate::t!("Select Drawing").as_ref())
|
||
.add_filter(crate::t!("Drawing (*.dwg)").as_ref(), &["dwg", "DWG", "dwt", "DWT"])
|
||
.pick_file()
|
||
.await
|
||
.map(|h| crate::sys::handle_path(&h))
|
||
},
|
||
|p| Message::SheetSet(SheetSetMsg::SelectBlockPicked(p)),
|
||
)
|
||
}
|
||
SheetSetMsg::SelectBlockPicked(None) => {}
|
||
SheetSetMsg::SelectBlockPicked(Some(path)) => return self.select_block_file(path),
|
||
SheetSetMsg::SelectBlockWhole(v) => {
|
||
let file = match self.sheet_set.dialog.as_ref() {
|
||
Some(SsDialog::SelectBlock(b)) => b.file.clone(),
|
||
_ => String::new(),
|
||
};
|
||
let preview = if v && !file.is_empty() { self.block_preview(&file, "") } else { None };
|
||
if let Some(SsDialog::SelectBlock(b)) = self.sheet_set.dialog.as_mut() {
|
||
b.whole = v;
|
||
b.preview = preview;
|
||
}
|
||
}
|
||
SheetSetMsg::SelectBlockCheck(k, v) => {
|
||
let picked = match self.sheet_set.dialog.as_ref() {
|
||
Some(SsDialog::SelectBlock(b)) => b.names.get(k).map(|n| (b.file.clone(), n.0.clone())),
|
||
_ => None,
|
||
};
|
||
// The block just checked is previewed.
|
||
let preview = picked.and_then(|(file, name)| self.block_preview(&file, &name));
|
||
if let Some(SsDialog::SelectBlock(b)) = self.sheet_set.dialog.as_mut() {
|
||
if let Some(n) = b.names.get_mut(k) {
|
||
n.2 = v;
|
||
}
|
||
b.preview = preview;
|
||
}
|
||
}
|
||
SheetSetMsg::ToggleDrawing(path) => {
|
||
if !self.sheet_set.open_drawings.remove(&ss::path_key(&path)) {
|
||
return self.on_sheet_set(SheetSetMsg::SeeViews(path));
|
||
}
|
||
}
|
||
SheetSetMsg::SeeViews(path) => {
|
||
if let Err(task) = self.load_model_views(&path) {
|
||
return task;
|
||
}
|
||
self.sheet_set.open_drawings.insert(ss::path_key(&path));
|
||
}
|
||
SheetSetMsg::PlaceOnSheet(path, view) => return self.start_place_view(path, view),
|
||
SheetSetMsg::ViewMenu(id, action) => {
|
||
self.sheet_set.submenu = None;
|
||
return self.view_menu(id, action);
|
||
}
|
||
// Browsers expose no folder picker.
|
||
#[cfg(target_arch = "wasm32")]
|
||
SheetSetMsg::AddLocation | SheetSetMsg::Browse(_) => {}
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
SheetSetMsg::AddLocation => {
|
||
return Task::perform(
|
||
async {
|
||
crate::sys::file_dialog()
|
||
.set_title(crate::t!("Browse for Folder").as_ref())
|
||
.pick_folder()
|
||
.await
|
||
.map(|h| crate::sys::handle_path(&h))
|
||
},
|
||
|p| Message::SheetSet(SheetSetMsg::LocationPicked(p)),
|
||
)
|
||
}
|
||
SheetSetMsg::LocationPicked(Some(path)) => {
|
||
if let Some(db) = self.sheet_set.db_mut() {
|
||
db.add_resource(&path.to_string_lossy());
|
||
}
|
||
self.save_current_sheet_set();
|
||
}
|
||
SheetSetMsg::RemoveLocation(id) => {
|
||
if let Some(db) = self.sheet_set.db_mut() {
|
||
db.remove(&id);
|
||
}
|
||
self.save_current_sheet_set();
|
||
}
|
||
SheetSetMsg::OpenDrawing(path) => return self.update(Message::OpenRecent(PathBuf::from(path))),
|
||
SheetSetMsg::Refresh => {
|
||
if let Some(path) = self.sheet_set.db().and_then(|db| db.path.clone()) {
|
||
match read_set(&path) {
|
||
Ok(db) => {
|
||
if let Some(slot) = self.sheet_set.db_mut() {
|
||
*slot = db;
|
||
}
|
||
self.sheet_sets_changed();
|
||
}
|
||
Err(e) => self.command_line.push_error(&format!("{path}: {e}")),
|
||
}
|
||
}
|
||
}
|
||
SheetSetMsg::Menu(id, action) => return self.sheet_set_menu(id, action),
|
||
SheetSetMsg::OpenPicked(Some(path)) => {
|
||
if let Err(e) = self.open_sheet_set(&path, true) {
|
||
self.command_line.push_error(&format!("{}: {e}", path.display()));
|
||
}
|
||
}
|
||
SheetSetMsg::OpenPicked(None) | SheetSetMsg::ImportPicked(None) | SheetSetMsg::TemplatePicked(None) => {}
|
||
SheetSetMsg::ImportPicked(Some(path)) => return read_imports(vec![path], true),
|
||
SheetSetMsg::FolderPicked(field, Some(path)) => return self.dialog_folder(field, path),
|
||
SheetSetMsg::FolderPicked(_, None) => {}
|
||
SheetSetMsg::TemplatePicked(Some(path)) => return self.properties_template(path),
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
SheetSetMsg::Browse(field) => {
|
||
return Task::perform(
|
||
async {
|
||
crate::sys::file_dialog()
|
||
.set_title(crate::t!("Browse for Folder").as_ref())
|
||
.pick_folder()
|
||
.await
|
||
.map(|h| crate::sys::handle_path(&h))
|
||
},
|
||
move |p| Message::SheetSet(SheetSetMsg::FolderPicked(field, p)),
|
||
)
|
||
}
|
||
SheetSetMsg::BrowseTemplate => {
|
||
return Task::perform(
|
||
async {
|
||
crate::sys::file_dialog()
|
||
.set_title(crate::t!("Select Layout as Sheet Template").as_ref())
|
||
.add_filter(crate::t!("Drawing Template (*.dwt)").as_ref(), &["dwt", "DWT"])
|
||
.add_filter(crate::t!("Drawing (*.dwg)").as_ref(), &["dwg", "DWG"])
|
||
.pick_file()
|
||
.await
|
||
.map(|h| crate::sys::handle_path(&h))
|
||
},
|
||
|p| Message::SheetSet(SheetSetMsg::TemplatePicked(p)),
|
||
)
|
||
}
|
||
SheetSetMsg::Input(field, v) => self.dialog_input(field, v),
|
||
SheetSetMsg::Toggle(field, v) => {
|
||
let parent_folder = match self.sheet_set.dialog.as_ref() {
|
||
Some(SsDialog::Form(f)) if f.kind == FormKind::NewSubset => {
|
||
self.sheet_set.sets.get(f.set).map(|db| sheet_folder_of(db, &f.component)).unwrap_or_default()
|
||
}
|
||
_ => String::new(),
|
||
};
|
||
match (field, self.sheet_set.dialog.as_mut()) {
|
||
(FieldId::Hierarchy, Some(SsDialog::Wizard(w))) => w.hierarchy = v,
|
||
(FieldId::Hierarchy, Some(SsDialog::Form(f))) => {
|
||
f.hierarchy = v;
|
||
f.folder = if v { Path::new(&parent_folder).join(f.title.trim()).to_string_lossy().to_string() } else { parent_folder };
|
||
}
|
||
(FieldId::Publish, Some(SsDialog::Form(f))) => f.publish = v,
|
||
(FieldId::OpenAfter, Some(SsDialog::Form(f))) => f.open_after = v,
|
||
(FieldId::ImportPrefix, Some(SsDialog::Form(f))) => f.prefix = v,
|
||
(FieldId::ImportRow(k), Some(SsDialog::Form(f))) => {
|
||
if let Some(r) = f.rows.get_mut(k) {
|
||
r.on = v && !r.taken;
|
||
}
|
||
}
|
||
(FieldId::RenameOption(k), Some(SsDialog::Form(f))) => {
|
||
f.rename[k] = v;
|
||
// A prefix needs its "Sheet title" option.
|
||
if !f.rename[0] {
|
||
f.rename[1] = false;
|
||
}
|
||
if !f.rename[2] {
|
||
f.rename[3] = false;
|
||
}
|
||
follow_rename(f);
|
||
}
|
||
(FieldId::CategoryBlock(k), Some(SsDialog::Category(c))) => {
|
||
if let Some(b) = c.blocks.get_mut(k) {
|
||
b.2 = v;
|
||
}
|
||
}
|
||
(FieldId::Row(r), Some(SsDialog::Properties(p))) => {
|
||
if let Some(row) = p.rows.get_mut(r) {
|
||
row.value = if v { "Publish by Sheet 'Include for Publish' Setting" } else { "Do Not Publish Sheets" }.into();
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
SheetSetMsg::Choose(field, k) => match (field, self.sheet_set.dialog.as_mut()) {
|
||
(FieldId::Layout, Some(SsDialog::Template(t))) => t.layout = k,
|
||
(FieldId::CustomOwner, Some(SsDialog::Properties(p))) => {
|
||
if let Some(a) = p.adding.as_mut() {
|
||
a.2 = k == 1;
|
||
}
|
||
}
|
||
(FieldId::Row(r), Some(SsDialog::Properties(p))) => {
|
||
if let Some(row) = p.rows.get_mut(r) {
|
||
row.value = if k == 0 { "Yes" } else { "No" }.into();
|
||
}
|
||
}
|
||
_ => {}
|
||
},
|
||
SheetSetMsg::Wizard(w) => self.on_wizard(w),
|
||
SheetSetMsg::WizardProperties => {
|
||
if let Some(SsDialog::Wizard(mut w)) = self.sheet_set.dialog.take() {
|
||
sync_wizard_draft(&mut w);
|
||
let set = w.draft.sheet_set().id().to_string();
|
||
let mut p = build_properties(&w.draft, &set, None);
|
||
// The set is not written yet: show where it will be.
|
||
for row in &mut p.rows {
|
||
match row.key {
|
||
RowKey::ReadOnly if row.label == "Sheet set data file" => {
|
||
row.value = crate::ui::window::sheet_set::wizard_dst_path(&w)
|
||
}
|
||
RowKey::Folder(_) if row.value.is_empty() => row.value = w.folder.clone(),
|
||
_ => {}
|
||
}
|
||
}
|
||
p.wizard = Some(Box::new(w));
|
||
self.sheet_set.dialog = Some(SsDialog::Properties(p));
|
||
}
|
||
}
|
||
SheetSetMsg::AddCustom => {
|
||
if let Some(SsDialog::Properties(p)) = self.sheet_set.dialog.as_mut() {
|
||
let mut n = 1;
|
||
while p.custom.iter().any(|(name, _, _)| *name == format!("New Name ({n})")) {
|
||
n += 1;
|
||
}
|
||
p.adding = Some((format!("New Name ({n})"), "Value".into(), false));
|
||
}
|
||
}
|
||
SheetSetMsg::AddCustomCancel => {
|
||
if let Some(SsDialog::Properties(p)) = self.sheet_set.dialog.as_mut() {
|
||
p.adding = None;
|
||
}
|
||
}
|
||
SheetSetMsg::AddCustomOk => {
|
||
if let Some(SsDialog::Properties(p)) = self.sheet_set.dialog.as_mut() {
|
||
if let Some((name, value, sheet)) = p.adding.take() {
|
||
let name = name.trim().to_string();
|
||
if name.is_empty() {
|
||
p.error = Some(crate::t!("The name cannot be empty.").into_owned());
|
||
} else if p.custom.iter().any(|(n, _, _)| n.eq_ignore_ascii_case(&name)) {
|
||
p.error = Some(crate::t!("A custom property with this name already exists.").into_owned());
|
||
} else {
|
||
p.error = None;
|
||
let flags = if sheet { ss::CUSTOM_SHEET_PROP } else { ss::CUSTOM_SHEET_SET_PROP };
|
||
p.custom.push((name, value, flags));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
SheetSetMsg::CustomValue(k, v) => {
|
||
if let Some(SsDialog::Properties(p)) = self.sheet_set.dialog.as_mut() {
|
||
if let Some(c) = p.custom.get_mut(k) {
|
||
c.1 = v;
|
||
}
|
||
}
|
||
}
|
||
SheetSetMsg::Ok => return self.sheet_set_dialog_ok(),
|
||
SheetSetMsg::Cancel => self.close_sheet_set_dialog(),
|
||
SheetSetMsg::Help => self.command_line.push_info(
|
||
crate::t!("Sheet sets organize layouts from several drawings into one named set of sheets.").as_ref(),
|
||
),
|
||
}
|
||
Task::none()
|
||
}
|
||
|
||
/// Cancel / × of the sheet set dialogs: the Properties dialog opened from
|
||
/// the wizard goes back to it.
|
||
pub(in crate::app) fn close_sheet_set_dialog(&mut self) {
|
||
match self.sheet_set.dialog.take() {
|
||
Some(SsDialog::BlockList(b)) => self.sheet_set.dialog = Some(SsDialog::Category(*b.category)),
|
||
Some(SsDialog::SelectBlock(b)) => self.sheet_set.dialog = Some(SsDialog::BlockList(*b.list)),
|
||
Some(SsDialog::Properties(p)) if p.wizard.is_some() => {
|
||
self.sheet_set.dialog = p.wizard.map(|w| SsDialog::Wizard(*w));
|
||
}
|
||
_ => self.close_active_modal(),
|
||
}
|
||
}
|
||
|
||
fn sheet_set_menu(&mut self, id: String, action: MenuAction) -> Task<Message> {
|
||
let Some(set) = self.sheet_set.current else {
|
||
return Task::none();
|
||
};
|
||
self.sheet_set.selected = Some(id.clone());
|
||
let Some(db) = self.sheet_set.db() else {
|
||
return Task::none();
|
||
};
|
||
// On a sheet, New Sheet and Import work on the subset holding it.
|
||
let id = match action {
|
||
MenuAction::NewSheet | MenuAction::ImportLayout
|
||
if db.find(&id).and_then(ComponentKind::of) == Some(ComponentKind::Sheet) =>
|
||
{
|
||
db.parent_of(&id).map_or(id, |p| p.id().to_string())
|
||
}
|
||
_ => id,
|
||
};
|
||
match action {
|
||
MenuAction::Open => return self.open_sheet(&id),
|
||
MenuAction::NewSheet => {
|
||
let template = template_of(db, &id);
|
||
let form = Form {
|
||
kind: FormKind::NewSheet,
|
||
set,
|
||
component: id.clone(),
|
||
folder: sheet_folder_of(db, &id),
|
||
drawing: template
|
||
.as_ref()
|
||
.map(|t| format!("{} ({})", t.name, t.file_name))
|
||
.unwrap_or_else(|| crate::t!("(default new drawing)").into_owned()),
|
||
..Form::default()
|
||
};
|
||
// A subset that prompts for its template asks for it first.
|
||
if prompts_for_template(db, &id) {
|
||
let file = template.map(|t| t.file_name).unwrap_or_default();
|
||
let layouts = if Path::new(&file).is_file() {
|
||
match self.drawing_or_read(&file, SheetSetMsg::Menu(id.clone(), MenuAction::NewSheet)) {
|
||
Ok(doc) => paper_layouts(&doc).into_iter().map(|(n, _)| n).collect(),
|
||
Err(task) => return task,
|
||
}
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
self.sheet_set.dialog = Some(SsDialog::Template(TemplatePick { form, file, layouts, layout: 0 }));
|
||
} else {
|
||
self.sheet_set.dialog = Some(SsDialog::Form(form));
|
||
}
|
||
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
||
}
|
||
MenuAction::NewSubset => {
|
||
let mut n = 1;
|
||
let parent = db.find(&id);
|
||
while parent.is_some_and(|p| {
|
||
p.children.iter().any(|c| c.prop("Name") == Some(format!("New Subset ({n})").as_str()))
|
||
}) {
|
||
n += 1;
|
||
}
|
||
self.sheet_set.dialog = Some(SsDialog::Form(Form {
|
||
kind: FormKind::NewSubset,
|
||
set,
|
||
component: id.clone(),
|
||
title: format!("New Subset ({n})"),
|
||
folder: sheet_folder_of(db, &id),
|
||
publish: true,
|
||
..Form::default()
|
||
}));
|
||
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
||
}
|
||
MenuAction::ImportLayout => {
|
||
self.sheet_set_import_parent = Some(id);
|
||
return Task::perform(
|
||
async {
|
||
crate::sys::file_dialog()
|
||
.set_title(crate::t!("Select Drawing").as_ref())
|
||
.add_filter(crate::t!("Drawing (*.dwg)").as_ref(), &["dwg", "DWG"])
|
||
.pick_file()
|
||
.await
|
||
.map(|h| crate::sys::handle_path(&h))
|
||
},
|
||
|p| Message::SheetSet(SheetSetMsg::ImportPicked(p)),
|
||
);
|
||
}
|
||
MenuAction::Rename => {
|
||
self.sheet_set.dialog = Some(SsDialog::Form(rename_form(db, set, &id)));
|
||
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
||
}
|
||
MenuAction::Remove => {
|
||
// A sheet asks first; a subset can only be removed while empty.
|
||
if db.find(&id).and_then(ComponentKind::of) == Some(ComponentKind::Sheet) {
|
||
let sheet = db.find(&id).map(ss::number_and_title).unwrap_or_default();
|
||
let question = crate::tf!("Are you sure that you want to remove {} Sheet from {} Sheet Set?", sheet, db.name()).into_owned();
|
||
self.sheet_set.dialog = Some(SsDialog::Confirm(set, id.clone(), question));
|
||
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
||
} else if db.find(&id).is_some_and(|el| el.children.iter().all(|c| ComponentKind::of(c).is_none())) {
|
||
if let Some(db) = self.sheet_set.db_mut() {
|
||
db.remove(&id);
|
||
}
|
||
self.sheet_set.selected = None;
|
||
self.save_current_sheet_set();
|
||
}
|
||
}
|
||
MenuAction::Properties => {
|
||
let p = build_properties(db, &id, Some(set));
|
||
self.sheet_set.dialog = Some(SsDialog::Properties(p));
|
||
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
||
}
|
||
MenuAction::Close => {
|
||
self.sheet_set.sets.remove(set);
|
||
self.sheet_set.current = (!self.sheet_set.sets.is_empty()).then_some(0);
|
||
self.sheet_set.selected = None;
|
||
self.sheet_sets_changed();
|
||
}
|
||
}
|
||
Task::none()
|
||
}
|
||
|
||
/// Open a sheet's drawing (or switch to it) at the sheet's layout.
|
||
fn open_sheet(&mut self, id: &str) -> Task<Message> {
|
||
let Some(reference) = self.sheet_set.db().and_then(|db| db.layout_reference(id, "Layout")) else {
|
||
return Task::none();
|
||
};
|
||
let path = PathBuf::from(&reference.file_name);
|
||
if let Some(k) = self.tab_showing(&path) {
|
||
return Task::batch([
|
||
self.update(Message::TabSwitch(k)),
|
||
Task::done(Message::LayoutSwitch(reference.name)),
|
||
]);
|
||
}
|
||
if !path.exists() {
|
||
self.command_line
|
||
.push_error(crate::tf!("Sheet drawing not found: {}", path.display()).as_ref());
|
||
return Task::none();
|
||
}
|
||
self.sheet_set_pending_layout = Some((ss::path_key(&reference.file_name), reference.name));
|
||
self.update(Message::OpenRecent(path))
|
||
}
|
||
|
||
/// After a drawing opened: switch to the sheet layout that was asked for,
|
||
/// and locate the sheet set the drawing names (SSLOCATE / SSMAUTOOPEN).
|
||
pub(in crate::app) fn sheet_set_after_open(&mut self, i: usize) -> Task<Message> {
|
||
let mut task = Task::none();
|
||
// A sheet's fields show its current values once it is open.
|
||
if !self.sheet_set.sets.is_empty() && refresh_sheet_fields(&mut self.tabs[i].scene) {
|
||
self.tabs[i].dirty = true;
|
||
}
|
||
let path = self.tabs[i].current_path.as_ref().map(|p| p.to_string_lossy().to_string());
|
||
if let (Some(path), Some((want, layout))) = (path.as_ref(), self.sheet_set_pending_layout.as_ref()) {
|
||
if ss::path_key(path) == *want {
|
||
task = Task::done(Message::LayoutSwitch(layout.clone()));
|
||
self.sheet_set_pending_layout = None;
|
||
if let Some(view) = self.sheet_set.pending_view.take() {
|
||
task = task.chain(Task::done(Message::Command(format!("VIEW RESTORE {view}"))));
|
||
}
|
||
}
|
||
}
|
||
if self.sheet_set.settings.locate == 1 {
|
||
if let Some(data) = self.tabs[i].scene.document.sheet_set_data() {
|
||
let named = PathBuf::from(&data.sheet_set_file_name);
|
||
// A moved set is looked for next to the drawing.
|
||
let found = if named.exists() {
|
||
Some(named)
|
||
} else {
|
||
path.as_deref().and_then(|p| {
|
||
let beside = Path::new(p).parent()?.join(named.file_name()?);
|
||
beside.exists().then_some(beside)
|
||
})
|
||
};
|
||
self.sheet_set.found = found.as_ref().map(|f| f.to_string_lossy().to_string()).unwrap_or_default();
|
||
if let Some(found) = found {
|
||
let show = self.sheet_set.settings.auto_open == 1;
|
||
if let Err(e) = self.open_sheet_set(&found, show) {
|
||
self.command_line.push_error(&format!("{}: {e}", found.display()));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
task
|
||
}
|
||
|
||
/// Before a native save: a drawing that is a sheet of an open set records
|
||
/// its link (`AcSheetSetData`), as the reference does when it saves a
|
||
/// sheet while its set is open.
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
pub(in crate::app) fn stamp_sheet_set_data(&mut self, i: usize) {
|
||
let Some(path) = self.tabs[i].current_path.as_ref().map(|p| p.to_string_lossy().to_string()) else {
|
||
return;
|
||
};
|
||
let layout = self.tabs[i].scene.current_layout.clone();
|
||
let layouts = paper_layouts(&self.tabs[i].scene.document);
|
||
for db in &self.sheet_set.sets {
|
||
let wanted = (!layout.eq_ignore_ascii_case("Model")).then_some(layout.as_str());
|
||
let Some(sheet) = db.sheet_for(&path, wanted) else {
|
||
continue;
|
||
};
|
||
let Some(reference) = db.layout_reference(sheet.id(), "Layout") else {
|
||
continue;
|
||
};
|
||
let handle = layouts
|
||
.iter()
|
||
.find(|(n, _)| n.eq_ignore_ascii_case(&reference.name))
|
||
.map(|(_, h)| h.to_lowercase())
|
||
.unwrap_or_else(|| reference.handle.to_lowercase());
|
||
let doc = &mut self.tabs[i].scene.document;
|
||
let previous = doc.sheet_set_data().unwrap_or_default();
|
||
doc.set_sheet_set_data(&SheetSetData {
|
||
layout_handle: handle,
|
||
layout_name: reference.name,
|
||
sheet_dwg_name: ss::native_path(&path),
|
||
sheet_set_file_name: ss::native_path(db.path.as_deref().unwrap_or_default()),
|
||
sheet_set_version: db.file_revision(),
|
||
update_count: previous.update_count + 1,
|
||
update_time: utc_stamp(),
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
fn dialog_input(&mut self, field: FieldId, v: String) {
|
||
match self.sheet_set.dialog.as_mut() {
|
||
Some(SsDialog::Wizard(w)) => match field {
|
||
FieldId::Name => w.name = v,
|
||
FieldId::Description => w.description = v,
|
||
FieldId::Folder => w.folder = v,
|
||
_ => {}
|
||
},
|
||
Some(SsDialog::Properties(p)) => match field {
|
||
FieldId::Row(r) => {
|
||
if let Some(row) = p.rows.get_mut(r) {
|
||
row.value = v;
|
||
}
|
||
}
|
||
FieldId::CustomName => {
|
||
if let Some(a) = p.adding.as_mut() {
|
||
a.0 = v;
|
||
}
|
||
}
|
||
FieldId::CustomDefault => {
|
||
if let Some(a) = p.adding.as_mut() {
|
||
a.1 = v;
|
||
}
|
||
}
|
||
_ => {}
|
||
},
|
||
Some(SsDialog::Category(c)) => {
|
||
if field == FieldId::Name {
|
||
c.name = v;
|
||
}
|
||
}
|
||
Some(SsDialog::Confirm(..)) | Some(SsDialog::Template(_)) | Some(SsDialog::BlockList(_)) | Some(SsDialog::SelectBlock(_)) => {}
|
||
Some(SsDialog::Form(f)) => {
|
||
let file_follows = f.file_name == format!("{} {}", f.number, f.title).trim();
|
||
match field {
|
||
FieldId::Number => f.number = v,
|
||
FieldId::Title => {
|
||
// A folder hierarchy follows the subset name.
|
||
if f.kind == FormKind::NewSubset && f.hierarchy {
|
||
if let Some(parent) = Path::new(&f.folder).parent() {
|
||
f.folder = parent.join(v.trim()).to_string_lossy().to_string();
|
||
}
|
||
}
|
||
f.title = v
|
||
}
|
||
FieldId::FileName => f.file_name = v,
|
||
FieldId::Folder => f.folder = v,
|
||
FieldId::LayoutName => f.layout_name = v,
|
||
_ => {}
|
||
}
|
||
if f.kind == FormKind::Rename {
|
||
follow_rename(f);
|
||
}
|
||
// The file name follows "Number Title" until edited.
|
||
if f.kind == FormKind::NewSheet && file_follows && field != FieldId::FileName {
|
||
f.file_name = format!("{} {}", f.number, f.title).trim().to_string();
|
||
}
|
||
}
|
||
None => {}
|
||
}
|
||
}
|
||
|
||
fn dialog_folder(&mut self, field: FieldId, path: PathBuf) -> Task<Message> {
|
||
if field == FieldId::AddFolder {
|
||
return self.wizard_add_folder(path);
|
||
}
|
||
self.dialog_input(field, path.to_string_lossy().to_string());
|
||
Task::none()
|
||
}
|
||
|
||
/// The wizard's Browse for existing drawings: the folder's drawings are
|
||
/// read off the UI thread.
|
||
fn wizard_add_folder(&mut self, path: PathBuf) -> Task<Message> {
|
||
let Some(SsDialog::Wizard(w)) = self.sheet_set.dialog.as_ref() else {
|
||
return Task::none();
|
||
};
|
||
if w.folders.contains(&path.to_string_lossy().to_string()) {
|
||
return Task::none();
|
||
}
|
||
crate::app::update::background_task(
|
||
crate::t!("Sheet Set Manager"),
|
||
move || {
|
||
let found = scan_folder(&path);
|
||
(path, found)
|
||
},
|
||
|(path, found)| Message::SheetSet(SheetSetMsg::FolderScanned(path, found)),
|
||
)
|
||
}
|
||
|
||
fn folder_scanned(&mut self, path: PathBuf, found: Vec<FoundLayout>) {
|
||
let Some(SsDialog::Wizard(w)) = self.sheet_set.dialog.as_mut() else {
|
||
return;
|
||
};
|
||
let folder = path.to_string_lossy().to_string();
|
||
if !w.folders.contains(&folder) {
|
||
w.layouts.extend(found);
|
||
w.folders.push(folder);
|
||
}
|
||
}
|
||
|
||
fn on_wizard(&mut self, m: WizardMsg) {
|
||
let Some(SsDialog::Wizard(w)) = self.sheet_set.dialog.as_mut() else {
|
||
return;
|
||
};
|
||
match m {
|
||
WizardMsg::Step(s) => {
|
||
w.error = None;
|
||
if s > 1 && w.step <= 1 {
|
||
if w.name.trim().is_empty() {
|
||
w.error = Some(crate::t!("The sheet set name cannot be empty.").into_owned());
|
||
return;
|
||
}
|
||
if !Path::new(&w.folder).is_dir() {
|
||
w.error = Some(crate::t!("The folder for the sheet set data file does not exist.").into_owned());
|
||
return;
|
||
}
|
||
}
|
||
w.step = if s == 2 && !w.existing { 3 } else { s };
|
||
sync_wizard_draft(w);
|
||
}
|
||
WizardMsg::Existing(on) => w.existing = on,
|
||
WizardMsg::LayoutOn(k, on) => {
|
||
if let Some(l) = w.layouts.get_mut(k) {
|
||
l.on = on;
|
||
}
|
||
}
|
||
WizardMsg::RemoveFolder(k) => {
|
||
if k < w.folders.len() {
|
||
let folder = w.folders.remove(k);
|
||
w.layouts.retain(|l| l.folder != folder);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The drawings picked for Import, read: a new Import dialog for the
|
||
/// subset whose menu asked, or the open dialog's rows replaced.
|
||
fn import_read(&mut self, new_form: bool, read: Vec<(PathBuf, ImportRead)>) {
|
||
let set = match self.sheet_set.dialog.as_ref() {
|
||
_ if new_form => self.sheet_set.current,
|
||
Some(SsDialog::Form(f)) => Some(f.set),
|
||
_ => None,
|
||
};
|
||
let Some(db) = set.and_then(|k| self.sheet_set.sets.get(k)) else { return };
|
||
let mut rows = Vec::new();
|
||
let mut errors = Vec::new();
|
||
for (path, r) in read {
|
||
match r {
|
||
Ok((layouts, link)) => rows.extend(import_rows(&path, layouts, link.as_ref(), db)),
|
||
Err(e) => errors.push(format!("{}: {e}", path.display())),
|
||
}
|
||
}
|
||
let failed = !errors.is_empty();
|
||
for e in errors {
|
||
self.command_line.push_error(&e);
|
||
}
|
||
if !new_form {
|
||
// The drawings picked replace the list.
|
||
if let Some(SsDialog::Form(f)) = self.sheet_set.dialog.as_mut() {
|
||
if !rows.is_empty() {
|
||
f.rows = rows;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
let (Some(set), Some(parent)) = (set, self.sheet_set_import_parent.take()) else {
|
||
return;
|
||
};
|
||
if failed {
|
||
return;
|
||
}
|
||
self.sheet_set.dialog = Some(SsDialog::Form(Form {
|
||
kind: FormKind::ImportLayout,
|
||
set,
|
||
component: parent,
|
||
rows,
|
||
prefix: true,
|
||
..Form::default()
|
||
}));
|
||
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
||
}
|
||
|
||
/// Rename & Renumber: number and title into the set, and the layout and
|
||
/// drawing file renamed when they changed.
|
||
fn apply_rename(&mut self, f: &Form) -> Result<(), String> {
|
||
let Some(db) = self.sheet_set.sets.get(f.set) else { return Ok(()) };
|
||
let old = db.layout_reference(&f.component, "Layout").unwrap_or_default();
|
||
let new_layout = f.layout_name.trim().to_string();
|
||
let new_file = if old.file_name.is_empty() || f.file_name.trim().is_empty() {
|
||
old.file_name.clone()
|
||
} else {
|
||
Path::new(&f.folder).join(f.file_name.trim()).to_string_lossy().to_string()
|
||
};
|
||
let file_changed = !old.file_name.is_empty() && ss::path_key(&new_file) != ss::path_key(&old.file_name);
|
||
let layout_changed = !old.name.is_empty() && !new_layout.is_empty() && new_layout != old.name;
|
||
let open_tab = self.tab_showing(Path::new(&old.file_name));
|
||
if file_changed {
|
||
if open_tab.is_some() {
|
||
return Err(crate::t!("The drawing is open; close it to rename its file.").into_owned());
|
||
}
|
||
if Path::new(&new_file).exists() {
|
||
return Err(crate::tf!("{} already exists.", new_file).into_owned());
|
||
}
|
||
}
|
||
if layout_changed {
|
||
match open_tab {
|
||
Some(k) => {
|
||
self.push_undo_snapshot(k, "LAYOUT RENAME");
|
||
let scene = &mut self.tabs[k].scene;
|
||
scene.rename_layout(&old.name, &new_layout);
|
||
if scene.current_layout == old.name {
|
||
scene.set_current_layout(new_layout.clone());
|
||
}
|
||
if let Some(mut link) = scene.document.sheet_set_data() {
|
||
link.layout_name = new_layout.clone();
|
||
scene.document.set_sheet_set_data(&link);
|
||
}
|
||
self.tabs[k].dirty = true;
|
||
}
|
||
None => {
|
||
// The closed drawing is read and written back whole: its
|
||
// previous file is kept as `.bak`, as a save over it does.
|
||
let path = Path::new(&old.file_name);
|
||
let mut scene = crate::scene::Scene::new();
|
||
scene.document = crate::io::load_file(path).map_err(|e| e.to_string())?;
|
||
scene.rename_layout(&old.name, &new_layout);
|
||
if let Some(mut link) = scene.document.sheet_set_data() {
|
||
link.layout_name = new_layout.clone();
|
||
scene.document.set_sheet_set_data(&link);
|
||
}
|
||
if self.backup_on_save {
|
||
crate::io::write_backup(path);
|
||
}
|
||
crate::io::save(&scene.document, path).map_err(|e| e.to_string())?;
|
||
}
|
||
}
|
||
}
|
||
if file_changed {
|
||
std::fs::rename(&old.file_name, &new_file).map_err(|e| e.to_string())?;
|
||
}
|
||
let Some(db) = self.sheet_set.sets.get_mut(f.set) else { return Ok(()) };
|
||
if let Some(el) = db.find_mut(&f.component) {
|
||
el.set_prop("Number", f.number.trim());
|
||
el.set_prop("Title", f.title.trim());
|
||
}
|
||
if layout_changed || file_changed {
|
||
db.set_layout_reference(
|
||
&f.component,
|
||
"Layout",
|
||
&LayoutReference {
|
||
file_name: ss::native_path(&new_file),
|
||
name: if layout_changed { new_layout } else { old.name },
|
||
handle: old.handle,
|
||
},
|
||
);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Rename & Renumber < Previous / Next >: apply, then the neighbouring sheet.
|
||
fn rename_step(&mut self, forward: bool) {
|
||
let Some(SsDialog::Form(f)) = self.sheet_set.dialog.take() else { return };
|
||
if f.kind == FormKind::RenameView {
|
||
let Some(db) = self.sheet_set.sets.get_mut(f.set) else { return };
|
||
if let Some(el) = db.find_mut(&f.component) {
|
||
if f.number.trim().is_empty() {
|
||
el.remove_named("Number");
|
||
} else {
|
||
el.set_prop("Number", f.number.trim());
|
||
}
|
||
el.set_prop("Title", f.title.trim());
|
||
}
|
||
let ids: Vec<String> = db.sheets().into_iter().flat_map(|s| db.sheet_views(s)).map(|v| v.id().to_string()).collect();
|
||
let k = ids.iter().position(|i| *i == f.component).unwrap_or(0);
|
||
let next = if forward { (k + 1).min(ids.len().saturating_sub(1)) } else { k.saturating_sub(1) };
|
||
let form = ids.get(next).map(|id| view_form(db, f.set, id));
|
||
self.sheet_set.current = Some(f.set);
|
||
self.save_current_sheet_set();
|
||
match form {
|
||
Some(form) => self.sheet_set.dialog = Some(SsDialog::Form(form)),
|
||
None => self.close_active_modal(),
|
||
}
|
||
return;
|
||
}
|
||
if let Err(e) = self.apply_rename(&f) {
|
||
self.sheet_set.dialog = Some(SsDialog::Form(Form { error: Some(e), ..f }));
|
||
return;
|
||
}
|
||
self.sheet_set.current = Some(f.set);
|
||
self.save_current_sheet_set();
|
||
let Some(db) = self.sheet_set.sets.get(f.set) else { return };
|
||
let ids: Vec<String> = db.sheets().iter().map(|s| s.id().to_string()).collect();
|
||
let k = ids.iter().position(|i| *i == f.component).unwrap_or(0);
|
||
let next = if forward { (k + 1).min(ids.len().saturating_sub(1)) } else { k.saturating_sub(1) };
|
||
let Some(id) = ids.get(next) else {
|
||
self.close_active_modal();
|
||
return;
|
||
};
|
||
let mut form = rename_form(db, f.set, id);
|
||
form.rename = f.rename;
|
||
follow_rename(&mut form);
|
||
self.sheet_set.selected = Some(id.clone());
|
||
self.sheet_set.dialog = Some(SsDialog::Form(form));
|
||
}
|
||
|
||
/// The View Category dialog for a new (`None`) or existing category.
|
||
fn open_category(&mut self, id: Option<String>) {
|
||
let (Some(set), Some(db)) = (self.sheet_set.current, self.sheet_set.db()) else { return };
|
||
let used = id.as_deref().map(|i| db.category_blocks(i));
|
||
let blocks = db
|
||
.callout_blocks()
|
||
.iter()
|
||
.map(|b| {
|
||
let label = format!("{} ({})", b.prop("Name").unwrap_or(""), db.resolve_file(b));
|
||
let on = used.as_ref().is_none_or(|u| u.iter().any(|x| x == b.id()));
|
||
(b.id().to_string(), label, on)
|
||
})
|
||
.collect();
|
||
let name = id.as_deref().and_then(|i| db.find(i)).and_then(|c| c.prop("Name")).unwrap_or("").to_string();
|
||
self.sheet_set.dialog = Some(SsDialog::Category(Category { set, id, name, blocks, removed: Vec::new(), error: None }));
|
||
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
||
}
|
||
|
||
fn category_ok(&mut self, mut c: Category) {
|
||
let name = c.name.trim().to_string();
|
||
if name.is_empty() {
|
||
c.error = Some(crate::t!("The category name cannot be empty.").into_owned());
|
||
self.sheet_set.dialog = Some(SsDialog::Category(c));
|
||
return;
|
||
}
|
||
self.close_active_modal();
|
||
let Some(db) = self.sheet_set.sets.get_mut(c.set) else { return };
|
||
// Blocks deleted in List of Blocks leave the set and every category.
|
||
if !c.removed.is_empty() {
|
||
let cats: Vec<(String, String, Vec<String>)> = db
|
||
.view_categories()
|
||
.iter()
|
||
.map(|k| (k.id().to_string(), k.prop("Name").unwrap_or("").to_string(), db.category_blocks(k.id())))
|
||
.collect();
|
||
for (id, name, blocks) in cats {
|
||
let kept: Vec<String> = blocks.into_iter().filter(|b| !c.removed.contains(b)).collect();
|
||
db.set_view_category(Some(&id), &name, &kept);
|
||
}
|
||
for id in &c.removed {
|
||
db.remove(id);
|
||
}
|
||
}
|
||
let mut used = Vec::new();
|
||
for (id, _, on) in &c.blocks {
|
||
let id = match id.strip_prefix("new:") {
|
||
Some(spec) => {
|
||
let mut p = spec.splitn(3, '|');
|
||
let (file, block, handle) = (p.next().unwrap_or(""), p.next().unwrap_or(""), p.next().unwrap_or(""));
|
||
match db.add_callout_block(file, block, handle) {
|
||
Some(id) => id,
|
||
None => continue,
|
||
}
|
||
}
|
||
None => id.clone(),
|
||
};
|
||
if *on {
|
||
used.push(id);
|
||
}
|
||
}
|
||
db.set_view_category(c.id.as_deref(), &name, &used);
|
||
self.sheet_set.current = Some(c.set);
|
||
self.save_current_sheet_set();
|
||
}
|
||
|
||
/// A drawing read and cached, or the task that reads it off the UI thread
|
||
/// and then handles `then` again.
|
||
fn drawing_or_read(
|
||
&mut self,
|
||
path: &str,
|
||
then: SheetSetMsg,
|
||
) -> Result<std::sync::Arc<codec::CadDocument>, Task<Message>> {
|
||
if let Some(doc) = self.sheet_set.drawings.get(path) {
|
||
return Ok(doc);
|
||
}
|
||
let path = path.to_string();
|
||
Err(crate::app::update::background_task(
|
||
crate::t!("Sheet Set Manager"),
|
||
{
|
||
let path = path.clone();
|
||
move || crate::ui::window::sheet_set::read_drawing(&path)
|
||
},
|
||
move |read| Message::SheetSet(SheetSetMsg::DrawingRead(path, read, Box::new(then))),
|
||
))
|
||
}
|
||
|
||
/// A block preview (`name` empty: the drawing's model space) of a cached
|
||
/// drawing, cached by `file|name`; returns its key.
|
||
fn block_preview(&mut self, file: &str, name: &str) -> Option<String> {
|
||
let key = format!("{}|{}", ss::path_key(file), name);
|
||
if self.sheet_set.previews.contains_key(&key) {
|
||
return Some(key);
|
||
}
|
||
// The cached drawing is lent to a scene for tessellation, not copied.
|
||
let (modified, doc) = self.sheet_set.drawings.take(file)?;
|
||
let mut scene = crate::scene::Scene::new();
|
||
scene.document = std::sync::Arc::try_unwrap(doc).unwrap_or_else(|doc| (*doc).clone());
|
||
let wires = if name.is_empty() {
|
||
let model = scene.document.header.model_space_block_handle;
|
||
let entities: Vec<codec::EntityType> = scene
|
||
.document
|
||
.entities()
|
||
.filter(|e| e.common().owner_handle == model)
|
||
.filter(|e| !matches!(e, codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_)))
|
||
.take(20_000)
|
||
.cloned()
|
||
.collect();
|
||
scene.wires_for_entities(&entities)
|
||
} else {
|
||
scene.block_preview_wires(name)
|
||
};
|
||
let doc = std::mem::take(&mut scene.document);
|
||
self.sheet_set.drawings.insert(file, modified, std::sync::Arc::new(doc));
|
||
self.sheet_set.previews.insert(key.clone(), wires);
|
||
Some(key)
|
||
}
|
||
|
||
/// The file and block name behind a List of Blocks id.
|
||
fn listed_block(&self, set: usize, id: &str) -> Option<(String, String)> {
|
||
if let Some(spec) = id.strip_prefix("new:") {
|
||
let mut p = spec.splitn(3, '|');
|
||
return Some((p.next()?.to_string(), p.next()?.to_string()));
|
||
}
|
||
let db = self.sheet_set.sets.get(set)?;
|
||
let b = db.find(id)?;
|
||
Some((db.resolve_file(b), b.prop("Name").unwrap_or("").to_string()))
|
||
}
|
||
|
||
/// Named model views of a drawing (read once, when its row is opened);
|
||
/// `Err` reads the drawing first.
|
||
fn load_model_views(&mut self, path: &str) -> Result<(), Task<Message>> {
|
||
let key = ss::path_key(path);
|
||
if self.sheet_set.model_views.contains_key(&key) {
|
||
return Ok(());
|
||
}
|
||
let doc = self.drawing_or_read(path, SheetSetMsg::SeeViews(path.to_string()))?;
|
||
let views = doc.views.iter().filter(|v| !v.paper_space).map(|v| v.name.clone()).collect();
|
||
self.sheet_set.model_views.insert(key, views);
|
||
Ok(())
|
||
}
|
||
|
||
/// The current set's sheet shown by the active tab at its current layout.
|
||
fn current_sheet(&self) -> Option<(usize, String)> {
|
||
let set = self.sheet_set.current?;
|
||
let db = self.sheet_set.db()?;
|
||
let tab = &self.tabs[self.active_tab];
|
||
let path = tab.current_path.as_ref()?.to_string_lossy().to_string();
|
||
let layout = tab.scene.current_layout.clone();
|
||
let sheet = db.sheet_for(&path, Some(&layout))?;
|
||
sheet
|
||
.named("Layout")
|
||
.and_then(|r| r.prop("Name"))
|
||
.is_some_and(|n| n.eq_ignore_ascii_case(&layout))
|
||
.then(|| (set, sheet.id().to_string()))
|
||
}
|
||
|
||
/// Start the insertion point of Place on Sheet / a callout or label block.
|
||
fn start_placing(&mut self, placing: Placing, size: (f64, f64)) {
|
||
let i = self.active_tab;
|
||
let scales: Vec<(String, f64)> = if size.0 > 0.0 {
|
||
self.tabs[i]
|
||
.scene
|
||
.scale_list()
|
||
.into_iter()
|
||
.filter(|(_, _, factor)| *factor > 0.0)
|
||
.map(|(name, _, factor)| (name, factor))
|
||
.collect()
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
self.sheet_set.placing = Some(placing);
|
||
let command = SheetSetPlaceCommand { size, scale: 1.0, scales };
|
||
self.command_line.push_info(&command.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||
}
|
||
|
||
/// Place on Sheet: only on a sheet of this set open at its layout.
|
||
/// The model drawing and the set's view label block drawing are read
|
||
/// first.
|
||
fn start_place_view(&mut self, drawing: String, view: Option<String>) -> Task<Message> {
|
||
if self.tabs[self.active_tab].scene.current_layout.eq_ignore_ascii_case("Model") {
|
||
self.command_line.push_error(
|
||
crate::t!("Content cannot be placed into model space. Please switch to paper space and try again.")
|
||
.as_ref(),
|
||
);
|
||
return Task::none();
|
||
}
|
||
let Some((set, sheet)) = self.current_sheet() else {
|
||
self.command_line
|
||
.push_error(crate::t!("Open a sheet of this sheet set at its layout to place a view on it.").as_ref());
|
||
return Task::none();
|
||
};
|
||
let again = || SheetSetMsg::PlaceOnSheet(drawing.clone(), view.clone());
|
||
let label = self.sheet_set.db().and_then(|db| db.sheet_set().named("DefLabelBlk").map(|b| db.resolve_file(b)));
|
||
if let Some(label) = label.filter(|l| Path::new(l).is_file()) {
|
||
if let Err(task) = self.drawing_or_read(&label, again()) {
|
||
return task;
|
||
}
|
||
}
|
||
let size = match self.drawing_or_read(&drawing, again()) {
|
||
Ok(doc) => model_view(&doc, view.as_deref()).map(|v| (v.width, v.height)),
|
||
Err(task) => return task,
|
||
};
|
||
let Some(size) = size else {
|
||
self.command_line.push_error(crate::tf!("{}: no view to place.", drawing).as_ref());
|
||
return Task::none();
|
||
};
|
||
self.start_placing(Placing::View { set, sheet, drawing, view }, size);
|
||
Task::none()
|
||
}
|
||
|
||
/// A sheet view's commands.
|
||
fn view_menu(&mut self, id: String, action: ViewAction) -> Task<Message> {
|
||
let Some(set) = self.sheet_set.current else { return Task::none() };
|
||
let Some(db) = self.sheet_set.db() else { return Task::none() };
|
||
let Some(sheet) = db.parent_of(&id).and_then(|v| db.parent_of(v.id())).map(|s| s.id().to_string()) else {
|
||
return Task::none();
|
||
};
|
||
let view_name = db.find(&id).and_then(|v| v.named("NamedView")).and_then(|r| r.prop("Name")).unwrap_or("").to_string();
|
||
match action {
|
||
ViewAction::Display => {
|
||
let open = db.layout_reference(&sheet, "Layout").map(|r| self.tab_showing(Path::new(&r.file_name)).is_some()).unwrap_or(false);
|
||
let task = self.open_sheet(&sheet);
|
||
if open {
|
||
return task.chain(Task::done(Message::Command(format!("VIEW RESTORE {view_name}"))));
|
||
}
|
||
self.sheet_set.pending_view = Some(view_name);
|
||
return task;
|
||
}
|
||
ViewAction::Rename => {
|
||
self.sheet_set.dialog = Some(SsDialog::Form(view_form(db, set, &id)));
|
||
self.active_modal = Some(crate::app::ModalKind::SheetSet);
|
||
}
|
||
ViewAction::Category(category) => {
|
||
if let Some(db) = self.sheet_set.db_mut() {
|
||
if let Some(v) = db.find_mut(&id) {
|
||
let mut r = ss::object("AcSmObjectReference", ss::CLSID_OBJECT_REFERENCE, Some("Category"));
|
||
r.set_prop_vt("ReferencedObject", -1, &category);
|
||
v.put_named(r);
|
||
}
|
||
}
|
||
self.save_current_sheet_set();
|
||
}
|
||
ViewAction::Callout(_) | ViewAction::Label if self.tabs[self.active_tab].scene.current_layout.eq_ignore_ascii_case("Model") => {
|
||
self.command_line
|
||
.push_error(crate::t!("Content cannot be placed into model space. Please switch to paper space and try again.").as_ref());
|
||
}
|
||
ViewAction::Callout(_) | ViewAction::Label if self.current_sheet().map(|s| s.1) != Some(sheet.clone()) => {
|
||
self.command_line
|
||
.push_error(crate::t!("Open the view's sheet at its layout to place a block on it.").as_ref());
|
||
}
|
||
ViewAction::Callout(_) | ViewAction::Label => {
|
||
let b = match &action {
|
||
ViewAction::Callout(block) => db.find(block),
|
||
_ => db.sheet_set().named("DefLabelBlk"),
|
||
};
|
||
let Some(b) = b else {
|
||
if action == ViewAction::Label {
|
||
self.command_line.push_error(crate::t!("The sheet set has no label block for views.").as_ref());
|
||
}
|
||
return Task::none();
|
||
};
|
||
let (file, name) = (db.resolve_file(b), b.prop("Name").unwrap_or("").to_string());
|
||
// The block's drawing is read first.
|
||
if let Err(task) = self.drawing_or_read(&file, SheetSetMsg::ViewMenu(id.clone(), action)) {
|
||
return task;
|
||
}
|
||
self.start_placing(Placing::Block { set, sheet, view: id, file, name }, (0.0, 0.0));
|
||
}
|
||
}
|
||
Task::none()
|
||
}
|
||
|
||
/// Bring block `name` of drawing `file` into tab `i` (when missing) and
|
||
/// insert it at `at` with its attributes; their `?View.` / `?Sheet.`
|
||
/// fields become the view's and sheet's navigation fields.
|
||
fn insert_set_block(&mut self, i: usize, file: &str, name: &str, at: codec::types::Vector3, view: &str, sheet: &str, set: usize) {
|
||
let Some(source) = self.sheet_set.drawings.get(file) else {
|
||
self.command_line.push_error(crate::tf!("{}: cannot be read.", file).as_ref());
|
||
return;
|
||
};
|
||
let probe = codec::EntityType::Insert(codec::entities::Insert::new(name.to_string(), codec::types::Vector3::ZERO));
|
||
let deps = crate::app::ClipboardDeps::capture(&source, std::slice::from_ref(&probe));
|
||
self.merge_dependencies(i, &deps);
|
||
for def in deps.blocks {
|
||
let scene = &mut self.tabs[i].scene;
|
||
if scene.document.block_records.get(&def.name).is_none() {
|
||
scene.define_block_raw(&def.name, def.base_point, def.entities);
|
||
}
|
||
}
|
||
let units = source.block_records.get(name).map(|b| b.units).filter(|u| *u != 0).unwrap_or(source.header.insertion_units);
|
||
let host = self.tabs[i].scene.document.header.insertion_units;
|
||
let k = crate::app::properties::insert_unit_scale(host, units).unwrap_or(1.0);
|
||
let mut insert = codec::entities::Insert::new(name.to_string(), at).with_scale(k, k, k);
|
||
let transform = insert.get_transform();
|
||
let doc = &self.tabs[i].scene.document;
|
||
let attdefs: Vec<codec::entities::AttributeDefinition> = doc
|
||
.block_records
|
||
.get(name)
|
||
.map(|br| {
|
||
br.entity_handles
|
||
.iter()
|
||
.filter_map(|h| match doc.get_entity(*h) {
|
||
Some(codec::EntityType::AttributeDefinition(d)) if !d.flags.constant => Some(d.clone()),
|
||
_ => None,
|
||
})
|
||
.collect()
|
||
})
|
||
.unwrap_or_default();
|
||
for d in &attdefs {
|
||
let mut a = codec::entities::AttributeEntity::from_definition(d, None);
|
||
a.apply_transform(&transform);
|
||
insert.attributes.push(a);
|
||
}
|
||
let handle = self.tabs[i].scene.add_entity(codec::EntityType::Insert(insert));
|
||
let Some(db) = self.sheet_set.sets.get(set) else { return };
|
||
let (view_prefix, sheet_prefix) = (component_prefix(db, view), component_prefix(db, sheet));
|
||
crate::entities::field::attach_attribute_fields_mapped(&mut self.tabs[i].scene.document, handle, &|code: &str| {
|
||
code.replace("?View.", &view_prefix).replace("?Sheet.", &sheet_prefix)
|
||
});
|
||
self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||
}
|
||
|
||
/// The insertion point was picked: place the view (xref, viewport, paper
|
||
/// named view, view label, sheet view record) or the block.
|
||
fn place_at(&mut self, i: usize, x: f64, y: f64, scale: f64) {
|
||
use codec::types::Vector3;
|
||
let Some(placing) = self.sheet_set.placing.take() else { return };
|
||
self.push_undo_snapshot(i, "Place on Sheet");
|
||
match placing {
|
||
Placing::Block { set, sheet, view, file, name } => {
|
||
self.insert_set_block(i, &file, &name, Vector3::new(x, y, 0.0), &view, &sheet, set);
|
||
}
|
||
Placing::View { set, sheet, drawing, view } => {
|
||
let Some(mv) = self.sheet_set.drawings.get(&drawing).and_then(|d| model_view(&d, view.as_deref())) else {
|
||
return;
|
||
};
|
||
let title = view.clone().unwrap_or_else(|| {
|
||
Path::new(&drawing).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default()
|
||
});
|
||
// The model drawing is attached to the sheet's model space.
|
||
let host = self.tabs[i].current_path.clone();
|
||
let request = crate::modules::insert::xattach::XrefAttachRequest {
|
||
path: drawing.clone(),
|
||
overlay: false,
|
||
path_type: crate::io::xref_model::Pathtype::Relative,
|
||
};
|
||
let scene = &mut self.tabs[i].scene;
|
||
let xref = crate::modules::insert::xattach::prepare_xref_definition(scene, &request, host.as_deref());
|
||
let model = scene.document.header.model_space_block_handle;
|
||
let inserted = scene.document.entities().any(|e| {
|
||
e.common().owner_handle == model && matches!(e, codec::EntityType::Insert(ins) if ins.block_name.eq_ignore_ascii_case(&xref))
|
||
});
|
||
if !inserted {
|
||
let saved = std::mem::replace(&mut scene.current_layout, "Model".to_string());
|
||
scene.add_entity(codec::EntityType::Insert(codec::entities::Insert::new(xref, Vector3::ZERO)));
|
||
scene.current_layout = saved;
|
||
}
|
||
// The viewport: lower-left corner at the point, the view at the scale.
|
||
let (w, h) = (mv.width * scale, mv.height * scale);
|
||
let mut vp = codec::entities::Viewport::new();
|
||
vp.center = Vector3::new(x + w / 2.0, y + h / 2.0, 0.0);
|
||
vp.width = w;
|
||
vp.height = h;
|
||
vp.id = scene.next_viewport_id();
|
||
vp.view_target = mv.target.clone();
|
||
vp.view_direction = mv.direction.clone();
|
||
// ponytail: plan views only (the DCS offset is the world XY
|
||
// offset); a twisted or 3D view needs the view basis here.
|
||
vp.view_center = Vector3::new(mv.center.x - mv.target.x, mv.center.y - mv.target.y, 0.0);
|
||
vp.view_height = mv.height;
|
||
vp.custom_scale = scale;
|
||
scene.add_entity(codec::EntityType::Viewport(vp));
|
||
// A paper-space named view of the same name frames it.
|
||
let mut named = codec::tables::View::new(title.as_str());
|
||
named.handle = scene.document.allocate_handle();
|
||
named.paper_space = true;
|
||
named.center = Vector3::new(x + w / 2.0, y + h / 2.0, 0.0);
|
||
named.width = w;
|
||
named.height = h;
|
||
named.direction = Vector3::new(0.0, 0.0, 1.0);
|
||
let view_handle = format!("{:X}", named.handle.value());
|
||
scene.document.views.add_or_replace(named);
|
||
let file = host.map(|p| ss::native_path(&p.to_string_lossy())).unwrap_or_default();
|
||
let Some(db) = self.sheet_set.sets.get_mut(set) else { return };
|
||
// The view joins the set's default (unnamed) category.
|
||
let category = db
|
||
.sheet_set()
|
||
.named("ViewCategories")
|
||
.and_then(|c| c.children.iter().find(|k| k.name == "AcSmViewCategory" && k.prop("Name").is_none()))
|
||
.map(|k| k.id().to_string());
|
||
let reference = LayoutReference { file_name: file, name: title.clone(), handle: view_handle };
|
||
let Some(view_id) = db.add_sheet_view(&sheet, category.as_deref(), &reference, &title) else { return };
|
||
let label = db.sheet_set().named("DefLabelBlk").map(|b| (db.resolve_file(b), b.prop("Name").unwrap_or("").to_string()));
|
||
// The set is written first, so the label's fields find the new view.
|
||
self.sheet_set.current = Some(set);
|
||
self.save_current_sheet_set();
|
||
if let Some((file, name)) = label {
|
||
self.insert_set_block(i, &file, &name, Vector3::new(x, y, 0.0), &view_id, &sheet, set);
|
||
}
|
||
}
|
||
}
|
||
self.tabs[i].dirty = true;
|
||
self.refresh_xref_manager();
|
||
}
|
||
|
||
/// Select Block: read a drawing's named blocks.
|
||
fn select_block_file(&mut self, path: PathBuf) -> Task<Message> {
|
||
let file = path.to_string_lossy().to_string();
|
||
let doc = match self.drawing_or_read(&file, SheetSetMsg::SelectBlockPicked(Some(path))) {
|
||
Ok(doc) => doc,
|
||
Err(task) => return task,
|
||
};
|
||
let mut names: Vec<(String, String, bool)> = doc
|
||
.block_records
|
||
.iter()
|
||
.filter(|b| !b.name.starts_with('*') && !b.is_layout())
|
||
.map(|b| (b.name.clone(), format!("{:X}", b.handle.value()), false))
|
||
.collect();
|
||
names.sort();
|
||
drop(doc);
|
||
// The whole drawing is the first preview.
|
||
let preview = self.block_preview(&file, "");
|
||
if let Some(SsDialog::SelectBlock(b)) = self.sheet_set.dialog.as_mut() {
|
||
b.file = file;
|
||
b.names = names;
|
||
b.preview = preview;
|
||
b.error = None;
|
||
}
|
||
Task::none()
|
||
}
|
||
|
||
/// Model Views: each location folder of the current set and its drawings.
|
||
pub(in crate::app) fn refresh_locations(&mut self) {
|
||
let Some(db) = self.sheet_set.db() else {
|
||
self.sheet_set.locations.clear();
|
||
return;
|
||
};
|
||
self.sheet_set.locations = db
|
||
.sheet_set()
|
||
.named("Resources")
|
||
.map(|r| {
|
||
r.children
|
||
.iter()
|
||
.map(|c| {
|
||
let folder = db.resolve_file(c);
|
||
let mut files: Vec<String> = std::fs::read_dir(&folder)
|
||
.map(|d| {
|
||
d.filter_map(|e| e.ok())
|
||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||
.filter(|n| n.to_ascii_lowercase().ends_with(".dwg"))
|
||
.collect()
|
||
})
|
||
.unwrap_or_default();
|
||
files.sort_by_key(|n| n.to_lowercase());
|
||
(c.id().to_string(), folder, files)
|
||
})
|
||
.collect()
|
||
})
|
||
.unwrap_or_default();
|
||
}
|
||
|
||
/// SSMSHEETSTATUS: a sheet whose drawing is missing, or open (its `.dwl`
|
||
/// lock file, or a tab of this application).
|
||
pub(in crate::app) fn refresh_sheet_status(&mut self) {
|
||
self.sheet_set.status_at = Some(iced::time::Instant::now());
|
||
self.sheet_set.status.clear();
|
||
if self.sheet_set.settings.sheet_status == 0 {
|
||
return;
|
||
}
|
||
let Some(db) = self.sheet_set.db() else { return };
|
||
let folder = db.path.as_deref().and_then(|p| Path::new(p).parent().map(Path::to_path_buf));
|
||
let mut status = std::collections::HashMap::new();
|
||
for sheet in db.sheets() {
|
||
let Some(r) = db.layout_reference(sheet.id(), "Layout") else { continue };
|
||
let path = Path::new(&r.file_name);
|
||
// The reference finds a sheet through its relative path: when that
|
||
// names no file the sheet is missing, even if the stored absolute
|
||
// path still exists.
|
||
let relative = sheet.named("Layout").and_then(|l| l.prop("Relative_FileName")).zip(folder.as_deref());
|
||
let found = match relative {
|
||
Some((rel, folder)) => folder.join(rel.replace('\\', std::path::MAIN_SEPARATOR_STR)).exists(),
|
||
None => path.exists(),
|
||
};
|
||
let state = if !found {
|
||
Some(SheetStatus::Missing)
|
||
} else if path.with_extension("dwl").exists() {
|
||
Some(SheetStatus::Locked)
|
||
} else {
|
||
None
|
||
};
|
||
if let Some(st) = state {
|
||
status.insert(sheet.id().to_string(), st);
|
||
}
|
||
}
|
||
self.sheet_set.status = status;
|
||
}
|
||
|
||
fn properties_template(&mut self, path: PathBuf) -> Task<Message> {
|
||
let file = path.to_string_lossy().to_string();
|
||
let layouts = match self.drawing_or_read(&file, SheetSetMsg::TemplatePicked(Some(path))) {
|
||
Ok(doc) => paper_layouts(&doc),
|
||
Err(task) => return task,
|
||
};
|
||
if let Some(SsDialog::Template(t)) = self.sheet_set.dialog.as_mut() {
|
||
t.file = file;
|
||
t.layouts = layouts.into_iter().map(|(n, _)| n).collect();
|
||
t.layout = 0;
|
||
return Task::none();
|
||
}
|
||
let Some(SsDialog::Properties(p)) = self.sheet_set.dialog.as_mut() else {
|
||
return Task::none();
|
||
};
|
||
match layouts.first() {
|
||
Some((name, _)) => {
|
||
if let Some(row) = p.rows.iter_mut().find(|r| r.key == RowKey::Template) {
|
||
row.value = format!("{name} ({file})");
|
||
}
|
||
p.template = Some((file, name.clone()));
|
||
p.error = None;
|
||
}
|
||
None => p.error = Some(crate::t!("The drawing has no layout to use as a sheet template.").into_owned()),
|
||
}
|
||
Task::none()
|
||
}
|
||
|
||
fn sheet_set_dialog_ok(&mut self) -> Task<Message> {
|
||
match self.sheet_set.dialog.take() {
|
||
Some(SsDialog::Wizard(w)) => self.finish_wizard(w),
|
||
Some(SsDialog::Properties(p)) => {
|
||
self.apply_properties(p);
|
||
Task::none()
|
||
}
|
||
Some(SsDialog::Form(f)) => self.form_ok(f),
|
||
Some(SsDialog::Confirm(set, id, _)) => {
|
||
self.close_active_modal();
|
||
if let Some(db) = self.sheet_set.sets.get_mut(set) {
|
||
db.remove(&id);
|
||
}
|
||
self.sheet_set.current = Some(set);
|
||
self.sheet_set.selected = None;
|
||
self.save_current_sheet_set();
|
||
Task::none()
|
||
}
|
||
Some(SsDialog::Template(t)) => {
|
||
let mut form = t.form;
|
||
if let Some(layout) = t.layouts.get(t.layout) {
|
||
form.drawing = format!("{layout} ({})", t.file);
|
||
form.template = Some((t.file, layout.clone()));
|
||
}
|
||
self.sheet_set.dialog = Some(SsDialog::Form(form));
|
||
Task::none()
|
||
}
|
||
Some(SsDialog::Category(c)) => {
|
||
self.category_ok(c);
|
||
Task::none()
|
||
}
|
||
// List of Blocks: the list goes back to the View Category dialog.
|
||
Some(SsDialog::BlockList(b)) => {
|
||
let mut c = *b.category;
|
||
for (id, _, _) in &c.blocks {
|
||
if !id.starts_with("new:") && !b.blocks.iter().any(|(k, _)| k == id) {
|
||
c.removed.push(id.clone());
|
||
}
|
||
}
|
||
c.blocks = b
|
||
.blocks
|
||
.into_iter()
|
||
.map(|(id, label)| {
|
||
let on = c.blocks.iter().find(|(k, _, _)| *k == id).is_none_or(|(_, _, on)| *on);
|
||
(id, label, on)
|
||
})
|
||
.collect();
|
||
self.sheet_set.dialog = Some(SsDialog::Category(c));
|
||
Task::none()
|
||
}
|
||
// Select Block: the whole drawing or its checked blocks join the list.
|
||
Some(SsDialog::SelectBlock(mut b)) => {
|
||
if b.file.is_empty() {
|
||
b.error = Some(crate::t!("Enter the drawing file name.").into_owned());
|
||
self.sheet_set.dialog = Some(SsDialog::SelectBlock(b));
|
||
return Task::none();
|
||
}
|
||
let file = ss::native_path(&b.file);
|
||
let mut list = *b.list;
|
||
if b.whole {
|
||
let stem = Path::new(&file).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
|
||
list.blocks.push((format!("new:{file}|{stem}|"), format!("{stem} ({file})")));
|
||
} else {
|
||
for (name, handle, _) in b.names.into_iter().filter(|n| n.2) {
|
||
list.blocks.push((format!("new:{file}|{name}|{handle}"), format!("{name} ({file})")));
|
||
}
|
||
}
|
||
self.sheet_set.dialog = Some(SsDialog::BlockList(list));
|
||
Task::none()
|
||
}
|
||
None => Task::none(),
|
||
}
|
||
}
|
||
|
||
fn finish_wizard(&mut self, mut w: Wizard) -> Task<Message> {
|
||
sync_wizard_draft(&mut w);
|
||
let path = crate::ui::window::sheet_set::wizard_dst_path(&w);
|
||
if w.name.trim().is_empty() || !Path::new(&w.folder).is_dir() {
|
||
w.error = Some(crate::t!("The folder for the sheet set data file does not exist.").into_owned());
|
||
self.sheet_set.dialog = Some(SsDialog::Wizard(w));
|
||
return Task::none();
|
||
}
|
||
if Path::new(&path).exists() {
|
||
w.error = Some(crate::tf!("{} already exists.", path).into_owned());
|
||
self.sheet_set.dialog = Some(SsDialog::Wizard(w));
|
||
return Task::none();
|
||
}
|
||
let mut db = w.draft.clone();
|
||
db.path = Some(path.clone());
|
||
let set = db.sheet_set().id().to_string();
|
||
// Written again now the `.dst` has a place, so it carries its relative path.
|
||
let location = db
|
||
.file_reference(&set, "NewSheetLocation")
|
||
.filter(|f| !f.is_empty())
|
||
.unwrap_or_else(|| w.folder.clone());
|
||
db.set_file_reference(&set, "NewSheetLocation", &location);
|
||
// Existing drawings: a subset per folder, a sheet per chosen layout.
|
||
if w.existing {
|
||
for folder in &w.folders {
|
||
let chosen: Vec<&FoundLayout> = w.layouts.iter().filter(|l| l.on && &l.folder == folder).collect();
|
||
if chosen.is_empty() {
|
||
continue;
|
||
}
|
||
let name = Path::new(folder).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default();
|
||
let Some(sub) = db.add_subset(&set, &name, "") else {
|
||
continue;
|
||
};
|
||
if w.hierarchy {
|
||
db.set_file_reference(&sub, "NewSheetLocation", folder);
|
||
}
|
||
for l in chosen {
|
||
let stem = Path::new(&l.file).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
|
||
if let Some(sheet) = db.add_sheet(&sub, "", &format!("{stem} - {}", l.layout), "") {
|
||
db.set_layout_reference(
|
||
&sheet,
|
||
"Layout",
|
||
&LayoutReference { file_name: l.file.clone(), name: l.layout.clone(), handle: l.handle.clone() },
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if let Err(e) = write_set(&mut db, &path) {
|
||
w.error = Some(e);
|
||
self.sheet_set.dialog = Some(SsDialog::Wizard(w));
|
||
return Task::none();
|
||
}
|
||
self.close_active_modal();
|
||
if let Err(e) = self.open_sheet_set(Path::new(&path), true) {
|
||
self.command_line.push_error(&e);
|
||
}
|
||
Task::none()
|
||
}
|
||
|
||
fn apply_properties(&mut self, mut p: Properties) {
|
||
// Back to the wizard: the draft takes the edits.
|
||
if let Some(mut w) = p.wizard.take() {
|
||
write_properties(&mut w.draft, &p);
|
||
let set = w.draft.sheet_set();
|
||
w.name = set.prop("Name").unwrap_or("").to_string();
|
||
w.description = set.prop("Desc").unwrap_or("").to_string();
|
||
self.sheet_set.dialog = Some(SsDialog::Wizard(*w));
|
||
return;
|
||
}
|
||
self.close_active_modal();
|
||
let Some(db) = p.set.and_then(|k| self.sheet_set.sets.get_mut(k)) else {
|
||
return;
|
||
};
|
||
write_properties(db, &p);
|
||
self.sheet_set.current = p.set;
|
||
self.save_current_sheet_set();
|
||
}
|
||
|
||
fn form_ok(&mut self, mut f: Form) -> Task<Message> {
|
||
let Some(db) = self.sheet_set.sets.get_mut(f.set) else {
|
||
self.close_active_modal();
|
||
return Task::none();
|
||
};
|
||
match f.kind {
|
||
FormKind::Rename => {
|
||
if let Err(e) = self.apply_rename(&f) {
|
||
f.error = Some(e);
|
||
self.sheet_set.dialog = Some(SsDialog::Form(f));
|
||
return Task::none();
|
||
}
|
||
}
|
||
FormKind::NewSubset => {
|
||
let name = f.title.trim().to_string();
|
||
if name.is_empty() {
|
||
f.error = Some(crate::t!("The subset name cannot be empty.").into_owned());
|
||
self.sheet_set.dialog = Some(SsDialog::Form(f));
|
||
return Task::none();
|
||
}
|
||
let parent_folder = sheet_folder_of(db, &f.component);
|
||
if let Some(sub) = db.add_subset(&f.component, &name, "New subset added") {
|
||
// Folder hierarchy: the parent's location + the subset name, created now.
|
||
let folder = if f.hierarchy {
|
||
let folder = Path::new(&parent_folder).join(&name);
|
||
let _ = std::fs::create_dir_all(&folder);
|
||
folder.to_string_lossy().to_string()
|
||
} else {
|
||
f.folder.trim().to_string()
|
||
};
|
||
if !folder.is_empty() {
|
||
db.set_file_reference(&sub, "NewSheetLocation", &folder);
|
||
}
|
||
if !f.publish {
|
||
if let Some(el) = db.find_mut(&sub) {
|
||
el.set_prop_vt("OverrideSheetPublish", 2, "-1");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
FormKind::ImportLayout => {
|
||
let chosen: Vec<&ImportRow> = f.rows.iter().filter(|r| r.on && !r.taken).collect();
|
||
if chosen.is_empty() {
|
||
f.error = Some(crate::t!("The drawing has no layout to import.").into_owned());
|
||
self.sheet_set.dialog = Some(SsDialog::Form(f));
|
||
return Task::none();
|
||
}
|
||
for r in chosen {
|
||
let stem = Path::new(&r.drawing).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default();
|
||
let title = if f.prefix { format!("{stem} - {}", r.layout) } else { r.layout.clone() };
|
||
if let Some(sheet) = db.add_sheet(&f.component, "", &title, "") {
|
||
db.set_layout_reference(
|
||
&sheet,
|
||
"Layout",
|
||
&LayoutReference { file_name: r.drawing.clone(), name: r.layout.clone(), handle: r.handle.clone() },
|
||
);
|
||
}
|
||
}
|
||
}
|
||
FormKind::NewSheet => return self.create_sheet(f),
|
||
FormKind::RenameView => {
|
||
if let Some(el) = db.find_mut(&f.component) {
|
||
if f.number.trim().is_empty() {
|
||
el.remove_named("Number");
|
||
} else {
|
||
el.set_prop("Number", f.number.trim());
|
||
}
|
||
el.set_prop("Title", f.title.trim());
|
||
}
|
||
}
|
||
}
|
||
self.close_active_modal();
|
||
self.sheet_set.current = Some(f.set);
|
||
self.save_current_sheet_set();
|
||
Task::none()
|
||
}
|
||
|
||
/// New Sheet: a drawing in the sheet folder holding one layout, named
|
||
/// "Number Title", from the sheet creation template (a new drawing's
|
||
/// Layout1 when the set names none); then the sheet and its layout
|
||
/// reference, and the drawing opens at it.
|
||
fn create_sheet(&mut self, mut f: Form) -> Task<Message> {
|
||
let fail = |app: &mut Self, mut f: Form, e: String| {
|
||
f.error = Some(e);
|
||
app.sheet_set.dialog = Some(SsDialog::Form(f));
|
||
Task::none()
|
||
};
|
||
let title = f.title.trim().to_string();
|
||
if title.is_empty() {
|
||
return fail(self, f, crate::t!("The sheet title cannot be empty.").into_owned());
|
||
}
|
||
let stem = if f.file_name.trim().is_empty() {
|
||
format!("{} {}", f.number.trim(), title).trim().to_string()
|
||
} else {
|
||
f.file_name.trim().trim_end_matches(".dwg").to_string()
|
||
};
|
||
let folder = PathBuf::from(f.folder.trim());
|
||
if std::fs::create_dir_all(&folder).is_err() {
|
||
return fail(self, f, crate::t!("The sheet folder cannot be created.").into_owned());
|
||
}
|
||
let path = folder.join(format!("{stem}.dwg"));
|
||
if path.exists() {
|
||
return fail(self, f, crate::tf!("{} already exists.", path.display()).into_owned());
|
||
}
|
||
let layout_name = format!("{} {}", f.number.trim(), title).trim().to_string();
|
||
let template = match f.template.clone() {
|
||
Some((file_name, name)) => Some(LayoutReference { file_name, name, handle: String::new() }),
|
||
None => self.sheet_set.sets.get(f.set).and_then(|db| template_of(db, &f.component)),
|
||
};
|
||
let mut scene = crate::scene::Scene::new();
|
||
let source_layout = match template.as_ref().and_then(|t| crate::io::load_file(Path::new(&t.file_name)).ok().map(|d| (d, t.name.clone()))) {
|
||
Some((doc, name)) => {
|
||
scene.document = doc;
|
||
name
|
||
}
|
||
None => {
|
||
scene.populate_new_drawing_defaults();
|
||
paper_layouts(&scene.document).into_iter().next().map(|(n, _)| n).unwrap_or_else(|| "Layout1".into())
|
||
}
|
||
};
|
||
for (name, _) in paper_layouts(&scene.document) {
|
||
if !name.eq_ignore_ascii_case(&source_layout) {
|
||
scene.delete_layout(&name);
|
||
}
|
||
}
|
||
scene.rename_layout(&source_layout, &layout_name);
|
||
let handle = paper_layouts(&scene.document)
|
||
.into_iter()
|
||
.find(|(n, _)| *n == layout_name)
|
||
.map(|(_, h)| h)
|
||
.unwrap_or_default();
|
||
f.number = f.number.trim().to_string();
|
||
let Some(db) = self.sheet_set.sets.get(f.set) else {
|
||
return Task::none();
|
||
};
|
||
// The drawing is saved first: a failed save leaves no sheet in the set.
|
||
// It records the revision the set's next write counts up to.
|
||
let dst = db.path.clone().unwrap_or_default();
|
||
let file = ss::native_path(&path.to_string_lossy());
|
||
scene.document.set_sheet_set_data(&SheetSetData {
|
||
layout_handle: handle.to_lowercase(),
|
||
layout_name: layout_name.clone(),
|
||
sheet_dwg_name: file.clone(),
|
||
sheet_set_file_name: ss::native_path(&dst),
|
||
sheet_set_version: db.file_revision() + 1,
|
||
update_count: 1,
|
||
update_time: utc_stamp(),
|
||
});
|
||
crate::entities::field::stamp_save_dates(&mut scene.document);
|
||
if let Err(e) = crate::io::save(&scene.document, &path) {
|
||
return fail(self, f, format!("{file}: {e}"));
|
||
}
|
||
let Some(db) = self.sheet_set.sets.get_mut(f.set) else {
|
||
return Task::none();
|
||
};
|
||
let Some(sheet) = db.add_sheet(&f.component, &f.number, &title, "") else {
|
||
return Task::none();
|
||
};
|
||
db.set_layout_reference(
|
||
&sheet,
|
||
"Layout",
|
||
&LayoutReference { file_name: file, name: layout_name, handle },
|
||
);
|
||
if let Err(e) = write_set(db, &dst) {
|
||
self.command_line.push_error(&format!("{dst}: {e}"));
|
||
}
|
||
// "Open in drawing editor" (off by default) opens the new sheet.
|
||
self.close_active_modal();
|
||
self.sheet_set.current = Some(f.set);
|
||
self.sheet_set.selected = Some(sheet.clone());
|
||
self.sheet_sets_changed();
|
||
if f.open_after {
|
||
return self.open_sheet(&sheet);
|
||
}
|
||
Task::none()
|
||
}
|
||
}
|
||
|
||
/// The default folder for new sheet set data: the user's Documents folder,
|
||
/// else the home folder.
|
||
fn dirs_next_documents() -> String {
|
||
let home = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap_or_default();
|
||
let documents = Path::new(&home).join("Documents");
|
||
if documents.is_dir() {
|
||
return documents.to_string_lossy().to_string();
|
||
}
|
||
home
|
||
}
|
||
|
||
/// Copy the wizard's name and description into its draft set.
|
||
fn sync_wizard_draft(w: &mut Wizard) {
|
||
let set = w.draft.sheet_set_mut();
|
||
set.set_prop("Name", w.name.trim());
|
||
set.set_prop("Desc", &w.description);
|
||
}
|
||
|
||
/// The Properties rows for component `id` of `db` (`set` = its index among
|
||
/// the open sets; `None` for the wizard's draft).
|
||
pub(in crate::app) fn build_properties(db: &SheetSetDatabase, id: &str, set: Option<usize>) -> Properties {
|
||
let el = db.find(id).cloned().unwrap_or_default();
|
||
let kind = ComponentKind::of(&el).unwrap_or(ComponentKind::SheetSet);
|
||
let prop = |name: &str| el.prop(name).unwrap_or("").to_string();
|
||
let row = |group: &'static str, label: &'static str, key: RowKey, value: String| PropRow { group, label, key, value };
|
||
let template = || {
|
||
db.layout_reference(id, "DefDwtLayout")
|
||
.map(|t| format!("{} ({})", t.name, t.file_name))
|
||
.unwrap_or_default()
|
||
};
|
||
let prompt = || if el.prop("PromptForDwt").is_some_and(|v| v.trim() != "0") { "Yes" } else { "No" }.to_string();
|
||
let named_file = |name: &str| el.named(name).map(|r| db.resolve_file(r)).unwrap_or_default();
|
||
let mut rows = Vec::new();
|
||
let title;
|
||
match kind {
|
||
ComponentKind::SheetSet => {
|
||
title = format!("{} - {}", crate::t!("Sheet Set Properties"), prop("Name"));
|
||
rows.push(row("Sheet Set", "Name", RowKey::Prop("Name"), prop("Name")));
|
||
rows.push(row("Sheet Set", "Sheet set data file", RowKey::ReadOnly, db.path.clone().unwrap_or_default()));
|
||
rows.push(row("Sheet Set", "Description", RowKey::Prop("Desc"), prop("Desc")));
|
||
let resources = el
|
||
.named("Resources")
|
||
.map(|r| r.children.iter().map(|c| db.resolve_file(c)).collect::<Vec<_>>().join("; "))
|
||
.unwrap_or_default();
|
||
rows.push(row("Sheet Set", "Model view", RowKey::ReadOnly, resources));
|
||
let label_block = el
|
||
.named("DefLabelBlk")
|
||
.map(|r| format!("{} ({})", r.prop("Name").unwrap_or(""), db.resolve_file(r)))
|
||
.unwrap_or_default();
|
||
rows.push(row("Sheet Set", "Label block for views", RowKey::ReadOnly, label_block));
|
||
let callouts = el
|
||
.named("CalloutBlocks")
|
||
.map(|r| r.children.iter().filter_map(|c| c.prop("Name")).collect::<Vec<_>>().join("; "))
|
||
.unwrap_or_default();
|
||
rows.push(row("Sheet Set", "Callout blocks", RowKey::ReadOnly, callouts));
|
||
rows.push(row("Sheet Set", "Page setup overrides file", RowKey::ReadOnly, named_file("AltPageSetups")));
|
||
for (label, name) in [
|
||
("Project number", "ProjectNumber"),
|
||
("Project name", "ProjectName"),
|
||
("Project phase", "ProjectPhase"),
|
||
("Project milestone", "ProjectMilestone"),
|
||
] {
|
||
rows.push(row("Project Control", label, RowKey::Prop(name), prop(name)));
|
||
}
|
||
rows.push(row("Sheet Creation", "Sheet storage location", RowKey::Folder("NewSheetLocation"), named_file("NewSheetLocation")));
|
||
rows.push(row("Sheet Creation", "Sheet creation template", RowKey::Template, template()));
|
||
rows.push(row("Sheet Creation", "Prompt for template", RowKey::PromptTemplate, prompt()));
|
||
}
|
||
ComponentKind::Subset => {
|
||
title = format!("{} - {}", crate::t!("Subset Properties"), prop("Name"));
|
||
rows.push(row("Subset", "Subset name", RowKey::Prop("Name"), prop("Name")));
|
||
let publish = if el.prop("OverrideSheetPublish").is_some_and(|v| v.trim() == "-1") {
|
||
"Do Not Publish Sheets"
|
||
} else {
|
||
"Publish by Sheet 'Include for Publish' Setting"
|
||
};
|
||
rows.push(row("Subset", "Publish sheets in subset", RowKey::Publish, publish.to_string()));
|
||
rows.push(row("Subset", "New sheet location", RowKey::Folder("NewSheetLocation"), named_file("NewSheetLocation")));
|
||
rows.push(row("Subset", "Sheet creation template", RowKey::Template, template()));
|
||
rows.push(row("Subset", "Prompt for template", RowKey::PromptTemplate, prompt()));
|
||
}
|
||
ComponentKind::Sheet => {
|
||
title = format!("{} - {}", crate::t!("Sheet Properties"), ss::number_and_title(&el));
|
||
for (label, name) in [
|
||
("Title", "Title"),
|
||
("Number", "Number"),
|
||
("Description", "Desc"),
|
||
("Revision number", "RevisionNumber"),
|
||
("Revision date", "RevisionDate"),
|
||
("Issue purpose", "IssuePurpose"),
|
||
("Category", "Category"),
|
||
] {
|
||
rows.push(row("Sheet", label, RowKey::Prop(name), prop(name)));
|
||
}
|
||
let layout = db.layout_reference(id, "Layout").unwrap_or_default();
|
||
rows.push(row("Sheet", "Layout name", RowKey::ReadOnly, layout.name));
|
||
rows.push(row("Sheet", "Drawing file", RowKey::ReadOnly, layout.file_name));
|
||
}
|
||
}
|
||
// A sheet shows the set's sheet properties, with its own value when set.
|
||
let custom = match kind {
|
||
ComponentKind::Sheet => {
|
||
let own = ss::custom_properties(&el);
|
||
ss::custom_properties(db.sheet_set())
|
||
.into_iter()
|
||
.filter(|(_, _, f)| f & ss::CUSTOM_SHEET_PROP != 0)
|
||
.map(|(n, v, f)| {
|
||
let value = own.iter().find(|(o, _, _)| *o == n).map(|(_, v, _)| v.clone()).unwrap_or(v);
|
||
(n, value, f)
|
||
})
|
||
.collect()
|
||
}
|
||
ComponentKind::SheetSet => ss::custom_properties(&el),
|
||
ComponentKind::Subset => Vec::new(),
|
||
};
|
||
Properties {
|
||
set,
|
||
component: id.to_string(),
|
||
kind,
|
||
title,
|
||
rows,
|
||
custom,
|
||
adding: None,
|
||
template: None,
|
||
wizard: None,
|
||
error: None,
|
||
}
|
||
}
|
||
|
||
/// Write a Properties dialog back into its component.
|
||
fn write_properties(db: &mut SheetSetDatabase, p: &Properties) {
|
||
for r in &p.rows {
|
||
match &r.key {
|
||
RowKey::Prop(name) => {
|
||
if let Some(el) = db.find_mut(&p.component) {
|
||
el.set_prop(name, &r.value);
|
||
}
|
||
}
|
||
RowKey::Folder(name) => {
|
||
db.set_file_reference(&p.component, name, r.value.trim());
|
||
}
|
||
RowKey::PromptTemplate => {
|
||
// The set keeps it as an int (1), a subset as a short (-1).
|
||
if let Some(el) = db.find_mut(&p.component) {
|
||
el.remove_named("PromptForDwt");
|
||
match (r.value == "Yes", p.kind) {
|
||
(true, ComponentKind::SheetSet) => el.set_prop_vt("PromptForDwt", 3, "1"),
|
||
(true, _) => el.set_prop_vt("PromptForDwt", 2, "-1"),
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
RowKey::Publish => {
|
||
if let Some(el) = db.find_mut(&p.component) {
|
||
if r.value == "Do Not Publish Sheets" {
|
||
el.set_prop_vt("OverrideSheetPublish", 2, "-1");
|
||
} else {
|
||
el.remove_named("OverrideSheetPublish");
|
||
}
|
||
}
|
||
}
|
||
RowKey::Template | RowKey::ReadOnly => {}
|
||
}
|
||
}
|
||
if let Some((file, name)) = &p.template {
|
||
db.set_layout_reference(
|
||
&p.component,
|
||
"DefDwtLayout",
|
||
&LayoutReference { file_name: file.clone(), name: name.clone(), handle: String::new() },
|
||
);
|
||
}
|
||
match p.kind {
|
||
ComponentKind::SheetSet => {
|
||
if let Some(el) = db.find_mut(&p.component) {
|
||
let old: Vec<String> = ss::custom_properties(el).into_iter().map(|(n, _, _)| n).collect();
|
||
for n in old.iter().filter(|n| !p.custom.iter().any(|(c, _, _)| c == *n)) {
|
||
ss::remove_custom_property(el, n);
|
||
}
|
||
for (n, v, f) in &p.custom {
|
||
ss::set_custom_property(el, n, v, *f);
|
||
}
|
||
}
|
||
}
|
||
ComponentKind::Sheet => {
|
||
if let Some(el) = db.find_mut(&p.component) {
|
||
for (n, v, f) in &p.custom {
|
||
ss::set_custom_property(el, n, v, *f);
|
||
}
|
||
}
|
||
}
|
||
ComponentKind::Subset => {}
|
||
}
|
||
}
|
||
|
||
/// Store fresh values for a drawing's sheet set fields (the fields keep them,
|
||
/// as when the reference updates them) and redraw the hosts that changed;
|
||
/// whether any did.
|
||
fn refresh_sheet_fields(scene: &mut crate::scene::Scene) -> bool {
|
||
let changes: Vec<_> = crate::entities::field::refresh_sheet_set_fields(&mut scene.document)
|
||
.into_iter()
|
||
.map(|h| (h, crate::scene::ChangeKind::Modified))
|
||
.collect();
|
||
if !changes.is_empty() {
|
||
scene.bump_entities(&changes);
|
||
}
|
||
!changes.is_empty()
|
||
}
|