1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2018 - 2019 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
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
20 #include "FeedsContentsWidget.h"
21 #include "../../../core/Application.h"
22 #include "../../../core/BookmarksManager.h"
23 #include "../../../core/ThemesManager.h"
24 #include "../../../ui/Action.h"
25 #include "../../../ui/Animation.h"
26 #include "../../../ui/FeedPropertiesDialog.h"
27 #include "../../../ui/MainWindow.h"
28
29 #include "ui_FeedsContentsWidget.h"
30
31 #include <QtGui/QDesktopServices>
32 #include <QtWidgets/QDesktopWidget>
33 #include <QtWidgets/QInputDialog>
34 #include <QtWidgets/QMenu>
35 #include <QtWidgets/QMessageBox>
36 #include <QtWidgets/QToolButton>
37 #include <QtWidgets/QToolTip>
38
39 namespace Otter
40 {
41
EntryDelegate(QObject * parent)42 EntryDelegate::EntryDelegate(QObject *parent) : ItemDelegate(parent)
43 {
44 }
45
initStyleOption(QStyleOptionViewItem * option,const QModelIndex & index) const46 void EntryDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
47 {
48 ItemDelegate::initStyleOption(option, index);
49
50 if (index.sibling(index.row(), 0).data(FeedsContentsWidget::LastReadTimeRole).isNull())
51 {
52 option->font.setBold(true);
53 }
54 }
55
FeedDelegate(QObject * parent)56 FeedDelegate::FeedDelegate(QObject *parent) : ItemDelegate(parent)
57 {
58 }
59
initStyleOption(QStyleOptionViewItem * option,const QModelIndex & index) const60 void FeedDelegate::initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
61 {
62 ItemDelegate::initStyleOption(option, index);
63
64 if (index.data(FeedsModel::IsUpdatingRole).toBool())
65 {
66 const Animation *animation(FeedsContentsWidget::getUpdateAnimation());
67
68 if (animation)
69 {
70 option->icon = QIcon(animation->getCurrentPixmap());
71 }
72 }
73 }
74
75 Animation* FeedsContentsWidget::m_updateAnimation = nullptr;
76
FeedsContentsWidget(const QVariantMap & parameters,QWidget * parent)77 FeedsContentsWidget::FeedsContentsWidget(const QVariantMap ¶meters, QWidget *parent) : ContentsWidget(parameters, nullptr, parent),
78 m_feed(nullptr),
79 m_feedModel(nullptr),
80 m_ui(new Ui::FeedsContentsWidget)
81 {
82 m_ui->setupUi(this);
83 m_ui->subscribeFeedWidget->hide();
84 m_ui->feedsHorizontalSplitterWidget->setSizes({300, qMax(500, (width() - 300))});
85 m_ui->entriesFilterLineEditWidget->setClearOnEscape(true);
86 m_ui->entriesViewWidget->setItemDelegate(new EntryDelegate(this));
87 m_ui->entriesViewWidget->installEventFilter(this);
88 m_ui->entriesViewWidget->viewport()->installEventFilter(this);
89 m_ui->entriesViewWidget->viewport()->setMouseTracking(true);
90 m_ui->feedsFilterLineEditWidget->setClearOnEscape(true);
91 m_ui->feedsViewWidget->setViewMode(ItemViewWidget::TreeView);
92 m_ui->feedsViewWidget->setModel(FeedsManager::getModel());
93 m_ui->feedsViewWidget->setItemDelegate(new FeedDelegate(this));
94 m_ui->feedsViewWidget->expandAll();
95 m_ui->feedsViewWidget->installEventFilter(this);
96 m_ui->feedsViewWidget->viewport()->installEventFilter(this);
97 m_ui->feedsViewWidget->viewport()->setMouseTracking(true);
98 m_ui->emailButton->setIcon(ThemesManager::createIcon(QLatin1String("mail-send")));
99 m_ui->urlButton->setIcon(ThemesManager::createIcon(QLatin1String("text-html")));
100 m_ui->textBrowserWidget->setOpenExternalLinks(true);
101
102 if (isSidebarPanel())
103 {
104 m_ui->entriesWidget->hide();
105 m_ui->entryWidget->hide();
106 }
107
108 connect(FeedsManager::getInstance(), &FeedsManager::feedModified, this, &FeedsContentsWidget::handleFeedModified);
109 connect(m_ui->entriesFilterLineEditWidget, &LineEditWidget::textChanged, m_ui->entriesViewWidget, &ItemViewWidget::setFilterString);
110 connect(m_ui->entriesViewWidget, &ItemViewWidget::doubleClicked, this, &FeedsContentsWidget::openEntry);
111 connect(m_ui->entriesViewWidget, &ItemViewWidget::customContextMenuRequested, this, &FeedsContentsWidget::showEntriesContextMenu);
112 connect(m_ui->entriesViewWidget, &ItemViewWidget::needsActionsUpdate, this, &FeedsContentsWidget::updateEntry);
113 connect(m_ui->feedsFilterLineEditWidget, &LineEditWidget::textChanged, m_ui->feedsViewWidget, &ItemViewWidget::setFilterString);
114 connect(m_ui->feedsViewWidget, &ItemViewWidget::doubleClicked, this, &FeedsContentsWidget::openFeed);
115 connect(m_ui->feedsViewWidget, &ItemViewWidget::customContextMenuRequested, this, &FeedsContentsWidget::showFeedsContextMenu);
116 connect(m_ui->feedsViewWidget, &ItemViewWidget::needsActionsUpdate, this, &FeedsContentsWidget::updateActions);
117 connect(m_ui->okButton, &QToolButton::clicked, this, &FeedsContentsWidget::subscribeFeed);
118 connect(m_ui->cancelButton, &QToolButton::clicked, m_ui->subscribeFeedWidget, &QWidget::hide);
119 connect(m_ui->emailButton, &QToolButton::clicked, [&]()
120 {
121 const QModelIndex index(m_ui->entriesViewWidget->currentIndex());
122
123 QDesktopServices::openUrl(QLatin1String("mailto:") + index.sibling(index.row(), 0).data(EmailRole).toString());
124 });
125 connect(m_ui->urlButton, &QToolButton::clicked, [&]()
126 {
127 const QModelIndex index(m_ui->entriesViewWidget->currentIndex());
128
129 Application::getInstance()->triggerAction(ActionsManager::OpenUrlAction, {{QLatin1String("url"), index.sibling(index.row(), 0).data(UrlRole).toString()}}, this);
130 });
131 }
132
~FeedsContentsWidget()133 FeedsContentsWidget::~FeedsContentsWidget()
134 {
135 delete m_ui;
136 }
137
changeEvent(QEvent * event)138 void FeedsContentsWidget::changeEvent(QEvent *event)
139 {
140 ContentsWidget::changeEvent(event);
141
142 if (event->type() == QEvent::LanguageChange)
143 {
144 m_ui->retranslateUi(this);
145
146 if (m_feedModel)
147 {
148 m_feedModel->setHorizontalHeaderLabels({tr("Title"), tr("From"), tr("Published")});
149 }
150 }
151 }
152
triggerAction(int identifier,const QVariantMap & parameters,ActionsManager::TriggerType trigger)153 void FeedsContentsWidget::triggerAction(int identifier, const QVariantMap ¶meters, ActionsManager::TriggerType trigger)
154 {
155 switch (identifier)
156 {
157 case ActionsManager::ReloadAction:
158 if (m_feed && !m_feed->isUpdating())
159 {
160 m_feed->update();
161 }
162
163 break;
164 case ActionsManager::DeleteAction:
165 if (m_ui->feedsViewWidget->hasFocus())
166 {
167 removeFeed();
168 }
169 else if (m_ui->entriesViewWidget->hasFocus())
170 {
171 removeEntry();
172 }
173
174 break;
175 default:
176 ContentsWidget::triggerAction(identifier, parameters, trigger);
177
178 break;
179 }
180 }
181
addFeed()182 void FeedsContentsWidget::addFeed()
183 {
184 FeedPropertiesDialog dialog(nullptr, findFolder(m_ui->feedsViewWidget->currentIndex()), this);
185
186 if (dialog.exec() == QDialog::Rejected)
187 {
188 return;
189 }
190
191 if (BookmarksManager::getModel()->hasFeed(dialog.getFeed()->getUrl()) || FeedsManager::getModel()->hasFeed(dialog.getFeed()->getUrl()))
192 {
193 QMessageBox messageBox;
194 messageBox.setWindowTitle(tr("Question"));
195 messageBox.setText(tr("You already subscribed this feed."));
196 messageBox.setInformativeText(tr("Do you want to continue?"));
197 messageBox.setIcon(QMessageBox::Question);
198 messageBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
199 messageBox.setDefaultButton(QMessageBox::Yes);
200
201 if (messageBox.exec() == QMessageBox::Cancel)
202 {
203 return;
204 }
205 }
206
207 FeedsManager::getModel()->addEntry(dialog.getFeed(), dialog.getFolder());
208
209 updateActions();
210 }
211
addFolder()212 void FeedsContentsWidget::addFolder()
213 {
214 const QString title(QInputDialog::getText(this, tr("Select Folder Name"), tr("Enter folder name:")));
215
216 if (!title.isEmpty())
217 {
218 m_ui->feedsViewWidget->setCurrentIndex(FeedsManager::getModel()->addEntry(FeedsModel::FolderEntry, {{FeedsModel::TitleRole, title}}, findFolder(m_ui->feedsViewWidget->currentIndex()))->index());
219 }
220 }
221
openFeed()222 void FeedsContentsWidget::openFeed()
223 {
224 const FeedsModel::Entry *entry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
225 MainWindow *mainWindow(MainWindow::findMainWindow(this));
226
227 if (mainWindow && entry && entry->getFeed())
228 {
229 mainWindow->triggerAction(ActionsManager::OpenUrlAction, {{QLatin1String("url"), QUrl(QLatin1String("view-feed:") + entry->getFeed()->getUrl().toDisplayString())}});
230 }
231 }
232
updateFeed()233 void FeedsContentsWidget::updateFeed()
234 {
235 const FeedsModel::Entry *entry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
236
237 if (entry && entry->getFeed())
238 {
239 entry->getFeed()->update();
240 }
241 }
242
removeFeed()243 void FeedsContentsWidget::removeFeed()
244 {
245 FeedsManager::getModel()->trashEntry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
246 }
247
subscribeFeed()248 void FeedsContentsWidget::subscribeFeed()
249 {
250 if (!m_feed)
251 {
252 return;
253 }
254
255 if (m_ui->applicationComboBox->currentIndex() == 0)
256 {
257 FeedPropertiesDialog dialog(m_feed, findFolder(m_ui->feedsViewWidget->currentIndex()), this);
258
259 if (dialog.exec() == QDialog::Accepted)
260 {
261 FeedsManager::getModel()->addEntry(m_feed, dialog.getFolder());
262 }
263 }
264 else
265 {
266 Utils::runApplication(m_ui->applicationComboBox->currentData(Qt::UserRole).toString(), m_feed->getUrl());
267 }
268
269 m_ui->subscribeFeedWidget->hide();
270 }
271
feedProperties()272 void FeedsContentsWidget::feedProperties()
273 {
274 FeedsModel::Entry *entry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
275
276 if (entry)
277 {
278 FeedsModel::Entry *folder(findFolder(m_ui->feedsViewWidget->currentIndex()));
279 FeedPropertiesDialog dialog(entry->getFeed(), folder, this);
280
281 if (dialog.exec() == QDialog::Accepted && dialog.getFolder() != folder)
282 {
283 FeedsManager::getModel()->moveEntry(entry, dialog.getFolder());
284 }
285
286 updateActions();
287 }
288 }
289
openEntry()290 void FeedsContentsWidget::openEntry()
291 {
292 const QModelIndex index(m_ui->entriesViewWidget->currentIndex().sibling(m_ui->entriesViewWidget->currentIndex().row(), 0));
293 MainWindow *mainWindow(MainWindow::findMainWindow(this));
294
295 if (mainWindow && index.isValid() && !index.data(UrlRole).isNull())
296 {
297 mainWindow->triggerAction(ActionsManager::OpenUrlAction, {{QLatin1String("url"), index.data(UrlRole)}});
298 }
299 }
300
removeEntry()301 void FeedsContentsWidget::removeEntry()
302 {
303 if (m_feed)
304 {
305 const QModelIndex index(m_ui->entriesViewWidget->currentIndex().sibling(m_ui->entriesViewWidget->currentIndex().row(), 0));
306
307 if (index.isValid() && !index.data(IdentifierRole).isNull())
308 {
309 m_feed->markEntryAsRemoved(index.data(IdentifierRole).toString());
310
311 m_ui->entriesViewWidget->removeRow();
312 }
313 }
314 }
315
selectCategory()316 void FeedsContentsWidget::selectCategory()
317 {
318 const QToolButton *toolButton(qobject_cast<QToolButton*>(sender()));
319
320 if (toolButton)
321 {
322 m_categories = QStringList({toolButton->objectName()});
323
324 updateFeedModel();
325 }
326 }
327
handleFeedModified(const QUrl & url)328 void FeedsContentsWidget::handleFeedModified(const QUrl &url)
329 {
330 const Feed *feed(FeedsManager::getFeed(url));
331
332 if (feed && feed->isUpdating() && FeedsManager::getModel()->hasFeed(url))
333 {
334 if (!m_updateAnimation)
335 {
336 const QString path(ThemesManager::getAnimationPath(QLatin1String("spinner")));
337
338 if (path.isEmpty())
339 {
340 m_updateAnimation = new SpinnerAnimation(QCoreApplication::instance());
341 }
342 else
343 {
344 m_updateAnimation = new GenericAnimation(path, QCoreApplication::instance());
345 }
346
347 m_updateAnimation->start();
348 }
349
350 connect(m_updateAnimation, &Animation::frameChanged, m_ui->feedsViewWidget->viewport(), static_cast<void(QWidget::*)()>(&QWidget::update), Qt::UniqueConnection);
351 }
352 }
353
showEntriesContextMenu(const QPoint & position)354 void FeedsContentsWidget::showEntriesContextMenu(const QPoint &position)
355 {
356 const QModelIndex index(m_ui->entriesViewWidget->indexAt(position).sibling(m_ui->entriesViewWidget->indexAt(position).row(), 0));
357 ActionExecutor::Object executor(this, this);
358 QMenu menu(this);
359 Action *openAction(new Action(ActionsManager::OpenUrlAction, {{QLatin1String("url"), index.data(UrlRole)}}, {{QLatin1String("text"), tr("Open")}}, executor, &menu));
360 openAction->setEnabled(!index.data(UrlRole).isNull());
361
362 menu.addAction(openAction);
363 menu.addSeparator();
364 menu.addAction(new Action(ActionsManager::DeleteAction, {}, executor, &menu));
365 menu.exec(m_ui->entriesViewWidget->mapToGlobal(position));
366 }
367
showFeedsContextMenu(const QPoint & position)368 void FeedsContentsWidget::showFeedsContextMenu(const QPoint &position)
369 {
370 const QModelIndex index(m_ui->feedsViewWidget->indexAt(position));
371 const FeedsModel::EntryType type(static_cast<FeedsModel::EntryType>(index.data(FeedsModel::TypeRole).toInt()));
372 QMenu menu(this);
373
374 switch (type)
375 {
376 case FeedsModel::TrashEntry:
377 menu.addAction(ThemesManager::createIcon(QLatin1String("trash-empty")), tr("Empty Trash"), FeedsManager::getModel(), &FeedsModel::emptyTrash)->setEnabled(FeedsManager::getModel()->getTrashEntry()->rowCount() > 0);
378
379 break;
380 case FeedsModel::UnknownEntry:
381 menu.addAction(ThemesManager::createIcon(QLatin1String("inode-directory")), tr("Add Folder…"), this, &FeedsContentsWidget::addFolder);
382 menu.addAction(tr("Add Feed…"), this, &FeedsContentsWidget::addFeed);
383
384 break;
385 default:
386 {
387 const bool isInTrash(index.data(FeedsModel::IsTrashedRole).toBool());
388
389 ActionExecutor::Object executor(this, this);
390
391 if (!isInTrash)
392 {
393 if (type == FeedsModel::FeedEntry)
394 {
395 menu.addAction(ThemesManager::createIcon(QLatin1String("view-refresh")), QCoreApplication::translate("actions", "Update"), this, &FeedsContentsWidget::updateFeed);
396 menu.addSeparator();
397 menu.addAction(ThemesManager::createIcon(QLatin1String("document-open")), QCoreApplication::translate("actions", "Open"), this, &FeedsContentsWidget::openFeed);
398 }
399
400 menu.addSeparator();
401
402 QMenu *addMenu(menu.addMenu(tr("Add New")));
403 addMenu->addAction(ThemesManager::createIcon(QLatin1String("inode-directory")), tr("Add Folder…"), this, &FeedsContentsWidget::addFolder);
404 addMenu->addAction(tr("Add Feed…"), this, &FeedsContentsWidget::addFeed);
405 }
406
407 if (type != FeedsModel::RootEntry)
408 {
409 menu.addSeparator();
410
411 if (isInTrash)
412 {
413 menu.addAction(tr("Restore Feed"), &menu, [&]()
414 {
415 FeedsManager::getModel()->restoreEntry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
416 });
417 }
418 else
419 {
420 menu.addAction(new Action(ActionsManager::DeleteAction, {}, executor, &menu));
421 }
422
423 if (type == FeedsModel::FeedEntry)
424 {
425 menu.addSeparator();
426 menu.addAction(tr("Properties…"), this, &FeedsContentsWidget::feedProperties);
427 }
428 }
429 }
430
431 break;
432 }
433
434 menu.exec(m_ui->feedsViewWidget->mapToGlobal(position));
435 }
436
updateActions()437 void FeedsContentsWidget::updateActions()
438 {
439 if (!isSidebarPanel())
440 {
441 const FeedsModel::Entry *entry(FeedsManager::getModel()->getEntry(m_ui->feedsViewWidget->currentIndex()));
442
443 setFeed((entry && entry->getFeed()) ? entry->getFeed() : nullptr);
444 }
445 }
446
updateEntry()447 void FeedsContentsWidget::updateEntry()
448 {
449 const QModelIndex index(m_ui->entriesViewWidget->currentIndex().sibling(m_ui->entriesViewWidget->currentIndex().row(), 0));
450 QString content(index.data(ContentRole).toString());
451
452 if (!index.data(SummaryRole).isNull())
453 {
454 QString summary(index.data(SummaryRole).toString());
455
456 if (!summary.contains(QLatin1Char('<')))
457 {
458 summary = QLatin1String("<p>") + summary + QLatin1String("</p>");
459 }
460
461 summary.append(QLatin1Char('\n'));
462
463 content.prepend(summary);
464 }
465
466 const QString enableImages(SettingsManager::getOption(SettingsManager::Permissions_EnableImagesOption, Utils::extractHost(m_feed->getUrl())).toString());
467 TextBrowserWidget::ImagesPolicy imagesPolicy(TextBrowserWidget::AllImages);
468
469 if (enableImages == QLatin1String("onlyCached"))
470 {
471 imagesPolicy = TextBrowserWidget::OnlyCachedImages;
472 }
473 else if (enableImages == QLatin1String("disabled"))
474 {
475 imagesPolicy = TextBrowserWidget::NoImages;
476 }
477
478 m_ui->titleLabelWidget->setText(index.isValid() ? index.data(Qt::DisplayRole).toString() : QString());
479 m_ui->emailButton->setVisible(!index.data(EmailRole).isNull());
480 m_ui->emailButton->setToolTip(tr("Send email to %1").arg(index.data(EmailRole).toString()));
481 m_ui->urlButton->setVisible(!index.data(UrlRole).isNull());
482 m_ui->urlButton->setToolTip(tr("Go to %1").arg(index.data(UrlRole).toUrl().toDisplayString()));
483 m_ui->authorLabelWidget->setText(index.isValid() ? index.data(AuthorRole).toString() : QString());
484 m_ui->timeLabelWidget->setText(index.isValid() ? Utils::formatDateTime(index.data(index.data(UpdateTimeRole).isNull() ? PublicationTimeRole : UpdateTimeRole).toDateTime()) : QString());
485 m_ui->textBrowserWidget->setImagesPolicy(imagesPolicy);
486 m_ui->textBrowserWidget->setText(content);
487
488 for (int i = (m_ui->categoriesLayout->count() - 1); i >= 0; --i)
489 {
490 m_ui->categoriesLayout->takeAt(i)->widget()->deleteLater();
491 }
492
493 const QStringList entryCategories(index.data(CategoriesRole).toStringList());
494 const QMap<QString, QString> feedCategories(m_feed ? m_feed->getCategories() : QMap<QString, QString>());
495
496 for (int i = 0; i < entryCategories.count(); ++i)
497 {
498 if (!entryCategories.at(i).isEmpty())
499 {
500 const QString label(feedCategories.value(entryCategories.at(i)));
501 QToolButton *toolButton(new QToolButton(m_ui->entryWidget));
502 toolButton->setText(label.isEmpty() ? QString(entryCategories.at(i)).replace(QLatin1Char('_'), QLatin1Char(' ')) : label);
503 toolButton->setObjectName(entryCategories.at(i));
504
505 m_ui->categoriesLayout->addWidget(toolButton);
506
507 connect(toolButton, &QToolButton::clicked, this, &FeedsContentsWidget::selectCategory);
508 }
509 }
510
511 m_ui->categoriesLayout->addStretch();
512
513 if (index.isValid() && m_feed && m_feedModel)
514 {
515 disconnect(m_ui->entriesViewWidget, &ItemViewWidget::needsActionsUpdate, this, &FeedsContentsWidget::updateEntry);
516
517 m_feed->markEntryAsRead(index.data(IdentifierRole).toString());
518
519 m_feedModel->setData(index, QDateTime::currentDateTimeUtc(), LastReadTimeRole);
520
521 m_ui->entriesViewWidget->update();
522
523 connect(m_ui->entriesViewWidget, &ItemViewWidget::needsActionsUpdate, this, &FeedsContentsWidget::updateEntry);
524 }
525
526 emit arbitraryActionsStateChanged({ActionsManager::DeleteAction});
527 }
528
updateFeedModel()529 void FeedsContentsWidget::updateFeedModel()
530 {
531 if (!m_feedModel)
532 {
533 return;
534 }
535
536 const QString identifier(m_ui->entriesViewWidget->currentIndex().data(IdentifierRole).toString());
537
538 m_feedModel->clear();
539 m_feedModel->setHorizontalHeaderLabels({tr("Title"), tr("From"), tr("Published")});
540 m_feedModel->setHeaderData(0, Qt::Horizontal, 300, HeaderViewWidget::WidthRole);
541
542 m_ui->applicationComboBox->clear();
543
544 if (m_feed && !FeedsManager::getModel()->hasFeed(m_feed->getUrl()))
545 {
546 m_ui->applicationComboBox->setItemDelegate(new ItemDelegate(m_ui->applicationComboBox));
547 m_ui->applicationComboBox->addItem(Application::windowIcon(), Application::applicationDisplayName());
548
549 const QVector<ApplicationInformation> applications(Utils::getApplicationsForMimeType(m_feed->getMimeType()));
550
551 if (applications.count() > 0)
552 {
553 m_ui->applicationComboBox->addItem({});
554 m_ui->applicationComboBox->setItemData(1, QLatin1String("separator"), Qt::AccessibleDescriptionRole);
555
556 for (int i = 0; i < applications.count(); ++i)
557 {
558 m_ui->applicationComboBox->addItem(applications.at(i).icon, applications.at(i).name);
559 m_ui->applicationComboBox->setItemData((i + 2), applications.at(i).command, Qt::UserRole);
560
561 if (applications.at(i).icon.isNull())
562 {
563 m_ui->applicationComboBox->setItemData((i + 2), QColor(Qt::transparent), Qt::DecorationRole);
564 }
565 }
566 }
567 }
568
569 if (m_ui->categoriesButton->menu())
570 {
571 m_ui->categoriesButton->menu()->deleteLater();
572 }
573
574 QMenu *menu(new QMenu(m_ui->categoriesButton));
575
576 m_ui->categoriesButton->setMenu(menu);
577
578 connect(menu, &QMenu::triggered, [=](QAction *action)
579 {
580 if (action->data().isNull() && action->isChecked())
581 {
582 m_categories.clear();
583 }
584 else if (menu->actions().count() > 0)
585 {
586 QStringList categories;
587 bool hasAllCategories = true;
588
589 m_categories.clear();
590
591 for (int i = 2; i < menu->actions().count(); ++i)
592 {
593 if (menu->actions().at(i)->isChecked())
594 {
595 categories.append(menu->actions().at(i)->data().toString());
596 }
597 else
598 {
599 hasAllCategories = false;
600 }
601 }
602
603 menu->actions().first()->setChecked(hasAllCategories);
604
605 if (!hasAllCategories)
606 {
607 m_categories = categories;
608 }
609 }
610
611 updateFeedModel();
612 });
613
614 if (!m_feed)
615 {
616 return;
617 }
618
619 QAction *allAction(menu->addAction(tr("All (%1)").arg(m_feed->getEntries().count())));
620 allAction->setCheckable(true);
621 allAction->setChecked(m_categories.isEmpty());
622
623 menu->addSeparator();
624
625 const QMap<QString, QString> categories(m_feed->getCategories());
626 QMap<QString, QString>::const_iterator iterator;
627
628 for (iterator = categories.begin(); iterator != categories.end(); ++iterator)
629 {
630 const int amount(m_feed->getEntries({iterator.key()}).count());
631
632 if (amount > 0)
633 {
634 QAction *action(menu->addAction((iterator.value().isEmpty() ? QString(iterator.key()).replace(QLatin1Char('_'), QLatin1Char(' ')) : iterator.value()) + QLatin1String(" (") + QString::number(amount) + QLatin1Char(')')));
635 action->setCheckable(true);
636 action->setChecked(m_categories.isEmpty() || m_categories.contains(iterator.key()));
637 action->setData(iterator.key());
638 }
639 }
640
641 m_ui->categoriesButton->setEnabled(!categories.isEmpty());
642
643 const QVector<Feed::Entry> entries(m_feed->getEntries(m_categories));
644
645 for (int i = 0; i < entries.count(); ++i)
646 {
647 const Feed::Entry entry(entries.at(i));
648
649 if (!m_categories.isEmpty())
650 {
651 bool hasFound(false);
652
653 for (int j = 0; j < m_categories.count(); ++j)
654 {
655 if (entry.categories.contains(m_categories.at(j)))
656 {
657 hasFound = true;
658
659 break;
660 }
661 }
662
663 if (!hasFound)
664 {
665 continue;
666 }
667 }
668
669 QList<QStandardItem*> items({new QStandardItem(entry.title.isEmpty() ? tr("(Untitled)") : entry.title), new QStandardItem(entry.author.isEmpty() ? tr("(Untitled)") : entry.author), new QStandardItem(Utils::formatDateTime(entry.updateTime.isNull() ? entry.publicationTime : entry.updateTime))});
670 items[0]->setData(entry.url, UrlRole);
671 items[0]->setData(entry.identifier, IdentifierRole);
672 items[0]->setData(entry.summary, SummaryRole);
673 items[0]->setData(entry.content, ContentRole);
674 items[0]->setData(entry.publicationTime, PublicationTimeRole);
675 items[0]->setData(entry.updateTime, UpdateTimeRole);
676 items[0]->setData(entry.author, AuthorRole);
677 items[0]->setData(entry.email, EmailRole);
678 items[0]->setData(entry.lastReadTime, LastReadTimeRole);
679 items[0]->setData(entry.categories, CategoriesRole);
680 items[0]->setFlags(items[0]->flags() | Qt::ItemNeverHasChildren);
681 items[1]->setFlags(items[1]->flags() | Qt::ItemNeverHasChildren);
682 items[2]->setFlags(items[2]->flags() | Qt::ItemNeverHasChildren);
683
684 m_feedModel->appendRow(items);
685
686 if (entry.identifier == identifier)
687 {
688 m_ui->entriesViewWidget->setCurrentIndex(m_ui->entriesViewWidget->getIndex(i));
689 }
690 }
691
692 if (!m_ui->entriesViewWidget->selectionModel()->hasSelection())
693 {
694 m_ui->entriesViewWidget->setCurrentIndex(m_ui->entriesViewWidget->getIndex(0));
695 }
696 }
697
setFeed(Feed * feed)698 void FeedsContentsWidget::setFeed(Feed *feed)
699 {
700 if (feed == m_feed)
701 {
702 return;
703 }
704
705 if (m_feed)
706 {
707 disconnect(m_feed, &Feed::entriesModified, this, &FeedsContentsWidget::updateFeedModel);
708 }
709
710 m_feed = feed;
711
712 if (m_feedModel)
713 {
714 m_feedModel->clear();
715 }
716
717 if (m_feed)
718 {
719 m_categories.clear();
720
721 if (!m_feedModel)
722 {
723 m_feedModel = new QStandardItemModel(this);
724
725 m_ui->entriesViewWidget->setModel(m_feedModel);
726 m_ui->entriesViewWidget->setViewMode(ItemViewWidget::ListView);
727 }
728
729 if (m_feed->getLastSynchronizationTime().isNull())
730 {
731 m_feed->update();
732 }
733
734 if (!FeedsManager::getModel()->hasFeed(m_feed->getUrl()))
735 {
736 m_ui->iconLabel->setPixmap(ThemesManager::createIcon(QLatin1String("application-rss+xml"), false).pixmap(m_ui->iconLabel->size()));
737 m_ui->messageLabel->setText(tr("Subscribe to this feed using:"));
738
739 m_ui->subscribeFeedWidget->show();
740 }
741 else
742 {
743 m_ui->subscribeFeedWidget->hide();
744 }
745
746 connect(m_feed, &Feed::entriesModified, this, &FeedsContentsWidget::updateFeedModel);
747 }
748
749 updateFeedModel();
750
751 emit arbitraryActionsStateChanged({ActionsManager::ReloadAction});
752 emit titleChanged(getTitle());
753 emit iconChanged(getIcon());
754 emit urlChanged(getUrl());
755 }
756
setUrl(const QUrl & url,bool isTyped)757 void FeedsContentsWidget::setUrl(const QUrl &url, bool isTyped)
758 {
759 Q_UNUSED(isTyped)
760
761 if (url.scheme() == QLatin1String("view-feed"))
762 {
763 setFeed(FeedsManager::createFeed(url.toDisplayString().mid(10)));
764 }
765 }
766
getUpdateAnimation()767 Animation* FeedsContentsWidget::getUpdateAnimation()
768 {
769 return m_updateAnimation;
770 }
771
findFolder(const QModelIndex & index) const772 FeedsModel::Entry* FeedsContentsWidget::findFolder(const QModelIndex &index) const
773 {
774 FeedsModel::Entry *entry(FeedsManager::getModel()->getEntry(index));
775
776 if (!entry || entry == FeedsManager::getModel()->getRootEntry() || entry == FeedsManager::getModel()->getTrashEntry())
777 {
778 return FeedsManager::getModel()->getRootEntry();
779 }
780
781 const FeedsModel::EntryType type(static_cast<FeedsModel::EntryType>(entry->data(FeedsModel::TypeRole).toInt()));
782
783 return ((type == FeedsModel::RootEntry || type == FeedsModel::FolderEntry) ? entry : static_cast<FeedsModel::Entry*>(entry->parent()));
784 }
785
getTitle() const786 QString FeedsContentsWidget::getTitle() const
787 {
788 if (m_feed)
789 {
790 const QString title(m_feed->getTitle());
791
792 return tr("Feed: %1").arg(title.isEmpty() ? tr("(Untitled)") : title);
793 }
794
795 return tr("Feeds");
796 }
797
getType() const798 QLatin1String FeedsContentsWidget::getType() const
799 {
800 return QLatin1String("feeds");
801 }
802
getUrl() const803 QUrl FeedsContentsWidget::getUrl() const
804 {
805 return (m_feed ? QUrl(QLatin1String("view-feed:") + m_feed->getUrl().toDisplayString()) : QUrl(QLatin1String("about:feeds")));
806 }
807
getIcon() const808 QIcon FeedsContentsWidget::getIcon() const
809 {
810 if (m_feed)
811 {
812 const QIcon icon(m_feed->getIcon());
813
814 return (icon.isNull() ? ThemesManager::createIcon(QLatin1String("application-rss+xml")) : icon);
815 }
816
817 return ThemesManager::createIcon(QLatin1String("feeds"), false);
818 }
819
getActionState(int identifier,const QVariantMap & parameters) const820 ActionsManager::ActionDefinition::State FeedsContentsWidget::getActionState(int identifier, const QVariantMap ¶meters) const
821 {
822 ActionsManager::ActionDefinition::State state(ActionsManager::getActionDefinition(identifier).getDefaultState());
823
824 switch (identifier)
825 {
826 case ActionsManager::ReloadAction:
827 state.isEnabled = (m_feed != nullptr);
828
829 return state;
830 case ActionsManager::DeleteAction:
831 state.isEnabled = false;
832
833 if (m_ui->feedsViewWidget->hasFocus())
834 {
835 const FeedsModel::EntryType type(static_cast<FeedsModel::EntryType>(m_ui->feedsViewWidget->currentIndex().data(FeedsModel::TypeRole).toInt()));
836
837 state.isEnabled = (type == FeedsModel::FeedEntry || type == FeedsModel::FolderEntry);
838 }
839 else if (m_ui->entriesViewWidget->hasFocus())
840 {
841 state.isEnabled = (m_feed && m_ui->entriesViewWidget->selectionModel()->hasSelection());
842 }
843
844 return state;
845 default:
846 break;
847 }
848
849 return ContentsWidget::getActionState(identifier, parameters);
850 }
851
eventFilter(QObject * object,QEvent * event)852 bool FeedsContentsWidget::eventFilter(QObject *object, QEvent *event)
853 {
854 if ((object == m_ui->entriesViewWidget || object == m_ui->feedsViewWidget ) && event->type() == QEvent::KeyPress)
855 {
856 switch (static_cast<QKeyEvent*>(event)->key())
857 {
858 case Qt::Key_Delete:
859 if (m_ui->feedsViewWidget->hasFocus())
860 {
861 removeFeed();
862 }
863 else if (m_ui->entriesViewWidget->hasFocus())
864 {
865 removeEntry();
866 }
867
868 return true;
869 case Qt::Key_Enter:
870 case Qt::Key_Return:
871 if (m_ui->feedsViewWidget->hasFocus())
872 {
873 openFeed();
874 }
875 else if (m_ui->entriesViewWidget->hasFocus())
876 {
877 openEntry();
878 }
879
880 return true;
881 default:
882 break;
883 }
884 }
885 else if ((object == m_ui->entriesViewWidget->viewport() || object == m_ui->feedsViewWidget->viewport()) && event->type() == QEvent::ToolTip)
886 {
887 const QHelpEvent *helpEvent(static_cast<QHelpEvent*>(event));
888 ItemViewWidget *viewWidget(object == m_ui->feedsViewWidget->viewport() ? m_ui->feedsViewWidget : m_ui->entriesViewWidget);
889 const QModelIndex index(viewWidget->indexAt(helpEvent->pos()));
890
891 if (static_cast<FeedsModel::EntryType>(index.data(FeedsModel::TypeRole).toInt()) != FeedsModel::FeedEntry)
892 {
893 return false;
894 }
895
896 QString toolTip;
897
898 if (index.isValid())
899 {
900 if (object == m_ui->feedsViewWidget)
901 {
902 toolTip = tr("Title: %1").arg(index.data(FeedsModel::TitleRole).toString()) + QLatin1Char('\n') + tr("Address: %1").arg(index.data(FeedsModel::UrlRole).toUrl().toDisplayString());
903
904 if (!index.data(FeedsModel::LastSynchronizationTimeRole).isNull())
905 {
906 toolTip.append(QLatin1Char('\n') + tr("Last update: %1").arg(Utils::formatDateTime(index.data(FeedsModel::LastSynchronizationTimeRole).toDateTime())));
907 }
908 }
909 else
910 {
911 toolTip = tr("Title: %1").arg(index.data(TitleRole).toString());
912
913 if (!index.data(UrlRole).isNull())
914 {
915 toolTip.append(QLatin1Char('\n') + tr("Address: %1").arg(index.data(UrlRole).toUrl().toDisplayString()));
916 }
917 }
918 }
919
920 QToolTip::showText(helpEvent->globalPos(), QFontMetrics(QToolTip::font()).elidedText(toolTip, Qt::ElideRight, (QApplication::desktop()->screenGeometry(viewWidget).width() / 2)), viewWidget, viewWidget->visualRect(index));
921
922 return true;
923 }
924
925 return ContentsWidget::eventFilter(object, event);
926 }
927
928 }
929