From b78f1cd7c4c0744eb4e7c8810e110479a4bbdc0e Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 13:13:39 +0200 Subject: [PATCH] Add What's New dialog, accessibility labels, focus management Add What's New dialog accessible from hamburger menu showing version changelog. Add accessible property labels to step indicator for screen reader support with current step/total announcements. Add focus management on step transitions - focus moves to first interactive element when navigating to a new step. --- pixstrip-gtk/src/app.rs | 90 ++++++++++++++++++++++++++++++ pixstrip-gtk/src/step_indicator.rs | 16 ++++++ 2 files changed, 106 insertions(+) diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index 708107a..70db4bd 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -349,6 +349,7 @@ fn build_menu() -> gtk::gio::Menu { menu.append(Some("Settings"), Some("win.show-settings")); menu.append(Some("History"), Some("win.show-history")); menu.append(Some("Keyboard Shortcuts"), Some("win.show-shortcuts")); + menu.append(Some("What's New"), Some("win.show-whats-new")); menu } @@ -500,6 +501,16 @@ fn setup_window_actions(window: &adw::ApplicationWindow, ui: &WizardUi) { action_group.add_action(&action); } + // What's New action + { + let window = window.clone(); + let action = gtk::gio::SimpleAction::new("show-whats-new", None); + action.connect_activate(move |_, _| { + show_whats_new_dialog(&window); + }); + action_group.add_action(&action); + } + // Connect button clicks ui.back_button.connect_clicked({ let action_group = action_group.clone(); @@ -541,6 +552,13 @@ fn navigate_to_step(ui: &WizardUi, target: usize) { if target < ui.pages.len() { ui.nav_view.replace(&ui.pages[..=target]); ui.title.set_subtitle(&ui.pages[target].title()); + + // Focus management - move focus to first interactive element on new step + // Use idle callback to let the page fully render first + let page = ui.pages[target].clone(); + glib::idle_add_local_once(move || { + page.child_focus(gtk::DirectionType::TabForward); + }); } // Update dynamic content on certain steps @@ -857,6 +875,78 @@ fn show_history_dialog(window: &adw::ApplicationWindow) { dialog.present(Some(window)); } +fn show_whats_new_dialog(window: &adw::ApplicationWindow) { + let dialog = adw::Dialog::builder() + .title("What's New") + .content_width(450) + .content_height(350) + .build(); + + let toolbar_view = adw::ToolbarView::new(); + let header = adw::HeaderBar::new(); + toolbar_view.add_top_bar(&header); + + let scrolled = gtk::ScrolledWindow::builder() + .hscrollbar_policy(gtk::PolicyType::Never) + .vexpand(true) + .build(); + + let content = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(12) + .margin_top(12) + .margin_bottom(12) + .margin_start(24) + .margin_end(24) + .build(); + + let version_label = gtk::Label::builder() + .label("Pixstrip 0.1.0") + .css_classes(["title-1"]) + .halign(gtk::Align::Start) + .build(); + + let subtitle_label = gtk::Label::builder() + .label("First release") + .css_classes(["dim-label"]) + .halign(gtk::Align::Start) + .build(); + + content.append(&version_label); + content.append(&subtitle_label); + + let changes_group = adw::PreferencesGroup::builder() + .title("Changes in this version") + .build(); + + let features = [ + ("Wizard workflow", "Step-by-step batch image processing"), + ("Resize", "Width/height, social media presets, fit-in-box"), + ("Convert", "JPEG, PNG, WebP, AVIF, GIF, TIFF conversion"), + ("Compress", "Quality presets with per-format controls"), + ("Metadata", "Strip all, privacy mode, photographer mode"), + ("Watermark", "Text and image watermarks with positioning"), + ("Rename", "Prefix, suffix, counter, template engine"), + ("Adjustments", "Brightness, contrast, crop, trim, effects"), + ]; + + for (title, subtitle) in &features { + let row = adw::ActionRow::builder() + .title(*title) + .subtitle(*subtitle) + .build(); + row.add_prefix(>k::Image::from_icon_name("emblem-ok-symbolic")); + changes_group.add(&row); + } + + content.append(&changes_group); + + scrolled.set_child(Some(&content)); + toolbar_view.set_content(Some(&scrolled)); + dialog.set_child(Some(&toolbar_view)); + dialog.present(Some(window)); +} + fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) { let files = ui.state.loaded_files.borrow().clone(); if files.is_empty() { diff --git a/pixstrip-gtk/src/step_indicator.rs b/pixstrip-gtk/src/step_indicator.rs index 3409989..b2d4bf5 100644 --- a/pixstrip-gtk/src/step_indicator.rs +++ b/pixstrip-gtk/src/step_indicator.rs @@ -27,6 +27,10 @@ impl StepIndicator { .margin_end(12) .build(); + container.update_property(&[ + gtk::accessible::Property::Label("Wizard step indicator"), + ]); + let mut dots = Vec::new(); for (i, name) in step_names.iter().enumerate() { @@ -93,14 +97,26 @@ impl StepIndicator { pub fn set_current(&self, index: usize) { let dots = self.dots.borrow(); + let total = dots.len(); for (i, dot) in dots.iter().enumerate() { if i == index { dot.icon.set_icon_name(Some("radio-checked-symbolic")); dot.button.set_sensitive(true); dot.label.add_css_class("accent"); + // Update accessible description for screen readers + dot.button.update_property(&[ + gtk::accessible::Property::Label( + &format!("Step {} of {}: {} (current)", i + 1, total, dot.label.label()) + ), + ]); } else if dot.icon.icon_name().as_deref() != Some("emblem-ok-symbolic") { dot.icon.set_icon_name(Some("radio-symbolic")); dot.label.remove_css_class("accent"); + dot.button.update_property(&[ + gtk::accessible::Property::Label( + &format!("Step {} of {}: {}", i + 1, total, dot.label.label()) + ), + ]); } } }