feat: right-click context menu + CONSTRUCTIONLINE alias
Viewport right-click now shows a proper widget-based context menu: - When a command is active: Cancel / Enter - Otherwise: Repeat last commands, Delete/Move/Copy (if selected), Select All, Zoom Extents CONSTRUCTIONLINE added as an alias for XLINE (XL). Canvas-drawn placeholder removed; context menu closes on Escape, click-outside, or when any action is taken. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a20558a024
commit
3cdd86a674
7 changed files with 145 additions and 50 deletions
|
|
@ -96,7 +96,7 @@ Underlay (PDF/DWF/DGN)
|
||||||
| SOLID (2D dolu dörtgen) | ✅ |
|
| SOLID (2D dolu dörtgen) | ✅ |
|
||||||
| RECTANG (REC) | ✅ |
|
| RECTANG (REC) | ✅ |
|
||||||
| POLYGON (POL) | ✅ |
|
| POLYGON (POL) | ✅ |
|
||||||
| CONSTRUCTION LINE (tam sonsuz) | ⬜ |
|
| CONSTRUCTION LINE (tam sonsuz) | ✅ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -276,7 +276,7 @@ Underlay (PDF/DWF/DGN)
|
||||||
| MATCHPROP (özellik kopyala) | ✅ |
|
| MATCHPROP (özellik kopyala) | ✅ |
|
||||||
| BYLAYER hızlı atama | ✅ |
|
| BYLAYER hızlı atama | ✅ |
|
||||||
| Çoklu seçim (window/crossing) | ✅ |
|
| Çoklu seçim (window/crossing) | ✅ |
|
||||||
| Sağ tık bağlam menüsü | ⬜ |
|
| Sağ tık bağlam menüsü | ✅ |
|
||||||
| Araç çubuğu özelleştirme | ⬜ |
|
| Araç çubuğu özelleştirme | ⬜ |
|
||||||
| Tema / Renk şeması seçimi | ⬜ |
|
| Tema / Renk şeması seçimi | ⬜ |
|
||||||
| Klavye kısayol düzenleyici | ⬜ |
|
| Klavye kısayol düzenleyici | ⬜ |
|
||||||
|
|
|
||||||
|
|
@ -872,7 +872,7 @@ impl H7CAD {
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
"XLINE"|"XL" => {
|
"XLINE"|"XL"|"CONSTRUCTIONLINE" => {
|
||||||
use crate::modules::home::draw::ray::XLineCommand;
|
use crate::modules::home::draw::ray::XLineCommand;
|
||||||
let new_cmd = XLineCommand::new();
|
let new_cmd = XLineCommand::new();
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
|
|
|
||||||
|
|
@ -329,6 +329,8 @@ pub enum Message {
|
||||||
LayoutContextMenu(String),
|
LayoutContextMenu(String),
|
||||||
/// Close the layout context menu.
|
/// Close the layout context menu.
|
||||||
LayoutContextMenuClose,
|
LayoutContextMenuClose,
|
||||||
|
/// Close the viewport right-click context menu without performing any action.
|
||||||
|
ViewportContextMenuClose,
|
||||||
/// A window was closed by the OS (e.g. the user clicked the title-bar ✕).
|
/// A window was closed by the OS (e.g. the user clicked the title-bar ✕).
|
||||||
OsWindowClosed(window::Id),
|
OsWindowClosed(window::Id),
|
||||||
/// No-op — used as a fallback when a TabEvent has no host mapping.
|
/// No-op — used as a fallback when a TabEvent has no host mapping.
|
||||||
|
|
|
||||||
|
|
@ -498,7 +498,15 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::CommandEscape => {
|
Message::CommandEscape => {
|
||||||
// Cancel layout rename / context menu first, then fall through.
|
// Cancel layout rename / context menus first, then fall through.
|
||||||
|
let i_e = self.active_tab;
|
||||||
|
{
|
||||||
|
let mut sel = self.tabs[i_e].scene.selection.borrow_mut();
|
||||||
|
if sel.context_menu.is_some() {
|
||||||
|
sel.context_menu = None;
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
}
|
||||||
if self.layout_rename_state.take().is_some() || self.layout_context_menu.take().is_some() {
|
if self.layout_rename_state.take().is_some() || self.layout_context_menu.take().is_some() {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
@ -522,7 +530,12 @@ impl H7CAD {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::Command(cmd) => self.dispatch_command(&cmd),
|
Message::Command(cmd) => {
|
||||||
|
// Close viewport context menu if open.
|
||||||
|
let i = self.active_tab;
|
||||||
|
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
||||||
|
self.dispatch_command(&cmd)
|
||||||
|
}
|
||||||
|
|
||||||
Message::ToggleLayers => {
|
Message::ToggleLayers => {
|
||||||
if let Some(id) = self.layer_window.take() {
|
if let Some(id) = self.layer_window.take() {
|
||||||
|
|
@ -1708,6 +1721,7 @@ impl H7CAD {
|
||||||
|
|
||||||
Message::DeleteSelected => {
|
Message::DeleteSelected => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
||||||
let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect();
|
let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect();
|
||||||
if !handles.is_empty() {
|
if !handles.is_empty() {
|
||||||
self.push_undo_snapshot(i, "ERASE");
|
self.push_undo_snapshot(i, "ERASE");
|
||||||
|
|
@ -2127,6 +2141,12 @@ impl H7CAD {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::ViewportContextMenuClose => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
Message::EnterViewport(handle) => {
|
Message::EnterViewport(handle) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
// Clear paper-space selection before entering model space.
|
// Clear paper-space selection before entering model space.
|
||||||
|
|
|
||||||
118
src/app/view.rs
118
src/app/view.rs
|
|
@ -325,7 +325,21 @@ impl H7CAD {
|
||||||
iced::widget::Space::new().width(0).height(0).into()
|
iced::widget::Space::new().width(0).height(0).into()
|
||||||
};
|
};
|
||||||
|
|
||||||
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, textstyle_layer, tablestyle_layer, mlstyle_layer, dimstyle_layer].into()
|
// ── Viewport right-click context menu ─────────────────────────────
|
||||||
|
let viewport_ctx_layer: Element<'_, Message> = {
|
||||||
|
let ctx_pos = tab.scene.selection.borrow().context_menu;
|
||||||
|
if let Some(p) = ctx_pos {
|
||||||
|
let has_cmd = tab.active_cmd.is_some();
|
||||||
|
let has_selection = !tab.scene.selected.is_empty();
|
||||||
|
let last_cmds: Vec<String> = self.command_line.cmd_recall
|
||||||
|
.iter().rev().take(3).cloned().collect();
|
||||||
|
viewport_context_menu_overlay(p, has_cmd, has_selection, last_cmds)
|
||||||
|
} else {
|
||||||
|
iced::widget::Space::new().width(0).height(0).into()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, textstyle_layer, tablestyle_layer, mlstyle_layer, dimstyle_layer, viewport_ctx_layer].into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscription(&self) -> Subscription<Message> {
|
pub fn subscription(&self) -> Subscription<Message> {
|
||||||
|
|
@ -517,6 +531,108 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
|
||||||
|
|
||||||
// ── Layout context-menu overlay ────────────────────────────────────────────
|
// ── Layout context-menu overlay ────────────────────────────────────────────
|
||||||
|
|
||||||
|
// ── Viewport right-click context menu ──────────────────────────────────────
|
||||||
|
|
||||||
|
fn viewport_context_menu_overlay(
|
||||||
|
pos: iced::Point,
|
||||||
|
has_cmd: bool,
|
||||||
|
has_selection: bool,
|
||||||
|
last_cmds: Vec<String>,
|
||||||
|
) -> Element<'static, Message> {
|
||||||
|
const MENU_BG: Color = Color { r: 0.17, g: 0.17, b: 0.17, a: 1.0 };
|
||||||
|
const MENU_BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
||||||
|
const ITEM_HOVER: Color = Color { r: 0.25, g: 0.45, b: 0.70, a: 1.0 };
|
||||||
|
const TEXT_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||||||
|
const SEP_COL: Color = Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 };
|
||||||
|
|
||||||
|
let item = |label: String, msg: Message| -> Element<'static, Message> {
|
||||||
|
button(text(label).size(12).color(TEXT_COL))
|
||||||
|
.on_press(msg)
|
||||||
|
.style(|_: &Theme, status| button::Style {
|
||||||
|
background: Some(Background::Color(match status {
|
||||||
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
||||||
|
_ => Color::TRANSPARENT,
|
||||||
|
})),
|
||||||
|
text_color: TEXT_COL,
|
||||||
|
border: Border::default(),
|
||||||
|
shadow: iced::Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
})
|
||||||
|
.padding([4, 12])
|
||||||
|
.width(Fill)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sep = || -> Element<'static, Message> {
|
||||||
|
container(iced::widget::Space::new().width(Fill).height(1))
|
||||||
|
.style(move |_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(SEP_COL)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.width(Fill)
|
||||||
|
.height(1)
|
||||||
|
.padding([0, 6])
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut items: Vec<Element<'static, Message>> = Vec::new();
|
||||||
|
|
||||||
|
if has_cmd {
|
||||||
|
items.push(item("Cancel".to_string(), Message::CommandEscape));
|
||||||
|
items.push(item("Enter".to_string(), Message::CommandFinalize));
|
||||||
|
} else {
|
||||||
|
if !last_cmds.is_empty() {
|
||||||
|
let last = last_cmds[0].clone();
|
||||||
|
items.push(item(
|
||||||
|
format!("Repeat {last}"),
|
||||||
|
Message::Command(last.to_uppercase()),
|
||||||
|
));
|
||||||
|
if last_cmds.len() > 1 {
|
||||||
|
for cmd in last_cmds.iter().skip(1) {
|
||||||
|
let c = cmd.clone();
|
||||||
|
items.push(item(c.clone(), Message::Command(c.to_uppercase())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.push(sep());
|
||||||
|
}
|
||||||
|
if has_selection {
|
||||||
|
items.push(item("Delete".to_string(), Message::DeleteSelected));
|
||||||
|
items.push(item("Move".to_string(), Message::Command("MOVE".to_string())));
|
||||||
|
items.push(item("Copy".to_string(), Message::Command("COPY".to_string())));
|
||||||
|
items.push(sep());
|
||||||
|
}
|
||||||
|
items.push(item("Select All".to_string(), Message::Command("SELECTALL".to_string())));
|
||||||
|
items.push(item("Zoom Extents".to_string(), Message::Command("ZOOM".to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
let menu_col = column(items).spacing(0).width(180);
|
||||||
|
|
||||||
|
let menu = container(menu_col)
|
||||||
|
.style(move |_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(MENU_BG)),
|
||||||
|
border: Border { color: MENU_BORDER, width: 1.0, radius: 4.0.into() },
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.padding([4, 0]);
|
||||||
|
|
||||||
|
// Click-catcher to close the menu when clicking outside.
|
||||||
|
let catcher = mouse_area(
|
||||||
|
container(Space::new()).width(Fill).height(Fill),
|
||||||
|
)
|
||||||
|
.on_press(Message::ViewportContextMenuClose)
|
||||||
|
.on_right_press(Message::ViewportContextMenuClose);
|
||||||
|
|
||||||
|
// Position using top/left spacing.
|
||||||
|
let positioned = column![
|
||||||
|
Space::new().height(pos.y),
|
||||||
|
row![Space::new().width(pos.x), menu],
|
||||||
|
]
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill);
|
||||||
|
|
||||||
|
stack![catcher, positioned].into()
|
||||||
|
}
|
||||||
|
|
||||||
/// A small right-click context menu rendered above the status bar.
|
/// A small right-click context menu rendered above the status bar.
|
||||||
/// The `name` is the layout tab that was right-clicked.
|
/// The `name` is the layout tab that was right-clicked.
|
||||||
fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> {
|
fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ pub struct CommandLine {
|
||||||
pub input: String,
|
pub input: String,
|
||||||
pub history: Vec<HistoryEntry>,
|
pub history: Vec<HistoryEntry>,
|
||||||
/// Commands the user has typed (for ↑/↓ recall).
|
/// Commands the user has typed (for ↑/↓ recall).
|
||||||
cmd_recall: Vec<String>,
|
pub cmd_recall: Vec<String>,
|
||||||
/// Current position in `cmd_recall` while navigating (None = not navigating).
|
/// Current position in `cmd_recall` while navigating (None = not navigating).
|
||||||
recall_cursor: Option<usize>,
|
recall_cursor: Option<usize>,
|
||||||
/// Saved draft input before the user started navigating history.
|
/// Saved draft input before the user started navigating history.
|
||||||
|
|
|
||||||
|
|
@ -385,49 +385,6 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(p) = self.selection.context_menu {
|
|
||||||
let w = 140.0;
|
|
||||||
let h = 72.0;
|
|
||||||
let rect = canvas::Path::rectangle(Point::new(p.x, p.y), Size::new(w, h));
|
|
||||||
frame.fill(
|
|
||||||
&rect,
|
|
||||||
Color {
|
|
||||||
r: 0.12,
|
|
||||||
g: 0.12,
|
|
||||||
b: 0.12,
|
|
||||||
a: 0.95,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
frame.stroke(
|
|
||||||
&rect,
|
|
||||||
canvas::Stroke {
|
|
||||||
width: 1.0,
|
|
||||||
style: canvas::Style::Solid(Color {
|
|
||||||
r: 0.30,
|
|
||||||
g: 0.30,
|
|
||||||
b: 0.30,
|
|
||||||
a: 1.0,
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let items = ["Open", "Properties", "Hide"];
|
|
||||||
for (i, item) in items.iter().enumerate() {
|
|
||||||
frame.fill_text(canvas::Text {
|
|
||||||
content: item.to_string(),
|
|
||||||
position: Point::new(p.x + 10.0, p.y + 10.0 + i as f32 * 20.0),
|
|
||||||
color: Color::WHITE,
|
|
||||||
size: iced::Pixels(11.0),
|
|
||||||
font: iced::Font::DEFAULT,
|
|
||||||
align_x: iced::alignment::Horizontal::Left.into(),
|
|
||||||
align_y: iced::alignment::Vertical::Top.into(),
|
|
||||||
max_width: f32::INFINITY,
|
|
||||||
line_height: iced::widget::text::LineHeight::default(),
|
|
||||||
shaping: iced::widget::text::Shaping::default(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Grip markers ──────────────────────────────────────────────────
|
// ── Grip markers ──────────────────────────────────────────────────
|
||||||
for grip in &self.grips {
|
for grip in &self.grips {
|
||||||
let sp = grip.pos;
|
let sp = grip.pos;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue