1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "gui/feedmessageviewer.h"
4 
5 #include "3rd-party/boolinq/boolinq.h"
6 #include "core/feeddownloader.h"
7 #include "core/feedsproxymodel.h"
8 #include "core/messagesproxymodel.h"
9 #include "database/databasecleaner.h"
10 #include "database/databasefactory.h"
11 #include "exceptions/applicationexception.h"
12 #include "gui/dialogs/formdatabasecleanup.h"
13 #include "gui/dialogs/formmain.h"
14 #include "gui/feedsview.h"
15 #include "gui/messagebox.h"
16 #include "gui/messagepreviewer.h"
17 #include "gui/messagesview.h"
18 #include "gui/systemtrayicon.h"
19 #include "gui/toolbars/feedstoolbar.h"
20 #include "gui/toolbars/messagestoolbar.h"
21 #include "gui/toolbars/statusbar.h"
22 #include "miscellaneous/feedreader.h"
23 #include "miscellaneous/iconfactory.h"
24 #include "miscellaneous/mutex.h"
25 #include "miscellaneous/settings.h"
26 #include "miscellaneous/systemfactory.h"
27 #include "miscellaneous/templates.h"
28 #include "services/standard/standardfeed.h"
29 #include "services/standard/standardfeedsimportexportmodel.h"
30 #include "services/standard/standardserviceroot.h"
31 
32 #if defined(USE_WEBENGINE)
33 #include "gui/webbrowser.h"
34 #endif
35 
36 #include <QAction>
37 #include <QDebug>
38 #include <QLineEdit>
39 #include <QMenu>
40 #include <QPointer>
41 #include <QProgressBar>
42 #include <QSplitter>
43 #include <QStatusBar>
44 #include <QToolBar>
45 #include <QToolButton>
46 #include <QVBoxLayout>
47 #include <QWidgetAction>
48 
FeedMessageViewer(QWidget * parent)49 FeedMessageViewer::FeedMessageViewer(QWidget* parent) : TabContent(parent), m_toolBarsEnabled(true), m_listHeadersEnabled(true),
50   m_toolBarFeeds(new FeedsToolBar(tr("Toolbar for feeds"), this)), m_toolBarMessages(new MessagesToolBar(tr("Toolbar for articles"), this)),
51   m_messagesView(new MessagesView(this)), m_feedsView(new FeedsView(this)),
52   m_messagesBrowser(new MessagePreviewer(false, this)) {
53   initialize();
54   initializeViews();
55   createConnections();
56 }
57 
~FeedMessageViewer()58 FeedMessageViewer::~FeedMessageViewer() {
59   qDebugNN << LOGSEC_GUI << "Destroying FeedMessageViewer instance.";
60 }
61 
62 #if defined(USE_WEBENGINE)
63 
webBrowser() const64 WebBrowser* FeedMessageViewer::webBrowser() const {
65   return m_messagesBrowser->webBrowser();
66 }
67 
68 #endif
69 
feedsView() const70 FeedsView* FeedMessageViewer::feedsView() const {
71   return m_feedsView;
72 }
73 
messagesView() const74 MessagesView* FeedMessageViewer::messagesView() const {
75   return m_messagesView;
76 }
77 
messagesToolBar() const78 MessagesToolBar* FeedMessageViewer::messagesToolBar() const {
79   return m_toolBarMessages;
80 }
81 
feedsToolBar() const82 FeedsToolBar* FeedMessageViewer::feedsToolBar() const {
83   return m_toolBarFeeds;
84 }
85 
saveSize()86 void FeedMessageViewer::saveSize() {
87   Settings* settings = qApp->settings();
88 
89   m_feedsView->saveAllExpandStates();
90 
91   // Store offsets of splitters.
92   settings->setValue(GROUP(GUI), GUI::SplitterFeeds, toVariant(m_feedSplitter->sizes()));
93 
94   // We need to display message previewer so that it "has" some dimensions
95   // so that they can be saved.
96   m_messagesBrowser->show();
97   qApp->processEvents();
98 
99   if (!settings->value(GROUP(GUI), SETTING(GUI::SplitterMessagesIsVertical)).toBool()) {
100     settings->setValue(GROUP(GUI),
101                        GUI::SplitterMessagesHorizontal,
102                        toVariant(m_messageSplitter->sizes()));
103   }
104   else {
105     settings->setValue(GROUP(GUI),
106                        GUI::SplitterMessagesVertical,
107                        toVariant(m_messageSplitter->sizes()));
108   }
109 
110   settings->setValue(GROUP(GUI), GUI::MessageViewState, QString(m_messagesView->saveHeaderState().toBase64()));
111 
112   // Store "visibility" of toolbars and list headers.
113   settings->setValue(GROUP(GUI), GUI::ToolbarsVisible, m_toolBarsEnabled);
114   settings->setValue(GROUP(GUI), GUI::ListHeadersVisible, m_listHeadersEnabled);
115 }
116 
loadSize()117 void FeedMessageViewer::loadSize() {
118   const Settings* settings = qApp->settings();
119 
120   // Restore offsets of splitters.
121   m_feedSplitter->setSizes(toList<int>(settings->value(GROUP(GUI),
122                                                        SETTING(GUI::SplitterFeeds))));
123 
124   if (settings->value(GROUP(GUI), SETTING(GUI::SplitterMessagesIsVertical)).toBool()) {
125     m_messageSplitter->setSizes(toList<int>(settings->value(GROUP(GUI),
126                                                             SETTING(GUI::SplitterMessagesVertical))));
127   }
128   else {
129     switchMessageSplitterOrientation(false);
130   }
131 
132   QString settings_msg_header = settings->value(GROUP(GUI), SETTING(GUI::MessageViewState)).toString();
133 
134   if (!settings_msg_header.isEmpty()) {
135     m_messagesView->restoreHeaderState(QByteArray::fromBase64(settings_msg_header.toLocal8Bit()));
136   }
137 }
138 
loadMessageViewerFonts()139 void FeedMessageViewer::loadMessageViewerFonts() {
140   m_messagesBrowser->reloadFontSettings();
141   m_messagesView->reloadFontSettings();
142   m_feedsView->reloadFontSettings();
143 }
144 
areToolBarsEnabled() const145 bool FeedMessageViewer::areToolBarsEnabled() const {
146   return m_toolBarsEnabled;
147 }
148 
areListHeadersEnabled() const149 bool FeedMessageViewer::areListHeadersEnabled() const {
150   return m_listHeadersEnabled;
151 }
152 
switchMessageSplitterOrientation(bool save_settings)153 void FeedMessageViewer::switchMessageSplitterOrientation(bool save_settings) {
154   bool preview_visible = m_messagesBrowser->isVisible();
155 
156   if (!preview_visible && save_settings) {
157     // Must be visible to get correct dimensions to be saved.
158     m_messagesBrowser->show();
159     qApp->processEvents();
160   }
161 
162   if (m_messageSplitter->orientation() == Qt::Orientation::Vertical) {
163     if (save_settings) {
164       qApp->settings()->setValue(GROUP(GUI),
165                                  GUI::SplitterMessagesVertical,
166                                  toVariant(m_messageSplitter->sizes()));
167     }
168 
169     m_messageSplitter->setOrientation(Qt::Orientation::Horizontal);
170     m_messageSplitter->setSizes(toList<int>(qApp->settings()->value(GROUP(GUI),
171                                                                     SETTING(GUI::SplitterMessagesHorizontal))));
172   }
173   else {
174     if (save_settings) {
175       qApp->settings()->setValue(GROUP(GUI),
176                                  GUI::SplitterMessagesHorizontal,
177                                  toVariant(m_messageSplitter->sizes()));
178     }
179 
180     m_messageSplitter->setOrientation(Qt::Orientation::Vertical);
181     m_messageSplitter->setSizes(toList<int>(qApp->settings()->value(GROUP(GUI),
182                                                                     SETTING(GUI::SplitterMessagesVertical))));
183   }
184 
185   if (!preview_visible && save_settings) {
186     // Must be visible to get correct dimensions to be saved.
187     m_messagesBrowser->hide();
188     qApp->processEvents();
189   }
190 
191   qApp->settings()->setValue(GROUP(GUI),
192                              GUI::SplitterMessagesIsVertical,
193                              m_messageSplitter->orientation() == Qt::Orientation::Vertical);
194 }
195 
setToolBarsEnabled(bool enable)196 void FeedMessageViewer::setToolBarsEnabled(bool enable) {
197   m_toolBarsEnabled = enable;
198   m_toolBarFeeds->setVisible(enable);
199   m_toolBarMessages->setVisible(enable);
200 }
201 
setListHeadersEnabled(bool enable)202 void FeedMessageViewer::setListHeadersEnabled(bool enable) {
203   m_listHeadersEnabled = enable;
204   m_feedsView->header()->setVisible(enable);
205   m_messagesView->header()->setVisible(enable);
206 }
207 
switchFeedComponentVisibility()208 void FeedMessageViewer::switchFeedComponentVisibility() {
209   auto* sen = qobject_cast<QAction*>(sender());
210 
211   if (sen != nullptr) {
212     m_feedsWidget->setVisible(sen->isChecked());
213   }
214   else {
215     m_feedsWidget->setVisible(!m_feedsWidget->isVisible());
216   }
217 }
218 
toggleShowOnlyUnreadMessages()219 void FeedMessageViewer::toggleShowOnlyUnreadMessages() {
220   const QAction* origin = qobject_cast<QAction*>(sender());
221 
222   if (origin == nullptr) {
223     m_messagesView->switchShowUnreadOnly(true, false);
224   }
225   else {
226     m_messagesView->switchShowUnreadOnly(true, origin->isChecked());
227   }
228 }
229 
toggleShowOnlyUnreadFeeds()230 void FeedMessageViewer::toggleShowOnlyUnreadFeeds() {
231   const QAction* origin = qobject_cast<QAction*>(sender());
232 
233   if (origin == nullptr) {
234     m_feedsView->model()->invalidateReadFeedsFilter(true, false);
235   }
236   else {
237     m_feedsView->model()->invalidateReadFeedsFilter(true, origin->isChecked());
238   }
239 }
240 
toggleShowFeedTreeBranches()241 void FeedMessageViewer::toggleShowFeedTreeBranches() {
242   const QAction* origin = qobject_cast<QAction*>(sender());
243 
244   m_feedsView->setRootIsDecorated(origin->isChecked());
245   qApp->settings()->setValue(GROUP(Feeds), Feeds::ShowTreeBranches, origin->isChecked());
246 }
247 
toggleItemsAutoExpandingOnSelection()248 void FeedMessageViewer::toggleItemsAutoExpandingOnSelection() {
249   const QAction* origin = qobject_cast<QAction*>(sender());
250 
251   qApp->settings()->setValue(GROUP(Feeds), Feeds::AutoExpandOnSelection, origin->isChecked());
252 }
253 
alternateRowColorsInLists()254 void FeedMessageViewer::alternateRowColorsInLists() {
255   const QAction* origin = qobject_cast<QAction*>(sender());
256 
257   m_feedsView->setAlternatingRowColors(origin->isChecked());
258   m_messagesView->setAlternatingRowColors(origin->isChecked());
259 
260   qApp->settings()->setValue(GROUP(GUI), GUI::AlternateRowColorsInLists, origin->isChecked());
261 }
262 
displayMessage(const Message & message,RootItem * root)263 void FeedMessageViewer::displayMessage(const Message& message, RootItem* root) {
264   if (qApp->settings()->value(GROUP(Messages), SETTING(Messages::EnableMessagePreview)).toBool()) {
265     m_messagesBrowser->loadMessage(message, root);
266   }
267   else {
268     m_messagesBrowser->hide();
269   }
270 }
271 
createConnections()272 void FeedMessageViewer::createConnections() {
273   // Filtering & searching.
274   connect(m_toolBarMessages, &MessagesToolBar::messageSearchPatternChanged, m_messagesView, &MessagesView::searchMessages);
275   connect(m_toolBarFeeds, &FeedsToolBar::feedsFilterPatternChanged, m_feedsView, &FeedsView::filterItems);
276   connect(m_toolBarMessages, &MessagesToolBar::messageFilterChanged, m_messagesView, &MessagesView::filterMessages);
277 
278   connect(m_messagesView, &MessagesView::currentMessageRemoved, m_messagesBrowser, &MessagePreviewer::clear);
279   connect(m_messagesBrowser, &MessagePreviewer::markMessageRead, m_messagesView->sourceModel(), &MessagesModel::setMessageReadById);
280   connect(m_messagesBrowser, &MessagePreviewer::markMessageImportant,
281           m_messagesView->sourceModel(), &MessagesModel::setMessageImportantById);
282 
283   connect(m_messagesView, &MessagesView::currentMessageChanged, this, &FeedMessageViewer::displayMessage);
284 
285   // If user selects feeds, load their messages.
286   connect(m_feedsView, &FeedsView::itemSelected, m_messagesView, &MessagesView::loadItem);
287   connect(m_feedsView, &FeedsView::requestViewNextUnreadMessage, m_messagesView, &MessagesView::selectNextUnreadItem);
288 
289   // State of many messages is changed, then we need
290   // to reload selections.
291   connect(m_feedsView->sourceModel(), &FeedsModel::reloadMessageListRequested, m_messagesView, &MessagesView::reloadSelections);
292 }
293 
messagesBrowser() const294 MessagePreviewer* FeedMessageViewer::messagesBrowser() const {
295   return m_messagesBrowser;
296 }
297 
initialize()298 void FeedMessageViewer::initialize() {
299   // Initialize/populate toolbars.
300   m_toolBarFeeds->setFloatable(false);
301   m_toolBarFeeds->setMovable(false);
302   m_toolBarFeeds->setAllowedAreas(Qt::ToolBarArea::TopToolBarArea);
303   m_toolBarMessages->setFloatable(false);
304   m_toolBarMessages->setMovable(false);
305   m_toolBarMessages->setAllowedAreas(Qt::ToolBarArea::TopToolBarArea);
306   m_messagesBrowser->clear();
307 
308   // Now refresh visual setup.
309   refreshVisualProperties();
310 }
311 
initializeViews()312 void FeedMessageViewer::initializeViews() {
313   m_feedsWidget = new QWidget(this);
314   m_messagesWidget = new QWidget(this);
315   m_feedSplitter = new QSplitter(Qt::Orientation::Horizontal, this);
316   m_messageSplitter = new QSplitter(Qt::Orientation::Vertical, this);
317 
318   // Instantiate needed components.
319   auto* central_layout = new QVBoxLayout(this);
320   auto* feed_layout = new QVBoxLayout(m_feedsWidget);
321   auto* message_layout = new QVBoxLayout(m_messagesWidget);
322 
323   // Set layout properties.
324   central_layout->setContentsMargins({});
325   feed_layout->setContentsMargins({});
326   message_layout->setContentsMargins({});
327 
328   central_layout->setSpacing(0);
329   feed_layout->setSpacing(0);
330   message_layout->setSpacing(0);
331 
332   // Set views.
333   m_feedsView->setFrameStyle(QFrame::Shape::NoFrame);
334   m_messagesView->setFrameStyle(QFrame::Shape::NoFrame);
335 
336   // Setup message splitter.
337   m_messageSplitter->setObjectName(QSL("MessageSplitter"));
338   m_messageSplitter->setHandleWidth(1);
339   m_messageSplitter->setOpaqueResize(false);
340   m_messageSplitter->setChildrenCollapsible(false);
341   m_messageSplitter->addWidget(m_messagesView);
342   m_messageSplitter->addWidget(m_messagesBrowser);
343 
344   // Assemble message-related components to single widget.
345   message_layout->addWidget(m_toolBarMessages);
346   message_layout->addWidget(m_messageSplitter);
347 
348   // Assemble feed-related components to another widget.
349   feed_layout->addWidget(m_toolBarFeeds);
350   feed_layout->addWidget(m_feedsView);
351 
352   // Assembler everything together.
353   m_feedSplitter->setHandleWidth(1);
354   m_feedSplitter->setOpaqueResize(false);
355   m_feedSplitter->setChildrenCollapsible(false);
356   m_feedSplitter->addWidget(m_feedsWidget);
357   m_feedSplitter->addWidget(m_messagesWidget);
358 
359   // Add toolbar and main feeds/messages widget to main layout.
360   central_layout->addWidget(m_feedSplitter);
361   setTabOrder(m_feedsView, m_messagesView);
362   setTabOrder(m_messagesView, m_toolBarFeeds);
363   setTabOrder(m_toolBarFeeds, m_toolBarMessages);
364   setTabOrder(m_toolBarMessages, m_messagesBrowser);
365 
366   // Set initial ratio of sizes.
367   m_feedSplitter->setStretchFactor(0, 1);
368   m_feedSplitter->setStretchFactor(1, 3);
369 }
370 
refreshVisualProperties()371 void FeedMessageViewer::refreshVisualProperties() {
372   const Qt::ToolButtonStyle button_style =
373     static_cast<Qt::ToolButtonStyle>(qApp->settings()->value(GROUP(GUI), SETTING(GUI::ToolbarStyle)).toInt());
374 
375   m_toolBarFeeds->setToolButtonStyle(button_style);
376   m_toolBarMessages->setToolButtonStyle(button_style);
377 }
378