Files
driftwood/src/ui/integration_dialog.rs
lashman 7e55d5796f Add WCAG 2.2 AAA compliance and automated AT-SPI audit tool
- Bring all UI widgets to WCAG 2.2 AAA conformance across all views
- Add accessible labels, roles, descriptions, and announcements
- Bump focus outlines to 3px, target sizes to 44px AAA minimum
- Fix announce()/announce_result() to walk widget tree via parent()
- Add AT-SPI accessibility audit script (tools/a11y-audit.py) that
  checks SC 4.1.2, 1.1.1, 1.3.1, 2.1.1, 2.5.5, 2.5.8, 2.4.8,
  2.4.9, 2.4.10, 2.1.3 with JSON report output for CI
- Clean up project structure, archive old plan documents
2026-03-01 12:44:21 +02:00

228 lines
8.0 KiB
Rust

use adw::prelude::*;
use std::rc::Rc;
use crate::core::database::{AppImageRecord, Database};
use crate::core::fuse::FuseStatus;
use crate::core::integrator;
use crate::core::wayland::WaylandStatus;
use crate::i18n::{i18n, i18n_f};
use super::widgets;
/// Show a confirmation dialog before integrating an AppImage into the desktop.
/// Returns true if the user confirms, false if cancelled.
pub fn show_integration_dialog(
parent: &impl IsA<gtk::Widget>,
record: &AppImageRecord,
db: &Rc<Database>,
on_complete: impl Fn(bool) + 'static,
) {
let name = record.app_name.as_deref().unwrap_or(&record.filename);
let dialog = adw::AlertDialog::builder()
.heading(&i18n_f("Integrate {name}?", &[("{name}", name)]))
.body(&i18n("This will add the application to your desktop menu."))
.close_response("cancel")
.default_response("integrate")
.build();
dialog.add_response("cancel", &i18n("Cancel"));
dialog.add_response("integrate", &i18n("Integrate"));
dialog.set_response_appearance("integrate", adw::ResponseAppearance::Suggested);
// Build extra content with details
let content = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(12)
.margin_top(6)
.build();
// App identity section
let identity_box = gtk::ListBox::new();
identity_box.add_css_class("boxed-list");
identity_box.set_selection_mode(gtk::SelectionMode::None);
identity_box.update_property(&[
gtk::accessible::Property::Label(&i18n("Application details")),
]);
// Name
let name_row = adw::ActionRow::builder()
.title(&i18n("Application"))
.subtitle(name)
.build();
if let Some(ref icon_path) = record.icon_path {
let path = std::path::Path::new(icon_path);
if path.exists() {
if let Ok(texture) = gtk::gdk::Texture::from_filename(path) {
let image = gtk::Image::builder()
.pixel_size(32)
.build();
image.set_paintable(Some(&texture));
image.update_property(&[gtk::accessible::Property::Label(name)]);
name_row.add_prefix(&image);
}
}
}
identity_box.append(&name_row);
// Version
if let Some(ref version) = record.app_version {
let row = adw::ActionRow::builder()
.title(&i18n("Version"))
.subtitle(version)
.build();
identity_box.append(&row);
}
content.append(&identity_box);
// What will happen section
let actions_box = gtk::ListBox::new();
actions_box.add_css_class("boxed-list");
actions_box.set_selection_mode(gtk::SelectionMode::None);
actions_box.update_property(&[
gtk::accessible::Property::Label(&i18n("Integration actions")),
]);
let desktop_row = adw::ActionRow::builder()
.title(&i18n("Desktop entry"))
.subtitle(&i18n("A .desktop file will be created in ~/.local/share/applications"))
.build();
let check1 = gtk::Image::from_icon_name("emblem-ok-symbolic");
check1.set_valign(gtk::Align::Center);
check1.set_accessible_role(gtk::AccessibleRole::Presentation);
desktop_row.add_prefix(&check1);
actions_box.append(&desktop_row);
let icon_row = adw::ActionRow::builder()
.title(&i18n("Icon"))
.subtitle(&i18n("The app icon will be installed to ~/.local/share/icons"))
.build();
let check2 = gtk::Image::from_icon_name("emblem-ok-symbolic");
check2.set_valign(gtk::Align::Center);
check2.set_accessible_role(gtk::AccessibleRole::Presentation);
icon_row.add_prefix(&check2);
actions_box.append(&icon_row);
content.append(&actions_box);
// Runtime compatibility warnings - now with styled banner
let wayland_status = record
.wayland_status
.as_deref()
.map(WaylandStatus::from_str)
.unwrap_or(WaylandStatus::Unknown);
let fuse_status = record
.fuse_status
.as_deref()
.map(FuseStatus::from_str)
.unwrap_or(FuseStatus::MissingLibfuse2);
let mut warnings: Vec<(String, String, String)> = Vec::new();
if wayland_status == WaylandStatus::X11Only {
warnings.push((
i18n("X11 only"),
i18n("This app does not support Wayland and will run through XWayland"),
"X11".to_string(),
));
} else if wayland_status == WaylandStatus::XWayland {
warnings.push((
i18n("XWayland"),
i18n("This app runs through the XWayland compatibility layer"),
"XWayland".to_string(),
));
}
if !fuse_status.is_functional() {
let fuse_msg = match fuse_status {
FuseStatus::Fuse3Only => i18n("Only FUSE3 is installed - libfuse2 may be needed"),
FuseStatus::NoFusermount => i18n("fusermount not found - AppImage mount may fail"),
FuseStatus::NoDevFuse => i18n("/dev/fuse not available - AppImage mount will fail"),
FuseStatus::MissingLibfuse2 => i18n("libfuse2 not installed - fallback extraction will be used"),
_ => i18n("FUSE issue detected"),
};
warnings.push(("FUSE".to_string(), fuse_msg, fuse_status.label().to_string()));
}
if !warnings.is_empty() {
// Styled warning banner
let warning_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.build();
warning_box.add_css_class("compat-warning-banner");
let warning_header = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
let warning_icon = gtk::Image::from_icon_name("dialog-warning-symbolic");
warning_icon.set_pixel_size(16);
warning_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
warning_header.append(&warning_icon);
let warning_title = gtk::Label::builder()
.label(&i18n("Compatibility Notes"))
.css_classes(["title-4"])
.halign(gtk::Align::Start)
.build();
warning_header.append(&warning_title);
warning_box.append(&warning_header);
let compat_list = gtk::ListBox::new();
compat_list.add_css_class("boxed-list");
compat_list.set_selection_mode(gtk::SelectionMode::None);
compat_list.update_property(&[
gtk::accessible::Property::Label(&i18n("Compatibility warnings")),
]);
for (title, subtitle, badge_text) in &warnings {
let row = adw::ActionRow::builder()
.title(title.as_str())
.subtitle(subtitle.as_str())
.build();
let badge = widgets::status_badge(badge_text, "warning");
badge.set_valign(gtk::Align::Center);
row.add_suffix(&badge);
compat_list.append(&row);
}
warning_box.append(&compat_list);
content.append(&warning_box);
}
dialog.set_extra_child(Some(&content));
let record_clone = record.clone();
let db_ref = db.clone();
let record_id = record.id;
dialog.connect_response(None, move |_dialog, response| {
if response == "integrate" {
match integrator::integrate_tracked(&record_clone, &db_ref) {
Ok(result) => {
if let Some(ref icon_path) = result.icon_install_path {
log::info!("Icon installed to: {}", icon_path.display());
}
db_ref
.set_integrated(
record_id,
true,
Some(&result.desktop_file_path.to_string_lossy()),
)
.ok();
on_complete(true);
}
Err(e) => {
log::error!("Integration failed: {}", e);
on_complete(false);
}
}
} else {
on_complete(false);
}
});
dialog.present(Some(parent));
}