Improve UX, add popover tour, metadata, and hicolor icons

- Redesign tutorial tour from modal dialogs to popovers pointing at actual UI elements
- Add beginner-friendly improvements: help buttons, tooltips, welcome wizard enhancements
- Add AppStream metainfo with screenshots, branding, categories, keywords, provides
- Update desktop file with GTK category and SingleMainWindow
- Add hicolor icon theme with all sizes (16-512px)
- Fix debounce SourceId panic in rename step
- Various step UI improvements and bug fixes
This commit is contained in:
2026-03-08 14:18:15 +02:00
parent 8d754017fa
commit f3668c45c3
26 changed files with 2292 additions and 473 deletions

View File

@@ -25,6 +25,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
.title("Enable Watermark")
.subtitle("Add text or image watermark to processed images")
.active(cfg.watermark_enabled)
.tooltip_text("Toggle watermark on or off")
.build();
enable_group.add(&enable_row);
outer.append(&enable_group);
@@ -37,6 +38,10 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
.vexpand(true)
.build();
preview_picture.set_can_target(true);
preview_picture.set_focusable(true);
preview_picture.update_property(&[
gtk::accessible::Property::Label("Watermark preview - press Space to cycle images"),
]);
let info_label = gtk::Label::builder()
.label("No images loaded")
@@ -78,6 +83,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
.title("Type")
.subtitle("Choose text or image watermark")
.use_subtitle(true)
.tooltip_text("Choose between text or image/logo overlay")
.build();
type_row.set_model(Some(&gtk::StringList::new(&["Text Watermark", "Image Watermark"])));
type_row.set_list_factory(Some(&super::full_text_list_factory()));
@@ -95,6 +101,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
let text_row = adw::EntryRow::builder()
.title("Watermark Text")
.text(&cfg.watermark_text)
.tooltip_text("The text that appears as a watermark on each image")
.build();
let font_row = adw::ActionRow::builder()
@@ -115,12 +122,16 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
let desc = gtk::pango::FontDescription::from_string(&cfg.watermark_font_family);
font_button.set_font_desc(&desc);
}
font_button.update_property(&[
gtk::accessible::Property::Label("Choose watermark font"),
]);
font_row.add_suffix(&font_button);
let font_size_row = adw::SpinRow::builder()
.title("Font Size")
.subtitle("Size in pixels")
.adjustment(&gtk::Adjustment::new(cfg.watermark_font_size as f64, 8.0, 200.0, 1.0, 10.0, 0.0))
.tooltip_text("Size of watermark text in pixels")
.build();
text_group.add(&text_row);
@@ -144,7 +155,9 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
)
.activatable(true)
.build();
image_path_row.add_prefix(&gtk::Image::from_icon_name("image-x-generic-symbolic"));
let image_prefix_icon = gtk::Image::from_icon_name("image-x-generic-symbolic");
image_prefix_icon.set_accessible_role(gtk::AccessibleRole::Presentation);
image_path_row.add_prefix(&image_prefix_icon);
let choose_image_button = gtk::Button::builder()
.icon_name("document-open-symbolic")
@@ -152,6 +165,9 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
.valign(gtk::Align::Center)
.has_frame(false)
.build();
choose_image_button.update_property(&[
gtk::accessible::Property::Label("Choose logo image"),
]);
image_path_row.add_suffix(&choose_image_button);
image_group.add(&image_path_row);
@@ -287,6 +303,9 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
.rgba(&initial_color)
.valign(gtk::Align::Center)
.build();
color_button.update_property(&[
gtk::accessible::Property::Label("Choose watermark text color"),
]);
color_row.add_suffix(&color_button);
// Opacity slider + reset
@@ -300,12 +319,18 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
opacity_scale.set_hexpand(false);
opacity_scale.set_valign(gtk::Align::Center);
opacity_scale.set_width_request(180);
opacity_scale.update_property(&[
gtk::accessible::Property::Label("Watermark opacity, 0 to 100 percent"),
]);
let opacity_reset = gtk::Button::builder()
.icon_name("edit-undo-symbolic")
.valign(gtk::Align::Center)
.tooltip_text("Reset to 50%")
.has_frame(false)
.build();
opacity_reset.update_property(&[
gtk::accessible::Property::Label("Reset opacity to 50%"),
]);
opacity_reset.set_sensitive((cfg.watermark_opacity - 0.5).abs() > 0.01);
opacity_row.add_suffix(&opacity_scale);
opacity_row.add_suffix(&opacity_reset);
@@ -321,12 +346,18 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
rotation_scale.set_hexpand(false);
rotation_scale.set_valign(gtk::Align::Center);
rotation_scale.set_width_request(180);
rotation_scale.update_property(&[
gtk::accessible::Property::Label("Watermark rotation, -180 to +180 degrees"),
]);
let rotation_reset = gtk::Button::builder()
.icon_name("edit-undo-symbolic")
.valign(gtk::Align::Center)
.tooltip_text("Reset to 0 degrees")
.has_frame(false)
.build();
rotation_reset.update_property(&[
gtk::accessible::Property::Label("Reset rotation to 0 degrees"),
]);
rotation_reset.set_sensitive(cfg.watermark_rotation != 0);
rotation_row.add_suffix(&rotation_scale);
rotation_row.add_suffix(&rotation_reset);
@@ -336,6 +367,7 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
.title("Tiled / Repeated")
.subtitle("Repeat watermark across the entire image")
.active(cfg.watermark_tiled)
.tooltip_text("Repeat the watermark in a grid pattern across the entire image")
.build();
// Margin slider + reset
@@ -349,12 +381,18 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
margin_scale.set_hexpand(false);
margin_scale.set_valign(gtk::Align::Center);
margin_scale.set_width_request(180);
margin_scale.update_property(&[
gtk::accessible::Property::Label("Watermark margin from edges, 0 to 200 pixels"),
]);
let margin_reset = gtk::Button::builder()
.icon_name("edit-undo-symbolic")
.valign(gtk::Align::Center)
.tooltip_text("Reset to 10 px")
.has_frame(false)
.build();
margin_reset.update_property(&[
gtk::accessible::Property::Label("Reset margin to 10 pixels"),
]);
margin_reset.set_sensitive(cfg.watermark_margin != 10);
margin_row.add_suffix(&margin_scale);
margin_row.add_suffix(&margin_reset);
@@ -371,12 +409,18 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
scale_scale.set_hexpand(false);
scale_scale.set_valign(gtk::Align::Center);
scale_scale.set_width_request(180);
scale_scale.update_property(&[
gtk::accessible::Property::Label("Watermark scale, 1 to 100 percent of image"),
]);
let scale_reset = gtk::Button::builder()
.icon_name("edit-undo-symbolic")
.valign(gtk::Align::Center)
.tooltip_text("Reset to 20%")
.has_frame(false)
.build();
scale_reset.update_property(&[
gtk::accessible::Property::Label("Reset scale to 20%"),
]);
scale_reset.set_sensitive((cfg.watermark_scale - 20.0).abs() > 0.5);
scale_row.add_suffix(&scale_scale);
scale_row.add_suffix(&scale_reset);
@@ -573,6 +617,27 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
preview_picture.add_controller(click);
}
// Keyboard support for preview cycling (Space/Enter)
{
let key = gtk::EventControllerKey::new();
let pidx = preview_index.clone();
let files = state.loaded_files.clone();
let up = update_preview.clone();
key.connect_key_pressed(move |_, keyval, _, _| {
if keyval == gtk::gdk::Key::space || keyval == gtk::gdk::Key::Return {
let loaded = files.borrow();
if loaded.len() > 1 {
let next = (pidx.get() + 1) % loaded.len();
pidx.set(next);
up();
}
return gtk::glib::Propagation::Stop;
}
gtk::glib::Propagation::Proceed
});
preview_picture.add_controller(key);
}
// === Wire signals ===
// Enable toggle
@@ -857,12 +922,16 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
.child(&outer)
.build();
// Refresh preview and sensitivity when navigating to this page
// Sync enable toggle, refresh preview and sensitivity when navigating to this page
{
let up = update_preview.clone();
let lf = state.loaded_files.clone();
let ctrl = controls.clone();
let jc = state.job_config.clone();
let er = enable_row.clone();
page.connect_map(move |_| {
let enabled = jc.borrow().watermark_enabled;
er.set_active(enabled);
ctrl.set_sensitive(!lf.borrow().is_empty());
up();
});