diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index cbf21ddd9..0cbd66de7 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1194,6 +1194,30 @@ mod tests { assert_eq!(opened.borrow().len(), 1); } + /// #2181: an OK that arrives with synthetic control-channel input (`ui.key` Enter, `ui.click` + /// on OK) opens nothing and keeps the prompt open; the same OK from the user opens the link. + #[test] + fn synthetic_input_cannot_confirm_open_hyperlink() { + use show::{SYNTHETIC_LINK_CONFIRM, confirm_open_link}; + use std::{cell::RefCell, rc::Rc}; + let opened = Rc::new(RefCell::new(Vec::::new())); + let sink = opened.clone(); + let services = crate::Services { open_url: Some(Box::new(move |u: &str| sink.borrow_mut().push(u.to_string()))), ..Default::default() }; + let mut session = gridcraft_engine::Session::default(); + session.new_workbook(); + let mut app = SheetApp::new(session, services); + let d = Dialog::open(&mut app, "openLink", json!({"url": "https://example.org/a"})).unwrap(); + + app.synthetic_pending = true; + assert!(!confirm_open_link(&mut app, &d), "the prompt stays open"); + assert!(opened.borrow().is_empty(), "synthetic input opens nothing"); + assert_eq!(app.message.as_ref().map(|m| m.1.as_str()), Some(SYNTHETIC_LINK_CONFIRM)); + + app.synthetic_pending = false; + app.message = None; + assert!(confirm_open_link(&mut app, &d), "the user's OK closes the prompt"); + assert_eq!(*opened.borrow(), vec!["https://example.org/a".to_string()]); + } #[test] fn data_validation_without_a_rule_opens_on_a_blank_list() { diff --git a/crates/ui-egui/src/dialogs/show.rs b/crates/ui-egui/src/dialogs/show.rs index 1aa013442..6dd5519c3 100644 --- a/crates/ui-egui/src/dialogs/show.rs +++ b/crates/ui-egui/src/dialogs/show.rs @@ -409,9 +409,8 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) { } if confirm && d.name == "openLink" { confirm = false; - open = false; - if let Err(e) = open_confirmed_link(app, &d) { - app.message = Some((d.title.clone(), crate::clean_error(&e))); + if confirm_open_link(app, &d) { + open = false; } } if confirm && d.command.is_some() && home_sheet_deleted(app, &mut d) { @@ -492,6 +491,24 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) { } } +/// OK in Open Hyperlink; returns whether the prompt closes. A pass that carries control-channel +/// input (`ui.key`, `ui.click`, `ui.drag`) can't say yes for the user (#2181): the link stays +/// unopened and the prompt open, with a message. Real input in that same pass is refused too, so +/// the user just presses OK again. +pub(super) fn confirm_open_link(app: &mut SheetApp, d: &Dialog) -> bool { + if app.synthetic_pending { + app.message = Some((d.title.clone(), SYNTHETIC_LINK_CONFIRM.into())); + return false; + } + if let Err(e) = open_confirmed_link(app, d) { + app.message = Some((d.title.clone(), crate::clean_error(&e))); + } + true +} + +/// Why Open Hyperlink ignored an OK that arrived with synthetic control-channel input. +pub(super) const SYNTHETIC_LINK_CONFIRM: &str = "Synthetic input from the control channel can't open a link: press OK yourself to open it."; + /// The user pressed OK in Open Hyperlink: opens the address the prompt showed (`d.link`), never /// one a caller edited into its values afterwards. pub(super) fn open_confirmed_link(app: &mut SheetApp, d: &Dialog) -> Result<(), String> {