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 {
|
pub(super) fn control_state(&self) -> Value {
|
||||||
let tab = &self.tabs[self.active_tab];
|
let tab = &self.tabs[self.active_tab];
|
||||||
let command = tab.active_cmd.as_deref().map(active_command_metadata);
|
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"),
|
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,
|
"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,
|
"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())),
|
"hand_seed":format!("{:X}",tab.scene.document.header.handle_seed.max(tab.scene.document.next_handle())),
|
||||||
"plugins":plugin_ids(),
|
"plugins":plugin_ids(),
|
||||||
|
|||||||
@@ -1258,6 +1258,15 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// Missing SHX fonts of the last opened drawing, offered for download
|
/// Missing SHX fonts of the last opened drawing, offered for download
|
||||||
/// from the community repository (see `crate::io::font_repo`).
|
/// from the community repository (see `crate::io::font_repo`).
|
||||||
missing_fonts: Option<Vec<String>>,
|
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.
|
/// Drawings handed to us by other launches while `opening` was busy.
|
||||||
/// `opening` is a single slot that a second `OpenPathPicked` would
|
/// `opening` is a single slot that a second `OpenPathPicked` would
|
||||||
/// overwrite, and `on_file_opened` drops any result arriving once it is
|
/// overwrite, and `on_file_opened` drops any result arriving once it is
|
||||||
@@ -4178,6 +4187,9 @@ impl OpenCADStudio {
|
|||||||
open_job_serial: 0,
|
open_job_serial: 0,
|
||||||
recovery_report: None,
|
recovery_report: None,
|
||||||
missing_fonts: 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(),
|
pending_opens: std::collections::VecDeque::new(),
|
||||||
active_interaction_index: None,
|
active_interaction_index: None,
|
||||||
queued_interaction_indices: std::collections::VecDeque::new(),
|
queued_interaction_indices: std::collections::VecDeque::new(),
|
||||||
|
|||||||
+12
-2
@@ -1911,12 +1911,22 @@ impl OpenCADStudio {
|
|||||||
// fetch them from the community repository before the user
|
// fetch them from the community repository before the user
|
||||||
// studies garbled substitute text (unless recovery already
|
// studies garbled substitute text (unless recovery already
|
||||||
// owns the modal slot).
|
// 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,
|
&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.font_source_input = self.font_source_url.clone();
|
||||||
self.missing_fonts = Some(missing);
|
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);
|
self.active_modal = Some(crate::app::ModalKind::MissingFonts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-7
@@ -1180,13 +1180,20 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::MissingFontsDownload => {
|
Message::MissingFontsDownload => {
|
||||||
|
if self.missing_fonts_downloading {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
// Remember the source across sessions — and across drawings.
|
// Remember the source across sessions — and across drawings.
|
||||||
let source = self.font_source_input.trim().to_string();
|
let source = self.font_source_input.trim().to_string();
|
||||||
if self.font_source_url != source {
|
if self.font_source_url != source {
|
||||||
self.font_source_url = source.clone();
|
self.font_source_url = source.clone();
|
||||||
self.save_config();
|
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(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
let source = crate::io::font_repo::FontSource::from_url(&source);
|
let source = crate::io::font_repo::FontSource::from_url(&source);
|
||||||
@@ -1196,31 +1203,66 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Message::MissingFontsDismiss => {
|
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();
|
self.close_active_modal();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::MissingFontsResult(result) => {
|
Message::MissingFontsResult(result) => {
|
||||||
self.missing_fonts = None;
|
self.missing_fonts_downloading = false;
|
||||||
self.close_active_modal();
|
|
||||||
match result {
|
match result {
|
||||||
Ok(pairs) if pairs.is_empty() => {
|
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!(
|
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());
|
).as_ref());
|
||||||
}
|
}
|
||||||
Ok(pairs) => {
|
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 {
|
for (name, path) in &pairs {
|
||||||
self.command_line.push_output(crate::tf!(
|
self.command_line.push_output(crate::tf!(
|
||||||
"FONT Downloaded {name} → {path}",
|
"FONT Downloaded {name} → {path}",
|
||||||
path = path.display()
|
path = path.display()
|
||||||
).as_ref());
|
).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
|
// The downloaded files change glyph resolution for the
|
||||||
// whole drawing — reload it through the standard open
|
// whole drawing — reload it through the standard open
|
||||||
// pipeline so every wire is rebuilt with the real fonts.
|
// pipeline so every wire is rebuilt with the real fonts.
|
||||||
let i = self.active_tab;
|
let path = self.missing_fonts_path.take();
|
||||||
if let Some(path) = self.tabs[i].current_path.clone() {
|
self.close_active_modal();
|
||||||
|
if let Some(path) = path {
|
||||||
return Task::done(Message::OpenExternal(path));
|
return Task::done(Message::OpenExternal(path));
|
||||||
}
|
}
|
||||||
self.command_line.push_info(crate::t!(
|
self.command_line.push_info(crate::t!(
|
||||||
@@ -1228,6 +1270,8 @@ impl OpenCADStudio {
|
|||||||
).as_ref());
|
).as_ref());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
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());
|
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) {
|
if self.active_modal == Some(super::ModalKind::RecoveryPrompt) {
|
||||||
return self.update(Message::RecoveryDecline);
|
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.
|
// The Options window's × and Esc behave like its Close button.
|
||||||
if self.active_modal == Some(super::ModalKind::Options) {
|
if self.active_modal == Some(super::ModalKind::Options) {
|
||||||
return self.update(Message::OptionsClose);
|
return self.update(Message::OptionsClose);
|
||||||
|
|||||||
@@ -1675,6 +1675,7 @@ impl OpenCADStudio {
|
|||||||
crate::ui::window::missing_fonts::view_window(
|
crate::ui::window::missing_fonts::view_window(
|
||||||
fonts,
|
fonts,
|
||||||
&font_source,
|
&font_source,
|
||||||
|
self.missing_fonts_downloading,
|
||||||
flow,
|
flow,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ pub enum FontSource {
|
|||||||
Custom(String),
|
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 {
|
impl FontSource {
|
||||||
/// Empty / whitespace input selects the community repository.
|
/// Empty / whitespace input selects the community repository.
|
||||||
pub fn from_url(url: &str) -> Self {
|
pub fn from_url(url: &str) -> Self {
|
||||||
@@ -265,6 +274,12 @@ mod tests {
|
|||||||
assert_eq!(percent_encode_path("vn 30f.shx"), "vn%2030f.shx");
|
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]
|
#[test]
|
||||||
fn rejects_a_non_listing_response() {
|
fn rejects_a_non_listing_response() {
|
||||||
assert!(parse_contents("{\"message\":\"Not Found\"}").is_err());
|
assert!(parse_contents("{\"message\":\"Not Found\"}").is_err());
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ fn muted_style(theme: &Theme) -> text::Style {
|
|||||||
pub fn view_window<'a>(
|
pub fn view_window<'a>(
|
||||||
fonts: &'a [String],
|
fonts: &'a [String],
|
||||||
font_source: &'a str,
|
font_source: &'a str,
|
||||||
|
downloading: bool,
|
||||||
sizing: crate::ui::modal::ModalSizing,
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let content_width = if matches!(sizing.width, Length::Fill) {
|
let content_width = if matches!(sizing.width, Length::Fill) {
|
||||||
@@ -33,16 +34,24 @@ pub fn view_window<'a>(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|name| text(format!(" {name}")).size(12).into())
|
.map(|name| text(format!(" {name}")).size(12).into())
|
||||||
.collect();
|
.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![
|
let actions = row![
|
||||||
Space::new().width(content_width),
|
Space::new().width(content_width),
|
||||||
button(text(crate::t!("Skip")).size(12))
|
button(text(crate::t!("Skip")).size(12))
|
||||||
.on_press(Message::MissingFontsDismiss)
|
.on_press_maybe((!downloading).then_some(Message::MissingFontsDismiss))
|
||||||
.style(button::secondary)
|
.style(button::secondary)
|
||||||
.padding([6, 14]),
|
.padding([6, 14]),
|
||||||
button(text(crate::t!("Download")).size(12))
|
download,
|
||||||
.on_press(Message::MissingFontsDownload)
|
|
||||||
.style(button::primary)
|
|
||||||
.padding([6, 14]),
|
|
||||||
]
|
]
|
||||||
.spacing(8);
|
.spacing(8);
|
||||||
|
|
||||||
@@ -50,7 +59,7 @@ pub fn view_window<'a>(
|
|||||||
column![
|
column![
|
||||||
text(crate::t!("Missing fonts")).size(20),
|
text(crate::t!("Missing fonts")).size(20),
|
||||||
text(crate::t!(
|
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)
|
.size(11)
|
||||||
.style(muted_style),
|
.style(muted_style),
|
||||||
|
|||||||
Reference in new issue
Block a user