forked from 1b-agent/opencadstudio-upstream
Refactor FILTER validation and operator normalization (v2.2)
This commit is contained in:
1 parent
390e12169a
commit
7c61583ac2
2 files changed
+145
-70
No files matched your search
+64
-2
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user