Merge pull request #1777 from ramox81/codex/refedit-dialog

REFEDIT: Reference Edit dialog and unique names for xref objects

# Conflicts:
#	locales/ar-SA/opencadstudio.ftl
#	locales/bg-BG/opencadstudio.ftl
#	locales/cs-CZ/opencadstudio.ftl
#	locales/de-DE/opencadstudio.ftl
#	locales/el-GR/opencadstudio.ftl
#	locales/en-US/opencadstudio.ftl
#	locales/es-ES/opencadstudio.ftl
#	locales/fi-FI/opencadstudio.ftl
#	locales/fr-FR/opencadstudio.ftl
#	locales/hi-IN/opencadstudio.ftl
#	locales/hu-HU/opencadstudio.ftl
#	locales/it-IT/opencadstudio.ftl
#	locales/ja-JP/opencadstudio.ftl
#	locales/ko-KR/opencadstudio.ftl
#	locales/lo-LA/opencadstudio.ftl
#	locales/nl-NL/opencadstudio.ftl
#	locales/pl-PL/opencadstudio.ftl
#	locales/pt-BR/opencadstudio.ftl
#	locales/ru-RU/opencadstudio.ftl
#	locales/tr-TR/opencadstudio.ftl
#	locales/zh-CN/opencadstudio.ftl
#	locales/zh-TW/opencadstudio.ftl
#	src/app/control/actions.rs
#	src/app/mod.rs
#	src/app/view/modal.rs
This commit is contained in:
Hakan Seven committed 2026-10-14 12:10:40 +03:00
commit 21f33e933e
34 files changed
+1070 -23

No files matched your search

