fix: a credential or host-key prompt takes Enter and Escape from the modals underneath
Every modal read Enter/Escape globally, so a prompt that popped up over a dialog, the palette, Quick Connect or an SFTP dialog could be answered and the modal underneath confirmed or dismissed by the same key press. All modals except the prompts now read those keys through chrome::modal_key, which yields while a prompt is on screen.
This commit is contained in:
+3
-1
@@ -931,7 +931,7 @@ impl eframe::App for App {
|
||||
if self.palette.is_none() && self.dialog.is_none() && self.prompts.is_empty() {
|
||||
self.shortcuts(&ctx);
|
||||
}
|
||||
if ctx.input(|i| i.key_pressed(Key::Escape)) && ctx.input(|i| i.viewport().fullscreen.unwrap_or(false)) && self.palette.is_none() {
|
||||
if crate::chrome::modal_key(&ctx, Key::Escape) && ctx.input(|i| i.viewport().fullscreen.unwrap_or(false)) && self.palette.is_none() {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(false));
|
||||
}
|
||||
self.file_drops(&ctx);
|
||||
@@ -979,6 +979,8 @@ impl eframe::App for App {
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.dock)).show(ui, |ui| tabs::workspace(self, ui));
|
||||
|
||||
// Floating windows and modals.
|
||||
let prompt_open = !self.prompts.is_empty();
|
||||
ctx.data_mut(|d| d.insert_temp(egui::Id::new("nexus-prompt-open"), prompt_open));
|
||||
self.quick.ui(&ctx, &mut self.s);
|
||||
self.settings.ui(&ctx, &mut self.s);
|
||||
self.keys.ui(&ctx, &mut self.s);
|
||||
|
||||
+39
-7
@@ -598,14 +598,14 @@ pub fn palette(app: &mut App, ctx: &egui::Context) {
|
||||
let matches: Vec<_> = matches.into_iter().map(|(_, c)| c).collect();
|
||||
pal.sel = pal.sel.min(matches.len().saturating_sub(1));
|
||||
let mut chosen = None;
|
||||
let mut close = ctx.input(|i| i.key_pressed(Key::Escape));
|
||||
let mut close = crate::chrome::modal_key(ctx, Key::Escape);
|
||||
if ctx.input(|i| i.key_pressed(Key::ArrowDown)) {
|
||||
pal.sel = (pal.sel + 1).min(matches.len().saturating_sub(1));
|
||||
}
|
||||
if ctx.input(|i| i.key_pressed(Key::ArrowUp)) {
|
||||
pal.sel = pal.sel.saturating_sub(1);
|
||||
}
|
||||
if ctx.input(|i| i.key_pressed(Key::Enter)) {
|
||||
if crate::chrome::modal_key(ctx, Key::Enter) {
|
||||
chosen = matches.get(pal.sel).map(|m| m.0.clone());
|
||||
}
|
||||
modal_backdrop(ctx);
|
||||
@@ -677,6 +677,15 @@ pub fn modal(ctx: &egui::Context, id: &str, width: f32, body: impl FnOnce(&mut U
|
||||
});
|
||||
}
|
||||
|
||||
/// Set each frame while a credential or host-key prompt is on screen.
|
||||
const PROMPT_OPEN: &str = "nexus-prompt-open";
|
||||
|
||||
/// Enter/Escape for any modal except the prompts: while a prompt is showing it takes those keys,
|
||||
/// so one key press can't also confirm or dismiss the dialog underneath.
|
||||
pub fn modal_key(ctx: &egui::Context, key: Key) -> bool {
|
||||
!ctx.data(|d| d.get_temp::<bool>(egui::Id::new(PROMPT_OPEN)).unwrap_or(false)) && ctx.input(|i| i.key_pressed(key))
|
||||
}
|
||||
|
||||
pub fn prompts(app: &mut App, ctx: &egui::Context) {
|
||||
let Some(p) = app.prompts.front_mut() else {
|
||||
return;
|
||||
@@ -781,7 +790,7 @@ pub fn dialogs(app: &mut App, ctx: &egui::Context) {
|
||||
return;
|
||||
};
|
||||
let t = Tokens::get(ctx);
|
||||
let esc = ctx.input(|i| i.key_pressed(Key::Escape));
|
||||
let esc = crate::chrome::modal_key(ctx, Key::Escape);
|
||||
let mut keep = !esc;
|
||||
match &mut d {
|
||||
Dialog::Reuse { profile, existing } => {
|
||||
@@ -835,7 +844,7 @@ pub fn dialogs(app: &mut App, ctx: &egui::Context) {
|
||||
ui.label(format!("{name} is still connected."));
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::primary(ui, icon::X, "Close").clicked() || ctx.input(|i| i.key_pressed(Key::Enter)) {
|
||||
if widgets::primary(ui, icon::X, "Close").clicked() || crate::chrome::modal_key(ctx, Key::Enter) {
|
||||
let id = session.clone();
|
||||
app.close_session(&id);
|
||||
keep = false;
|
||||
@@ -869,7 +878,7 @@ pub fn dialogs(app: &mut App, ctx: &egui::Context) {
|
||||
let r = ui.add(egui::TextEdit::singleline(name).desired_width(f32::INFINITY));
|
||||
r.request_focus();
|
||||
ui.horizontal(|ui| {
|
||||
if widgets::primary(ui, icon::CHECK, "Rename").clicked() || ctx.input(|i| i.key_pressed(Key::Enter)) {
|
||||
if widgets::primary(ui, icon::CHECK, "Rename").clicked() || crate::chrome::modal_key(ctx, Key::Enter) {
|
||||
let n = name.trim().to_string();
|
||||
if !n.is_empty() {
|
||||
let _ = app.s.svc.sessions.rename(session, &n);
|
||||
@@ -943,7 +952,7 @@ pub fn dialogs(app: &mut App, ctx: &egui::Context) {
|
||||
}
|
||||
keep = false;
|
||||
}
|
||||
if ui.button("Cancel").clicked() || ctx.input(|i| i.key_pressed(Key::Escape)) {
|
||||
if ui.button("Cancel").clicked() || crate::chrome::modal_key(ctx, Key::Escape) {
|
||||
keep = false;
|
||||
}
|
||||
});
|
||||
@@ -1012,7 +1021,7 @@ pub fn dialogs(app: &mut App, ctx: &egui::Context) {
|
||||
let r = ui.add(egui::TextEdit::singleline(value).desired_width(f32::INFINITY));
|
||||
r.request_focus();
|
||||
ui.horizontal(|ui| {
|
||||
submit = widgets::primary(ui, icon::CHECK, "OK").clicked() || ctx.input(|i| i.key_pressed(Key::Enter));
|
||||
submit = widgets::primary(ui, icon::CHECK, "OK").clicked() || crate::chrome::modal_key(ctx, Key::Enter);
|
||||
if ui.button("Cancel").clicked() {
|
||||
keep = false;
|
||||
}
|
||||
@@ -1093,3 +1102,26 @@ pub fn toasts(app: &mut App, ctx: &egui::Context) {
|
||||
app.s.toasts.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod modal_key_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn an_open_prompt_takes_enter_and_escape() {
|
||||
let ctx = egui::Context::default();
|
||||
let press = |key| egui::RawInput {
|
||||
events: vec![egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }],
|
||||
..Default::default()
|
||||
};
|
||||
for (prompt_open, want) in [(false, true), (true, false)] {
|
||||
let mut got = None;
|
||||
let mut out = ctx.run_ui(press(Key::Enter), |ui| {
|
||||
ui.ctx().data_mut(|d| d.insert_temp(egui::Id::new(PROMPT_OPEN), prompt_open));
|
||||
got = Some(modal_key(ui.ctx(), Key::Enter));
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
assert_eq!(got, Some(want), "prompt open: {prompt_open}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ impl Forwards {
|
||||
}
|
||||
};
|
||||
let t = Tokens::get(ctx);
|
||||
let mut keep = !ctx.input(|i| i.key_pressed(Key::Escape));
|
||||
let mut keep = !crate::chrome::modal_key(ctx, Key::Escape);
|
||||
let mut stop = None;
|
||||
let mut start = false;
|
||||
let f = &mut self.form;
|
||||
|
||||
@@ -84,7 +84,7 @@ impl QuickConnect {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get(ctx);
|
||||
let (enter, esc) = ctx.input(|i| (i.key_pressed(Key::Enter), i.key_pressed(Key::Escape)));
|
||||
let (enter, esc) = (crate::chrome::modal_key(ctx, Key::Enter), crate::chrome::modal_key(ctx, Key::Escape));
|
||||
let mut connect = enter;
|
||||
let mut close = esc;
|
||||
let label = |ui: &mut egui::Ui, l: &str| ui.label(RichText::new(l).color(t.text_dim).size(12.0));
|
||||
|
||||
@@ -266,7 +266,7 @@ impl Recordings {
|
||||
let Some(r) = &self.confirm_delete else {
|
||||
return;
|
||||
};
|
||||
let (mut yes, mut no) = (false, ctx.input(|i| i.key_pressed(Key::Escape)));
|
||||
let (mut yes, mut no) = (false, crate::chrome::modal_key(ctx, Key::Escape));
|
||||
let name = if r.title.is_empty() { r.id.clone() } else { r.title.clone() };
|
||||
crate::chrome::modal(ctx, "rec-delete", 380.0, |ui| {
|
||||
ui.label(RichText::new("Delete recording?").font(theme::semibold(15.0)));
|
||||
|
||||
@@ -772,8 +772,8 @@ impl Sftp {
|
||||
return;
|
||||
};
|
||||
let t = Tokens::get(ctx);
|
||||
let esc = ctx.input(|i| i.key_pressed(Key::Escape));
|
||||
let enter = ctx.input(|i| i.key_pressed(Key::Enter));
|
||||
let esc = crate::chrome::modal_key(ctx, Key::Escape);
|
||||
let enter = crate::chrome::modal_key(ctx, Key::Enter);
|
||||
let mut keep = !esc;
|
||||
match &mut d {
|
||||
Dialog::Input { session, kind, value } => {
|
||||
@@ -961,7 +961,7 @@ impl Sftp {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ctx.input(|i| i.key_pressed(Key::Enter)) && writable && !target.is_empty() {
|
||||
if crate::chrome::modal_key(ctx, Key::Enter) && writable && !target.is_empty() {
|
||||
chosen = Some(target.clone());
|
||||
}
|
||||
if back && pk.filter.is_empty() && !pk.cwd.is_empty() && pk.cwd != "/" {
|
||||
|
||||
@@ -291,7 +291,7 @@ impl Snippets {
|
||||
|
||||
fn var_form(&mut self, ctx: &egui::Context, s: &mut Shared) {
|
||||
let Some(f) = &mut self.vars else { return };
|
||||
let (mut send, mut cancel) = (false, ctx.input(|i| i.key_pressed(Key::Escape)));
|
||||
let (mut send, mut cancel) = (false, crate::chrome::modal_key(ctx, Key::Escape));
|
||||
crate::chrome::modal(ctx, "snippet-vars", 360.0, |ui| {
|
||||
ui.label(RichText::new("Snippet Variables").font(theme::semibold(15.0)));
|
||||
ui.add_space(4.0);
|
||||
@@ -304,7 +304,7 @@ impl Snippets {
|
||||
}
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
send = widgets::primary(ui, icon::PAPER_PLANE_RIGHT, "Send").clicked() || ui.input(|i| i.key_pressed(Key::Enter));
|
||||
send = widgets::primary(ui, icon::PAPER_PLANE_RIGHT, "Send").clicked() || crate::chrome::modal_key(ui.ctx(), Key::Enter);
|
||||
cancel |= ui.button("Cancel").clicked();
|
||||
});
|
||||
});
|
||||
@@ -322,7 +322,7 @@ impl Snippets {
|
||||
return;
|
||||
};
|
||||
let name = self.list.iter().find(|x| x.id == id).map(|x| x.name.clone()).unwrap_or_default();
|
||||
let (mut yes, mut no) = (false, ctx.input(|i| i.key_pressed(Key::Escape)));
|
||||
let (mut yes, mut no) = (false, crate::chrome::modal_key(ctx, Key::Escape));
|
||||
crate::chrome::modal(ctx, "snippet-delete", 360.0, |ui| {
|
||||
ui.label(RichText::new("Delete snippet?").font(theme::semibold(15.0)));
|
||||
ui.label(format!("\u{201c}{name}\u{201d} will be permanently deleted."));
|
||||
|
||||
Reference in New Issue
Block a user