diff --git a/cosmic-settings/src/pages/printers/backend.rs b/cosmic-settings/src/pages/printers/backend.rs index b3344bf..4590e5d 100644 --- a/cosmic-settings/src/pages/printers/backend.rs +++ b/cosmic-settings/src/pages/printers/backend.rs @@ -14,6 +14,11 @@ pub use cups_rs::{JobStatus, PrinterState}; const TEST_PAGE: &str = "/usr/share/cups/data/testprint"; +/// `printer-type` bit marking destinations that CUPS merely discovered on the +/// network (dnssd browsing). Those are not real queues: they cannot be removed +/// or configured and vanish on their own, so the page must not list them. +const CUPS_PRINTER_DISCOVERED: u32 = 0x1000000; + /// libcups keeps process-wide global state; serialize every FFI call. static CUPS_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); @@ -66,6 +71,12 @@ pub fn printers_sync() -> Result, String> { Ok(destinations .iter() + .filter(|dest| { + !dest + .get_option("printer-type") + .and_then(|value| value.parse::().ok()) + .is_some_and(|printer_type| printer_type & CUPS_PRINTER_DISCOVERED != 0) + }) .map(|dest| Printer { name: dest.name.clone(), info: dest @@ -108,15 +119,11 @@ pub fn cancel_job_sync(job_id: i32) -> Result<(), String> { cups_rs::job::cancel_job(job_id).map_err(|err| err.to_string()) } -pub fn print_test_page_sync(printer: &str) -> Result<(), String> { - let _guard = cups_lock(); - let dest = cups_rs::get_destination(printer).map_err(|err| err.to_string())?; - let job = cups_rs::create_job(&dest, "COSMIC Settings test page") - .map_err(|err| err.to_string())?; - // Let CUPS sniff the format: the test page is a cups-filters PDF banner - // template, not PostScript. - job.submit_file(TEST_PAGE, "application/octet-stream") - .map_err(|err| err.to_string()) +pub async fn print_test_page(printer: &str) -> Result<(), String> { + // Submit through `lp` so CUPS sniffs the format itself: the test page is + // a cups-filters PDF banner template (#PDF-BANNER), not PostScript, and + // cups_rs only allows a hardcoded list of document formats. + run_cli("lp", &["-d", printer, TEST_PAGE]).await } /// Discovery and administration (CUPS command line tools). @@ -124,17 +131,36 @@ pub fn print_test_page_sync(printer: &str) -> Result<(), String> { pub async fn devices() -> Result, String> { let output = run_cli_capture("lpinfo", &["-v"]).await?; - Ok(output - .lines() - .filter_map(|line| { - let (_, uri) = line.split_once(char::is_whitespace)?; - let uri = uri.trim(); - uri.contains("://").then(|| Device { - info: device_info(uri), - uri: uri.to_string(), - }) - }) - .collect()) + let mut devices: Vec = Vec::new(); + + for line in output.lines() { + let Some((_, uri)) = line.split_once(char::is_whitespace) else { + continue; + }; + let uri = uri.trim(); + if !uri.contains("://") { + continue; + } + + let info = device_info(uri); + + // One entry per physical device: the same printer is advertised over + // several schemes (dnssd, ipp, ipps…). Prefer the dnssd URI, which + // keeps working across IP changes. + if let Some(existing) = devices.iter_mut().find(|device| device.info == info) { + if uri.starts_with("dnssd://") && !existing.uri.starts_with("dnssd://") { + existing.uri = uri.to_string(); + } + continue; + } + + devices.push(Device { + info, + uri: uri.to_string(), + }); + } + + Ok(devices) } pub async fn drivers() -> Result, String> { @@ -161,7 +187,17 @@ pub async fn add_printer(name: &str, uri: &str, driver: &str) -> Result<(), Stri } pub async fn remove_printer(name: &str) -> Result<(), String> { - run_cli("lpadmin", &["-x", name]).await + match run_cli("lpadmin", &["-x", name]).await { + Ok(()) => Ok(()), + // A queue that vanished between listing and removal is already gone; + // treat it as a successful removal instead of an error. + Err(err) + if err.contains("n'existe pas") || err.to_lowercase().contains("not exist") => + { + Ok(()) + } + Err(err) => Err(err), + } } pub async fn set_default(name: &str) -> Result<(), String> { @@ -176,9 +212,12 @@ pub async fn set_enabled(name: &str, enabled: bool) -> Result<(), String> { } } -/// Sanitize a display name into a valid CUPS queue name and make it unique -/// against the current printer list. -pub fn unique_name(raw: &str, existing: &[Printer]) -> String { +/// Sanitize a display name into a valid CUPS queue name. +/// +/// No deduplication: `lpadmin` modifies the queue when the name already +/// exists, which also promotes the temporary queues CUPS auto-creates for +/// discovered devices into permanent ones. +pub fn queue_name(raw: &str) -> String { let mut sanitized = String::with_capacity(raw.len()); for c in raw.chars() { if c.is_alphanumeric() { @@ -198,19 +237,11 @@ pub fn unique_name(raw: &str, existing: &[Printer]) -> String { .collect::>() .join("_"); - let base = if base.is_empty() { + if base.is_empty() { "Printer".to_string() } else { base - }; - - let mut candidate = base.clone(); - let mut index = 2; - while existing.iter().any(|printer| printer.name == candidate) { - candidate = format!("{base}_{index}"); - index += 1; } - candidate } async fn run_cli(program: &str, args: &[&str]) -> Result<(), String> { @@ -270,6 +301,19 @@ fn device_info(uri: &str) -> String { return "PDF".to_string(); } + // ipp/ipps/socket/lpd/http(s) URIs: keep the host label and drop the + // dnssd-style service suffix ("Name._ipp._tcp.local" -> "Name"). + if let Some((_, rest)) = uri.split_once("://") + && let Some(host) = rest.split(['/', '?']).next() + { + let decoded = percent_decode(host); + let label = decoded.split("._").next().unwrap_or(&decoded); + let label = label.trim_end_matches(".local"); + if !label.is_empty() && !label.chars().all(|c| c.is_ascii_digit() || c == '.' || c == ':') { + return label.to_string(); + } + } + percent_decode(uri) } diff --git a/cosmic-settings/src/pages/printers/mod.rs b/cosmic-settings/src/pages/printers/mod.rs index f0491b0..830716d 100644 --- a/cosmic-settings/src/pages/printers/mod.rs +++ b/cosmic-settings/src/pages/printers/mod.rs @@ -2,6 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only //! Printers page: manage CUPS queues, the default printer, and print jobs. +//! +//! The main view is a single list of printers with an empty state. Printer +//! details, actions, and jobs live in a context drawer, mirroring the way +//! other COSMIC settings pages present device details. mod backend; @@ -52,7 +56,7 @@ pub enum Message { Reload, Printers(Result, String>), Jobs(Result, String>), - Select(Option), + Select(String), SetDefault(String), ToggleEnabled(String, bool), PrintTestPage(String), @@ -78,6 +82,36 @@ impl From for crate::pages::Message { } } +fn state_text(state: PrinterState) -> String { + match state { + PrinterState::Idle => fl!("printers-state-idle"), + PrinterState::Processing => fl!("printers-state-processing"), + PrinterState::Stopped => fl!("printers-state-stopped"), + PrinterState::Unknown => fl!("printers-state-unknown"), + } +} + +fn job_status_text(status: JobStatus) -> String { + match status { + JobStatus::Pending => fl!("printers-job-pending"), + JobStatus::Processing => fl!("printers-job-processing"), + JobStatus::Held => fl!("printers-job-held"), + JobStatus::Stopped => fl!("printers-job-stopped"), + JobStatus::Completed => fl!("printers-job-completed"), + JobStatus::Canceled => fl!("printers-job-canceled"), + JobStatus::Aborted => fl!("printers-job-aborted"), + JobStatus::Unknown => fl!("printers-job-unknown"), + } +} + +fn printer_icon(printer: &backend::Printer) -> &'static str { + match printer.state { + PrinterState::Processing => "printer-printing-symbolic", + PrinterState::Stopped => "printer-error-symbolic", + _ => "printer-symbolic", + } +} + impl page::Page for Page { fn info(&self) -> page::Info { page::Info::new("printers", "printer-symbolic") @@ -89,11 +123,7 @@ impl page::Page for Page { &self, sections: &mut SlotMap>, ) -> Option { - Some(vec![ - sections.insert(printers_section()), - sections.insert(selected_printer_section()), - sections.insert(jobs_section()), - ]) + Some(vec![sections.insert(printers_section())]) } fn set_id(&mut self, entity: page::Entity) { @@ -106,6 +136,7 @@ impl page::Page for Page { fn on_context_drawer_close(&mut self) -> Task { self.add = AddFlow::default(); + self.selected = None; Task::none() } @@ -132,16 +163,28 @@ impl page::Page for Page { } fn context_drawer(&self) -> Option> { - if !self.add.active { - return None; + if self.add.active { + return Some( + cosmic::app::context_drawer( + self.add_view().map(crate::pages::Message::Printers), + crate::pages::Message::CloseContextDrawer, + ) + .title(fl!("printers-add")), + ); } + let printer = self + .selected + .as_deref() + .and_then(|name| self.printers.iter().find(|p| p.name == name))?; + Some( cosmic::app::context_drawer( - self.add_view().map(crate::pages::Message::Printers), + self.printer_view(printer) + .map(crate::pages::Message::Printers), crate::pages::Message::CloseContextDrawer, ) - .title(fl!("printers-add")), + .title(fl!("printers-details")), ) } @@ -174,7 +217,11 @@ impl Page { } Message::Select(name) => { - self.selected = name; + self.selected = Some(name); + let open_drawer: cosmic::Task = cosmic::task::message( + crate::app::Message::OpenContextDrawer(self.entity), + ); + return open_drawer; } Message::SetDefault(name) => { @@ -189,15 +236,7 @@ impl Page { Message::PrintTestPage(name) => { self.notice = None; - return cosmic::task::future(async move { - Message::OperationDone(tokio::task::spawn_blocking(move || { - backend::print_test_page_sync(&name) - }) - .await - .unwrap_or_else(|err| Err(err.to_string()))) - }) - .map(crate::pages::Message::Printers) - .map(Into::into); + return Self::admin(async move { backend::print_test_page(&name).await }); } Message::Remove(name) => { @@ -209,10 +248,11 @@ impl Page { } Message::ConfirmRemove => { - self.dialog = None; - if let Some(name) = self.selected_printer_name() { - return Self::admin(async move { backend::remove_printer(&name).await }); - } + let Some(Dialog::RemovePrinter { name }) = self.dialog.take() else { + return Task::none(); + }; + self.selected = None; + return Self::admin(async move { backend::remove_printer(&name).await }); } Message::CancelJob(id) => { @@ -235,6 +275,7 @@ impl Page { } Message::OpenAdd => { + self.selected = None; self.add = AddFlow { active: true, loading: true, @@ -277,7 +318,7 @@ impl Page { Message::SelectDevice(index) => { if let Some(device) = self.add.devices.get(index) { - self.add.name = backend::unique_name(&device.info, &self.printers); + self.add.name = backend::queue_name(&device.info); self.add.selected_device = Some(index); } } @@ -303,7 +344,7 @@ impl Page { return Task::none(); }; - let name = backend::unique_name(self.add.name.trim(), &self.printers); + let name = backend::queue_name(self.add.name.trim()); let uri = device.uri.clone(); let driver = self.add.selected_driver.clone(); self.add.submitting = true; @@ -319,7 +360,6 @@ impl Page { Message::AddDone(result) => match result { Ok(name) => { self.add = AddFlow::default(); - self.selected = Some(name.clone()); self.notice = Some(fl!("printers-added", name = name)); let close_drawer: cosmic::Task = cosmic::task::message( @@ -338,14 +378,6 @@ impl Page { Task::none() } - fn selected_printer_name(&self) -> Option { - // Removal is confirmed from the dialog; operate on the selected printer. - match self.dialog.as_ref() { - Some(Dialog::RemovePrinter { name }) => Some(name.clone()), - None => self.selected.clone(), - } - } - fn reload(&self) -> Task { Task::batch([ cosmic::task::future(async { @@ -375,9 +407,130 @@ impl Page { .map(Into::into) } + /// Details, actions, and jobs of a printer, shown in the context drawer. + fn printer_view(&self, printer: &backend::Printer) -> Element<'_, Message> { + let spacing = theme::spacing(); + let mut content = + widget::column::with_children(Vec::>::new()) + .spacing(spacing.space_s); + + content = content.push( + text::title4(printer.info.to_string()) + .wrapping(cosmic::iced::core::text::Wrapping::Word), + ); + + let mut state = state_text(printer.state); + if !printer.accepting_jobs { + state.push_str(&format!(" · {}", fl!("printers-not-accepting"))); + } + + content = content.push( + widget::settings::flex_item(fl!("printers-state"), text::body(state)) + .align_items(Alignment::Center), + ); + + if let Some(location) = printer.location.as_deref() + && !location.is_empty() + { + content = content.push( + widget::settings::flex_item(fl!("printers-location"), text::body(location.to_string())) + .align_items(Alignment::Center), + ); + } + + if let Some(uri) = printer.device_uri.as_deref() { + content = content.push( + widget::settings::flex_item(fl!("printers-device-uri"), text::body(uri.to_string())) + .align_items(Alignment::Center), + ); + } + + if !printer.state_reasons.is_empty() { + content = content.push(text::caption(printer.state_reasons.join(", "))); + } + + let name = printer.name.clone(); + let mut actions = + widget::column::with_children(Vec::>::new()) + .spacing(spacing.space_xs); + + if !printer.is_default { + actions = actions.push( + widget::button::standard(fl!("printers-make-default")) + .width(Length::Fill) + .on_press(Message::SetDefault(name.clone()).into()), + ); + } + + let paused = printer.state == PrinterState::Stopped; + actions = actions.push(if paused { + widget::button::standard(fl!("printers-resume")) + .width(Length::Fill) + .on_press(Message::ToggleEnabled(name.clone(), true).into()) + } else { + widget::button::standard(fl!("printers-pause")) + .width(Length::Fill) + .on_press(Message::ToggleEnabled(name.clone(), false).into()) + }); + + actions = actions.push( + widget::button::standard(fl!("printers-test-page")) + .width(Length::Fill) + .on_press(Message::PrintTestPage(name.clone()).into()), + ); + + actions = actions.push( + widget::button::destructive(fl!("printers-remove")) + .width(Length::Fill) + .on_press(Message::Remove(name.clone()).into()), + ); + + content = content.push(actions); + + let jobs: Vec<&backend::Job> = self + .jobs + .iter() + .filter(|job| job.dest == printer.name) + .collect(); + + if !jobs.is_empty() { + content = content.push(text::body(fl!("printers-jobs"))); + + let mut list = widget::list_column(); + for job in jobs { + let cancel = widget::icon::from_name("window-close-symbolic") + .apply(widget::button::icon) + .on_press(Message::CancelJob(job.id).into()); + + let title = if job.title.is_empty() { + format!("#{}", job.id) + } else { + job.title.clone() + }; + + list = list.add( + widget::settings::item::builder(title) + .description(job_status_text(job.status)) + .control(widget::tooltip::tooltip( + cancel, + text::caption(fl!("printers-cancel-job")), + widget::tooltip::Position::Top, + )), + ); + } + + content = content.push(list); + } + + content.into() + } + + /// Add-printer flow, shown in the context drawer. fn add_view(&self) -> Element<'_, Message> { let spacing = theme::spacing(); - let mut content = widget::column::with_children(Vec::new()).spacing(spacing.space_s); + let mut content = + widget::column::with_children(Vec::>::new()) + .spacing(spacing.space_s); content = content.push(text::body(fl!("printers-device"))); @@ -388,29 +541,15 @@ impl Page { devices = devices.add(text::caption(fl!("printers-no-devices"))); } else { for (index, device) in self.add.devices.iter().enumerate() { - let selected = self.add.selected_device == Some(index); - let control: Element<'_, Message> = if selected { - widget::icon::from_name("object-select-symbolic") - .size(16) - .into() - } else { - widget::space::horizontal() - .width(Length::Fixed(16.0)) - .into() - }; - devices = devices.add( - widget::list::button( - widget::settings::item::builder(device.info.to_string()) - .description(device.uri.to_string()) - .control(control), - ) - .on_press(Message::SelectDevice(index)), + widget::settings::item::builder(device.info.to_string()) + .description(device.uri.to_string()) + .radio(index, self.add.selected_device, Message::SelectDevice), ); } } - content = content.push(widget::scrollable(devices).height(Length::Fixed(220.0))); + content = content.push(widget::scrollable(devices).height(Length::Fixed(200.0))); content = content.push( widget::text_input("", &self.add.name) @@ -429,27 +568,19 @@ impl Page { let filter = self.add.driver_filter.to_lowercase(); let mut drivers = widget::list_column(); let mut shown = 0usize; - for driver in self.add.drivers.iter().filter(|driver| { + for (index, driver) in self.add.drivers.iter().enumerate().filter(|(_, driver)| { filter.is_empty() || driver.description.to_lowercase().contains(&filter) || driver.id.to_lowercase().contains(&filter) }) { let selected = self.add.selected_driver == driver.id; - let control: Element<'_, Message> = if selected { - widget::icon::from_name("object-select-symbolic").size(16).into() - } else { - widget::space::horizontal() - .width(Length::Fixed(16.0)) - .into() - }; - + let id = driver.id.clone(); drivers = drivers.add( - widget::list::button( - widget::settings::item::builder(driver.description.to_string()) - .description(driver.id.to_string()) - .control(control), - ) - .on_press(Message::SelectDriver(driver.id.clone())), + widget::settings::item::builder(driver.description.to_string()) + .description(driver.id.to_string()) + .radio(index, selected.then_some(index), move |_| { + Message::SelectDriver(id.clone()) + }), ); shown += 1; @@ -462,7 +593,7 @@ impl Page { drivers = drivers.add(text::caption(fl!("printers-no-matching-drivers"))); } - content = content.push(widget::scrollable(drivers).height(Length::Fixed(260.0))); + content = content.push(widget::scrollable(drivers).height(Length::Fixed(240.0))); if let Some(error) = self.add.error.as_deref() { content = content.push(text::caption(error.to_string())); @@ -493,100 +624,76 @@ impl Page { } } -fn state_label(state: PrinterState, descriptions: &slab::Slab, keys: &StateKeys) -> String { - match state { - PrinterState::Idle => descriptions[keys.idle].clone(), - PrinterState::Processing => descriptions[keys.processing].clone(), - PrinterState::Stopped => descriptions[keys.stopped].clone(), - PrinterState::Unknown => descriptions[keys.unknown].clone(), - } -} - -struct StateKeys { - idle: usize, - processing: usize, - stopped: usize, - unknown: usize, -} - -fn printer_icon(printer: &backend::Printer) -> &'static str { - match printer.state { - PrinterState::Processing => "printer-printing-symbolic", - PrinterState::Stopped => "printer-error-symbolic", - _ => "printer-symbolic", - } -} - fn printers_section() -> Section { crate::slab!(descriptions { add = fl!("printers-add"); default = fl!("printers-default"); make_default = fl!("printers-make-default"); no_printers = fl!("printers-no-printers"); + no_printers_desc = fl!("printers-no-printers-desc"); not_accepting = fl!("printers-not-accepting"); - idle = fl!("printers-state-idle"); - processing = fl!("printers-state-processing"); - stopped = fl!("printers-state-stopped"); - unknown = fl!("printers-state-unknown"); }); - let keys = StateKeys { - idle, - processing, - stopped, - unknown, - }; - Section::default() .title(fl!("printers")) .descriptions(descriptions) .view::(move |_binder, page, section| { let descriptions = §ion.descriptions; + + if page.printers.is_empty() { + return widget::column::with_children(vec![ + widget::icon::from_name("printer-symbolic") + .size(64) + .apply(Element::from), + text::title3(descriptions[no_printers].clone()).apply(Element::from), + text::caption(descriptions[no_printers_desc].clone()).apply(Element::from), + widget::button::suggested(descriptions[add].clone()) + .on_press(Message::OpenAdd.into()) + .apply(Element::from), + ]) + .spacing(theme::spacing().space_s) + .align_x(Alignment::Center) + .apply(widget::container) + .padding(theme::spacing().space_l) + .width(Length::Fill) + .apply(Element::from); + } + let mut list = widget::list_column(); if let Some(notice) = page.notice.as_deref() { list = list.add(text::caption(notice.to_string())); } - if page.printers.is_empty() { - list = list.add(text::body(descriptions[no_printers].clone())); - } - for printer in &page.printers { - let is_selected = page.selected.as_deref() == Some(printer.name.as_str()); - let mut parts: Vec = Vec::new(); if let Some(model) = printer.make_and_model.as_deref() && !model.is_empty() { parts.push(model.to_string()); } - parts.push(state_label(printer.state, descriptions, &keys)); + parts.push(state_text(printer.state)); if !printer.accepting_jobs { parts.push(descriptions[not_accepting].clone()); } let control: Element<'_, crate::pages::Message> = if printer.is_default { - widget::button::suggested(descriptions[default].clone()) - .apply(Element::from) + widget::button::suggested(descriptions[default].clone()).apply(Element::from) } else { widget::button::link(descriptions[make_default].clone()) .on_press(Message::SetDefault(printer.name.clone()).into()) .apply(Element::from) }; - let row = widget::settings::item::builder(printer.info.to_string()) - .description(parts.join(" · ")) - .icon(widget::icon::from_name(printer_icon(printer)).size(24)) - .control(control); - - let message = if is_selected { - Message::Select(None) - } else { - Message::Select(Some(printer.name.clone())) - }; - - list = list.add(widget::list::button(row).on_press(message.into())); + list = list.add( + widget::list::button( + widget::settings::item::builder(printer.info.to_string()) + .description(parts.join(" · ")) + .icon(widget::icon::from_name(printer_icon(printer)).size(24)) + .control(control), + ) + .on_press(Message::Select(printer.name.clone()).into()), + ); } list = list.add( @@ -603,174 +710,4 @@ fn printers_section() -> Section { }) } -fn selected_printer_section() -> Section { - crate::slab!(descriptions { - make_default = fl!("printers-make-default"); - pause = fl!("printers-pause"); - resume = fl!("printers-resume"); - remove = fl!("printers-remove"); - test_page = fl!("printers-test-page"); - idle = fl!("printers-state-idle"); - processing = fl!("printers-state-processing"); - stopped = fl!("printers-state-stopped"); - unknown = fl!("printers-state-unknown"); - }); - - let keys = StateKeys { - idle, - processing, - stopped, - unknown, - }; - - Section::default() - .title(fl!("printers-details")) - .descriptions(descriptions) - .show_while::(|page| page.selected.is_some()) - .view::(move |_binder, page, section| { - let descriptions = §ion.descriptions; - - let Some(printer) = page - .selected - .as_deref() - .and_then(|name| page.printers.iter().find(|p| p.name == name)) - else { - return Element::from(widget::column::with_children(Vec::new())); - }; - - let spacing = theme::spacing(); - let mut column = widget::list_column(); - - column = column.add( - widget::settings::flex_item( - fl!("printers-state"), - text::body(state_label(printer.state, descriptions, &keys)), - ) - .align_items(Alignment::Center), - ); - - if let Some(location) = printer.location.as_deref() - && !location.is_empty() - { - column = column.add( - widget::settings::flex_item(fl!("printers-location"), text::body(location.to_string())) - .align_items(Alignment::Center), - ); - } - - if let Some(uri) = printer.device_uri.as_deref() { - column = column.add( - widget::settings::flex_item(fl!("printers-device-uri"), text::body(uri.to_string())) - .align_items(Alignment::Center), - ); - } - - if !printer.state_reasons.is_empty() { - column = column.add( - widget::settings::flex_item( - fl!("printers-state-reasons"), - text::body(printer.state_reasons.join(", ")), - ) - .align_items(Alignment::Center), - ); - } - - let name = printer.name.clone(); - let mut actions: Vec> = Vec::new(); - - if !printer.is_default { - actions.push( - widget::button::standard(descriptions[make_default].clone()) - .on_press(Message::SetDefault(name.clone()).into()) - .apply(Element::from), - ); - } - - let paused = printer.state == PrinterState::Stopped; - actions.push(if paused { - widget::button::standard(descriptions[resume].clone()) - .on_press(Message::ToggleEnabled(name.clone(), true).into()) - .apply(Element::from) - } else { - widget::button::standard(descriptions[pause].clone()) - .on_press(Message::ToggleEnabled(name.clone(), false).into()) - .apply(Element::from) - }); - - actions.push( - widget::button::standard(descriptions[test_page].clone()) - .on_press(Message::PrintTestPage(name.clone()).into()) - .apply(Element::from), - ); - - actions.push(widget::space::horizontal().apply(Element::from)); - - actions.push( - widget::button::destructive(descriptions[remove].clone()) - .on_press(Message::Remove(name).into()) - .apply(Element::from), - ); - - column = column.add( - widget::flex_row(actions) - .spacing(spacing.space_s) - .width(Length::Fill), - ); - - column.apply(Element::from) - }) -} - -fn job_status_label(status: JobStatus) -> String { - match status { - JobStatus::Pending => fl!("printers-job-pending"), - JobStatus::Processing => fl!("printers-job-processing"), - JobStatus::Held => fl!("printers-job-held"), - JobStatus::Stopped => fl!("printers-job-stopped"), - JobStatus::Completed => fl!("printers-job-completed"), - JobStatus::Canceled => fl!("printers-job-canceled"), - JobStatus::Aborted => fl!("printers-job-aborted"), - JobStatus::Unknown => fl!("printers-job-unknown"), - } -} - -fn jobs_section() -> Section { - crate::slab!(descriptions { - cancel_job = fl!("printers-cancel-job"); - }); - - Section::default() - .title(fl!("printers-jobs")) - .descriptions(descriptions) - .show_while::(|page| !page.jobs.is_empty()) - .view::(move |_binder, page, section| { - let descriptions = §ion.descriptions; - let mut list = widget::list_column(); - - for job in &page.jobs { - let cancel_button = widget::icon::from_name("window-close-symbolic") - .apply(widget::button::icon) - .on_press(Message::CancelJob(job.id).into()); - - let title = if job.title.is_empty() { - format!("#{}", job.id) - } else { - job.title.clone() - }; - - list = list.add( - widget::settings::item::builder(title) - .description(format!("{} · {}", job.dest, job_status_label(job.status))) - .control(widget::tooltip::tooltip( - cancel_button, - text::caption(descriptions[cancel_job].clone()), - widget::tooltip::Position::Top, - )), - ); - } - - list.apply(Element::from) - }) -} - impl page::AutoBind for Page {} diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 9ec5e26..f662da9 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -671,6 +671,7 @@ printers-name = Name printers-no-devices = No devices found printers-no-matching-drivers = No matching drivers printers-no-printers = No printers configured +printers-no-printers-desc = Printers connected to this computer or discovered on the network will appear here. printers-not-accepting = Not accepting jobs printers-pause = Pause printers-remove = Remove printer diff --git a/i18n/fr/cosmic_settings.ftl b/i18n/fr/cosmic_settings.ftl index 3d6e06c..ae3797c 100644 --- a/i18n/fr/cosmic_settings.ftl +++ b/i18n/fr/cosmic_settings.ftl @@ -459,6 +459,7 @@ printers-name = Nom printers-no-devices = Aucun périphérique trouvé printers-no-matching-drivers = Aucun pilote ne correspond printers-no-printers = Aucune imprimante configurée +printers-no-printers-desc = Les imprimantes connectées à cet ordinateur ou découvertes sur le réseau apparaîtront ici. printers-not-accepting = N'accepte pas les travaux printers-pause = Suspendre printers-remove = Supprimer l'imprimante