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.
146 lines
4.4 KiB
Rust
146 lines
4.4 KiB
Rust
use gtk::prelude::*;
|
|
use gtk::glib::prelude::ToVariant;
|
|
use std::cell::RefCell;
|
|
|
|
#[derive(Clone)]
|
|
pub struct StepIndicator {
|
|
container: gtk::Box,
|
|
dots: RefCell<Vec<StepDot>>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct StepDot {
|
|
button: gtk::Button,
|
|
icon: gtk::Image,
|
|
label: gtk::Label,
|
|
}
|
|
|
|
impl StepIndicator {
|
|
pub fn new(step_names: &[String]) -> Self {
|
|
let container = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Horizontal)
|
|
.halign(gtk::Align::Center)
|
|
.spacing(0)
|
|
.margin_top(8)
|
|
.margin_bottom(8)
|
|
.margin_start(12)
|
|
.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() {
|
|
if i > 0 {
|
|
// Connector line between dots
|
|
let line = gtk::Separator::builder()
|
|
.orientation(gtk::Orientation::Horizontal)
|
|
.hexpand(false)
|
|
.valign(gtk::Align::Center)
|
|
.build();
|
|
line.set_size_request(24, -1);
|
|
container.append(&line);
|
|
}
|
|
|
|
let dot_box = gtk::Box::builder()
|
|
.orientation(gtk::Orientation::Vertical)
|
|
.spacing(2)
|
|
.halign(gtk::Align::Center)
|
|
.build();
|
|
|
|
let icon = gtk::Image::builder()
|
|
.icon_name("radio-symbolic")
|
|
.pixel_size(16)
|
|
.build();
|
|
|
|
let button = gtk::Button::builder()
|
|
.child(&icon)
|
|
.has_frame(false)
|
|
.tooltip_text(format!("Step {}: {} (Alt+{})", i + 1, name, i + 1))
|
|
.sensitive(false)
|
|
.action_name("win.goto-step")
|
|
.action_target(&(i as i32 + 1).to_variant())
|
|
.build();
|
|
button.add_css_class("circular");
|
|
|
|
let label = gtk::Label::builder()
|
|
.label(name)
|
|
.css_classes(["caption"])
|
|
.build();
|
|
|
|
dot_box.append(&button);
|
|
dot_box.append(&label);
|
|
container.append(&dot_box);
|
|
|
|
dots.push(StepDot {
|
|
button,
|
|
icon,
|
|
label,
|
|
});
|
|
}
|
|
|
|
// First step starts as current
|
|
if let Some(first) = dots.first() {
|
|
first.icon.set_icon_name(Some("radio-checked-symbolic"));
|
|
first.button.set_sensitive(true);
|
|
first.label.add_css_class("accent");
|
|
}
|
|
|
|
Self {
|
|
container,
|
|
dots: RefCell::new(dots),
|
|
}
|
|
}
|
|
|
|
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())
|
|
),
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_completed(&self, index: usize) {
|
|
let dots = self.dots.borrow();
|
|
if let Some(dot) = dots.get(index) {
|
|
dot.icon.set_icon_name(Some("emblem-ok-symbolic"));
|
|
dot.button.set_sensitive(true);
|
|
dot.label.remove_css_class("accent");
|
|
}
|
|
}
|
|
|
|
pub fn widget(&self) -> >k::Box {
|
|
&self.container
|
|
}
|
|
}
|
|
|
|
// Allow appending the step indicator to containers
|
|
impl std::ops::Deref for StepIndicator {
|
|
type Target = gtk::Box;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.container
|
|
}
|
|
}
|