//! `dialog` arms and helpers, split out of the original `update.rs` (#mechanical decomposition). #![allow(unused_imports)] use super::util::*; use super::{format_size, VIEWCUBE_HIT_SIZE}; use crate::app::helpers::{ ortho_constrain, parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, CoordKind, }; use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS}; use crate::modules::ModuleEvent; use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit}; use crate::scene::model::object::GripApply; use crate::scene::{ self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX, }; use crate::ui::PropertiesPanel; use acadrust::types::Color as AcadColor; use acadrust::{EntityType as AcadEntityType, Handle}; use iced::time::Instant; use iced::{mouse, Point, Task}; impl OpenCADStudio { pub(in crate::app) fn open_save_dialog_window(&mut self, tab_idx: usize) -> Task { // Default the format dropdown to the loaded file's own format — its // DWG-vs-DXF kind (from the extension) and its version (from the parsed // document) — so Save-As round-trips the format instead of silently // re-targeting it. A new/unsaved drawing has no source format, so it // uses the application-wide default chosen in Options (#529). self.save_dialog_format = if let Some(path) = &self.tabs[tab_idx].current_path { let document = &self.tabs[tab_idx].scene.document; let is_dxf = crate::io::source_is_dxf(Some(path), document); let version = if is_dxf { document.version } else { document.dwg_source_version.unwrap_or(document.version) }; crate::io::format_for_version(version, is_dxf) } else { self.default_save_format.clone() }; // Pre-fill the default file name from the current path or the tab name; // the destination folder comes from the native OS dialog that follows. if let Some(p) = &self.tabs[tab_idx].current_path.clone() { if let Some(name) = p.file_name() { self.save_dialog_filename = name.to_string_lossy().into_owned(); } } else { let (ext, _) = crate::io::parse_save_format(&self.save_dialog_format); self.save_dialog_filename = format!("{}.{ext}", self.tabs[tab_idx].tab_display_name()); } if self.tabs[tab_idx].recovery_save_as_required { let (ext, _) = crate::io::parse_save_format(&self.save_dialog_format); let stem = std::path::Path::new(&self.save_dialog_filename) .file_stem() .map(|value| value.to_string_lossy().into_owned()) .unwrap_or_else(|| self.tabs[tab_idx].tab_display_name()); self.save_dialog_filename = format!("{stem}_recovered.{ext}"); } self.aec_drop_acknowledged = false; self.active_modal = Some(crate::app::ModalKind::SaveDialog); Task::none() } pub(in crate::app) fn close_save_dialog_window(&mut self) -> Task { self.aec_drop_acknowledged = false; if self.active_modal == Some(crate::app::ModalKind::SaveDialog) { self.active_modal = None; self.reset_modal_geometry(); } Task::none() } pub(in crate::app) fn open_unsaved_dialog_window(&mut self) -> Task { self.active_modal = Some(crate::app::ModalKind::Unsaved); // The unsaved-changes prompt renders inside the main window, so bring // that window to the foreground — a close signal can arrive while the // app is backgrounded, leaving the prompt unseen behind other windows. // `gain_focus` alone is ignored by most Linux WMs (focus-stealing // prevention), so pair it with an urgency hint so the window is at // least flagged for attention when the compositor blocks the raise. match self.main_window { Some(id) => Task::batch([ iced::window::gain_focus(id), iced::window::request_user_attention( id, Some(iced::window::UserAttention::Critical), ), ]), None => Task::none(), } } pub(in crate::app) fn close_unsaved_dialog_window(&mut self) -> Task { if self.active_modal == Some(crate::app::ModalKind::Unsaved) { self.active_modal = None; self.reset_modal_geometry(); } Task::none() } pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEvent) -> Task { // On the Start page there is no drawing to act on, so a tool that // touches the scene is inert — point the user at New / Open // instead of running it into the empty welcome tab (#299). // // Commands are exempt: `dispatch_command` already decides which // ones stand alone (About, Donate, Report, the web link…) and // reports the rest. Refusing them here shadowed that list and // killed the welcome page's own buttons, which by definition can // only ever be clicked while `is_start` holds (#388, #389). // Keep the policy in one place — this door must not second-guess // it. Every other event below mutates the scene or its panels. if self.tabs[self.active_tab].is_start && !matches!(event, ModuleEvent::Command(_)) { self.ribbon.close_dropdown(); self.command_line .push_info(crate::t!("No drawing open — use New or Open first.").as_ref()); return Task::none(); } // Dismiss any open dropdown / collapsed-panel flyout on tool use, // and remember this tool as its panel's last-used one. self.ribbon.close_dropdown(); self.ribbon.note_panel_tool(&tool_id); self.ribbon.activate_tool(&tool_id); match event { ModuleEvent::Command(cmd) => { let task = self.dispatch_command(&cmd); // One-shot tools (view changes, clipboard, toggles, // audits…) leave nothing running: no interactive // command and no dialog. Their highlight would stick // forever — turn it off now. Interactive commands and // dialog owners keep theirs; the command end / modal // close clears those. (#355) let i = self.active_tab; if self.tabs[i].active_cmd.is_none() && self.active_modal.is_none() { self.ribbon.deactivate_tool(); } return task; } ModuleEvent::OpenFileDialog => { self.command_line .push_info(crate::t!("Open DWG/DXF: not yet implemented.").as_ref()); } ModuleEvent::ClearModels => { let i = self.active_tab; self.tabs[i].scene.clear(); self.tabs[i].properties = PropertiesPanel::empty(); self.command_line.push_output(crate::t!("Scene cleared.").as_ref()); } ModuleEvent::SetWireframe(w) => { let i = self.active_tab; self.tabs[i].wireframe = w; self.ribbon.set_wireframe(w); self.tabs[i].visual_style = if w { "Wireframe".into() } else { "Shaded".into() }; self.command_line.push_output(if w { "Visual style: Wireframe" } else { "Visual style: Shaded" }); } ModuleEvent::ToggleLayers => { return Task::done(Message::ToggleLayers); } ModuleEvent::PluginFileDialog { command, title, filter_name, extensions, } => { return Task::perform( async move { let exts: Vec<&str> = extensions.iter().map(|s| s.as_str()).collect(); let path = crate::sys::file_dialog() .set_title(title) .add_filter(filter_name, &exts) .add_filter("All Files", &["*"]) .pick_file() .await .map(|h| crate::sys::handle_path(&h)); (command, path) }, |(command, path)| Message::PluginFileDialogResult { command, path }, ); } } // Every non-Command event above is a one-shot (state toggle, // clear, dialog spawn) — nothing stays running to clear the // highlight later, so turn it off here. (#355) self.ribbon.deactivate_tool(); Task::none() } pub(super) fn on_unsaved_dialog_discard(&mut self) -> Task { match self.pending_close.take() { Some(crate::app::PendingClose::Tab(idx)) => { let close_win = self.close_unsaved_dialog_window(); // Discarded — drop this tab's autosave recovery copy. #[cfg(not(target_arch = "wasm32"))] let _ = std::fs::remove_file(self.autosave_target(idx)); if self.tabs.len() == 1 { self.tab_counter += 1; self.tabs[0] = crate::app::document::DocumentTab::new_drawing(self.tab_counter); self.active_tab = 0; self.apply_bg_default(0); } else { self.tabs.remove(idx); if self.active_tab >= self.tabs.len() { self.active_tab = self.tabs.len() - 1; } } // The active tab is now a fresh blank or a // different existing tab; sync ribbon chips so // they don't keep showing the discarded tab's // last selection. #21. self.sync_ribbon_layers(); self.sync_ribbon_from_selection(); return Task::batch([close_win, self.continue_tab_close_queue()]); } Some(crate::app::PendingClose::Quit) => { if let Some(idx) = self.tabs.iter().position(|t| t.dirty) { self.tabs[idx].dirty = false; } if self.tabs.iter().any(|t| t.dirty) { // More dirty tabs remain — keep window open. self.pending_close = Some(crate::app::PendingClose::Quit); } else { let close_win = self.close_unsaved_dialog_window(); return Task::batch(vec![close_win, self.exit_app()]); } } None => {} } Task::none() } pub(super) fn on_unsaved_dialog_save(&mut self) -> Task { #[cfg(not(target_arch = "wasm32"))] { let Some(pending) = self.pending_close.clone() else { return Task::none(); }; let (idx, continuation) = match pending { crate::app::PendingClose::Tab(idx) => { (idx, crate::app::SaveContinuation::CloseTab) } crate::app::PendingClose::Quit => { let Some(idx) = self.tabs.iter().position(|tab| tab.dirty) else { self.pending_close = None; return Task::batch([ self.close_unsaved_dialog_window(), self.exit_app(), ]); }; (idx, crate::app::SaveContinuation::Quit) } }; if self.active_save_jobs.contains_key(&self.tabs[idx].id) { self.command_line .push_info(crate::t!("Save already running for this drawing.").as_ref()); return Task::none(); } if !self.tabs[idx].recovery_save_as_required { if let Some(path) = self.tabs[idx].current_path.clone() { let version = self.tabs[idx].scene.document.version; self.prepare_native_save(idx); let close = self.close_unsaved_dialog_window(); let save = self.queue_native_save( idx, path, version, crate::app::SavePurpose::Manual, continuation, false, true, ); return Task::batch([close, save]); } } self.active_tab = idx; self.save_dialog_for_unsaved = true; let close = self.close_unsaved_dialog_window(); let save = self.save_with_default_format(idx); return Task::batch([close, save]); } #[cfg(target_arch = "wasm32")] { match self.pending_close.take() { Some(crate::app::PendingClose::Tab(idx)) => { self.pending_close = Some(crate::app::PendingClose::Tab(idx)); self.save_dialog_for_unsaved = true; let close = self.close_unsaved_dialog_window(); let save = self.save_with_default_format(idx); Task::batch([close, save]) } Some(crate::app::PendingClose::Quit) => { if let Some(idx) = self.tabs.iter().position(|tab| tab.dirty) { self.active_tab = idx; self.pending_close = Some(crate::app::PendingClose::Quit); self.save_dialog_for_unsaved = true; let close = self.close_unsaved_dialog_window(); let save = self.save_with_default_format(idx); Task::batch([close, save]) } else { Task::batch([ self.close_unsaved_dialog_window(), self.exit_app(), ]) } } None => Task::none(), } } } }