Refactor FILTER validation and operator normalization (v2.2)

This commit is contained in:
turbuczn committed 2026-10-12 22:27:19 +02:00
1 parent 390e12169a
commit 7c61583ac2
2 files changed
+145 -70

No files matched your search

+64 -2
View File
@@ -223,10 +223,42 @@ impl std::fmt::Display for QSelectPropertyChoice {
}
}
/// Open Quick Select panel state. The filter is one
/// `(scope, type, property, op, value)` row plus result behavior.
/// One committed condition in the Quick Select filter list.
#[derive(Clone, Debug)]
pub struct QSelectCondition {
pub type_filter: Option<String>,
pub property: Option<QSelectPropertyChoice>,
pub operator: QSelectOp,
pub value: String,
}
impl QSelectCondition {
pub fn validation_error(&self) -> Option<String> {
if self.type_filter.is_none() && self.property.is_none() {
return Some(crate::t!("Choose an object type or property.").into_owned());
}
let Some(property) = &self.property else { return None; };
if matches!(self.operator, QSelectOp::Any) { return None; }
if matches!(self.operator, QSelectOp::Gt | QSelectOp::Lt)
&& !matches!(property.editor, QSelectValueEditor::Number) {
return Some(crate::t!("This operator requires a numeric property.").into_owned());
}
match &property.editor {
QSelectValueEditor::Number if crate::entities::common::parse_f64(&self.value)
.is_none_or(|value| !value.is_finite()) =>
Some(crate::t!("Enter a valid number.").into_owned()),
QSelectValueEditor::Choice(_) if self.value.is_empty() =>
Some(crate::t!("Choose a value.").into_owned()),
_ => None,
}
}
}
/// Quick Select editor and committed conditions (combined with AND).
#[derive(Clone, Debug)]
pub struct QSelectState {
pub conditions: Vec<QSelectCondition>,
pub editing_condition: Option<usize>,
pub scope: QSelectScope,
pub available_types: Vec<String>,
pub available_properties: Vec<QSelectPropertyChoice>,
@@ -242,8 +274,30 @@ pub struct QSelectState {
pub error: Option<String>,
}
impl QSelectState {
pub fn normalize_operator(&mut self) {
if matches!(self.operator, QSelectOp::Gt | QSelectOp::Lt)
&& !self.property.as_ref().is_some_and(|property| {
matches!(property.editor, QSelectValueEditor::Number)
})
{
self.operator = QSelectOp::Eq;
}
}
pub fn draft_condition(&self) -> QSelectCondition {
QSelectCondition {
type_filter: self.type_filter.clone(),
property: self.property.clone(),
operator: self.operator,
value: self.value.clone(),
}
}
}
#[derive(Clone, Debug)]
struct QSelectSettings {
conditions: Vec<QSelectCondition>,
scope: QSelectScope,
type_filter: Option<String>,
property_field: Option<String>,
@@ -256,6 +310,7 @@ struct QSelectSettings {
impl From<&QSelectState> for QSelectSettings {
fn from(state: &QSelectState) -> Self {
Self {
conditions: state.conditions.clone(),
scope: state.scope,
type_filter: state.type_filter.clone(),
property_field: state
@@ -3891,6 +3946,13 @@ pub enum Message {
/// Append-to-current-selection toggle.
QSelectSetAppend(bool),
/// Apply the current filter and close the panel.
QSelectAddCondition,
/// Hide the filter and sample an object's filterable properties.
QSelectPickObject,
QSelectEditCondition(usize),
QSelectRemoveCondition(usize),
QSelectClearConditions,
QSelectCancelEdit,
QSelectApply,
/// The user clicked the title-bar ✕ (fires before the window closes).
WindowCloseRequested(window::Id),
+81 -68
View File
@@ -106,6 +106,7 @@ pub(in crate::app) use file::background_task;
#[cfg(not(target_arch = "wasm32"))]
pub(in crate::app) use file::native_paths_match;
mod page_setup_import;
mod qselect;
mod style;
pub(in crate::app) mod util;
mod viewport;
@@ -7011,14 +7012,7 @@ impl OpenCADStudio {
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;
}
state.normalize_operator();
}
Task::none()
}
@@ -7042,14 +7036,7 @@ impl OpenCADStudio {
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;
}
state.normalize_operator();
}
Task::none()
}
@@ -7059,14 +7046,7 @@ impl OpenCADStudio {
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;
}
state.normalize_operator();
}
Task::none()
}
@@ -7105,47 +7085,77 @@ impl OpenCADStudio {
Task::none()
}
Message::QSelectPickObject => self.on_qselect_pick_object(),
Message::QSelectAddCondition => {
if let Some(state) = self.qselect.as_mut() {
let condition = state.draft_condition();
if let Some(error) = condition.validation_error() {
state.error = Some(error);
return Task::none();
}
if let Some(index) = state.editing_condition.take() {
if let Some(existing) = state.conditions.get_mut(index) { *existing = condition; }
} else { state.conditions.push(condition); }
state.error = None;
}
Task::none()
}
Message::QSelectEditCondition(index) => {
let condition = self.qselect.as_ref().and_then(|state| state.conditions.get(index).cloned());
if let Some(condition) = condition {
let scope = self.qselect.as_ref().unwrap().scope;
let properties = self.tabs[self.active_tab].scene.qselect_properties(condition.type_filter.as_deref(), scope);
if let Some(state) = self.qselect.as_mut() {
state.available_properties = properties;
state.type_filter = condition.type_filter;
state.property = condition.property;
state.operator = condition.operator;
state.value = condition.value;
state.editing_condition = Some(index);
state.error = None;
}
}
Task::none()
}
Message::QSelectRemoveCondition(index) => {
if let Some(state) = self.qselect.as_mut() {
if index < state.conditions.len() {
state.conditions.remove(index);
state.editing_condition = match state.editing_condition {
Some(editing) if editing == index => None,
Some(editing) if editing > index => Some(editing - 1),
other => other,
};
}
state.error = None;
}
Task::none()
}
Message::QSelectClearConditions => {
if let Some(state) = self.qselect.as_mut() {
state.conditions.clear();
state.editing_condition = None;
state.error = None;
}
Task::none()
}
Message::QSelectCancelEdit => {
if let Some(state) = self.qselect.as_mut() {
state.editing_condition = None;
state.error = None;
}
Task::none()
}
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 {
if self.tabs[self.active_tab].scene.qselect_candidate_count(state.scope) == 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 if state.editing_condition.is_some() {
Some(crate::t!("Update or cancel the condition edit before applying.").into_owned())
} else if state.conditions.is_empty() {
state.draft_condition().validation_error()
} else {
None
state.conditions.iter().find_map(|condition| condition.validation_error())
}
});
if let Some(error) = validation_error {
@@ -7160,13 +7170,11 @@ impl OpenCADStudio {
self.qselect_settings = Some((&state).into());
self.reset_modal_geometry();
let i = self.active_tab;
let matched = self.tabs[i].scene.qselect(
state.scope,
state.type_filter.as_deref(),
state.property.as_ref().map(|p| p.field.as_str()),
state.operator,
&state.value,
state.mode,
let conditions = if state.conditions.is_empty() {
vec![state.draft_condition()]
} else { state.conditions.clone() };
let matched = self.tabs[i].scene.qselect_conditions(
state.scope, &conditions, state.mode,
state.append && matches!(state.scope, crate::app::QSelectScope::CurrentSpace),
);
self.command_line
@@ -7190,6 +7198,11 @@ impl OpenCADStudio {
.collect();
wblock.error_message = None;
self.active_modal = Some(super::ModalKind::WriteBlock);
} else {
// FILTER may be opened from a still-focused command input.
// Release text focus so Delete and clipboard shortcuts act
// on the resulting pick-first selection, not on that input.
return self.unfocus_widgets();
}
Task::none()
}