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