parent
7f1a1ff57b
commit
9d973a0fd6
8 changed files with 388 additions and 26 deletions
|
|
@ -568,6 +568,8 @@ pub(super) struct OpenCADStudio {
|
||||||
text_inline: Option<text_inline::TextInlineState>,
|
text_inline: Option<text_inline::TextInlineState>,
|
||||||
/// Which layout tab has its context menu open (None = closed).
|
/// Which layout tab has its context menu open (None = closed).
|
||||||
layout_context_menu: Option<String>,
|
layout_context_menu: Option<String>,
|
||||||
|
/// Which drawing tab has its context menu open (None = closed).
|
||||||
|
doc_tab_context_menu: Option<usize>,
|
||||||
/// Cursor-anchored one-shot snap override menu (Shift+RMB): the canvas
|
/// Cursor-anchored one-shot snap override menu (Shift+RMB): the canvas
|
||||||
/// point it opened at, or `None` when closed (#337).
|
/// point it opened at, or `None` when closed (#337).
|
||||||
snap_override_popup: Option<iced::Point>,
|
snap_override_popup: Option<iced::Point>,
|
||||||
|
|
@ -753,6 +755,10 @@ pub(super) struct OpenCADStudio {
|
||||||
// ── Unsaved-changes dialog ────────────────────────────────────────────
|
// ── Unsaved-changes dialog ────────────────────────────────────────────
|
||||||
/// Set when the user tries to close a tab or quit while there are unsaved changes.
|
/// Set when the user tries to close a tab or quit while there are unsaved changes.
|
||||||
pending_close: Option<PendingClose>,
|
pending_close: Option<PendingClose>,
|
||||||
|
/// Stable document ids waiting to be closed by "Close All" or
|
||||||
|
/// "Close All Other Drawings". Dirty drawings pause this queue at the
|
||||||
|
/// existing unsaved-changes dialog and resume after Save or Discard.
|
||||||
|
pending_tab_closes: std::collections::VecDeque<u64>,
|
||||||
/// Latest save job per stable tab id. Older completions may finish, but
|
/// Latest save job per stable tab id. Older completions may finish, but
|
||||||
/// cannot mark a newer document state clean or redirect its path.
|
/// cannot mark a newer document state clean or redirect its path.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
@ -1452,6 +1458,19 @@ pub enum Message {
|
||||||
TabSwitch(usize),
|
TabSwitch(usize),
|
||||||
/// Close the given tab index.
|
/// Close the given tab index.
|
||||||
TabClose(usize),
|
TabClose(usize),
|
||||||
|
/// Open/close the right-click menu for a drawing tab.
|
||||||
|
DocTabContextMenu(usize),
|
||||||
|
DocTabContextMenuClose,
|
||||||
|
/// Save every drawing that already has a file path.
|
||||||
|
DocTabSaveAll,
|
||||||
|
/// Close every non-Start drawing tab.
|
||||||
|
DocTabCloseAll,
|
||||||
|
/// Close every non-Start drawing tab except the given one.
|
||||||
|
DocTabCloseOthers(usize),
|
||||||
|
/// Copy the saved drawing's absolute path to the system clipboard.
|
||||||
|
DocTabCopyFullPath(usize),
|
||||||
|
/// Reveal the saved drawing in the platform file manager.
|
||||||
|
DocTabOpenFileLocation(usize),
|
||||||
// ── Unsaved-changes confirmation dialog ───────────────────────────────
|
// ── Unsaved-changes confirmation dialog ───────────────────────────────
|
||||||
/// User clicked "Save" in the unsaved-changes dialog.
|
/// User clicked "Save" in the unsaved-changes dialog.
|
||||||
UnsavedDialogSave,
|
UnsavedDialogSave,
|
||||||
|
|
@ -2493,6 +2512,7 @@ impl OpenCADStudio {
|
||||||
mtext_editor: None,
|
mtext_editor: None,
|
||||||
text_inline: None,
|
text_inline: None,
|
||||||
layout_context_menu: None,
|
layout_context_menu: None,
|
||||||
|
doc_tab_context_menu: None,
|
||||||
snap_override_popup: None,
|
snap_override_popup: None,
|
||||||
axis_lock_dir: None,
|
axis_lock_dir: None,
|
||||||
layout_rename_state: None,
|
layout_rename_state: None,
|
||||||
|
|
@ -2512,6 +2532,7 @@ impl OpenCADStudio {
|
||||||
active_interaction_index: None,
|
active_interaction_index: None,
|
||||||
queued_interaction_indices: std::collections::VecDeque::new(),
|
queued_interaction_indices: std::collections::VecDeque::new(),
|
||||||
pending_close: None,
|
pending_close: None,
|
||||||
|
pending_tab_closes: std::collections::VecDeque::new(),
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
active_save_jobs: std::collections::HashMap::new(),
|
active_save_jobs: std::collections::HashMap::new(),
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,34 @@ use iced::{mouse, Point, Task};
|
||||||
|
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
|
pub(super) fn begin_tab_close_queue(&mut self, tab_ids: Vec<u64>) -> Task<Message> {
|
||||||
|
self.pending_tab_closes.clear();
|
||||||
|
self.pending_tab_closes.extend(tab_ids);
|
||||||
|
self.continue_tab_close_queue()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close queued drawings until a dirty tab requires confirmation. Queue
|
||||||
|
/// entries use stable document ids because removing an earlier tab changes
|
||||||
|
/// every later vector index.
|
||||||
|
pub(super) fn continue_tab_close_queue(&mut self) -> Task<Message> {
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
while let Some(tab_id) = self.pending_tab_closes.pop_front() {
|
||||||
|
let Some(idx) = self.tabs.iter().position(|tab| tab.id == tab_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if self.tabs[idx].is_start {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.tabs[idx].dirty {
|
||||||
|
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
||||||
|
tasks.push(self.open_unsaved_dialog_window());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tasks.push(self.on_tab_close(idx));
|
||||||
|
}
|
||||||
|
Task::batch(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
// Start tab is fixed — close requests on it are no-ops.
|
// Start tab is fixed — close requests on it are no-ops.
|
||||||
if self.tabs.get(idx).map_or(false, |t| t.is_start) {
|
if self.tabs.get(idx).map_or(false, |t| t.is_start) {
|
||||||
|
|
@ -541,6 +569,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
}
|
}
|
||||||
if self.layout_rename_state.take().is_some()
|
if self.layout_rename_state.take().is_some()
|
||||||
|| self.layout_context_menu.take().is_some()
|
|| self.layout_context_menu.take().is_some()
|
||||||
|
|| self.doc_tab_context_menu.take().is_some()
|
||||||
{
|
{
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||||
// last selection. #21.
|
// last selection. #21.
|
||||||
self.sync_ribbon_layers();
|
self.sync_ribbon_layers();
|
||||||
self.sync_ribbon_from_selection();
|
self.sync_ribbon_from_selection();
|
||||||
return close_win;
|
return Task::batch([close_win, self.continue_tab_close_queue()]);
|
||||||
}
|
}
|
||||||
Some(crate::app::PendingClose::Quit) => {
|
Some(crate::app::PendingClose::Quit) => {
|
||||||
if let Some(idx) = self.tabs.iter().position(|t| t.dirty) {
|
if let Some(idx) = self.tabs.iter().position(|t| t.dirty) {
|
||||||
|
|
|
||||||
|
|
@ -937,6 +937,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
self.pending_close = None;
|
self.pending_close = None;
|
||||||
tasks.push(self.close_unsaved_dialog_window());
|
tasks.push(self.close_unsaved_dialog_window());
|
||||||
tasks.push(self.update(Message::TabClose(i)));
|
tasks.push(self.update(Message::TabClose(i)));
|
||||||
|
tasks.push(self.continue_tab_close_queue());
|
||||||
}
|
}
|
||||||
crate::app::SaveContinuation::Quit if snapshot_is_current => {
|
crate::app::SaveContinuation::Quit if snapshot_is_current => {
|
||||||
self.pending_close = None;
|
self.pending_close = None;
|
||||||
|
|
@ -1084,19 +1085,32 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
self.stamp_header_sysvars(i);
|
self.stamp_header_sysvars(i);
|
||||||
self.sync_truck_solids_to_acis(i);
|
self.sync_truck_solids_to_acis(i);
|
||||||
self.stamp_thumbnail(i, version);
|
self.stamp_thumbnail(i, version);
|
||||||
match crate::io::save_to_bytes(&self.tabs[i].scene.document, ext, version) {
|
let saved =
|
||||||
|
match crate::io::save_to_bytes(&self.tabs[i].scene.document, ext, version) {
|
||||||
Ok(bytes) => {
|
Ok(bytes) => {
|
||||||
crate::sys::download_bytes(&filename, &bytes);
|
crate::sys::download_bytes(&filename, &bytes);
|
||||||
self.tabs[i].dirty = false;
|
self.tabs[i].dirty = false;
|
||||||
self.command_line.push_output(&format!("Saved: {filename}"));
|
self.command_line.push_output(&format!("Saved: {filename}"));
|
||||||
|
true
|
||||||
}
|
}
|
||||||
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
Err(e) => {
|
||||||
}
|
self.command_line.push_error(&format!("Save failed: {e}"));
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
// Continue a pending tab close.
|
// Continue a pending tab close.
|
||||||
if self.save_dialog_for_unsaved {
|
if self.save_dialog_for_unsaved {
|
||||||
if let Some(crate::app::PendingClose::Tab(idx)) = self.pending_close.take() {
|
if saved {
|
||||||
let cont = self.update(Message::TabClose(idx));
|
if let Some(crate::app::PendingClose::Tab(idx)) =
|
||||||
return Task::batch([close, cont]);
|
self.pending_close.take()
|
||||||
|
{
|
||||||
|
let cont = self.update(Message::TabClose(idx));
|
||||||
|
let rest = self.continue_tab_close_queue();
|
||||||
|
return Task::batch([close, cont, rest]);
|
||||||
|
}
|
||||||
|
} else if self.pending_close.is_some() {
|
||||||
|
let retry = self.open_unsaved_dialog_window();
|
||||||
|
return Task::batch([close, retry]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close
|
close
|
||||||
|
|
|
||||||
|
|
@ -906,6 +906,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::TabSwitch(idx) => {
|
Message::TabSwitch(idx) => {
|
||||||
|
self.doc_tab_context_menu = None;
|
||||||
if idx < self.tabs.len() {
|
if idx < self.tabs.len() {
|
||||||
if idx != self.active_tab {
|
if idx != self.active_tab {
|
||||||
// The attribute editor is tab-scoped; leaving its tab
|
// The attribute editor is tab-scoped; leaving its tab
|
||||||
|
|
@ -940,7 +941,116 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::TabClose(idx) => self.on_tab_close(idx),
|
Message::TabClose(idx) => {
|
||||||
|
self.doc_tab_context_menu = None;
|
||||||
|
self.on_tab_close(idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DocTabContextMenu(idx) => {
|
||||||
|
if self.tabs.get(idx).is_some_and(|tab| !tab.is_start) {
|
||||||
|
self.layout_context_menu = None;
|
||||||
|
self.doc_tab_context_menu = Some(idx);
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DocTabContextMenuClose => {
|
||||||
|
self.doc_tab_context_menu = None;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DocTabSaveAll => {
|
||||||
|
self.doc_tab_context_menu = None;
|
||||||
|
self.dispatch_command("SAVEALL")
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DocTabCloseAll => {
|
||||||
|
self.doc_tab_context_menu = None;
|
||||||
|
let ids = self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.filter(|tab| !tab.is_start)
|
||||||
|
.map(|tab| tab.id)
|
||||||
|
.collect();
|
||||||
|
self.begin_tab_close_queue(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DocTabCloseOthers(idx) => {
|
||||||
|
self.doc_tab_context_menu = None;
|
||||||
|
let Some(keep_id) = self.tabs.get(idx).filter(|tab| !tab.is_start).map(|t| t.id)
|
||||||
|
else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let switch = self.update(Message::TabSwitch(idx));
|
||||||
|
let ids = self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.filter(|tab| !tab.is_start && tab.id != keep_id)
|
||||||
|
.map(|tab| tab.id)
|
||||||
|
.collect();
|
||||||
|
Task::batch([switch, self.begin_tab_close_queue(ids)])
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DocTabCopyFullPath(idx) => {
|
||||||
|
self.doc_tab_context_menu = None;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let Some(path) = self.tabs.get(idx).and_then(|tab| tab.current_path.clone())
|
||||||
|
else {
|
||||||
|
self.command_line
|
||||||
|
.push_error("Save the drawing before copying its file path.");
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
let full_path = path.canonicalize().unwrap_or_else(|_| {
|
||||||
|
if path.is_absolute() {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
std::env::current_dir()
|
||||||
|
.map(|dir| dir.join(&path))
|
||||||
|
.unwrap_or(path)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.command_line
|
||||||
|
.push_output(&format!("Copied path: {}", full_path.display()));
|
||||||
|
return iced::clipboard::write(full_path.to_string_lossy().into_owned());
|
||||||
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let _ = idx;
|
||||||
|
self.command_line
|
||||||
|
.push_error("Full file paths are unavailable in the web application.");
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::DocTabOpenFileLocation(idx) => {
|
||||||
|
self.doc_tab_context_menu = None;
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let Some(path) = self.tabs.get(idx).and_then(|tab| tab.current_path.clone())
|
||||||
|
else {
|
||||||
|
self.command_line
|
||||||
|
.push_error("Save the drawing before opening its file location.");
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
match crate::sys::reveal_in_file_manager(&path) {
|
||||||
|
Ok(()) => self
|
||||||
|
.command_line
|
||||||
|
.push_output(&format!("Opened file location: {}", path.display())),
|
||||||
|
Err(error) => self
|
||||||
|
.command_line
|
||||||
|
.push_error(&format!("Could not open file location: {error}")),
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
{
|
||||||
|
let _ = idx;
|
||||||
|
self.command_line
|
||||||
|
.push_error("File locations are unavailable in the web application.");
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Message::CommandInput(s) => {
|
Message::CommandInput(s) => {
|
||||||
// Space submits (acts like Enter) so a command advances
|
// Space submits (acts like Enter) so a command advances
|
||||||
|
|
@ -3227,6 +3337,7 @@ impl OpenCADStudio {
|
||||||
.map(|be| be.block_name == name)
|
.map(|be| be.block_name == name)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if name != "Model" && !is_block_tab {
|
if name != "Model" && !is_block_tab {
|
||||||
|
self.doc_tab_context_menu = None;
|
||||||
self.layout_context_menu = Some(name);
|
self.layout_context_menu = Some(name);
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -3936,6 +4047,7 @@ impl OpenCADStudio {
|
||||||
// ── Unsaved-changes dialog ────────────────────────────────────
|
// ── Unsaved-changes dialog ────────────────────────────────────
|
||||||
Message::UnsavedDialogCancel => {
|
Message::UnsavedDialogCancel => {
|
||||||
self.pending_close = None;
|
self.pending_close = None;
|
||||||
|
self.pending_tab_closes.clear();
|
||||||
self.close_unsaved_dialog_window()
|
self.close_unsaved_dialog_window()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ mod viewcube;
|
||||||
|
|
||||||
use controls::{dyn_component_value, viewport_controls};
|
use controls::{dyn_component_value, viewport_controls};
|
||||||
use overlay::{
|
use overlay::{
|
||||||
layout_context_menu_overlay, mtext_editor_overlay, position_canvas_overlay, qselect_overlay,
|
doc_tab_context_menu_overlay, layout_context_menu_overlay, mtext_editor_overlay,
|
||||||
text_inline_overlay, viewport_context_menu_overlay,
|
position_canvas_overlay, qselect_overlay, text_inline_overlay, viewport_context_menu_overlay,
|
||||||
};
|
};
|
||||||
use viewcube::{viewcube_nav_controls, viewcube_ucs_picker, UCS_PICKER_W};
|
use viewcube::{viewcube_nav_controls, viewcube_ucs_picker, UCS_PICKER_W};
|
||||||
|
|
||||||
|
|
@ -1596,6 +1596,27 @@ impl OpenCADStudio {
|
||||||
iced::widget::Space::new().width(0).height(0).into()
|
iced::widget::Space::new().width(0).height(0).into()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let doc_tab_ctx_layer: Element<'_, Message> =
|
||||||
|
if let Some(idx) = self.doc_tab_context_menu {
|
||||||
|
if let Some(context_tab) = self.tabs.get(idx) {
|
||||||
|
let has_other_drawings = self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.any(|(other_idx, tab)| other_idx != idx && !tab.is_start);
|
||||||
|
doc_tab_context_menu_overlay(
|
||||||
|
idx,
|
||||||
|
context_tab.current_path.as_deref(),
|
||||||
|
has_other_drawings,
|
||||||
|
win,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
iced::widget::Space::new().width(0).height(0).into()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
iced::widget::Space::new().width(0).height(0).into()
|
||||||
|
};
|
||||||
|
|
||||||
let snap_override_layer: Element<'_, Message> =
|
let snap_override_layer: Element<'_, Message> =
|
||||||
if let Some(pos) = self.snap_override_popup {
|
if let Some(pos) = self.snap_override_popup {
|
||||||
overlay::snap_override_overlay(pos)
|
overlay::snap_override_overlay(pos)
|
||||||
|
|
@ -1629,6 +1650,7 @@ impl OpenCADStudio {
|
||||||
sel_filter_layer,
|
sel_filter_layer,
|
||||||
dropdown_layer,
|
dropdown_layer,
|
||||||
layout_ctx_layer,
|
layout_ctx_layer,
|
||||||
|
doc_tab_ctx_layer,
|
||||||
qselect_layer,
|
qselect_layer,
|
||||||
snap_override_layer,
|
snap_override_layer,
|
||||||
open_progress_layer,
|
open_progress_layer,
|
||||||
|
|
@ -2156,22 +2178,29 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele
|
||||||
row![title_btn, close_btn].spacing(0).align_y(iced::Center)
|
row![title_btn, close_btn].spacing(0).align_y(iced::Center)
|
||||||
};
|
};
|
||||||
|
|
||||||
items.push(
|
let tab_container = container(row_inner).style(move |_: &Theme| container::Style {
|
||||||
container(row_inner)
|
border: Border {
|
||||||
.style(move |_: &Theme| container::Style {
|
color: if is_active {
|
||||||
border: Border {
|
BORDER_COLOR
|
||||||
color: if is_active {
|
} else {
|
||||||
BORDER_COLOR
|
Color::TRANSPARENT
|
||||||
} else {
|
},
|
||||||
Color::TRANSPARENT
|
width: if is_active { 1.0 } else { 0.0 },
|
||||||
},
|
radius: 0.0.into(),
|
||||||
width: if is_active { 1.0 } else { 0.0 },
|
},
|
||||||
radius: 0.0.into(),
|
..Default::default()
|
||||||
},
|
});
|
||||||
..Default::default()
|
|
||||||
})
|
let tab_element: Element<'_, Message> = if tab.is_start {
|
||||||
.into(),
|
tab_container.into()
|
||||||
);
|
} else {
|
||||||
|
crate::ui::wrap_bar::PosReport::owned(
|
||||||
|
format!("DOC_TAB:{idx}"),
|
||||||
|
mouse_area(tab_container).on_right_press(Message::DocTabContextMenu(idx)),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
items.push(tab_element);
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_btn = button(text("+").size(14).color(Color {
|
let new_btn = button(text("+").size(14).color(Color {
|
||||||
|
|
|
||||||
|
|
@ -711,6 +711,132 @@ pub(super) fn mtext_editor_overlay<'a>(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Drawing-tab right-click context menu ───────────────────────────────────
|
||||||
|
|
||||||
|
/// Right-click menu for a drawing tab. The scope deliberately contains only
|
||||||
|
/// the actions approved in issue #493.
|
||||||
|
pub(super) fn doc_tab_context_menu_overlay(
|
||||||
|
tab_idx: usize,
|
||||||
|
current_path: Option<&std::path::Path>,
|
||||||
|
has_other_drawings: bool,
|
||||||
|
win: (f32, f32),
|
||||||
|
) -> Element<'_, Message> {
|
||||||
|
const MENU_W: f32 = 210.0;
|
||||||
|
const MENU_H: f32 = 136.0;
|
||||||
|
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_COLOR: Color = Color {
|
||||||
|
r: 0.88,
|
||||||
|
g: 0.88,
|
||||||
|
b: 0.88,
|
||||||
|
a: 1.0,
|
||||||
|
};
|
||||||
|
const DISABLED_TEXT: Color = Color {
|
||||||
|
r: 0.43,
|
||||||
|
g: 0.43,
|
||||||
|
b: 0.43,
|
||||||
|
a: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let item = |label: &'static str, msg: Option<Message>| {
|
||||||
|
let enabled = msg.is_some();
|
||||||
|
let mut item = button(text(label).size(12).color(if enabled {
|
||||||
|
TEXT_COLOR
|
||||||
|
} else {
|
||||||
|
DISABLED_TEXT
|
||||||
|
}))
|
||||||
|
.style(move |_: &Theme, status| button::Style {
|
||||||
|
background: Some(Background::Color(if enabled {
|
||||||
|
match status {
|
||||||
|
button::Status::Hovered | button::Status::Pressed => ITEM_HOVER,
|
||||||
|
_ => Color::TRANSPARENT,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Color::TRANSPARENT
|
||||||
|
})),
|
||||||
|
text_color: if enabled { TEXT_COLOR } else { DISABLED_TEXT },
|
||||||
|
border: Border::default(),
|
||||||
|
shadow: iced::Shadow::default(),
|
||||||
|
snap: false,
|
||||||
|
})
|
||||||
|
.padding([4, 12])
|
||||||
|
.width(Fill);
|
||||||
|
if let Some(msg) = msg {
|
||||||
|
item = item.on_press(msg);
|
||||||
|
}
|
||||||
|
item
|
||||||
|
};
|
||||||
|
|
||||||
|
let native_path_actions = cfg!(not(target_arch = "wasm32")) && current_path.is_some();
|
||||||
|
let menu = container(
|
||||||
|
column![
|
||||||
|
item("Save All", Some(Message::DocTabSaveAll)),
|
||||||
|
item("Close All", Some(Message::DocTabCloseAll)),
|
||||||
|
item(
|
||||||
|
"Close All Other Drawings",
|
||||||
|
has_other_drawings.then_some(Message::DocTabCloseOthers(tab_idx)),
|
||||||
|
),
|
||||||
|
item(
|
||||||
|
"Copy Full File Path",
|
||||||
|
native_path_actions.then_some(Message::DocTabCopyFullPath(tab_idx)),
|
||||||
|
),
|
||||||
|
item(
|
||||||
|
"Open File Location",
|
||||||
|
native_path_actions.then_some(Message::DocTabOpenFileLocation(tab_idx)),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.spacing(0)
|
||||||
|
.width(MENU_W),
|
||||||
|
)
|
||||||
|
.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])
|
||||||
|
.width(iced::Length::Fixed(MENU_W));
|
||||||
|
|
||||||
|
let catcher = mouse_area(
|
||||||
|
container(Space::new().width(Fill).height(Fill))
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill),
|
||||||
|
)
|
||||||
|
.on_press(Message::DocTabContextMenuClose)
|
||||||
|
.on_right_press(Message::DocTabContextMenuClose);
|
||||||
|
|
||||||
|
let bounds = crate::ui::wrap_bar::dropdown_bounds(&format!("DOC_TAB:{tab_idx}"));
|
||||||
|
let pos = bounds
|
||||||
|
.map(|b| {
|
||||||
|
iced::Point::new(
|
||||||
|
b.x.clamp(0.0, (win.0 - MENU_W).max(0.0)),
|
||||||
|
(b.y + b.height).clamp(0.0, (win.1 - MENU_H).max(0.0)),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or(iced::Point::new(4.0, 30.0));
|
||||||
|
|
||||||
|
stack![catcher, position_canvas_overlay(pos, menu.into())].into()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Viewport right-click context menu ──────────────────────────────────────
|
// ── Viewport right-click context menu ──────────────────────────────────────
|
||||||
|
|
||||||
pub(super) fn viewport_context_menu_overlay(
|
pub(super) fn viewport_context_menu_overlay(
|
||||||
|
|
|
||||||
31
src/sys.rs
31
src/sys.rs
|
|
@ -17,6 +17,37 @@ pub fn open_url(url: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reveal a saved drawing in the native platform's file manager.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn reveal_in_file_manager(path: &std::path::Path) -> Result<(), String> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let status = std::process::Command::new("explorer.exe")
|
||||||
|
.arg("/select,")
|
||||||
|
.arg(path)
|
||||||
|
.status();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let status = std::process::Command::new("open")
|
||||||
|
.arg("-R")
|
||||||
|
.arg(path)
|
||||||
|
.status();
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||||
|
{
|
||||||
|
let folder = path
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| format!("Path has no parent folder: {}", path.display()))?;
|
||||||
|
return open::that(folder).map_err(|e| e.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
|
match status {
|
||||||
|
Ok(status) if status.success() => Ok(()),
|
||||||
|
Ok(status) => Err(format!("File manager exited with {status}")),
|
||||||
|
Err(error) => Err(error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Web: read text from the system clipboard via the async Clipboard API.
|
/// Web: read text from the system clipboard via the async Clipboard API.
|
||||||
/// iced's own `clipboard::read` is a no-op on the web (the browser clipboard is
|
/// iced's own `clipboard::read` is a no-op on the web (the browser clipboard is
|
||||||
/// async + permission-gated), so the editor paste paths use this instead. The
|
/// async + permission-gated), so the editor paste paths use this instead. The
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue