Merge lp:~jonas-drange/ubuntu-ui-extras/consolidated-devices into lp:~phablet-team/ubuntu-ui-extras/printer-staging

Proposed by Jonas G. Drange
Status: Superseded
Proposed branch: lp:~jonas-drange/ubuntu-ui-extras/consolidated-devices
Merge into: lp:~phablet-team/ubuntu-ui-extras/printer-staging
Diff against target: 2069 lines (+1492/-112)
26 files modified
modules/Ubuntu/Components/Extras/Example/Printers.qml (+73/-28)
modules/Ubuntu/Components/Extras/Printers/CMakeLists.txt (+1/-0)
modules/Ubuntu/Components/Extras/Printers/backend/backend_cups.cpp (+9/-8)
modules/Ubuntu/Components/Extras/Printers/cups/devicesearcher.cpp (+36/-5)
modules/Ubuntu/Components/Extras/Printers/cups/devicesearcher.h (+1/-0)
modules/Ubuntu/Components/Extras/Printers/cups/ppdutils.cpp (+343/-0)
modules/Ubuntu/Components/Extras/Printers/cups/ppdutils.h (+87/-0)
modules/Ubuntu/Components/Extras/Printers/models/devicemodel.cpp (+216/-1)
modules/Ubuntu/Components/Extras/Printers/models/devicemodel.h (+80/-1)
modules/Ubuntu/Components/Extras/Printers/printer/printer.cpp (+2/-1)
modules/Ubuntu/Components/Extras/Printers/printer/printerjob.cpp (+2/-2)
modules/Ubuntu/Components/Extras/Printers/printers/printers.cpp (+9/-0)
modules/Ubuntu/Components/Extras/Printers/printers/printers.h (+3/-0)
modules/Ubuntu/Components/Extras/Printers/structs.h (+0/-2)
modules/Ubuntu/Components/Extras/Printers/utils.h (+52/-53)
po/ubuntu-ui-extras.pot (+5/-5)
tests/unittests/Printers/CMakeLists.txt (+16/-0)
tests/unittests/Printers/mockbackend.h (+3/-3)
tests/unittests/Printers/tst_consolidateddevicemodel.cpp (+162/-0)
tests/unittests/Printers/tst_devicefilter.cpp (+106/-0)
tests/unittests/Printers/tst_jobmodel.cpp (+1/-0)
tests/unittests/Printers/tst_ppdutils.cpp (+138/-0)
tests/unittests/Printers/tst_printer.cpp (+2/-2)
tests/unittests/Printers/tst_printerdevicemodel.cpp (+35/-0)
tests/unittests/Printers/tst_printermodel.cpp (+3/-1)
tests/unittests/Printers/tst_utils.cpp (+107/-0)
To merge this branch: bzr merge lp:~jonas-drange/ubuntu-ui-extras/consolidated-devices
Reviewer Review Type Date Requested Status
Andrew Hayzen (community) Needs Information
Review via email: mp+320637@code.launchpad.net

This proposal has been superseded by a proposal from 2017-03-30.

Commit message

Adds ConsolidatedDeviceModel to make sure printers with multiple connections to it (e.g. dnssd, socket, etc.) are presented as one.

Description of the change

Adds ConsolidatedDeviceModel to make sure printers with multiple connections to it (e.g. dnssd, socket, etc.) are presented as one.

To post a comment you must log in.
Revision history for this message
Andrew Hayzen (ahayzen) wrote :

Getting a crash when using this http://pastebin.ubuntu.com/24228896/

review: Needs Fixing
159. By Jonas G. Drange

add ppdutils

160. By Jonas G. Drange

matching now works well for hp devices

161. By Jonas G. Drange

adds ppdutils, a IEEE 1284 parser, and other fixes to consolidate devices

162. By Jonas G. Drange

make fixmakemodel static

Revision history for this message
Andrew Hayzen (ahayzen) wrote :

This is looking much better than before (it doesn't crash :-) ).

Now we have Utils and PpdUtils, would it make sense to move some of the methods from Utils into PpdUtils, eg ppdChoiceToDuplexMode. Or are we not able to move them because they are used in the models and not specifically the backend?

review: Needs Information
Revision history for this message
Jonas G. Drange (jonas-drange) wrote :

Good point. Yeah, I think that should be doable.

163. By Jonas G. Drange

do RAAI via parent->child semantics, and be more aggressive when to clear out the consolidated model

164. By Jonas G. Drange

pushes ppd specific utils from Utils into PpdUtils

165. By Jonas G. Drange

smaller refactor making device makemodel fixes easily testable

166. By Jonas G. Drange

fixes broken test after changing the test data

167. By Jonas G. Drange

syncs with staging and fixes a test that broke

168. By Jonas G. Drange

uses makeLower

169. By Jonas G. Drange

syncs with staging

170. By Jonas G. Drange

creates hp devices for hplip capable printers

171. By Jonas G. Drange

sets the faxid on any fax

172. By Jonas G. Drange

actually start the process, suggest hplip

173. By Jonas G. Drange

drop the device id for fax

174. By Jonas G. Drange

uses info as a last resort

175. By Jonas G. Drange

make sure non-consolidateable devices are kept

176. By Jonas G. Drange

fixes test that now broke, but had to change, and add .get to drivermodel

Unmerged revisions

176. By Jonas G. Drange

fixes test that now broke, but had to change, and add .get to drivermodel

175. By Jonas G. Drange

make sure non-consolidateable devices are kept

174. By Jonas G. Drange

uses info as a last resort

173. By Jonas G. Drange

drop the device id for fax

172. By Jonas G. Drange

actually start the process, suggest hplip

171. By Jonas G. Drange

sets the faxid on any fax

170. By Jonas G. Drange

creates hp devices for hplip capable printers

169. By Jonas G. Drange

syncs with staging

168. By Jonas G. Drange

uses makeLower

167. By Jonas G. Drange

syncs with staging and fixes a test that broke

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'modules/Ubuntu/Components/Extras/Example/Printers.qml'
2--- modules/Ubuntu/Components/Extras/Example/Printers.qml 2017-03-21 23:02:42 +0000
3+++ modules/Ubuntu/Components/Extras/Example/Printers.qml 2017-03-30 13:23:27 +0000
4@@ -685,7 +685,7 @@
5 verticalCenter: parent.verticalCenter
6 }
7 property var target
8- Component.onCompleted: target = Printers.devices
9+ Component.onCompleted: target = Printers.consolidatedDevices
10 running: target.searching
11 }
12 }
13@@ -699,27 +699,12 @@
14 topMargin: units.gu(2)
15 }
16 height: contentItem.childrenRect.height
17- model: Printers.devices
18+ model: Printers.consolidatedDevices
19 delegate: ListItem {
20 height: modelLayout.height + (divider.visible ? divider.height : 0)
21 ListItemLayout {
22 id: modelLayout
23 title.text: displayName
24- subtitle.text: {
25- if (type == PrinterEnum.LPDType) return "LPD";
26- if (type == PrinterEnum.IppSType) return "IppS";
27- if (type == PrinterEnum.Ipp14Type) return "Ipp14";
28- if (type == PrinterEnum.HttpType) return "Http";
29- if (type == PrinterEnum.BehType) return "Beh";
30- if (type == PrinterEnum.SocketType) return "Socket";
31- if (type == PrinterEnum.HttpsType) return "Https";
32- if (type == PrinterEnum.IppType) return "Ipp";
33- if (type == PrinterEnum.HPType) return "HP";
34- if (type == PrinterEnum.USBType) return "USB";
35- if (type == PrinterEnum.HPFaxType) return "HPFax";
36- if (type == PrinterEnum.DNSSDType) return "DNSSD";
37- else return "Unknown protocol";
38- }
39
40 Icon {
41 id: icon
42@@ -729,17 +714,22 @@
43 SlotsLayout.position: SlotsLayout.First
44 }
45
46- Button {
47- text: "Select printer"
48- onClicked: {
49- var suggestedPrinterName = (" " + displayName).slice(1);
50- suggestedPrinterName = suggestedPrinterName.replace(/\ /g, "\-");
51- printerUri.text = uri;
52- printerName.text = suggestedPrinterName;
53- printerDescription.text = info;
54- printerLocation.text = location;
55- }
56- }
57+ ProgressionSlot {}
58+ }
59+ onClicked: {
60+ var p = pageStack.push(chooseConnectionPage, {
61+ consolidatedDevice: model
62+ });
63+ p.onConnectionChosen.connect(function (conn) {
64+ pageStack.pop();
65+
66+ var suggestedPrinterName = (" " + conn.displayName).slice(1);
67+ suggestedPrinterName = suggestedPrinterName.replace(/\ /g, "\-");
68+ printerUri.text = conn.uri;
69+ printerName.text = suggestedPrinterName;
70+ printerDescription.text = conn.info;
71+ printerLocation.text = conn.location;
72+ });
73 }
74 }
75 }
76@@ -757,4 +747,59 @@
77 }
78 }
79 }
80+
81+ Component {
82+ id: chooseConnectionPage
83+
84+ Page {
85+ visible: false
86+ property var consolidatedDevice
87+ signal connectionChosen(var conn)
88+ header: PageHeader {
89+ id: chooseConnectionPageHeader
90+ title: "Choose connection"
91+ flickable: connectionsList
92+ }
93+
94+ ListView {
95+ id: connectionsList
96+ anchors.fill: parent
97+ model: consolidatedDevice.devices
98+ delegate: ListItem {
99+ height: modelLayout.height + (divider.visible ? divider.height : 0)
100+ ListItemLayout {
101+ id: modelLayout
102+ title.text: info
103+ subtitle.text: {
104+ if (type == PrinterEnum.LPDType) return "LPD";
105+ if (type == PrinterEnum.IppSType) return "IppS";
106+ if (type == PrinterEnum.Ipp14Type) return "Ipp14";
107+ if (type == PrinterEnum.HttpType) return "Http";
108+ if (type == PrinterEnum.BehType) return "Beh";
109+ if (type == PrinterEnum.SocketType) return "Socket";
110+ if (type == PrinterEnum.HttpsType) return "Https";
111+ if (type == PrinterEnum.IppType) return "Ipp";
112+ if (type == PrinterEnum.HPType) return "HP";
113+ if (type == PrinterEnum.USBType) return "USB";
114+ if (type == PrinterEnum.HPFaxType) return "HPFax";
115+ if (type == PrinterEnum.DNSSDType) return "DNSSD";
116+ else return "Unknown protocol";
117+ }
118+ summary.text: uri
119+
120+ Icon {
121+ id: icon
122+ width: height
123+ height: units.gu(2.5)
124+ name: "network-printer-symbolic"
125+ SlotsLayout.position: SlotsLayout.First
126+ }
127+
128+ ProgressionSlot {}
129+ }
130+ onClicked: connectionChosen(model)
131+ }
132+ }
133+ }
134+ }
135 }
136
137=== modified file 'modules/Ubuntu/Components/Extras/Printers/CMakeLists.txt'
138--- modules/Ubuntu/Components/Extras/Printers/CMakeLists.txt 2017-03-13 12:51:16 +0000
139+++ modules/Ubuntu/Components/Extras/Printers/CMakeLists.txt 2017-03-30 13:23:27 +0000
140@@ -34,6 +34,7 @@
141 cups/jobloader.cpp
142 cups/printerdriverloader.cpp
143 cups/printerloader.cpp
144+ cups/ppdutils.cpp
145
146 models/devicemodel.cpp
147 models/drivermodel.cpp
148
149=== modified file 'modules/Ubuntu/Components/Extras/Printers/backend/backend_cups.cpp'
150--- modules/Ubuntu/Components/Extras/Printers/backend/backend_cups.cpp 2017-03-15 17:42:21 +0000
151+++ modules/Ubuntu/Components/Extras/Printers/backend/backend_cups.cpp 2017-03-30 13:23:27 +0000
152@@ -17,6 +17,7 @@
153 #include "backend/backend_cups.h"
154 #include "cups/devicesearcher.h"
155 #include "cups/jobloader.h"
156+#include "cups/ppdutils.h"
157 #include "cups/printerdriverloader.h"
158 #include "cups/printerloader.h"
159 #include "utils.h"
160@@ -264,9 +265,9 @@
161 ppd_choice_t* def = ppdFindChoice(ppdColorModel,
162 ppdColorModel->defchoice);
163 if (def) {
164- model = Utils::parsePpdColorModel(def->choice,
165- def->text,
166- "ColorModel");
167+ model = PpdUtils::parsePpdColorModel(def->choice,
168+ def->text,
169+ "ColorModel");
170 }
171 }
172 ret[option] = QVariant::fromValue(model);
173@@ -278,8 +279,8 @@
174 ppd_choice_t* def = ppdFindChoice(ppdQuality,
175 ppdQuality->defchoice);
176 if (def) {
177- quality = Utils::parsePpdPrintQuality(def->choice,
178- def->text, opt);
179+ quality = PpdUtils::parsePpdPrintQuality(def->choice,
180+ def->text, opt);
181 }
182 }
183 }
184@@ -291,7 +292,7 @@
185 if (qualityOpt) {
186 for (int i = 0; i < qualityOpt->num_choices; ++i) {
187 qualities.append(
188- Utils::parsePpdPrintQuality(
189+ PpdUtils::parsePpdPrintQuality(
190 qualityOpt->choices[i].choice,
191 qualityOpt->choices[i].text,
192 opt
193@@ -307,7 +308,7 @@
194 if (colorModels) {
195 for (int i = 0; i < colorModels->num_choices; ++i) {
196 models.append(
197- Utils::parsePpdColorModel(
198+ PpdUtils::parsePpdColorModel(
199 colorModels->choices[i].choice,
200 colorModels->choices[i].text,
201 QStringLiteral("ColorModel")
202@@ -357,7 +358,7 @@
203
204 __CUPS_ADD_OPTION(dest, "copies", QString::number(options->copies()).toLocal8Bit());
205 __CUPS_ADD_OPTION(dest, "ColorModel", options->getColorModel().name.toLocal8Bit());
206- __CUPS_ADD_OPTION(dest, "Duplex", Utils::duplexModeToPpdChoice(options->getDuplexMode()).toLocal8Bit());
207+ __CUPS_ADD_OPTION(dest, "Duplex", PpdUtils::duplexModeToPpdChoice(options->getDuplexMode()).toLocal8Bit());
208
209 if (options->landscape()) {
210 __CUPS_ADD_OPTION(dest, "landscape", "");
211
212=== modified file 'modules/Ubuntu/Components/Extras/Printers/cups/devicesearcher.cpp'
213--- modules/Ubuntu/Components/Extras/Printers/cups/devicesearcher.cpp 2017-03-08 14:47:16 +0000
214+++ modules/Ubuntu/Components/Extras/Printers/cups/devicesearcher.cpp 2017-03-30 13:23:27 +0000
215@@ -15,9 +15,9 @@
216 */
217
218 #include "cups/ippclient.h"
219+#include "cups/ppdutils.h"
220 #include "devicesearcher.h"
221-
222-#include <QUrl>
223+#include "utils.h"
224
225 DeviceSearcher::DeviceSearcher(IppClient *client, QObject *parent)
226 : QObject(parent)
227@@ -54,14 +54,45 @@
228 }
229
230 Device d;
231+
232 d.cls = deviceClass;
233 d.id = deviceId;
234 d.info = deviceInfo;
235- d.makeModel = deviceMakeAndModel;
236+
237 d.uri = deviceUri;
238 d.location = deviceLocation;
239-
240- searcher->deviceFound(d);
241+ d.makeModel = deviceMakeAndModel;
242+
243+ DeviceSearcher::fixMakeModel(d);
244+
245+ if (!d.makeModel.isEmpty()) {
246+ searcher->deviceFound(d);
247+ }
248+}
249+
250+void DeviceSearcher::fixMakeModel(Device &device)
251+{
252+ // We can't set make-model on devices without enough info.
253+ if (device.id.isEmpty() &&
254+ (device.makeModel.isEmpty() ||
255+ device.makeModel == QStringLiteral("Unknown"))) {
256+ device.makeModel = QString::null;
257+
258+ return;
259+ }
260+
261+ QString canidateMakeModel;
262+
263+ if (!device.id.isEmpty()) {
264+ auto idDict = Utils::parseDeviceId(device.id);
265+ canidateMakeModel = QString("%1 %2").arg(idDict["MFG"], idDict["MDL"]);
266+ } else if (!device.makeModel.isEmpty()) {
267+ canidateMakeModel = device.makeModel;
268+ }
269+
270+ auto split = PpdUtils::splitMakeModel(canidateMakeModel);
271+ canidateMakeModel = QString("%1 %2").arg(split.first, split.second);
272+ device.makeModel = canidateMakeModel;
273 }
274
275 void DeviceSearcher::deviceFound(const Device &device)
276
277=== modified file 'modules/Ubuntu/Components/Extras/Printers/cups/devicesearcher.h'
278--- modules/Ubuntu/Components/Extras/Printers/cups/devicesearcher.h 2017-03-08 11:29:01 +0000
279+++ modules/Ubuntu/Components/Extras/Printers/cups/devicesearcher.h 2017-03-30 13:23:27 +0000
280@@ -47,6 +47,7 @@
281 const char *location,
282 void *context);
283 void deviceFound(const Device &device);
284+ static void fixMakeModel(Device &device);
285
286 Q_SIGNALS:
287 void loaded(const Device &device);
288
289=== added file 'modules/Ubuntu/Components/Extras/Printers/cups/ppdutils.cpp'
290--- modules/Ubuntu/Components/Extras/Printers/cups/ppdutils.cpp 1970-01-01 00:00:00 +0000
291+++ modules/Ubuntu/Components/Extras/Printers/cups/ppdutils.cpp 2017-03-30 13:23:27 +0000
292@@ -0,0 +1,343 @@
293+/*
294+ * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2014, 2015 Red Hat, Inc.
295+ * Copyright (C) 2006 Florian Festi <ffesti@redhat.com>
296+ * Copyright (C) 2006, 2007, 2008, 2009 Tim Waugh <twaugh@redhat.com>
297+ * Copyright (C) 2017 Canonical, Ltd.
298+ *
299+ * This program is free software; you can redistribute it and/or modify
300+ * it under the terms of the GNU Lesser General Public License as published by
301+ * the Free Software Foundation; version 3.
302+ *
303+ * This program is distributed in the hope that it will be useful,
304+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
305+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
306+ * GNU Lesser General Public License for more details.
307+ *
308+ * You should have received a copy of the GNU Lesser General Public License
309+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
310+ */
311+
312+#include "ppdutils.h"
313+
314+#include <QSet>
315+
316+const QRegularExpression PpdUtils::versionNumbers = QRegularExpression(
317+ " v(?:er\\.)?\\d(?:\\d*\\.\\d+)?(?: |$)"
318+);
319+const QRegularExpression PpdUtils::ignoredSuffixes = QRegularExpression(
320+ ","
321+ "| hpijs"
322+ "| foomatic/"
323+ "| - "
324+ "| w/"
325+ "| \\("
326+ "| postscript"
327+ "| ps"
328+ "| pdf"
329+ "| pxl"
330+ "| zjs"
331+ "| zxs"
332+ "| pcl3"
333+ "| printer"
334+ "|_bt"
335+ "| pcl"
336+ "| ufr ii"
337+ "| br-script"
338+);
339+const QRegularExpression PpdUtils::ignoreSeries = QRegularExpression(
340+ " series| all-in-one",
341+ QRegularExpression::CaseInsensitiveOption
342+);
343+const QList<QPair<QString, QRegularExpression>> PpdUtils::manufacturerByModels
344+ = QList<QPair<QString, QRegularExpression>>({
345+ QPair<QString, QRegularExpression>(
346+ QStringLiteral("HP"), QRegularExpression(
347+ "deskjet"
348+ "|dj[ 0-9]?"
349+ "|laserjet"
350+ "|lj"
351+ "|color laserjet"
352+ "|color lj"
353+ "|designjet"
354+ "|officejet"
355+ "|oj"
356+ "|photosmart"
357+ "|ps "
358+ "|psc"
359+ "|edgeline"
360+ )
361+ ),
362+ QPair<QString, QRegularExpression>(
363+ QStringLiteral("Epson"), QRegularExpression("stylus|aculaser")
364+ ),
365+ QPair<QString, QRegularExpression>(
366+ QStringLiteral("Apple"), QRegularExpression(
367+ "stylewriter"
368+ "|imagewriter"
369+ "|deskwriter"
370+ "|laserwriter"
371+ )
372+ ),
373+ QPair<QString, QRegularExpression>(
374+ QStringLiteral("Canon"), QRegularExpression(
375+ "pixus"
376+ "|pixma"
377+ "|selphy"
378+ "|imagerunner"
379+ "|bj"
380+ "|lbp"
381+ )
382+ ),
383+ QPair<QString, QRegularExpression>(
384+ QStringLiteral("Brother"), QRegularExpression("hl|dcp|mfc")
385+ ),
386+ QPair<QString, QRegularExpression>(
387+ QStringLiteral("Xerox"), QRegularExpression(
388+ "docuprint"
389+ "|docupage"
390+ "|phaser"
391+ "|workcentre"
392+ "|homecentre"
393+ )
394+ ),
395+ QPair<QString, QRegularExpression>(
396+ QStringLiteral("Lexmark"),
397+ QRegularExpression("optra|(:color )?jetprinter")
398+ ),
399+ QPair<QString, QRegularExpression>(
400+ QStringLiteral("KONICA MINOLTA"), QRegularExpression(
401+ "magicolor"
402+ "|pageworks"
403+ "|pagepro"
404+ )
405+ ),
406+ QPair<QString, QRegularExpression>(
407+ QStringLiteral("Kyocera"), QRegularExpression(
408+ "fs-"
409+ "|km-"
410+ "|taskalfa"
411+ )
412+ ),
413+ QPair<QString, QRegularExpression>(
414+ QStringLiteral("Ricoh"), QRegularExpression("aficio")
415+ ),
416+ QPair<QString, QRegularExpression>(
417+ QStringLiteral("Oce"), QRegularExpression("varioprint")
418+ ),
419+ QPair<QString, QRegularExpression>(
420+ QStringLiteral("Oki"), QRegularExpression("okipage|microline")
421+ ),
422+});
423+
424+const QMap<QString, QString> PpdUtils::hpByModel = QMap<QString, QString>{
425+ {"dj", "DeskJet"},
426+ {"lj", "LaserJet"},
427+ {"laserjet", "LaserJet"},
428+ {"oj", "OfficeJet"},
429+ {"officejet", "OfficeJet"},
430+ {"color lj", "Color LaserJet"},
431+ {"ps ", "PhotoSmart"},
432+ {"hp ", ""},
433+};
434+
435+QString PpdUtils::dedupeMakeModel(const QString &str)
436+{
437+ QSet<QString> stringSet;
438+ QStringList deduped;
439+ Q_FOREACH(auto m, str.split(" ")) {
440+ if (!stringSet.contains(m)) {
441+ deduped << m;
442+ }
443+ stringSet << m;
444+ }
445+ return deduped.join(" ");
446+}
447+
448+QPair<QString, QString> PpdUtils::splitMakeModel(const QString &makeModel)
449+{
450+ const QString mm = dedupeMakeModel(makeModel);
451+ const QString makeModelLower = mm.toLower();
452+
453+ QString make;
454+ QString model;
455+
456+ /* Informs us whether or not the make part was produced in such a way that
457+ it might need cleaning. I.e. we've made a guess at what the make is. */
458+ bool cleanupMake = false;
459+
460+ /* If the string starts with a known model name (like "LaserJet") assume
461+ that the manufacturer name is missing and add the manufacturer name
462+ corresponding to the model name */
463+ Q_FOREACH(auto manufacturerByModel, PpdUtils::manufacturerByModels) {
464+ if (manufacturerByModel.second.match(makeModelLower).hasMatch()) {
465+ make = manufacturerByModel.first;
466+ model = mm;
467+ }
468+ }
469+
470+ /* Take the first word as the name of the manufacturer.
471+
472+ Note that we have skipped a bunch of special cases where the first word
473+ isn't necessarily the manufacturer, hence the next TODO.
474+
475+ TODO: Handle special cases for TurboPrint, lexmark international,
476+ fuji xerox, etc. */
477+ int firstSpace = mm.indexOf(" ");
478+ if (make.isEmpty() && firstSpace > 0) {
479+ make = mm.left(firstSpace);
480+ model = mm.mid(make.size() + 1);
481+
482+ /* We've picked the first part as the make, but it may be that we've
483+ split "hewlett packard" and are left with "hewlett". */
484+ cleanupMake = true;
485+ }
486+
487+ // Standardised names for manufacturers.
488+ const QString makeLower = make.toLower();
489+ if (cleanupMake) {
490+ if (makeLower.startsWith("hewlett") && makeLower.endsWith("packard")) {
491+ make = "HP";
492+ // makeLower = "hp"
493+ } else if (makeLower.startsWith("konica") &&
494+ makeLower.endsWith("minolta")) {
495+ make = "KONICA MINOLTA";
496+ // makeLower = "konica minolta"
497+ } else {
498+ // Fix case errors.
499+ Q_FOREACH(auto manufacturerByModel, manufacturerByModels) {
500+ if (makeLower == manufacturerByModel.first.toLower()) {
501+ make = manufacturerByModel.first;
502+ break;
503+ }
504+ }
505+ }
506+ }
507+
508+ // Remove the word "Series" if present.
509+ model = model.remove(ignoreSeries);
510+
511+ /* Find a version number and cut the model from there. Note that some
512+ models may legitimately look like version numbers. So we cut only if the
513+ version number has max one digit, or a dot with digits before and after. */
514+ QString modelLower = model.toLower();
515+ int versionAt = modelLower.indexOf(" v");
516+ if (versionAt >= 0) {
517+ // Look for v or ver. followed by digits and dots.
518+ auto match = versionNumbers.match(modelLower);
519+ if (match.hasMatch()) {
520+ int matchAt = match.capturedStart();
521+ model.truncate(matchAt);
522+ modelLower.truncate(matchAt);
523+ }
524+ }
525+
526+ // Remove ignored suffixes.
527+ auto suffixMatch = ignoredSuffixes.match(modelLower);
528+ if (suffixMatch.hasMatch()) {
529+ int matchAt = suffixMatch.capturedStart();
530+ model.truncate(matchAt);
531+ modelLower.truncate(matchAt);
532+ }
533+
534+ // Replace lj or dj with LaserJet and DeskJet respectively, etc.
535+ if (makeLower == "hp") {
536+ Q_FOREACH(auto name, hpByModel.keys()) {
537+ if (modelLower.contains(name)) {
538+ int start = modelLower.indexOf(name);
539+ model.remove(start, name.size());
540+ model.insert(start, hpByModel[name]);
541+ modelLower = model.toLower();
542+ }
543+ }
544+ }
545+
546+ return QPair<QString, QString>(make, model);
547+}
548+
549+QString PpdUtils::normalize(const QString &str)
550+{
551+ QString strLower = str.trimmed().toLower();
552+ QString normalized;
553+
554+ const int BLANK = 0;
555+ const int ALPHA = 1;
556+ const int DIGIT = 2;
557+ int lastchar = BLANK;
558+
559+ bool alnumfound = false;
560+
561+ for (int i = 0; i < strLower.size(); i++) {
562+ if (strLower[i].isLetter()) {
563+ if (lastchar != ALPHA && alnumfound) {
564+ normalized += " ";
565+ }
566+ lastchar = ALPHA;
567+ } else if (strLower[i].isDigit()) {
568+ if (lastchar != DIGIT && alnumfound) {
569+ normalized += " ";
570+ }
571+ lastchar = DIGIT;
572+ } else {
573+ lastchar = BLANK;
574+ }
575+
576+ if (strLower[i].isLetterOrNumber()) {
577+ normalized += strLower[i];
578+ alnumfound = true;
579+ }
580+ }
581+ return normalized;
582+}
583+
584+PrinterEnum::DuplexMode PpdUtils::ppdChoiceToDuplexMode(const QString &choice)
585+{
586+ if (choice == "DuplexTumble")
587+ return PrinterEnum::DuplexMode::DuplexShortSide;
588+ else if (choice == "DuplexNoTumble")
589+ return PrinterEnum::DuplexMode::DuplexLongSide;
590+ else
591+ return PrinterEnum::DuplexMode::DuplexNone;
592+}
593+
594+const QString PpdUtils::duplexModeToPpdChoice(
595+ const PrinterEnum::DuplexMode &mode)
596+{
597+ switch (mode) {
598+ case PrinterEnum::DuplexMode::DuplexShortSide:
599+ return "DuplexTumble";
600+ case PrinterEnum::DuplexMode::DuplexLongSide:
601+ return "DuplexNoTumble";
602+ case PrinterEnum::DuplexMode::DuplexNone:
603+ default:
604+ return "None";
605+ }
606+}
607+
608+
609+ColorModel PpdUtils::parsePpdColorModel(const QString &name,
610+ const QString &text,
611+ const QString &optionName)
612+{
613+ ColorModel ret;
614+ ret.name = name;
615+ ret.text = text;
616+ ret.originalOption = optionName;
617+
618+ if (ret.name.contains("Gray") || ret.name.contains("Black")) {
619+ ret.colorType = PrinterEnum::ColorModelType::GrayType;
620+ } else {
621+ ret.colorType = PrinterEnum::ColorModelType::ColorType;
622+ }
623+ return ret;
624+}
625+
626+PrintQuality PpdUtils::parsePpdPrintQuality(const QString &choice,
627+ const QString &text,
628+ const QString &optionName)
629+{
630+ PrintQuality quality;
631+ quality.name = choice;
632+ quality.text = text;
633+ quality.originalOption = optionName;
634+ return quality;
635+}
636
637=== added file 'modules/Ubuntu/Components/Extras/Printers/cups/ppdutils.h'
638--- modules/Ubuntu/Components/Extras/Printers/cups/ppdutils.h 1970-01-01 00:00:00 +0000
639+++ modules/Ubuntu/Components/Extras/Printers/cups/ppdutils.h 2017-03-30 13:23:27 +0000
640@@ -0,0 +1,87 @@
641+/*
642+ * Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2014, 2015 Red Hat, Inc.
643+ * Copyright (C) 2006 Florian Festi <ffesti@redhat.com>
644+ * Copyright (C) 2006, 2007, 2008, 2009 Tim Waugh <twaugh@redhat.com>
645+ * Copyright (C) 2017 Canonical, Ltd.
646+ *
647+ * This program is free software; you can redistribute it and/or modify
648+ * it under the terms of the GNU Lesser General Public License as published by
649+ * the Free Software Foundation; version 3.
650+ *
651+ * This program is distributed in the hope that it will be useful,
652+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
653+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
654+ * GNU Lesser General Public License for more details.
655+ *
656+ * You should have received a copy of the GNU Lesser General Public License
657+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
658+ */
659+
660+#ifndef USC_PRINTERS_CUPS_PPDUTILS_H
661+#define USC_PRINTERS_CUPS_PPDUTILS_H
662+
663+#include "enums.h"
664+#include "i18n.h"
665+#include "printers_global.h"
666+#include "structs.h"
667+
668+#include <cups/ppd.h>
669+
670+#include <QList>
671+#include <QMap>
672+#include <QPair>
673+#include <QRegularExpression>
674+#include <QString>
675+
676+struct PRINTERS_DECL_EXPORT PpdUtils
677+{
678+private:
679+ static const QList<QPair<QString, QRegularExpression>> manufacturerByModels;
680+ static const QRegularExpression ignoredSuffixes;
681+ static const QRegularExpression versionNumbers;
682+ static const QRegularExpression ignoreSeries;
683+ static const QMap<QString, QString> hpByModel;
684+ static QString dedupeMakeModel(const QString &str);
685+public:
686+
687+ /**
688+ Split a ppd-make-and-model string into a canonical make and model pair.
689+ Returns a string pair representing the make and the model.
690+ */
691+ static QPair<QString, QString> splitMakeModel(const QString &makeModel);
692+
693+ /**
694+ This function normalizes manufacturer and model names for comparing.
695+ The string is turned to lower case and leading and trailing white
696+ space is removed. After that each sequence of non-alphanumeric
697+ characters (including white space) is replaced by a single space and
698+ also at each change between letters and numbers a single space is added.
699+ This makes the comparison only done by alphanumeric characters and the
700+ words formed from them. So mostly two strings which sound the same when
701+ you pronounce them are considered equal. Printer manufacturers do not
702+ market two models whose names sound the same but differ only by
703+ upper/lower case, spaces, dashes, ..., but in printer drivers names can
704+ be easily supplied with these details of the name written in the wrong
705+ way, especially if the IEEE-1284 device ID of the printer is not known.
706+ This way we get a very reliable matching of printer model names.
707+ Examples:
708+ - Epson PM-A820 -> epson pm a 820
709+ - Epson PM A820 -> epson pm a 820
710+ - HP PhotoSmart C 8100 -> hp photosmart c 8100
711+ - hp Photosmart C8100 -> hp photosmart c 8100
712+ */
713+ static QString normalize(const QString &str);
714+
715+ static PrinterEnum::DuplexMode ppdChoiceToDuplexMode(
716+ const QString &choice);
717+ static const QString duplexModeToPpdChoice(
718+ const PrinterEnum::DuplexMode &mode);
719+ static ColorModel parsePpdColorModel(const QString &name,
720+ const QString &text,
721+ const QString &optionName);
722+ static PrintQuality parsePpdPrintQuality(const QString &choice,
723+ const QString &text,
724+ const QString &optionName);
725+};
726+
727+#endif // USC_PRINTERS_CUPS_PPDUTILS_H
728
729=== modified file 'modules/Ubuntu/Components/Extras/Printers/models/devicemodel.cpp'
730--- modules/Ubuntu/Components/Extras/Printers/models/devicemodel.cpp 2017-03-09 14:34:05 +0000
731+++ modules/Ubuntu/Components/Extras/Printers/models/devicemodel.cpp 2017-03-30 13:23:27 +0000
732@@ -15,9 +15,13 @@
733 */
734
735 #include "backend/backend_cups.h"
736+#include "cups/ppdutils.h"
737 #include "models/devicemodel.h"
738
739 #include <QDebug>
740+#include <QPair>
741+#include <QQmlEngine>
742+#include <QSet>
743
744 DeviceModel::DeviceModel(PrinterBackend *backend, QObject *parent)
745 : QAbstractListModel(parent)
746@@ -116,8 +120,16 @@
747
748 bool DeviceModel::deviceWanted(const Device &device)
749 {
750+ bool wanted = true;
751+
752+ // Skip non-uris.
753 auto parts = device.uri.split(":", QString::SkipEmptyParts);
754- return parts.size() > 1;
755+ wanted = wanted && parts.size() > 1;
756+
757+ // Skip "Unknown" make-models.
758+ wanted = wanted && device.makeModel != QStringLiteral("Unknown");
759+
760+ return wanted;
761 }
762
763 void DeviceModel::clear()
764@@ -147,3 +159,206 @@
765 m_isSearching = false;
766 Q_EMIT searchingChanged();
767 }
768+
769+bool DeviceModel::searching()
770+{
771+ return m_isSearching;
772+}
773+
774+DeviceFilter::DeviceFilter(QObject *parent) : QSortFilterProxyModel(parent)
775+{
776+ connect(this, SIGNAL(sourceModelChanged()), SLOT(onSourceModelChanged()));
777+}
778+
779+DeviceFilter::~DeviceFilter()
780+{
781+}
782+
783+void DeviceFilter::onSourceModelChanged()
784+{
785+ connect(
786+ (DeviceModel*) sourceModel(), SIGNAL(countChanged()),
787+ this, SIGNAL(countChanged())
788+ );
789+}
790+
791+void DeviceFilter::onSourceModelCountChanged()
792+{
793+ Q_EMIT countChanged();
794+}
795+
796+int DeviceFilter::count() const
797+{
798+ return rowCount();
799+}
800+
801+void DeviceFilter::filterOnNormalizedMakeModel(
802+ const QString &normalizedMakeModel)
803+{
804+ m_normalizedMakeModel = normalizedMakeModel;
805+ m_normalizedMakeModelFilterEnabled = true;
806+ invalidate();
807+}
808+
809+void DeviceFilter::filterOnUri(const QString &uri)
810+{
811+ m_uri = uri;
812+ m_uriFilterEnabled = true;
813+ invalidate();
814+}
815+
816+bool DeviceFilter::filterAcceptsRow(int sourceRow,
817+ const QModelIndex &sourceParent) const
818+{
819+ bool accepts = true;
820+ QModelIndex childIndex = sourceModel()->index(sourceRow, 0, sourceParent);
821+
822+ if (accepts && m_normalizedMakeModelFilterEnabled) {
823+ QString makeModel = childIndex.model()->data(
824+ childIndex, DeviceModel::MakeModelRole).toString();
825+ accepts = m_normalizedMakeModel == PpdUtils::normalize(makeModel);
826+ }
827+
828+ if (accepts && m_uriFilterEnabled) {
829+ QString uri = childIndex.model()->data(
830+ childIndex, DeviceModel::UriRole).toString();
831+ accepts = m_uri == uri;
832+ }
833+
834+ return accepts;
835+}
836+
837+ConsolidatedDeviceModel::ConsolidatedDeviceModel(DeviceModel *deviceModel,
838+ QObject *parent)
839+ : QAbstractListModel(parent)
840+ , m_deviceModel(deviceModel)
841+{
842+ connect(m_deviceModel, SIGNAL(searchingChanged()),
843+ this, SLOT(deviceModelSearchChanged()));
844+}
845+
846+ConsolidatedDeviceModel::~ConsolidatedDeviceModel()
847+{
848+}
849+
850+int ConsolidatedDeviceModel::rowCount(const QModelIndex &parent) const
851+{
852+ Q_UNUSED(parent);
853+ return m_consolidatedDevices.size();
854+}
855+
856+int ConsolidatedDeviceModel::count() const
857+{
858+ return rowCount();
859+}
860+
861+bool ConsolidatedDeviceModel::searching()
862+{
863+ return m_deviceModel->searching();
864+}
865+
866+bool ConsolidatedDeviceModel::makeModelIsConsolidateable(
867+ const QString &makeModel) const
868+{
869+ return !makeModel.isEmpty() && makeModel != QStringLiteral("Unknown");
870+}
871+
872+void ConsolidatedDeviceModel::deviceModelSearchChanged()
873+{
874+ int oldCount = rowCount();
875+
876+ beginResetModel();
877+ m_consolidatedDevices.clear();
878+
879+ /* Device model has finished searching, so let's sync our model with it.
880+ Note that if the deviceModel is searching, we simply dump our model. */
881+ if (!m_deviceModel->searching()) {
882+ // A set of normalized make-models.
883+ QSet<QString> normalizedMakeModels;
884+
885+ // Used to preserve makeModels after it has been normalized.
886+ QMap<QString, QString> normalizedToMakeModel;
887+ for (int i = 0; i < m_deviceModel->rowCount(); i++) {
888+ QModelIndex idx = m_deviceModel->index(i, 0);
889+ QString makeModel = m_deviceModel->data(
890+ idx, DeviceModel::MakeModelRole
891+ ).toString();
892+
893+ // We can't do anything about a make-model-less device.
894+ if (makeModel.isEmpty()) {
895+ continue;
896+ }
897+
898+ if (makeModelIsConsolidateable(makeModel)) {
899+ QString normalized = PpdUtils::normalize(makeModel);
900+ normalizedMakeModels << normalized;
901+ normalizedToMakeModel[normalized] = makeModel;
902+ }
903+ }
904+
905+ /* For every make-and-model, create a proxy model, and install a filter
906+ on it. Then do the same for those consolidated under URI (i.e. not
907+ consolidated).
908+ Note that a parent is set on each filter so that it is cleaned up when
909+ we the consolidated model is destroyed. */
910+ Q_FOREACH(auto mm, normalizedMakeModels) {
911+ auto filter = new DeviceFilter(this);
912+ filter->setSourceModel(m_deviceModel);
913+ filter->filterOnNormalizedMakeModel(mm);
914+
915+ QPair<QString, DeviceFilter*> p(mm, filter);
916+ m_consolidatedDevices << p;
917+ }
918+ }
919+
920+ endResetModel();
921+
922+ if (oldCount != rowCount()) {
923+ Q_EMIT countChanged();
924+ }
925+ Q_EMIT searchingChanged();
926+}
927+
928+QVariant ConsolidatedDeviceModel::data(const QModelIndex &index, int role) const
929+{
930+ QVariant ret;
931+
932+ if ((0 <= index.row()) && (index.row() < m_consolidatedDevices.size())) {
933+
934+ auto consolidated = m_consolidatedDevices[index.row()];
935+ auto mm = consolidated.first;
936+ auto filter = consolidated.second;
937+
938+ switch (role) {
939+ case Qt::DisplayRole:
940+ case ConsolidatedNameRole:
941+ ret = filter->data(
942+ filter->index(0, 0), DeviceModel::Roles::MakeModelRole
943+ ).toString();
944+ break;
945+ case DevicesRole: {
946+ ret = QVariant::fromValue(filter);
947+ break;
948+ case ConnectionsCountRole:
949+ ret = filter->count();
950+ break;
951+ }
952+ }
953+ }
954+
955+ return ret;
956+}
957+
958+QHash<int, QByteArray> ConsolidatedDeviceModel::roleNames() const
959+{
960+ static QHash<int,QByteArray> names;
961+
962+ if (Q_UNLIKELY(names.empty())) {
963+ names[Qt::DisplayRole] = "displayName";
964+ names[ConsolidatedNameRole] = "consolidatedName";
965+ names[DevicesRole] = "devices";
966+ names[ConnectionsCountRole] = "connectionsCount";
967+ }
968+
969+ return names;
970+}
971
972=== modified file 'modules/Ubuntu/Components/Extras/Printers/models/devicemodel.h'
973--- modules/Ubuntu/Components/Extras/Printers/models/devicemodel.h 2017-03-09 14:34:05 +0000
974+++ modules/Ubuntu/Components/Extras/Printers/models/devicemodel.h 2017-03-30 13:23:27 +0000
975@@ -31,7 +31,7 @@
976 {
977 Q_OBJECT
978 Q_PROPERTY(int count READ count NOTIFY countChanged)
979- Q_PROPERTY(bool searching MEMBER m_isSearching NOTIFY searchingChanged)
980+ Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
981 public:
982 explicit DeviceModel(PrinterBackend *backend, QObject *parent = Q_NULLPTR);
983 ~DeviceModel();
984@@ -56,6 +56,7 @@
985 int count() const;
986 void load();
987 void clear();
988+ bool searching();
989
990 private Q_SLOTS:
991 void deviceLoaded(const Device &device);
992@@ -75,4 +76,82 @@
993 bool m_isSearching;
994 };
995
996+
997+class PRINTERS_DECL_EXPORT DeviceFilter : public QSortFilterProxyModel
998+{
999+ Q_OBJECT
1000+ Q_PROPERTY(int count READ count NOTIFY countChanged)
1001+public:
1002+ explicit DeviceFilter(QObject *parent = Q_NULLPTR);
1003+ ~DeviceFilter();
1004+
1005+ void filterOnNormalizedMakeModel(const QString &normalizedMakeModel);
1006+ void filterOnUri(const QString &uri);
1007+ int count() const;
1008+protected:
1009+ virtual bool filterAcceptsRow(
1010+ int sourceRow, const QModelIndex &sourceParent) const override;
1011+
1012+Q_SIGNALS:
1013+ void countChanged();
1014+
1015+private Q_SLOTS:
1016+ void onSourceModelChanged();
1017+ void onSourceModelCountChanged();
1018+
1019+private:
1020+ QString m_normalizedMakeModel = QString::null;
1021+ bool m_normalizedMakeModelFilterEnabled = false;
1022+
1023+ QString m_uri = QString::null;
1024+ bool m_uriFilterEnabled = false;
1025+};
1026+
1027+/* The ConsolidatedDeviceModel is a model of ConsolidatedDevices. A
1028+ConsolidatedDevice is a set of Devices that share a printer name (assuming
1029+unique printer names).
1030+
1031+TODO: The DeviceModel should implement the consolidation by use of e.g. a
1032+table model, or tree model. This model can then be deleted. */
1033+class PRINTERS_DECL_EXPORT ConsolidatedDeviceModel : public QAbstractListModel
1034+{
1035+ Q_OBJECT
1036+ Q_PROPERTY(int count READ count NOTIFY countChanged)
1037+ Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged)
1038+public:
1039+ explicit ConsolidatedDeviceModel(DeviceModel *deviceModel,
1040+ QObject *parent = Q_NULLPTR);
1041+ ~ConsolidatedDeviceModel();
1042+
1043+ enum Roles
1044+ {
1045+ // Qt::DisplayRole holds consolidated device name
1046+ DevicesRole = Qt::UserRole,
1047+ ConsolidatedNameRole,
1048+ ConnectionsCountRole,
1049+ LastRole = ConnectionsCountRole,
1050+ };
1051+
1052+ virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override;
1053+ virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
1054+ virtual QHash<int, QByteArray> roleNames() const override;
1055+
1056+ int count() const;
1057+ void load();
1058+ bool searching();
1059+ bool makeModelIsConsolidateable(const QString &makeModel) const;
1060+
1061+private Q_SLOTS:
1062+ void deviceModelSearchChanged();
1063+
1064+Q_SIGNALS:
1065+ void countChanged();
1066+ void searchingChanged();
1067+
1068+private:
1069+ DeviceModel *m_deviceModel;
1070+
1071+ QList<QPair<QString, DeviceFilter*>> m_consolidatedDevices;
1072+};
1073+
1074 #endif // USC_PRINTER_DEVICEMODEL_H
1075
1076=== modified file 'modules/Ubuntu/Components/Extras/Printers/printer/printer.cpp'
1077--- modules/Ubuntu/Components/Extras/Printers/printer/printer.cpp 2017-03-21 13:01:42 +0000
1078+++ modules/Ubuntu/Components/Extras/Printers/printer/printer.cpp 2017-03-30 13:23:27 +0000
1079@@ -14,6 +14,7 @@
1080 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1081 */
1082
1083+#include "cups/ppdutils.h"
1084 #include "i18n.h"
1085 #include "utils.h"
1086
1087@@ -291,7 +292,7 @@
1088 return;
1089 }
1090
1091- QStringList vals({Utils::duplexModeToPpdChoice(duplexMode)});
1092+ QStringList vals({PpdUtils::duplexModeToPpdChoice(duplexMode)});
1093 m_backend->printerAddOption(name(), "Duplex", vals);
1094 }
1095
1096
1097=== modified file 'modules/Ubuntu/Components/Extras/Printers/printer/printerjob.cpp'
1098--- modules/Ubuntu/Components/Extras/Printers/printer/printerjob.cpp 2017-03-16 15:47:11 +0000
1099+++ modules/Ubuntu/Components/Extras/Printers/printer/printerjob.cpp 2017-03-30 13:23:27 +0000
1100@@ -18,9 +18,9 @@
1101 #include <QUrl>
1102
1103 #include "backend/backend_cups.h"
1104+#include "cups/ppdutils.h"
1105 #include "models/printermodel.h"
1106 #include "printer/printerjob.h"
1107-#include "utils.h"
1108
1109 PrinterJob::PrinterJob(QString printerName,
1110 PrinterBackend *backend,
1111@@ -166,7 +166,7 @@
1112
1113 // No duplexMode will result in PrinterJob using defaultDuplexMode
1114 QString duplex = attributes.value("Duplex").toString();
1115- PrinterEnum::DuplexMode duplexMode = Utils::ppdChoiceToDuplexMode(duplex);
1116+ PrinterEnum::DuplexMode duplexMode = PpdUtils::ppdChoiceToDuplexMode(duplex);
1117 for (int i=0; i < m_printer->supportedDuplexModes().length(); i++) {
1118 if (m_printer->supportedDuplexModes().at(i) == duplexMode) {
1119 setDuplexMode(i);
1120
1121=== modified file 'modules/Ubuntu/Components/Extras/Printers/printers/printers.cpp'
1122--- modules/Ubuntu/Components/Extras/Printers/printers/printers.cpp 2017-03-21 14:51:38 +0000
1123+++ modules/Ubuntu/Components/Extras/Printers/printers/printers.cpp 2017-03-30 13:23:27 +0000
1124@@ -35,6 +35,7 @@
1125 : QObject(parent)
1126 , m_backend(backend)
1127 , m_devices(backend)
1128+ , m_consolidatedDevices(&m_devices)
1129 , m_drivers(backend)
1130 , m_model(backend)
1131 , m_jobs(backend)
1132@@ -259,6 +260,14 @@
1133 m_devices.load();
1134 }
1135
1136+QAbstractItemModel* Printers::consolidatedDevices()
1137+{
1138+ m_devices.load();
1139+ auto ret = &m_consolidatedDevices;
1140+ QQmlEngine::setObjectOwnership(ret, QQmlEngine::CppOwnership);
1141+ return ret;
1142+}
1143+
1144 bool Printers::addPrinter(const QString &name, const QString &ppd,
1145 const QString &device, const QString &description,
1146 const QString &location)
1147
1148=== modified file 'modules/Ubuntu/Components/Extras/Printers/printers/printers.h'
1149--- modules/Ubuntu/Components/Extras/Printers/printers/printers.h 2017-03-21 14:51:38 +0000
1150+++ modules/Ubuntu/Components/Extras/Printers/printers/printers.h 2017-03-30 13:23:27 +0000
1151@@ -42,6 +42,7 @@
1152 Q_PROPERTY(QAbstractItemModel* printJobs READ printJobs CONSTANT)
1153 Q_PROPERTY(QAbstractItemModel* drivers READ drivers CONSTANT)
1154 Q_PROPERTY(QAbstractItemModel* devices READ devices CONSTANT)
1155+ Q_PROPERTY(QAbstractItemModel* consolidatedDevices READ consolidatedDevices CONSTANT)
1156 Q_PROPERTY(QString driverFilter READ driverFilter WRITE setDriverFilter NOTIFY driverFilterChanged)
1157 Q_PROPERTY(QString defaultPrinterName READ defaultPrinterName WRITE setDefaultPrinterName NOTIFY defaultPrinterNameChanged)
1158 Q_PROPERTY(QString lastMessage READ lastMessage CONSTANT)
1159@@ -60,6 +61,7 @@
1160 QAbstractItemModel* printJobs();
1161 QAbstractItemModel* drivers();
1162 QAbstractItemModel* devices();
1163+ QAbstractItemModel* consolidatedDevices();
1164 QString driverFilter() const;
1165 QString defaultPrinterName() const;
1166 QString lastMessage() const;
1167@@ -111,6 +113,7 @@
1168 void provisionPrinter(const QString &name);
1169 PrinterBackend *m_backend;
1170 DeviceModel m_devices;
1171+ ConsolidatedDeviceModel m_consolidatedDevices;
1172 DriverModel m_drivers;
1173 PrinterModel m_model;
1174 JobModel m_jobs;
1175
1176=== modified file 'modules/Ubuntu/Components/Extras/Printers/structs.h'
1177--- modules/Ubuntu/Components/Extras/Printers/structs.h 2017-03-16 15:34:02 +0000
1178+++ modules/Ubuntu/Components/Extras/Printers/structs.h 2017-03-30 13:23:27 +0000
1179@@ -163,8 +163,6 @@
1180 }
1181 };
1182
1183-
1184-
1185 Q_DECLARE_TYPEINFO(ColorModel, Q_PRIMITIVE_TYPE);
1186 Q_DECLARE_METATYPE(ColorModel)
1187
1188
1189=== modified file 'modules/Ubuntu/Components/Extras/Printers/utils.h'
1190--- modules/Ubuntu/Components/Extras/Printers/utils.h 2017-02-21 10:46:29 +0000
1191+++ modules/Ubuntu/Components/Extras/Printers/utils.h 2017-03-30 13:23:27 +0000
1192@@ -19,39 +19,14 @@
1193
1194 #include "enums.h"
1195 #include "i18n.h"
1196-#include "structs.h"
1197-
1198-#include <cups/ppd.h>
1199-
1200+
1201+#include <QMap>
1202+#include <QPrinter>
1203 #include <QString>
1204-#include <QPrinter>
1205
1206 class Utils
1207 {
1208 public:
1209- static PrinterEnum::DuplexMode ppdChoiceToDuplexMode(const QString &choice)
1210- {
1211- if (choice == "DuplexTumble")
1212- return PrinterEnum::DuplexMode::DuplexShortSide;
1213- else if (choice == "DuplexNoTumble")
1214- return PrinterEnum::DuplexMode::DuplexLongSide;
1215- else
1216- return PrinterEnum::DuplexMode::DuplexNone;
1217- }
1218-
1219- static const QString duplexModeToPpdChoice(const PrinterEnum::DuplexMode &mode)
1220- {
1221- switch (mode) {
1222- case PrinterEnum::DuplexMode::DuplexShortSide:
1223- return "DuplexTumble";
1224- case PrinterEnum::DuplexMode::DuplexLongSide:
1225- return "DuplexNoTumble";
1226- case PrinterEnum::DuplexMode::DuplexNone:
1227- default:
1228- return "None";
1229- }
1230- }
1231-
1232 static const QString duplexModeToUIString(const PrinterEnum::DuplexMode &mode)
1233 {
1234 switch (mode) {
1235@@ -79,31 +54,55 @@
1236 }
1237 }
1238
1239- static ColorModel parsePpdColorModel(const QString &name, const QString &text,
1240- const QString &optionName)
1241- {
1242- ColorModel ret;
1243- ret.name = name;
1244- ret.text = text;
1245- ret.originalOption = optionName;
1246-
1247- if (ret.name.contains("Gray") || ret.name.contains("Black")) {
1248- ret.colorType = PrinterEnum::ColorModelType::GrayType;
1249- } else {
1250- ret.colorType = PrinterEnum::ColorModelType::ColorType;
1251- }
1252- return ret;
1253- }
1254-
1255- static PrintQuality parsePpdPrintQuality(const QString &choice,
1256- const QString &text,
1257- const QString &optionName)
1258- {
1259- PrintQuality quality;
1260- quality.name = choice;
1261- quality.text = text;
1262- quality.originalOption = optionName;
1263- return quality;
1264+ // Parse an IEEE 1284 Device ID, so that it may be indexed by field name.
1265+ static QMap<QString, QString> parseDeviceId(const QString &did)
1266+ {
1267+ QMap<QString, QString> idDict;
1268+ const auto parts = did.split(";");
1269+ const auto col = QStringLiteral(":");
1270+
1271+ Q_FOREACH(auto part, parts) {
1272+ if (!part.contains(col)) {
1273+ continue;
1274+ }
1275+ auto nameValue = part.split(col);
1276+ idDict[nameValue[0].trimmed()] = nameValue[1].trimmed();
1277+ }
1278+
1279+ const auto man = QStringLiteral("MANUFACTURER");
1280+ const auto mfg = QStringLiteral("MFG");
1281+ if (idDict.contains(man)) {
1282+ idDict.insert(mfg, idDict.value(mfg, idDict.value(man)));
1283+ }
1284+
1285+ const auto mod = QStringLiteral("MODEL");
1286+ const auto mdl = QStringLiteral("MDL");
1287+ if (idDict.contains(mod)) {
1288+ idDict.insert(mdl, idDict.value(mdl, idDict.value(mod)));
1289+ }
1290+
1291+ const auto com = QStringLiteral("COMMAND SET");
1292+ const auto cmd = QStringLiteral("CMD");
1293+ if (idDict.contains(com)) {
1294+ idDict.insert(cmd, idDict.value(cmd, idDict.value(com)));
1295+ }
1296+
1297+ const auto defaultNames = QStringList({
1298+ mfg, mdl, cmd,
1299+ QStringLiteral("CLS"),
1300+ QStringLiteral("DES"),
1301+ QStringLiteral("SN"),
1302+ QStringLiteral("S"),
1303+ QStringLiteral("P"),
1304+ QStringLiteral("J"),
1305+ });
1306+ Q_FOREACH(auto name, defaultNames) {
1307+ idDict.insert(name, idDict.value(name, QString::null));
1308+ }
1309+
1310+ // TODO: We'd make an array of CMD, but we don't currently need it.
1311+
1312+ return idDict;
1313 }
1314 };
1315
1316
1317=== modified file 'po/ubuntu-ui-extras.pot'
1318--- po/ubuntu-ui-extras.pot 2017-03-21 14:51:38 +0000
1319+++ po/ubuntu-ui-extras.pot 2017-03-30 13:23:27 +0000
1320@@ -8,7 +8,7 @@
1321 msgstr ""
1322 "Project-Id-Version: ubuntu-ui-extras\n"
1323 "Report-Msgid-Bugs-To: \n"
1324-"POT-Creation-Date: 2017-03-21 15:33+0100\n"
1325+"POT-Creation-Date: 2017-03-30 15:15+0200\n"
1326 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1327 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1328 "Language-Team: LANGUAGE <LL@li.org>\n"
1329@@ -55,7 +55,7 @@
1330 msgid "Idle"
1331 msgstr ""
1332
1333-#: modules/Ubuntu/Components/Extras/Printers/utils.h:61
1334+#: modules/Ubuntu/Components/Extras/Printers/utils.h:36
1335 msgid "Long Edge (Standard)"
1336 msgstr ""
1337
1338@@ -67,7 +67,7 @@
1339 msgid "Normal"
1340 msgstr ""
1341
1342-#: modules/Ubuntu/Components/Extras/Printers/utils.h:64
1343+#: modules/Ubuntu/Components/Extras/Printers/utils.h:39
1344 msgid "One Sided"
1345 msgstr ""
1346
1347@@ -91,7 +91,7 @@
1348 msgid "Rotate"
1349 msgstr ""
1350
1351-#: modules/Ubuntu/Components/Extras/Printers/utils.h:59
1352+#: modules/Ubuntu/Components/Extras/Printers/utils.h:34
1353 msgid "Short Edge (Flip)"
1354 msgstr ""
1355
1356@@ -99,7 +99,7 @@
1357 msgid "Stopped"
1358 msgstr ""
1359
1360-#: modules/Ubuntu/Components/Extras/Printers/printers/printers.cpp:387
1361+#: modules/Ubuntu/Components/Extras/Printers/printers/printers.cpp:396
1362 msgid "Test page"
1363 msgstr ""
1364
1365
1366=== modified file 'tests/unittests/Printers/CMakeLists.txt'
1367--- tests/unittests/Printers/CMakeLists.txt 2017-03-10 10:28:20 +0000
1368+++ tests/unittests/Printers/CMakeLists.txt 2017-03-30 13:23:27 +0000
1369@@ -56,3 +56,19 @@
1370 add_executable(testPrintersDeviceModel tst_printerdevicemodel.cpp ${MOCK_SOURCES})
1371 target_link_libraries(testPrintersDeviceModel UbuntuComponentsExtrasPrintersQml Qt5::Test Qt5::Gui)
1372 add_test(tst_printerdevicemodel testPrintersDeviceModel)
1373+
1374+add_executable(testPrintersPpdUtils tst_ppdutils.cpp)
1375+target_link_libraries(testPrintersPpdUtils UbuntuComponentsExtrasPrintersQml Qt5::Test Qt5::Gui)
1376+add_test(tst_ppdutils testPrintersPpdUtils)
1377+
1378+add_executable(testPrintersUtils tst_utils.cpp)
1379+target_link_libraries(testPrintersUtils UbuntuComponentsExtrasPrintersQml Qt5::Test Qt5::Gui)
1380+add_test(tst_utils testPrintersUtils)
1381+
1382+add_executable(testPrintersDeviceFilter tst_devicefilter.cpp ${MOCK_SOURCES})
1383+target_link_libraries(testPrintersDeviceFilter UbuntuComponentsExtrasPrintersQml Qt5::Test Qt5::Gui)
1384+add_test(tst_devicefilter testPrintersDeviceFilter)
1385+
1386+add_executable(testPrintersConsolidatedDeviceModel tst_consolidateddevicemodel.cpp ${MOCK_SOURCES})
1387+target_link_libraries(testPrintersConsolidatedDeviceModel UbuntuComponentsExtrasPrintersQml Qt5::Test Qt5::Gui)
1388+add_test(tst_consolidateddevicemodel testPrintersConsolidatedDeviceModel)
1389
1390=== modified file 'tests/unittests/Printers/mockbackend.h'
1391--- tests/unittests/Printers/mockbackend.h 2017-03-15 17:42:21 +0000
1392+++ tests/unittests/Printers/mockbackend.h 2017-03-30 13:23:27 +0000
1393@@ -18,7 +18,7 @@
1394 #define USC_PRINTERS_MOCK_BACKEND_H
1395
1396 #include "backend/backend.h"
1397-#include "utils.h"
1398+#include "cups/ppdutils.h"
1399
1400 class MockPrinterBackend : public PrinterBackend
1401 {
1402@@ -235,7 +235,7 @@
1403 attributes.insert("ColorModel", job->getColorModel().name);
1404 attributes.insert("CompletedTime", job->completedTime());
1405 attributes.insert("CreationTime", job->creationTime());
1406- attributes.insert("Duplex", Utils::duplexModeToPpdChoice(job->getDuplexMode()));
1407+ attributes.insert("Duplex", PpdUtils::duplexModeToPpdChoice(job->getDuplexMode()));
1408 attributes.insert("impressionsCompleted", job->impressionsCompleted());
1409 attributes.insert("landscape", job->landscape());
1410 attributes.insert("messages", job->messages());
1411@@ -322,7 +322,7 @@
1412 virtual PrinterEnum::DuplexMode defaultDuplexMode() const override
1413 {
1414 auto ppdMode = printerGetOption(printerName(), "Duplex").toString();
1415- return Utils::ppdChoiceToDuplexMode(ppdMode);
1416+ return PpdUtils::ppdChoiceToDuplexMode(ppdMode);
1417 }
1418
1419 virtual QList<PrinterEnum::DuplexMode> supportedDuplexModes() const override
1420
1421=== added file 'tests/unittests/Printers/tst_consolidateddevicemodel.cpp'
1422--- tests/unittests/Printers/tst_consolidateddevicemodel.cpp 1970-01-01 00:00:00 +0000
1423+++ tests/unittests/Printers/tst_consolidateddevicemodel.cpp 2017-03-30 13:23:27 +0000
1424@@ -0,0 +1,162 @@
1425+/*
1426+ * Copyright (C) 2017 Canonical, Ltd.
1427+ *
1428+ * This program is free software; you can redistribute it and/or modify
1429+ * it under the terms of the GNU Lesser General Public License as published by
1430+ * the Free Software Foundation; version 3.
1431+ *
1432+ * This program is distributed in the hope that it will be useful,
1433+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1434+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1435+ * GNU Lesser General Public License for more details.
1436+ *
1437+ * You should have received a copy of the GNU Lesser General Public License
1438+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1439+ */
1440+
1441+#include "mockbackend.h"
1442+
1443+#include "backend/backend.h"
1444+#include "models/devicemodel.h"
1445+#include "structs.h"
1446+
1447+#include <QDebug>
1448+#include <QObject>
1449+#include <QSignalSpy>
1450+#include <QTest>
1451+
1452+Q_DECLARE_METATYPE(ConsolidatedDeviceModel::Roles)
1453+
1454+class TestConsolidatedDeviceModel : public QObject
1455+{
1456+ Q_OBJECT
1457+private Q_SLOTS:
1458+ void init()
1459+ {
1460+ m_backend = new MockPrinterBackend();
1461+ m_devicesModel = new DeviceModel(m_backend);
1462+ }
1463+ void cleanup()
1464+ {
1465+ QSignalSpy destroyedSpy(m_devicesModel, SIGNAL(destroyed(QObject*)));
1466+ m_devicesModel->deleteLater();
1467+ QTRY_COMPARE(destroyedSpy.count(), 1);
1468+ delete m_backend;
1469+ }
1470+ void testConsolidation_data()
1471+ {
1472+ QTest::addColumn<QList<Device>>("devices");
1473+ QTest::addColumn<int>("targetConsolidatedDeviceCount");
1474+
1475+ {
1476+ Device a; a.uri = "dnssd://foo/bar"; a.makeModel = "foo";
1477+ Device b; b.uri = "ipps://bar/qux"; b.makeModel = "bar";
1478+
1479+ QList<Device> devs({a, b});
1480+ QTest::newRow("no consolidation") << devs << 2;
1481+ }
1482+ {
1483+ Device a; a.uri = "dnssd://foo/bar"; a.makeModel = "foo";
1484+ Device b; b.uri = "ipps://foo/bar"; b.makeModel = "foo";
1485+
1486+ QList<Device> devs({a, b});
1487+ QTest::newRow("consolidation") << devs << 1;
1488+ }
1489+ {
1490+ Device a; a.uri = "dnssd://foo/bar"; a.makeModel = "foo";
1491+ Device b; b.uri = "ipps://foo/bar"; b.makeModel = "foo";
1492+ Device c; c.uri = "socket://baz/qux"; c.makeModel = "baz";
1493+
1494+ QList<Device> devs({a, b, c});
1495+ QTest::newRow("mixed") << devs << 2;
1496+ }
1497+ {
1498+ Device a; a.uri = "dnssd://foo/bar"; a.makeModel = "foo";
1499+ Device b; b.uri = "ipps://foo/bar"; b.makeModel = "";
1500+ Device c; c.uri = "socket://baz/qux"; c.makeModel = "";
1501+
1502+ QList<Device> devs({a, b, c});
1503+ QTest::newRow("don't consolidate make-and-model-less devices") << devs << 1;
1504+ }
1505+ {
1506+ Device a; a.uri = "dnssd://foo/bar"; a.makeModel = "Unknown";
1507+ Device b; b.uri = "ipps://foo/bar"; b.makeModel = "Unknown";
1508+ Device c; c.uri = "socket://baz/qux"; c.makeModel = "Unknown";
1509+
1510+ QList<Device> devs({a, b, c});
1511+ QTest::newRow("Unknown devices aren't even considered.") << devs << 0;
1512+ }
1513+ }
1514+ void testConsolidation()
1515+ {
1516+ QFETCH(QList<Device>, devices);
1517+ QFETCH(int, targetConsolidatedDeviceCount);
1518+
1519+ ConsolidatedDeviceModel model(m_devicesModel);
1520+
1521+ Q_FOREACH(auto device, devices) {
1522+ m_backend->mockDeviceFound(device);
1523+ }
1524+ m_backend->deviceSearchFinished();
1525+
1526+ QCOMPARE(model.rowCount(), targetConsolidatedDeviceCount);
1527+ }
1528+ void testRoles_data()
1529+ {
1530+ QTest::addColumn<ConsolidatedDeviceModel::Roles>("role");
1531+ QTest::addColumn<QVariant>("value");
1532+ QTest::addColumn<QList<Device>>("devices");
1533+
1534+ {
1535+ Device a; a.uri = "ipp://foo/bar"; a.makeModel = "HP LaserFjert";
1536+ Device b; a.uri = "ipp://foo/bar"; b.makeModel = "HP LaserFjert";
1537+
1538+ QList<Device> devs({a, b});
1539+
1540+ ConsolidatedDeviceModel::Roles role(
1541+ ConsolidatedDeviceModel::ConsolidatedNameRole
1542+ );
1543+ QVariant value(QString("HP LaserFjert"));
1544+ QTest::newRow("ConsolidatedNameRole") << role << value << devs;
1545+ }
1546+ }
1547+ void testRoles()
1548+ {
1549+ QFETCH(ConsolidatedDeviceModel::Roles, role);
1550+ QFETCH(QVariant, value);
1551+ QFETCH(QList<Device>, devices);
1552+
1553+ ConsolidatedDeviceModel model(m_devicesModel);
1554+
1555+ Q_FOREACH(auto device, devices) {
1556+ m_backend->mockDeviceFound(device);
1557+ }
1558+ m_backend->deviceSearchFinished();
1559+
1560+ QCOMPARE(model.data(model.index(0), role), value);
1561+ }
1562+ void testDevicesRole()
1563+ {
1564+ Device a; a.uri = "ipp://foo/bar"; a.makeModel = "foo";
1565+ Device b; b.uri = "dnssd://foo/bar"; b.makeModel = "foo";
1566+
1567+ ConsolidatedDeviceModel model(m_devicesModel);
1568+
1569+ m_backend->mockDeviceFound(a);
1570+ m_backend->mockDeviceFound(b);
1571+ m_backend->deviceSearchFinished();
1572+
1573+ auto filter = model.data(
1574+ model.index(0), ConsolidatedDeviceModel::Roles::DevicesRole
1575+ ).value<DeviceFilter*>();
1576+
1577+ QCOMPARE(filter->count(), 2);
1578+ }
1579+private:
1580+ MockPrinterBackend *m_backend;
1581+ DeviceModel *m_devicesModel;
1582+};
1583+
1584+QTEST_GUILESS_MAIN(TestConsolidatedDeviceModel)
1585+#include "tst_consolidateddevicemodel.moc"
1586+
1587
1588=== added file 'tests/unittests/Printers/tst_devicefilter.cpp'
1589--- tests/unittests/Printers/tst_devicefilter.cpp 1970-01-01 00:00:00 +0000
1590+++ tests/unittests/Printers/tst_devicefilter.cpp 2017-03-30 13:23:27 +0000
1591@@ -0,0 +1,106 @@
1592+/*
1593+ * Copyright (C) 2017 Canonical, Ltd.
1594+ *
1595+ * This program is free software; you can redistribute it and/or modify
1596+ * it under the terms of the GNU Lesser General Public License as published by
1597+ * the Free Software Foundation; version 3.
1598+ *
1599+ * This program is distributed in the hope that it will be useful,
1600+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1601+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1602+ * GNU Lesser General Public License for more details.
1603+ *
1604+ * You should have received a copy of the GNU Lesser General Public License
1605+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1606+ */
1607+
1608+#include "mockbackend.h"
1609+
1610+#include "cups/ppdutils.h"
1611+#include "models/devicemodel.h"
1612+
1613+#include <QObject>
1614+#include <QSignalSpy>
1615+#include <QScopedPointer>
1616+#include <QTest>
1617+
1618+class TestDeviceFilter : public QObject
1619+{
1620+ Q_OBJECT
1621+private Q_SLOTS:
1622+ void testFilterOnMakeModel()
1623+ {
1624+ QScopedPointer<MockPrinterBackend> backend(new MockPrinterBackend);
1625+ DeviceModel model(backend.data());
1626+
1627+ Device dev1; dev1.uri = "ipp://foo/bar";
1628+ dev1.makeModel = "foo";
1629+ Device dev2; dev2.uri = "dnssd://bar/qux";
1630+ dev2.makeModel = "bar";
1631+
1632+ backend->mockDeviceFound(dev1);
1633+ backend->mockDeviceFound(dev2);
1634+
1635+ DeviceFilter filter;
1636+ filter.setSourceModel(&model);
1637+
1638+ QCOMPARE(filter.count(), 2);
1639+
1640+ // Install filter.
1641+ filter.filterOnNormalizedMakeModel("foo");
1642+ QCOMPARE(filter.count(), 1);
1643+ }
1644+
1645+ /* Test that "Foo BazJet-5000" and "Foo BazJet 5000" are accepted in same
1646+ filter. */
1647+ void testNormalizedFiltering()
1648+ {
1649+ QScopedPointer<MockPrinterBackend> backend(new MockPrinterBackend);
1650+ DeviceModel model(backend.data());
1651+
1652+ Device dev1; dev1.uri = "ipp://foo/bar";
1653+ dev1.makeModel = "Foo BazJet 5000";
1654+ Device dev2; dev2.uri = "dnssd://bar/qux";
1655+ dev2.makeModel = "Foo BazJet-5000";
1656+ Device dev3; dev3.uri = "dnssd://bar/baz";
1657+ dev3.makeModel = "Foo DeskQux 350";
1658+
1659+ backend->mockDeviceFound(dev1);
1660+ backend->mockDeviceFound(dev2);
1661+ backend->mockDeviceFound(dev3);
1662+
1663+ DeviceFilter filter;
1664+ filter.setSourceModel(&model);
1665+
1666+ // Install filter.
1667+ filter.filterOnNormalizedMakeModel(
1668+ PpdUtils::normalize("Foo BazJet 5000"));
1669+ QCOMPARE(filter.count(), 2);
1670+ }
1671+ void testFilterOnUri()
1672+ {
1673+ QScopedPointer<MockPrinterBackend> backend(new MockPrinterBackend);
1674+ DeviceModel model(backend.data());
1675+
1676+ Device dev1; dev1.uri = "ipp://foo/bar";
1677+ dev1.id = "foo";
1678+ Device dev2; dev2.uri = "dnssd://bar/qux";
1679+ dev2.id = "bar";
1680+
1681+ backend->mockDeviceFound(dev1);
1682+ backend->mockDeviceFound(dev2);
1683+
1684+ DeviceFilter filter;
1685+ filter.setSourceModel(&model);
1686+
1687+ QCOMPARE(filter.count(), 2);
1688+
1689+ // Install filter.
1690+ filter.filterOnUri("ipp://foo/bar");
1691+ QCOMPARE(filter.count(), 1);
1692+ }
1693+};
1694+
1695+QTEST_GUILESS_MAIN(TestDeviceFilter)
1696+#include "tst_devicefilter.moc"
1697+
1698
1699=== modified file 'tests/unittests/Printers/tst_jobmodel.cpp'
1700--- tests/unittests/Printers/tst_jobmodel.cpp 2017-03-15 17:30:21 +0000
1701+++ tests/unittests/Printers/tst_jobmodel.cpp 2017-03-30 13:23:27 +0000
1702@@ -19,6 +19,7 @@
1703 #include "backend/backend.h"
1704 #include "models/jobmodel.h"
1705 #include "printers/printers.h"
1706+#include "utils.h"
1707
1708 #include <QDebug>
1709 #include <QObject>
1710
1711=== added file 'tests/unittests/Printers/tst_ppdutils.cpp'
1712--- tests/unittests/Printers/tst_ppdutils.cpp 1970-01-01 00:00:00 +0000
1713+++ tests/unittests/Printers/tst_ppdutils.cpp 2017-03-30 13:23:27 +0000
1714@@ -0,0 +1,138 @@
1715+/*
1716+ * Copyright (C) 2017 Canonical, Ltd.
1717+ *
1718+ * This program is free software; you can redistribute it and/or modify
1719+ * it under the terms of the GNU Lesser General Public License as published by
1720+ * the Free Software Foundation; version 3.
1721+ *
1722+ * This program is distributed in the hope that it will be useful,
1723+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1724+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1725+ * GNU Lesser General Public License for more details.
1726+ *
1727+ * You should have received a copy of the GNU Lesser General Public License
1728+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1729+ */
1730+
1731+#include "cups/ppdutils.h"
1732+
1733+#include <QObject>
1734+#include <QTest>
1735+
1736+typedef QPair<QString, QString> MakeModel;
1737+Q_DECLARE_METATYPE(MakeModel)
1738+
1739+class TstPpdUtils : public QObject
1740+{
1741+ Q_OBJECT
1742+private Q_SLOTS:
1743+ void testSplitMakeModel_data()
1744+ {
1745+ QTest::addColumn<QString>("makeModel");
1746+ QTest::addColumn<MakeModel>("expected");
1747+
1748+ {
1749+ QString actual("HP HP Officejet Pro 8610");
1750+ auto expected = MakeModel("HP", "OfficeJet Pro 8610");
1751+ QTest::newRow("duplicate hp") << actual << expected;
1752+ }
1753+ {
1754+ QString actual("Officejet Officejet Pro 8610");
1755+ auto expected = MakeModel("HP", "OfficeJet Pro 8610");
1756+ QTest::newRow("officejet officejet (yes it actually happens)") << actual << expected;
1757+ }
1758+ {
1759+ QString actual("laserjet 4500");
1760+ auto expected = MakeModel("HP", "LaserJet 4500");
1761+ QTest::newRow("just a hp model") << actual << expected;
1762+ }
1763+ {
1764+ QString actual("HP OfficeJet Pro 8600");
1765+ auto expected = MakeModel("HP", "OfficeJet Pro 8600");
1766+ QTest::newRow("a sane hp officejet") << actual << expected;
1767+ }
1768+ {
1769+ QString actual("HP Foo All-In-One");
1770+ auto expected = MakeModel("HP", "Foo");
1771+ QTest::newRow("remove all-in-one") << actual << expected;
1772+ }
1773+ {
1774+ QString actual("Hewlett-Packard SmartJet 50 v0.10");
1775+ auto expected = MakeModel("HP", "SmartJet 50");
1776+ QTest::newRow("Hewlett-Packard SmartJet 50 v0.10") << actual << expected;
1777+ }
1778+ {
1779+ QString actual("Foobar Bazjet 9000");
1780+ auto expected = MakeModel("Foobar", "Bazjet 9000");
1781+ QTest::newRow("the excellent foobar bazjet 9000") << actual << expected;
1782+ }
1783+ {
1784+ QString actual("HP LaserJet 4 Plus v2013.111 Postscript (recommended)");
1785+ auto expected = MakeModel("HP", "LaserJet 4 Plus");
1786+ QTest::newRow("example #1") << actual << expected;
1787+ }
1788+ {
1789+ QString actual("Canon MG4100 series Ver.3.90");
1790+ auto expected = MakeModel("Canon", "MG4100");
1791+ QTest::newRow("example #2") << actual << expected;
1792+ }
1793+ {
1794+ QString actual("Xerox Foo Printer");
1795+ auto expected = MakeModel("Xerox", "Foo");
1796+ QTest::newRow("remove ignored suffix 'printer'") << actual << expected;
1797+ }
1798+ {
1799+ QString actual("hp lj 4500");
1800+ auto expected = MakeModel("HP", "LaserJet 4500");
1801+ QTest::newRow("shortened hp model name") << actual << expected;
1802+ }
1803+ {
1804+ QString actual("okipage foobar");
1805+ auto expected = MakeModel("Oki", "okipage foobar");
1806+ QTest::newRow("some okipage printer") << actual << expected;
1807+ }
1808+ }
1809+ void testSplitMakeModel()
1810+ {
1811+ QFETCH(QString, makeModel);
1812+ QFETCH(MakeModel, expected);
1813+ auto actual = PpdUtils::splitMakeModel(makeModel);
1814+ QCOMPARE(actual, expected);
1815+ }
1816+ void testNormalize_data()
1817+ {
1818+ QTest::addColumn<QString>("actual");
1819+ QTest::addColumn<QString>("expected");
1820+
1821+ {
1822+ QString actual("Epson PM-A820");
1823+ QString expected("epson pm a 820");
1824+ QTest::newRow("epson with dash") << actual << expected;
1825+ }
1826+ {
1827+ QString actual("Epson PM A820");
1828+ QString expected("epson pm a 820");
1829+ QTest::newRow("epson without dash") << actual << expected;
1830+ }
1831+ {
1832+ QString actual("HP PhotoSmart C 8100");
1833+ QString expected("hp photosmart c 8100");
1834+ QTest::newRow("C 8100") << actual << expected;
1835+ }
1836+ {
1837+ QString actual("HP PhotoSmart C8100");
1838+ QString expected("hp photosmart c 8100");
1839+ QTest::newRow("C8100") << actual << expected;
1840+ }
1841+ }
1842+ void testNormalize()
1843+ {
1844+ QFETCH(QString, actual);
1845+ QFETCH(QString, expected);
1846+ actual = PpdUtils::normalize(actual);
1847+ QCOMPARE(actual, expected);
1848+ }
1849+};
1850+
1851+QTEST_GUILESS_MAIN(TstPpdUtils)
1852+#include "tst_ppdutils.moc"
1853
1854=== modified file 'tests/unittests/Printers/tst_printer.cpp'
1855--- tests/unittests/Printers/tst_printer.cpp 2017-03-20 13:31:55 +0000
1856+++ tests/unittests/Printers/tst_printer.cpp 2017-03-30 13:23:27 +0000
1857@@ -14,12 +14,12 @@
1858 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1859 */
1860
1861-#include "utils.h"
1862
1863 #include "mockbackend.h"
1864
1865 #include "backend/backend.h"
1866 #include "backend/backend_pdf.h"
1867+#include "cups/ppdutils.h"
1868 #include "printer/printer.h"
1869
1870 #include <QDebug>
1871@@ -158,7 +158,7 @@
1872 QStringList duplexVals = duplexVar.toStringList();
1873 QCOMPARE(
1874 duplexVals.at(0),
1875- (QString) Utils::duplexModeToPpdChoice(PrinterEnum::DuplexMode::DuplexLongSide)
1876+ (QString) PpdUtils::duplexModeToPpdChoice(PrinterEnum::DuplexMode::DuplexLongSide)
1877 );
1878 }
1879 void testSetDefaultPageSize_data()
1880
1881=== modified file 'tests/unittests/Printers/tst_printerdevicemodel.cpp'
1882--- tests/unittests/Printers/tst_printerdevicemodel.cpp 2017-03-08 16:13:02 +0000
1883+++ tests/unittests/Printers/tst_printerdevicemodel.cpp 2017-03-30 13:23:27 +0000
1884@@ -60,6 +60,11 @@
1885 Device d; d.uri = "dnssd:";
1886 QTest::newRow("non-empty uri, malformed") << d << false;
1887 }
1888+ {
1889+ Device d; d.uri = "dnssd://foo/bar";
1890+ d.makeModel = "Unknown";
1891+ QTest::newRow("non-empty uri, but Unknown make/model") << d << false;
1892+ }
1893 }
1894 void testAcceptedDevices()
1895 {
1896@@ -152,6 +157,36 @@
1897 m_backend->mockDeviceFound(device);
1898 QCOMPARE(m_model->data(m_model->index(0), role), value);
1899 }
1900+ void testFiltering_data()
1901+ {
1902+ QTest::addColumn<QList<Device>>("devices");
1903+ QTest::addColumn<QString>("filterId");
1904+ QTest::addColumn<int>("targetCount");
1905+ QTest::addColumn<QStringList>("targetIds");
1906+
1907+ {
1908+ Device a; a.uri = "dnssd://foo/bar"; a.id = "foo";
1909+ Device b; b.uri = "ipps://bar/qux"; b.id = "bar";
1910+
1911+ QList<Device> devs({a, b});
1912+ QTest::newRow("no filtering") << devs << "" << 2 << QStringList({"foo", "bar"});
1913+ }
1914+ }
1915+ void testFiltering()
1916+ {
1917+ QFETCH(QList<Device>, devices);
1918+ QFETCH(QString, filterId);
1919+ QFETCH(int, targetCount);
1920+ QFETCH(QStringList, targetIds);
1921+
1922+ Q_FOREACH(auto dev, devices) {
1923+ m_backend->mockDeviceFound(dev);
1924+ }
1925+
1926+ DeviceFilter filter;
1927+ filter.setSourceModel(m_model);
1928+ QCOMPARE(filter.count(), targetCount);
1929+ }
1930 private:
1931 MockPrinterBackend *m_backend;
1932 DeviceModel *m_model;
1933
1934=== modified file 'tests/unittests/Printers/tst_printermodel.cpp'
1935--- tests/unittests/Printers/tst_printermodel.cpp 2017-03-10 13:15:27 +0000
1936+++ tests/unittests/Printers/tst_printermodel.cpp 2017-03-30 13:23:27 +0000
1937@@ -17,9 +17,11 @@
1938 #include "mockbackend.h"
1939
1940 #include "backend/backend.h"
1941+#include "cups/ppdutils.h"
1942 #include "models/printermodel.h"
1943 #include "printer/printer.h"
1944 #include "printer/printerjob.h"
1945+#include "utils.h"
1946
1947 #include <QDebug>
1948 #include <QObject>
1949@@ -231,7 +233,7 @@
1950 PrinterBackend* backend = new MockPrinterBackend("a-printer");
1951 ((MockPrinterBackend*) backend)->m_supportedDuplexModes = modes;
1952 ((MockPrinterBackend*) backend)->printerOptions["a-printer"].insert(
1953- "Duplex", QVariant::fromValue(Utils::duplexModeToPpdChoice(PrinterEnum::DuplexMode::DuplexLongSide))
1954+ "Duplex", QVariant::fromValue(PpdUtils::duplexModeToPpdChoice(PrinterEnum::DuplexMode::DuplexLongSide))
1955 );
1956
1957 auto printerA = QSharedPointer<Printer>(new Printer(backend));
1958
1959=== added file 'tests/unittests/Printers/tst_utils.cpp'
1960--- tests/unittests/Printers/tst_utils.cpp 1970-01-01 00:00:00 +0000
1961+++ tests/unittests/Printers/tst_utils.cpp 2017-03-30 13:23:27 +0000
1962@@ -0,0 +1,107 @@
1963+/*
1964+ * Copyright (C) 2017 Canonical, Ltd.
1965+ *
1966+ * This program is free software; you can redistribute it and/or modify
1967+ * it under the terms of the GNU Lesser General Public License as published by
1968+ * the Free Software Foundation; version 3.
1969+ *
1970+ * This program is distributed in the hope that it will be useful,
1971+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1972+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1973+ * GNU Lesser General Public License for more details.
1974+ *
1975+ * You should have received a copy of the GNU Lesser General Public License
1976+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1977+ */
1978+
1979+#include "utils.h"
1980+
1981+#include <QObject>
1982+#include <QTest>
1983+
1984+typedef QMap<QString, QString> IdDict;
1985+Q_DECLARE_METATYPE(IdDict)
1986+
1987+class TstPrinterUtils : public QObject
1988+{
1989+ Q_OBJECT
1990+private Q_SLOTS:
1991+ void tstParseDeviceId_data()
1992+ {
1993+ QTest::addColumn<QString>("deviceId");
1994+ QTest::addColumn<IdDict>("expected");
1995+
1996+ {
1997+ QString actual("MFG:Hewlett-Packard;CMD:PJL,BIDI-ECP,PCLXL,PCL,PDF,PJL,POSTSCRIPT;MDL:HP Color LaserJet CM3530 MFP;CLS:PRINTER;DES:Hewlett-Packard Color LaserJet CM3530 MFP;");
1998+ auto expected = QMap<QString, QString>{
1999+ {"MFG", "Hewlett-Packard"},
2000+ {"MDL", "HP Color LaserJet CM3530 MFP"},
2001+ {"CMD", "PJL,BIDI-ECP,PCLXL,PCL,PDF,PJL,POSTSCRIPT"},
2002+ {"CLS", "PRINTER"},
2003+ {"DES", "Hewlett-Packard Color LaserJet CM3530 MFP"},
2004+ {"SN", QString::null},
2005+ {"S", QString::null},
2006+ {"P", QString::null},
2007+ {"J", QString::null},
2008+ };
2009+ QTest::newRow("duplicate hp") << actual << expected;
2010+ }
2011+ {
2012+ QString actual("MFG:HP;MDL:HP Officejet Pro 8610;CMD:PCL,JPEG,URF;");
2013+ auto expected = QMap<QString, QString>{
2014+ {"MFG", "HP"},
2015+ {"MDL", "HP Officejet Pro 8610"},
2016+ {"CMD", "PCL,JPEG,URF"},
2017+ {"CLS", QString::null},
2018+ {"DES", QString::null},
2019+ {"SN", QString::null},
2020+ {"S", QString::null},
2021+ {"P", QString::null},
2022+ {"J", QString::null},
2023+ };
2024+ QTest::newRow("actual hp device id") << actual << expected;
2025+ }
2026+ {
2027+ QString actual("MFG:HP;MDL:Officejet Pro 8600;CMD:PCL3GUI,PCL3,PJL,JPEG,PCLM,URF,DW-PCL,802.11,802.3,DESKJET,DYN;CLS:PRINTER;DES:CM749A;CID:HPIJVIPAV2;LEDMDIS:USB#FF#CC#00,USB#07#01#02;SN:CN371DWH1V05KC;S:038080C484201021005a00800004518005a4418003c4618005a4118005a;Z:0102,05000009000008000008000008000008,0600,0700000000000000000000,0b0000000000000000000099ba0000000099b50000000099b90000000099b6,0c0,0e00000000000000000000,0f00000000000000000000,10000002000008000008000008000008,110,12002,150,17000000000000000000000000000000,181;");
2028+ auto expected = QMap<QString, QString>{
2029+ {"MFG", "HP"},
2030+ {"MDL", "Officejet Pro 8600"},
2031+ {"CMD", "PCL3GUI,PCL3,PJL,JPEG,PCLM,URF,DW-PCL,802.11,802.3,DESKJET,DYN"},
2032+ {"CLS", "PRINTER"},
2033+ {"DES", "CM749A"},
2034+ {"SN", "CN371DWH1V05KC"},
2035+ {"S", "038080C484201021005a00800004518005a4418003c4618005a4118005a"},
2036+ {"P", QString::null},
2037+ {"J", QString::null},
2038+ {"Z", "0102,05000009000008000008000008000008,0600,0700000000000000000000,0b0000000000000000000099ba0000000099b50000000099b90000000099b6,0c0,0e00000000000000000000,0f00000000000000000000,10000002000008000008000008000008,110,12002,150,17000000000000000000000000000000,181"},
2039+ {"CID", "HPIJVIPAV2"},
2040+ {"LEDMDIS", "USB#FF#CC#00,USB#07#01#02"}
2041+ };
2042+ QTest::newRow("actual, but crazy, hp device id") << actual << expected;
2043+ }
2044+ {
2045+ QString actual("MFG:EPSON;CMD:ESCPL2,BDC,D4;MDL:Stylus Photo RX420;CLS:PRINTER;DES:EPSON Stylus Photo RX420;");
2046+ auto expected = QMap<QString, QString>{
2047+ {"MFG", "EPSON"},
2048+ {"MDL", "Stylus Photo RX420"},
2049+ {"CMD", "ESCPL2,BDC,D4"},
2050+ {"CLS", "PRINTER"},
2051+ {"DES", "EPSON Stylus Photo RX420"},
2052+ {"SN", QString::null},
2053+ {"S", QString::null},
2054+ {"P", QString::null},
2055+ {"J", QString::null},
2056+ };
2057+ QTest::newRow("some epson stylus") << actual << expected;
2058+ }
2059+ }
2060+ void tstParseDeviceId()
2061+ {
2062+ QFETCH(QString, deviceId);
2063+ QFETCH(IdDict, expected);
2064+ QCOMPARE(Utils::parseDeviceId(deviceId), expected);
2065+ }
2066+};
2067+
2068+QTEST_GUILESS_MAIN(TstPrinterUtils)
2069+#include "tst_utils.moc"

Subscribers

People subscribed via source and target branches