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 }