1 /**
2  * UGENE - Integrated Bioinformatics Tools.
3  * Copyright (C) 2008-2021 UniPro <ugene@unipro.ru>
4  * http://ugene.net
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19  * MA 02110-1301, USA.
20  */
21 
22 #include "ProjectViewFilterModel.h"
23 
24 #include <QFontMetrics>
25 
26 #include <U2Core/AppContext.h>
27 #include <U2Core/BunchMimeData.h>
28 #include <U2Core/L10n.h>
29 #include <U2Core/ProjectModel.h>
30 #include <U2Core/U2ObjectDbi.h>
31 #include <U2Core/U2SafePoints.h>
32 
33 #include "FilteredProjectGroup.h"
34 #include "ProjectFilterNames.h"
35 #include "ProjectUtils.h"
36 #include "ProjectViewModel.h"
37 
38 namespace U2 {
39 
ProjectViewFilterModel(ProjectViewModel * srcModel,const ProjectTreeControllerModeSettings & settings,QObject * p)40 ProjectViewFilterModel::ProjectViewFilterModel(ProjectViewModel *srcModel, const ProjectTreeControllerModeSettings &settings, QObject *p)
41     : QAbstractItemModel(p), settings(settings), srcModel(srcModel) {
42     SAFE_POINT(nullptr != srcModel, L10N::nullPointerError("Project view model"), );
43     connect(&filterController, SIGNAL(si_objectsFiltered(const QString &, const QList<QPointer<GObject>> &)), SLOT(sl_objectsFiltered(const QString &, const QList<QPointer<GObject>> &)));
44     connect(&filterController, SIGNAL(si_filteringStarted()), SIGNAL(si_filteringStarted()));
45     connect(&filterController, SIGNAL(si_filteringFinished()), SIGNAL(si_filteringFinished()));
46 
47     connect(srcModel, SIGNAL(rowsAboutToBeRemoved(const QModelIndex &, int, int)), SLOT(sl_rowsAboutToBeRemoved(const QModelIndex &, int, int)));
48     connect(srcModel, SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &)), SLOT(sl_dataChanged(const QModelIndex &, const QModelIndex &)));
49 }
50 
~ProjectViewFilterModel()51 ProjectViewFilterModel::~ProjectViewFilterModel() {
52     clearFilterGroups();
53 }
54 
55 namespace {
56 
getAllDocumentsSafely()57 QList<QPointer<Document>> getAllDocumentsSafely() {
58     QList<QPointer<Document>> result;
59 
60     Project *proj = AppContext::getProject();
61     SAFE_POINT(nullptr != proj, L10N::nullPointerError("project"), result);
62     foreach (Document *doc, proj->getDocuments()) {
63         result.append(doc);
64     }
65     return result;
66 }
67 
68 }  // namespace
69 
updateSettings(const ProjectTreeControllerModeSettings & newSettings)70 void ProjectViewFilterModel::updateSettings(const ProjectTreeControllerModeSettings &newSettings) {
71     settings = newSettings;
72     clearFilterGroups();
73 
74     if (settings.isObjectFilterActive()) {
75         const QList<QPointer<Document>> allDocs = getAllDocumentsSafely();
76         CHECK(!allDocs.isEmpty(), );
77         filterController.startFiltering(settings, allDocs);
78     }
79 }
80 
addFilteredObject(const QString & filterGroupName,GObject * obj)81 void ProjectViewFilterModel::addFilteredObject(const QString &filterGroupName, GObject *obj) {
82     SAFE_POINT(!filterGroupName.isEmpty(), "Empty project filter group name", );
83     SAFE_POINT(nullptr != obj, L10N::nullPointerError("object"), );
84 
85     if (!hasFilterGroup(filterGroupName)) {
86         addFilterGroup(filterGroupName);
87     }
88 
89     FilteredProjectGroup *group = findFilterGroup(filterGroupName);
90     SAFE_POINT(nullptr != group, L10N::nullPointerError("project filter group"), );
91 
92 #ifdef _DEBUG
93     SAFE_POINT(!group->contains(obj), "Attempting to add duplicate to a filter group", );
94 #endif
95 
96     const int objectRow = group->getNewObjectNumber(obj);
97     beginInsertRows(getIndexForGroup(group), objectRow, objectRow);
98     group->addObject(obj, objectRow);
99     endInsertRows();
100 }
101 
findFilterGroup(const QString & name) const102 FilteredProjectGroup *ProjectViewFilterModel::findFilterGroup(const QString &name) const {
103     SAFE_POINT(!name.isEmpty(), "Empty project filter group name", nullptr);
104 
105     if (ProjectFilterNames::OBJ_NAME_FILTER_NAME == name) {
106         return filterGroups.isEmpty() ? nullptr : *(filterGroups.constBegin());
107     } else {
108         FilteredProjectGroup testGroup(name);
109         const QList<FilteredProjectGroup *>::const_iterator begin = filterGroups.constBegin();
110         const QList<FilteredProjectGroup *>::const_iterator end = filterGroups.constEnd();
111 
112         QList<FilteredProjectGroup *>::const_iterator posNextToResult = std::upper_bound(begin, end, &testGroup, FilteredProjectGroup::groupLessThan);
113         return (posNextToResult != begin && (*(--posNextToResult))->getGroupName() == name) ? *posNextToResult : nullptr;
114     }
115 }
116 
getIndexForGroup(FilteredProjectGroup * group) const117 QModelIndex ProjectViewFilterModel::getIndexForGroup(FilteredProjectGroup *group) const {
118     const int groupRow = filterGroups.indexOf(group);
119     SAFE_POINT(-1 != groupRow, "Unexpected filter project group detected", QModelIndex());
120     return createIndex(groupRow, 0, group);
121 }
122 
getIndexForObject(const QString & groupName,GObject * obj) const123 QModelIndex ProjectViewFilterModel::getIndexForObject(const QString &groupName, GObject *obj) const {
124     FilteredProjectGroup *group = findFilterGroup(groupName);
125     SAFE_POINT(nullptr != group, L10N::nullPointerError("project filter group"), QModelIndex());
126 
127     WrappedObject *wrappedObj = group->getWrappedObject(obj);
128     SAFE_POINT(nullptr != wrappedObj, L10N::nullPointerError("filtered object"), QModelIndex());
129     return createIndex(group->getWrappedObjectNumber(wrappedObj), 0, wrappedObj);
130 }
131 
addFilterGroup(const QString & name)132 void ProjectViewFilterModel::addFilterGroup(const QString &name) {
133     SAFE_POINT(!name.isEmpty(), "Empty project filter group name", );
134 #ifdef _DEBUG
135     SAFE_POINT(!hasFilterGroup(name), "Attempting to add a duplicate filter group", );
136 #endif
137 
138     FilteredProjectGroup *newGroup = new FilteredProjectGroup(name);
139     QList<FilteredProjectGroup *>::iterator insertionPlace = std::upper_bound(filterGroups.begin(), filterGroups.end(), newGroup, FilteredProjectGroup::groupLessThan);
140 
141     const int groupNumber = insertionPlace - filterGroups.begin();
142     beginInsertRows(QModelIndex(), groupNumber, groupNumber);
143     filterGroups.insert(insertionPlace, newGroup);
144     endInsertRows();
145 
146     emit si_filterGroupAdded(createIndex(groupNumber, 0, newGroup));
147 }
148 
hasFilterGroup(const QString & name) const149 bool ProjectViewFilterModel::hasFilterGroup(const QString &name) const {
150     return nullptr != findFilterGroup(name);
151 }
152 
clearFilterGroups()153 void ProjectViewFilterModel::clearFilterGroups() {
154     filterController.stopFiltering();
155 
156     beginResetModel();
157     qDeleteAll(filterGroups);
158     filterGroups.clear();
159     endResetModel();
160 }
161 
mapToSource(const QModelIndex & proxyIndex) const162 QModelIndex ProjectViewFilterModel::mapToSource(const QModelIndex &proxyIndex) const {
163     switch (getType(proxyIndex)) {
164         case GROUP:
165             return QModelIndex();
166         case OBJECT: {
167             WrappedObject *obj = toObject(proxyIndex);
168             return srcModel->getIndexForObject(obj->getObject());
169         }
170         default:
171             FAIL("Unexpected parent item type", QModelIndex());
172     }
173 }
174 
rowCount(const QModelIndex & parent) const175 int ProjectViewFilterModel::rowCount(const QModelIndex &parent) const {
176     CHECK(parent.isValid(), filterGroups.size());
177 
178     switch (getType(parent)) {
179         case GROUP: {
180             FilteredProjectGroup *parentFilterGroup = toGroup(parent);
181             return parentFilterGroup->getObjectsCount();
182         }
183         case OBJECT:
184             return 0;
185         default:
186             FAIL("Unexpected parent item type", 0);
187     }
188 }
189 
columnCount(const QModelIndex &) const190 int ProjectViewFilterModel::columnCount(const QModelIndex & /*parent*/) const {
191     return 1;
192 }
193 
sl_objectsFiltered(const QString & groupName,const QList<QPointer<GObject>> & objs)194 void ProjectViewFilterModel::sl_objectsFiltered(const QString &groupName, const QList<QPointer<GObject>> &objs) {
195     foreach (const QPointer<GObject> &obj, objs) {
196         const QString objPath = srcModel->getObjectFolder(obj->getDocument(), obj.data());
197         if (!obj.isNull() && !ProjectUtils::isFolderInRecycleBinSubtree(objPath)) {
198             addFilteredObject(groupName, obj.data());
199         }
200     }
201 }
202 
index(int row,int column,const QModelIndex & parent) const203 QModelIndex ProjectViewFilterModel::index(int row, int column, const QModelIndex &parent) const {
204     if (!parent.isValid()) {
205         CHECK(row < filterGroups.size(), QModelIndex());
206         return createIndex(row, column, filterGroups[row]);
207     }
208 
209     switch (getType(parent)) {
210         case GROUP:
211             return createIndex(row, column, toGroup(parent)->getWrappedObject(row));
212         default:
213             FAIL("Unexpected parent item type", QModelIndex());
214     }
215 }
216 
parent(const QModelIndex & index) const217 QModelIndex ProjectViewFilterModel::parent(const QModelIndex &index) const {
218     CHECK(index.isValid(), QModelIndex());
219 
220     switch (getType(index)) {
221         case GROUP:
222             return QModelIndex();
223         case OBJECT:
224             return getIndexForGroup(toObject(index)->getParentGroup());
225         default:
226             FAIL("Unexpected parent item type", QModelIndex());
227     }
228 }
229 
flags(const QModelIndex & index) const230 Qt::ItemFlags ProjectViewFilterModel::flags(const QModelIndex &index) const {
231     CHECK(index.isValid(), QAbstractItemModel::flags(index));
232 
233     switch (getType(index)) {
234         case GROUP:
235             return QAbstractItemModel::flags(index);
236         default: {
237             Qt::ItemFlags result = srcModel->flags(mapToSource(index));
238             result &= ~Qt::ItemIsEditable;
239             result &= ~Qt::ItemIsDropEnabled;
240             return result;
241         }
242     }
243 }
244 
getGroupData(const QModelIndex & index,int role) const245 QVariant ProjectViewFilterModel::getGroupData(const QModelIndex &index, int role) const {
246     SAFE_POINT(0 <= index.row() && index.row() < filterGroups.size(), "Project group number out of range", QVariant());
247 
248     switch (role) {
249         case Qt::DisplayRole:
250             return filterGroups[index.row()]->getGroupName();
251         default:
252             return QVariant();
253     }
254 }
255 
data(const QModelIndex & index,int role) const256 QVariant ProjectViewFilterModel::data(const QModelIndex &index, int role) const {
257     const ItemType itemType = getType(index);
258     switch (itemType) {
259         case GROUP:
260             return getGroupData(index, role);
261         case OBJECT:
262             return getObjectData(index, role);
263         default:
264             FAIL("Unexpected model item type", QVariant());
265     }
266 }
267 
sl_dataChanged(const QModelIndex & before,const QModelIndex & after)268 void ProjectViewFilterModel::sl_dataChanged(const QModelIndex &before, const QModelIndex &after) {
269     SAFE_POINT(before == after, "Unexpected project item index change", );
270 
271     if (ProjectViewModel::itemType(before) == ProjectViewModel::OBJECT) {
272         GObject *object = ProjectViewModel::toObject(before);
273         foreach (FilteredProjectGroup *group, filterGroups) {
274             if (group->contains(object)) {
275                 const QModelIndex proxyObjIndex = getIndexForObject(group->getGroupName(), object);
276                 emit dataChanged(proxyObjIndex, proxyObjIndex);
277             }
278         }
279     }
280 }
281 
sl_rowsAboutToBeRemoved(const QModelIndex & parent,int first,int last)282 void ProjectViewFilterModel::sl_rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last) {
283     SAFE_POINT(first == last, "Unexpected row range", );
284 
285     const QModelIndex removedIndex = srcModel->index(first, 0, parent);
286     QList<GObject *> objectsBeingRemoved;
287     switch (ProjectViewModel::itemType(removedIndex)) {
288         case ProjectViewModel::OBJECT:
289             objectsBeingRemoved.append(ProjectViewModel::toObject(removedIndex));
290             break;
291         case ProjectViewModel::FOLDER: {
292             Folder *folder = ProjectViewModel::toFolder(removedIndex);
293             objectsBeingRemoved.append(srcModel->getFolderObjects(folder->getDocument(), folder->getFolderPath()));
294         } break;
295         case ProjectViewModel::DOCUMENT:
296             objectsBeingRemoved.append(ProjectViewModel::toDocument(removedIndex)->getObjects());
297             break;
298         default:
299             FAIL("Unexpected project item type", );
300     }
301 
302     foreach (GObject *obj, objectsBeingRemoved) {
303         foreach (FilteredProjectGroup *group, filterGroups) {
304             WrappedObject *wrappedObj = group->getWrappedObject(obj);
305             if (nullptr != wrappedObj) {
306                 const QModelIndex parentIndex = getIndexForGroup(group);
307                 const int objNumber = group->getWrappedObjectNumber(wrappedObj);
308                 SAFE_POINT(-1 != objNumber, "Unexpected object number", );
309                 beginRemoveRows(parentIndex, objNumber, objNumber);
310                 group->removeAt(objNumber);
311                 endRemoveRows();
312             }
313         }
314     }
315 }
316 
getStyledObjectName(GObject * obj,FilteredProjectGroup * group) const317 QString ProjectViewFilterModel::getStyledObjectName(GObject *obj, FilteredProjectGroup *group) const {
318     SAFE_POINT(nullptr != obj && nullptr != group, "Invalid arguments supplied", QString());
319 
320     QString result = obj->getGObjectName();
321     if (group->getGroupName() == ProjectFilterNames::OBJ_NAME_FILTER_NAME) {
322         const QString stylePattern = "<span style=\"background-color:yellow;color:black\">%1</span>";
323         foreach (const QString &token, settings.tokensToShow) {
324             int nextTokenPos = -1;
325             const int tokenSize = token.length();
326             while (-1 != (nextTokenPos = result.indexOf(token, nextTokenPos + 1, Qt::CaseInsensitive))) {
327                 const QString coloredText = QString(stylePattern).arg(result.mid(nextTokenPos, tokenSize));
328                 result.replace(nextTokenPos, tokenSize, coloredText);
329                 nextTokenPos += coloredText.size();
330             }
331         }
332     }
333 
334     const QVariant objFontData = srcModel->getIndexForObject(obj).data(Qt::FontRole);
335     const QFont objectFont = objFontData.isValid() ? objFontData.value<QFont>() : QFont();
336     if (objectFont.bold()) {
337         result = QString("<b>%1</b>").arg(result);
338     }
339     return result;
340 }
341 
getObjectData(const QModelIndex & index,int role) const342 QVariant ProjectViewFilterModel::getObjectData(const QModelIndex &index, int role) const {
343     QVariant result = srcModel->data(mapToSource(index), role);
344 
345     if (Qt::DisplayRole == role) {
346         GObject *object = toObject(index)->getObject();
347         Document *parentDoc = object->getDocument();
348         if (nullptr != parentDoc) {
349             const QString objectPath = srcModel->getObjectFolder(parentDoc, object);
350             const bool isDatabase = parentDoc->isDatabaseConnection();
351             const QString itemDocInfo = parentDoc->getName() + (isDatabase ? ": " + objectPath : QString());
352             const QString objectName = getStyledObjectName(object, toGroup(index.parent()));
353             result = QString("%1<p style=\"margin-top:0px;font-size:small;\">%2</p>").arg(objectName).arg(itemDocInfo);
354         }
355     }
356     return result;
357 }
358 
mimeData(const QModelIndexList & indexes) const359 QMimeData *ProjectViewFilterModel::mimeData(const QModelIndexList &indexes) const {
360     QSet<GObject *> uniqueObjs;
361     foreach (const QModelIndex &index, indexes) {
362         if (isObject(index)) {
363             uniqueObjs.insert(toObject(index)->getObject());
364         }
365     }
366 
367     QModelIndexList reducedIndexes;
368     foreach (GObject *obj, uniqueObjs) {
369         reducedIndexes.append(srcModel->getIndexForObject(obj));
370     }
371 
372     return srcModel->mimeData(reducedIndexes);
373 }
374 
mimeTypes() const375 QStringList ProjectViewFilterModel::mimeTypes() const {
376     QStringList result;
377     result << GObjectMimeData::MIME_TYPE;
378     result << BunchMimeData::MIME_TYPE;
379     return result;
380 }
381 
getType(const QModelIndex & index)382 ProjectViewFilterModel::ItemType ProjectViewFilterModel::getType(const QModelIndex &index) {
383     QObject *data = toQObject(index);
384     CHECK(nullptr != data, GROUP);
385 
386     if (nullptr != qobject_cast<WrappedObject *>(data)) {
387         return OBJECT;
388     } else if (nullptr != qobject_cast<FilteredProjectGroup *>(data)) {
389         return GROUP;
390     } else {
391         FAIL("Unexpected data type", GROUP);
392     }
393 }
394 
isObject(const QModelIndex & index)395 bool ProjectViewFilterModel::isObject(const QModelIndex &index) {
396     return OBJECT == getType(index);
397 }
398 
toQObject(const QModelIndex & index)399 QObject *ProjectViewFilterModel::toQObject(const QModelIndex &index) {
400     QObject *internalObj = static_cast<QObject *>(index.internalPointer());
401     SAFE_POINT(nullptr != internalObj, "Invalid index data", nullptr);
402     return internalObj;
403 }
404 
toGroup(const QModelIndex & index)405 FilteredProjectGroup *ProjectViewFilterModel::toGroup(const QModelIndex &index) {
406     return qobject_cast<FilteredProjectGroup *>(toQObject(index));
407 }
408 
toObject(const QModelIndex & index)409 WrappedObject *ProjectViewFilterModel::toObject(const QModelIndex &index) {
410     return qobject_cast<WrappedObject *>(toQObject(index));
411 }
412 
413 }  // namespace U2
414