Add GTK app shell with wizard navigation, step indicator, and actions
7-step wizard flow (Workflow, Images, Resize, Convert, Compress, Metadata, Output) with AdwNavigationView, step indicator dots, Back/Next buttons, keyboard shortcuts (Alt+arrows, Alt+1-9), and hamburger menu with Settings and History placeholders.
This commit is contained in:
282
pixstrip-gtk/src/app.rs
Normal file
282
pixstrip-gtk/src/app.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use adw::prelude::*;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::step_indicator::StepIndicator;
|
||||
use crate::wizard::WizardState;
|
||||
|
||||
pub const APP_ID: &str = "live.lashman.Pixstrip";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WizardUi {
|
||||
nav_view: adw::NavigationView,
|
||||
step_indicator: StepIndicator,
|
||||
back_button: gtk::Button,
|
||||
next_button: gtk::Button,
|
||||
title: adw::WindowTitle,
|
||||
pages: Vec<adw::NavigationPage>,
|
||||
state: Rc<RefCell<WizardState>>,
|
||||
}
|
||||
|
||||
pub fn build_app() -> adw::Application {
|
||||
let app = adw::Application::builder()
|
||||
.application_id(APP_ID)
|
||||
.build();
|
||||
|
||||
app.connect_activate(build_ui);
|
||||
setup_shortcuts(&app);
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
fn setup_shortcuts(app: &adw::Application) {
|
||||
app.set_accels_for_action("win.next-step", &["<Alt>Right"]);
|
||||
app.set_accels_for_action("win.prev-step", &["<Alt>Left"]);
|
||||
app.set_accels_for_action("win.process", &["<Control>Return"]);
|
||||
for i in 1..=9 {
|
||||
app.set_accels_for_action(
|
||||
&format!("win.goto-step({})", i),
|
||||
&[&format!("<Alt>{}", i)],
|
||||
);
|
||||
}
|
||||
app.set_accels_for_action("win.add-files", &["<Control>o"]);
|
||||
}
|
||||
|
||||
fn build_ui(app: &adw::Application) {
|
||||
let state = Rc::new(RefCell::new(WizardState::new()));
|
||||
|
||||
// Header bar
|
||||
let header = adw::HeaderBar::new();
|
||||
let title = adw::WindowTitle::new("Pixstrip", "Batch Image Processor");
|
||||
header.set_title_widget(Some(&title));
|
||||
|
||||
// Hamburger menu
|
||||
let menu = build_menu();
|
||||
let menu_button = gtk::MenuButton::builder()
|
||||
.icon_name("open-menu-symbolic")
|
||||
.menu_model(&menu)
|
||||
.primary(true)
|
||||
.tooltip_text("Main Menu")
|
||||
.build();
|
||||
header.pack_end(&menu_button);
|
||||
|
||||
// Step indicator
|
||||
let step_indicator = StepIndicator::new(&state.borrow().step_names());
|
||||
|
||||
// Navigation view for wizard content
|
||||
let nav_view = adw::NavigationView::new();
|
||||
nav_view.set_vexpand(true);
|
||||
|
||||
// Build wizard pages
|
||||
let pages = crate::wizard::build_wizard_pages();
|
||||
for page in &pages {
|
||||
nav_view.add(page);
|
||||
}
|
||||
|
||||
// Bottom action bar
|
||||
let back_button = gtk::Button::builder()
|
||||
.label("Back")
|
||||
.tooltip_text("Go to previous step (Alt+Left)")
|
||||
.build();
|
||||
back_button.add_css_class("flat");
|
||||
|
||||
let next_button = gtk::Button::builder()
|
||||
.label("Next")
|
||||
.tooltip_text("Go to next step (Alt+Right)")
|
||||
.build();
|
||||
next_button.add_css_class("suggested-action");
|
||||
|
||||
let bottom_box = gtk::CenterBox::new();
|
||||
bottom_box.set_start_widget(Some(&back_button));
|
||||
bottom_box.set_end_widget(Some(&next_button));
|
||||
bottom_box.set_margin_start(12);
|
||||
bottom_box.set_margin_end(12);
|
||||
bottom_box.set_margin_top(6);
|
||||
bottom_box.set_margin_bottom(6);
|
||||
|
||||
let bottom_bar = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
let separator = gtk::Separator::new(gtk::Orientation::Horizontal);
|
||||
bottom_bar.append(&separator);
|
||||
bottom_bar.append(&bottom_box);
|
||||
|
||||
// Main content layout
|
||||
let content_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
content_box.append(step_indicator.widget());
|
||||
content_box.append(&nav_view);
|
||||
|
||||
// Toolbar view with header and bottom bar
|
||||
let toolbar_view = adw::ToolbarView::new();
|
||||
toolbar_view.add_top_bar(&header);
|
||||
toolbar_view.set_content(Some(&content_box));
|
||||
toolbar_view.add_bottom_bar(&bottom_bar);
|
||||
|
||||
// Window
|
||||
let window = adw::ApplicationWindow::builder()
|
||||
.application(app)
|
||||
.default_width(900)
|
||||
.default_height(700)
|
||||
.content(&toolbar_view)
|
||||
.title("Pixstrip")
|
||||
.build();
|
||||
|
||||
let ui = WizardUi {
|
||||
nav_view,
|
||||
step_indicator,
|
||||
back_button,
|
||||
next_button,
|
||||
title,
|
||||
pages,
|
||||
state,
|
||||
};
|
||||
|
||||
setup_window_actions(&window, &ui);
|
||||
update_nav_buttons(&ui.state.borrow(), &ui.back_button, &ui.next_button);
|
||||
ui.step_indicator.set_current(0);
|
||||
|
||||
window.present();
|
||||
}
|
||||
|
||||
fn build_menu() -> gtk::gio::Menu {
|
||||
let menu = gtk::gio::Menu::new();
|
||||
menu.append(Some("Settings"), Some("win.show-settings"));
|
||||
menu.append(Some("History"), Some("win.show-history"));
|
||||
menu
|
||||
}
|
||||
|
||||
fn setup_window_actions(window: &adw::ApplicationWindow, ui: &WizardUi) {
|
||||
let action_group = gtk::gio::SimpleActionGroup::new();
|
||||
|
||||
// Next step action
|
||||
{
|
||||
let ui = ui.clone();
|
||||
let action = gtk::gio::SimpleAction::new("next-step", None);
|
||||
action.connect_activate(move |_, _| {
|
||||
let mut s = ui.state.borrow_mut();
|
||||
if s.can_go_next() {
|
||||
s.go_next();
|
||||
let idx = s.current_step;
|
||||
drop(s);
|
||||
navigate_to_step(&ui, idx);
|
||||
}
|
||||
});
|
||||
action_group.add_action(&action);
|
||||
}
|
||||
|
||||
// Previous step action
|
||||
{
|
||||
let ui = ui.clone();
|
||||
let action = gtk::gio::SimpleAction::new("prev-step", None);
|
||||
action.connect_activate(move |_, _| {
|
||||
let mut s = ui.state.borrow_mut();
|
||||
if s.can_go_back() {
|
||||
s.go_back();
|
||||
let idx = s.current_step;
|
||||
drop(s);
|
||||
navigate_to_step(&ui, idx);
|
||||
}
|
||||
});
|
||||
action_group.add_action(&action);
|
||||
}
|
||||
|
||||
// Go to specific step action
|
||||
{
|
||||
let ui = ui.clone();
|
||||
let action = gtk::gio::SimpleAction::new(
|
||||
"goto-step",
|
||||
Some(&i32::static_variant_type()),
|
||||
);
|
||||
action.connect_activate(move |_, param| {
|
||||
if let Some(step) = param.and_then(|p| p.get::<i32>()) {
|
||||
let target = (step - 1) as usize;
|
||||
let s = ui.state.borrow();
|
||||
if target < s.total_steps && s.visited[target] {
|
||||
drop(s);
|
||||
ui.state.borrow_mut().current_step = target;
|
||||
navigate_to_step(&ui, target);
|
||||
}
|
||||
}
|
||||
});
|
||||
action_group.add_action(&action);
|
||||
}
|
||||
|
||||
// Process action (placeholder)
|
||||
{
|
||||
let action = gtk::gio::SimpleAction::new("process", None);
|
||||
action.connect_activate(move |_, _| {});
|
||||
action_group.add_action(&action);
|
||||
}
|
||||
|
||||
// Add files action (placeholder)
|
||||
{
|
||||
let action = gtk::gio::SimpleAction::new("add-files", None);
|
||||
action.connect_activate(move |_, _| {});
|
||||
action_group.add_action(&action);
|
||||
}
|
||||
|
||||
// Settings action (placeholder)
|
||||
{
|
||||
let action = gtk::gio::SimpleAction::new("show-settings", None);
|
||||
action.connect_activate(move |_, _| {});
|
||||
action_group.add_action(&action);
|
||||
}
|
||||
|
||||
// History action (placeholder)
|
||||
{
|
||||
let action = gtk::gio::SimpleAction::new("show-history", None);
|
||||
action.connect_activate(move |_, _| {});
|
||||
action_group.add_action(&action);
|
||||
}
|
||||
|
||||
// Connect button clicks
|
||||
ui.back_button.connect_clicked({
|
||||
let action_group = action_group.clone();
|
||||
move |_| {
|
||||
ActionGroupExt::activate_action(&action_group, "prev-step", None);
|
||||
}
|
||||
});
|
||||
|
||||
ui.next_button.connect_clicked({
|
||||
let action_group = action_group.clone();
|
||||
move |_| {
|
||||
ActionGroupExt::activate_action(&action_group, "next-step", None);
|
||||
}
|
||||
});
|
||||
|
||||
window.insert_action_group("win", Some(&action_group));
|
||||
}
|
||||
|
||||
fn navigate_to_step(ui: &WizardUi, target: usize) {
|
||||
let s = ui.state.borrow();
|
||||
|
||||
// Update step indicator
|
||||
ui.step_indicator.set_current(target);
|
||||
for (i, visited) in s.visited.iter().enumerate() {
|
||||
if *visited && i != target {
|
||||
ui.step_indicator.set_completed(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate - replace entire stack up to target
|
||||
if target < ui.pages.len() {
|
||||
ui.nav_view.replace(&ui.pages[..=target]);
|
||||
ui.title.set_subtitle(&ui.pages[target].title());
|
||||
}
|
||||
|
||||
update_nav_buttons(&s, &ui.back_button, &ui.next_button);
|
||||
}
|
||||
|
||||
fn update_nav_buttons(state: &WizardState, back_button: >k::Button, next_button: >k::Button) {
|
||||
back_button.set_sensitive(state.can_go_back());
|
||||
back_button.set_visible(state.current_step > 0);
|
||||
|
||||
if state.is_last_step() {
|
||||
next_button.set_label("Process");
|
||||
next_button.remove_css_class("suggested-action");
|
||||
next_button.add_css_class("suggested-action");
|
||||
next_button.set_tooltip_text(Some("Start processing (Ctrl+Enter)"));
|
||||
} else {
|
||||
next_button.set_label("Next");
|
||||
next_button.add_css_class("suggested-action");
|
||||
next_button.set_tooltip_text(Some("Go to next step (Alt+Right)"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user