forked from 1b-agent/opencadstudio-upstream
1 parent
e07f4f959b
commit
afc94dce13
6 files changed
+722
-163
No files matched your search
+3
-4
@@ -1720,12 +1720,11 @@ impl OpenCADStudio {
|
||||
};
|
||||
|
||||
let qselect_layer: Element<'_, Message> = if let Some(state) = &self.qselect {
|
||||
let types = tab.scene.entity_type_names_in_layout();
|
||||
let properties = tab.scene.qselect_properties(state.type_filter.as_deref());
|
||||
qselect_overlay(
|
||||
state,
|
||||
&types,
|
||||
&properties,
|
||||
&state.available_types,
|
||||
&state.available_properties,
|
||||
state.candidate_count,
|
||||
self.modal_offset,
|
||||
self.modal_resize,
|
||||
)
|
||||
|
||||
+179
-75
@@ -1199,16 +1199,14 @@ pub(super) fn snap_override_overlay(pos: iced::Point) -> Element<'static, Messag
|
||||
const QSELECT_ANY_TYPE: &str = "(Any type)";
|
||||
const QSELECT_ANY_PROP: &str = "(Any property)";
|
||||
|
||||
/// Floating panel for the Quick Select feature. Single-row filter:
|
||||
/// object type → property → operator → value, plus an "Append to current
|
||||
/// selection" checkbox. The property dropdown is type-aware — Common
|
||||
/// properties (Layer, Color, Linetype, Lineweight) are always shown;
|
||||
/// picking a specific Object type adds that type's `geometry_properties`
|
||||
/// fields (Start X, Length, Radius, …) so type-specific filtering works.
|
||||
/// Quick Select uses the application's shared movable modal frame. Its form
|
||||
/// groups scope, filter, and result behavior while reusing the same compact
|
||||
/// field and button styles as the other workspace dialogs.
|
||||
pub(super) fn qselect_overlay<'a>(
|
||||
state: &'a crate::app::QSelectState,
|
||||
types: &[String],
|
||||
properties: &[(String, String)],
|
||||
properties: &[crate::app::QSelectPropertyChoice],
|
||||
candidate_count: usize,
|
||||
modal_offset: iced::Vector,
|
||||
modal_resize: iced::Vector,
|
||||
) -> Element<'a, Message> {
|
||||
@@ -1216,12 +1214,14 @@ pub(super) fn qselect_overlay<'a>(
|
||||
state,
|
||||
types,
|
||||
properties,
|
||||
candidate_count,
|
||||
crate::ui::modal::ModalSizing::INTRINSIC,
|
||||
);
|
||||
let content = qselect_content(
|
||||
state,
|
||||
types,
|
||||
properties,
|
||||
candidate_count,
|
||||
crate::ui::modal::ModalSizing::FILL,
|
||||
);
|
||||
let content = crate::ui::modal::intrinsic(
|
||||
@@ -1244,10 +1244,11 @@ pub(super) fn qselect_overlay<'a>(
|
||||
fn qselect_content<'a>(
|
||||
state: &'a crate::app::QSelectState,
|
||||
types: &[String],
|
||||
properties: &[(String, String)],
|
||||
properties: &[crate::app::QSelectPropertyChoice],
|
||||
candidate_count: usize,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
use iced::widget::{checkbox};
|
||||
use iced::widget::{checkbox, radio, rule};
|
||||
let mut type_options: Vec<String> = vec![QSELECT_ANY_TYPE.to_string()];
|
||||
type_options.extend(types.iter().cloned());
|
||||
|
||||
@@ -1255,21 +1256,22 @@ fn qselect_content<'a>(
|
||||
vec![crate::app::QSelectPropertyChoice {
|
||||
field: String::new(),
|
||||
label: QSELECT_ANY_PROP.to_string(),
|
||||
editor: crate::app::QSelectValueEditor::Text,
|
||||
}];
|
||||
prop_options.extend(properties.iter().map(|(field, label)| {
|
||||
crate::app::QSelectPropertyChoice {
|
||||
field: field.clone(),
|
||||
label: label.clone(),
|
||||
}
|
||||
}));
|
||||
prop_options.extend(properties.iter().cloned());
|
||||
|
||||
let op_options: Vec<crate::app::QSelectOp> = vec![
|
||||
let number_property = state.property.as_ref().is_some_and(|property| {
|
||||
matches!(property.editor, crate::app::QSelectValueEditor::Number)
|
||||
});
|
||||
let mut op_options: Vec<crate::app::QSelectOp> = vec![
|
||||
crate::app::QSelectOp::Eq,
|
||||
crate::app::QSelectOp::Neq,
|
||||
crate::app::QSelectOp::Gt,
|
||||
crate::app::QSelectOp::Lt,
|
||||
crate::app::QSelectOp::Any,
|
||||
];
|
||||
if number_property {
|
||||
op_options.push(crate::app::QSelectOp::Gt);
|
||||
op_options.push(crate::app::QSelectOp::Lt);
|
||||
}
|
||||
op_options.push(crate::app::QSelectOp::Any);
|
||||
|
||||
let type_sel = state
|
||||
.type_filter
|
||||
@@ -1281,11 +1283,9 @@ fn qselect_content<'a>(
|
||||
.unwrap_or(crate::app::QSelectPropertyChoice {
|
||||
field: String::new(),
|
||||
label: QSELECT_ANY_PROP.to_string(),
|
||||
editor: crate::app::QSelectValueEditor::Text,
|
||||
});
|
||||
|
||||
// The value field is disabled (visually de-emphasised; we still
|
||||
// render the same widget) when no property is picked or the
|
||||
// operator is "*Any value" — both of those skip the value test.
|
||||
let value_enabled =
|
||||
state.property.is_some() && !matches!(state.operator, crate::app::QSelectOp::Any);
|
||||
let field_width = if matches!(sizing.width, iced::Length::Fill) {
|
||||
@@ -1293,49 +1293,121 @@ fn qselect_content<'a>(
|
||||
} else {
|
||||
iced::Length::Shrink
|
||||
};
|
||||
let flex_width = if matches!(sizing.width, iced::Length::Fill) {
|
||||
Fill
|
||||
} else {
|
||||
iced::Length::Shrink
|
||||
};
|
||||
|
||||
let label = |s: std::borrow::Cow<'static, str>| {
|
||||
text(s)
|
||||
.size(12)
|
||||
.width(iced::Length::Fixed(90.0))
|
||||
.width(iced::Length::Fixed(112.0))
|
||||
};
|
||||
let section_label = |s: std::borrow::Cow<'static, str>| {
|
||||
text(s).size(11).style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.65)),
|
||||
})
|
||||
};
|
||||
|
||||
let btn = |lbl: std::borrow::Cow<'static, str>, msg: Message, primary: bool| {
|
||||
button(text(lbl).size(12))
|
||||
.on_press(msg)
|
||||
.style(move |theme: &Theme, st| {
|
||||
let palette = theme.palette();
|
||||
let pair = match (
|
||||
primary,
|
||||
matches!(st, button::Status::Hovered | button::Status::Pressed),
|
||||
) {
|
||||
(true, true) => palette.primary.strong,
|
||||
(true, false) => palette.primary.base,
|
||||
(false, true) => palette.background.strong,
|
||||
(false, false) => palette.background.weak,
|
||||
};
|
||||
button::Style {
|
||||
background: Some(Background::Color(pair.color)),
|
||||
text_color: pair.text,
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
let value_editor: Element<'a, Message> = match state.property.as_ref() {
|
||||
Some(property) => match &property.editor {
|
||||
crate::app::QSelectValueEditor::Choice(options) => {
|
||||
let selected = (!state.value.is_empty()).then(|| state.value.clone());
|
||||
let picker = iced::widget::pick_list(
|
||||
selected,
|
||||
options.clone(),
|
||||
|value| value.to_string(),
|
||||
)
|
||||
.width(field_width);
|
||||
if value_enabled {
|
||||
picker.on_select(Message::QSelectSetValue).into()
|
||||
} else {
|
||||
picker.into()
|
||||
}
|
||||
})
|
||||
.padding([4, 14])
|
||||
}
|
||||
crate::app::QSelectValueEditor::Text
|
||||
| crate::app::QSelectValueEditor::Number => {
|
||||
let mut input = text_input("", &state.value).size(12).width(field_width);
|
||||
if value_enabled {
|
||||
input = input.on_input(Message::QSelectSetValue);
|
||||
}
|
||||
input.into()
|
||||
}
|
||||
},
|
||||
None => text_input("", "").size(12).width(field_width).into(),
|
||||
};
|
||||
|
||||
let mut value_input = text_input("", &state.value)
|
||||
.size(12)
|
||||
.width(field_width);
|
||||
if value_enabled {
|
||||
value_input = value_input.on_input(Message::QSelectSetValue);
|
||||
let derived_error = if candidate_count == 0 {
|
||||
Some(crate::t!("No objects are available in this scope.").into_owned())
|
||||
} else if value_enabled && number_property
|
||||
&& crate::entities::common::parse_f64(&state.value).is_none()
|
||||
{
|
||||
Some(crate::t!("Enter a valid number.").into_owned())
|
||||
} else if value_enabled
|
||||
&& state.property.as_ref().is_some_and(|property| {
|
||||
matches!(property.editor, crate::app::QSelectValueEditor::Choice(_))
|
||||
})
|
||||
&& state.value.is_empty()
|
||||
{
|
||||
Some(crate::t!("Choose a value.").into_owned())
|
||||
} else if matches!(state.operator, crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt)
|
||||
&& !number_property
|
||||
{
|
||||
Some(crate::t!("This operator requires a numeric property.").into_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let error = state.error.clone().or(derived_error);
|
||||
|
||||
let scope_options = vec![
|
||||
crate::app::QSelectScope::CurrentSpace,
|
||||
crate::app::QSelectScope::CurrentSelection,
|
||||
];
|
||||
let scope_picker = iced::widget::pick_list(
|
||||
Some(state.scope),
|
||||
scope_options,
|
||||
|value| value.to_string(),
|
||||
)
|
||||
.on_select(Message::QSelectSetScope)
|
||||
.width(field_width);
|
||||
|
||||
let mut append = checkbox(state.append).size(14);
|
||||
if matches!(state.scope, crate::app::QSelectScope::CurrentSpace) {
|
||||
append = append.on_toggle(Message::QSelectSetAppend);
|
||||
}
|
||||
|
||||
let cancel = button(text(t!("Cancel")).size(12))
|
||||
.on_press(Message::QSelectClose)
|
||||
.style(button::subtle)
|
||||
.padding([5, 16]);
|
||||
let apply = button(text(t!("Apply")).size(12))
|
||||
.style(button::primary)
|
||||
.padding([5, 18]);
|
||||
let apply = if error.is_none() {
|
||||
apply.on_press(Message::QSelectApply)
|
||||
} else {
|
||||
apply
|
||||
};
|
||||
|
||||
let panel_body = column![
|
||||
section_label(t!("Scope")),
|
||||
Space::new().height(5),
|
||||
row![label(t!("Apply to:")), scope_picker]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8)
|
||||
.width(sizing.width),
|
||||
Space::new().height(4),
|
||||
text(format!("{} candidate object(s)", candidate_count))
|
||||
.size(11)
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().background.base.text.scale_alpha(0.65)),
|
||||
}),
|
||||
Space::new().height(10),
|
||||
container(rule::horizontal(1)).width(flex_width),
|
||||
Space::new().height(10),
|
||||
section_label(t!("Filter")),
|
||||
Space::new().height(5),
|
||||
row![
|
||||
label(t!("Object type:")),
|
||||
iced::widget::pick_list(
|
||||
@@ -1364,12 +1436,12 @@ fn qselect_content<'a>(
|
||||
|value| value.to_string(),
|
||||
)
|
||||
.on_select(|p: crate::app::QSelectPropertyChoice| {
|
||||
if p.field.is_empty() {
|
||||
Message::QSelectSetProperty(None)
|
||||
} else {
|
||||
Message::QSelectSetProperty(Some(p))
|
||||
}
|
||||
})
|
||||
if p.field.is_empty() {
|
||||
Message::QSelectSetProperty(None)
|
||||
} else {
|
||||
Message::QSelectSetProperty(Some(p))
|
||||
}
|
||||
})
|
||||
.width(field_width),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
@@ -1390,25 +1462,57 @@ fn qselect_content<'a>(
|
||||
.spacing(8)
|
||||
.width(sizing.width),
|
||||
Space::new().height(6),
|
||||
row![label(t!("Value:")), value_input,]
|
||||
row![label(t!("Value:")), value_editor]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8)
|
||||
.width(sizing.width),
|
||||
Space::new().height(10),
|
||||
container(rule::horizontal(1)).width(flex_width),
|
||||
Space::new().height(10),
|
||||
section_label(t!("Result")),
|
||||
Space::new().height(5),
|
||||
column![
|
||||
radio(
|
||||
t!("Include matching objects"),
|
||||
crate::app::QSelectMode::Include,
|
||||
Some(state.mode),
|
||||
Message::QSelectSetMode,
|
||||
)
|
||||
.size(14)
|
||||
.text_size(12),
|
||||
radio(
|
||||
t!("Exclude matching objects"),
|
||||
crate::app::QSelectMode::Exclude,
|
||||
Some(state.mode),
|
||||
Message::QSelectSetMode,
|
||||
)
|
||||
.size(14)
|
||||
.text_size(12),
|
||||
]
|
||||
.spacing(5),
|
||||
Space::new().height(8),
|
||||
row![
|
||||
checkbox(state.append)
|
||||
.on_toggle(Message::QSelectSetAppend)
|
||||
.size(14),
|
||||
append,
|
||||
Space::new().width(6),
|
||||
text(t!("Append to current selection")).size(12),
|
||||
text(t!("Append result to current selection")).size(12),
|
||||
]
|
||||
.align_y(iced::Alignment::Center),
|
||||
Space::new().height(14),
|
||||
Space::new().height(10),
|
||||
if let Some(message) = error {
|
||||
text(message)
|
||||
.size(11)
|
||||
.style(|theme: &Theme| iced::widget::text::Style {
|
||||
color: Some(theme.palette().danger.base.color),
|
||||
})
|
||||
} else {
|
||||
text("").size(11)
|
||||
},
|
||||
Space::new().height(8),
|
||||
row![
|
||||
Space::new().width(field_width),
|
||||
btn(t!("Cancel"), Message::QSelectClose, false),
|
||||
Space::new().width(flex_width),
|
||||
cancel,
|
||||
Space::new().width(8),
|
||||
btn(t!("Apply"), Message::QSelectApply, true),
|
||||
apply,
|
||||
]
|
||||
.align_y(iced::Alignment::Center),
|
||||
]
|
||||
@@ -1423,13 +1527,13 @@ fn qselect_content<'a>(
|
||||
.style(|theme: &Theme| {
|
||||
let palette = theme.palette();
|
||||
container::Style {
|
||||
background: Some(Background::Color(palette.background.weak.color)),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 6.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
background: Some(Background::Color(palette.background.weak.color)),
|
||||
border: Border {
|
||||
color: palette.background.neutral.color,
|
||||
width: 1.0,
|
||||
radius: 6.0.into(),
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user