feat(ui): classic pull-down menu bar and ribbon toggle

MENUBAR 1 shows File, Edit, View, Insert, Format, Tools, Draw,
Dimension, Modify, Parametric and Help menus above the ribbon; every
entry runs an existing command, and a test keeps each entry pointing at
a registered command. RIBBONCLOSE hides the ribbon and RIBBON brings it
back, so the menus can replace it. Both choices persist.

Also translates the "File" label, which every locale had left in
English.

Refs #1450

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-13 16:05:51 +03:00
1 parent 9724447dd0
commit ee18f7ad3c
32 files changed
+1113 -19

No files matched your search

+5
View File
@@ -618,6 +618,11 @@ impl OpenCADStudio {
"CLEANSCREEN" => {
return Some(Task::done(Message::ToggleCleanScreen));
}
// ── RIBBON / RIBBONCLOSE — show or hide the ribbon ───────────────────
"RIBBON" | "RIBBONCLOSE" => {
self.ribbon_closed = cmd == "RIBBONCLOSE";
self.persist_settings_if_changed();
}
// ── QUICKPROPERTIES — toggle the floating quick-properties readout ───
"QUICKPROPERTIES" => {
return Some(Task::done(Message::ToggleQuickProperties));
+3
View File
@@ -755,6 +755,9 @@ inventory::submit!(crate::command::CommandRegistration {
"CLIPROMPTLINES",
"COMMANDLINEFADETIME",
"COMMANDLINEOPACITY",
"MENUBAR",
"RIBBON",
"RIBBONCLOSE",
"COLORTHEME",
"SELECTIONAREA",
"SELECTIONAREAOPACITY",
+13
View File
@@ -1150,6 +1150,7 @@ impl OpenCADStudio {
| "PICKBOX"
| "HPASSOC"
| "COMMANDLINEOPACITY"
| "MENUBAR"
| "CURSORTYPE"
| "SNAPANG"
| "TEXTFILL"
@@ -2190,6 +2191,17 @@ impl OpenCADStudio {
},
None => Ok((format!("PICKBOX = {}", self.pick_box), false)),
},
"MENUBAR" => match value.as_deref() {
Some(v @ ("0" | "1")) => {
self.show_menu_bar = v == "1";
Ok((format!("MENUBAR = {v}"), true))
}
Some(_) => Err("SETVAR: 0 or 1 required.".into()),
None => Ok((
format!("MENUBAR = {}", u8::from(self.show_menu_bar)),
false,
)),
},
"HPASSOC" => match value.as_deref() {
Some(v @ ("0" | "1")) => {
self.hatch_associative = v == "1";
@@ -3165,6 +3177,7 @@ impl OpenCADStudio {
| "PICKBOX"
| "HPASSOC"
| "COMMANDLINEOPACITY"
| "MENUBAR"
| "CURSORTYPE"
| "SNAPANG"
| "CLIPROMPTLINES"
+6
View File
@@ -642,6 +642,10 @@ pub(super) struct OpenCADStudio {
pick_box: i32,
/// HPASSOC: new hatches follow their boundary objects.
hatch_associative: bool,
/// MENUBAR: the classic pull-down menus show above the ribbon.
show_menu_bar: bool,
/// RIBBONCLOSE hid the ribbon; RIBBON brings it back.
ribbon_closed: bool,
/// Use REFEDIT rather than BEDIT when double-clicking an attribute-free block.
double_click_block_refedit: bool,
/// Open ATTEDIT when double-clicking a block with attributes.
@@ -4543,6 +4547,8 @@ impl OpenCADStudio {
cursor_size: 5,
pick_box: 3,
hatch_associative: true,
show_menu_bar: false,
ribbon_closed: false,
double_click_block_refedit: false,
double_click_block_attedit: true,
right_click_mode: settings::RightClickMode::ShortcutMenu,
+6
View File
@@ -400,6 +400,10 @@ pub struct UserSettings {
pub pick_box: i32,
/// HPASSOC: new hatches follow their boundary objects.
pub hatch_associative: bool,
/// MENUBAR: the classic pull-down menus show above the ribbon.
pub show_menu_bar: bool,
/// RIBBONCLOSE hid the ribbon; RIBBON brings it back.
pub ribbon_closed: bool,
/// When true, double-clicking a block reference starts REFEDIT instead of BEDIT.
pub double_click_block_refedit: bool,
/// When true, double-clicking a block with attributes opens ATTEDIT.
@@ -834,6 +838,8 @@ impl Default for UserSettings {
cursor_size: 5,
pick_box: 3,
hatch_associative: true,
show_menu_bar: false,
ribbon_closed: false,
options_tab: crate::ui::window::options::OptionsTab::General,
show_viewcube: true,
show_ucs_icon: true,
+4
View File
@@ -781,6 +781,8 @@ impl OpenCADStudio {
cursor_size: self.cursor_size,
pick_box: self.pick_box,
hatch_associative: self.hatch_associative,
show_menu_bar: self.show_menu_bar,
ribbon_closed: self.ribbon_closed,
options_tab: self.options_tab,
show_viewcube: self.show_viewcube,
show_ucs_icon: self.show_ucs_icon,
@@ -894,6 +896,8 @@ impl OpenCADStudio {
self.cursor_size = s.cursor_size.clamp(1, 100);
self.pick_box = s.pick_box.clamp(0, 50);
self.hatch_associative = s.hatch_associative;
self.show_menu_bar = s.show_menu_bar;
self.ribbon_closed = s.ribbon_closed;
self.options_tab = s.options_tab;
self.spacemouse_preferences = s.spacemouse;
// These four drive real features with commands and status-bar pills,
+4 -1
View File
@@ -2020,7 +2020,10 @@ bg={bg_ms:.1}ms n={view_count}"
// Clean-screen mode drops the ribbon for a full-canvas view; the
// status bar stays so the mode can be toggled back off.
let mut col = column![];
if !self.clean_screen {
if !self.clean_screen && self.show_menu_bar {
col = col.push(crate::ui::menu_bar::view());
}
if !self.clean_screen && !self.ribbon_closed {
col = col.push(self.ribbon.view(
is_paper,
self.tabs[self.active_tab].is_start,
+34
View File
@@ -3697,6 +3697,40 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"Width factor" => Some(("common", "width-factor-sentence-case")),
"Width: %{value}" => Some(("ui", "width-value")),
"Window" => Some(("common", "window")),
"Arc Length" => Some(("common", "menu-arc-length")),
"Construction Line" => Some(("common", "menu-construction-line")),
"Dimension Style" => Some(("common", "menu-dimension-style")),
"Donut" => Some(("common", "menu-donut")),
"Drawing Limits" => Some(("common", "menu-drawing-limits")),
"Edit Hatch" => Some(("common", "menu-edit-hatch")),
"Edit Polyline" => Some(("common", "menu-edit-polyline")),
"Edit Spline" => Some(("common", "menu-edit-spline")),
"Explode Text" => Some(("common", "menu-explode-text")),
"ID Point" => Some(("common", "menu-id-point")),
"Jogged" => Some(("common", "menu-jogged")),
"Lengthen" => Some(("common", "menu-lengthen")),
"Match Properties" => Some(("common", "menu-match-properties")),
"Multileader Style" => Some(("common", "menu-multileader-style")),
"Multiline" => Some(("common", "menu-multiline")),
"Multiline Style" => Some(("common", "menu-multiline-style")),
"Multiline Text" => Some(("common", "menu-multiline-text")),
"Named Views" => Some(("common", "menu-named-views")),
"New UCS" => Some(("common", "menu-new-ucs")),
"Orbit" => Some(("common", "menu-orbit")),
"Page Setup Manager" => Some(("common", "menu-page-setup-manager")),
"Parametric" => Some(("common", "menu-parametric")),
"Paste Special" => Some(("common", "menu-paste-special")),
"Publish" => Some(("common", "menu-publish")),
"Quick Dimension" => Some(("common", "menu-quick-dimension")),
"Rectangle" => Some(("common", "menu-rectangle")),
"Redraw" => Some(("common", "menu-redraw")),
"Regen All" => Some(("common", "menu-regen-all")),
"Revision Cloud" => Some(("common", "menu-revision-cloud")),
"Run Script" => Some(("common", "menu-run-script")),
"Table Style" => Some(("common", "menu-table-style")),
"Tools" => Some(("common", "menu-tools")),
"View Twist" => Some(("common", "menu-view-twist")),
"Viewports" => Some(("common", "menu-viewports")),
"Command line opacity" => Some(("common", "command-line-opacity")),
"TXTEXP: no text selected." => Some(("common", "txtexp-no-text-selected")),
"Windows desktop" => Some(("styles", "windows-desktop")),
+271
View File
@@ -0,0 +1,271 @@
//! Classic pull-down menus above the ribbon (MENUBAR 1). Every entry runs a
//! command, so the menus stay a view of the command set rather than a second
//! implementation of it.
use crate::app::Message;
use iced::widget::{button, container, text};
use iced::{Background, Border, Element, Fill, Theme};
use iced_aw::menu::{Item, Menu, MenuBar};
type Entries = &'static [(&'static str, &'static str)];
/// Menu title and its (label, command) entries, in menu-bar order.
const MENUS: &[(&str, Entries)] = &[
(
"File",
&[
("New", "QNEW"),
("Open", "OPEN"),
("Close", "CLOSE"),
("Save", "QSAVE"),
("Save As", "SAVEAS"),
("Page Setup Manager", "PAGESETUP"),
("Plot", "PLOT"),
("Publish", "PUBLISH"),
("Audit", "AUDIT"),
("Purge", "PURGE"),
("Exit", "QUIT"),
],
),
(
"Edit",
&[
("Undo", "UNDO"),
("Redo", "REDO"),
("Cut", "CUTCLIP"),
("Copy", "COPYCLIP"),
("Copy with Base Point", "COPYBASE"),
("Paste", "PASTECLIP"),
("Paste as Block", "PASTEBLOCK"),
("Paste to Original Coordinates", "PASTEORIG"),
("Paste Special", "PASTESPEC"),
("Erase", "ERASE"),
("Find and Replace", "FIND"),
],
),
(
"View",
&[
("Redraw", "REDRAW"),
("Regen", "REGEN"),
("Regen All", "REGENALL"),
("Zoom", "ZOOM"),
("Pan", "PAN"),
("Orbit", "3DORBIT"),
("Named Views", "VIEW"),
("View Twist", "DVIEW"),
("Viewports", "VPORTS"),
("Clean Screen", "CLEANSCREEN"),
],
),
(
"Insert",
&[
("Block", "INSERT"),
("External References", "EXTERNALREFERENCES"),
("Raster Image", "IMAGEATTACH"),
("PDF Underlay", "PDFATTACH"),
("Field", "FIELD"),
("Hyperlink", "HYPERLINK"),
],
),
(
"Format",
&[
("Layer", "LAYER"),
("Color", "COLOR"),
("Linetype", "LINETYPE"),
("Text Style", "STYLE"),
("Dimension Style", "DIMSTYLE"),
("Table Style", "TABLESTYLE"),
("Multileader Style", "MLEADERSTYLE"),
("Multiline Style", "MLSTYLE"),
("Point Style", "PTYPE"),
("Units", "UNITS"),
("Drawing Limits", "LIMITS"),
],
),
(
"Tools",
&[
("Quick Select", "QSELECT"),
("Draw Order", "DRAWORDER"),
("Distance", "DIST"),
("Area", "AREA"),
("List", "LIST"),
("ID Point", "ID"),
("Run Script", "SCRIPT"),
("New UCS", "UCS"),
("Drafting Settings", "DSETTINGS"),
("Options", "OPTIONS"),
],
),
(
"Draw",
&[
("Line", "LINE"),
("Ray", "RAY"),
("Construction Line", "XLINE"),
("Multiline", "MLINE"),
("Polyline", "PLINE"),
("3D Polyline", "3DPOLY"),
("Polygon", "POLYGON"),
("Rectangle", "RECTANG"),
("Arc", "ARC"),
("Circle", "CIRCLE"),
("Donut", "DONUT"),
("Spline", "SPLINE"),
("Ellipse", "ELLIPSE"),
("Block", "BLOCK"),
("Table", "TABLE"),
("Point", "POINT"),
("Hatch", "HATCH"),
("Boundary", "BOUNDARY"),
("Region", "REGION"),
("Wipeout", "WIPEOUT"),
("Revision Cloud", "REVCLOUD"),
("Text", "TEXT"),
("Multiline Text", "MTEXT"),
],
),
(
"Dimension",
&[
("Quick Dimension", "QDIM"),
("Linear", "DIMLINEAR"),
("Aligned", "DIMALIGNED"),
("Arc Length", "DIMARC"),
("Ordinate", "DIMORDINATE"),
("Radius", "DIMRADIUS"),
("Jogged", "DIMJOGGED"),
("Diameter", "DIMDIAMETER"),
("Angular", "DIMANGULAR"),
("Baseline", "DIMBASELINE"),
("Continue", "DIMCONTINUE"),
("Multileader", "MLEADER"),
("Tolerance", "TOLERANCE"),
("Center Mark", "DIMCENTER"),
("Dimension Style", "DIMSTYLE"),
],
),
(
"Modify",
&[
("Properties", "PROPERTIES"),
("Match Properties", "MATCHPROP"),
("Erase", "ERASE"),
("Copy", "COPY"),
("Mirror", "MIRROR"),
("Offset", "OFFSET"),
("Array", "ARRAY"),
("Move", "MOVE"),
("Rotate", "ROTATE"),
("Scale", "SCALE"),
("Stretch", "STRETCH"),
("Lengthen", "LENGTHEN"),
("Trim", "TRIM"),
("Extend", "EXTEND"),
("Break", "BREAK"),
("Join", "JOIN"),
("Chamfer", "CHAMFER"),
("Fillet", "FILLET"),
("Explode", "EXPLODE"),
("Explode Text", "TXTEXP"),
("Edit Polyline", "PEDIT"),
("Edit Spline", "SPLINEDIT"),
("Edit Hatch", "HATCHEDIT"),
],
),
(
"Parametric",
&[
("Auto Constrain", "AUTOCONSTRAIN"),
("Coincident", "GCCOINCIDENT"),
("Parallel", "PCONSTRAINT"),
("Perpendicular", "GCPERPENDICULAR"),
("Horizontal", "GCHORIZONTAL"),
("Vertical", "GCVERTICAL"),
("Tangent", "TCONSTRAINT"),
("Equal", "GCEQUAL"),
("Fix", "GCFIX"),
("Dimensional", "DIMCONSTRAINT"),
("Delete Constraints", "DELCONSTRAINT"),
("Parameters Manager", "PARAMETERS"),
],
),
("Help", &[("Help", "HELP"), ("About", "ABOUT")]),
];
fn entry_style(theme: &Theme, status: button::Status) -> button::Style {
let palette = theme.palette();
let pair = match status {
button::Status::Hovered | button::Status::Pressed => palette.primary.weak,
_ => palette.background.base,
};
button::Style {
background: Some(Background::Color(pair.color)),
text_color: pair.text,
..Default::default()
}
}
/// The menu bar, one pull-down per menu.
pub fn view<'a>() -> Element<'a, Message> {
let roots = MENUS
.iter()
.map(|&(title, entries)| {
let items = entries
.iter()
.map(|&(label, command)| {
Item::new(
button(text(crate::t!(label).into_owned()).size(12))
.on_press(Message::Command(command.to_string()))
.style(entry_style)
.padding([4, 12])
.width(Fill),
)
})
.collect();
Item::with_menu(
container(text(crate::t!(title).into_owned()).size(12)).padding([3, 8]),
Menu::new(items).width(240.0).offset(2.0),
)
})
.collect();
MenuBar::new(roots)
.close_on_item_click_global(true)
.style(|theme: &Theme, _| {
let palette = theme.palette();
iced_aw::style::menu_bar::Style {
bar_background: Background::Color(palette.background.weakest.color),
bar_border: Border::default(),
bar_shadow: Default::default(),
menu_background: Background::Color(palette.background.base.color),
menu_border: Border {
color: palette.background.strong.color,
width: 1.0,
radius: 3.0.into(),
},
menu_shadow: Default::default(),
path: Background::Color(palette.primary.weak.color),
path_border: Border::default(),
}
})
.width(Fill)
.into()
}
#[cfg(test)]
mod tests {
#[test]
fn every_menu_entry_runs_a_known_command() {
let mut known = crate::command::all_registered_command_names();
known.extend(["UNDO", "REDO", "QSAVE", "OPEN", "ZOOM"]);
let unknown: Vec<_> = super::MENUS
.iter()
.flat_map(|(_, entries)| entries.iter())
.filter(|(_, command)| !known.contains(command))
.collect();
assert!(unknown.is_empty(), "{unknown:?}");
}
}
+1
View File
@@ -8,6 +8,7 @@ pub mod dock;
pub mod icons;
pub mod modal;
pub mod node_graph;
pub mod menu_bar;
pub mod overlay;
pub mod popup;
pub mod properties;