forked from 1b-agent/opencadstudio-upstream
Merge pull request #1338 from ramox81/codex/properties-hyperlink-control
Fix Properties hyperlink collection editing
This commit is contained in:
commit
37b24296d8
11 files changed
+272
-13
No files matched your search
@@ -1163,7 +1163,7 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
}
|
||||
cmd if cmd.starts_with("HYPERLINK ") => {
|
||||
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
||||
use acadrust::xdata::XDataValue;
|
||||
let url = cmd.strip_prefix("HYPERLINK").unwrap_or("").trim().to_string();
|
||||
if url.is_empty() {
|
||||
self.command_line.push_info(crate::t!("Usage: HYPERLINK <url> (select objects first)").as_ref());
|
||||
@@ -1183,15 +1183,19 @@ impl OpenCADStudio {
|
||||
self.push_undo_snapshot(i, "HYPERLINK");
|
||||
let mut n = 0usize;
|
||||
for h in &handles {
|
||||
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
|
||||
let xd = &mut e.common_mut().extended_data;
|
||||
let mut rec = ExtendedDataRecord::new("PE_URL");
|
||||
rec.add_value(XDataValue::String(url.clone()));
|
||||
xd.add_record(rec);
|
||||
if self.tabs[i].scene.document.get_entity(*h).is_some() {
|
||||
crate::scene::view::dispatch::set_entity_xdata(
|
||||
&mut self.tabs[i].scene.document,
|
||||
*h,
|
||||
"PE_URL",
|
||||
Some(vec![XDataValue::String(url.clone())]),
|
||||
);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
self.invalidate_property_targets(i, &handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
self.command_line
|
||||
.push_output(crate::tf!("HYPERLINK: attached to {n} object(s).").as_ref());
|
||||
return Some(Task::none());
|
||||
|
||||
@@ -122,6 +122,7 @@ impl OpenCADStudio {
|
||||
PropValue::ReadOnly(v)|PropValue::ReadOnlyWithTooltip{value:v,..}=>("readonly",json!(v),Value::Null),
|
||||
PropValue::EditText(v)=>("number",json!(v),Value::Null),
|
||||
PropValue::PlainText(v)=>("text",json!(v),Value::Null),
|
||||
PropValue::Hyperlink(v)=>("hyperlink",json!(v),Value::Null),
|
||||
PropValue::Choice{selected,options}=>("choice",json!(selected),json!(options)),
|
||||
PropValue::EditChoice{value,options}=>("editable_choice",json!(value),json!(options)),
|
||||
PropValue::LayerChoice(v)=>("layer",json!(v),Value::Null),
|
||||
@@ -157,7 +158,10 @@ impl OpenCADStudio {
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| req["value"].to_string());
|
||||
Ok(match p.value {
|
||||
PropValue::EditText(_) | PropValue::PlainText(_) | PropValue::EditChoice { .. } => {
|
||||
PropValue::EditText(_)
|
||||
| PropValue::PlainText(_)
|
||||
| PropValue::Hyperlink(_)
|
||||
| PropValue::EditChoice { .. } => {
|
||||
let input = self.update(Message::PropGeomInput {
|
||||
field: p.field,
|
||||
value,
|
||||
|
||||
@@ -803,6 +803,12 @@ pub(super) struct OpenCADStudio {
|
||||
/// The open in-canvas modal dialog, if any (Plan B: shared overlay instead
|
||||
/// of OS windows).
|
||||
active_modal: Option<ModalKind>,
|
||||
/// Selection and staged values owned by the Properties hyperlink dialog.
|
||||
hyperlink_editor_handles: Vec<acadrust::Handle>,
|
||||
hyperlink_editor_url: String,
|
||||
hyperlink_editor_description: String,
|
||||
hyperlink_editor_mixed: bool,
|
||||
hyperlink_editor_dirty: bool,
|
||||
pending_startup_modals: std::collections::VecDeque<ModalKind>,
|
||||
/// What is drawing the scene, once the first frame has told us. Drives
|
||||
/// the graphics warning (popup, status-bar pill, command line).
|
||||
@@ -1862,6 +1868,8 @@ pub enum ModalKind {
|
||||
/// Add / remove the annotation scales a single selected object has a
|
||||
/// per-object representation for.
|
||||
AnnoObjectScale,
|
||||
/// URL/description collection editor opened by the Hyperlink property row.
|
||||
Hyperlink,
|
||||
InsertTable,
|
||||
DataLinkManager,
|
||||
DataExtraction,
|
||||
@@ -2703,6 +2711,13 @@ pub enum Message {
|
||||
ScaleManagerOpen,
|
||||
/// Open the Annotation Object Scale dialog for the current single selection.
|
||||
AnnoObjectScaleOpen,
|
||||
/// Open and edit the selected objects' PE_URL hyperlink collection.
|
||||
PropHyperlinkOpen,
|
||||
HyperlinkUrlChanged(String),
|
||||
HyperlinkDescriptionChanged(String),
|
||||
HyperlinkApply,
|
||||
HyperlinkRemove,
|
||||
HyperlinkCancel,
|
||||
/// Toggle whether the dialog's object has a representation for this scale.
|
||||
AnnoObjectScaleToggle(String),
|
||||
/// Select a scale row in the manager (loads it into the editor).
|
||||
@@ -3980,6 +3995,11 @@ impl OpenCADStudio {
|
||||
color_picker_tab: ColorPickerTab::Index,
|
||||
recent_colors: Vec::new(),
|
||||
active_modal: None,
|
||||
hyperlink_editor_handles: Vec::new(),
|
||||
hyperlink_editor_url: String::new(),
|
||||
hyperlink_editor_description: String::new(),
|
||||
hyperlink_editor_mixed: false,
|
||||
hyperlink_editor_dirty: false,
|
||||
pending_startup_modals: std::collections::VecDeque::new(),
|
||||
gpu_status: crate::scene::pipeline::GpuStatus::Unknown,
|
||||
gpu_status_generation: 0,
|
||||
|
||||
@@ -3234,6 +3234,7 @@ fn make_sections_read_only(sections: &mut [crate::scene::model::object::PropSect
|
||||
| PropValue::ReadOnlyWithTooltip { value, .. }
|
||||
| PropValue::EditText(value)
|
||||
| PropValue::PlainText(value)
|
||||
| PropValue::Hyperlink(value)
|
||||
| PropValue::LayerChoice(value)
|
||||
| PropValue::LinetypeChoice(value)
|
||||
| PropValue::HatchPatternChoice(value) => value.clone(),
|
||||
@@ -3508,6 +3509,9 @@ fn merge_prop_value(
|
||||
(PropValue::PlainText(_), PropValue::PlainText(_)) => {
|
||||
PropValue::PlainText(VARIES_LABEL.into())
|
||||
}
|
||||
(PropValue::Hyperlink(_), PropValue::Hyperlink(_)) => {
|
||||
PropValue::Hyperlink(VARIES_LABEL.into())
|
||||
}
|
||||
(PropValue::ReadOnly(_), PropValue::ReadOnly(_)) => {
|
||||
PropValue::ReadOnly(VARIES_LABEL.into())
|
||||
}
|
||||
@@ -3581,7 +3585,8 @@ fn update_row_text(
|
||||
match &mut row.value {
|
||||
PropValue::ReadOnly(current)
|
||||
| PropValue::EditText(current)
|
||||
| PropValue::PlainText(current) => *current = value,
|
||||
| PropValue::PlainText(current)
|
||||
| PropValue::Hyperlink(current) => *current = value,
|
||||
PropValue::ReadOnlyWithTooltip { value: current, .. } => *current = value,
|
||||
PropValue::Choice { selected, .. } => *selected = value,
|
||||
_ => {}
|
||||
|
||||
@@ -201,6 +201,13 @@ impl OpenCADStudio {
|
||||
self.attr_editor_tab = crate::ui::window::attribute_editor::AttrTab::Attribute;
|
||||
}
|
||||
Some(GeometricTolerance) => self.geometric_tolerance = None,
|
||||
Some(Hyperlink) => {
|
||||
self.hyperlink_editor_handles.clear();
|
||||
self.hyperlink_editor_url.clear();
|
||||
self.hyperlink_editor_description.clear();
|
||||
self.hyperlink_editor_mixed = false;
|
||||
self.hyperlink_editor_dirty = false;
|
||||
}
|
||||
// Closing (✕) discards edits made since the last Apply — matching the
|
||||
// style editors. Committing happens only through the Apply button.
|
||||
Some(Aliases) => {
|
||||
@@ -4131,6 +4138,106 @@ impl OpenCADStudio {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::PropHyperlinkOpen => {
|
||||
let i = self.active_tab;
|
||||
let handles = self.property_target_handles(i);
|
||||
if handles.is_empty() {
|
||||
return Task::none();
|
||||
}
|
||||
let mut first: Option<(String, String)> = None;
|
||||
let mut mixed = false;
|
||||
for handle in &handles {
|
||||
let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) else {
|
||||
continue;
|
||||
};
|
||||
let current = (
|
||||
crate::scene::pe_url_of(entity).unwrap_or_default().to_owned(),
|
||||
crate::scene::pe_url_description_of(entity)
|
||||
.unwrap_or_default()
|
||||
.to_owned(),
|
||||
);
|
||||
if first.as_ref().is_some_and(|value| value != ¤t) {
|
||||
mixed = true;
|
||||
} else if first.is_none() {
|
||||
first = Some(current);
|
||||
}
|
||||
}
|
||||
let (url, description) = if mixed {
|
||||
(String::new(), String::new())
|
||||
} else {
|
||||
first.unwrap_or_default()
|
||||
};
|
||||
self.hyperlink_editor_handles = handles;
|
||||
self.hyperlink_editor_url = url;
|
||||
self.hyperlink_editor_description = description;
|
||||
self.hyperlink_editor_mixed = mixed;
|
||||
self.hyperlink_editor_dirty = false;
|
||||
self.active_modal = Some(crate::app::ModalKind::Hyperlink);
|
||||
Task::none()
|
||||
}
|
||||
Message::HyperlinkUrlChanged(value) => {
|
||||
self.hyperlink_editor_url = value;
|
||||
self.hyperlink_editor_dirty = true;
|
||||
Task::none()
|
||||
}
|
||||
Message::HyperlinkDescriptionChanged(value) => {
|
||||
self.hyperlink_editor_description = value;
|
||||
self.hyperlink_editor_dirty = true;
|
||||
Task::none()
|
||||
}
|
||||
Message::HyperlinkApply => {
|
||||
if !self.hyperlink_editor_dirty {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
}
|
||||
let url = self.hyperlink_editor_url.trim().to_owned();
|
||||
if self.hyperlink_editor_mixed && url.is_empty() {
|
||||
self.command_line.push_info(
|
||||
crate::t!("Enter a URL, or use Remove to clear all hyperlinks.").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let description = self.hyperlink_editor_description.trim().to_owned();
|
||||
let values = if url.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut values = vec![acadrust::xdata::XDataValue::String(url)];
|
||||
if !description.is_empty() {
|
||||
values.push(acadrust::xdata::XDataValue::String(description));
|
||||
}
|
||||
Some(values)
|
||||
};
|
||||
let i = self.active_tab;
|
||||
let handles = self.hyperlink_editor_handles.clone();
|
||||
self.apply_property_op(i, "HYPERLINK", &handles, |app, handle| {
|
||||
crate::scene::view::dispatch::set_entity_xdata(
|
||||
&mut app.tabs[i].scene.document,
|
||||
handle,
|
||||
"PE_URL",
|
||||
values.clone(),
|
||||
);
|
||||
});
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
Message::HyperlinkRemove => {
|
||||
let i = self.active_tab;
|
||||
let handles = self.hyperlink_editor_handles.clone();
|
||||
self.apply_property_op(i, "HYPERLINK", &handles, |app, handle| {
|
||||
crate::scene::view::dispatch::set_entity_xdata(
|
||||
&mut app.tabs[i].scene.document,
|
||||
handle,
|
||||
"PE_URL",
|
||||
None,
|
||||
);
|
||||
});
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
Message::HyperlinkCancel => {
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
Message::AnnoObjectScaleToggle(name) => {
|
||||
let i = self.active_tab;
|
||||
if let Some(entity) = self.anno_object_scale_target {
|
||||
|
||||
@@ -32,6 +32,7 @@ impl OpenCADStudio {
|
||||
Some(K::LayoutManager) => crate::tr!("modal", "layout-manager"),
|
||||
Some(K::ScaleManager) => crate::tr!("modal", "scale-manager"),
|
||||
Some(K::AnnoObjectScale) => crate::tr!("modal", "annotation-object-scale"),
|
||||
Some(K::Hyperlink) => crate::t!("Hyperlink").into_owned(),
|
||||
Some(K::InsertTable) => crate::t!("Insert Table").into_owned(),
|
||||
Some(K::DataLinkManager) => crate::t!("Data Link Manager").into_owned(),
|
||||
Some(K::DataExtraction) => crate::t!("Data Extraction Wizard").into_owned(),
|
||||
@@ -1602,6 +1603,14 @@ impl OpenCADStudio {
|
||||
)
|
||||
},
|
||||
),
|
||||
super::super::ModalKind::Hyperlink => sized_flow(ex, 560, 260, |flow| {
|
||||
hyperlink_dialog_window(
|
||||
&self.hyperlink_editor_url,
|
||||
&self.hyperlink_editor_description,
|
||||
self.hyperlink_editor_mixed,
|
||||
flow,
|
||||
)
|
||||
}),
|
||||
super::super::ModalKind::AttributeEditor => {
|
||||
let doc = &self.tabs[self.active_tab].scene.document;
|
||||
let layers: Vec<String> = doc.layers.iter().map(|l| l.name.clone()).collect();
|
||||
@@ -1734,6 +1743,80 @@ fn dialog_muted_text_style(theme: &Theme) -> iced::widget::text::Style {
|
||||
}
|
||||
}
|
||||
|
||||
fn hyperlink_dialog_window<'a>(
|
||||
url: &'a str,
|
||||
description: &'a str,
|
||||
mixed: bool,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let label = |value: Cow<'static, str>| {
|
||||
text(value)
|
||||
.size(11)
|
||||
.style(dialog_muted_text_style)
|
||||
.width(90)
|
||||
};
|
||||
let mut items: Vec<Element<'a, Message>> = Vec::new();
|
||||
if mixed {
|
||||
items.push(
|
||||
text(t!("Selected objects have different hyperlink values."))
|
||||
.size(11)
|
||||
.style(dialog_muted_text_style)
|
||||
.into(),
|
||||
);
|
||||
items.push(Space::new().height(8).into());
|
||||
}
|
||||
items.push(
|
||||
row![
|
||||
label(t!("URL:")),
|
||||
iced::widget::text_input("https://", url)
|
||||
.on_input(Message::HyperlinkUrlChanged)
|
||||
.size(13)
|
||||
.padding([5, 8])
|
||||
.width(Fill),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Alignment::Center)
|
||||
.width(sizing.width)
|
||||
.into(),
|
||||
);
|
||||
items.push(Space::new().height(8).into());
|
||||
items.push(
|
||||
row![
|
||||
label(t!("Description:")),
|
||||
iced::widget::text_input("", description)
|
||||
.on_input(Message::HyperlinkDescriptionChanged)
|
||||
.size(13)
|
||||
.padding([5, 8])
|
||||
.width(Fill),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Alignment::Center)
|
||||
.width(sizing.width)
|
||||
.into(),
|
||||
);
|
||||
items.push(Space::new().height(Fill).into());
|
||||
items.push(
|
||||
row![
|
||||
dialog_button(t!("Remove"), Message::HyperlinkRemove, button::danger),
|
||||
Space::new().width(Fill),
|
||||
dialog_button(t!("Cancel"), Message::HyperlinkCancel, button::secondary),
|
||||
Space::new().width(8),
|
||||
dialog_button(t!("OK"), Message::HyperlinkApply, button::primary),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.into(),
|
||||
);
|
||||
container(
|
||||
column(items)
|
||||
.spacing(0)
|
||||
.width(sizing.width)
|
||||
.height(sizing.height),
|
||||
)
|
||||
.style(dialog_body_style)
|
||||
.padding([14, 16])
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Compact Save-As options dialog: pick the format/version and a default file
|
||||
/// name. The destination folder and overwrite confirmation come from the
|
||||
/// native OS save dialog (native) or the browser download (web) that follows.
|
||||
|
||||
Reference in new issue
Block a user