Merge pull request #1549 from MarcelP102/codex/graphic-attributes-cleanup

[UX] Introducing graphic attributes panel

# Conflicts:
#	locales/ar-SA/opencadstudio.ftl
#	locales/bg-BG/opencadstudio.ftl
#	locales/cs-CZ/opencadstudio.ftl
#	locales/de-DE/opencadstudio.ftl
#	locales/el-GR/opencadstudio.ftl
#	locales/en-US/opencadstudio.ftl
#	locales/es-ES/opencadstudio.ftl
#	locales/fi-FI/opencadstudio.ftl
#	locales/fr-FR/opencadstudio.ftl
#	locales/hi-IN/opencadstudio.ftl
#	locales/hu-HU/opencadstudio.ftl
#	locales/it-IT/opencadstudio.ftl
#	locales/ja-JP/opencadstudio.ftl
#	locales/ko-KR/opencadstudio.ftl
#	locales/lo-LA/opencadstudio.ftl
#	locales/nl-NL/opencadstudio.ftl
#	locales/pl-PL/opencadstudio.ftl
#	locales/pt-BR/opencadstudio.ftl
#	locales/ru-RU/opencadstudio.ftl
#	locales/tr-TR/opencadstudio.ftl
#	locales/zh-CN/opencadstudio.ftl
#	locales/zh-TW/opencadstudio.ftl
#	src/app/mod.rs
#	src/app/update/dialog.rs
#	src/app/view/mod.rs
#	src/locale_catalog.rs
#	src/ui/dock.rs
This commit is contained in:
Hakan Seven committed 2026-10-12 00:49:27 +03:00
commit 1333efcad2
68 files changed
+6178 -173

No files matched your search

