forked from 1b-agent/opencadstudio-upstream
Fix Properties hyperlink collection editor
This commit is contained in:
1 parent
bb29b987c2
commit
6a1c649016
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,
|
||||
|
||||
@@ -772,6 +772,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).
|
||||
@@ -1823,6 +1829,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,
|
||||
@@ -2648,6 +2656,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).
|
||||
@@ -3904,6 +3919,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,
|
||||
_ => {}
|
||||
|
||||
@@ -200,6 +200,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) => {
|
||||
@@ -4085,6 +4092,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(),
|
||||
@@ -1595,6 +1596,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();
|
||||
@@ -1727,6 +1736,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.
|
||||
|
||||
Vendored
+5
-2
@@ -30,7 +30,10 @@ pub fn general_section(entity: &EntityType) -> PropSection {
|
||||
},
|
||||
);
|
||||
|
||||
let hyperlink = crate::scene::pe_url_of(entity).unwrap_or_default().to_owned();
|
||||
let hyperlink = crate::scene::pe_url_description_of(entity)
|
||||
.or_else(|| crate::scene::pe_url_of(entity))
|
||||
.unwrap_or_default()
|
||||
.to_owned();
|
||||
|
||||
let section = PropSection {
|
||||
title: t!("General").into_owned(),
|
||||
@@ -83,7 +86,7 @@ pub fn general_section(entity: &EntityType) -> PropSection {
|
||||
Property {
|
||||
label: t!("Hyperlink").into_owned(),
|
||||
field: "hyperlink",
|
||||
value: PropValue::PlainText(hyperlink),
|
||||
value: PropValue::Hyperlink(hyperlink),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ mod preview;
|
||||
mod project;
|
||||
mod scene_markers;
|
||||
mod selection;
|
||||
pub(crate) use selection::pe_url_of;
|
||||
pub(crate) use selection::{pe_url_description_of, pe_url_of};
|
||||
pub mod parametric_constraints;
|
||||
mod parametric_solve;
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ pub enum PropValue {
|
||||
EditText(String),
|
||||
/// Editable text that must not be expression-evaluated.
|
||||
PlainText(String),
|
||||
/// Hyperlink collection summary. The value is display-only in the row;
|
||||
/// the adjacent collection button opens the URL/description editor.
|
||||
Hyperlink(String),
|
||||
/// Layer name — rendered as a combo_box.
|
||||
LayerChoice(String),
|
||||
/// Generic string choice rendered as a combo_box.
|
||||
|
||||
+19
-2
@@ -12,6 +12,20 @@ pub(crate) fn pe_url_of(entity: &EntityType) -> Option<&str> {
|
||||
.filter(|text| !text.is_empty())
|
||||
}
|
||||
|
||||
/// Optional description stored after the URL in the standard PE_URL record.
|
||||
pub(crate) fn pe_url_description_of(entity: &EntityType) -> Option<&str> {
|
||||
let record = entity.common().extended_data.get_record("PE_URL")?;
|
||||
record
|
||||
.values
|
||||
.iter()
|
||||
.filter_map(|value| match value {
|
||||
acadrust::xdata::XDataValue::String(text) => Some(text.trim()),
|
||||
_ => None,
|
||||
})
|
||||
.nth(1)
|
||||
.filter(|text| !text.is_empty())
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
// ── Selection ─────────────────────────────────────────────────────────
|
||||
/// Treat a classic LEADER and its attached annotation as one logical object.
|
||||
@@ -670,7 +684,9 @@ impl Scene {
|
||||
])
|
||||
} else {
|
||||
match prop.value {
|
||||
PropValue::PlainText(_) => QSelectValueEditor::Text,
|
||||
PropValue::PlainText(_) | PropValue::Hyperlink(_) => {
|
||||
QSelectValueEditor::Text
|
||||
}
|
||||
PropValue::ReadOnly(ref value)
|
||||
| PropValue::ReadOnlyWithTooltip { ref value, .. }
|
||||
| PropValue::EditText(ref value) => {
|
||||
@@ -872,7 +888,8 @@ impl Scene {
|
||||
PropValue::ReadOnly(s)
|
||||
| PropValue::ReadOnlyWithTooltip { value: s, .. }
|
||||
| PropValue::EditText(s)
|
||||
| PropValue::PlainText(s) => s,
|
||||
| PropValue::PlainText(s)
|
||||
| PropValue::Hyperlink(s) => s,
|
||||
PropValue::LayerChoice(s) => s,
|
||||
PropValue::Choice { selected, .. } => selected,
|
||||
PropValue::EditChoice { value, .. } => value,
|
||||
|
||||
@@ -854,6 +854,7 @@ impl PropertiesPanel {
|
||||
PropValue::EditText(val) | PropValue::PlainText(val) => {
|
||||
self.render_edit_row(label, prop.field, val)
|
||||
}
|
||||
PropValue::Hyperlink(val) => render_hyperlink_row(label, val),
|
||||
PropValue::ReadOnly(val) if prop.field == "annotative_scale" => {
|
||||
render_annotative_scale_row(label, val)
|
||||
}
|
||||
@@ -1892,6 +1893,18 @@ fn render_annotative_scale_row<'a>(
|
||||
|
||||
prop_row_widget(label, controls.into())
|
||||
}
|
||||
fn render_hyperlink_row<'a>(label: &'a str, value: &'a str) -> Element<'a, Message> {
|
||||
let field = crate::ui::read_only::field(value, FONT_SZ, Length::Fill);
|
||||
let manage = button(text("...").size(FONT_SZ))
|
||||
.on_press(Message::PropHyperlinkOpen)
|
||||
.style(button::secondary)
|
||||
.padding([2, 7]);
|
||||
let controls = row![field, manage, iced::widget::space().width(10)]
|
||||
.spacing(2)
|
||||
.align_y(iced::Center)
|
||||
.width(Length::Fill);
|
||||
prop_row_widget(label, controls.into())
|
||||
}
|
||||
fn render_ro_row<'a>(label: &'a str, value: &'a str) -> Element<'a, Message> {
|
||||
// A read-only value is shown as a non-editable but selectable field: no
|
||||
// on_input means the caret never appears, but the text can be selected
|
||||
|
||||
Reference in new issue
Block a user