fix: move Print button to footer to fix mouse scroll

- Remove menu bar from header_start (it consumed mouse wheel events)
- Add Print button in footer alongside page info
- Remove unused About message and menu code
- Scroll of mouse wheel now works correctly for page navigation
This commit is contained in:
Lionel DARNIS 2026-07-10 11:55:10 +02:00
parent 03fd8306a6
commit 2bc2abf070
2 changed files with 5 additions and 273 deletions

View file

@ -10,8 +10,6 @@ use cosmic::iced::{
keyboard::{self, key::Named, Key},
mouse, Alignment, ContentFit, Event, Length, Limits, Subscription,
};
use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::Item as MenuItem;
use cosmic::{executor, theme, widget, Application, Element};
use std::collections::hash_map::DefaultHasher;
use std::ffi::OsStr;
@ -117,25 +115,6 @@ enum Message {
PrintTask {
result: Result<(), String>,
},
About,
}
/// Menu action type – only the variants that can appear in the menu.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MenuActionType {
Print,
About,
}
impl MenuAction for MenuActionType {
type Message = Message;
fn message(&self) -> Self::Message {
match self {
MenuActionType::Print => Message::Print,
MenuActionType::About => Message::About,
}
}
}
struct NoctuaApp {
@ -342,12 +321,6 @@ impl Application for NoctuaApp {
}
}
}
Message::About => {
// Simple about: show info in the document area
if let Some(document) = self.current_document_mut() {
document.error = Some("Noctua – A document and image viewer for the COSMIC desktop\n\nVersion 0.1.0\n© 2025 Wolfgang Morawetz\nLicensed under GPL-3.0-or-later".to_string());
}
}
}
self.refresh_title();
@ -377,18 +350,6 @@ impl Application for NoctuaApp {
})
}
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
let doc = self.current_document();
let has_document = doc.is_some();
let menu_bar = widget::menu::bar(vec![widget::menu::Tree::with_children(
cosmic::widget::RcElementWrapper::new(Element::from(widget::menu::root("File"))),
widget::menu::items(&std::collections::HashMap::new(), menu_items(has_document)),
)]);
vec![Element::from(menu_bar)]
}
fn view(&self) -> Element<'_, Self::Message> {
let content = match self.current_document() {
Some(document) => self.document_view(document),
@ -427,7 +388,7 @@ impl Application for NoctuaApp {
format!("{} / {}", self.current + 1, self.documents.len())
};
let row = widget::row::with_capacity(9)
let row = widget::row::with_capacity(10)
.align_y(Alignment::Center)
.spacing(8)
.padding(8)
@ -452,6 +413,10 @@ impl Application for NoctuaApp {
.width(Length::Fixed(36.0)),
)
.push(widget::space::horizontal().width(Length::Fill))
.push(
widget::button::standard("Print")
.on_press(Message::Print),
)
.push(widget::text::body(page_label))
.push(
widget::button::standard("Prev page")
@ -466,31 +431,6 @@ impl Application for NoctuaApp {
}
}
fn menu_items(has_document: bool) -> Vec<MenuItem<MenuActionType, &'static str>> {
let mut items = Vec::new();
// Print button (only when a document is open)
if has_document {
items.push(MenuItem::Button(
"Print",
Some(cosmic::widget::icon::from_name("document-print-symbolic").into()),
MenuActionType::Print,
));
}
// Separator
items.push(MenuItem::Divider);
// About
items.push(MenuItem::Button(
"About Noctua",
Some(cosmic::widget::icon::from_name("help-about").into()),
MenuActionType::About,
));
items
}
impl NoctuaApp {
fn zoom_in(&mut self) {
self.zoom = (self.zoom * 1.2).min(5.0);

View file

@ -1,208 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// src/print_service.rs
//
// Print service – sends documents to the system printer via CUPS (lp).
//
// Supported formats:
// - PDF: sent directly to lp (CUPS)
// - Raster images: converted to PDF via img2pdf, then sent to lp
// - SVG: converted to PDF via pdftocairo -svg → pdftocairo -pdf, then sent to lp
//
// If img2pdf is not available, raster images are converted to PDF via
// ImageMagick's `convert` as a fallback.
use std::path::PathBuf;
use std::process::Command;
/// Result of a print operation.
#[derive(Debug)]
pub enum PrintResult {
/// The document was sent to the printer successfully.
Success,
/// The operation was cancelled by the user.
Cancelled,
/// An error occurred during printing.
Error(String),
}
/// Print the currently active document.
///
/// For PDF files the document is sent directly to CUPS via `lp`.
/// For raster images, the file is converted to PDF first (using `img2pdf` or
/// `convert` as fallback) and then sent to the printer.
/// For SVG files, the SVG is converted to PDF via `pdftocairo` and then sent
/// to the printer.
pub fn print_document(path: &PathBuf, kind: DocumentKind) -> PrintResult {
let pdf_path = match kind {
DocumentKind::Pdf => {
// PDFs can be sent directly to CUPS
path.clone()
}
DocumentKind::Raster => {
// Raster images need conversion to PDF first
convert_raster_to_pdf(path)
}
DocumentKind::Svg => {
// SVG needs conversion to PDF first
convert_svg_to_pdf(path)
}
};
match pdf_path {
Ok(pdf_path) => send_to_printer(&pdf_path),
Err(e) => PrintResult::Error(e),
}
}
/// Convert a raster image to a temporary PDF file.
///
/// Uses `img2pdf` first, then falls back to `convert` (ImageMagick).
fn convert_raster_to_pdf(path: &PathBuf) -> Result<PathBuf, String> {
// Try img2pdf first (cleaner, no quality loss)
if let Ok(result) = convert_with_cmd("img2pdf", vec![path.to_string_lossy().into_owned()], path) {
return Ok(result);
}
// Fallback to ImageMagick convert
convert_with_cmd("convert", vec![path.to_string_lossy().into_owned()], path)
}
/// Convert an SVG to a temporary PDF file.
///
/// Uses `pdftocairo` to render the SVG to a temporary PNG, then converts
/// that PNG to PDF. For a more direct approach, we try `svg2pdf` if available.
fn convert_svg_to_pdf(path: &PathBuf) -> Result<PathBuf, String> {
// Try svg2pdf first (direct SVG → PDF)
if let Ok(result) = convert_with_cmd(
"svg2pdf",
vec![
path.to_string_lossy().into_owned(),
"-".to_string(),
],
path,
) {
return Ok(result);
}
// Fallback: render SVG to PNG with pdftocairo, then convert PNG to PDF
// First render SVG to PNG
let prefix = temp_prefix(path, "svg");
let status = Command::new("pdftocairo")
.arg("-png")
.arg("-singlefile")
.arg("-f")
.arg("1")
.arg("-l")
.arg("1")
.arg("-scale-to")
.arg("2400")
.arg(path)
.arg(&prefix)
.status()
.map_err(|e| format!("Failed to run pdftocairo: {e}"))?;
if !status.success() {
return Err("Failed to render SVG with pdftocairo".to_string());
}
let png_path = PathBuf::from(format!("{}.png", prefix));
if !png_path.exists() {
return Err("pdftocairo did not produce output".to_string());
}
// Convert PNG to PDF
convert_with_cmd("img2pdf", vec![png_path.to_string_lossy().into_owned()], path)
}
/// Convert a file to PDF using a command-line tool.
///
/// Writes the output to a temporary file and returns its path.
fn convert_with_cmd(
cmd: &str,
args: Vec<String>,
_source_path: &PathBuf,
) -> Result<PathBuf, String> {
let status = Command::new(cmd)
.args(&args)
.output()
.map_err(|e| format!("Failed to run {cmd}: {e}"))?;
if !status.status.success() {
let stderr = String::from_utf8_lossy(&status.stderr);
return Err(format!("{cmd} failed: {stderr}"));
}
// img2pdf writes to stdout by default, convert writes to a file
// For img2pdf, we need to capture stdout
// For convert, the output is specified in args
// Let's handle both:
// - img2pdf: write to temp file
// - convert: find the output file
// For img2pdf, we need to re-run with output redirect
// Actually, let's use a simpler approach: write to a known temp file
let output_path = temp_prefix(_source_path, "print");
// Re-run with explicit output
let mut cmd = Command::new(cmd);
if cmd.get_program().to_string() == "img2pdf" {
cmd.args(&args).arg("-o").arg(&output_path);
} else {
// For convert, the last arg is the output
if !args.is_empty() {
cmd.args(&args[..args.len().saturating_sub(1)]).arg(&output_path);
}
}
let status = cmd.status().map_err(|e| format!("Failed to run {cmd}: {e}"))?;
if !status.success() {
return Err(format!("{cmd} failed"));
}
if output_path.exists() {
Ok(output_path)
} else {
Err(format!("{cmd} did not produce output"))
}
}
/// Send a PDF file to the system printer via CUPS (lp command).
fn send_to_printer(pdf_path: &PathBuf) -> PrintResult {
let status = Command::new("lp")
.arg(pdf_path)
.status()
.map_err(|e| format!("Failed to run lp: {e}"))?;
if status.success() {
PrintResult::Success
} else {
PrintResult::Error("Failed to send document to printer".to_string())
}
}
/// Generate a temporary file path for printing.
fn temp_prefix(source_path: &PathBuf, prefix: &str) -> PathBuf {
std::env::temp_dir()
.join(format!(
"noctua-print-{}-{}",
path_hash(source_path),
prefix
))
.with_extension("pdf")
}
/// Hash a path for use in temp file names.
fn path_hash(path: &PathBuf) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
path.hash(&mut hasher);
hasher.finish()
}
/// The document kind for print operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocumentKind {
Pdf,
Raster,
Svg,
}