diff --git a/crates/ui-egui/locales/de-formats.json b/crates/ui-egui/locales/de-formats.json index 43afc32fb..fd5e19861 100644 --- a/crates/ui-egui/locales/de-formats.json +++ b/crates/ui-egui/locales/de-formats.json @@ -214,5 +214,6 @@ "“{parent}” has a “{name}” already. Merge the two?": "„{parent}“ enthält bereits „{name}“. Beide zusammenführen?", "There is a “{name}” at the top level already. Merge the two?": "Auf der obersten Ebene gibt es bereits „{name}“. Beide zusammenführen?", "Added “{keyword}” to {n} photo{}": "„{keyword}“ zu {n} Foto{} hinzugefügt", - "On {have} of {of} selected photos": "Auf {have} von {of} ausgewählten Fotos" + "On {have} of {of} selected photos": "Auf {have} von {of} ausgewählten Fotos", + "Remove “{name}”": "„{name}“ entfernen" } diff --git a/crates/ui-egui/locales/es-formats.json b/crates/ui-egui/locales/es-formats.json index f7109ab26..dfd320993 100644 --- a/crates/ui-egui/locales/es-formats.json +++ b/crates/ui-egui/locales/es-formats.json @@ -214,5 +214,6 @@ "“{parent}” has a “{name}” already. Merge the two?": "“{parent}” ya tiene “{name}”. ¿Combinar las dos?", "There is a “{name}” at the top level already. Merge the two?": "Ya hay “{name}” en el nivel superior. ¿Combinar las dos?", "Added “{keyword}” to {n} photo{}": "Se añadió “{keyword}” a {n} foto{}", - "On {have} of {of} selected photos": "En {have} de {of} fotos seleccionadas" + "On {have} of {of} selected photos": "En {have} de {of} fotos seleccionadas", + "Remove “{name}”": "Quitar “{name}”" } diff --git a/crates/ui-egui/locales/ja-formats.json b/crates/ui-egui/locales/ja-formats.json index 589509c4c..cab18a74a 100644 --- a/crates/ui-egui/locales/ja-formats.json +++ b/crates/ui-egui/locales/ja-formats.json @@ -214,5 +214,6 @@ "“{parent}” has a “{name}” already. Merge the two?": "「{parent}」にはすでに「{name}」があります。統合しますか?", "There is a “{name}” at the top level already. Merge the two?": "最上位にはすでに「{name}」があります。統合しますか?", "Added “{keyword}” to {n} photo{}": "写真{n}枚に「{keyword}」を追加しました{:.0}", - "On {have} of {of} selected photos": "選択した写真{of}枚のうち{have}枚" + "On {have} of {of} selected photos": "選択した写真{of}枚のうち{have}枚", + "Remove “{name}”": "「{name}」を削除" } diff --git a/crates/ui-egui/locales/pt-br-formats.json b/crates/ui-egui/locales/pt-br-formats.json index d4d8441bb..5215af8b3 100644 --- a/crates/ui-egui/locales/pt-br-formats.json +++ b/crates/ui-egui/locales/pt-br-formats.json @@ -214,5 +214,6 @@ "“{parent}” has a “{name}” already. Merge the two?": "“{parent}” já tem “{name}”. Mesclar as duas?", "There is a “{name}” at the top level already. Merge the two?": "Já existe “{name}” no nível superior. Mesclar as duas?", "Added “{keyword}” to {n} photo{}": "“{keyword}” adicionada a {n} foto{}", - "On {have} of {of} selected photos": "Em {have} de {of} fotos selecionadas" + "On {have} of {of} selected photos": "Em {have} de {of} fotos selecionadas", + "Remove “{name}”": "Remover “{name}”" } diff --git a/crates/ui-egui/locales/ru-formats.json b/crates/ui-egui/locales/ru-formats.json index f0e819db9..b59e94746 100644 --- a/crates/ui-egui/locales/ru-formats.json +++ b/crates/ui-egui/locales/ru-formats.json @@ -214,5 +214,6 @@ "“{parent}” has a “{name}” already. Merge the two?": "В «{parent}» уже есть «{name}». Объединить их?", "There is a “{name}” at the top level already. Merge the two?": "На верхнем уровне уже есть «{name}». Объединить их?", "Added “{keyword}” to {n} photo{}": "«{keyword}» добавлено к фото ({n}){:.0}", - "On {have} of {of} selected photos": "На {have} из {of} выбранных фото" + "On {have} of {of} selected photos": "На {have} из {of} выбранных фото", + "Remove “{name}”": "Убрать «{name}»" } diff --git a/crates/ui-egui/locales/zh-hans-formats.json b/crates/ui-egui/locales/zh-hans-formats.json index 9d9b6b42a..3d32f0968 100644 --- a/crates/ui-egui/locales/zh-hans-formats.json +++ b/crates/ui-egui/locales/zh-hans-formats.json @@ -214,5 +214,6 @@ "“{parent}” has a “{name}” already. Merge the two?": "“{parent}”中已有“{name}”。要合并两者吗?", "There is a “{name}” at the top level already. Merge the two?": "顶层已有“{name}”。要合并两者吗?", "Added “{keyword}” to {n} photo{}": "已将“{keyword}”添加到 {n} 张照片{:.0}", - "On {have} of {of} selected photos": "所选 {of} 张照片中的 {have} 张" + "On {have} of {of} selected photos": "所选 {of} 张照片中的 {have} 张", + "Remove “{name}”": "移除“{name}”" } diff --git a/crates/ui-egui/locales/zh-hant-formats.json b/crates/ui-egui/locales/zh-hant-formats.json index 29203443f..dfe74be2a 100644 --- a/crates/ui-egui/locales/zh-hant-formats.json +++ b/crates/ui-egui/locales/zh-hant-formats.json @@ -214,5 +214,6 @@ "“{parent}” has a “{name}” already. Merge the two?": "「{parent}」中已有「{name}」。要合併兩者嗎?", "There is a “{name}” at the top level already. Merge the two?": "最上層已有「{name}」。要合併兩者嗎?", "Added “{keyword}” to {n} photo{}": "已將「{keyword}」加入 {n} 張照片{:.0}", - "On {have} of {of} selected photos": "選取的 {of} 張照片中的 {have} 張" + "On {have} of {of} selected photos": "選取的 {of} 張照片中的 {have} 張", + "Remove “{name}”": "移除「{name}」" } diff --git a/crates/ui-egui/src/panels/keywording.rs b/crates/ui-egui/src/panels/keywording.rs index 64767c916..336378156 100644 --- a/crates/ui-egui/src/panels/keywording.rs +++ b/crates/ui-egui/src/panels/keywording.rs @@ -61,22 +61,30 @@ fn chip(ui: &mut egui::Ui, path: &str, partial: bool, cross: bool) -> (egui::Res let measure = |s: &str| ui.painter().layout_no_wrap(s.to_string(), font.clone(), color).size().x; let shown = chip_label(path, partial, room, measure); let w = measure(&shown) + 16.0 + cross_w; - let (rect, resp) = ui.allocate_exact_size(vec2(w, 22.0), Sense::click()); + // the row places it; its name and × answer under ids of their own keyword, so an open menu + // stays with its keyword when the chips change + let (rect, _) = ui.allocate_exact_size(vec2(w, 22.0), Sense::hover()); + let id = egui::Id::new(("keyword-chip", path.to_lowercase(), cross)); + let body = Rect::from_min_max(rect.min, pos2(rect.right() - cross_w, rect.bottom())); + let full = path.replace('|', " › "); + let resp = ui.interact(body, id, Sense::click()); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &full)); + let x = cross.then(|| { + let xr = Rect::from_center_size(pos2(rect.right() - 11.0, rect.center().y), vec2(14.0, 14.0)); + let xresp = ui.interact(xr, id.with("remove"), Sense::click()); + let said = crate::i18n::tr_format!("Remove “{name}”", name = full); + xresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &said)); + xresp + }); ui.painter().rect_stroke(rect, 10.0, egui::Stroke::new(1.0, t.field_border), egui::StrokeKind::Inside); - if resp.hovered() { + if resp.hovered() || x.as_ref().is_some_and(egui::Response::hovered) { ui.painter().rect_filled(rect.shrink(1.0), 10.0, t.hover.gamma_multiply(0.5)); } ui.painter().text(pos2(rect.left() + 8.0, rect.center().y), Align2::LEFT_CENTER, &shown, font.clone(), color); - let body = Rect::from_min_max(rect.min, pos2(rect.right() - cross_w, rect.bottom())); - let x = cross.then(|| { - let xr = Rect::from_center_size(pos2(rect.right() - 11.0, rect.center().y), vec2(14.0, 14.0)); - let xresp = ui.interact(xr, resp.id.with("remove"), Sense::click()); - ui.painter().text(xr.center(), Align2::CENTER_CENTER, "×", t.font(13.0), if xresp.hovered() { t.text } else { t.text_dim }); - xresp - }); - let mut body_resp = resp; - body_resp.rect = body; - (body_resp, x) + if let Some(x) = &x { + ui.painter().text(x.rect.center(), Align2::CENTER_CENTER, "×", t.font(13.0), if x.hovered() { t.text } else { t.text_dim }); + } + (resp, x) } /// A chip's menu: it acts on the selected photos (deleting a keyword from the whole library is the @@ -311,4 +319,27 @@ mod tests { c.apply(Op::SetKeyword { path: "Beach".into(), info: Some(KeywordInfo::default()) }).unwrap(); assert_eq!(shown(&chips(&c, &ids)), [("Beach", 1, 1)]); } + + /// Screen readers hear a chip by its whole keyword (even when cut short on screen) and its × + /// as removing that keyword. + #[test] + fn chips_describe_themselves() { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + ctx.enable_accesskit(); + let mut found = Vec::new(); + for _ in 0..3 { + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { + ui.set_max_width(120.0); + let _ = chip(ui, "Places|Portugal|Lisbon|Belém tower", true, true); + }); + out.textures_delta.clear(); + if let Some(update) = out.platform_output.accesskit_update.take() { + found = update.nodes.iter().map(|(_, n)| (format!("{:?}", n.role()), n.label().unwrap_or_default().to_string())).collect(); + } + } + let has = |role: &str, label: &str| found.iter().any(|(r, l)| r == role && l == label); + assert!(has("Button", "Places › Portugal › Lisbon › Belém tower"), "{found:?}"); + assert!(has("Button", "Remove “Places › Portugal › Lisbon › Belém tower”"), "{found:?}"); + } }