forked from 1b-agent/opencadstudio-upstream
feat(props): select whole value when a property field is clicked
Clicking an editable property value field now selects its entire value instead of only placing the caret, so typing immediately replaces it. The click is observed through a global left-press subscription (PropPointerPressed); a focus sweep reports which widget took focus and PropSyncActive maps it back to the active property row, then issues a select-all operation only when focus landed on a different field. The active row stays highlighted while its text input holds keyboard focus and clears when focus moves elsewhere.
This commit is contained in:
1 parent
dceb9626ae
commit
9f68034a60
6 files changed
+336
-18
No files matched your search
@@ -2362,6 +2362,17 @@ pub enum Message {
|
||||
PropAttrInput { tag: String, value: String },
|
||||
/// User committed a block-attribute value edit (Enter pressed).
|
||||
PropAttrCommit(String),
|
||||
/// Reports the currently keyboard-focused widget (if any) after a
|
||||
/// [`sync_active_field_task`](crate::ui::properties::sync_active_field_task)
|
||||
/// sweep, so the update handler can keep the active-row marker reconciled
|
||||
/// against real focus instead of pointer hover.
|
||||
PropSyncActive(Option<iced::widget::Id>),
|
||||
/// A left mouse button was pressed anywhere. The clicked widget has already
|
||||
/// been given focus by the time this arrives (the widget tree processes the
|
||||
/// event before the runtime broadcasts it to subscriptions), so a focus
|
||||
/// sweep — which resolves as `PropSyncActive` — reveals whether a property
|
||||
/// value field was clicked and can select its whole value.
|
||||
PropPointerPressed,
|
||||
/// Toggle the inline color picker dropdown open/closed.
|
||||
PropColorPickerToggle,
|
||||
/// Toggle the MTEXT background-colour picker dropdown open/closed.
|
||||
|
||||
@@ -42,6 +42,7 @@ impl OpenCADStudio {
|
||||
// closes, matching the deselect / reselect / click-away expectation.
|
||||
let color_palette_open = self.tabs[i].properties.color_palette_open;
|
||||
let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf);
|
||||
let active_field = std::mem::take(&mut self.tabs[i].properties.active_field);
|
||||
// Expanded coordinate groups persist across rebuilds AND selection
|
||||
// changes — it's a per-user view preference, not per-entity state.
|
||||
let expanded_groups = std::mem::take(&mut self.tabs[i].properties.expanded_groups);
|
||||
@@ -1441,6 +1442,14 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
// The active-row highlight only survives a rebuild for the same
|
||||
// selection (like the edit buffer); a selection change clears it so
|
||||
// an old row isn't marked active against new content.
|
||||
panel.active_field = if prev_handles == new_handles {
|
||||
active_field
|
||||
} else {
|
||||
None
|
||||
};
|
||||
panel.expanded_groups = expanded_groups;
|
||||
panel.source_handles = new_handles;
|
||||
panel.prop_vertex = prop_vertex;
|
||||
@@ -1458,6 +1467,7 @@ impl OpenCADStudio {
|
||||
if locked_only {
|
||||
make_sections_read_only(&mut panel.sections);
|
||||
panel.edit_buf.clear();
|
||||
panel.active_field = None;
|
||||
panel.color_picker_open = false;
|
||||
panel.color_palette_open = false;
|
||||
panel.bg_color_picker_open = false;
|
||||
|
||||
@@ -2120,6 +2120,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
|
||||
pub(super) fn on_prop_geom_commit(&mut self, field: &'static str) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].properties.active_field = None;
|
||||
let handles = self.property_target_handles(i);
|
||||
if !handles.is_empty() {
|
||||
if let Some(raw_val) = self.tabs[i].properties.edit_buf.remove(field) {
|
||||
@@ -2348,6 +2349,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
/// it is not run through the expression evaluator like numeric fields).
|
||||
pub(super) fn on_prop_attr_commit(&mut self, tag: String) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].properties.active_field = None;
|
||||
let key = crate::ui::properties::attr_edit_key(&tag);
|
||||
let handles = self.property_target_handles(i);
|
||||
if handles.is_empty() {
|
||||
|
||||
+62
-5
@@ -217,6 +217,20 @@ impl OpenCADStudio {
|
||||
self.reset_modal_geometry();
|
||||
}
|
||||
|
||||
/// Fire the focus-sweep once when a property field is active, so a
|
||||
/// click-away that moved focus off the field (viewport, toolbar, command
|
||||
/// line) clears the active-row highlight. Idle (`Task::none()`) unless a
|
||||
/// field is active, and the sweep itself keeps the marker whenever the
|
||||
/// active field still holds focus — re-firing it on unrelated messages is
|
||||
/// cheap and safe.
|
||||
fn sync_active_field_if_any(&self) -> Task<Message> {
|
||||
if self.tabs[self.active_tab].properties.active_field.is_some() {
|
||||
crate::ui::properties::sync_active_field_task()
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, msg: Message) -> Task<Message> {
|
||||
let perf_started = crate::perf::enabled().then(Instant::now);
|
||||
let perf_label = perf_message_label(&msg);
|
||||
@@ -1158,7 +1172,8 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
Message::RibbonToolClick { tool_id, event } => {
|
||||
self.on_ribbon_tool_click(tool_id, event)
|
||||
let sweep = self.sync_active_field_if_any();
|
||||
Task::batch(vec![sweep, self.on_ribbon_tool_click(tool_id, event)])
|
||||
}
|
||||
Message::PluginFileDialogResult { command, path } => {
|
||||
if let Some(path) = path {
|
||||
@@ -1401,14 +1416,15 @@ impl OpenCADStudio {
|
||||
// the submit path, which tokenises multi-token lines — so a typed
|
||||
// token, a pasted `LINE 0,0 10,10`, or API-fed text all run their
|
||||
// spaces as step separators. A leading `>` keeps spaces literal.
|
||||
let sweep = self.sync_active_field_if_any();
|
||||
if !self.command_line.literal_spaces && !s.starts_with('>') && s.contains(' ') {
|
||||
self.command_line.input = s;
|
||||
return self.update(Message::CommandSubmit);
|
||||
return Task::batch(vec![sweep, self.update(Message::CommandSubmit)]);
|
||||
}
|
||||
self.command_line.input = s;
|
||||
self.command_line.autocomplete_cursor = None;
|
||||
self.command_line.cancel_history_navigation();
|
||||
Task::none()
|
||||
Task::batch(vec![sweep, Task::none()])
|
||||
}
|
||||
|
||||
Message::CommandAppendChar(s) => self.on_command_append_char(s),
|
||||
@@ -2591,10 +2607,13 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::PanePress(idx) => {
|
||||
// A click-away into the drawing area re-syncs the active-row
|
||||
// highlight against real focus before the pick runs.
|
||||
let sweep = self.sync_active_field_if_any();
|
||||
// A fresh press ends any stale (un-dropped) pane move.
|
||||
self.pane_move_from = None;
|
||||
self.focus_model_pane(idx);
|
||||
self.on_viewport_left_press()
|
||||
Task::batch(vec![sweep, self.on_viewport_left_press()])
|
||||
}
|
||||
Message::PaneRelease(idx) => {
|
||||
// Finishing a pane-move drag: swap the source pane with the one
|
||||
@@ -2629,7 +2648,10 @@ impl OpenCADStudio {
|
||||
self.update(Message::ViewportScroll(d))
|
||||
}
|
||||
|
||||
Message::ViewportLeftPress => self.on_viewport_left_press(),
|
||||
Message::ViewportLeftPress => {
|
||||
let sweep = self.sync_active_field_if_any();
|
||||
Task::batch(vec![sweep, self.on_viewport_left_press()])
|
||||
}
|
||||
|
||||
Message::ViewportLeftRelease => self.on_viewport_left_release(),
|
||||
|
||||
@@ -4610,6 +4632,41 @@ impl OpenCADStudio {
|
||||
|
||||
Message::PropAttrCommit(tag) => self.on_prop_attr_commit(tag),
|
||||
|
||||
Message::PropPointerPressed => {
|
||||
if self.tabs[self.active_tab].is_start || self.clean_screen {
|
||||
return Task::none();
|
||||
}
|
||||
crate::ui::properties::sync_active_field_task()
|
||||
}
|
||||
|
||||
Message::PropSyncActive(focused) => {
|
||||
let panel = &mut self.tabs[self.active_tab].properties;
|
||||
if let Some(id) = focused.as_ref() {
|
||||
if let Some(key) = crate::ui::properties::prop_field_key_for_id(
|
||||
&panel.sections,
|
||||
id,
|
||||
) {
|
||||
let changed = panel.active_field.as_deref() != Some(key.as_str());
|
||||
panel.active_field = Some(key);
|
||||
// Only select the whole value when focus landed on a
|
||||
// field that wasn't already the active one; re-focusing
|
||||
// the same field (or sweeping after a click elsewhere
|
||||
// left its focus in place) must keep caret placement.
|
||||
if changed {
|
||||
return iced::widget::operation::select_all(id.clone());
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
if !crate::ui::properties::active_key_focused(
|
||||
panel.active_field.as_deref(),
|
||||
focused.as_ref(),
|
||||
) {
|
||||
panel.active_field = None;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::PropColorPickerToggle => {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].properties.color_picker_open =
|
||||
|
||||
@@ -2116,6 +2116,9 @@ impl OpenCADStudio {
|
||||
event::listen_with(|ev, status, win_id| {
|
||||
use iced::event::Status;
|
||||
match ev {
|
||||
iced::Event::Mouse(iced::mouse::Event::ButtonPressed(
|
||||
iced::mouse::Button::Left,
|
||||
)) => Some(Message::PropPointerPressed),
|
||||
iced::Event::Window(window::Event::CloseRequested) => {
|
||||
Some(Message::WindowCloseRequested(win_id))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user