forked from 1b-agent/opencadstudio-upstream
Merge pull request #1460 from lalomalvi:codex/missing-font-prompt-hardening-pr
This commit is contained in:
commit
9cf9184d9a
7 files changed
+114
-15
No files matched your search
@@ -426,8 +426,10 @@ impl OpenCADStudio {
|
||||
pub(super) fn control_state(&self) -> Value {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let command = tab.active_cmd.as_deref().map(active_command_metadata);
|
||||
let missing_fonts = crate::io::font_repo::missing_shx_fonts(&tab.scene.document);
|
||||
json!({"ok":true,"protocol":1,"session_id":session_id(),"version":env!("OCS_APP_VERSION"),
|
||||
"mode":if self.main_window.is_some(){"gui"}else{"headless"},"enabled":self.control.enabled,
|
||||
"read_only":self.read_only,"missing_fonts":missing_fonts,
|
||||
"document_id":tab.id,"revision":tab.edit_revision,"geometry_revision":tab.scene.geometry_epoch,"camera_revision":tab.scene.camera_generation,
|
||||
"hand_seed":format!("{:X}",tab.scene.document.header.handle_seed.max(tab.scene.document.next_handle())),
|
||||
"plugins":plugin_ids(),
|
||||
|
||||
@@ -1258,6 +1258,15 @@ pub(super) struct OpenCADStudio {
|
||||
/// Missing SHX fonts of the last opened drawing, offered for download
|
||||
/// from the community repository (see `crate::io::font_repo`).
|
||||
missing_fonts: Option<Vec<String>>,
|
||||
/// Drawing that produced `missing_fonts`; the active tab may change while
|
||||
/// a download is in flight.
|
||||
missing_fonts_path: Option<PathBuf>,
|
||||
/// Prevent duplicate download tasks and keep modal contents visible while
|
||||
/// the background request is running.
|
||||
missing_fonts_downloading: bool,
|
||||
/// Fonts skipped or unavailable during this process lifetime. Reopening a
|
||||
/// drawing must not repeatedly nag for the same unresolved file.
|
||||
suppressed_missing_fonts: rustc_hash::FxHashSet<String>,
|
||||
/// Drawings handed to us by other launches while `opening` was busy.
|
||||
/// `opening` is a single slot that a second `OpenPathPicked` would
|
||||
/// overwrite, and `on_file_opened` drops any result arriving once it is
|
||||
@@ -4178,6 +4187,9 @@ impl OpenCADStudio {
|
||||
open_job_serial: 0,
|
||||
recovery_report: None,
|
||||
missing_fonts: None,
|
||||
missing_fonts_path: None,
|
||||
missing_fonts_downloading: false,
|
||||
suppressed_missing_fonts: rustc_hash::FxHashSet::default(),
|
||||
pending_opens: std::collections::VecDeque::new(),
|
||||
active_interaction_index: None,
|
||||
queued_interaction_indices: std::collections::VecDeque::new(),
|
||||
|
||||
+12
-2
@@ -1911,12 +1911,22 @@ impl OpenCADStudio {
|
||||
// fetch them from the community repository before the user
|
||||
// studies garbled substitute text (unless recovery already
|
||||
// owns the modal slot).
|
||||
let missing = crate::io::font_repo::missing_shx_fonts(
|
||||
let mut missing = crate::io::font_repo::missing_shx_fonts(
|
||||
&self.tabs[i].scene.document,
|
||||
);
|
||||
if !missing.is_empty() && self.check_missing_fonts {
|
||||
missing.retain(|name| {
|
||||
!self
|
||||
.suppressed_missing_fonts
|
||||
.contains(&crate::io::font_repo::font_key(name))
|
||||
});
|
||||
// Read-only/MCP evaluation sessions must remain non-blocking:
|
||||
// report missing fonts through control state, but never offer
|
||||
// a network/download mutation from a read-only launch.
|
||||
if !missing.is_empty() && self.check_missing_fonts && !self.read_only {
|
||||
self.font_source_input = self.font_source_url.clone();
|
||||
self.missing_fonts = Some(missing);
|
||||
self.missing_fonts_path = self.tabs[i].current_path.clone();
|
||||
self.missing_fonts_downloading = false;
|
||||
self.active_modal = Some(crate::app::ModalKind::MissingFonts);
|
||||
}
|
||||
}
|
||||
|
||||
+57
-7
@@ -1180,13 +1180,20 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
Message::MissingFontsDownload => {
|
||||
if self.missing_fonts_downloading {
|
||||
return Task::none();
|
||||
}
|
||||
// Remember the source across sessions — and across drawings.
|
||||
let source = self.font_source_input.trim().to_string();
|
||||
if self.font_source_url != source {
|
||||
self.font_source_url = source.clone();
|
||||
self.save_config();
|
||||
}
|
||||
let fonts = self.missing_fonts.take().unwrap_or_default();
|
||||
let fonts = self.missing_fonts.clone().unwrap_or_default();
|
||||
if fonts.is_empty() {
|
||||
return Task::none();
|
||||
}
|
||||
self.missing_fonts_downloading = true;
|
||||
Task::perform(
|
||||
async move {
|
||||
let source = crate::io::font_repo::FontSource::from_url(&source);
|
||||
@@ -1196,31 +1203,66 @@ impl OpenCADStudio {
|
||||
)
|
||||
}
|
||||
Message::MissingFontsDismiss => {
|
||||
self.missing_fonts = None;
|
||||
if self.missing_fonts_downloading {
|
||||
return Task::none();
|
||||
}
|
||||
for name in self.missing_fonts.take().unwrap_or_default() {
|
||||
self.suppressed_missing_fonts
|
||||
.insert(crate::io::font_repo::font_key(&name));
|
||||
}
|
||||
self.missing_fonts_path = None;
|
||||
self.close_active_modal();
|
||||
Task::none()
|
||||
}
|
||||
Message::MissingFontsResult(result) => {
|
||||
self.missing_fonts = None;
|
||||
self.close_active_modal();
|
||||
self.missing_fonts_downloading = false;
|
||||
match result {
|
||||
Ok(pairs) if pairs.is_empty() => {
|
||||
let unavailable = self.missing_fonts.take().unwrap_or_default();
|
||||
for name in &unavailable {
|
||||
self.suppressed_missing_fonts
|
||||
.insert(crate::io::font_repo::font_key(name));
|
||||
}
|
||||
self.missing_fonts_path = None;
|
||||
self.close_active_modal();
|
||||
self.command_line.push_error(crate::t!(
|
||||
"None of the missing fonts are in the community repository yet. Contribute them at github.com/huaninstratech/OpenCADStudio/tree/main/fonts."
|
||||
"None of the requested fonts are available from the selected source. Substitute fonts remain active; this notice will not repeat during this session."
|
||||
).as_ref());
|
||||
}
|
||||
Ok(pairs) => {
|
||||
let requested = self.missing_fonts.take().unwrap_or_default();
|
||||
let downloaded: rustc_hash::FxHashSet<String> = pairs
|
||||
.iter()
|
||||
.map(|(name, _)| crate::io::font_repo::font_key(name))
|
||||
.collect();
|
||||
let unavailable: Vec<String> = requested
|
||||
.into_iter()
|
||||
.filter(|name| {
|
||||
!downloaded.contains(&crate::io::font_repo::font_key(name))
|
||||
})
|
||||
.collect();
|
||||
for name in &unavailable {
|
||||
self.suppressed_missing_fonts
|
||||
.insert(crate::io::font_repo::font_key(name));
|
||||
}
|
||||
for (name, path) in &pairs {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"FONT Downloaded {name} → {path}",
|
||||
path = path.display()
|
||||
).as_ref());
|
||||
}
|
||||
if !unavailable.is_empty() {
|
||||
self.command_line.push_info(crate::tf!(
|
||||
"FONT Not available from the selected source: {fonts}. Substitute fonts remain active.",
|
||||
fonts = unavailable.join(", ")
|
||||
).as_ref());
|
||||
}
|
||||
// The downloaded files change glyph resolution for the
|
||||
// whole drawing — reload it through the standard open
|
||||
// pipeline so every wire is rebuilt with the real fonts.
|
||||
let i = self.active_tab;
|
||||
if let Some(path) = self.tabs[i].current_path.clone() {
|
||||
let path = self.missing_fonts_path.take();
|
||||
self.close_active_modal();
|
||||
if let Some(path) = path {
|
||||
return Task::done(Message::OpenExternal(path));
|
||||
}
|
||||
self.command_line.push_info(crate::t!(
|
||||
@@ -1228,6 +1270,8 @@ impl OpenCADStudio {
|
||||
).as_ref());
|
||||
}
|
||||
Err(e) => {
|
||||
// Keep the prompt open: the user can correct a custom
|
||||
// source or retry a transient network failure.
|
||||
self.command_line.push_error(crate::tf!("Font download failed: {e}").as_ref());
|
||||
}
|
||||
}
|
||||
@@ -8043,6 +8087,12 @@ impl OpenCADStudio {
|
||||
if self.active_modal == Some(super::ModalKind::RecoveryPrompt) {
|
||||
return self.update(Message::RecoveryDecline);
|
||||
}
|
||||
// The title-bar X, Escape, and MCP `close_modal` action mean
|
||||
// the same thing as Skip for this prompt. Record that choice
|
||||
// so reopening the drawing does not immediately nag again.
|
||||
if self.active_modal == Some(super::ModalKind::MissingFonts) {
|
||||
return self.update(Message::MissingFontsDismiss);
|
||||
}
|
||||
// The Options window's × and Esc behave like its Close button.
|
||||
if self.active_modal == Some(super::ModalKind::Options) {
|
||||
return self.update(Message::OptionsClose);
|
||||
|
||||
@@ -1675,6 +1675,7 @@ impl OpenCADStudio {
|
||||
crate::ui::window::missing_fonts::view_window(
|
||||
fonts,
|
||||
&font_source,
|
||||
self.missing_fonts_downloading,
|
||||
flow,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -42,6 +42,15 @@ pub enum FontSource {
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// Stable key for comparing bare font names without path or case differences.
|
||||
pub fn font_key(name: &str) -> String {
|
||||
name.rsplit(['/', '\\'])
|
||||
.next()
|
||||
.unwrap_or(name)
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
impl FontSource {
|
||||
/// Empty / whitespace input selects the community repository.
|
||||
pub fn from_url(url: &str) -> Self {
|
||||
@@ -265,6 +274,12 @@ mod tests {
|
||||
assert_eq!(percent_encode_path("vn 30f.shx"), "vn%2030f.shx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_keys_are_bare_trimmed_and_case_insensitive() {
|
||||
assert_eq!(font_key(r#" C:\Fonts\ROMANS.SHX "#), "romans.shx");
|
||||
assert_eq!(font_key("folder/simplex.shx"), "simplex.shx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_a_non_listing_response() {
|
||||
assert!(parse_contents("{\"message\":\"Not Found\"}").is_err());
|
||||
|
||||
@@ -17,6 +17,7 @@ fn muted_style(theme: &Theme) -> text::Style {
|
||||
pub fn view_window<'a>(
|
||||
fonts: &'a [String],
|
||||
font_source: &'a str,
|
||||
downloading: bool,
|
||||
sizing: crate::ui::modal::ModalSizing,
|
||||
) -> Element<'a, Message> {
|
||||
let content_width = if matches!(sizing.width, Length::Fill) {
|
||||
@@ -33,16 +34,24 @@ pub fn view_window<'a>(
|
||||
.iter()
|
||||
.map(|name| text(format!(" {name}")).size(12).into())
|
||||
.collect();
|
||||
let download = button(
|
||||
text(if downloading {
|
||||
crate::t!("Downloading...")
|
||||
} else {
|
||||
crate::t!("Download available")
|
||||
})
|
||||
.size(12),
|
||||
)
|
||||
.on_press_maybe((!downloading).then_some(Message::MissingFontsDownload))
|
||||
.style(button::primary)
|
||||
.padding([6, 14]);
|
||||
let actions = row![
|
||||
Space::new().width(content_width),
|
||||
button(text(crate::t!("Skip")).size(12))
|
||||
.on_press(Message::MissingFontsDismiss)
|
||||
.on_press_maybe((!downloading).then_some(Message::MissingFontsDismiss))
|
||||
.style(button::secondary)
|
||||
.padding([6, 14]),
|
||||
button(text(crate::t!("Download")).size(12))
|
||||
.on_press(Message::MissingFontsDownload)
|
||||
.style(button::primary)
|
||||
.padding([6, 14]),
|
||||
download,
|
||||
]
|
||||
.spacing(8);
|
||||
|
||||
@@ -50,7 +59,7 @@ pub fn view_window<'a>(
|
||||
column![
|
||||
text(crate::t!("Missing fonts")).size(20),
|
||||
text(crate::t!(
|
||||
"This drawing uses fonts that are not installed on this machine. Download them from the OpenCADStudio community font repository?"
|
||||
"This drawing uses fonts that are not installed on this machine. OpenCADStudio can download any redistributable fonts that exist in the selected repository; unavailable fonts will keep using a substitute."
|
||||
))
|
||||
.size(11)
|
||||
.style(muted_style),
|
||||
|
||||
Reference in new issue
Block a user