BibleTime
btbookshelftreemodel.cpp
Go to the documentation of this file.
1 /*********
2 *
3 * In the name of the Father, and of the Son, and of the Holy Spirit.
4 *
5 * This file is part of BibleTime's source code, https://bibletime.info/
6 *
7 * Copyright 1999-2021 by the BibleTime developers.
8 * The BibleTime source code is licensed under the GNU General Public License
9 * version 2.0.
10 *
11 **********/
12 
13 #include "btbookshelftreemodel.h"
14 
15 #include <QDataStream>
16 #include <QModelIndexList>
17 #include <QPair>
18 #include <QtGlobal>
19 #include <type_traits>
20 #include <utility>
21 #include "../../util/btassert.h"
22 #include "../../util/btconnect.h"
23 #include "../../util/macros.h"
24 #include "../config/btconfigcore.h"
25 #include "../drivers/btconstmoduleset.h"
26 #include "../drivers/btmoduleset.h"
27 #include "categoryitem.h"
28 #include "indexingitem.h"
29 #include "languageitem.h"
30 #include "moduleitem.h"
31 
32 
33 using namespace BookshelfModel;
34 
36  const QString & key)
37 {
38  BT_ASSERT(!key.isNull());
39  QVariant v = config.qVariantValue(key, QVariant());
40  if (!v.canConvert<Grouping>())
41  return false;
42 
43  (*this) = v.value<Grouping>();
44  return true;
45 }
46 
48  QString const & key) const
49 {
50  BT_ASSERT(!key.isNull());
51  config.setValue(key, QVariant::fromValue(*this));
52 }
53 
55  : QAbstractItemModel(parent)
56  , m_rootItem(std::make_unique<RootItem>())
57  , m_defaultChecked(MODULE_HIDDEN)
58  , m_checkable(false) {}
59 
61  QString const & configKey,
62  QObject * parent)
63  : QAbstractItemModel(parent)
64  , m_rootItem(std::make_unique<RootItem>())
65  , m_groupingOrder(config, configKey)
66  , m_defaultChecked(MODULE_HIDDEN)
67  , m_checkable(false) {}
68 
70  QObject * parent)
71  : QAbstractItemModel(parent)
72  , m_rootItem(std::make_unique<RootItem>())
73  , m_groupingOrder(grouping)
74  , m_defaultChecked(MODULE_HIDDEN)
75  , m_checkable(false) {}
76 
78 
79 int BtBookshelfTreeModel::rowCount(const QModelIndex & parent) const {
80  return getItem(parent).children().size();
81 }
82 
83 int BtBookshelfTreeModel::columnCount(const QModelIndex & parent) const {
84  Q_UNUSED(parent)
85 
86  return 1;
87 }
88 
89 bool BtBookshelfTreeModel::hasChildren(const QModelIndex & parent) const {
90  return !getItem(parent).children().isEmpty();
91 }
92 
93 QModelIndex BtBookshelfTreeModel::index(int row, int column,
94  const QModelIndex & parent) const
95 {
96  if (!hasIndex(row, column, parent)) return QModelIndex();
97 
98  Item & parentItem = getItem(parent);
99  Item * const childItem = parentItem.children().at(row);
100  if (!childItem)
101  return QModelIndex();
102 
103  return createIndex(row, column, childItem);
104 }
105 
106 QModelIndex BtBookshelfTreeModel::parent(const QModelIndex & index) const {
107  if (!index.isValid())
108  return QModelIndex();
109 
110  Item * childItem(static_cast<Item*>(index.internalPointer()));
111  BT_ASSERT(childItem);
112  Item * parentItem(childItem->parent());
113  BT_ASSERT(parentItem);
114 
115  if (parentItem == m_rootItem.get())
116  return QModelIndex();
117 
118  return createIndex(parentItem->childIndex(), 0, parentItem);
119 }
120 
121 QVariant BtBookshelfTreeModel::data(const QModelIndex & index, int role) const {
122  if (!index.isValid() || index.column() != 0)
123  return QVariant();
124 
125  const Item * const i = static_cast<Item*>(index.internalPointer());
126  BT_ASSERT(i);
127  switch (role) {
128 
129  case Qt::CheckStateRole:
130  if (!m_checkable)
131  break;
132  [[fallthrough]];
133 
135  return i->checkState();
136 
138  /* This case is just an optimization. */
139  if (i->type() == Item::ITEM_MODULE) {
140  const ModuleItem & mi = *static_cast<const ModuleItem *>(i);
141  return QVariant::fromValue(static_cast<void *>(&mi.moduleInfo()));
142  }
143  return QVariant::fromValue(nullptr);
144 
145  case Qt::DisplayRole:
146  case Qt::DecorationRole:
148  default:
149  if (i->type() == Item::ITEM_MODULE)
150  return data(static_cast<const ModuleItem *>(i)->moduleInfo(), role);
151 
152  return i->data(role);
153 
154  }
155  return QVariant();
156 }
157 
158 QVariant BtBookshelfTreeModel::data(CSwordModuleInfo & module, int role) const {
159  BT_ASSERT(m_sourceIndexMap.contains(&module));
160  return m_sourceModel->data(m_sourceIndexMap.value(&module), role);
161 }
162 
163 bool BtBookshelfTreeModel::setData(const QModelIndex & itemIndex,
164  const QVariant & value,
165  int role) {
166  BT_ASSERT(itemIndex.isValid());
167  using IP = QPair<Item *, QModelIndex>;
168 
169  if (UNLIKELY(role != Qt::CheckStateRole))
170  return false;
171 
172  bool ok;
173  Qt::CheckState newState = static_cast<Qt::CheckState>(value.toInt(&ok));
174  if (UNLIKELY(!ok))
175  return false;
176 
177  // Handle partially checked as checked here in setData():
178  if (newState == Qt::PartiallyChecked)
179  newState = Qt::Checked;
180 
181  Item * item = static_cast<Item *>(itemIndex.internalPointer());
182  BT_ASSERT(item);
183  if (item->checkState() == newState) return false;
184 
185  // Recursively (un)check all children:
186  QList<IP> q;
187  IP p(item, itemIndex);
188  for (;;) {
189  if (item->checkState() != newState) {
190  item->setCheckState(newState);
191  Q_EMIT dataChanged(p.second, p.second);
192  if (item->type() == Item::ITEM_MODULE) {
193  ModuleItem & mItem = *static_cast<ModuleItem *>(item);
194  CSwordModuleInfo & mInfo = mItem.moduleInfo();
195  if (newState == Qt::Checked) {
196  m_checkedModulesCache.insert(&mInfo);
197  Q_EMIT moduleChecked(&mInfo, true);
198  } else {
199  m_checkedModulesCache.remove(&mInfo);
200  Q_EMIT moduleChecked(&mInfo, false);
201  }
202  } else {
203  const QList<Item *> & children = item->children();
204  for (int i = 0; i < children.size(); i++)
205  q.append(IP(children.at(i), index(i, 0, p.second)));
206  }
207  }
208  if (q.empty())
209  break;
210  p = q.takeFirst();
211  item = p.first;
212  }
213 
214  // Recursively change parent check states.
215  resetParentCheckStates(itemIndex.parent());
216 
217  return true;
218 }
219 
220 Qt::ItemFlags BtBookshelfTreeModel::flags(const QModelIndex & index) const {
221  if (!index.isValid())
222  return Qt::ItemFlags();
223 
224  Qt::ItemFlags f(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
225 
226  if (m_checkable) {
227  f |= Qt::ItemIsUserCheckable;
228 
229  const Item & i = *static_cast<Item*>(index.internalPointer());
230  if (i.type() != Item::ITEM_MODULE)
231  f |= Qt::ItemIsAutoTristate;
232  }
233 
234  return f;
235 }
236 
237 QVariant BtBookshelfTreeModel::headerData(int section,
238  Qt::Orientation orientation,
239  int role) const
240 {
241  if (orientation == Qt::Horizontal)
242  return m_sourceModel->headerData(section, orientation, role);
243 
244  return QVariant();
245 }
246 
248  std::shared_ptr<QAbstractItemModel> sourceModel)
249 {
250  if (m_sourceModel == sourceModel)
251  return;
252 
253  if (m_sourceModel) {
254  auto & model = *m_sourceModel;
255  disconnect(&model, &QAbstractItemModel::rowsAboutToBeRemoved,
257  disconnect(&model, &QAbstractItemModel::rowsInserted,
259  disconnect(&model, &QAbstractItemModel::dataChanged,
261  beginRemoveRows(QModelIndex(), 0, m_rootItem->children().size() - 1);
262  m_rootItem = std::make_unique<RootItem>();
263  m_modules.clear();
264  m_sourceIndexMap.clear();
265  m_checkedModulesCache.clear();
266  endRemoveRows();
267  }
268 
269  m_sourceModel = std::move(sourceModel);
270 
271  if (m_sourceModel) {
272  auto & model = *m_sourceModel;
273  BT_CONNECT(&model, &QAbstractItemModel::rowsAboutToBeRemoved,
275  BT_CONNECT(&model, &QAbstractItemModel::rowsInserted,
277  BT_CONNECT(&model, &QAbstractItemModel::dataChanged,
279 
280  for (int i = 0; i < model.rowCount(); i++) {
281  const QModelIndex moduleIndex(model.index(i, 0));
282  CSwordModuleInfo & module = *static_cast<CSwordModuleInfo *>(
283  model.data(moduleIndex,
284  BtBookshelfModel::ModulePointerRole).value<void*>());
285 
286  bool checked;
288  checked = !model.data(moduleIndex,
290  } else if (m_defaultChecked == MODULE_INDEXED) {
291  checked = !model.data(moduleIndex,
293  } else {
294  checked = (m_defaultChecked == CHECKED);
295  }
296  m_sourceIndexMap[&module] = moduleIndex;
297  addModule(module, checked);
298  }
299  }
300 }
301 
303  bool emitSignal)
304 {
306  return;
307 
309 
310  if (m_sourceModel != nullptr) {
311  BtModuleSet const checked(m_checkedModulesCache);
312  m_checkedModulesCache.clear();
313 
314  beginRemoveRows(QModelIndex(), 0, m_rootItem->children().size() - 1);
315  m_rootItem = std::make_unique<RootItem>();
316  m_modules.clear();
317  endRemoveRows();
318 
319  for (int i = 0; i < m_sourceModel->rowCount(); i++) {
320  const QModelIndex sourceIndex(m_sourceModel->index(i, 0));
321  CSwordModuleInfo & module = *static_cast<CSwordModuleInfo *>(
322  m_sourceModel->data(sourceIndex,
323  BtBookshelfModel::ModulePointerRole).value<void *>());
324  m_sourceIndexMap[&module] = sourceIndex;
325  addModule(module, checked.contains(&module));
326  }
327  }
328 
329  if (emitSignal)
331 }
332 
334  if (m_checkable == checkable)
335  return;
337  if (m_sourceModel == nullptr)
338  return;
339 
340  // Notify views that flags changed for all items:
341  resetData();
342 }
343 
345  for (auto it = m_modules.constBegin(); it != m_modules.constEnd(); ++it) {
346  if (modules.contains(it.key())) {
347  setData(getIndex(*it.value()), Qt::Checked, Qt::CheckStateRole);
348  } else {
349  setData(getIndex(*it.value()), Qt::Unchecked, Qt::CheckStateRole);
350  }
351  }
352 }
353 
355  QModelIndexList queue;
356  queue.append(QModelIndex());
357  do {
358  QModelIndex parent(queue.takeFirst());
359  Q_EMIT dataChanged(index(0, 0, parent),
360  index(rowCount(parent) - 1, columnCount() - 1, parent));
361  for (int i = 0; i < rowCount(parent); i++) {
362  const QModelIndex childIndex(index(i, 0, parent));
363  if (rowCount(childIndex) > 0)
364  queue.append(childIndex);
365  }
366  } while (!queue.isEmpty());
367 }
368 
369 void BtBookshelfTreeModel::addModule(CSwordModuleInfo & module, bool checked) {
370  if (m_modules.contains(&module))
371  return;
372 
373  beginResetModel();
375  addModule(module, QModelIndex(), g, checked);
376 
377  /**
378  \bug Calling reset() shouldn't be necessary here, but omitting it will
379  will break things like switching to a grouped layout or installing
380  new modules. As a side effect, all attached views will also reset
381  themselves.
382  */
383 
384  endResetModel();
385 }
386 
388  QModelIndex parentIndex,
389  Grouping & intermediateGrouping,
390  bool checked)
391 {
392  if (!intermediateGrouping.empty()) {
393  QModelIndex newIndex;
394  switch (intermediateGrouping.front()) {
395 
396  case GROUP_CATEGORY:
397  newIndex = getGroup<CategoryItem>(module, parentIndex);
398  break;
399 
400  case GROUP_LANGUAGE:
401  newIndex = getGroup<LanguageItem>(module, parentIndex);
402  break;
403 
404  case GROUP_INDEXING:
405  newIndex = getGroup<IndexingItem>(module, parentIndex);
406  break;
407 
408  }
409  intermediateGrouping.pop_front();
410  addModule(module, newIndex, intermediateGrouping, checked);
411  } else {
412  Item & parentItem = getItem(parentIndex);
413  ModuleItem * const newItem = new ModuleItem(module, *this);
414  newItem->setCheckState(checked ? Qt::Checked : Qt::Unchecked);
415  const int newIndex(parentItem.indexFor(*newItem));
416 
417  // Actually do the insertion:
418  beginInsertRows(parentIndex, newIndex, newIndex);
419  parentItem.insertChild(newIndex, newItem);
420  m_modules.insert(&module, newItem);
421  if (checked) // Add to checked modules cache
422  m_checkedModulesCache.insert(&module);
423 
424  endInsertRows();
425 
426  // Reset parent item check states, if needed:
427  resetParentCheckStates(parentIndex);
428  }
429 }
430 
432  auto const it = m_modules.find(&module);
433  if (it == m_modules.end())
434  return;
435 
436  Item * i = it.value();
437 
438  // Set i to be the lowest item (including empty groups) to remove:
439  BT_ASSERT(i->parent());
440  while (i->parent() != m_rootItem.get()
441  && i->parent()->children().size() <= 1)
442  i = i->parent();
443  BT_ASSERT(i);
444  BT_ASSERT(i->parent());
445 
446  // Calculate item indexes:
447  const int index = i->childIndex();
448  const QModelIndex parentIndex(getIndex(*i->parent()));
449 
450  // Actually remove the item:
451  beginRemoveRows(parentIndex, index, index);
452  delete i->parent()->children().takeAt(index);
453  m_modules.erase(it);
454  m_checkedModulesCache.remove(&module);
455  endRemoveRows();
456 
457  // Reset parent item check states, if needed:
458  resetParentCheckStates(parentIndex);
459 }
460 
461 Item & BtBookshelfTreeModel::getItem(const QModelIndex & index) const {
462  if (UNLIKELY(!index.isValid()))
463  return *m_rootItem;
464 
465  Item * const item = static_cast<Item *>(index.internalPointer());
466  BT_ASSERT(item);
467  return *item;
468 }
469 
471  const BookshelfModel::Item * it = &item;
472  QList<int> indexes;
473  for (;;) {
474  const int i = it->childIndex();
475  if (i < 0)
476  break;
477  indexes.append(i);
478  it = it->parent();
479  }
480 
481  QModelIndex i;
482  while (!indexes.isEmpty())
483  i = index(indexes.takeLast(), 0, i);
484  return i;
485 }
486 
487 void BtBookshelfTreeModel::resetParentCheckStates(QModelIndex parentIndex) {
488  for ( ; parentIndex.isValid(); parentIndex = parentIndex.parent()) {
489  Item & parentItem = *static_cast<Item *>(parentIndex.internalPointer());
490 
491  const Qt::CheckState oldState = parentItem.checkState();
492  bool haveCheckedChildren = false;
493  bool haveUncheckedChildren = false;
494  for (int i = 0; i < parentItem.children().size(); i++) {
495  const Qt::CheckState state = parentItem.children().at(i)->checkState();
496  if (state == Qt::PartiallyChecked) {
497  haveCheckedChildren = true;
498  haveUncheckedChildren = true;
499  break;
500  } else if (state == Qt::Checked) {
501  haveCheckedChildren = true;
502  if (haveUncheckedChildren)
503  break;
504  } else {
505  BT_ASSERT(state == Qt::Unchecked);
506  haveUncheckedChildren = true;
507  if (haveCheckedChildren)
508  break;
509  }
510  }
511 
512  Qt::CheckState newState;
513  if (haveCheckedChildren) {
514  if (haveUncheckedChildren) {
515  newState = Qt::PartiallyChecked;
516  } else {
517  newState = Qt::Checked;
518  }
519  } else {
520  newState = Qt::Unchecked;
521  }
522  if (newState == oldState)
523  break;
524 
525  parentItem.setCheckState(newState);
526  Q_EMIT dataChanged(parentIndex, parentIndex);
527  } // for ( ; parentIndex.isValid(); parentIndex = parentIndex.parent())
528 }
529 
530 void BtBookshelfTreeModel::moduleDataChanged(const QModelIndex & topLeft,
531  const QModelIndex & bottomRight)
532 {
533  BT_ASSERT(!topLeft.parent().isValid());
534  BT_ASSERT(!bottomRight.parent().isValid());
535  BT_ASSERT(topLeft.column() == 0);
536  BT_ASSERT(bottomRight.column() == 0);
537 
538  for (int i = topLeft.row(); i <= bottomRight.row(); i++) {
539  const QModelIndex moduleIndex(m_sourceModel->index(i, 0, topLeft.parent()));
540  const QVariant data(m_sourceModel->data(moduleIndex,
542  CSwordModuleInfo & module = *static_cast<CSwordModuleInfo *>(data.value<void *>());
543  QModelIndex itemIndex(getIndex(*m_modules[&module]));
544  BT_ASSERT(itemIndex.isValid());
545 
546  Q_EMIT dataChanged(itemIndex, itemIndex);
547 
548  /*
549  Also emit signals for parent items because the change might alter them
550  as well, e.g. isHidden()
551  */
552  do {
553  itemIndex = itemIndex.parent();
554  Q_EMIT dataChanged(itemIndex, itemIndex);
555  } while (itemIndex.isValid());
556  }
557 }
558 
559 void BtBookshelfTreeModel::moduleInserted(const QModelIndex & parent,
560  int start,
561  int end)
562 {
563  BT_ASSERT(start <= end);
564 
565  for (int i = start; i <= end; i++) {
566  const QModelIndex moduleIndex(m_sourceModel->index(i, 0, parent));
567  const QVariant data(m_sourceModel->data(moduleIndex,
569  CSwordModuleInfo & module = *static_cast<CSwordModuleInfo *>(data.value<void *>());
570 
571  bool checked;
573  checked = !m_sourceModel->data(moduleIndex,
575  } else if (m_defaultChecked == MODULE_INDEXED) {
576  checked = !m_sourceModel->data(moduleIndex,
578  } else {
580  checked = (m_defaultChecked == CHECKED);
581  }
582  m_sourceIndexMap[&module] = moduleIndex;
583  addModule(module, checked);
584  }
585 }
586 
587 void BtBookshelfTreeModel::moduleRemoved(const QModelIndex & parent,
588  int start,
589  int end)
590 {
591  BT_ASSERT(start <= end);
592 
593  for (int i = start; i <= end; i++) {
594  const QModelIndex moduleIndex(m_sourceModel->index(i, 0, parent));
595  const QVariant data(m_sourceModel->data(moduleIndex,
597  CSwordModuleInfo & module = *static_cast<CSwordModuleInfo *>(data.value<void*>());
599  m_sourceIndexMap.remove(&module);
600  }
601 }
602 
603 QDataStream & operator <<(QDataStream & os,
605 {
606  os << o.size();
607  for (BtBookshelfTreeModel::Group const g : o)
608  os << static_cast<std::underlying_type<decltype(g)>::type>(g);
609  return os;
610 }
611 
612 QDataStream & operator >>(QDataStream & is,
614 {
615  int s;
616  is >> s;
617  o.clear();
618  for (int i = 0; i < s; i++) {
619  int g;
620  is >> g;
621  o.append(static_cast<BtBookshelfTreeModel::Group>(g));
622  }
623  return is;
624 }
#define BT_ASSERT(...)
Definition: btassert.h:17
QDataStream & operator<<(QDataStream &os, const BtBookshelfTreeModel::Grouping &o)
QDataStream & operator>>(QDataStream &is, BtBookshelfTreeModel::Grouping &o)
#define BT_CONNECT(...)
Definition: btconnect.h:20
void setCheckState(const Qt::CheckState state)
Sets the check state of this item.
Definition: item.h:119
virtual QVariant data(int role=Qt::DisplayRole) const
Returns data for this item.
Definition: item.cpp:44
int indexFor(Item const &newItem)
Returns the position for where the given child item would be inserted.
Definition: item.cpp:27
Type type() const
Returns the type of this item.
Definition: item.h:49
QList< Item * > & children()
Returns the list of child items of this node.
Definition: item.h:60
int childIndex() const
Returns the index of this item under its parent.
Definition: item.h:66
Item * parent() const
Returns a pointer to the parent item of this item.
Definition: item.h:55
void insertChild(int index, Item *newItem)
Inserts the given item as a child at the given index.
Definition: item.h:85
Qt::CheckState checkState() const
Returns the check state of this item.
Definition: item.h:113
CSwordModuleInfo & moduleInfo() const
Definition: moduleitem.h:42
void saveTo(BtConfigCore &config, QString const &key) const
bool loadFrom(BtConfigCore const &config, QString const &key)
void moduleInserted(const QModelIndex &parent, int start, int end)
CheckedBehavior m_defaultChecked
std::unique_ptr< BookshelfModel::Item > m_rootItem
void groupingOrderChanged(BtBookshelfTreeModel::Grouping newGrouping)
void removeModule(CSwordModuleInfo &module)
void resetParentCheckStates(QModelIndex parentIndex)
Qt::ItemFlags flags(const QModelIndex &index) const override
QModelIndex getIndex(const BookshelfModel::Item &item)
void moduleRemoved(const QModelIndex &parent, int start, int end)
QModelIndex parent(const QModelIndex &index) const override
BookshelfModel::Item & getItem(const QModelIndex &index) const
std::shared_ptr< QAbstractItemModel > m_sourceModel
void setCheckable(bool checkable)
void setCheckedModules(BtConstModuleSet const &modules)
BtBookshelfTreeModel(QObject *parent=nullptr)
std::shared_ptr< QAbstractItemModel > sourceModel() const noexcept
bool setData(const QModelIndex &index, const QVariant &value, int role) override
QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const override
int columnCount(const QModelIndex &parent=QModelIndex()) const override
~BtBookshelfTreeModel() override
void setGroupingOrder(const BtBookshelfTreeModel::Grouping &groupingOrder, bool emitSignal=true)
QVariant headerData(int section, Qt::Orientation orientation, int role=Qt::DisplayRole) const override
SourceIndexMap m_sourceIndexMap
Grouping const & groupingOrder() const
void moduleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight)
QList< CSwordModuleInfo * > modules() const
int rowCount(const QModelIndex &parent=QModelIndex()) const override
bool hasChildren(const QModelIndex &parent=QModelIndex()) const override
void setSourceModel(std::shared_ptr< QAbstractItemModel > sourceModel)
CSwordModuleInfo * module(QModelIndex const &index) const
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
void moduleChecked(CSwordModuleInfo *module, bool checked)
void addModule(CSwordModuleInfo &module, bool checked)
void setValue(QString const &key, T const &value)
Sets a value for a key.
Definition: btconfigcore.h:73
QVariant qVariantValue(QString const &key, QVariant const &defaultValue=QVariant()) const
Returns the settings value for the given global key as a QVariant.
bool contains(CSwordModuleInfo const *const m) const
Definition: btmoduleset.h:30
#define UNLIKELY(c)
Gives the compiler a hint that the given conditional is likely to evaluate to false.
Definition: macros.h:44