Warning, /plasma/print-manager/src/kcm/ui/PrinterSettings.qml is written in an unsupported language. File is not indexed.
0001 /** 0002 * SPDX-FileCopyrightText: 2022 Nicolas Fella <nicolas.fella@gmx.de> 0003 * SPDX-FileCopyrightText: 2023 Mike Noe <noeerover@gmail.com> 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 import QtQuick 0008 import QtQuick.Layouts 0009 import QtQuick.Controls as QQC2 0010 import org.kde.plasma.components as PComp 0011 import org.kde.kirigami as Kirigami 0012 import org.kde.kcmutils as KCM 0013 import org.kde.kitemmodels as KSFM 0014 import org.kde.plasma.printmanager as PM 0015 0016 KCM.AbstractKCM { 0017 id: root 0018 0019 // Add mode means adding a new printer/group 0020 property bool addMode: false 0021 property var modelData 0022 // Printer ppd attributes 0023 property var ppd 0024 0025 property PM.PrinterModel printerModel 0026 property PM.PPDModel ppdModel 0027 0028 function openMakeModelDlg() { 0029 const dlg = mmComp.createObject(root) 0030 dlg.open() 0031 } 0032 0033 function openFindPrinterDlg() { 0034 const dlg = newComp.createObject(root) 0035 dlg.open() 0036 } 0037 0038 title: { 0039 if (addMode) { 0040 return modelData.isClass 0041 ? i18nc("@title:window", "Add Group") 0042 : i18nc("@title:window", "Add Printer") 0043 } else { 0044 const locStr = modelData.location && printerModel.displayLocationHint 0045 ? " (%1)".arg(modelData.location) 0046 : "" 0047 return modelData.info + locStr 0048 } 0049 } 0050 0051 actions: [ 0052 Kirigami.Action { 0053 text: i18n("Configure…") 0054 icon.name: "configure-symbolic" 0055 visible: !addMode 0056 onTriggered: PM.ProcessRunner.configurePrinter(modelData.printerName) 0057 }, Kirigami.Action { 0058 text: i18n("Remove") 0059 icon.name: "edit-delete-remove-symbolic" 0060 visible: !addMode 0061 onTriggered: removeLoader.active = true 0062 } 0063 ] 0064 0065 header: BannerWithTimer { 0066 id: error 0067 } 0068 0069 footer: RowLayout { 0070 Layout.margins: Kirigami.Units.largeSpacing 0071 0072 Kirigami.UrlButton { 0073 id: urlButton 0074 text: i18nc("@action:button", "CUPS Printers Overview Help") 0075 url: "http://localhost:631/help/overview.html" 0076 padding: Kirigami.Units.largeSpacing 0077 } 0078 0079 Item { Layout.fillWidth: true } 0080 0081 //TODO: Is this a valid feature? (addMode && !modelData.isClass) 0082 QQC2.CheckBox { 0083 id: autoConfig 0084 text: i18nc("@option:check", "Auto Configure") 0085 checked: false 0086 visible: false 0087 } 0088 0089 QQC2.Button { 0090 text: addMode 0091 ? i18nc("@action:button Add printer", "Add") 0092 : i18nc("@action:button Apply changes", "Apply") 0093 icon.name: "dialog-ok-apply" 0094 enabled: config.hasPending 0095 0096 onClicked: { 0097 if (addMode) { 0098 if (queueName.text.length === 0) { 0099 queueName.focus = true 0100 error.text = i18nc("@info:status", "Queue name is required. Enter a unique queue name.") 0101 error.visible = true 0102 return 0103 } 0104 if (!modelData.isClass) { 0105 if (driver.text.length === 0) { 0106 driverSelect.focus = true 0107 error.text = i18nc("@info:status", "Make/Model is required. Choose \"Select\" to pick Make/Model") 0108 error.visible = true 0109 return 0110 } 0111 } 0112 0113 config.add("add", true) 0114 if (autoConfig.checked) { 0115 config.add("autoConfig", true) 0116 } 0117 } 0118 0119 kcm.savePrinter(queueName.text, config.pending, modelData.isClass) 0120 } 0121 } 0122 } 0123 0124 Component.onCompleted: { 0125 if (!modelData.isClass) { 0126 if (addMode) { 0127 config.set({"ppd-name": modelData["ppd-name"] 0128 , "ppd-type": modelData["ppd-type"]}) 0129 } else { 0130 ppd = kcm.getPrinterPPD(modelData.printerName) 0131 } 0132 } 0133 } 0134 0135 // Each item stores its corresponding CUPS field name in objectName 0136 // This is then used to generate the "pending" changes map 0137 ConfigValues { 0138 id: config 0139 } 0140 0141 Connections { 0142 id: kcmConn 0143 target: kcm 0144 0145 property int saveCount 0146 0147 function onRequestError(errorMessage) { 0148 error.text = errorMessage 0149 error.visible = true 0150 config.clear() 0151 } 0152 0153 function onRemoveDone() { 0154 // check for successful remove 0155 if (saveCount < printerModel.rowCount()) { 0156 kcm.pop() 0157 } else { 0158 error.text = i18n("Failed to remove the printer: %1", error.text) 0159 } 0160 } 0161 } 0162 0163 Loader { 0164 id: removeLoader 0165 active: false 0166 0167 width: Math.round(root.width/2) 0168 height: Kirigami.Units.gridUnit * 15 0169 0170 sourceComponent: Kirigami.PromptDialog { 0171 id: prompt 0172 0173 Component.onCompleted: open() 0174 onClosed: removeLoader.active = false 0175 0176 title: modelData.isClass ? i18n("Remove Group") : i18n("Remove Printer") 0177 subtitle: i18n("Are you sure you really want to remove: %1 (%2)?" 0178 , modelData.info, modelData.printerName) 0179 0180 standardButtons: Kirigami.Dialog.NoButton 0181 0182 customFooterActions: [ 0183 Kirigami.Action { 0184 text: prompt.title 0185 icon.name: "edit-delete-remove-symbolic" 0186 onTriggered: { 0187 // save the current count to verify successful remove 0188 kcmConn.saveCount = printerModel.rowCount() 0189 kcm.removePrinter(modelData.printerName) 0190 close() 0191 } 0192 }, 0193 Kirigami.Action { 0194 text: i18n("Cancel") 0195 icon.name: "dialog-cancel-symbolic" 0196 onTriggered: close() 0197 } 0198 ] 0199 } 0200 0201 } 0202 0203 Component { 0204 id: newComp 0205 0206 FindPrinter { 0207 anchors.centerIn: parent 0208 implicitWidth: Math.ceil(parent.width*.90) 0209 implicitHeight: Math.ceil(parent.height*.90) 0210 0211 // Selected printer and/or driver 0212 // ppd-name contains the driver file 0213 onSetValues: configMap => { 0214 // Set the text entry items 0215 if (configMap.hasOwnProperty("printer-model")) { 0216 queueName.text = configMap["printer-model"].replace(/ /g, "_") 0217 } 0218 queueInfo.text = configMap[queueInfo.objectName] 0219 devUri.text = configMap[devUri.objectName] 0220 location.text = configMap[location.objectName] 0221 driver.text = configMap["printer-make-and-model"] 0222 0223 // Initialize the config map 0224 config.set(configMap) 0225 config.clean() 0226 0227 // Set the PPD attrs 0228 ppd.make = configMap["printer-make"] 0229 ppd.makeModel = configMap["printer-make-and-model"] 0230 ppd.type = configMap["ppd-type"] 0231 ppd.file = configMap["ppd-name"] ?? "" 0232 0233 // strip out the base file name 0234 if (ppd.file) { 0235 const i = ppd.file.lastIndexOf('/') 0236 if (i !== -1) { 0237 ppd.pcfile = ppd.file.slice(-(ppd.file.length-i-1)) 0238 } else { 0239 ppd.pcfile = ppd.file 0240 } 0241 } else { 0242 ppd.pcfile = "" 0243 } 0244 0245 // If we have a driver file, then no need to offer 0246 // the make/model selection 0247 if (!config.value("remote") && ppd.file.length === 0) { 0248 openMakeModelDlg() 0249 } 0250 } 0251 } 0252 } 0253 0254 Component { 0255 id: mmComp 0256 0257 MakeModel { 0258 anchors.centerIn: parent 0259 implicitWidth: Math.ceil(parent.width*.85) 0260 implicitHeight: Math.ceil(parent.height*.85) 0261 0262 model: ppdModel 0263 ppdData: Object.assign({}, ppd) 0264 0265 onSaveValues: ppdMap => { 0266 Object.assign(ppd, ppdMap) 0267 driver.text = ppd.type !== PM.PPDType.Manual 0268 ? ppd.makeModel 0269 : ppd.file 0270 0271 if (ppd.file.length > 0) { 0272 config.set({"ppd-type": ppd.type 0273 , "ppd-name": ppd.file}) 0274 const i = ppd.file.lastIndexOf('/') 0275 if (i !== -1) { 0276 ppd.pcfile = ppd.file.slice(-(ppd.file.length-i-1)) 0277 } else { 0278 ppd.pcfile = ppd.file 0279 } 0280 } else { 0281 config.remove(["ppd-name", "ppd-type"]) 0282 ppd.pcfile = "" 0283 } 0284 } 0285 } 0286 } 0287 0288 component PrinterField: QQC2.TextField { 0289 Layout.fillWidth: true 0290 property string orig 0291 text: orig 0292 0293 Component.onCompleted: { 0294 if (addMode) { 0295 config.add(objectName, text) 0296 } 0297 } 0298 0299 onEditingFinished: { 0300 if (!addMode) { 0301 config.remove(objectName) 0302 } 0303 0304 if (text !== orig) { 0305 config.add(objectName, text) 0306 } 0307 } 0308 } 0309 0310 component PrinterOption: QQC2.CheckBox { 0311 property bool orig 0312 checked: orig 0313 0314 Component.onCompleted: { 0315 if (addMode) { 0316 config.add(objectName, checked) 0317 } 0318 } 0319 0320 onToggled: { 0321 if (!addMode) { 0322 config.remove(objectName) 0323 } 0324 0325 if (checked !== orig) { 0326 config.add(objectName, checked) 0327 } 0328 } 0329 } 0330 0331 ColumnLayout { 0332 anchors.centerIn: parent 0333 0334 Kirigami.SelectableLabel { 0335 visible: modelData.isClass 0336 Layout.alignment: Qt.AlignHCenter 0337 Layout.preferredWidth: Math.ceil(root.width/2) 0338 0339 textFormat: Text.RichText 0340 text: i18nc("@info:whatsthis", "A <b>printer group</b> is used to pool printing resources. 0341 Member printers can be added to a group and print jobs sent to that group 0342 will be dispatched to the appropriate printer.") 0343 wrapMode: Text.WordWrap 0344 } 0345 0346 Kirigami.Separator { 0347 visible: modelData.isClass 0348 Layout.topMargin: Kirigami.Units.largeSpacing 0349 Layout.bottomMargin: Kirigami.Units.largeSpacing 0350 Layout.fillWidth: true 0351 } 0352 0353 RowLayout { 0354 spacing: Kirigami.Units.smallSpacing 0355 Layout.bottomMargin: Kirigami.Units.largeSpacing 0356 0357 Kirigami.Icon { 0358 source: modelData.isClass ? "folder-print" : modelData.iconName 0359 Layout.preferredWidth: Kirigami.Units.iconSizes.enormous 0360 Layout.preferredHeight: Layout.preferredWidth 0361 } 0362 0363 ColumnLayout { 0364 spacing: Kirigami.Units.smallSpacing 0365 0366 Kirigami.Heading { 0367 text: modelData.info ?? "" 0368 visible: !addMode 0369 level: 3 0370 type: Kirigami.Heading.Type.Primary 0371 } 0372 0373 Kirigami.Heading { 0374 text: modelData.kind.replace("Class", "Group") 0375 visible: !addMode 0376 level: 5 0377 type: Kirigami.Heading.Type.Secondary 0378 } 0379 0380 PrinterOption { 0381 objectName: "isDefault" 0382 text: i18nc("@action:check Set default printer", "Default printer") 0383 orig: modelData.isDefault 0384 } 0385 0386 PrinterOption { 0387 objectName: "printer-is-shared" 0388 text: modelData.isClass 0389 ? i18nc("@action:check", "Share this group") 0390 : i18nc("@action:check", "Share this printer") 0391 enabled: kcm.shareConnectedPrinters 0392 orig: modelData.isShared 0393 } 0394 0395 PrinterOption { 0396 objectName: "printer-is-accepting-jobs" 0397 text: i18nc("@action:check", "Accepting print jobs") 0398 orig: modelData.isAcceptingJobs 0399 } 0400 } 0401 } 0402 0403 // Marker (ink) status 0404 Repeater { 0405 model: !addMode ? modelData.markers["marker-names"] : null 0406 0407 delegate: RowLayout { 0408 QQC2.Label { 0409 text: modelData 0410 Layout.minimumWidth: Kirigami.Units.gridUnit*7 0411 } 0412 0413 QQC2.ProgressBar { 0414 from: 0 0415 to: 100 0416 value: root.modelData.markers["marker-levels"][index] 0417 palette.highlight: root.modelData.markers["marker-colors"][index] 0418 } 0419 } 0420 } 0421 0422 // Maint actions 0423 RowLayout { 0424 visible: !addMode 0425 Layout.topMargin: Kirigami.Units.largeSpacing 0426 0427 QQC2.Button { 0428 text: i18nc("@action:button", "Print Test Page") 0429 icon.name: "document-print-symbolic" 0430 onClicked: kcm.printTestPage(modelData.printerName, modelData.isClass) 0431 } 0432 0433 QQC2.Button { 0434 text: i18nc("@action:button", "Print Self-Test Page") 0435 icon.name: "document-print-symbolic" 0436 visible: modelData.commands.indexOf("PrintSelfTestPage") !== -1 0437 onClicked: kcm.printSelfTestPage(modelData.printerName) 0438 } 0439 0440 QQC2.Button { 0441 text: i18nc("@action:button", "Clean Print Heads") 0442 icon.name: "document-cleanup-symbolic" 0443 visible: modelData.commands.indexOf("Clean") !== -1 0444 onClicked: kcm.cleanPrintHeads(modelData.printerName) 0445 } 0446 } 0447 0448 Kirigami.Separator { 0449 Layout.topMargin: Kirigami.Units.largeSpacing*2 0450 Layout.bottomMargin: Kirigami.Units.largeSpacing 0451 Layout.fillWidth: true 0452 } 0453 0454 GridLayout { 0455 columns: 2 0456 columnSpacing: Kirigami.Units.gridUnit 0457 0458 QQC2.Button { 0459 Layout.fillWidth: true 0460 Layout.columnSpan: 2 0461 text: i18nc("@action:button", "Find a Printer…") 0462 icon.name: "search-symbolic" 0463 visible: addMode && !modelData.isClass 0464 0465 onClicked: openFindPrinterDlg() 0466 } 0467 0468 QQC2.Label { 0469 text: i18nc("@label:textbox", "Queue Name:") 0470 Layout.alignment: Qt.AlignRight 0471 } 0472 0473 PrinterField { 0474 id: queueName 0475 objectName: "printer-name" 0476 orig: modelData.printerName 0477 enabled: addMode 0478 validator: RegularExpressionValidator { regularExpression: /[^/#\\ ]*/ } 0479 } 0480 0481 QQC2.Label { 0482 text: i18nc("@label:textbox", "Description:") 0483 Layout.alignment: Qt.AlignRight 0484 } 0485 0486 PrinterField { 0487 id: queueInfo 0488 objectName: "printer-info" 0489 readOnly: modelData.remote 0490 orig: modelData.info ?? "" 0491 } 0492 0493 QQC2.Label { 0494 text: i18nc("@label:textbox", "Location:") 0495 Layout.alignment: Qt.AlignRight 0496 } 0497 0498 PrinterField { 0499 id: location 0500 objectName: "printer-location" 0501 readOnly: modelData.remote 0502 orig: modelData.location ?? "" 0503 } 0504 0505 QQC2.Label { 0506 text: i18nc("@label:textbox", "Connection:") 0507 Layout.alignment: Qt.AlignRight 0508 visible: !modelData.isClass 0509 } 0510 0511 PrinterField { 0512 id: devUri 0513 visible: !modelData.isClass 0514 objectName: "device-uri" 0515 orig: modelData.printerUri ?? "" 0516 readOnly: modelData.remote 0517 } 0518 0519 QQC2.Label { 0520 text: i18nc("@label:listbox", "Member Printers:") 0521 Layout.alignment: Qt.AlignRight | Qt.AlignTop 0522 visible: modelData.isClass 0523 } 0524 0525 // Printer Class member list 0526 Loader { 0527 active: modelData.isClass 0528 visible: active 0529 Layout.fillHeight: true 0530 Layout.fillWidth: true 0531 0532 sourceComponent: PComp.ScrollView { 0533 0534 contentItem: ListView { 0535 id: memberList 0536 // cups key for the member list 0537 objectName: "member-uris" 0538 clip: true 0539 0540 property bool showClasses: false 0541 0542 model: KSFM.KSortFilterProxyModel { 0543 sourceModel: printerModel 0544 0545 filterRowCallback: (source_row, source_parent) => { 0546 const ndx = sourceModel.index(source_row, 0, source_parent) 0547 const pn = sourceModel.data(ndx, PM.PrinterModel.DestName) 0548 0549 if (!memberList.showClasses) { 0550 const isClass = sourceModel.data(ndx, PM.PrinterModel.DestIsClass) 0551 const isRemote = sourceModel.data(ndx, PM.PrinterModel.DestRemote) 0552 if (isClass || isRemote) { 0553 return false 0554 } 0555 } 0556 return pn !== root.modelData.printerName 0557 } 0558 } 0559 0560 // TODO: Seems to be a timing issue with the delegates and the KSFM. 0561 // They're not available right away, so push the setting of 0562 // check state a bit later. 0563 Component.onCompleted: { 0564 if (root.modelData.memberNames.length > 0) { 0565 checkTimer.start() 0566 } 0567 } 0568 0569 Timer { 0570 id: checkTimer 0571 interval: 100; repeat: true; running: false 0572 onTriggered: { 0573 if (memberList.count > 0) { 0574 stop() 0575 memberList.setChecked() 0576 } 0577 } 0578 } 0579 0580 // CUPS strips the queue name from the URI 0581 // so for display, compare the queue name. 0582 function setChecked() { 0583 for (let i=0; i<count; ++i) { 0584 const cb = itemAtIndex(i) 0585 if (cb instanceof Kirigami.CheckSubtitleDelegate) 0586 cb.checked = cb?.visible 0587 && root.modelData.memberNames.includes(cb.objectName) 0588 } 0589 } 0590 0591 // For save, use the full URI 0592 function getChecked(keysOnly: bool) { 0593 let ret = [] 0594 for (let i=0; i<count; ++i) { 0595 const item = itemAtIndex(i) 0596 if (item.checked) { 0597 ret.push(keysOnly ? item.objectName : item.supportedUri) 0598 } 0599 } 0600 return ret 0601 } 0602 0603 function hasChanges() { 0604 let changed = false 0605 const vals = getChecked(true) 0606 if (vals.length !== root.modelData.memberNames.length 0607 || JSON.stringify(vals) !== JSON.stringify(root.modelData.memberNames)) { 0608 changed = true 0609 } 0610 0611 return changed 0612 } 0613 0614 delegate: Kirigami.CheckSubtitleDelegate { 0615 width: ListView.view.width 0616 icon.width: 0 0617 objectName: printerName 0618 property string supportedUri: uriSupported 0619 0620 text: info 0621 subtitle: printerName 0622 0623 // if there are changes, send checked list 0624 // if there are changes and nothing is checked, send empty object 0625 // if there are NO changes, just send the checked list 0626 onToggled: { 0627 const cfg = {} 0628 if (memberList.hasChanges()) { 0629 const list = memberList.getChecked() 0630 if (list.length > 0) 0631 cfg[memberList.objectName] = list 0632 } else { 0633 cfg[memberList.objectName] = memberList.getChecked() 0634 } 0635 0636 // an empty member list implies the class should be removed 0637 if (Object.keys(cfg).length > 0) { 0638 config.set(cfg) 0639 } else { 0640 config.remove(memberList.objectName) 0641 } 0642 0643 } 0644 } 0645 } 0646 } 0647 0648 } 0649 0650 QQC2.Label { 0651 text: i18nc("@label:textbox", "Make/Model:") 0652 Layout.alignment: Qt.AlignRight 0653 visible: !modelData.isClass 0654 } 0655 0656 RowLayout { 0657 visible: !modelData.isClass 0658 0659 QQC2.Label { 0660 id: driver 0661 text: modelData.kind ?? "" 0662 } 0663 0664 QQC2.Button { 0665 id: driverSelect 0666 text: i18nc("@action:button Select printer make/model", "Select…") 0667 icon.name: "printer-symbolic" 0668 enabled: !modelData.remote 0669 0670 onClicked: { 0671 openMakeModelDlg() 0672 } 0673 } 0674 } 0675 0676 } 0677 0678 } 0679 }