feat(app): first-launch prompt to set Open CAD Studio as default for .dwg/.dxf

On the first launch that hasn't answered it yet, the app shows a small
dialog offering to become the default handler for .dwg and .dxf files.
The choice is persisted (default_assoc_prompted) so the prompt never
reappears.

The platform plumbing lives in io/file_association.rs:
  * Windows — opens the OS's own per-app default-programs dialog via
    IApplicationAssociationRegistrationUI::LaunchAdvancedAssociationUI,
    passing the "Open CAD Studio" RegisteredApplications name the MSI
    registers. Modern Windows guards the actual default behind a
    UserChoice hash, so this is the supported consent path.
  * Linux — xdg-mime default against the installed .desktop file, whose
    MimeType entries already cover image/vnd.dwg and image/vnd.dxf.
  * macOS — LSSetDefaultRoleHandlerForContentType binds the DWG/DXF UTIs
    declared in Info.plist to the app bundle id.

The work runs on a dedicated thread (the Windows dialog is modal) with
the result delivered back through a oneshot channel and surfaced in the
command line.

https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
This commit is contained in:
Claude committed 2026-06-12 07:50:41 +00:00
1 parent e24447a70d
commit a7d85fe148
7 files changed
+457 -3

No files matched your search

+45
View File
@@ -85,6 +85,7 @@ impl OpenCADStudio {
snap_enabled: self.snapper.snap_enabled,
otrack: self.snapper.otrack_enabled,
snap_modes: super::settings::UserSettings::modes_from(self.snapper.enabled.iter()),
default_assoc_prompted: self.default_assoc_prompted,
}
}
@@ -98,6 +99,7 @@ impl OpenCADStudio {
self.snapper.snap_enabled = s.snap_enabled;
self.snapper.otrack_enabled = s.otrack;
self.snapper.enabled = s.snap_modes.iter().copied().collect();
self.default_assoc_prompted = s.default_assoc_prompted;
}
/// Write preferences to disk only when they differ from the last write,
@@ -110,6 +112,13 @@ impl OpenCADStudio {
}
}
/// Record that the one-time default-association prompt has been answered and
/// flush it to disk, so the dialog never reappears on later launches.
fn mark_assoc_prompted(&mut self) {
self.default_assoc_prompted = true;
self.persist_settings_if_changed();
}
fn update_inner(&mut self, msg: Message) -> Task<Message> {
match msg {
Message::Tick(t) => {
@@ -1613,6 +1622,12 @@ impl OpenCADStudio {
if self.update_notice_window == Some(id) {
self.update_notice_window = None;
}
// Closing the default-association prompt via the window chrome
// counts as answering it — never nag again.
if self.assoc_prompt_window == Some(id) {
self.assoc_prompt_window = None;
self.mark_assoc_prompted();
}
Task::none()
}
@@ -5647,6 +5662,36 @@ impl OpenCADStudio {
Task::none()
}
}
Message::AssocPromptYes => {
self.mark_assoc_prompted();
let close = if let Some(id) = self.assoc_prompt_window.take() {
window::close(id)
} else {
Task::none()
};
let run = Task::perform(
crate::io::file_association::set_default_app(),
Message::AssocResult,
);
Task::batch([close, run])
}
Message::AssocPromptNo => {
self.mark_assoc_prompted();
if let Some(id) = self.assoc_prompt_window.take() {
window::close(id)
} else {
Task::none()
}
}
Message::AssocResult(result) => {
match result {
Ok(msg) => self.command_line.push_info(&msg),
Err(err) => self
.command_line
.push_error(&format!("Could not set default app: {err}")),
}
Task::none()
}
Message::PageSetupWidthEdit(s) => {
self.page_setup_w = s;
Task::none()