+51 -1
View File
@@ -394,6 +394,10 @@ impl OpenCADStudio {
// dropdown so the dispatched command's new prompt is
// immediately visible on the overlay.
self.command_line.close_history();
if self.awaiting_fill_close_answer() {
let input = std::mem::take(&mut self.command_line.input);
return self.on_graphic_fill_close_input(&input);
}
// A leading `>` was only a "literal spaces" typing hint (see
// CommandSpace) — drop it before the input is interpreted.
// Free-form text prompts keep it: there it is content, not a
@@ -1100,6 +1104,9 @@ impl OpenCADStudio {
if !self.command_line.input.trim().is_empty() {
return self.update(Message::CommandSubmit);
}
if self.awaiting_fill_close_answer() {
return self.on_graphic_fill_close_input("");
}
// A grip edit is not an active CAD command, but its Dynamic Input
// fields use the same keyboard path. Route Enter through CommandSubmit,
// whose grip branch resolves Distance / Angle and finalizes the edit.
@@ -1154,6 +1161,11 @@ impl OpenCADStudio {
if self.ribbon.escape_extension() {
return Task::none();
}
if self.awaiting_fill_close_answer() {
self.graphic_attributes.pending_fill_close = None;
self.command_line.push_info(crate::t!("*Cancel*").as_ref());
return Task::none();
}
// Esc drops an unconsumed one-shot snap override and closes
// its menu (#337). Falls through — Esc keeps its usual effect.
self.snap_override_popup = None;
@@ -2225,6 +2237,35 @@ impl OpenCADStudio {
Task::none()
}
pub(super) fn on_ribbon_lineweight_changed(
&mut self,
lw: codec::types::LineWeight,
) -> Task<Message> {
let i = self.active_tab;
self.ribbon.close_dropdown();
let handles = self.property_target_handles(i);
if handles.is_empty() {
if self.has_property_selection(i) {
return Task::none();
}
// Persist into the tab's header (CELWEIGHT). #21.
self.tabs[i].scene.document.header.current_line_weight = lw.value();
self.tabs[i].dirty = true;
self.ribbon.active_lineweight = lw;
} else {
// Lineweight is baked into the cached wire geometry —
// re-tessellate so the change shows immediately (issue #231
// class).
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
crate::scene::view::dispatch::apply_line_weight(entity, lw);
}
});
self.ribbon.active_lineweight = lw;
}
Task::none()
}
pub(super) fn on_ribbon_linetype_changed(&mut self, lt: String) -> Task<Message> {
let i = self.active_tab;
self.ribbon.close_dropdown();
@@ -2331,11 +2372,20 @@ impl OpenCADStudio {
pub(super) fn on_prop_hatch_pattern_changed(&mut self, name: String) -> Task<Message> {
let i = self.active_tab;
let handles = self.property_target_handles(i);
self.apply_hatch_pattern(i, &handles, name)
}
pub(super) fn apply_hatch_pattern(
&mut self,
i: usize,
handles: &[codec::Handle],
name: String,
) -> Task<Message> {
if !handles.is_empty() {
use crate::scene::model::hatch_patterns;
if let Some(entry) = hatch_patterns::find(&name) {
self.push_undo_snapshot(i, "HATCHEDIT");
for &handle in &handles {
for &handle in handles {
if let Some(codec::EntityType::Hatch(dxf)) =
self.tabs[i].scene.document.get_entity_mut(handle)
{
+9
View File
@@ -1326,6 +1326,8 @@ impl OpenCADStudio {
dock.ensure_settings();
dock
},
show_graphic_attributes: self.show_graphic_attributes,
graphic_fills_on_current_layer: self.graphic_attributes.fill_on_current_layer,
annotation_auto_scale: self.annotation_auto_scale,
ribbon: crate::app::config::RibbonConfig {
collapse: self.ribbon.collapse_mode(),
@@ -1384,6 +1386,13 @@ impl OpenCADStudio {
let mut dock = cfg.dock;
dock.ensure_settings();
self.dock = dock;
self.show_graphic_attributes = cfg.show_graphic_attributes;
self.graphic_attributes.fill_on_current_layer = cfg.graphic_fills_on_current_layer;
if self.show_graphic_attributes {
self.dock.dock_below_properties(crate::ui::dock::PanelId::GraphicAttributes);
}
self.ribbon
.set_graphic_attributes(self.show_graphic_attributes);
self.annotation_auto_scale = cfg.annotation_auto_scale.clamp(-4, 4);
self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
self.plot_dialog = cfg.plot;
File diff suppressed because it is too large. Load diff
+56 -27
View File
@@ -99,6 +99,7 @@ mod context_menu;
mod dialog;
mod dynamic;
mod file;
mod graphic_attributes;
pub(in crate::app) use file::background_task;
#[cfg(not(target_arch = "wasm32"))]
pub(in crate::app) use file::native_paths_match;
@@ -473,6 +474,10 @@ impl OpenCADStudio {
}
}
self.refresh_gpu_status();
if self.graphic_attributes.tracks_selection() {
let selected = self.tabs[self.active_tab].scene.selected_handles_in_order();
self.graphic_attributes.retain_selection(&selected);
}
self.show_next_startup_modal();
self.sync_open_command_history();
// Close the document-level first-touch transaction started by
@@ -486,6 +491,7 @@ impl OpenCADStudio {
.active_cmd
.as_ref()
.map(|c| c.prompt())
.or_else(|| self.graphic_attributes.fill_close_prompt())
.or_else(|| self.pending_pick_label());
self.command_line.set_step_prompt(prompt);
// Mirror the step's clickable options so they render as buttons (#304).
@@ -493,6 +499,14 @@ impl OpenCADStudio {
.active_cmd
.as_ref()
.map(|c| c.options())
.or_else(|| {
self.graphic_attributes.pending_fill_close.as_ref().map(|_| {
vec![
crate::command::CmdOption::new("Yes", "Y"),
crate::command::CmdOption::new("No", "N"),
]
})
})
.unwrap_or_default();
self.command_line.set_step_options(opts);
// Persist UI preferences whenever a toggle changes them (issue #68).
@@ -2535,6 +2549,9 @@ impl OpenCADStudio {
// finishes the step like Enter. (#304)
self.command_line.input.clear();
self.command_line.close_history();
if self.awaiting_fill_close_answer() {
return self.on_graphic_fill_close_input(&kw);
}
if kw.is_empty() {
return self.feed_command(crate::command::StepInput::Enter);
}
@@ -3877,6 +3894,7 @@ impl OpenCADStudio {
}
Message::ViewportLeftPress => {
self.graphic_attributes.open_menu = None;
let sweep = self.sync_active_field_if_any();
Task::batch(vec![sweep, self.on_viewport_left_press()])
}
@@ -3884,6 +3902,7 @@ impl OpenCADStudio {
Message::ViewportLeftRelease => self.on_viewport_left_release(),
Message::ViewportRightPress => {
self.graphic_attributes.open_menu = None;
let i = self.active_tab;
self.ribbon.close_dropdown();
// Shift+RMB: the one-shot snap override menu at the cursor —
@@ -3986,7 +4005,10 @@ impl OpenCADStudio {
self.unfocus_widgets()
}
Message::ViewportMiddlePress => self.on_viewport_middle_press(),
Message::ViewportMiddlePress => {
self.graphic_attributes.open_menu = None;
self.on_viewport_middle_press()
}
Message::ViewportMiddleRelease => {
let i = self.active_tab;
@@ -4318,6 +4340,14 @@ impl OpenCADStudio {
self.ribbon.set_properties(self.show_properties);
Task::none()
}
Message::ToggleGraphicAttributes => {
let id = crate::ui::dock::PanelId::GraphicAttributes;
let open = !self.show_graphic_attributes;
if open {
self.dock.dock_below_properties(id);
}
self.dock_set_open(id, open)
}
Message::ToggleFileTabs => {
self.show_file_tabs ^= true;
self.ribbon.set_file_tabs(self.show_file_tabs);
@@ -6697,31 +6727,7 @@ impl OpenCADStudio {
Task::none()
}
Message::RibbonLinetypeChanged(lt) => self.on_ribbon_linetype_changed(lt),
Message::RibbonLineweightChanged(lw) => {
let i = self.active_tab;
self.ribbon.close_dropdown();
let handles = self.property_target_handles(i);
if handles.is_empty() {
if self.has_property_selection(i) {
return Task::none();
}
// Persist into the tab's header (CELWEIGHT). #21.
self.tabs[i].scene.document.header.current_line_weight = lw.value();
self.tabs[i].dirty = true;
self.ribbon.active_lineweight = lw;
} else {
// Lineweight is baked into the cached wire geometry —
// re-tessellate so the change shows immediately (issue #231
// class).
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
crate::scene::view::dispatch::apply_line_weight(entity, lw);
}
});
self.ribbon.active_lineweight = lw;
}
Task::none()
}
Message::RibbonLineweightChanged(lw) => self.on_ribbon_lineweight_changed(lw),
Message::RibbonStyleChanged { key, name } => self.on_ribbon_style_changed(key, name),
@@ -7132,13 +7138,28 @@ impl OpenCADStudio {
Message::PropAttrCommit(tag) => self.on_prop_attr_commit(tag),
Message::PropPointerPressed => {
if !self.dock_panel_visible(crate::ui::dock::PanelId::Properties) {
if !self.dock_panel_visible(crate::ui::dock::PanelId::Properties)
&& !self.dock_panel_visible(crate::ui::dock::PanelId::GraphicAttributes)
{
return Task::none();
}
crate::ui::properties::sync_active_field_task()
}
Message::PropSyncActive(focused) => {
// The Graphic Attributes linetype scale field behaves like a
// Properties field: the first click selects its value.
let scale_field = iced::widget::Id::new(
crate::ui::window::graphic_attributes::LINETYPE_SCALE_FIELD,
);
if focused.as_ref() == Some(&scale_field) {
if !std::mem::replace(&mut self.graphic_attributes.linetype_scale_focused, true) {
return iced::widget::operation::select_all(scale_field);
}
return Task::none();
}
self.graphic_attributes.linetype_scale_focused = false;
self.graphic_attributes.linetype_scale_input = None;
let panel = &mut self.tabs[self.active_tab].properties;
if let Some(id) = focused.as_ref() {
if let Some(key) = panel.prop_field_key_for_id(id) {
@@ -9853,6 +9874,7 @@ impl OpenCADStudio {
Message::PlotDialogOpen => self.on_plot_dialog_open(),
Message::PlotDlg(m) => self.on_plot_dlg(m),
Message::BlockPalette(m) => self.on_block_palette(m),
Message::GraphicAttributes(m) => self.on_graphic_attributes(m),
Message::Dock(m) => self.on_dock(m),
Message::PrintAllOpen => self.on_print_all_open(),
Message::PrintAllToggle(name) => {
@@ -11015,6 +11037,13 @@ impl OpenCADStudio {
Task::none()
}
Message::OpenColorWindow(target, color) => {
if matches!(
target,
crate::app::ColorPickTarget::GraphicAttributesLine
| crate::app::ColorPickTarget::GraphicAttributesSolid
) {
self.graphic_attributes.open_menu = None;
}
self.color_pick_target = Some((target, color));
// Always open the shared CAD colour picker on the indexed ACI page.
+12
View File
@@ -13,6 +13,7 @@ use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip
use crate::scene::{
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
};
use crate::ui::window::graphic_attributes::GraphicAttributesMsg;
use crate::ui::PropertiesPanel;
use codec::types::Color as AcadColor;
use codec::{EntityType as AcadEntityType, Handle};
@@ -1156,6 +1157,17 @@ impl OpenCADStudio {
}
let s = crate::ui::color_select::color_to_aci_string(color);
let edit = match self.color_pick_target.take().map(|(target, _)| target) {
Some(crate::app::ColorPickTarget::Gradient(index)) => {
Some(Message::GraphicAttributes(GraphicAttributesMsg::Gradient(
crate::ui::window::gradient_editor::GradientMsg::Color(index, color),
)))
}
Some(crate::app::ColorPickTarget::GraphicAttributesLine) => Some(
Message::GraphicAttributes(GraphicAttributesMsg::LineColor(color)),
),
Some(crate::app::ColorPickTarget::GraphicAttributesSolid) => Some(
Message::GraphicAttributes(GraphicAttributesMsg::FillColor(color)),
),
Some(crate::app::ColorPickTarget::DimStyle(f)) => Some(Message::DsEdit(f, s)),
Some(crate::app::ColorPickTarget::MLeader(f)) => {
Some(Message::MLeaderStyleEdit { field: f, value: s })