Files
Panama/config/dot/quickshell/modules/settings/PrintersPage.qml
T

440 lines
19 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Printers, and one queue across all of them.
//
// The list answers "which printers exist"; the queue answers "where is my
// document", which is the question that actually brings someone here. They are
// separate cards because they are separate questions -- a job that has not come
// out is not necessarily a problem with the printer it was sent to.
//
// Driverless only. A printer old enough to need a PPD is named as such rather
// than offered and then failing at the moment it is added.
import Quickshell
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
objectName: "printers"
title: "Printers"
lede: "Printers this machine can use, and what they are waiting on."
property string expandedPrinter: ""
property string confirmingRemoval: ""
property bool addingByAddress: false
property string manualUri: ""
property string manualName: ""
readonly property bool manualReady: /^(ipp|ipps|socket):\/\/\S+$/.test(root.manualUri)
&& root.manualName.trim() !== ""
// The two defaults a queue can be told to change, dressed for reading. The
// values are the helper's closed vocabulary and nothing else reaches
// lpadmin; the choices come from what the printer said it supports, so a
// queue that cannot print two-sided never grows a two-sided dropdown.
//
// Empty while get-options is still on its way, which is why the rows are
// gated on the list rather than on the current value: an empty dropdown
// above a printer is worse than no dropdown at all.
function mediaChoices(reported: var): var {
const detail = {
"Letter": "8.5 × 11 in",
"A4": "210 × 297 mm",
"Legal": "8.5 × 14 in"
};
return (reported?.choices?.media ?? []).map(choice => ({
value: String(choice),
label: String(choice),
detail: detail[String(choice)] ?? ""
}));
}
function sidesChoices(reported: var): var {
const named = {
"one-sided": { label: "Off", detail: "One page per sheet" },
"two-sided-long-edge": { label: "Long edge", detail: "Bound like a book" },
"two-sided-short-edge": { label: "Short edge", detail: "Bound like a notepad" }
};
return (reported?.choices?.sides ?? []).map(choice => {
const known = named[String(choice)];
return {
value: String(choice),
label: known ? known.label : String(choice),
detail: known ? known.detail : ""
};
});
}
Component.onCompleted: if (!Printers.scanned) Printers.refresh()
ErrorRow {
label: "Printing needs attention"
message: Printers.lastError
}
// ── Which printers exist ─────────────────────────────────────────────────
SettingsCard {
visible: Printers.anyPrinters
title: "Printers"
subtitle: "Open one to change what it does by default."
Repeater {
model: Printers.printers
delegate: Column {
id: printerBlock
required property var modelData
required property int index
readonly property string name: String(printerBlock.modelData.name ?? "")
readonly property bool open: root.expandedPrinter === printerBlock.name
readonly property bool paused: String(printerBlock.modelData.state ?? "") === "stopped"
width: parent.width
SettingRow {
width: printerBlock.width
icon: printerBlock.paused ? "\u{F0026}" : "\u{F042A}"
label: String(printerBlock.modelData.description ?? printerBlock.name)
+ (printerBlock.modelData.isDefault ? " · Default" : "")
detail: Printers.stateSummary(printerBlock.modelData)
+ " · " + String(printerBlock.modelData.uri ?? "")
value: Printers.jobsFor(printerBlock.name) + " job"
+ (Printers.jobsFor(printerBlock.name) === 1 ? "" : "s")
activatable: !Printers.busy
divider: !printerBlock.open
&& printerBlock.index < Printers.printers.length - 1
onActivated: {
root.confirmingRemoval = "";
root.expandedPrinter = printerBlock.open ? "" : printerBlock.name;
}
}
Column {
id: printerBody
width: printerBlock.width
visible: printerBlock.open
// What this queue is set to, as CUPS reports it. Null until
// the read comes back, and a printer that reports neither
// key simply has no dropdowns -- an option offered for a
// printer that does not have it is a setting that silently
// does nothing.
readonly property var reported: Printers.optionsFor(printerBlock.name)
TextRow {
width: printerBlock.width
label: "Model"
detail: "Reported by the printer itself"
value: String(printerBlock.modelData.makeAndModel ?? "Unknown")
}
OptionPickerRow {
width: printerBlock.width
visible: root.mediaChoices(printerBody.reported).length > 0
label: "Paper size"
detail: "What applications print onto unless they ask for something else"
enabled: !Printers.busy
options: root.mediaChoices(printerBody.reported)
current: String(printerBody.reported?.options?.media ?? "")
onPicked: value => Printers.setOption(printerBlock.name, "media", String(value))
}
OptionPickerRow {
width: printerBlock.width
visible: root.sidesChoices(printerBody.reported).length > 0
label: "Two-sided"
detail: "Long edge is the usual choice for text; short edge flips like a notepad"
enabled: !Printers.busy
options: root.sidesChoices(printerBody.reported)
current: String(printerBody.reported?.options?.sides ?? "")
onPicked: value => Printers.setOption(printerBlock.name, "sides", String(value))
}
ActionRow {
width: printerBlock.width
visible: !printerBlock.modelData.isDefault
label: "Use by default"
detail: "Applications print here unless they are told otherwise"
action: "Make default"
enabled: !Printers.busy
onTriggered: Printers.setDefault(printerBlock.name)
}
ActionRow {
width: printerBlock.width
label: printerBlock.paused ? "Resume printing" : "Pause printing"
detail: printerBlock.paused
? "Queued jobs start again"
: "Jobs keep queuing, but nothing is sent until it resumes"
action: printerBlock.paused ? "Resume" : "Pause"
enabled: !Printers.busy
onTriggered: printerBlock.paused
? Printers.resume(printerBlock.name)
: Printers.pause(printerBlock.name)
}
ActionRow {
width: printerBlock.width
label: "Print a test page"
detail: "Confirms the printer answers and puts ink on paper"
action: "Print"
enabled: !Printers.busy && !printerBlock.paused
onTriggered: Printers.testPage(printerBlock.name)
}
SettingRow {
width: printerBlock.width
label: "Remove this printer"
detail: root.confirmingRemoval === printerBlock.name
? "Anything still queued for it is cancelled."
: "It can be added again later"
controlWidth: 200
divider: printerBlock.index < Printers.printers.length - 1
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 8
SettingsButton {
text: root.confirmingRemoval === printerBlock.name ? "Keep" : "Remove…"
enabled: !Printers.busy
onClicked: root.confirmingRemoval =
root.confirmingRemoval === printerBlock.name ? "" : printerBlock.name
}
SettingsButton {
visible: root.confirmingRemoval === printerBlock.name
text: "Remove"
tone: "danger"
enabled: !Printers.busy
onClicked: {
root.confirmingRemoval = "";
root.expandedPrinter = "";
Printers.remove(printerBlock.name);
}
}
}
}
}
}
}
}
// ── Where my document is ─────────────────────────────────────────────────
SettingsCard {
visible: Printers.anyPrinters
title: "Print queue"
subtitle: Printers.jobs.length === 0
? "Nothing is waiting."
: "Everything waiting, across every printer."
Repeater {
model: Printers.jobs
delegate: SettingRow {
id: jobRow
required property var modelData
required property int index
readonly property bool held: Printers.isHeld(jobRow.modelData)
width: parent.width
label: String(jobRow.modelData.name ?? "Untitled")
// Armed, the row makes the Hold/Cancel distinction the service
// was built around: holding keeps the job, cancelling throws it
// away and the document has to be sent from the app again.
detail: cancelJob.armed
? "The job is thrown away — printing it means sending it from the application again. Hold keeps it in the queue instead."
: String(jobRow.modelData.printer ?? "") + " · "
+ String(jobRow.modelData.state ?? "")
+ (Number(jobRow.modelData.pages ?? 0) > 0
? " · " + jobRow.modelData.pages + " pages" : "")
controlWidth: 265
divider: jobRow.index < Printers.jobs.length - 1
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 8
// Holding is the answer to "stop this one, I have not
// finished with it" -- cancelling was the only way out of
// the queue before, and it is not reversible.
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
text: jobRow.held ? "Release" : "Hold"
enabled: !Printers.busy
onClicked: jobRow.held
? Printers.release(Number(jobRow.modelData.id))
: Printers.hold(Number(jobRow.modelData.id))
}
ConfirmAction {
id: cancelJob
anchors.verticalCenter: parent.verticalCenter
actionId: "cancel-job:" + String(jobRow.modelData.id ?? "")
armText: "Cancel…"
confirmText: "Cancel it"
// Not "Keep": the job is not being kept in a drawer, it
// carries on out of the printer.
cancelText: "Keep printing"
enabled: !Printers.busy
onConfirmed: Printers.cancel(Number(jobRow.modelData.id))
}
}
}
}
TextRow {
visible: Printers.jobs.length === 0
label: "Queue is empty"
detail: "Jobs appear here while they wait, and can be cancelled from here"
value: ""
divider: false
}
}
// ── Adding ───────────────────────────────────────────────────────────────
//
// One card, not two. "No printers yet" and "Add a printer" carried the same
// Search row with two different wordings, so a machine with no printers
// offered the identical button twice under two headings.
SettingsCard {
title: Printers.anyPrinters ? "Add a printer" : "No printers yet"
subtitle: Printers.anyPrinters
? "Driverless printers only. One that needs a manufacturer driver has to be set up with the system printer tool."
: "Printers on this network that speak modern IPP appear here on their own. Driverless only."
ActionRow {
label: "Search the network"
detail: Printers.searching
? "Listening for printers that announce themselves…"
: (Printers.searched
? Printers.addable.length + " printer"
+ (Printers.addable.length === 1 ? "" : "s") + " found that are not set up here"
: "Looks for printers over mDNS, the same way phones and laptops find them")
action: Printers.searching ? "Searching…" : "Search"
enabled: !Printers.searching
onTriggered: Printers.search()
}
Repeater {
model: Printers.addable
delegate: ActionRow {
id: foundRow
required property var modelData
width: parent.width
label: String(foundRow.modelData.name ?? "")
detail: foundRow.modelData.driverless === true
? String(foundRow.modelData.uri ?? "") + " · driverless"
: String(foundRow.modelData.uri ?? "") + " · needs a manufacturer driver"
action: foundRow.modelData.driverless === true ? "Add" : "Not supported"
enabled: !Printers.busy && foundRow.modelData.driverless === true
onTriggered: Printers.add(String(foundRow.modelData.uri ?? ""),
Printers.suggestedName(String(foundRow.modelData.name ?? "")))
}
}
TextRow {
visible: Printers.searched && Printers.addable.length === 0 && !Printers.searching
label: "Nothing found"
detail: "No printer announced itself. It may be asleep, on another network, or may not support network discovery."
value: ""
}
ActionRow {
label: "Add by address"
detail: "For a printer that does not announce itself"
action: root.addingByAddress ? "Cancel" : "Enter…"
enabled: !Printers.busy
divider: root.addingByAddress
onTriggered: {
root.addingByAddress = !root.addingByAddress;
root.manualUri = "";
root.manualName = "";
}
}
Column {
width: parent.width
visible: root.addingByAddress
TextFieldRow {
width: parent.width
label: "Address"
detail: "ipp://, ipps://, or socket:// followed by the printer's host"
placeholder: "ipp://printer.local/ipp/print"
text: root.manualUri
onAccepted: value => root.manualUri = value
}
TextFieldRow {
width: parent.width
label: "Name"
detail: "What this printer is called on this machine"
placeholder: "office-printer"
text: root.manualName
onAccepted: value => root.manualName = value
}
ActionRow {
width: parent.width
label: "Add this printer"
detail: "The printer is asked what it can do; if it cannot answer, it is not added"
action: "Add"
enabled: root.manualReady && !Printers.busy
divider: false
onTriggered: {
Printers.add(root.manualUri, Printers.suggestedName(root.manualName));
root.addingByAddress = false;
}
}
}
}
// ── The service underneath ───────────────────────────────────────────────
SettingsCard {
title: "Printing service"
subtitle: "What has to be running for any of this to work."
TextRow {
label: "CUPS"
detail: Printers.service?.running === true
? "Running, so a printer added here works immediately"
: "Not running, so nothing can print"
value: Printers.service?.running === true ? "Running" : "Stopped"
}
TextRow {
label: "Starts at boot"
detail: Printers.service?.startsAtBoot === true
? "Started with the system"
: "Started on demand, when something prints. This is a normal configuration."
value: Printers.service?.startsAtBoot === true ? "Yes" : "On demand"
}
TextRow {
label: "Network discovery"
detail: Printers.service?.discoveryAvailable === true
? "Avahi is running, which is what finds printers that announce themselves"
: "Avahi is not running, so network printers cannot be discovered"
value: Printers.service?.discoveryAvailable === true ? "Available" : "Unavailable"
divider: false
}
}
}