Fix 26 bugs, edge cases, and consistency issues from fifth audit pass
Critical: undo toast now trashes only batch output files (not entire dir), JPEG scanline write errors propagated, selective metadata write result returned. High: zero-dimension guards in ResizeConfig/fit_within, negative aspect ratio rejection, FM integration toggle infinite recursion guard, saturating counter arithmetic in executor. Medium: PNG compression level passed to oxipng, pct mode updates job_config, external file loading updates step indicator, CLI undo removes history entries, watch config write failures reported, fast-copy path reads image dimensions for rename templates, discovery excludes unprocessable formats (heic/svg/ico/jxl), CLI warns on invalid algorithm/overwrite values, resolve_collision trailing dot fix, generation guards on all preview threads to cancel stale results, default DPI aligned to 0, watermark text width uses char count not byte length. Low: binary path escaped in Nautilus extension, file dialog filter aligned with discovery, reset_wizard clears preset_mode and output_dir.
This commit is contained in:
@@ -5,7 +5,10 @@ use std::cell::RefCell;
|
||||
#[derive(Clone)]
|
||||
pub struct StepIndicator {
|
||||
container: gtk::Box,
|
||||
grid: gtk::Grid,
|
||||
dots: RefCell<Vec<StepDot>>,
|
||||
/// Maps visual index -> actual step index
|
||||
step_map: RefCell<Vec<usize>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -27,65 +30,23 @@ impl StepIndicator {
|
||||
.margin_end(12)
|
||||
.build();
|
||||
|
||||
// Prevent negative allocation warnings when window is narrow
|
||||
container.set_overflow(gtk::Overflow::Hidden);
|
||||
|
||||
container.update_property(&[
|
||||
gtk::accessible::Property::Label("Wizard step indicator"),
|
||||
]);
|
||||
|
||||
let mut dots = Vec::new();
|
||||
let grid = gtk::Grid::builder()
|
||||
.column_homogeneous(false)
|
||||
.row_spacing(2)
|
||||
.column_spacing(0)
|
||||
.hexpand(false)
|
||||
.build();
|
||||
|
||||
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(12, -1);
|
||||
container.append(&line);
|
||||
}
|
||||
let indices: Vec<usize> = (0..step_names.len()).collect();
|
||||
let dots = Self::build_dots(&grid, step_names, &indices);
|
||||
|
||||
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"])
|
||||
.ellipsize(gtk::pango::EllipsizeMode::End)
|
||||
.max_width_chars(8)
|
||||
.build();
|
||||
|
||||
dot_box.append(&button);
|
||||
dot_box.append(&label);
|
||||
container.append(&dot_box);
|
||||
|
||||
dots.push(StepDot {
|
||||
button,
|
||||
icon,
|
||||
label,
|
||||
});
|
||||
}
|
||||
container.append(&grid);
|
||||
|
||||
// First step starts as current
|
||||
if let Some(first) = dots.first() {
|
||||
@@ -96,22 +57,95 @@ impl StepIndicator {
|
||||
|
||||
Self {
|
||||
container,
|
||||
grid,
|
||||
dots: RefCell::new(dots),
|
||||
step_map: RefCell::new(indices),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_current(&self, index: usize) {
|
||||
fn build_dots(grid: >k::Grid, names: &[String], step_indices: &[usize]) -> Vec<StepDot> {
|
||||
let mut dots = Vec::new();
|
||||
|
||||
for (visual_i, (name, &actual_i)) in names.iter().zip(step_indices.iter()).enumerate() {
|
||||
let col = (visual_i * 2) as i32;
|
||||
|
||||
if visual_i > 0 {
|
||||
let line = gtk::Separator::builder()
|
||||
.orientation(gtk::Orientation::Horizontal)
|
||||
.hexpand(false)
|
||||
.valign(gtk::Align::Center)
|
||||
.build();
|
||||
line.set_size_request(12, -1);
|
||||
grid.attach(&line, col - 1, 0, 1, 1);
|
||||
}
|
||||
|
||||
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+{})", visual_i + 1, name, actual_i + 1))
|
||||
.sensitive(false)
|
||||
.action_name("win.goto-step")
|
||||
.action_target(&(actual_i as i32 + 1).to_variant())
|
||||
.halign(gtk::Align::Center)
|
||||
.build();
|
||||
button.add_css_class("circular");
|
||||
|
||||
let label = gtk::Label::builder()
|
||||
.label(name)
|
||||
.css_classes(["caption"])
|
||||
.ellipsize(gtk::pango::EllipsizeMode::End)
|
||||
.width_chars(10)
|
||||
.halign(gtk::Align::Center)
|
||||
.build();
|
||||
|
||||
grid.attach(&button, col, 0, 1, 1);
|
||||
grid.attach(&label, col, 1, 1, 1);
|
||||
|
||||
dots.push(StepDot {
|
||||
button,
|
||||
icon,
|
||||
label,
|
||||
});
|
||||
}
|
||||
|
||||
dots
|
||||
}
|
||||
|
||||
/// Rebuild the indicator to show only the given steps.
|
||||
/// `visible_steps` is a list of (actual_step_index, name).
|
||||
pub fn rebuild(&self, visible_steps: &[(usize, String)]) {
|
||||
// Clear the grid
|
||||
while let Some(child) = self.grid.first_child() {
|
||||
self.grid.remove(&child);
|
||||
}
|
||||
|
||||
let names: Vec<String> = visible_steps.iter().map(|(_, n)| n.clone()).collect();
|
||||
let indices: Vec<usize> = visible_steps.iter().map(|(i, _)| *i).collect();
|
||||
|
||||
let dots = Self::build_dots(&self.grid, &names, &indices);
|
||||
*self.dots.borrow_mut() = dots;
|
||||
*self.step_map.borrow_mut() = indices;
|
||||
}
|
||||
|
||||
/// Set the current step by actual step index. Finds the visual position.
|
||||
pub fn set_current(&self, actual_index: usize) {
|
||||
let dots = self.dots.borrow();
|
||||
let map = self.step_map.borrow();
|
||||
let total = dots.len();
|
||||
for (i, dot) in dots.iter().enumerate() {
|
||||
if i == index {
|
||||
for (visual_i, dot) in dots.iter().enumerate() {
|
||||
let is_current = map.get(visual_i) == Some(&actual_index);
|
||||
if is_current {
|
||||
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())
|
||||
&format!("Step {} of {}: {} (current)", visual_i + 1, total, dot.label.label())
|
||||
),
|
||||
]);
|
||||
} else if dot.icon.icon_name().as_deref() != Some("emblem-ok-symbolic") {
|
||||
@@ -119,19 +153,23 @@ impl StepIndicator {
|
||||
dot.label.remove_css_class("accent");
|
||||
dot.button.update_property(&[
|
||||
gtk::accessible::Property::Label(
|
||||
&format!("Step {} of {}: {}", i + 1, total, dot.label.label())
|
||||
&format!("Step {} of {}: {}", visual_i + 1, total, dot.label.label())
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_completed(&self, index: usize) {
|
||||
/// Mark a step as completed by actual step index.
|
||||
pub fn set_completed(&self, actual_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");
|
||||
let map = self.step_map.borrow();
|
||||
if let Some(visual_i) = map.iter().position(|&i| i == actual_index) {
|
||||
if let Some(dot) = dots.get(visual_i) {
|
||||
dot.icon.set_icon_name(Some("emblem-ok-symbolic"));
|
||||
dot.button.set_sensitive(true);
|
||||
dot.label.remove_css_class("accent");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user