1 /*
2 Scan Tailor - Interactive post-processing tool for scanned pages.
3 Copyright (C) Joseph Artsimovich <joseph.artsimovich@gmail.com>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "FixDpiDialog.h"
20 #include <QSortFilterProxyModel>
21 #include <boost/foreach.hpp>
22 #include <boost/lambda/bind.hpp>
23 #include <boost/lambda/lambda.hpp>
24 #include "ColorSchemeManager.h"
25
26 // To be able to use it in QVariant
27 Q_DECLARE_METATYPE(ImageMetadata)
28
29 static const int NEED_FIXING_TAB = 0;
30 static const int ALL_PAGES_TAB = 1;
31
32 // Requests a group of ImageMetadata objects folded into one.
33 static const int AGGREGATE_METADATA_ROLE = Qt::UserRole;
34 // Same as the one above, but only objects with .isDpiOK() == false
35 // will be considered.
36 static const int AGGREGATE_NOT_OK_METADATA_ROLE = Qt::UserRole + 1;
37
38
39 /**
40 * This class computes an aggregate ImageMetadata object from a group of other
41 * ImageMetadata objects. If all ImageMetadata objects in a group are equal,
42 * that will make it an aggregate metadata. Otherwise, a null (default
43 * constructed) ImageMetadata() object will be considered
44 * the DPIs within the group are not consistent,
45 * the aggregate Image metadata object will have zeros both for size and for
46 * DPI values. If the DPIs are consistent but sizes are not, the aggregate
47 * ImageMetadata will have the consistent DPI and zero size.
48 */
49 class FixDpiDialog::DpiCounts {
50 public:
51 void add(const ImageMetadata& metadata);
52
53 void remove(const ImageMetadata& metadata);
54
55 /**
56 * Checks if all ImageMetadata objects return true for ImageMetadata::isDpiOK().
57 */
58 bool allDpisOK() const;
59
60 /**
61 * If all ImageMetadata objects are equal, one of them will be returned.
62 * Otherwise, a default-constructed ImageMetadata() object will be returned.
63 */
64 ImageMetadata aggregate(Scope scope) const;
65
66 private:
67 struct MetadataComparator {
68 bool operator()(const ImageMetadata& lhs, const ImageMetadata& rhs) const;
69 };
70
71 typedef std::map<ImageMetadata, int, MetadataComparator> Map;
72
73 Map m_counts;
74 };
75
76
77 /**
78 * This comparator puts objects that are not OK to the front.
79 */
operator ()(const ImageMetadata & lhs,const ImageMetadata & rhs) const80 bool FixDpiDialog::DpiCounts::MetadataComparator::operator()(const ImageMetadata& lhs, const ImageMetadata& rhs) const {
81 const bool lhs_ok = lhs.isDpiOK();
82 const bool rhs_ok = rhs.isDpiOK();
83 if (lhs_ok != rhs_ok) {
84 return rhs_ok;
85 }
86
87 if (lhs.size().width() < rhs.size().width()) {
88 return true;
89 } else if (lhs.size().width() > rhs.size().width()) {
90 return false;
91 } else if (lhs.size().height() < rhs.size().height()) {
92 return true;
93 } else if (lhs.size().height() > rhs.size().height()) {
94 return false;
95 } else if (lhs.dpi().horizontal() < rhs.dpi().horizontal()) {
96 return true;
97 } else if (lhs.dpi().horizontal() > rhs.dpi().horizontal()) {
98 return false;
99 } else {
100 return lhs.dpi().vertical() < rhs.dpi().vertical();
101 }
102 }
103
104 class FixDpiDialog::SizeGroup {
105 public:
106 struct Item {
107 int fileIdx;
108 int imageIdx;
109
ItemFixDpiDialog::SizeGroup::Item110 Item(int file_idx, int image_idx) : fileIdx(file_idx), imageIdx(image_idx) {}
111 };
112
SizeGroup(const QSize & size)113 explicit SizeGroup(const QSize& size) : m_size(size) {}
114
115 void append(const Item& item, const ImageMetadata& metadata);
116
size() const117 const QSize& size() const { return m_size; }
118
items() const119 const std::vector<Item>& items() const { return m_items; }
120
dpiCounts()121 DpiCounts& dpiCounts() { return m_dpiCounts; }
122
dpiCounts() const123 const DpiCounts& dpiCounts() const { return m_dpiCounts; }
124
125 private:
126 QSize m_size;
127 std::vector<Item> m_items;
128 DpiCounts m_dpiCounts;
129 };
130
131
132 class FixDpiDialog::TreeModel : private QAbstractItemModel {
133 public:
134 explicit TreeModel(const std::vector<ImageFileInfo>& files);
135
files() const136 const std::vector<ImageFileInfo>& files() const { return m_files; }
137
model()138 QAbstractItemModel* model() { return this; }
139
allDpisOK() const140 bool allDpisOK() const { return m_dpiCounts.allDpisOK(); }
141
142 bool isVisibleForFilter(const QModelIndex& parent, int row) const;
143
144 void applyDpiToSelection(Scope scope, const Dpi& dpi, const QItemSelection& selection);
145
146 private:
147 struct Tag {};
148
149 int columnCount(const QModelIndex& parent) const override;
150
151 int rowCount(const QModelIndex& parent) const override;
152
153 QModelIndex index(int row, int column, const QModelIndex& parent) const override;
154
155 QModelIndex parent(const QModelIndex& index) const override;
156
157 QVariant data(const QModelIndex& index, int role) const override;
158
159 void applyDpiToAllGroups(Scope scope, const Dpi& dpi);
160
161 void applyDpiToGroup(Scope scope, const Dpi& dpi, SizeGroup& group, DpiCounts& total_dpi_counts);
162
163 void applyDpiToItem(Scope scope,
164 const ImageMetadata& new_metadata,
165 SizeGroup::Item item,
166 DpiCounts& total_dpi_counts,
167 DpiCounts& group_dpi_counts);
168
169 void emitAllPagesChanged(const QModelIndex& idx);
170
171 void emitSizeGroupChanged(const QModelIndex& idx);
172
173 void emitItemChanged(const QModelIndex& idx);
174
175 SizeGroup& sizeGroupFor(QSize size);
176
177 static QString sizeToString(QSize size);
178
179 static Tag m_allPagesNodeId;
180 static Tag m_sizeGroupNodeId;
181
182 std::vector<ImageFileInfo> m_files;
183 std::vector<SizeGroup> m_sizes;
184 DpiCounts m_dpiCounts;
185 };
186
187
188 class FixDpiDialog::FilterModel : private QSortFilterProxyModel {
189 public:
190 explicit FilterModel(TreeModel& delegate);
191
model()192 QAbstractProxyModel* model() { return this; }
193
194 private:
195 bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
196
197 QVariant data(const QModelIndex& index, int role) const override;
198
199 TreeModel& m_delegate;
200 };
201
202
FixDpiDialog(const std::vector<ImageFileInfo> & files,QWidget * parent)203 FixDpiDialog::FixDpiDialog(const std::vector<ImageFileInfo>& files, QWidget* parent)
204 : QDialog(parent), m_pages(new TreeModel(files)), m_undefinedDpiPages(new FilterModel(*m_pages)) {
205 setupUi(this);
206
207 m_normalPalette = xDpi->palette();
208 m_errorPalette = m_normalPalette;
209 const QColor error_text_color
210 = ColorSchemeManager::instance()->getColorParam(ColorScheme::FixDpiDialogErrorText, QColor(Qt::red));
211 m_errorPalette.setColor(QPalette::Text, error_text_color);
212
213 dpiCombo->addItem("300 x 300", QSize(300, 300));
214 dpiCombo->addItem("400 x 400", QSize(400, 400));
215 dpiCombo->addItem("600 x 600", QSize(600, 600));
216
217 tabWidget->setTabText(NEED_FIXING_TAB, tr("Need Fixing"));
218 tabWidget->setTabText(ALL_PAGES_TAB, tr("All Pages"));
219 undefinedDpiView->setModel(m_undefinedDpiPages->model()), undefinedDpiView->header()->hide();
220 allPagesView->setModel(m_pages->model());
221 allPagesView->header()->hide();
222
223 xDpi->setMaxLength(4);
224 yDpi->setMaxLength(4);
225 xDpi->setValidator(new QIntValidator(xDpi));
226 yDpi->setValidator(new QIntValidator(yDpi));
227
228 connect(tabWidget, SIGNAL(currentChanged(int)), this, SLOT(tabChanged(int)));
229
230 connect(undefinedDpiView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
231 this, SLOT(selectionChanged(const QItemSelection&)));
232 connect(allPagesView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), this,
233 SLOT(selectionChanged(const QItemSelection&)));
234
235 connect(dpiCombo, SIGNAL(activated(int)), this, SLOT(dpiComboChangedByUser(int)));
236
237 connect(xDpi, SIGNAL(textEdited(const QString&)), this, SLOT(dpiValueChanged()));
238 connect(yDpi, SIGNAL(textEdited(const QString&)), this, SLOT(dpiValueChanged()));
239
240 connect(applyBtn, SIGNAL(clicked()), this, SLOT(applyClicked()));
241
242 enableDisableOkButton();
243 }
244
245 FixDpiDialog::~FixDpiDialog() = default;
246
files() const247 const std::vector<ImageFileInfo>& FixDpiDialog::files() const {
248 return m_pages->files();
249 }
250
tabChanged(const int tab)251 void FixDpiDialog::tabChanged(const int tab) {
252 QTreeView* views[2];
253 views[NEED_FIXING_TAB] = undefinedDpiView;
254 views[ALL_PAGES_TAB] = allPagesView;
255 updateDpiFromSelection(views[tab]->selectionModel()->selection());
256 }
257
selectionChanged(const QItemSelection & selection)258 void FixDpiDialog::selectionChanged(const QItemSelection& selection) {
259 updateDpiFromSelection(selection);
260 }
261
dpiComboChangedByUser(const int index)262 void FixDpiDialog::dpiComboChangedByUser(const int index) {
263 const QVariant data(dpiCombo->itemData(index));
264 if (data.isValid()) {
265 const QSize dpi(data.toSize());
266 xDpi->setText(QString::number(dpi.width()));
267 yDpi->setText(QString::number(dpi.height()));
268 dpiValueChanged();
269 }
270 }
271
dpiValueChanged()272 void FixDpiDialog::dpiValueChanged() {
273 updateDpiCombo();
274
275 const Dpi dpi(xDpi->text().toInt(), yDpi->text().toInt());
276 const ImageMetadata metadata(m_selectedItemPixelSize, dpi);
277
278 decorateDpiInputField(xDpi, metadata.horizontalDpiStatus());
279 decorateDpiInputField(yDpi, metadata.verticalDpiStatus());
280
281 if ((m_xDpiInitialValue == xDpi->text()) && (m_yDpiInitialValue == yDpi->text())) {
282 applyBtn->setEnabled(false);
283
284 return;
285 }
286
287
288 if (metadata.isDpiOK()) {
289 applyBtn->setEnabled(true);
290
291 return;
292 }
293
294 applyBtn->setEnabled(false);
295 }
296
applyClicked()297 void FixDpiDialog::applyClicked() {
298 const Dpi dpi(xDpi->text().toInt(), yDpi->text().toInt());
299 QItemSelectionModel* selection_model = nullptr;
300
301 if (tabWidget->currentIndex() == ALL_PAGES_TAB) {
302 selection_model = allPagesView->selectionModel();
303 const QItemSelection selection(selection_model->selection());
304 m_pages->applyDpiToSelection(ALL, dpi, selection);
305 } else {
306 selection_model = undefinedDpiView->selectionModel();
307 const QItemSelection selection(m_undefinedDpiPages->model()->mapSelectionToSource(selection_model->selection()));
308 m_pages->applyDpiToSelection(NOT_OK, dpi, selection);
309 }
310
311 updateDpiFromSelection(selection_model->selection());
312 enableDisableOkButton();
313 }
314
enableDisableOkButton()315 void FixDpiDialog::enableDisableOkButton() {
316 const bool enable = m_pages->allDpisOK();
317 buttonBox->button(QDialogButtonBox::Ok)->setEnabled(enable);
318 }
319
320 /**
321 * This function work with both TreeModel and FilterModel selections.
322 * It is assumed that only a single item is selected.
323 */
updateDpiFromSelection(const QItemSelection & selection)324 void FixDpiDialog::updateDpiFromSelection(const QItemSelection& selection) {
325 if (selection.isEmpty()) {
326 resetDpiForm();
327 dpiCombo->setEnabled(false);
328 xDpi->setEnabled(false);
329 yDpi->setEnabled(false);
330 // applyBtn is managed elsewhere.
331 return;
332 }
333
334 dpiCombo->setEnabled(true);
335 xDpi->setEnabled(true);
336 yDpi->setEnabled(true);
337
338 // FilterModel may replace AGGREGATE_METADATA_ROLE with AGGREGATE_NOT_OK_METADATA_ROLE.
339 const QVariant data(selection.front().topLeft().data(AGGREGATE_METADATA_ROLE));
340 if (data.isValid()) {
341 setDpiForm(data.value<ImageMetadata>());
342 } else {
343 resetDpiForm();
344 }
345 }
346
resetDpiForm()347 void FixDpiDialog::resetDpiForm() {
348 dpiCombo->setCurrentIndex(0);
349 m_xDpiInitialValue.clear();
350 m_yDpiInitialValue.clear();
351 xDpi->setText(m_xDpiInitialValue);
352 yDpi->setText(m_yDpiInitialValue);
353 dpiValueChanged();
354 }
355
setDpiForm(const ImageMetadata & metadata)356 void FixDpiDialog::setDpiForm(const ImageMetadata& metadata) {
357 const Dpi dpi(metadata.dpi());
358
359 if (dpi.isNull()) {
360 resetDpiForm();
361
362 return;
363 }
364
365 m_xDpiInitialValue = QString::number(dpi.horizontal());
366 m_yDpiInitialValue = QString::number(dpi.vertical());
367 m_selectedItemPixelSize = metadata.size();
368 xDpi->setText(m_xDpiInitialValue);
369 yDpi->setText(m_yDpiInitialValue);
370 dpiValueChanged();
371 }
372
updateDpiCombo()373 void FixDpiDialog::updateDpiCombo() {
374 bool x_ok = true, y_ok = true;
375 const QSize dpi(xDpi->text().toInt(&x_ok), yDpi->text().toInt(&y_ok));
376
377 if (x_ok && y_ok) {
378 const int count = dpiCombo->count();
379 for (int i = 0; i < count; ++i) {
380 const QVariant data(dpiCombo->itemData(i));
381 if (data.isValid()) {
382 if (dpi == data.toSize()) {
383 dpiCombo->setCurrentIndex(i);
384
385 return;
386 }
387 }
388 }
389 }
390
391 dpiCombo->setCurrentIndex(0);
392 }
393
decorateDpiInputField(QLineEdit * field,ImageMetadata::DpiStatus dpi_status) const394 void FixDpiDialog::decorateDpiInputField(QLineEdit* field, ImageMetadata::DpiStatus dpi_status) const {
395 if (dpi_status == ImageMetadata::DPI_OK) {
396 field->setPalette(m_normalPalette);
397 } else {
398 field->setPalette(m_errorPalette);
399 }
400
401 switch (dpi_status) {
402 case ImageMetadata::DPI_OK:
403 case ImageMetadata::DPI_UNDEFINED:
404 field->setToolTip(QString());
405 break;
406 case ImageMetadata::DPI_TOO_LARGE:
407 field->setToolTip(tr("DPI is too large and most likely wrong."));
408 break;
409 case ImageMetadata::DPI_TOO_SMALL:
410 field->setToolTip(
411 tr("DPI is too small. Even if it's correct, you are not going to get acceptable results with it."));
412 break;
413 case ImageMetadata::DPI_TOO_SMALL_FOR_THIS_PIXEL_SIZE:
414 field->setToolTip(
415 tr("DPI is too small for this pixel size. Such combination would probably lead to out of "
416 "memory errors."));
417 break;
418 }
419 }
420
421 /*====================== FixDpiDialog::DpiCounts ======================*/
422
add(const ImageMetadata & metadata)423 void FixDpiDialog::DpiCounts::add(const ImageMetadata& metadata) {
424 ++m_counts[metadata];
425 }
426
remove(const ImageMetadata & metadata)427 void FixDpiDialog::DpiCounts::remove(const ImageMetadata& metadata) {
428 if (--m_counts[metadata] == 0) {
429 m_counts.erase(metadata);
430 }
431 }
432
allDpisOK() const433 bool FixDpiDialog::DpiCounts::allDpisOK() const {
434 // We put wrong DPIs to the front, so if the first one is OK,
435 // the others are OK as well.
436 const auto it(m_counts.begin());
437
438 return it == m_counts.end() || it->first.isDpiOK();
439 }
440
aggregate(const Scope scope) const441 ImageMetadata FixDpiDialog::DpiCounts::aggregate(const Scope scope) const {
442 const auto it(m_counts.begin());
443
444 if (it == m_counts.end()) {
445 return ImageMetadata();
446 }
447
448 if ((scope == NOT_OK) && it->first.isDpiOK()) {
449 // If this one is OK, the following ones are OK as well.
450 return ImageMetadata();
451 }
452
453 Map::const_iterator next(it);
454 ++next;
455
456 if (next == m_counts.end()) {
457 return it->first;
458 }
459
460 if ((scope == NOT_OK) && next->first.isDpiOK()) {
461 // If this one is OK, the following ones are OK as well.
462 return it->first;
463 }
464
465 return ImageMetadata();
466 }
467
468 /*====================== FixDpiDialog::SizeGroup ======================*/
469
append(const Item & item,const ImageMetadata & metadata)470 void FixDpiDialog::SizeGroup::append(const Item& item, const ImageMetadata& metadata) {
471 m_items.push_back(item);
472 m_dpiCounts.add(metadata);
473 }
474
475 /*====================== FixDpiDialog::TreeModel ======================*/
476
477 FixDpiDialog::TreeModel::Tag FixDpiDialog::TreeModel::m_allPagesNodeId;
478 FixDpiDialog::TreeModel::Tag FixDpiDialog::TreeModel::m_sizeGroupNodeId;
479
TreeModel(const std::vector<ImageFileInfo> & files)480 FixDpiDialog::TreeModel::TreeModel(const std::vector<ImageFileInfo>& files) : m_files(files) {
481 const auto num_files = static_cast<const int>(m_files.size());
482 for (int i = 0; i < num_files; ++i) {
483 const ImageFileInfo& file = m_files[i];
484 const auto num_images = static_cast<const int>(file.imageInfo().size());
485 for (int j = 0; j < num_images; ++j) {
486 const ImageMetadata& metadata = file.imageInfo()[j];
487 SizeGroup& group = sizeGroupFor(metadata.size());
488 group.append(SizeGroup::Item(i, j), metadata);
489 m_dpiCounts.add(metadata);
490 }
491 }
492 }
493
isVisibleForFilter(const QModelIndex & parent,int row) const494 bool FixDpiDialog::TreeModel::isVisibleForFilter(const QModelIndex& parent, int row) const {
495 const void* const ptr = parent.internalPointer();
496
497 if (!parent.isValid()) {
498 // 'All Pages'.
499 return !m_dpiCounts.allDpisOK();
500 } else if (ptr == &m_allPagesNodeId) {
501 // A size group.
502 return !m_sizes[row].dpiCounts().allDpisOK();
503 } else if (ptr == &m_sizeGroupNodeId) {
504 // An image.
505 const SizeGroup& group = m_sizes[parent.row()];
506 const SizeGroup::Item& item = group.items()[row];
507 const ImageFileInfo& file = m_files[item.fileIdx];
508
509 return !file.imageInfo()[item.imageIdx].isDpiOK();
510 } else {
511 // Should not happen.
512 return false;
513 }
514 }
515
applyDpiToSelection(const Scope scope,const Dpi & dpi,const QItemSelection & selection)516 void FixDpiDialog::TreeModel::applyDpiToSelection(const Scope scope, const Dpi& dpi, const QItemSelection& selection) {
517 if (selection.isEmpty()) {
518 return;
519 }
520
521 const QModelIndex parent(selection.front().parent());
522 const int row = selection.front().top();
523 const void* const ptr = parent.internalPointer();
524 const QModelIndex idx(index(row, 0, parent));
525
526 if (!parent.isValid()) {
527 // Apply to all pages.
528 applyDpiToAllGroups(scope, dpi);
529 emitAllPagesChanged(idx);
530 } else if (ptr == &m_allPagesNodeId) {
531 // Apply to a size group.
532 SizeGroup& group = m_sizes[row];
533 applyDpiToGroup(scope, dpi, group, m_dpiCounts);
534 emitSizeGroupChanged(index(row, 0, parent));
535 } else if (ptr == &m_sizeGroupNodeId) {
536 // Images within a size group.
537 SizeGroup& group = m_sizes[parent.row()];
538 const SizeGroup::Item& item = group.items()[row];
539 const ImageMetadata metadata(group.size(), dpi);
540 applyDpiToItem(scope, metadata, item, m_dpiCounts, group.dpiCounts());
541 emitItemChanged(idx);
542 }
543 }
544
columnCount(const QModelIndex & parent) const545 int FixDpiDialog::TreeModel::columnCount(const QModelIndex& parent) const {
546 return 1;
547 }
548
rowCount(const QModelIndex & parent) const549 int FixDpiDialog::TreeModel::rowCount(const QModelIndex& parent) const {
550 const void* const ptr = parent.internalPointer();
551
552 if (!parent.isValid()) {
553 // The single 'All Pages' item.
554 return 1;
555 } else if (ptr == &m_allPagesNodeId) {
556 // Size groups.
557 return static_cast<int>(m_sizes.size());
558 } else if (ptr == &m_sizeGroupNodeId) {
559 // Images within a size group.
560 return static_cast<int>(m_sizes[parent.row()].items().size());
561 } else {
562 // Children of an image.
563 return 0;
564 }
565 }
566
index(const int row,const int column,const QModelIndex & parent) const567 QModelIndex FixDpiDialog::TreeModel::index(const int row, const int column, const QModelIndex& parent) const {
568 const void* const ptr = parent.internalPointer();
569
570 if (!parent.isValid()) {
571 // The 'All Pages' item.
572 return createIndex(row, column, &m_allPagesNodeId);
573 } else if (ptr == &m_allPagesNodeId) {
574 // A size group.
575 return createIndex(row, column, &m_sizeGroupNodeId);
576 } else if (ptr == &m_sizeGroupNodeId) {
577 // An image within some size group.
578 return createIndex(row, column, (void*) &m_sizes[parent.row()]);
579 }
580
581 return QModelIndex();
582 }
583
parent(const QModelIndex & index) const584 QModelIndex FixDpiDialog::TreeModel::parent(const QModelIndex& index) const {
585 const void* const ptr = index.internalPointer();
586
587 if (!index.isValid()) {
588 // Should not happen.
589 return QModelIndex();
590 } else if (ptr == &m_allPagesNodeId) {
591 // 'All Pages' -> tree root.
592 return QModelIndex();
593 } else if (ptr == &m_sizeGroupNodeId) {
594 // Size group -> 'All Pages'.
595 return createIndex(0, index.column(), &m_allPagesNodeId);
596 } else {
597 // Image -> size group.
598 const auto* group = static_cast<const SizeGroup*>(ptr);
599
600 return createIndex(static_cast<int>(group - &m_sizes[0]), index.column(), &m_sizeGroupNodeId);
601 }
602 }
603
data(const QModelIndex & index,const int role) const604 QVariant FixDpiDialog::TreeModel::data(const QModelIndex& index, const int role) const {
605 const void* const ptr = index.internalPointer();
606
607 if (!index.isValid()) {
608 // Should not happen.
609 return QVariant();
610 } else if (ptr == &m_allPagesNodeId) {
611 // 'All Pages'.
612 if (role == Qt::DisplayRole) {
613 return FixDpiDialog::tr("All Pages");
614 } else if (role == AGGREGATE_METADATA_ROLE) {
615 return QVariant::fromValue(m_dpiCounts.aggregate(ALL));
616 } else if (role == AGGREGATE_NOT_OK_METADATA_ROLE) {
617 return QVariant::fromValue(m_dpiCounts.aggregate(NOT_OK));
618 }
619 } else if (ptr == &m_sizeGroupNodeId) {
620 // Size group.
621 const SizeGroup& group = m_sizes[index.row()];
622 if (role == Qt::DisplayRole) {
623 return sizeToString(group.size());
624 } else if (role == AGGREGATE_METADATA_ROLE) {
625 return QVariant::fromValue(group.dpiCounts().aggregate(ALL));
626 } else if (role == AGGREGATE_NOT_OK_METADATA_ROLE) {
627 return QVariant::fromValue(group.dpiCounts().aggregate(NOT_OK));
628 }
629 } else {
630 // Image.
631 const auto* group = static_cast<const SizeGroup*>(ptr);
632 const SizeGroup::Item& item = group->items()[index.row()];
633 const ImageFileInfo& file = m_files[item.fileIdx];
634 if (role == Qt::DisplayRole) {
635 const QString& fname = file.fileInfo().fileName();
636 if (file.imageInfo().size() == 1) {
637 return fname;
638 } else {
639 return FixDpiDialog::tr("%1 (page %2)").arg(fname).arg(item.imageIdx + 1);
640 }
641 } else if ((role == AGGREGATE_METADATA_ROLE) || (role == AGGREGATE_NOT_OK_METADATA_ROLE)) {
642 return QVariant::fromValue(file.imageInfo()[item.imageIdx]);
643 }
644 }
645
646 return QVariant();
647 } // FixDpiDialog::TreeModel::data
648
applyDpiToAllGroups(const Scope scope,const Dpi & dpi)649 void FixDpiDialog::TreeModel::applyDpiToAllGroups(const Scope scope, const Dpi& dpi) {
650 const auto num_groups = static_cast<const int>(m_sizes.size());
651 for (int i = 0; i < num_groups; ++i) {
652 applyDpiToGroup(scope, dpi, m_sizes[i], m_dpiCounts);
653 }
654 }
655
applyDpiToGroup(const Scope scope,const Dpi & dpi,SizeGroup & group,DpiCounts & total_dpi_counts)656 void FixDpiDialog::TreeModel::applyDpiToGroup(const Scope scope,
657 const Dpi& dpi,
658 SizeGroup& group,
659 DpiCounts& total_dpi_counts) {
660 DpiCounts& group_dpi_counts = group.dpiCounts();
661 const ImageMetadata metadata(group.size(), dpi);
662 const std::vector<SizeGroup::Item>& items = group.items();
663 const auto num_items = static_cast<const int>(items.size());
664 for (int i = 0; i < num_items; ++i) {
665 applyDpiToItem(scope, metadata, items[i], total_dpi_counts, group_dpi_counts);
666 }
667 }
668
applyDpiToItem(const Scope scope,const ImageMetadata & new_metadata,const SizeGroup::Item item,DpiCounts & total_dpi_counts,DpiCounts & group_dpi_counts)669 void FixDpiDialog::TreeModel::applyDpiToItem(const Scope scope,
670 const ImageMetadata& new_metadata,
671 const SizeGroup::Item item,
672 DpiCounts& total_dpi_counts,
673 DpiCounts& group_dpi_counts) {
674 ImageFileInfo& file = m_files[item.fileIdx];
675 ImageMetadata& old_metadata = file.imageInfo()[item.imageIdx];
676
677 if ((scope == NOT_OK) && old_metadata.isDpiOK()) {
678 return;
679 }
680
681 total_dpi_counts.add(new_metadata);
682 group_dpi_counts.add(new_metadata);
683 total_dpi_counts.remove(old_metadata);
684 group_dpi_counts.remove(old_metadata);
685
686 old_metadata = new_metadata;
687 }
688
emitAllPagesChanged(const QModelIndex & idx)689 void FixDpiDialog::TreeModel::emitAllPagesChanged(const QModelIndex& idx) {
690 const auto num_groups = static_cast<const int>(m_sizes.size());
691 for (int i = 0; i < num_groups; ++i) {
692 const QModelIndex group_node(index(i, 0, idx));
693 const int num_items = rowCount(group_node);
694 for (int j = 0; j < num_items; ++j) {
695 const QModelIndex image_node(index(j, 0, group_node));
696 emit dataChanged(image_node, image_node);
697 }
698 emit dataChanged(group_node, group_node);
699 }
700
701 // The 'All Pages' node.
702 emit dataChanged(idx, idx);
703 }
704
emitSizeGroupChanged(const QModelIndex & idx)705 void FixDpiDialog::TreeModel::emitSizeGroupChanged(const QModelIndex& idx) {
706 // Every item in this size group.
707 emit dataChanged(index(0, 0, idx), index(rowCount(idx), 0, idx));
708
709 // The size group itself.
710 emit dataChanged(idx, idx);
711
712 // The 'All Pages' node.
713 const QModelIndex all_pages_node(idx.parent());
714 emit dataChanged(all_pages_node, all_pages_node);
715 }
716
emitItemChanged(const QModelIndex & idx)717 void FixDpiDialog::TreeModel::emitItemChanged(const QModelIndex& idx) {
718 // The item itself.
719 emit dataChanged(idx, idx);
720
721 // The size group node.
722 const QModelIndex group_node(idx.parent());
723 emit dataChanged(group_node, group_node);
724 // The 'All Pages' node.
725 const QModelIndex all_pages_node(group_node.parent());
726 emit dataChanged(all_pages_node, all_pages_node);
727 }
728
sizeGroupFor(const QSize size)729 FixDpiDialog::SizeGroup& FixDpiDialog::TreeModel::sizeGroupFor(const QSize size) {
730 using namespace boost::lambda;
731
732 const auto it(std::find_if(m_sizes.begin(), m_sizes.end(), bind(&SizeGroup::size, _1) == size));
733 if (it != m_sizes.end()) {
734 return *it;
735 } else {
736 m_sizes.emplace_back(size);
737
738 return m_sizes.back();
739 }
740 }
741
sizeToString(const QSize size)742 QString FixDpiDialog::TreeModel::sizeToString(const QSize size) {
743 return QString("%1 x %2 px").arg(size.width()).arg(size.height());
744 }
745
746 /*====================== FixDpiDialog::FilterModel ======================*/
747
FilterModel(TreeModel & delegate)748 FixDpiDialog::FilterModel::FilterModel(TreeModel& delegate) : m_delegate(delegate) {
749 setDynamicSortFilter(true);
750 setSourceModel(delegate.model());
751 }
752
filterAcceptsRow(const int source_row,const QModelIndex & source_parent) const753 bool FixDpiDialog::FilterModel::filterAcceptsRow(const int source_row, const QModelIndex& source_parent) const {
754 return m_delegate.isVisibleForFilter(source_parent, source_row);
755 }
756
data(const QModelIndex & index,int role) const757 QVariant FixDpiDialog::FilterModel::data(const QModelIndex& index, int role) const {
758 if (role == AGGREGATE_METADATA_ROLE) {
759 role = AGGREGATE_NOT_OK_METADATA_ROLE;
760 }
761
762 return QSortFilterProxyModel::data(index, role);
763 }
764