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.
This commit is contained in:
2026-03-06 13:13:39 +02:00
parent ea8444f039
commit b78f1cd7c4
2 changed files with 106 additions and 0 deletions

View File

@@ -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(&gtk::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() {

View File

@@ -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())
),
]);
}
}
}