+338 -18
View File
@@ -4,10 +4,11 @@
use crate::app::{Message, OpenCADStudio};
use crate::modules::draw::modify::refedit::{
apply_insert_inverse_transform, apply_insert_transform, level_hits,
nesting_chain, RefCloseCommand, RefEditOptionsCommand, RefEditPickCommand, RefEditSession,
RefSetCommand, NOT_CHECKED_OUT,
apply_insert_inverse_transform, apply_insert_transform, level_hits, nesting_chain,
rename_symbols, ChainLevel, RefCloseCommand, RefEditNames, RefEditOptionsCommand,
RefEditPickCommand, RefEditSession, RefSetCommand, SymbolKind, NOT_CHECKED_OUT,
};
use crate::ui::window::refedit_dialog::{RefEditDialogMsg, RefEditDialogState, RefEditLevel};
use codec::{EntityType, Handle};
use iced::Task;
use std::collections::HashSet;
@@ -147,25 +148,127 @@ impl OpenCADStudio {
.iter()
.map(|level| level_hits(&self.tabs[i].scene.document, level))
.collect();
let command = RefEditOptionsCommand::new(top, chain, hits);
let command = RefEditOptionsCommand::new(
top,
chain,
hits,
self.refedit_options.lock,
self.refedit_options.unique_names,
);
use crate::command::CadCommand;
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
} else {
// The dialog's defaults: the innermost reference, all objects,
// attribute definitions not displayed.
let path: Vec<String> = chain[1..]
.iter()
.map(|l| l.insert.value().to_string())
.collect();
let _ = self.dispatch_command(&format!(
"REFEDIT_BEGIN:{}:{}:0:",
top.value(),
path.join(",")
));
self.open_refedit_dialog(i, top, chain);
}
}
/// The Edit Reference dialog over the picked chain: the innermost level
/// chosen, the remembered settings.
fn open_refedit_dialog(&mut self, i: usize, top: Handle, chain: Vec<ChainLevel>) {
let scene = &self.tabs[i].scene;
let document = &scene.document;
let levels = chain
.iter()
.map(|level| {
let record = document.block_records.get(&level.block_name);
RefEditLevel {
insert: level.insert,
name: level.block_name.clone(),
wires: scene.block_preview_wires(&level.block_name),
has_attdefs: record.is_some_and(|br| {
br.entity_handles.iter().any(|h| {
matches!(document.get_entity(*h), Some(EntityType::AttributeDefinition(_)))
})
}),
xref_path: record
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.map(|br| br.xref_path.clone())
.filter(|p| !p.is_empty()),
}
})
.collect::<Vec<_>>();
let options = &self.refedit_options;
self.refedit_dialog = Some(RefEditDialogState {
top,
level: levels.len().saturating_sub(1),
levels,
chain,
nested: options.nested,
attributes: options.attributes,
lock: options.lock,
unique: options.unique_names,
});
self.active_modal = Some(crate::app::ModalKind::RefEdit);
}
pub(in crate::app) fn update_refedit_dialog(&mut self, message: RefEditDialogMsg) -> Task<Message> {
let Some(state) = self.refedit_dialog.as_mut() else {
return Task::none();
};
match message {
RefEditDialogMsg::Level(level) => {
state.level = level.min(state.levels.len().saturating_sub(1))
}
RefEditDialogMsg::Nested(on) => state.nested = on,
RefEditDialogMsg::Attributes(on) => state.attributes = on,
RefEditDialogMsg::Lock(on) => state.lock = on,
RefEditDialogMsg::Unique(on) => state.unique = on,
RefEditDialogMsg::Help => {
self.command_line
.push_info(&crate::tr!("ref-edit", "dialog-help"));
}
RefEditDialogMsg::Ok => {
let Some(state) = self.refedit_dialog.take() else {
return Task::none();
};
self.close_active_modal();
let attributes = state.attributes && state.attributes_enabled();
self.refedit_options = crate::app::settings::RefEditOptions {
unique_names: state.unique,
attributes: state.attributes,
lock: state.lock,
nested: state.nested,
};
let i = self.active_tab;
if state.nested {
let hits = state
.chain
.iter()
.map(|level| level_hits(&self.tabs[i].scene.document, level))
.collect();
let command = RefEditOptionsCommand::nested_pick(
state.top,
state.chain,
hits,
state.level,
attributes,
state.lock,
state.unique,
);
use crate::command::CadCommand;
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
} else {
let last = state.level.min(state.chain.len().saturating_sub(1));
let path: Vec<String> = state.chain[1..=last]
.iter()
.map(|l| l.insert.value().to_string())
.collect();
return self.dispatch_command(&format!(
"REFEDIT_BEGIN:{}:{}:{}::{}:{}",
state.top.value(),
path.join(","),
attributes as u8,
state.lock as u8,
state.unique as u8
));
}
}
}
Task::none()
}
/// `top[:path[:attributes[:nested]]]` — open the session.
fn begin_refedit(&mut self, i: usize, args: &str) {
let mut parts = args.split(':');
@@ -186,6 +289,10 @@ impl OpenCADStudio {
let path = handles(parts.next());
let attributes = parts.next() == Some("1");
let nested = handles(parts.next());
let lock = parts.next().map_or(self.refedit_options.lock, |v| v == "1");
let unique = parts
.next()
.map_or(self.refedit_options.unique_names, |v| v == "1");
if self.tabs[i].refedit_session.is_some() || self.reject_locked_edit(i, top) {
return;
}
@@ -211,6 +318,15 @@ impl OpenCADStudio {
});
}
let level = chain.last().expect("non-empty chain").clone();
// An external reference's dependent names (`XREF|NAME`) of the
// outermost xref in the chain.
let xref_prefix = chain.iter().find_map(|l| {
document
.block_records
.get(&l.block_name)
.filter(|br| br.flags.is_xref || br.flags.is_xref_overlay)
.map(|br| format!("{}|", br.name))
});
let Some(br) = document.block_records.get(&level.block_name) else {
return;
};
@@ -245,6 +361,8 @@ impl OpenCADStudio {
// Everything allocated from here on is part of the session.
handle_watermark: document.next_handle().max(document.header.handle_seed),
xref_path,
lock,
names: RefEditNames::default(),
};
let copies: Vec<EntityType> = extracted
.iter()
@@ -252,6 +370,15 @@ impl OpenCADStudio {
.collect();
self.push_undo_snapshot(i, "REFEDIT");
let mut session = session;
let copies = match xref_prefix {
Some(prefix) => {
let (copies, names) = self.refedit_host_names(i, copies, &prefix, unique);
session.names = names;
copies
}
None => copies,
};
let mut temp_handles = Vec::new();
for mut entity in copies {
apply_insert_transform(&mut entity, &session);
@@ -259,7 +386,6 @@ impl OpenCADStudio {
entity.common_mut().owner_handle = Handle::NULL;
temp_handles.push(self.tabs[i].scene.add_entity(entity));
}
let mut session = session;
session.temp_handles = temp_handles.clone();
self.tabs[i].refedit_session = Some(session);
self.tabs[i].refedit_closed.clear();
@@ -267,7 +393,7 @@ impl OpenCADStudio {
self.tabs[i]
.scene
.set_refedit_keep(Some(temp_handles.iter().copied().collect::<rustc_hash::FxHashSet<_>>()));
self.tabs[i].scene.refedit_lock = true;
self.tabs[i].scene.refedit_lock = lock;
self.tabs[i].scene.deselect_all();
self.tabs[i].dirty = true;
// No active command — the definition's objects are edited freely
@@ -375,6 +501,10 @@ impl OpenCADStudio {
.iter()
.filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned())
.map(|mut entity| {
// Back to the reference's own dependent names.
rename_symbols(&mut entity, &|kind, name| {
session.names.original(kind, name).map(str::to_string)
});
apply_insert_inverse_transform(&mut entity, &session);
let mut entity = normalize_entity_for_block(entity);
entity.common_mut().handle = Handle::NULL;
@@ -457,6 +587,7 @@ impl OpenCADStudio {
}
fn finish_refedit_close(&mut self, i: usize, session: RefEditSession) {
self.drop_refedit_names(i, &session.names);
self.tabs[i].refedit_closed.push(session);
self.tabs[i].scene.refedit_lock = false;
self.tabs[i].scene.set_refedit_keep(None);
@@ -586,8 +717,8 @@ impl OpenCADStudio {
.map(|e| e.common().handle)
.filter(|h| session.in_working_set(*h))
.collect();
self.tabs[i].scene.refedit_lock = session.lock;
self.tabs[i].refedit_session = Some(session);
self.tabs[i].scene.refedit_lock = true;
self.tabs[i].scene.set_refedit_keep(Some(keep));
}
}
@@ -605,4 +736,193 @@ impl OpenCADStudio {
_ => {}
}
}
/// The working set's objects of an external reference under host names:
/// `XREF|NAME` becomes `$N$NAME` (the first free N) with records of its
/// own when `unique`, else `NAME`, sharing a host record of that name when
/// one exists. Missing records are copied from the reference's.
fn refedit_host_names(
&mut self,
i: usize,
mut copies: Vec<EntityType>,
prefix: &str,
unique: bool,
) -> (Vec<EntityType>, RefEditNames) {
let document = &mut self.tabs[i].scene.document;
let deps = crate::app::ClipboardDeps::capture(document, &copies);
let strip = |name: &str| -> Option<String> {
(name.len() > prefix.len()
&& name.is_char_boundary(prefix.len())
&& name[..prefix.len()].eq_ignore_ascii_case(prefix))
.then(|| name[prefix.len()..].to_string())
};
let exists = |document: &codec::CadDocument, kind: SymbolKind, name: &str| match kind {
SymbolKind::Layer => document.layers.contains(name),
SymbolKind::Linetype => document.line_types.contains(name),
SymbolKind::TextStyle => document.text_styles.contains(name),
SymbolKind::Block => document.block_records.get(name).is_some(),
};
let wanted: Vec<(SymbolKind, String)> = deps
.layers
.iter()
.map(|r| (SymbolKind::Layer, r.name.clone()))
.chain(deps.linetypes.iter().map(|r| (SymbolKind::Linetype, r.name.clone())))
.chain(deps.text_styles.iter().map(|r| (SymbolKind::TextStyle, r.name.clone())))
.chain(deps.blocks.iter().map(|b| (SymbolKind::Block, b.name.clone())))
.collect();
let mut names = RefEditNames::default();
for (kind, original) in wanted {
let Some(base) = strip(&original) else {
continue;
};
let taken = |names: &RefEditNames, candidate: &str| {
exists(document, kind, candidate)
|| names
.renamed
.iter()
.any(|(k, used, _)| *k == kind && used.eq_ignore_ascii_case(candidate))
};
// Layer 0 is the host's own in every drawing.
let used = if kind == SymbolKind::Layer && base == "0" {
base
} else if unique {
(0u32..)
.map(|n| format!("${n}${base}"))
.find(|candidate| !taken(&names, candidate))
.expect("a free name")
} else {
base
};
if !exists(document, kind, &used)
&& !names
.created
.iter()
.any(|(k, n)| *k == kind && n.eq_ignore_ascii_case(&used))
{
names.created.push((kind, used.clone()));
}
names.renamed.push((kind, used, original));
}
let used_for = |kind: SymbolKind, name: &str| {
names
.renamed
.iter()
.find(|(k, _, original)| *k == kind && original.eq_ignore_ascii_case(name))
.map(|(_, used, _)| used.clone())
};
let created = |kind: SymbolKind, name: &str| -> Option<String> {
used_for(kind, name).filter(|used| {
names
.created
.iter()
.any(|(k, n)| *k == kind && n.eq_ignore_ascii_case(used))
})
};
for record in &deps.layers {
if let Some(used) = created(SymbolKind::Layer, &record.name) {
let mut copy = record.clone();
copy.name = used;
copy.flags.xref_dependent = false;
copy.xref_block_record_handle = Handle::NULL;
if let Some(linetype) = used_for(SymbolKind::Linetype, &copy.line_type) {
copy.line_type = linetype;
}
copy.handle = document.allocate_handle();
let _ = document.layers.add(copy);
}
}
for record in &deps.linetypes {
if let Some(used) = created(SymbolKind::Linetype, &record.name) {
let mut copy = record.clone();
copy.name = used;
copy.xref_dependent = false;
copy.xref_block_record_handle = Handle::NULL;
copy.handle = document.allocate_handle();
let _ = document.line_types.add(copy);
}
}
for record in &deps.text_styles {
if let Some(used) = created(SymbolKind::TextStyle, &record.name) {
let mut copy = record.clone();
copy.name = used;
copy.xref_dependent = false;
copy.xref_block_record_handle = Handle::NULL;
copy.handle = document.allocate_handle();
let _ = document.text_styles.add(copy);
}
}
let rename = |kind: SymbolKind, name: &str| used_for(kind, name);
let blocks: Vec<crate::app::BlockDef> = deps
.blocks
.iter()
.filter_map(|def| {
let used = created(SymbolKind::Block, &def.name)?;
let mut entities = def.entities.clone();
for entity in &mut entities {
rename_symbols(entity, &rename);
}
Some(crate::app::BlockDef {
name: used,
base_point: def.base_point,
entities,
})
})
.collect();
crate::app::command_driver::clipboard::merge_block_defs(document, &blocks);
for entity in &mut copies {
rename_symbols(entity, &rename);
}
(copies, names)
}
/// Removes the host records made for a session that nothing uses any more.
fn drop_refedit_names(&mut self, i: usize, names: &RefEditNames) {
for (kind, name) in names.created.iter().rev() {
let scene = &mut self.tabs[i].scene;
let in_use = scene.document.entities().any(|entity| {
let common = entity.common();
match kind {
SymbolKind::Layer => common.layer.eq_ignore_ascii_case(name),
SymbolKind::Linetype => common.linetype.eq_ignore_ascii_case(name),
SymbolKind::TextStyle => match entity {
EntityType::Text(t) => t.style.eq_ignore_ascii_case(name),
EntityType::MText(t) => t.style.eq_ignore_ascii_case(name),
EntityType::AttributeDefinition(a) => {
a.text_style.eq_ignore_ascii_case(name)
}
_ => false,
},
SymbolKind::Block => matches!(
entity,
EntityType::Insert(insert) if insert.block_name.eq_ignore_ascii_case(name)
),
}
});
// A linetype made for the session may also be a kept layer's.
let in_use = in_use
|| (*kind == SymbolKind::Linetype
&& scene
.document
.layers
.iter()
.any(|layer| layer.line_type.eq_ignore_ascii_case(name)));
if in_use {
continue;
}
match kind {
SymbolKind::Layer => {
scene.document.layers.remove(name);
}
SymbolKind::Linetype => {
scene.document.line_types.remove(name);
}
SymbolKind::TextStyle => {
scene.document.text_styles.remove(name);
}
SymbolKind::Block => {
let _ = crate::scene::block_service::erase(scene, name);
}
}
}
}
}
+31
View File
@@ -105,6 +105,7 @@ pub(super) const NAMES: &[&str] = &[
"pdf_dialog_ok",
"view_background",
"image_adjust",
"refedit_dialog",
"pdf_layer_toggle",
"pc_manager_toggle",
"pc_manager",
@@ -1885,6 +1886,36 @@ impl OpenCADStudio {
})
}
"ribbon_dropdown" => Message::ToggleRibbonDropdown(string(req, "value")?.into()),
// The Edit Reference dialog, one field per call: "level=<n>" (0 =
// the picked reference), "nested=0|1" (pick the working set in the
// drawing), "attributes=0|1", "lock=0|1", "unique=0|1", "ok" or
// "cancel".
"refedit_dialog" => {
use crate::ui::window::refedit_dialog::RefEditDialogMsg as R;
if self.active_modal != Some(crate::app::ModalKind::RefEdit) {
return Err(failure("no_dialog", "The Reference Edit dialog is not open"));
}
let spec = string(req, "value")?;
let (key, value) = spec.split_once('=').unwrap_or((spec, ""));
let flag = || match value {
"1" => Ok(true),
"0" => Ok(false),
_ => Err(failure("invalid_value", spec)),
};
if key == "cancel" {
Message::CloseModal
} else {
Message::RefEditDialog(match key {
"ok" => R::Ok,
"level" => R::Level(value.parse().map_err(|_| failure("invalid_value", spec))?),
"nested" => R::Nested(flag()?),
"attributes" => R::Attributes(flag()?),
"lock" => R::Lock(flag()?),
"unique" => R::Unique(flag()?),
_ => return Err(failure("invalid_value", "Unknown Reference Edit field")),
})
}
}
// The -VIEW Background dialog, one field per call: "type=gradient",
// "color|top|middle|bottom=r,g,b", "three=1", "rotation=30",
// "image=<path>", "position=center|stretch|tile", "aspect=1", "offset=x,y" (pixels), "scale=x,y",
+9
View File
@@ -586,6 +586,8 @@ pub(super) struct OpenCADStudio {
view_background: Option<crate::ui::window::pdf_dialogs::ViewBackgroundState>,
/// IMAGEADJUST dialog.
image_adjust: Option<crate::ui::window::pdf_dialogs::ImageAdjustState>,
/// The Edit Reference dialog (REFEDIT).
refedit_dialog: Option<crate::ui::window::refedit_dialog::RefEditDialogState>,
underlay_layers: Option<crate::ui::window::pdf_dialogs::UnderlayLayersState>,
pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>,
pdf_import_file: Option<crate::ui::window::pdf_dialogs::PdfImportFileState>,
@@ -794,6 +796,8 @@ pub(super) struct OpenCADStudio {
block_freq: std::collections::HashMap<String, u32>,
/// BLOCKMRULIST: how many recent blocks the palette keeps (0–100).
pub(crate) block_mru_list: u8,
/// The Edit Reference dialog's remembered settings.
pub(crate) refedit_options: crate::app::settings::RefEditOptions,
/// BLOCKREDEFINEMODE (0–2).
pub(crate) block_redefine_mode: u8,
/// BLOCKNAVIGATE: the Libraries tab's start folder ("." for none).
@@ -2158,6 +2162,7 @@ pub enum ModalKind {
PcSection,
ViewBackground,
ImageAdjust,
RefEdit,
UnderlayLayers,
PdfImportSettings,
PdfImportFile,
@@ -4356,6 +4361,8 @@ pub enum Message {
PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg),
AttdefDialog(crate::ui::window::attdef_dialog::AttdefDialogMsg),
FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg),
/// An edit in the Edit Reference dialog.
RefEditDialog(crate::ui::window::refedit_dialog::RefEditDialogMsg),
/// A click or search in the Point Cloud Manager.
PcManager(crate::ui::window::pc_manager::PcManagerMsg),
Count(crate::ui::window::count_palette::CountMsg),
@@ -4569,6 +4576,7 @@ impl OpenCADStudio {
pc_section: None,
view_background: None,
image_adjust: None,
refedit_dialog: None,
underlay_layers: None,
pdf_import_settings: None,
pdf_import_file: None,
@@ -4668,6 +4676,7 @@ impl OpenCADStudio {
block_mru: Vec::new(),
block_freq: std::collections::HashMap::new(),
block_mru_list: 50,
refedit_options: Default::default(),
block_redefine_mode: 1,
block_navigate: ".".to_string(),
insname: String::new(),
+28
View File
@@ -652,6 +652,9 @@ pub struct UserSettings {
/// BLOCKNAVIGATE (default ".").
#[serde(default = "default_block_navigate")]
pub block_navigate: String,
/// The Edit Reference dialog's settings.
#[serde(default)]
pub refedit: RefEditOptions,
/// XFADECTL: how far objects outside the REFEDIT working set fade (0–90, default 70).
#[serde(default = "default_xfadectl")]
pub xfadectl: u8,
@@ -687,6 +690,30 @@ pub struct PaletteBlockRef {
pub drawing: bool,
}
/// The Edit Reference dialog's settings, remembered between sessions: give
/// an external reference's clashing named objects unique names, check out
/// attribute definitions, lock what is outside the working set, pick the
/// working set's objects in the drawing.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct RefEditOptions {
pub unique_names: bool,
pub attributes: bool,
pub lock: bool,
pub nested: bool,
}
impl Default for RefEditOptions {
fn default() -> Self {
Self {
unique_names: true,
attributes: false,
lock: true,
nested: false,
}
}
}
/// The Blocks palette's insertion options.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BlockInsertOptions {
@@ -938,6 +965,7 @@ impl Default for UserSettings {
block_mru_list: 50,
block_redefine_mode: 1,
block_navigate: ".".to_string(),
refedit: RefEditOptions::default(),
xfadectl: 70,
count_color: 3,
count_error_color: 1,
+2
View File
@@ -873,6 +873,7 @@ impl OpenCADStudio {
block_mru_list: self.block_mru_list,
block_redefine_mode: self.block_redefine_mode,
block_navigate: self.block_navigate.clone(),
refedit: self.refedit_options.clone(),
xfadectl: crate::scene::cache::block_cache::refedit_fade_ctl() as u8,
count_color: self.count_palette.color,
count_error_color: self.count_palette.error_color,
@@ -1036,6 +1037,7 @@ impl OpenCADStudio {
self.block_redefine_mode = s.block_redefine_mode.min(2);
self.block_navigate = s.block_navigate.clone();
crate::scene::cache::block_cache::set_refedit_fade_ctl(i32::from(s.xfadectl.min(90)));
self.refedit_options = s.refedit.clone();
self.count_palette.color = s.count_color;
self.count_palette.error_color = s.count_error_color;
self.count_palette.service = s.count_service;
+4
View File
@@ -2281,6 +2281,7 @@ impl OpenCADStudio {
Message::PdfDialog(message) => self.update_pdf_dialog(message),
Message::AttdefDialog(message) => self.on_attdef_dialog(message),
Message::FieldDialog(message) => self.on_field_dialog(message),
Message::RefEditDialog(message) => self.update_refedit_dialog(message),
Message::PcManager(message) => self.update_pc_manager(message),
Message::Count(message) => self.on_count(message),
Message::SheetSet(message) => self.on_sheet_set(message),
@@ -9503,6 +9504,9 @@ impl OpenCADStudio {
Task::none()
};
}
if self.active_modal == Some(super::ModalKind::RefEdit) {
self.refedit_dialog = None;
}
// Cancelling the section settings drops the waiting extraction.
if self.active_modal == Some(super::ModalKind::PcSection) {
crate::modules::insert::pc_extract::drop_job();
+7
View File
@@ -41,6 +41,7 @@ impl OpenCADStudio {
self.view_background.as_ref().map_or("", |s| s.view.as_str())
),
Some(K::ImageAdjust) => crate::tr!("image-adjust", "title"),
Some(K::RefEdit) => crate::tr!("ref-edit", "dialog-title"),
Some(K::UnderlayLayers) => crate::t!("Underlay Layers").into_owned(),
Some(K::PdfImportSettings) => crate::t!("PDF Import Settings").into_owned(),
Some(K::PdfImportFile) => crate::t!("Import PDF").into_owned(),
@@ -542,6 +543,12 @@ impl OpenCADStudio {
crate::ui::window::pdf_dialogs::image_adjust(state, flow)
})
}
super::super::ModalKind::RefEdit => {
let state = self.refedit_dialog.as_ref()?;
sized_flow(ex, 560, 470, |flow| {
crate::ui::window::refedit_dialog::view(state, flow)
})
}
super::super::ModalKind::UnderlayLayers => {
let state = self.underlay_layers.as_ref()?;
sized_flow(ex, 460, 520, |flow| {
+1 -1
View File
@@ -32,7 +32,7 @@ const MODERN_PROTOCOL_VERSION: &str = "2026-07-28";
///
/// `block_delete` gained its optional `force` parameter.
#[cfg(test)]
const TOOL_SCHEMA_DIGEST: &str = "dde915fd17f3cc4667e1eee8d796030156e7ff9423b0fa9b48a648a1481df723";
const TOOL_SCHEMA_DIGEST: &str = "c5f9ad00e04c0acc809c7de0c75e789fd9bf3ad6db68bdde36bd771a859fef79";
const MAX_REQUEST: usize = 1_048_576;
const MAX_RESPONSE: u64 = 16 * 1024 * 1024;
const CACHE_TTL_MS: u64 = 3_600_000;
+115 -3
View File
@@ -94,6 +94,79 @@ pub struct RefEditSession {
pub handle_watermark: u64,
/// The referenced file when the definition is an xref.
pub xref_path: Option<String>,
/// Objects outside the working set can't be selected while it is open.
pub lock: bool,
/// The external reference's named objects the working set uses under
/// host names, and the host records made for the session.
pub names: RefEditNames,
}
/// A kind of named object the working set's objects refer to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolKind {
Layer,
Linetype,
TextStyle,
Block,
}
/// How an external reference's dependent names (`XREF|NAME`) are used in
/// the host while its objects are checked out: as `$0$NAME` records of
/// their own (unique names), or under the plain name, sharing a host record
/// of that name when there is one.
#[derive(Debug, Clone, Default)]
pub struct RefEditNames {
/// (kind, name used in the host, the reference's dependent name).
pub renamed: Vec<(SymbolKind, String, String)>,
/// Records added to the host for the session, removed when it closes.
pub created: Vec<(SymbolKind, String)>,
}
impl RefEditNames {
/// The reference's own name for a host name used in the session.
pub fn original(&self, kind: SymbolKind, name: &str) -> Option<&str> {
self.renamed
.iter()
.find(|(k, used, _)| *k == kind && used.eq_ignore_ascii_case(name))
.map(|(_, _, original)| original.as_str())
}
}
/// Rewrites every named-object reference of an entity through `map`
/// (layer, linetype, text style, block).
pub fn rename_symbols(entity: &mut EntityType, map: &dyn Fn(SymbolKind, &str) -> Option<String>) {
let common = entity.common_mut();
if let Some(name) = map(SymbolKind::Layer, &common.layer) {
common.layer = name;
common.layer_handle = None;
}
if let Some(name) = map(SymbolKind::Linetype, &common.linetype) {
common.linetype = name;
common.linetype_handle = None;
}
match entity {
EntityType::Insert(insert) => {
if let Some(name) = map(SymbolKind::Block, &insert.block_name) {
insert.block_name = name;
}
}
EntityType::Text(text) => {
if let Some(name) = map(SymbolKind::TextStyle, &text.style) {
text.style = name;
}
}
EntityType::MText(text) => {
if let Some(name) = map(SymbolKind::TextStyle, &text.style) {
text.style = name;
}
}
EntityType::AttributeDefinition(attdef) => {
if let Some(name) = map(SymbolKind::TextStyle, &attdef.text_style) {
attdef.text_style = name;
}
}
_ => {}
}
}
impl RefEditSession {
@@ -334,10 +407,23 @@ pub struct RefEditOptionsCommand {
level: usize,
step: OptionStep,
nested: Vec<Handle>,
/// The session settings (lock objects outside the working set, unique
/// names for an external reference's named objects).
lock: bool,
unique: bool,
/// Set when the Edit Reference dialog already chose the attribute
/// option: the nested pick then opens the session directly.
attributes: Option<bool>,
}
impl RefEditOptionsCommand {
pub fn new(top: Handle, chain: Vec<ChainLevel>, hits: Vec<LevelHits>) -> Self {
pub fn new(
top: Handle,
chain: Vec<ChainLevel>,
hits: Vec<LevelHits>,
lock: bool,
unique: bool,
) -> Self {
let level = chain.len().saturating_sub(1);
Self {
top,
@@ -346,9 +432,30 @@ impl RefEditOptionsCommand {
level,
step: OptionStep::Nesting,
nested: Vec::new(),
lock,
unique,
attributes: None,
}
}
/// `Select nested objects:` for the dialog's "pick in the drawing"
/// working set at `level`.
pub fn nested_pick(
top: Handle,
chain: Vec<ChainLevel>,
hits: Vec<LevelHits>,
level: usize,
attributes: bool,
lock: bool,
unique: bool,
) -> Self {
let mut command = Self::new(top, chain, hits, lock, unique);
command.level = level.min(command.chain.len().saturating_sub(1));
command.step = OptionStep::Nested;
command.attributes = Some(attributes);
command
}
fn begin(&self, attributes: bool) -> CmdResult {
let path: Vec<String> = self.chain[1..=self.level]
.iter()
@@ -357,11 +464,13 @@ impl RefEditOptionsCommand {
let nested: Vec<String> = self.nested.iter().map(|h| h.value().to_string()).collect();
CmdResult::Relaunch(
format!(
"REFEDIT_BEGIN:{}:{}:{}:{}",
"REFEDIT_BEGIN:{}:{}:{}:{}:{}:{}",
self.top.value(),
path.join(","),
attributes as u8,
nested.join(",")
nested.join(","),
self.lock as u8,
self.unique as u8
),
vec![],
)
@@ -469,6 +578,9 @@ impl CadCommand for RefEditOptionsCommand {
if n == 0 {
return CmdResult::CancelWithMessage(t!("0 items selected").into_owned());
}
if let Some(attributes) = self.attributes {
return self.begin(attributes);
}
self.step = OptionStep::Attributes;
CmdResult::ReportMeasurement(if n == 1 {
t!("1 item selected").into_owned()
+1
View File
@@ -30,6 +30,7 @@ pub mod plot;
pub mod plugin_manager;
pub mod print_all;
pub mod recovery;
pub mod refedit_dialog;
pub mod sheet_set;
pub mod shortcuts;
pub mod update_notice;
+1 -1
View File
@@ -312,7 +312,7 @@ pub(crate) fn well_style(theme: &Theme) -> container::Style {
}
/// A titled card.
fn card<'a>(title: String, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
pub(crate) fn card<'a>(title: String, content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
container(
column![
text(title.to_uppercase()).size(10).style(accent_text),
+247
View File
@@ -0,0 +1,247 @@
//! Edit Reference dialog (REFEDIT): the nesting level to check out, shown as
//! a chain of chips beside a preview of the chosen definition, how the
//! working set is filled, whether attribute definitions come along, and the
//! session settings (lock what is outside the working set, give clashing
//! named objects of an external reference unique names).
use iced::widget::{button, column, container, row, text, toggler, Space};
use iced::{Border, Element, Fill, Length, Theme};
use crate::app::Message;
use crate::scene::model::wire_model::WireModel;
use crate::ui::style::common::muted_style;
use crate::ui::style::form::{button_style, dialog_button};
use crate::ui::window::pdf_dialogs::{card, well_style};
/// One edit in the Edit Reference dialog.
#[derive(Debug, Clone)]
pub enum RefEditDialogMsg {
/// The nesting level picked in the chain (0 = the selected reference).
Level(usize),
/// Pick the working set's objects in the drawing (else all of them).
Nested(bool),
Attributes(bool),
Lock(bool),
Unique(bool),
Help,
Ok,
}
fn msg(m: RefEditDialogMsg) -> Message {
Message::RefEditDialog(m)
}
/// One level of the nesting chain as the dialog shows it.
pub struct RefEditLevel {
/// The reference's handle: the picked object for level 0, the
/// definition-owned reference below it.
pub insert: codec::Handle,
pub name: String,
/// The definition's preview.
pub wires: Vec<WireModel>,
/// The definition holds attribute definitions (the attribute switch is
/// only offered then).
pub has_attdefs: bool,
/// The referenced file of an external reference.
pub xref_path: Option<String>,
}
pub struct RefEditDialogState {
pub top: codec::Handle,
/// The picked chain, outermost first.
pub chain: Vec<crate::modules::draw::modify::refedit::ChainLevel>,
pub levels: Vec<RefEditLevel>,
pub level: usize,
pub nested: bool,
pub attributes: bool,
pub lock: bool,
pub unique: bool,
}
impl RefEditDialogState {
/// The attribute switch counts only where the chosen definition has
/// attribute definitions.
pub fn attributes_enabled(&self) -> bool {
self.levels.get(self.level).is_some_and(|l| l.has_attdefs)
}
}
fn label<'a>(value: String, enabled: bool) -> Element<'a, Message> {
text(value)
.size(12)
.style(move |theme: &Theme| {
if enabled {
text::Style::default()
} else {
text::Style {
color: Some(theme.palette().background.strong.color),
}
}
})
.width(Fill)
.into()
}
/// A labelled switch on one row; inactive while `on_toggle` is None.
fn switch_row<'a>(
name: String,
value: bool,
on_toggle: Option<fn(bool) -> RefEditDialogMsg>,
) -> Element<'a, Message> {
let enabled = on_toggle.is_some();
let mut switch = toggler(value).size(18.0);
if let Some(on) = on_toggle {
switch = switch.on_toggle(move |v| msg(on(v)));
}
row![label(name, enabled), switch]
.spacing(8)
.align_y(iced::Center)
.into()
}
/// A level of the chain as a pill; the chosen one framed in the accent colour.
fn level_chip<'a>(name: &str, index: usize, chosen: bool) -> Element<'a, Message> {
button(text(name.to_string()).size(12))
.on_press(msg(RefEditDialogMsg::Level(index)))
.padding([5, 12])
.style(move |theme: &Theme, status| {
let mut style = button_style(false)(theme, status);
style.border = Border {
width: if chosen { 1.5 } else { 1.0 },
radius: 14.0.into(),
color: if chosen {
theme.palette().primary.base.color
} else {
theme.palette().background.strong.color
},
};
if chosen {
style.text_color = theme.palette().primary.base.color;
}
style
})
.into()
}
pub fn view<'a>(
state: &'a RefEditDialogState,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'a, Message> {
let current = state.levels.get(state.level);
let preview: Element<'a, Message> = container(match current {
Some(level) => crate::ui::window::block_palette::preview_canvas(&level.wires, 104.0),
None => Space::new().into(),
})
.width(Length::Fixed(150.0))
.height(Length::Fixed(104.0))
.padding(4)
.style(well_style)
.into();
let mut chips = row![].spacing(6).align_y(iced::Center);
for (index, level) in state.levels.iter().enumerate() {
if index > 0 {
chips = chips.push(text("›").size(13).style(muted_style));
}
chips = chips.push(level_chip(&level.name, index, index == state.level));
}
let mut about = column![text(crate::tr!("ref-edit", "dialog-chain-hint"))
.size(11)
.style(muted_style)]
.spacing(4);
if let Some(path) = current.and_then(|l| l.xref_path.as_deref()) {
// A long path keeps its end, where the file name is.
const MAX: usize = 64;
let count = path.chars().count();
let shown = if count > MAX {
format!("…{}", path.chars().skip(count - (MAX - 1)).collect::<String>())
} else {
path.to_string()
};
about = about.push(
text(format!("{} {}", crate::tr!("ref-edit", "dialog-path"), shown))
.size(11)
.style(muted_style),
);
}
let reference = card(
crate::tr!("ref-edit", "dialog-reference"),
row![
preview,
column![iced::widget::scrollable(chips).direction(
iced::widget::scrollable::Direction::Horizontal(
iced::widget::scrollable::Scrollbar::new().width(4).scroller_width(4)
)
), about]
.spacing(8)
.width(Fill)
]
.spacing(12),
);
let segment = |nested: bool, name: String| {
button(text(name).size(12).width(Fill).align_x(iced::Center))
.on_press(msg(RefEditDialogMsg::Nested(nested)))
.style(button_style(state.nested == nested))
.padding([6, 8])
.width(Fill)
};
let method = container(
row![
segment(false, crate::tr!("ref-edit", "dialog-all-objects")),
segment(true, crate::tr!("ref-edit", "dialog-pick-objects")),
]
.spacing(2),
)
.padding(2)
.width(Fill)
.style(well_style);
let attributes_enabled = state.attributes_enabled();
let working_set = card(
crate::tr!("ref-edit", "dialog-working-set"),
column![
method,
switch_row(
crate::tr!("ref-edit", "dialog-attributes"),
state.attributes && attributes_enabled,
attributes_enabled.then_some(RefEditDialogMsg::Attributes as fn(bool) -> _),
),
]
.spacing(10),
);
let session = card(
crate::tr!("ref-edit", "dialog-session"),
column![
switch_row(
crate::tr!("ref-edit", "dialog-lock"),
state.lock,
Some(RefEditDialogMsg::Lock),
),
switch_row(
crate::tr!("ref-edit", "dialog-unique"),
state.unique,
Some(RefEditDialogMsg::Unique),
),
]
.spacing(10),
);
let footer = row![
button(text("?").size(12))
.on_press(msg(RefEditDialogMsg::Help))
.style(button_style(false))
.padding([5, 11]),
Space::new().width(Fill),
dialog_button(crate::t!("Cancel"), Message::CloseModal, false),
dialog_button(
std::borrow::Cow::Owned(crate::tr!("ref-edit", "dialog-start")),
msg(RefEditDialogMsg::Ok),
true
),
]
.spacing(6)
.align_y(iced::Center);
column![reference, working_set, session, footer]
.spacing(10)
.padding([10, 12])
.width(sizing.width)
.into()
}