feat: add visual feedback for printing
Print button is disabled while a job is being sent and a spinner shows in the footer. On success a transient 'Sent to printer' confirmation is displayed for a few seconds; on failure the error message stays in the footer instead of replacing the document view.
This commit is contained in:
parent
fe1d7c0ebb
commit
481f1ed702
1 changed files with 73 additions and 52 deletions
125
src/main.rs
125
src/main.rs
|
|
@ -16,6 +16,7 @@ use std::ffi::OsStr;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(
|
#[command(
|
||||||
|
|
@ -115,9 +116,17 @@ enum Message {
|
||||||
PrintTask {
|
PrintTask {
|
||||||
result: Result<(), String>,
|
result: Result<(), String>,
|
||||||
},
|
},
|
||||||
|
ClearPrintStatus,
|
||||||
KeyChar(Key),
|
KeyChar(Key),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
enum PrintStatus {
|
||||||
|
Sending,
|
||||||
|
Sent,
|
||||||
|
Failed(String),
|
||||||
|
}
|
||||||
|
|
||||||
struct NoctuaApp {
|
struct NoctuaApp {
|
||||||
core: Core,
|
core: Core,
|
||||||
documents: Vec<Document>,
|
documents: Vec<Document>,
|
||||||
|
|
@ -125,6 +134,7 @@ struct NoctuaApp {
|
||||||
zoom: f32,
|
zoom: f32,
|
||||||
modifiers: keyboard::Modifiers,
|
modifiers: keyboard::Modifiers,
|
||||||
printing: bool,
|
printing: bool,
|
||||||
|
print_status: Option<PrintStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> cosmic::iced::Result {
|
fn main() -> cosmic::iced::Result {
|
||||||
|
|
@ -188,6 +198,7 @@ impl Application for NoctuaApp {
|
||||||
zoom: 1.0,
|
zoom: 1.0,
|
||||||
modifiers: keyboard::Modifiers::default(),
|
modifiers: keyboard::Modifiers::default(),
|
||||||
printing: false,
|
printing: false,
|
||||||
|
print_status: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
app.refresh_title();
|
app.refresh_title();
|
||||||
|
|
@ -284,71 +295,35 @@ impl Application for NoctuaApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Print handling
|
// Print handling
|
||||||
Message::Print => {
|
Message::Print => return self.start_print(),
|
||||||
if self.printing {
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
|
|
||||||
let document = match self.current_document() {
|
|
||||||
Some(doc) => doc,
|
|
||||||
None => return Task::none(),
|
|
||||||
};
|
|
||||||
let path = document.path.clone();
|
|
||||||
let kind = document.kind;
|
|
||||||
|
|
||||||
self.printing = true;
|
|
||||||
|
|
||||||
return Task::perform(
|
|
||||||
tokio::task::spawn_blocking(move || print_document_sync(&path, kind)),
|
|
||||||
|result| {
|
|
||||||
let inner = result
|
|
||||||
.map_err(|e| format!("Spawn error: {e}"))
|
|
||||||
.and_then(|r| r);
|
|
||||||
cosmic::Action::App(Message::PrintTask { result: inner })
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Message::KeyChar(key) => {
|
Message::KeyChar(key) => {
|
||||||
// Handle Ctrl+P shortcut for printing
|
// Handle Ctrl+P shortcut for printing
|
||||||
if self.modifiers.control() && key == Key::Character("p".into()) {
|
if self.modifiers.control() && key == Key::Character("p".into()) {
|
||||||
if self.printing {
|
return self.start_print();
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
|
|
||||||
let document = match self.current_document() {
|
|
||||||
Some(doc) => doc,
|
|
||||||
None => return Task::none(),
|
|
||||||
};
|
|
||||||
let path = document.path.clone();
|
|
||||||
let kind = document.kind;
|
|
||||||
|
|
||||||
self.printing = true;
|
|
||||||
|
|
||||||
return Task::perform(
|
|
||||||
tokio::task::spawn_blocking(move || print_document_sync(&path, kind)),
|
|
||||||
|result| {
|
|
||||||
let inner = result
|
|
||||||
.map_err(|e| format!("Spawn error: {e}"))
|
|
||||||
.and_then(|r| r);
|
|
||||||
cosmic::Action::App(Message::PrintTask { result: inner })
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::PrintTask { result } => {
|
Message::PrintTask { result } => {
|
||||||
self.printing = false;
|
self.printing = false;
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
// Print succeeded – no visual feedback needed,
|
self.print_status = Some(PrintStatus::Sent);
|
||||||
// the document was sent to the printer.
|
return Task::perform(
|
||||||
|
tokio::task::spawn_blocking(|| {
|
||||||
|
std::thread::sleep(Duration::from_secs(4));
|
||||||
|
}),
|
||||||
|
|_| cosmic::Action::App(Message::ClearPrintStatus),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if let Some(document) = self.current_document_mut() {
|
self.print_status = Some(PrintStatus::Failed(err));
|
||||||
document.error = Some(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Message::ClearPrintStatus => {
|
||||||
|
if matches!(self.print_status, Some(PrintStatus::Sent)) {
|
||||||
|
self.print_status = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.refresh_title();
|
self.refresh_title();
|
||||||
|
|
@ -447,8 +422,9 @@ impl Application for NoctuaApp {
|
||||||
.push(widget::space::horizontal().width(Length::Fill))
|
.push(widget::space::horizontal().width(Length::Fill))
|
||||||
.push(
|
.push(
|
||||||
widget::button::standard("Print")
|
widget::button::standard("Print")
|
||||||
.on_press(Message::Print),
|
.on_press_maybe((!self.printing).then_some(Message::Print)),
|
||||||
)
|
)
|
||||||
|
.push(print_status_view(self.print_status.as_ref()))
|
||||||
.push(widget::text::body(page_label))
|
.push(widget::text::body(page_label))
|
||||||
.push(
|
.push(
|
||||||
widget::button::standard("Prev page")
|
widget::button::standard("Prev page")
|
||||||
|
|
@ -511,6 +487,32 @@ impl NoctuaApp {
|
||||||
self.core.set_header_title(title);
|
self.core.set_header_title(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_print(&mut self) -> Task<Message> {
|
||||||
|
if self.printing {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
|
||||||
|
let document = match self.current_document() {
|
||||||
|
Some(doc) => doc,
|
||||||
|
None => return Task::none(),
|
||||||
|
};
|
||||||
|
let path = document.path.clone();
|
||||||
|
let kind = document.kind;
|
||||||
|
|
||||||
|
self.printing = true;
|
||||||
|
self.print_status = Some(PrintStatus::Sending);
|
||||||
|
|
||||||
|
Task::perform(
|
||||||
|
tokio::task::spawn_blocking(move || print_document_sync(&path, kind)),
|
||||||
|
|result| {
|
||||||
|
let inner = result
|
||||||
|
.map_err(|e| format!("Spawn error: {e}"))
|
||||||
|
.and_then(|r| r);
|
||||||
|
cosmic::Action::App(Message::PrintTask { result: inner })
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_current_rendered(&mut self) -> Task<Message> {
|
fn ensure_current_rendered(&mut self) -> Task<Message> {
|
||||||
let render_task = {
|
let render_task = {
|
||||||
let Some(document) = self.current_document_mut() else {
|
let Some(document) = self.current_document_mut() else {
|
||||||
|
|
@ -634,6 +636,25 @@ fn empty_view<'a>(message: impl Into<String>) -> Element<'a, Message> {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_status_view(status: Option<&PrintStatus>) -> Element<'_, Message> {
|
||||||
|
match status {
|
||||||
|
Some(PrintStatus::Sending) => widget::progress_bar::indeterminate_circular()
|
||||||
|
.size(16.0)
|
||||||
|
.bar_height(2.0)
|
||||||
|
.into(),
|
||||||
|
Some(PrintStatus::Sent) => widget::text::body("Sent to printer")
|
||||||
|
.class(theme::Text::Accent)
|
||||||
|
.into(),
|
||||||
|
Some(PrintStatus::Failed(err)) => widget::text::body(format!("Print failed: {err}"))
|
||||||
|
.class(theme::Text::Custom(|t| cosmic::iced_widget::text::Style {
|
||||||
|
color: Some(t.cosmic().destructive_color().into()),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
.into(),
|
||||||
|
None => widget::space::horizontal().width(Length::Fixed(0.0)).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn scroll_delta_y(delta: mouse::ScrollDelta) -> Option<f32> {
|
fn scroll_delta_y(delta: mouse::ScrollDelta) -> Option<f32> {
|
||||||
let y = match delta {
|
let y = match delta {
|
||||||
mouse::ScrollDelta::Lines { y, .. } => y,
|
mouse::ScrollDelta::Lines { y, .. } => y,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue