1 /***************************************************************************
2 Copyright (C) 2001-2020 Robby Stephenson <robby@periapsis.org>
3 ***************************************************************************/
4
5 /***************************************************************************
6 * *
7 * This program is free software; you can redistribute it and/or *
8 * modify it under the terms of the GNU General Public License as *
9 * published by the Free Software Foundation; either version 2 of *
10 * the License or (at your option) version 3 or any later version *
11 * accepted by the membership of KDE e.V. (or its successor approved *
12 * by the membership of KDE e.V.), which shall act as a proxy *
13 * defined in Section 14 of version 3 of the license. *
14 * *
15 * This program is distributed in the hope that it will be useful, *
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
18 * GNU General Public License for more details. *
19 * *
20 * You should have received a copy of the GNU General Public License *
21 * along with this program. If not, see <http://www.gnu.org/licenses/>. *
22 * *
23 ***************************************************************************/
24
25 #include "groupview.h"
26 #include "collection.h"
27 #include "field.h"
28 #include "entry.h"
29 #include "entrygroup.h"
30 #include "filter.h"
31 #include "controller.h"
32 #include "models/entrygroupmodel.h"
33 #include "models/groupsortmodel.h"
34 #include "models/modelmanager.h"
35 #include "models/models.h"
36 #include "gui/countdelegate.h"
37 #include "tellico_debug.h"
38
39 #include <KLocalizedString>
40
41 #include <QMenu>
42 #include <QIcon>
43 #include <QStringList>
44 #include <QColor>
45 #include <QHeaderView>
46 #include <QContextMenuEvent>
47
48 using Tellico::GroupView;
49
GroupView(QWidget * parent_)50 GroupView::GroupView(QWidget* parent_)
51 : GUI::TreeView(parent_), m_notSortedYet(true) {
52 header()->setSectionResizeMode(QHeaderView::Stretch);
53 setHeaderHidden(false);
54 setSelectionMode(QAbstractItemView::ExtendedSelection);
55
56 connect(this, &GroupView::expanded, this, &GroupView::slotExpanded);
57 connect(this, &GroupView::collapsed, this, &GroupView::slotCollapsed);
58 connect(this, &GroupView::doubleClicked, this, &GroupView::slotDoubleClicked);
59 connect(header(), &QHeaderView::sortIndicatorChanged, this, &GroupView::slotSortingChanged);
60
61 m_groupOpenIconName = QStringLiteral("folder-open");
62 m_groupClosedIconName = QStringLiteral("folder");
63 EntryGroupModel* groupModel = new EntryGroupModel(this);
64 GroupSortModel* sortModel = new GroupSortModel(this);
65 sortModel->setSourceModel(groupModel);
66 setModel(sortModel);
67 setItemDelegate(new GUI::CountDelegate(this));
68
69 ModelManager::self()->setGroupModel(model());
70 }
71
sourceModel() const72 Tellico::EntryGroupModel* GroupView::sourceModel() const {
73 return static_cast<EntryGroupModel*>(sortModel()->sourceModel());
74 }
75
addCollection(Tellico::Data::CollPtr coll_)76 void GroupView::addCollection(Tellico::Data::CollPtr coll_) {
77 if(!coll_) {
78 myWarning() << "null coll pointer!";
79 return;
80 }
81
82 m_coll = coll_;
83 // if the collection doesn't have the grouped field, and it's not the pseudo-group,
84 // change it to default
85 if(m_groupBy.isEmpty() || (!coll_->hasField(m_groupBy) && m_groupBy != Data::Collection::s_peopleGroupName)) {
86 m_groupBy = coll_->defaultGroupField();
87 }
88
89 // when the coll gets set for the first time, the pixmaps need to be updated
90 if((m_coll->hasField(m_groupBy) && m_coll->fieldByName(m_groupBy)->formatType() == FieldFormat::FormatName)
91 || m_groupBy == Data::Collection::s_peopleGroupName) {
92 m_groupOpenIconName = QStringLiteral(":/icons/person-open");
93 m_groupClosedIconName = QStringLiteral(":/icons/person");
94 }
95
96 updateHeader();
97 populateCollection();
98 setEntrySortField(m_entrySortField);
99 }
100
removeCollection(Tellico::Data::CollPtr coll_)101 void GroupView::removeCollection(Tellico::Data::CollPtr coll_) {
102 if(!coll_) {
103 myWarning() << "null coll pointer!";
104 return;
105 }
106
107 blockSignals(true);
108 slotReset();
109 blockSignals(false);
110 }
111
populateCollection()112 void GroupView::populateCollection() {
113 if(!m_coll) {
114 return;
115 }
116
117 // myDebug() << m_groupBy;
118 if(m_groupBy.isEmpty()) {
119 m_groupBy = m_coll->defaultGroupField();
120 }
121
122 setUpdatesEnabled(false);
123 sourceModel()->clear(); // delete all groups
124
125 // if there's no group field, just return
126 if(m_groupBy.isEmpty()) {
127 setUpdatesEnabled(true);
128 return;
129 }
130
131 Data::EntryGroupDict* dict = m_coll->entryGroupDictByName(m_groupBy);
132 if(!dict) { // could happen if m_groupBy is non empty, but there are no entries with a value
133 setUpdatesEnabled(true);
134 return;
135 }
136
137 // add all the groups
138 sourceModel()->addGroups(dict->values(), m_groupClosedIconName);
139 // still need to change icon for empty group
140 foreach(Data::EntryGroup* group, *dict) {
141 if(group->hasEmptyGroupName()) {
142 QModelIndex emptyGroupIndex = sourceModel()->indexFromGroup(group);
143 sourceModel()->setData(emptyGroupIndex, QLatin1String("folder-red"), Qt::DecorationRole);
144 break;
145 }
146 }
147
148 setUpdatesEnabled(true);
149 // must re-sort in order to update view
150 model()->sort(0, sortOrder());
151 }
152
153 // don't 'shadow' QListView::setSelected
setEntrySelected(Tellico::Data::EntryPtr entry_)154 void GroupView::setEntrySelected(Tellico::Data::EntryPtr entry_) {
155 // DEBUG_LINE;
156 // if entry_ is null pointer, set no selected
157 if(!entry_) {
158 // don't move this one outside the block since it calls setCurrentItem(0)
159 clearSelection();
160 return;
161 }
162
163 // if the selected entry is the same as the current one, just return
164 QModelIndex curr = currentIndex();
165 if(entry_ == sourceModel()->entry(curr)) {
166 return;
167 }
168
169 // have to find a group whose field is the same as currently shown
170 if(m_groupBy.isEmpty()) {
171 myDebug() << "no group field";
172 return;
173 }
174
175 Data::EntryGroup* group = nullptr;
176 foreach(Data::EntryGroup* tmpGroup, entry_->groups()) {
177 if(tmpGroup->fieldName() == m_groupBy) {
178 group = tmpGroup;
179 break;
180 }
181 }
182 if(!group) {
183 myDebug() << "entry is not in any current groups!";
184 return;
185 }
186
187 QModelIndex index = sourceModel()->indexFromGroup(group);
188
189 clearSelection();
190 for(; index.isValid(); index = index.sibling(index.row()+1, 0)) {
191 if(sourceModel()->entry(index) == entry_) {
192 blockSignals(true);
193 selectionModel()->select(index, QItemSelectionModel::Select);
194 setCurrentIndex(index);
195 blockSignals(false);
196 scrollTo(index);
197 break;
198 }
199 }
200 }
201
modifyField(Tellico::Data::CollPtr,Tellico::Data::FieldPtr,Tellico::Data::FieldPtr newField_)202 void GroupView::modifyField(Tellico::Data::CollPtr, Tellico::Data::FieldPtr, Tellico::Data::FieldPtr newField_) {
203 if(newField_->name() == m_groupBy) {
204 updateHeader(newField_);
205 }
206 // if the grouping changed at all, our groups got deleted out from under us
207 populateCollection();
208 }
209
slotReset()210 void GroupView::slotReset() {
211 sourceModel()->clear();
212 }
213
slotModifyGroups(Tellico::Data::CollPtr coll_,QList<Tellico::Data::EntryGroup * > groups_)214 void GroupView::slotModifyGroups(Tellico::Data::CollPtr coll_, QList<Tellico::Data::EntryGroup*> groups_) {
215 if(!coll_ || groups_.isEmpty()) {
216 myWarning() << "null coll or group pointer!";
217 return;
218 }
219
220 /* for each group
221 - remove existing empty ones
222 - modify existing ones
223 - add new ones
224 */
225 foreach(Data::EntryGroup* group, groups_) {
226 // if the entries aren't grouped by field of the modified group,
227 // we don't care, so skip
228 if(m_groupBy != group->fieldName()) {
229 continue;
230 }
231
232 // myDebug() << group->fieldName() << "/" << group->groupName();
233 QModelIndex groupIndex = sourceModel()->indexFromGroup(group);
234 if(groupIndex.isValid()) {
235 if(group->isEmpty()) {
236 sourceModel()->removeGroup(group);
237 continue;
238 }
239 sourceModel()->modifyGroup(group);
240 } else {
241 if(group->isEmpty()) {
242 myDebug() << "trying to add empty group";
243 continue;
244 }
245 addGroup(group);
246 }
247 }
248 // don't want any selected
249 clearSelection();
250 }
251
contextMenuEvent(QContextMenuEvent * event_)252 void GroupView::contextMenuEvent(QContextMenuEvent* event_) {
253 QModelIndex index = indexAt(event_->pos());
254 if(!index.isValid()) {
255 return;
256 }
257
258 QMenu menu(this);
259 // no parent means it's a top-level item
260 if(!index.parent().isValid()) {
261 menu.addAction(QIcon::fromTheme(QStringLiteral("arrow-down-double")),
262 i18n("Expand All Groups"), this, &QTreeView::expandAll);
263 menu.addAction(QIcon::fromTheme(QStringLiteral("arrow-up-double")),
264 i18n("Collapse All Groups"), this, &QTreeView::collapseAll);
265 menu.addAction(QIcon::fromTheme(QStringLiteral("view-filter")),
266 i18n("Filter by Group"), this, &GroupView::slotFilterGroup);
267 } else {
268 Controller::self()->plugEntryActions(&menu);
269 }
270 menu.addSeparator();
271 QMenu* sortMenu = Controller::self()->plugSortActions(&menu);
272 connect(sortMenu, &QMenu::triggered, this, &GroupView::slotSortMenuActivated);
273 menu.exec(event_->globalPos());
274 }
275
slotCollapsed(const QModelIndex & index_)276 void GroupView::slotCollapsed(const QModelIndex& index_) {
277 if(model()->data(index_, Qt::DecorationRole).toString() == m_groupOpenIconName) {
278 model()->setData(index_, m_groupClosedIconName, Qt::DecorationRole);
279 }
280 }
281
slotExpanded(const QModelIndex & index_)282 void GroupView::slotExpanded(const QModelIndex& index_) {
283 if(model()->data(index_, Qt::DecorationRole).toString() == m_groupClosedIconName) {
284 model()->setData(index_, m_groupOpenIconName, Qt::DecorationRole);
285 }
286 }
287
setGroupField(const QString & groupField_)288 void GroupView::setGroupField(const QString& groupField_) {
289 // myDebug() << groupField_;
290 if(groupField_.isEmpty() || groupField_ == m_groupBy) {
291 return;
292 }
293
294 m_groupBy = groupField_;
295 if(!m_coll) {
296 return; // can't do anything yet, but still need to set the variable
297 }
298 if((m_coll->hasField(groupField_) && m_coll->fieldByName(groupField_)->formatType() == Tellico::FieldFormat::FormatName)
299 || groupField_ == Tellico::Data::Collection::s_peopleGroupName) {
300 m_groupOpenIconName = QStringLiteral(":/icons/person-open");
301 m_groupClosedIconName = QStringLiteral(":/icons/person");
302 } else {
303 m_groupOpenIconName = QStringLiteral("folder-open");
304 m_groupClosedIconName = QStringLiteral("folder");
305 }
306 updateHeader();
307 populateCollection();
308 }
309
entrySortField() const310 QString GroupView::entrySortField() const {
311 return m_entrySortField;
312 }
313
setEntrySortField(const QString & groupSortName_)314 void GroupView::setEntrySortField(const QString& groupSortName_) {
315 m_entrySortField = groupSortName_;
316 GroupSortModel* model = static_cast<GroupSortModel*>(sortModel());
317 Q_ASSERT(model);
318 model->setEntrySortField(groupSortName_);
319 }
320
slotFilterGroup()321 void GroupView::slotFilterGroup() {
322 QModelIndexList indexes = selectionModel()->selectedIndexes();
323 if(indexes.isEmpty()) {
324 return;
325 }
326
327 FilterPtr filter(new Filter(Filter::MatchAny));
328 foreach(const QModelIndex& index, indexes) {
329 // the indexes pointing to a group have no parent
330 if(index.parent().isValid()) {
331 continue;
332 }
333
334 if(!model()->hasChildren(index)) { //ignore empty items
335 continue;
336 }
337 // need to check for people group
338 if(m_groupBy == Data::Collection::s_peopleGroupName) {
339 QModelIndex firstChild = model()->index(0, 0, index);
340 QModelIndex sourceIndex = sortModel()->mapToSource(firstChild);
341 Data::EntryPtr entry = sourceModel()->entry(sourceIndex);
342 Q_ASSERT(entry);
343 Data::FieldList fields = entry->collection()->peopleFields();
344 foreach(Data::FieldPtr field, fields) {
345 filter->append(new FilterRule(field->name(),
346 FieldFormat::matchValueRegularExpression(model()->data(index).toString()),
347 FilterRule::FuncRegExp));
348 }
349 } else {
350 Data::EntryGroup* group = model()->data(index, GroupPtrRole).value<Data::EntryGroup*>();
351 if(group) {
352 if(group->hasEmptyGroupName()) {
353 filter->append(new FilterRule(m_groupBy, QString(), FilterRule::FuncEquals));
354 } else {
355 // if the field does not allow multiple values and is not a table
356 // then can just do an equal match
357 Data::FieldPtr field = group->at(0)->collection()->fieldByName(group->fieldName());
358 if(field && field->type() != Data::Field::Table && !field->hasFlag(Data::Field::AllowMultiple)) {
359 filter->append(new FilterRule(m_groupBy, group->groupName(), FilterRule::FuncEquals));
360 } else {
361 filter->append(new FilterRule(m_groupBy,
362 FieldFormat::matchValueRegularExpression(group->groupName()),
363 FilterRule::FuncRegExp));
364 }
365 }
366 }
367 }
368 }
369
370 if(!filter->isEmpty()) {
371 emit signalUpdateFilter(filter);
372 }
373 }
374
slotDoubleClicked(const QModelIndex & index_)375 void GroupView::slotDoubleClicked(const QModelIndex& index_) {
376 QModelIndex realIndex = sortModel()->mapToSource(index_);
377 Data::EntryPtr entry = sourceModel()->entry(realIndex);
378 if(entry) {
379 Controller::self()->editEntry(entry);
380 }
381 }
382
383 // this gets called when header() is clicked, so cycle through
slotSortingChanged(int col_,Qt::SortOrder order_)384 void GroupView::slotSortingChanged(int col_, Qt::SortOrder order_) {
385 Q_UNUSED(col_);
386 if(order_ == Qt::AscendingOrder && !m_notSortedYet) { // cycle through after ascending
387 if(sortModel()->sortRole() == RowCountRole) {
388 sortModel()->setSortRole(Qt::DisplayRole);
389 } else {
390 sortModel()->setSortRole(RowCountRole);
391 }
392 }
393
394 updateHeader();
395 m_notSortedYet = false;
396 }
397
slotSortMenuActivated(QAction * action_)398 void GroupView::slotSortMenuActivated(QAction* action_) {
399 Data::FieldPtr field = action_->data().value<Data::FieldPtr>();
400 Q_ASSERT(field);
401 if(!field) {
402 return;
403 }
404 setEntrySortField(field->name());
405 }
406
updateHeader(Tellico::Data::FieldPtr field_)407 void GroupView::updateHeader(Tellico::Data::FieldPtr field_/*=0*/) {
408 QString t = field_ ? field_->title() : groupTitle();
409 if(sortModel()->sortRole() == Qt::DisplayRole) {
410 model()->setHeaderData(0, Qt::Horizontal, t);
411 } else {
412 model()->setHeaderData(0, Qt::Horizontal, i18n("%1 (Sort by Count)", t));
413 }
414 }
415
groupTitle()416 QString GroupView::groupTitle() {
417 QString title;
418 if(!m_coll || m_groupBy.isEmpty()) {
419 title = i18nc("Group Name Header", "Group");
420 } else {
421 Data::FieldPtr f = m_coll->fieldByName(m_groupBy);
422 if(f) {
423 title = f->title();
424 } else if(m_groupBy == Data::Collection::s_peopleGroupName) {
425 title = i18n("People");
426 }
427 }
428 return title;
429 }
430
addGroup(Tellico::Data::EntryGroup * group_)431 void GroupView::addGroup(Tellico::Data::EntryGroup* group_) {
432 if(group_->isEmpty()) {
433 return;
434 }
435 QModelIndex index = sourceModel()->addGroup(group_);
436 if(group_->hasEmptyGroupName()) {
437 sourceModel()->setData(index, QLatin1String("folder-red"), Qt::DecorationRole);
438 } else {
439 sourceModel()->setData(index, m_groupClosedIconName, Qt::DecorationRole);
440 }
441 }
442