parent
e07f4f959b
commit
afc94dce13
6 changed files with 721 additions and 162 deletions
|
|
@ -109,6 +109,38 @@ pub enum QSelectOp {
|
||||||
Lt,
|
Lt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Candidate set searched by Quick Select.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum QSelectScope {
|
||||||
|
CurrentSpace,
|
||||||
|
CurrentSelection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for QSelectScope {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let source = match self {
|
||||||
|
QSelectScope::CurrentSpace => "Current space",
|
||||||
|
QSelectScope::CurrentSelection => "Current selection",
|
||||||
|
};
|
||||||
|
f.write_str(crate::t!(source).as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether matching candidates are kept or removed from the result.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum QSelectMode {
|
||||||
|
Include,
|
||||||
|
Exclude,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Editor used by the Quick Select value field.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum QSelectValueEditor {
|
||||||
|
Text,
|
||||||
|
Number,
|
||||||
|
Choice(Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for QSelectOp {
|
impl std::fmt::Display for QSelectOp {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let s = match self {
|
let s = match self {
|
||||||
|
|
@ -131,6 +163,7 @@ impl std::fmt::Display for QSelectOp {
|
||||||
pub struct QSelectPropertyChoice {
|
pub struct QSelectPropertyChoice {
|
||||||
pub field: String,
|
pub field: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
|
pub editor: QSelectValueEditor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for QSelectPropertyChoice {
|
impl PartialEq for QSelectPropertyChoice {
|
||||||
|
|
@ -148,19 +181,22 @@ impl std::fmt::Display for QSelectPropertyChoice {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open Quick Select panel state. The filter is one
|
/// Open Quick Select panel state. The filter is one
|
||||||
/// `(type, property, op, value)` row plus an Append-to-current-selection
|
/// `(scope, type, property, op, value)` row plus result behavior.
|
||||||
/// toggle, mirroring the classic QSELECT dialog: the panel filters
|
|
||||||
/// candidate entities (entire layout) by type, then by the chosen
|
|
||||||
/// property compared to the typed value using the operator.
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct QSelectState {
|
pub struct QSelectState {
|
||||||
|
pub scope: QSelectScope,
|
||||||
|
pub available_types: Vec<String>,
|
||||||
|
pub available_properties: Vec<QSelectPropertyChoice>,
|
||||||
|
pub candidate_count: usize,
|
||||||
/// `None` = "(Any type)".
|
/// `None` = "(Any type)".
|
||||||
pub type_filter: Option<String>,
|
pub type_filter: Option<String>,
|
||||||
/// `None` = no property filter; the type filter alone applies.
|
/// `None` = no property filter; the type filter alone applies.
|
||||||
pub property: Option<QSelectPropertyChoice>,
|
pub property: Option<QSelectPropertyChoice>,
|
||||||
pub operator: QSelectOp,
|
pub operator: QSelectOp,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
pub mode: QSelectMode,
|
||||||
pub append: bool,
|
pub append: bool,
|
||||||
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
|
@ -2493,6 +2529,8 @@ pub enum Message {
|
||||||
QSelectOpen,
|
QSelectOpen,
|
||||||
/// Close the Quick Select panel without applying.
|
/// Close the Quick Select panel without applying.
|
||||||
QSelectClose,
|
QSelectClose,
|
||||||
|
/// Candidate scope: active space or the current selection.
|
||||||
|
QSelectSetScope(QSelectScope),
|
||||||
/// Type filter — `None` means "any type".
|
/// Type filter — `None` means "any type".
|
||||||
QSelectSetType(Option<String>),
|
QSelectSetType(Option<String>),
|
||||||
/// Property to compare. `None` means "no property filter — just type
|
/// Property to compare. `None` means "no property filter — just type
|
||||||
|
|
@ -2503,6 +2541,8 @@ pub enum Message {
|
||||||
QSelectSetOperator(QSelectOp),
|
QSelectSetOperator(QSelectOp),
|
||||||
/// Compare-against value (free-text input).
|
/// Compare-against value (free-text input).
|
||||||
QSelectSetValue(String),
|
QSelectSetValue(String),
|
||||||
|
/// Include or exclude objects matching the filter.
|
||||||
|
QSelectSetMode(QSelectMode),
|
||||||
/// Append-to-current-selection toggle.
|
/// Append-to-current-selection toggle.
|
||||||
QSelectSetAppend(bool),
|
QSelectSetAppend(bool),
|
||||||
/// Apply the current filter and close the panel.
|
/// Apply the current filter and close the panel.
|
||||||
|
|
|
||||||
|
|
@ -1313,12 +1313,24 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
type_filter = Some(entity_type_name(e).to_string());
|
type_filter = Some(entity_type_name(e).to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let scope = crate::app::QSelectScope::CurrentSpace;
|
||||||
|
let available_types = self.tabs[i].scene.qselect_entity_type_names(scope);
|
||||||
|
let available_properties = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.qselect_properties(type_filter.as_deref(), scope);
|
||||||
|
let candidate_count = self.tabs[i].scene.qselect_candidate_count(scope);
|
||||||
self.qselect = Some(crate::app::QSelectState {
|
self.qselect = Some(crate::app::QSelectState {
|
||||||
|
scope,
|
||||||
|
available_types,
|
||||||
|
available_properties,
|
||||||
|
candidate_count,
|
||||||
type_filter,
|
type_filter,
|
||||||
property: None,
|
property: None,
|
||||||
operator: crate::app::QSelectOp::Eq,
|
operator: crate::app::QSelectOp::Eq,
|
||||||
value: String::new(),
|
value: String::new(),
|
||||||
|
mode: crate::app::QSelectMode::Include,
|
||||||
append: false,
|
append: false,
|
||||||
|
error: None,
|
||||||
});
|
});
|
||||||
self.reset_modal_geometry();
|
self.reset_modal_geometry();
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
|
||||||
|
|
@ -3845,22 +3845,77 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::QSelectSetType(t) => {
|
Message::QSelectSetScope(scope) => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let available_types = self.tabs[i].scene.qselect_entity_type_names(scope);
|
||||||
|
let type_filter = self.qselect.as_ref().and_then(|state| {
|
||||||
|
state
|
||||||
|
.type_filter
|
||||||
|
.as_ref()
|
||||||
|
.filter(|selected| available_types.iter().any(|item| item == *selected))
|
||||||
|
.cloned()
|
||||||
|
});
|
||||||
|
let available_properties = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.qselect_properties(type_filter.as_deref(), scope);
|
||||||
|
let candidate_count = self.tabs[i].scene.qselect_candidate_count(scope);
|
||||||
if let Some(state) = self.qselect.as_mut() {
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
// Drop the property when it no longer applies to the
|
state.scope = scope;
|
||||||
// chosen type: type-specific fields like `start_x`
|
state.available_types = available_types;
|
||||||
// would otherwise stay selected but never match.
|
state.available_properties = available_properties;
|
||||||
let kept_property = state.property.clone().and_then(|p| {
|
state.candidate_count = candidate_count;
|
||||||
let i = self.active_tab;
|
state.type_filter = type_filter;
|
||||||
let props = self.tabs[i].scene.qselect_properties(t.as_deref());
|
state.property = state.property.as_ref().and_then(|selected| {
|
||||||
if props.iter().any(|(f, _)| f == &p.field) {
|
state
|
||||||
Some(p)
|
.available_properties
|
||||||
} else {
|
.iter()
|
||||||
None
|
.find(|available| available.field == selected.field)
|
||||||
}
|
.cloned()
|
||||||
|
});
|
||||||
|
state.value.clear();
|
||||||
|
state.error = None;
|
||||||
|
if matches!(scope, crate::app::QSelectScope::CurrentSelection) {
|
||||||
|
state.append = false;
|
||||||
|
}
|
||||||
|
if matches!(state.operator, crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt)
|
||||||
|
&& !state.property.as_ref().is_some_and(|property| {
|
||||||
|
matches!(property.editor, crate::app::QSelectValueEditor::Number)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
state.operator = crate::app::QSelectOp::Eq;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::QSelectSetType(t) => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let scope = self
|
||||||
|
.qselect
|
||||||
|
.as_ref()
|
||||||
|
.map_or(crate::app::QSelectScope::CurrentSpace, |state| state.scope);
|
||||||
|
let properties = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.qselect_properties(t.as_deref(), scope);
|
||||||
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
|
let kept_property = state.property.as_ref().and_then(|selected| {
|
||||||
|
properties
|
||||||
|
.iter()
|
||||||
|
.find(|available| available.field == selected.field)
|
||||||
|
.cloned()
|
||||||
});
|
});
|
||||||
state.type_filter = t;
|
state.type_filter = t;
|
||||||
|
state.available_properties = properties;
|
||||||
state.property = kept_property;
|
state.property = kept_property;
|
||||||
|
state.value.clear();
|
||||||
|
state.error = None;
|
||||||
|
if matches!(state.operator, crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt)
|
||||||
|
&& !state.property.as_ref().is_some_and(|property| {
|
||||||
|
matches!(property.editor, crate::app::QSelectValueEditor::Number)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
state.operator = crate::app::QSelectOp::Eq;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -3868,6 +3923,15 @@ impl OpenCADStudio {
|
||||||
Message::QSelectSetProperty(p) => {
|
Message::QSelectSetProperty(p) => {
|
||||||
if let Some(state) = self.qselect.as_mut() {
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
state.property = p;
|
state.property = p;
|
||||||
|
state.value.clear();
|
||||||
|
state.error = None;
|
||||||
|
if matches!(state.operator, crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt)
|
||||||
|
&& !state.property.as_ref().is_some_and(|property| {
|
||||||
|
matches!(property.editor, crate::app::QSelectValueEditor::Number)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
state.operator = crate::app::QSelectOp::Eq;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -3875,6 +3939,7 @@ impl OpenCADStudio {
|
||||||
Message::QSelectSetOperator(op) => {
|
Message::QSelectSetOperator(op) => {
|
||||||
if let Some(state) = self.qselect.as_mut() {
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
state.operator = op;
|
state.operator = op;
|
||||||
|
state.error = None;
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
@ -3882,29 +3947,91 @@ impl OpenCADStudio {
|
||||||
Message::QSelectSetValue(v) => {
|
Message::QSelectSetValue(v) => {
|
||||||
if let Some(state) = self.qselect.as_mut() {
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
state.value = v;
|
state.value = v;
|
||||||
|
state.error = None;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::QSelectSetMode(mode) => {
|
||||||
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
|
state.mode = mode;
|
||||||
|
state.error = None;
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::QSelectSetAppend(b) => {
|
Message::QSelectSetAppend(b) => {
|
||||||
if let Some(state) = self.qselect.as_mut() {
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
state.append = b;
|
if matches!(state.scope, crate::app::QSelectScope::CurrentSpace) {
|
||||||
|
state.append = b;
|
||||||
|
}
|
||||||
|
state.error = None;
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::QSelectApply => {
|
Message::QSelectApply => {
|
||||||
|
let validation_error = self.qselect.as_ref().and_then(|state| {
|
||||||
|
let candidate_count = self.tabs[self.active_tab]
|
||||||
|
.scene
|
||||||
|
.qselect_candidate_count(state.scope);
|
||||||
|
if candidate_count == 0 {
|
||||||
|
Some(crate::t!("No objects are available in this scope.").into_owned())
|
||||||
|
} else if let Some(property) = state.property.as_ref() {
|
||||||
|
if matches!(state.operator, crate::app::QSelectOp::Any) {
|
||||||
|
None
|
||||||
|
} else if matches!(
|
||||||
|
state.operator,
|
||||||
|
crate::app::QSelectOp::Gt | crate::app::QSelectOp::Lt
|
||||||
|
) && !matches!(
|
||||||
|
property.editor,
|
||||||
|
crate::app::QSelectValueEditor::Number
|
||||||
|
) {
|
||||||
|
Some(
|
||||||
|
crate::t!("This operator requires a numeric property.")
|
||||||
|
.into_owned(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
match &property.editor {
|
||||||
|
crate::app::QSelectValueEditor::Number
|
||||||
|
if crate::entities::common::parse_f64(&state.value).is_none() =>
|
||||||
|
{
|
||||||
|
Some(crate::t!("Enter a valid number.").into_owned())
|
||||||
|
}
|
||||||
|
crate::app::QSelectValueEditor::Choice(_)
|
||||||
|
if state.value.is_empty() =>
|
||||||
|
{
|
||||||
|
Some(crate::t!("Choose a value.").into_owned())
|
||||||
|
}
|
||||||
|
crate::app::QSelectValueEditor::Text
|
||||||
|
| crate::app::QSelectValueEditor::Number
|
||||||
|
| crate::app::QSelectValueEditor::Choice(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Some(error) = validation_error {
|
||||||
|
if let Some(state) = self.qselect.as_mut() {
|
||||||
|
state.error = Some(error);
|
||||||
|
}
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
let Some(state) = self.qselect.take() else {
|
let Some(state) = self.qselect.take() else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
self.reset_modal_geometry();
|
self.reset_modal_geometry();
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let matched = self.tabs[i].scene.qselect(
|
let matched = self.tabs[i].scene.qselect(
|
||||||
|
state.scope,
|
||||||
state.type_filter.as_deref(),
|
state.type_filter.as_deref(),
|
||||||
state.property.as_ref().map(|p| p.field.as_str()),
|
state.property.as_ref().map(|p| p.field.as_str()),
|
||||||
state.operator,
|
state.operator,
|
||||||
&state.value,
|
&state.value,
|
||||||
state.append,
|
state.mode,
|
||||||
|
state.append
|
||||||
|
&& matches!(state.scope, crate::app::QSelectScope::CurrentSpace),
|
||||||
);
|
);
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("QSELECT: {} object(s) selected.", matched).as_ref());
|
.push_output(crate::tf!("QSELECT: {} object(s) selected.", matched).as_ref());
|
||||||
|
|
|
||||||
|
|
@ -1720,12 +1720,11 @@ impl OpenCADStudio {
|
||||||
};
|
};
|
||||||
|
|
||||||
let qselect_layer: Element<'_, Message> = if let Some(state) = &self.qselect {
|
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(
|
qselect_overlay(
|
||||||
state,
|
state,
|
||||||
&types,
|
&state.available_types,
|
||||||
&properties,
|
&state.available_properties,
|
||||||
|
state.candidate_count,
|
||||||
self.modal_offset,
|
self.modal_offset,
|
||||||
self.modal_resize,
|
self.modal_resize,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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_TYPE: &str = "(Any type)";
|
||||||
const QSELECT_ANY_PROP: &str = "(Any property)";
|
const QSELECT_ANY_PROP: &str = "(Any property)";
|
||||||
|
|
||||||
/// Floating panel for the Quick Select feature. Single-row filter:
|
/// Quick Select uses the application's shared movable modal frame. Its form
|
||||||
/// object type → property → operator → value, plus an "Append to current
|
/// groups scope, filter, and result behavior while reusing the same compact
|
||||||
/// selection" checkbox. The property dropdown is type-aware — Common
|
/// field and button styles as the other workspace dialogs.
|
||||||
/// 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.
|
|
||||||
pub(super) fn qselect_overlay<'a>(
|
pub(super) fn qselect_overlay<'a>(
|
||||||
state: &'a crate::app::QSelectState,
|
state: &'a crate::app::QSelectState,
|
||||||
types: &[String],
|
types: &[String],
|
||||||
properties: &[(String, String)],
|
properties: &[crate::app::QSelectPropertyChoice],
|
||||||
|
candidate_count: usize,
|
||||||
modal_offset: iced::Vector,
|
modal_offset: iced::Vector,
|
||||||
modal_resize: iced::Vector,
|
modal_resize: iced::Vector,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
|
|
@ -1216,12 +1214,14 @@ pub(super) fn qselect_overlay<'a>(
|
||||||
state,
|
state,
|
||||||
types,
|
types,
|
||||||
properties,
|
properties,
|
||||||
|
candidate_count,
|
||||||
crate::ui::modal::ModalSizing::INTRINSIC,
|
crate::ui::modal::ModalSizing::INTRINSIC,
|
||||||
);
|
);
|
||||||
let content = qselect_content(
|
let content = qselect_content(
|
||||||
state,
|
state,
|
||||||
types,
|
types,
|
||||||
properties,
|
properties,
|
||||||
|
candidate_count,
|
||||||
crate::ui::modal::ModalSizing::FILL,
|
crate::ui::modal::ModalSizing::FILL,
|
||||||
);
|
);
|
||||||
let content = crate::ui::modal::intrinsic(
|
let content = crate::ui::modal::intrinsic(
|
||||||
|
|
@ -1244,10 +1244,11 @@ pub(super) fn qselect_overlay<'a>(
|
||||||
fn qselect_content<'a>(
|
fn qselect_content<'a>(
|
||||||
state: &'a crate::app::QSelectState,
|
state: &'a crate::app::QSelectState,
|
||||||
types: &[String],
|
types: &[String],
|
||||||
properties: &[(String, String)],
|
properties: &[crate::app::QSelectPropertyChoice],
|
||||||
|
candidate_count: usize,
|
||||||
sizing: crate::ui::modal::ModalSizing,
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
) -> Element<'a, Message> {
|
) -> 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()];
|
let mut type_options: Vec<String> = vec![QSELECT_ANY_TYPE.to_string()];
|
||||||
type_options.extend(types.iter().cloned());
|
type_options.extend(types.iter().cloned());
|
||||||
|
|
||||||
|
|
@ -1255,21 +1256,22 @@ fn qselect_content<'a>(
|
||||||
vec![crate::app::QSelectPropertyChoice {
|
vec![crate::app::QSelectPropertyChoice {
|
||||||
field: String::new(),
|
field: String::new(),
|
||||||
label: QSELECT_ANY_PROP.to_string(),
|
label: QSELECT_ANY_PROP.to_string(),
|
||||||
|
editor: crate::app::QSelectValueEditor::Text,
|
||||||
}];
|
}];
|
||||||
prop_options.extend(properties.iter().map(|(field, label)| {
|
prop_options.extend(properties.iter().cloned());
|
||||||
crate::app::QSelectPropertyChoice {
|
|
||||||
field: field.clone(),
|
|
||||||
label: label.clone(),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
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::Eq,
|
||||||
crate::app::QSelectOp::Neq,
|
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
|
let type_sel = state
|
||||||
.type_filter
|
.type_filter
|
||||||
|
|
@ -1281,11 +1283,9 @@ fn qselect_content<'a>(
|
||||||
.unwrap_or(crate::app::QSelectPropertyChoice {
|
.unwrap_or(crate::app::QSelectPropertyChoice {
|
||||||
field: String::new(),
|
field: String::new(),
|
||||||
label: QSELECT_ANY_PROP.to_string(),
|
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 =
|
let value_enabled =
|
||||||
state.property.is_some() && !matches!(state.operator, crate::app::QSelectOp::Any);
|
state.property.is_some() && !matches!(state.operator, crate::app::QSelectOp::Any);
|
||||||
let field_width = if matches!(sizing.width, iced::Length::Fill) {
|
let field_width = if matches!(sizing.width, iced::Length::Fill) {
|
||||||
|
|
@ -1293,49 +1293,121 @@ fn qselect_content<'a>(
|
||||||
} else {
|
} else {
|
||||||
iced::Length::Shrink
|
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>| {
|
let label = |s: std::borrow::Cow<'static, str>| {
|
||||||
text(s)
|
text(s)
|
||||||
.size(12)
|
.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| {
|
let value_editor: Element<'a, Message> = match state.property.as_ref() {
|
||||||
button(text(lbl).size(12))
|
Some(property) => match &property.editor {
|
||||||
.on_press(msg)
|
crate::app::QSelectValueEditor::Choice(options) => {
|
||||||
.style(move |theme: &Theme, st| {
|
let selected = (!state.value.is_empty()).then(|| state.value.clone());
|
||||||
let palette = theme.palette();
|
let picker = iced::widget::pick_list(
|
||||||
let pair = match (
|
selected,
|
||||||
primary,
|
options.clone(),
|
||||||
matches!(st, button::Status::Hovered | button::Status::Pressed),
|
|value| value.to_string(),
|
||||||
) {
|
)
|
||||||
(true, true) => palette.primary.strong,
|
.width(field_width);
|
||||||
(true, false) => palette.primary.base,
|
if value_enabled {
|
||||||
(false, true) => palette.background.strong,
|
picker.on_select(Message::QSelectSetValue).into()
|
||||||
(false, false) => palette.background.weak,
|
} else {
|
||||||
};
|
picker.into()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.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)
|
let derived_error = if candidate_count == 0 {
|
||||||
.size(12)
|
Some(crate::t!("No objects are available in this scope.").into_owned())
|
||||||
.width(field_width);
|
} else if value_enabled && number_property
|
||||||
if value_enabled {
|
&& crate::entities::common::parse_f64(&state.value).is_none()
|
||||||
value_input = value_input.on_input(Message::QSelectSetValue);
|
{
|
||||||
|
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![
|
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![
|
row![
|
||||||
label(t!("Object type:")),
|
label(t!("Object type:")),
|
||||||
iced::widget::pick_list(
|
iced::widget::pick_list(
|
||||||
|
|
@ -1364,12 +1436,12 @@ fn qselect_content<'a>(
|
||||||
|value| value.to_string(),
|
|value| value.to_string(),
|
||||||
)
|
)
|
||||||
.on_select(|p: crate::app::QSelectPropertyChoice| {
|
.on_select(|p: crate::app::QSelectPropertyChoice| {
|
||||||
if p.field.is_empty() {
|
if p.field.is_empty() {
|
||||||
Message::QSelectSetProperty(None)
|
Message::QSelectSetProperty(None)
|
||||||
} else {
|
} else {
|
||||||
Message::QSelectSetProperty(Some(p))
|
Message::QSelectSetProperty(Some(p))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.width(field_width),
|
.width(field_width),
|
||||||
]
|
]
|
||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center)
|
||||||
|
|
@ -1390,25 +1462,57 @@ fn qselect_content<'a>(
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.width(sizing.width),
|
.width(sizing.width),
|
||||||
Space::new().height(6),
|
Space::new().height(6),
|
||||||
row![label(t!("Value:")), value_input,]
|
row![label(t!("Value:")), value_editor]
|
||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center)
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.width(sizing.width),
|
.width(sizing.width),
|
||||||
Space::new().height(10),
|
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![
|
row![
|
||||||
checkbox(state.append)
|
append,
|
||||||
.on_toggle(Message::QSelectSetAppend)
|
|
||||||
.size(14),
|
|
||||||
Space::new().width(6),
|
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),
|
.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![
|
row![
|
||||||
Space::new().width(field_width),
|
Space::new().width(flex_width),
|
||||||
btn(t!("Cancel"), Message::QSelectClose, false),
|
cancel,
|
||||||
Space::new().width(8),
|
Space::new().width(8),
|
||||||
btn(t!("Apply"), Message::QSelectApply, true),
|
apply,
|
||||||
]
|
]
|
||||||
.align_y(iced::Alignment::Center),
|
.align_y(iced::Alignment::Center),
|
||||||
]
|
]
|
||||||
|
|
@ -1423,13 +1527,13 @@ fn qselect_content<'a>(
|
||||||
.style(|theme: &Theme| {
|
.style(|theme: &Theme| {
|
||||||
let palette = theme.palette();
|
let palette = theme.palette();
|
||||||
container::Style {
|
container::Style {
|
||||||
background: Some(Background::Color(palette.background.weak.color)),
|
background: Some(Background::Color(palette.background.weak.color)),
|
||||||
border: Border {
|
border: Border {
|
||||||
color: palette.background.neutral.color,
|
color: palette.background.neutral.color,
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: 6.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,30 @@ impl Scene {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn qselect_candidate_handles(&self, scope: crate::app::QSelectScope) -> Vec<Handle> {
|
||||||
|
match scope {
|
||||||
|
crate::app::QSelectScope::CurrentSpace => self.current_layout_entity_handles(),
|
||||||
|
crate::app::QSelectScope::CurrentSelection => {
|
||||||
|
self.selected.iter().copied().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn qselect_candidate_count(&self, scope: crate::app::QSelectScope) -> usize {
|
||||||
|
self.qselect_candidate_handles(scope).len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn qselect_entity_type_names(&self, scope: crate::app::QSelectScope) -> Vec<String> {
|
||||||
|
use crate::entities::traits::entity_type_name;
|
||||||
|
let mut names = std::collections::BTreeSet::new();
|
||||||
|
for h in self.qselect_candidate_handles(scope) {
|
||||||
|
if let Some(entity) = self.document.get_entity(h) {
|
||||||
|
names.insert(entity_type_name(entity).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Extends the current selection with every entity in the active
|
/// Extends the current selection with every entity in the active
|
||||||
/// layout that matches one of the selected entities by `(variant,
|
/// layout that matches one of the selected entities by `(variant,
|
||||||
/// layer)`. The seed selection stays selected. No-op when nothing is
|
/// layer)`. The seed selection stays selected. No-op when nothing is
|
||||||
|
|
@ -112,9 +136,9 @@ impl Scene {
|
||||||
self.selected.len()
|
self.selected.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces (or extends, when `append` is true) the current
|
/// Builds a selection from the chosen scope and filter. Exclude mode
|
||||||
/// selection with every entity in the active layout that matches
|
/// keeps candidates that do not match. Append mode unions the result
|
||||||
/// the filter. Returns the number of newly-matching entities.
|
/// with the selection that existed before the query.
|
||||||
///
|
///
|
||||||
/// `type_name` of `None` means "any type". `property_field` of
|
/// `type_name` of `None` means "any type". `property_field` of
|
||||||
/// `None` skips the property test (only the type filter applies).
|
/// `None` skips the property test (only the type filter applies).
|
||||||
|
|
@ -123,19 +147,19 @@ impl Scene {
|
||||||
/// reject anything non-numeric.
|
/// reject anything non-numeric.
|
||||||
pub fn qselect(
|
pub fn qselect(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
scope: crate::app::QSelectScope,
|
||||||
type_name: Option<&str>,
|
type_name: Option<&str>,
|
||||||
property_field: Option<&str>,
|
property_field: Option<&str>,
|
||||||
op: crate::app::QSelectOp,
|
op: crate::app::QSelectOp,
|
||||||
value: &str,
|
value: &str,
|
||||||
|
mode: crate::app::QSelectMode,
|
||||||
append: bool,
|
append: bool,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
use crate::app::QSelectOp;
|
use crate::app::{QSelectMode, QSelectOp};
|
||||||
use crate::entities::traits::entity_type_name;
|
use crate::entities::traits::entity_type_name;
|
||||||
if !append {
|
let previous = self.selected.clone();
|
||||||
self.selected.clear();
|
let handles = self.qselect_candidate_handles(scope);
|
||||||
}
|
let mut result = HashSet::default();
|
||||||
let handles = self.current_layout_entity_handles();
|
|
||||||
let mut matched = 0;
|
|
||||||
for h in handles {
|
for h in handles {
|
||||||
let Some(e) = self.document.get_entity(h) else {
|
let Some(e) = self.document.get_entity(h) else {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -150,43 +174,48 @@ impl Scene {
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(t) = type_name {
|
let type_ok = type_name.is_none_or(|t| entity_type_name(e) == t);
|
||||||
if entity_type_name(e) != t {
|
let prop_ok = if !type_ok {
|
||||||
continue;
|
true
|
||||||
}
|
} else {
|
||||||
}
|
match (property_field, op) {
|
||||||
let prop_ok = match (property_field, op) {
|
(None, _) | (_, QSelectOp::Any) => true,
|
||||||
(None, _) | (_, QSelectOp::Any) => true,
|
(Some(field), op) => {
|
||||||
(Some(field), op) => {
|
match self.entity_property_value(e, field) {
|
||||||
let Some(actual) = self.entity_property_value(e, field) else {
|
Some(actual) => match op {
|
||||||
continue;
|
QSelectOp::Eq => actual.eq_ignore_ascii_case(value),
|
||||||
};
|
QSelectOp::Neq => !actual.eq_ignore_ascii_case(value),
|
||||||
match op {
|
QSelectOp::Gt | QSelectOp::Lt => {
|
||||||
QSelectOp::Eq => actual.eq_ignore_ascii_case(value),
|
match (
|
||||||
QSelectOp::Neq => !actual.eq_ignore_ascii_case(value),
|
crate::entities::common::parse_f64(&actual),
|
||||||
QSelectOp::Gt | QSelectOp::Lt => {
|
crate::entities::common::parse_f64(value),
|
||||||
let (Ok(a), Ok(b)) =
|
) {
|
||||||
(actual.parse::<f64>(), value.parse::<f64>())
|
(Some(a), Some(b)) if matches!(op, QSelectOp::Gt) => a > b,
|
||||||
else {
|
(Some(a), Some(b)) => a < b,
|
||||||
continue;
|
_ => false,
|
||||||
};
|
}
|
||||||
if matches!(op, QSelectOp::Gt) {
|
}
|
||||||
a > b
|
QSelectOp::Any => true,
|
||||||
} else {
|
},
|
||||||
a < b
|
None => false,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
QSelectOp::Any => true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if prop_ok {
|
let matches_filter = type_ok && prop_ok;
|
||||||
self.selected.insert(h);
|
let keep = match mode {
|
||||||
matched += 1;
|
QSelectMode::Include => matches_filter,
|
||||||
|
QSelectMode::Exclude => !matches_filter,
|
||||||
|
};
|
||||||
|
if keep {
|
||||||
|
result.insert(h);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.bump_selection();
|
if append {
|
||||||
matched
|
result.extend(previous);
|
||||||
|
}
|
||||||
|
self.replace_selection(result);
|
||||||
|
self.selected.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the sorted set of entity-type names present in the active
|
/// Returns the sorted set of entity-type names present in the active
|
||||||
|
|
@ -223,25 +252,100 @@ impl Scene {
|
||||||
!self.selection_filter.is_empty()
|
!self.selection_filter.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the list of `(field, label)` pairs the Quick Select
|
/// Returns common and type-specific filter properties with the editor
|
||||||
/// "Properties" dropdown should show given the current type filter:
|
/// each value needs. Choice editors are enriched from both document
|
||||||
///
|
/// tables and values present in the selected candidate scope.
|
||||||
/// * Common properties (Layer, Color, Linetype, Lineweight) are
|
|
||||||
/// always included.
|
|
||||||
/// * When `type_name` names a specific entity type present in the
|
|
||||||
/// active layout, the first entity of that type contributes its
|
|
||||||
/// `geometry_properties()` rows (Start X, Length, Radius, …) so
|
|
||||||
/// type-specific filtering works.
|
|
||||||
pub fn qselect_properties(
|
pub fn qselect_properties(
|
||||||
&self,
|
&self,
|
||||||
type_name: Option<&str>,
|
type_name: Option<&str>,
|
||||||
) -> Vec<(String, String)> {
|
scope: crate::app::QSelectScope,
|
||||||
|
) -> Vec<crate::app::QSelectPropertyChoice> {
|
||||||
use crate::entities::traits::{entity_type_name, EntityTypeOps};
|
use crate::entities::traits::{entity_type_name, EntityTypeOps};
|
||||||
let mut out: Vec<(String, String)> = vec![
|
use crate::scene::model::object::PropValue;
|
||||||
("layer".to_string(), "Layer".to_string()),
|
use crate::app::{QSelectPropertyChoice, QSelectValueEditor};
|
||||||
("color".to_string(), "Color".to_string()),
|
|
||||||
("linetype".to_string(), "Linetype".to_string()),
|
let candidate_handles: Vec<Handle> = self
|
||||||
("lineweight".to_string(), "Lineweight".to_string()),
|
.qselect_candidate_handles(scope)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|h| {
|
||||||
|
self.document.get_entity(*h).is_some_and(|entity| {
|
||||||
|
type_name.is_none_or(|t| entity_type_name(entity) == t)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut layer_options: Vec<String> = self
|
||||||
|
.document
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.map(|layer| layer.name.clone())
|
||||||
|
.collect();
|
||||||
|
layer_options.sort_by_key(|value| value.to_lowercase());
|
||||||
|
let mut linetype_options = vec!["ByLayer".to_string(), "ByBlock".to_string()];
|
||||||
|
linetype_options.extend(
|
||||||
|
self.document
|
||||||
|
.line_types
|
||||||
|
.iter()
|
||||||
|
.map(|linetype| linetype.name.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let choice = |field: &str, label: String, editor: QSelectValueEditor| {
|
||||||
|
QSelectPropertyChoice {
|
||||||
|
field: field.to_string(),
|
||||||
|
label,
|
||||||
|
editor,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut out = vec![
|
||||||
|
choice("handle", crate::t!("Handle").into_owned(), QSelectValueEditor::Text),
|
||||||
|
choice(
|
||||||
|
"color",
|
||||||
|
crate::t!("Color").into_owned(),
|
||||||
|
QSelectValueEditor::Choice(vec!["ByLayer".into(), "ByBlock".into()]),
|
||||||
|
),
|
||||||
|
choice(
|
||||||
|
"layer",
|
||||||
|
crate::t!("Layer").into_owned(),
|
||||||
|
QSelectValueEditor::Choice(layer_options.clone()),
|
||||||
|
),
|
||||||
|
choice(
|
||||||
|
"linetype",
|
||||||
|
crate::t!("Linetype").into_owned(),
|
||||||
|
QSelectValueEditor::Choice(linetype_options.clone()),
|
||||||
|
),
|
||||||
|
choice(
|
||||||
|
"linetype_scale",
|
||||||
|
crate::t!("Linetype scale").into_owned(),
|
||||||
|
QSelectValueEditor::Number,
|
||||||
|
),
|
||||||
|
choice(
|
||||||
|
"plot_style",
|
||||||
|
crate::t!("Plot style").into_owned(),
|
||||||
|
QSelectValueEditor::Choice(vec![
|
||||||
|
"ByLayer".into(),
|
||||||
|
"ByBlock".into(),
|
||||||
|
"ByColor".into(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
choice(
|
||||||
|
"lineweight",
|
||||||
|
crate::t!("Lineweight").into_owned(),
|
||||||
|
QSelectValueEditor::Choice(vec![
|
||||||
|
"ByLayer".into(),
|
||||||
|
"ByBlock".into(),
|
||||||
|
"Default".into(),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
choice(
|
||||||
|
"transparency",
|
||||||
|
crate::t!("Transparency").into_owned(),
|
||||||
|
QSelectValueEditor::Choice(vec!["ByLayer".into()]),
|
||||||
|
),
|
||||||
|
choice(
|
||||||
|
"hyperlink",
|
||||||
|
crate::t!("Hyperlink").into_owned(),
|
||||||
|
QSelectValueEditor::Text,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
if let Some(t) = type_name {
|
if let Some(t) = type_name {
|
||||||
let text_style_names: Vec<String> = self
|
let text_style_names: Vec<String> = self
|
||||||
|
|
@ -250,22 +354,146 @@ impl Scene {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.name.clone())
|
.map(|s| s.name.clone())
|
||||||
.collect();
|
.collect();
|
||||||
let sample = self
|
let sample = candidate_handles
|
||||||
.current_layout_entity_handles()
|
.iter()
|
||||||
.into_iter()
|
.copied()
|
||||||
.filter_map(|h| self.document.get_entity(h))
|
.filter_map(|h| self.document.get_entity(h))
|
||||||
.find(|e| entity_type_name(e) == t);
|
.find(|e| entity_type_name(e) == t);
|
||||||
if let Some(sample) = sample {
|
if let Some(sample) = sample {
|
||||||
for section in sample.geometry_properties(&text_style_names) {
|
let mut sections = vec![crate::scene::cache::properties::general_section(sample)];
|
||||||
|
if let Some(section) =
|
||||||
|
crate::scene::cache::properties::visualization_section(sample)
|
||||||
|
{
|
||||||
|
sections.push(section);
|
||||||
|
}
|
||||||
|
sections.extend(sample.geometry_properties(&text_style_names));
|
||||||
|
for section in sections {
|
||||||
for prop in section.props {
|
for prop in section.props {
|
||||||
// Skip rows that don't sensibly compare via
|
if out.iter().any(|item| item.field == prop.field) {
|
||||||
// `entity_property_value` (read-only labels are
|
continue;
|
||||||
// fine — users can still match against them).
|
}
|
||||||
out.push((prop.field.to_string(), prop.label.clone()));
|
let editor = if prop.field == "material" {
|
||||||
|
QSelectValueEditor::Choice(vec![
|
||||||
|
"ByLayer".into(),
|
||||||
|
"ByBlock".into(),
|
||||||
|
"Custom".into(),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
match prop.value {
|
||||||
|
PropValue::ReadOnly(ref value)
|
||||||
|
| PropValue::EditText(ref value) => {
|
||||||
|
let field = prop.field.to_ascii_lowercase();
|
||||||
|
let textual = [
|
||||||
|
"name",
|
||||||
|
"text",
|
||||||
|
"content",
|
||||||
|
"style",
|
||||||
|
"tag",
|
||||||
|
"prompt",
|
||||||
|
"value",
|
||||||
|
"description",
|
||||||
|
"format",
|
||||||
|
"font",
|
||||||
|
"path",
|
||||||
|
"file",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|part| field.contains(part));
|
||||||
|
if !textual
|
||||||
|
&& crate::entities::common::parse_f64(value).is_some()
|
||||||
|
{
|
||||||
|
QSelectValueEditor::Number
|
||||||
|
} else {
|
||||||
|
QSelectValueEditor::Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PropValue::LayerChoice(_) => {
|
||||||
|
QSelectValueEditor::Choice(layer_options.clone())
|
||||||
|
}
|
||||||
|
PropValue::Choice { ref options, .. } => {
|
||||||
|
QSelectValueEditor::Choice(options.clone())
|
||||||
|
}
|
||||||
|
PropValue::EditChoice {
|
||||||
|
ref value,
|
||||||
|
ref options,
|
||||||
|
} => {
|
||||||
|
let mut values = options.clone();
|
||||||
|
if !values.iter().any(|item| item == value) {
|
||||||
|
values.push(value.clone());
|
||||||
|
}
|
||||||
|
QSelectValueEditor::Choice(values)
|
||||||
|
}
|
||||||
|
PropValue::ColorChoice(_) => QSelectValueEditor::Choice(vec![
|
||||||
|
"ByLayer".into(),
|
||||||
|
"ByBlock".into(),
|
||||||
|
]),
|
||||||
|
PropValue::LwChoice(_) => QSelectValueEditor::Choice(vec![
|
||||||
|
"ByLayer".into(),
|
||||||
|
"ByBlock".into(),
|
||||||
|
"Default".into(),
|
||||||
|
]),
|
||||||
|
PropValue::LinetypeChoice(_) => {
|
||||||
|
QSelectValueEditor::Choice(linetype_options.clone())
|
||||||
|
}
|
||||||
|
PropValue::HatchPatternChoice(value) => {
|
||||||
|
let mut patterns: Vec<String> =
|
||||||
|
crate::scene::model::hatch_patterns::catalog()
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.name.clone())
|
||||||
|
.collect();
|
||||||
|
if !patterns.iter().any(|item| item == &value) {
|
||||||
|
patterns.push(value);
|
||||||
|
}
|
||||||
|
QSelectValueEditor::Choice(patterns)
|
||||||
|
}
|
||||||
|
PropValue::BoolToggle { .. } => {
|
||||||
|
QSelectValueEditor::Choice(vec![
|
||||||
|
"false".into(),
|
||||||
|
"true".into(),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
PropValue::AttrText { .. } => QSelectValueEditor::Text,
|
||||||
|
PropValue::Stepper { .. }
|
||||||
|
| PropValue::ColorVaries
|
||||||
|
| PropValue::LwVaries => continue,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
out.push(choice(prop.field, prop.label, editor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for property in &mut out {
|
||||||
|
let QSelectValueEditor::Choice(options) = &mut property.editor else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let common_choice = matches!(
|
||||||
|
property.field.as_str(),
|
||||||
|
"color"
|
||||||
|
| "layer"
|
||||||
|
| "linetype"
|
||||||
|
| "plot_style"
|
||||||
|
| "lineweight"
|
||||||
|
| "transparency"
|
||||||
|
| "material"
|
||||||
|
);
|
||||||
|
if !common_choice && !options.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for handle in &candidate_handles {
|
||||||
|
let Some(entity) = self.document.get_entity(*handle) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(value) = self.entity_property_value(entity, &property.field) {
|
||||||
|
if !options.iter().any(|option| option.eq_ignore_ascii_case(&value)) {
|
||||||
|
options.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options.sort_by_key(|value| value.to_lowercase());
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,10 +511,53 @@ impl Scene {
|
||||||
use crate::entities::traits::EntityTypeOps;
|
use crate::entities::traits::EntityTypeOps;
|
||||||
use crate::scene::model::object::PropValue;
|
use crate::scene::model::object::PropValue;
|
||||||
match field {
|
match field {
|
||||||
|
"handle" => Some(entity.common().handle.value().to_string()),
|
||||||
"layer" => Some(entity.common().layer.clone()),
|
"layer" => Some(entity.common().layer.clone()),
|
||||||
"color" => Some(Self::format_color(entity.common().color)),
|
"color" => Some(Self::format_color(entity.common().color)),
|
||||||
"linetype" => Some(entity.common().linetype.clone()),
|
"linetype" => Some(if entity.common().linetype.is_empty() {
|
||||||
|
"ByLayer".to_string()
|
||||||
|
} else {
|
||||||
|
entity.common().linetype.clone()
|
||||||
|
}),
|
||||||
|
"linetype_scale" => Some(format!("{:.4}", entity.common().linetype_scale)),
|
||||||
|
"plot_style" => Some(
|
||||||
|
match entity.common().plotstyle_flags {
|
||||||
|
0 => "ByLayer",
|
||||||
|
1 => "ByBlock",
|
||||||
|
_ => "ByColor",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
"lineweight" => Some(Self::format_lineweight(entity.common().line_weight)),
|
"lineweight" => Some(Self::format_lineweight(entity.common().line_weight)),
|
||||||
|
"transparency" => Some(if entity.common().transparency.alpha() == 0 {
|
||||||
|
"ByLayer".to_string()
|
||||||
|
} else {
|
||||||
|
((entity.common().transparency.alpha() as f64 / 255.0 * 100.0).round() as u32)
|
||||||
|
.to_string()
|
||||||
|
}),
|
||||||
|
"hyperlink" => Some(
|
||||||
|
entity
|
||||||
|
.common()
|
||||||
|
.extended_data
|
||||||
|
.get_record("PE_URL")
|
||||||
|
.and_then(|record| {
|
||||||
|
record.values.iter().find_map(|value| match value {
|
||||||
|
acadrust::xdata::XDataValue::String(text) if !text.is_empty() => {
|
||||||
|
Some(text.clone())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
),
|
||||||
|
"material" => Some(
|
||||||
|
match entity.common().material_flags {
|
||||||
|
0 => "ByLayer",
|
||||||
|
1 => "ByBlock",
|
||||||
|
_ => "Custom",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
_ => {
|
_ => {
|
||||||
let text_style_names: Vec<String> = self
|
let text_style_names: Vec<String> = self
|
||||||
.document
|
.document
|
||||||
|
|
@ -294,8 +565,14 @@ impl Scene {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.name.clone())
|
.map(|s| s.name.clone())
|
||||||
.collect();
|
.collect();
|
||||||
let prop = entity
|
let mut sections = vec![crate::scene::cache::properties::general_section(entity)];
|
||||||
.geometry_properties(&text_style_names)
|
if let Some(section) =
|
||||||
|
crate::scene::cache::properties::visualization_section(entity)
|
||||||
|
{
|
||||||
|
sections.push(section);
|
||||||
|
}
|
||||||
|
sections.extend(entity.geometry_properties(&text_style_names));
|
||||||
|
let prop = sections
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|s| s.props)
|
.flat_map(|s| s.props)
|
||||||
.find(|p| p.field == field)?;
|
.find(|p| p.field == field)?;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue