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