feat(printers): refonte en tiroir de details, filtre des destinations decouvertes, peripheriques dedoublonnes
Interface: liste unique avec etat vide, details/actions/travaux dans un tiroir lateral au titre court (le nom long debordait sur le bouton Fermer). Backend: exclusion des destinations decouvertes dnssd (bit CUPS_PRINTER_DISCOVERED du printer-type) qui ne sont pas de vraies files, dedoublonnage des peripheriques par libelle avec preference dnssd, noms propres pour tous les schemas, suppression tolere une file deja disparue, page de test via lp (auto-detection du template PDF-BANNER).
This commit is contained in:
parent
d5e57cc439
commit
aec716fa6a
4 changed files with 312 additions and 329 deletions
|
|
@ -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<Vec<Printer>, String> {
|
|||
|
||||
Ok(destinations
|
||||
.iter()
|
||||
.filter(|dest| {
|
||||
!dest
|
||||
.get_option("printer-type")
|
||||
.and_then(|value| value.parse::<u32>().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<Vec<Device>, 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<Device> = 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<Vec<Driver>, 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::<Vec<_>>()
|
||||
.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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Vec<backend::Printer>, String>),
|
||||
Jobs(Result<Vec<backend::Job>, String>),
|
||||
Select(Option<String>),
|
||||
Select(String),
|
||||
SetDefault(String),
|
||||
ToggleEnabled(String, bool),
|
||||
PrintTestPage(String),
|
||||
|
|
@ -78,6 +82,36 @@ impl From<Message> 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<crate::pages::Message> for Page {
|
||||
fn info(&self) -> page::Info {
|
||||
page::Info::new("printers", "printer-symbolic")
|
||||
|
|
@ -89,11 +123,7 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
&self,
|
||||
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
|
||||
) -> Option<page::Content> {
|
||||
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<crate::pages::Message> for Page {
|
|||
|
||||
fn on_context_drawer_close(&mut self) -> Task<crate::pages::Message> {
|
||||
self.add = AddFlow::default();
|
||||
self.selected = None;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
|
@ -132,16 +163,28 @@ impl page::Page<crate::pages::Message> for Page {
|
|||
}
|
||||
|
||||
fn context_drawer(&self) -> Option<cosmic::app::ContextDrawer<'_, crate::pages::Message>> {
|
||||
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<crate::Message> = 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<crate::Message> = cosmic::task::message(
|
||||
|
|
@ -338,14 +378,6 @@ impl Page {
|
|||
Task::none()
|
||||
}
|
||||
|
||||
fn selected_printer_name(&self) -> Option<String> {
|
||||
// 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<crate::pages::Message> {
|
||||
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::<Element<'_, Message>>::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::<Element<'_, Message>>::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::<Element<'_, Message>>::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<String>, 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::pages::Message> {
|
||||
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::<Page>(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<String> = 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<crate::pages::Message> {
|
|||
})
|
||||
}
|
||||
|
||||
fn selected_printer_section() -> Section<crate::pages::Message> {
|
||||
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| page.selected.is_some())
|
||||
.view::<Page>(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<Element<'_, crate::pages::Message>> = 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::pages::Message> {
|
||||
crate::slab!(descriptions {
|
||||
cancel_job = fl!("printers-cancel-job");
|
||||
});
|
||||
|
||||
Section::default()
|
||||
.title(fl!("printers-jobs"))
|
||||
.descriptions(descriptions)
|
||||
.show_while::<Page>(|page| !page.jobs.is_empty())
|
||||
.view::<Page>(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<crate::pages::Message> for Page {}
|
||||
|
|
|
|||
1
i18n/en/cosmic_settings.ftl
vendored
1
i18n/en/cosmic_settings.ftl
vendored
|
|
@ -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
|
||||
|
|
|
|||
1
i18n/fr/cosmic_settings.ftl
vendored
1
i18n/fr/cosmic_settings.ftl
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue