1 /* ============================================================
2 * QuiteRSS is a open-source cross-platform RSS/Atom news feeds reader
3 * Copyright (C) 2011-2020 QuiteRSS Team <quiterssteam@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 <https://www.gnu.org/licenses/>.
17 * ============================================================ */
18 #include "feedsmodel.h"
19 #include "feedsproxymodel.h"
20
21 #include <QtCore>
22 #include <QPainter>
23
FeedsModel(QObject * parent)24 FeedsModel::FeedsModel(QObject *parent)
25 : QAbstractItemModel(parent)
26 , defaultIconFeeds_(false)
27 , view_(0)
28 , rootParentId_(0)
29 {
30 setObjectName("FeedsModel");
31
32 refresh();
33 }
34
~FeedsModel()35 FeedsModel::~FeedsModel()
36 {
37 clear();
38 }
39
clear()40 void FeedsModel::clear()
41 {
42 id2RowList_.clear();
43 parid2RowList_.clear();
44 columnsList_.clear();
45
46 qDeleteAll(userDataList_);
47 userDataList_.clear();
48 }
49
refresh()50 void FeedsModel::refresh()
51 {
52 #ifdef HAVE_QT5
53 beginResetModel();
54 clear();
55 endResetModel();
56 #else
57 reset();
58 clear();
59 #endif
60
61 queryModel_.setQuery("SELECT * FROM feeds ORDER BY parentId, rowToParent");
62 while (queryModel_.canFetchMore())
63 queryModel_.fetchMore();
64
65 indexId_ = queryModel_.record().indexOf("id");
66 indexParid_ = queryModel_.record().indexOf("parentId");
67 for (int i = 0; i < queryModel_.record().count(); i++) {
68 columnsList_[i] = i;
69 }
70 columnsList_[0] = queryModel_.record().indexOf("text");
71 columnsList_[queryModel_.record().indexOf("text")] = 0;
72
73 for (int i = 0; i < queryModel_.rowCount(); i++) {
74 int id = queryModel_.record(i).value(indexId_).toInt();
75 id2RowList_[id] = i;
76 int parid = queryModel_.record(i).value(indexParid_).toInt();
77 parid2RowList_[i] = parid;
78 userDataList_[id] = new UserData(id, parid, queryModel_.record(i));
79 }
80 }
81
userDataById(int id) const82 UserData * FeedsModel::userDataById(int id) const
83 {
84 return userDataList_.value(id, 0);
85 }
86
rowById(int id) const87 int FeedsModel::rowById(int id) const
88 {
89 return id2RowList_.value(id, -1);
90 }
91
rowByParid(int parid) const92 int FeedsModel::rowByParid(int parid) const
93 {
94 return parid2RowList_.key(parid, -1);
95 }
96
rowCount(const QModelIndex & parent) const97 int FeedsModel::rowCount(const QModelIndex &parent) const
98 {
99 if (parent.isValid())
100 return parid2RowList_.keys(idByIndex(parent)).count();
101 else
102 return parid2RowList_.keys(rootParentId_).count();
103 }
104
columnCount(const QModelIndex &) const105 int FeedsModel::columnCount(const QModelIndex&) const
106 {
107 return queryModel_.record().count();
108 }
109
index(int row,int column,const QModelIndex & parent) const110 QModelIndex FeedsModel::index(int row, int column, const QModelIndex &parent) const
111 {
112 if (row == -1)
113 return QModelIndex();
114
115 int id = 0;
116 if (parent.isValid())
117 id = id2RowList_.key(row + rowByParid(idByIndex(parent)), 0);
118 else
119 id = id2RowList_.key(row, 0);
120
121 UserData *userData = userDataById(id);
122 if (userData)
123 return createIndex(row, column, userData);
124 else
125 return QModelIndex();
126 }
127
parent(const QModelIndex & index) const128 QModelIndex FeedsModel::parent(const QModelIndex &index) const
129 {
130 if (!index.isValid())
131 return QModelIndex();
132
133 int parid = paridByIndex(index);
134 if (parid == rootParentId_)
135 return QModelIndex();
136
137 UserData *userData = userDataById(parid);
138 if (userData) {
139 int row = rowById(parid) - rowByParid(userData->parid);
140 return createIndex(row, 0, userData);
141 } else {
142 return QModelIndex();
143 }
144 }
145
data(const QModelIndex & index,int role) const146 QVariant FeedsModel::data(const QModelIndex &index, int role) const
147 {
148 if (role == Qt::FontRole) {
149 QFont font = font_;
150 if (indexColumnOf("text") == index.column()) {
151 if (0 < indexSibling(index, "unread").data(Qt::EditRole).toInt())
152 font.setBold(true);
153 }
154 return font;
155 } else if (role == Qt::DisplayRole){
156 if (indexColumnOf("unread") == index.column()) {
157 int unread = indexSibling(index, "unread").data(Qt::EditRole).toInt();
158 if (0 == unread) {
159 return QVariant();
160 } else {
161 QString qStr = QString("(%1)").arg(unread);
162 return qStr;
163 }
164 } else if (indexColumnOf("undeleteCount") == index.column()) {
165 QString qStr = QString("(%1)").
166 arg(indexSibling(index, "undeleteCount").data(Qt::EditRole).toInt());
167 return qStr;
168 } else if (indexColumnOf("updated") == index.column()) {
169 QDateTime dtLocal;
170 QString strDate = indexSibling(index, "updated").data(Qt::EditRole).toString();
171
172 if (!strDate.isNull()) {
173 QDateTime dtLocalTime = QDateTime::currentDateTime();
174 QDateTime dtUTC = QDateTime(dtLocalTime.date(), dtLocalTime.time(), Qt::UTC);
175 int nTimeShift = dtLocalTime.secsTo(dtUTC);
176
177 QDateTime dt = QDateTime::fromString(strDate, Qt::ISODate);
178 dtLocal = dt.addSecs(nTimeShift);
179
180 QString strResult;
181 if (QDateTime::currentDateTime().date() <= dtLocal.date())
182 strResult = dtLocal.toString(formatTime_);
183 else
184 strResult = dtLocal.toString(formatDate_);
185 return strResult;
186 } else {
187 return QVariant();
188 }
189 }
190 } else if (role == Qt::TextColorRole) {
191 if (indexColumnOf("unread") == index.column()) {
192 return QColor(countNewsUnreadColor_);
193 }
194
195 QModelIndex currentIndex = ((FeedsProxyModel*)view_->model())->mapToSource(view_->currentIndex());
196 if ((index.row() == currentIndex.row()) && (index.parent() == currentIndex.parent()) &&
197 view_->selectionModel()->selectedRows(0).count()) {
198 return QColor(focusedFeedTextColor_);
199 }
200
201 if (indexColumnOf("text") == index.column()) {
202 if (indexSibling(index, "newCount").data(Qt::EditRole).toInt() > 0) {
203 return QColor(feedWithNewNewsColor_);
204 }
205 }
206
207 if (indexColumnOf("text") == index.column()) {
208 if (indexSibling(index, "disableUpdate").data(Qt::EditRole).toBool()) {
209 return QColor(feedDisabledUpdateColor_);
210 }
211 }
212
213 return QColor(textColor_);
214 } else if (role == Qt::BackgroundRole) {
215 QModelIndex currentIndex = ((FeedsProxyModel*)view_->model())->mapToSource(view_->currentIndex());
216 if ((index.row() == currentIndex.row()) && (index.parent() == currentIndex.parent()) &&
217 view_->selectionModel()->selectedRows(0).count()) {
218 if (!focusedFeedBGColor_.isEmpty())
219 return QColor(focusedFeedBGColor_);
220 }
221 } else if (role == Qt::DecorationRole) {
222 if (indexColumnOf("text") == index.column()) {
223 if (isFolder(index)) {
224 return QPixmap(":/images/folder");
225 } else {
226 if (!defaultIconFeeds_) {
227 QByteArray byteArray = indexSibling(index, "image").data(Qt::EditRole).toByteArray();
228 if (!byteArray.isNull()) {
229 QImage resultImage;
230 if (resultImage.loadFromData(QByteArray::fromBase64(byteArray))) {
231 QString strStatus = indexSibling(index, "status").data(Qt::EditRole).toString();
232 if (strStatus.section(" ", 0, 0).toInt() != 0) {
233 QImage image;
234 if (strStatus.section(" ", 0, 0).toInt() < 0)
235 image.load(":/images/bulletError");
236 else if (strStatus.section(" ", 0, 0).toInt() == 1)
237 image.load(":/images/bulletUpdate");
238 QPainter resultPainter(&resultImage);
239 resultPainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
240 resultPainter.drawImage(0, 0, image);
241 resultPainter.end();
242 }
243 return resultImage;
244 }
245 }
246 }
247 QImage resultImage(":/images/feed");
248 QString strStatus = indexSibling(index, "status").data(Qt::EditRole).toString();
249 if (strStatus.section(" ", 0, 0).toInt() != 0) {
250 QImage image;
251 if (strStatus.section(" ", 0, 0).toInt() < 0)
252 image.load(":/images/bulletError");
253 else if (strStatus.section(" ", 0, 0).toInt() == 1)
254 image.load(":/images/bulletUpdate");
255
256 QPainter resultPainter(&resultImage);
257 resultPainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
258 resultPainter.drawImage(0, 0, image);
259 resultPainter.end();
260 }
261 return resultImage;
262 }
263 }
264 } else if (role == Qt::TextAlignmentRole) {
265 if (indexColumnOf("id") == index.column()) {
266 int flag = Qt::AlignRight|Qt::AlignVCenter;
267 return flag;
268 }
269 } else if (role == Qt::ToolTipRole) {
270 if (indexColumnOf("text") == index.column()) {
271 QString title = index.data(Qt::EditRole).toString();
272 QRect rectText = view_->visualRect(index);
273 int width = rectText.width() - 16 - 12;
274 QFont font = font_;
275 if (0 < indexSibling(index, "unread").data(Qt::EditRole).toInt())
276 font.setBold(true);
277 QFontMetrics fontMetrics(font);
278
279 if (width < fontMetrics.width(title))
280 return title;
281 }
282 return QString("");
283 }
284
285 if (!((role == Qt::EditRole) || (role == Qt::DisplayRole)))
286 return QVariant();
287
288 QSqlRecord record = static_cast<UserData*>(index.internalPointer())->record;
289 return record.value(indexColumnOf(index.column()));
290 }
291
setData(const QModelIndex & index,const QVariant & value,int)292 bool FeedsModel::setData(const QModelIndex &index, const QVariant &value, int)
293 {
294 if (!index.isValid())
295 return false;
296
297 QSqlRecord *record = &static_cast<UserData*>(index.internalPointer())->record;
298 record->setValue(indexColumnOf(index.column()), value);
299 return true;
300 }
301
flags(const QModelIndex & index) const302 Qt::ItemFlags FeedsModel::flags(const QModelIndex &index) const
303 {
304 Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index);
305
306 if (index.isValid())
307 return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
308 else
309 return Qt::ItemIsDropEnabled | defaultFlags;
310 }
311
supportedDropActions() const312 Qt::DropActions FeedsModel::supportedDropActions() const
313 {
314 return Qt::MoveAction;
315 }
316
indexById(int id) const317 QModelIndex FeedsModel::indexById(int id) const
318 {
319 QModelIndex parentIndex = QModelIndex();
320 UserData *userData = userDataById(id);
321 if (userData)
322 parentIndex = indexById(userData->parid);
323 for (int i = 0; i < rowCount(parentIndex); i++) {
324 if (idByIndex(index(i, 0, parentIndex)) == id)
325 return index(i,0,parentIndex);
326 }
327 return QModelIndex();
328 }
329
idByIndex(const QModelIndex & index) const330 int FeedsModel::idByIndex(const QModelIndex &index) const
331 {
332 if (index.isValid())
333 return static_cast<UserData*>(index.internalPointer())->id;
334 return 0;
335 }
336
paridByIndex(const QModelIndex & index) const337 int FeedsModel::paridByIndex(const QModelIndex &index) const
338 {
339 if (index.isValid())
340 return static_cast<UserData*>(index.internalPointer())->parid;
341 return 0;
342 }
343
indexColumnOf(int column) const344 int FeedsModel::indexColumnOf(int column) const
345 {
346 return columnsList_.value(column, column);
347 }
348
indexColumnOf(const QString & name) const349 int FeedsModel::indexColumnOf(const QString &name) const
350 {
351 return indexColumnOf(queryModel_.record().indexOf(name));
352 }
353
setView(QTreeView * view)354 void FeedsModel::setView(QTreeView *view)
355 {
356 view_ = view;
357 }
358
dataField(const QModelIndex & index,const QString & fieldName) const359 QVariant FeedsModel::dataField(const QModelIndex &index, const QString &fieldName) const
360 {
361 return indexSibling(index, fieldName).data(Qt::EditRole);
362 }
363
364 /** @brief Check if item is folder
365 *
366 * If xmlUrl field is empty, than item is considered folder
367 * @param index Item to check
368 * @return Is folder sign
369 * @retval true Index item is category
370 * @retval false Index item is feed
371 *---------------------------------------------------------------------------*/
isFolder(const QModelIndex & index) const372 bool FeedsModel::isFolder(const QModelIndex &index) const
373 {
374 return indexSibling(index, "xmlUrl").data(Qt::EditRole).toString().isEmpty();
375 }
376
indexSibling(const QModelIndex & index,const QString & fieldName) const377 QModelIndex FeedsModel::indexSibling(const QModelIndex &index, const QString &fieldName) const
378 {
379 return this->index(index.row(), indexColumnOf(fieldName), index.parent());
380 }
381