1 // For license of this file, see <project-root-folder>/LICENSE.md.
2
3 #include "gui/messagebrowser.h"
4
5 #include "gui/messagebox.h"
6 #include "gui/messagetextbrowser.h"
7 #include "gui/reusable/searchtextwidget.h"
8 #include "miscellaneous/application.h"
9 #include "miscellaneous/settings.h"
10 #include "network-web/webfactory.h"
11 #include "services/abstract/serviceroot.h"
12
13 #include <QKeyEvent>
14 #include <QRegularExpression>
15 #include <QScrollBar>
16 #include <QToolTip>
17 #include <QVBoxLayout>
18
MessageBrowser(bool should_resize_to_fit,QWidget * parent)19 MessageBrowser::MessageBrowser(bool should_resize_to_fit, QWidget* parent)
20 : QWidget(parent), m_txtBrowser(new MessageTextBrowser(this)), m_searchWidget(new SearchTextWidget(this)),
21 m_layout(new QVBoxLayout(this)) {
22 m_layout->setContentsMargins(3, 3, 3, 3);
23 m_layout->addWidget(m_txtBrowser, 1);
24 m_layout->addWidget(m_searchWidget);
25
26 if (should_resize_to_fit) {
27 m_txtBrowser->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::MinimumExpanding);
28 }
29
30 connect(m_searchWidget, &SearchTextWidget::searchCancelled, this, [this]() {
31 m_txtBrowser->textCursor().clearSelection();
32 m_txtBrowser->moveCursor(QTextCursor::MoveOperation::Left);
33 });
34 connect(m_searchWidget, &SearchTextWidget::searchForText, this, [this](const QString& text, bool backwards) {
35 if (backwards) {
36 m_txtBrowser->find(text, QTextDocument::FindFlag::FindBackward);
37 }
38 else {
39 m_txtBrowser->find(text);
40 }
41 });
42
43 connect(m_txtBrowser, &QTextBrowser::anchorClicked, [=](const QUrl& url) {
44 if (url.toString().startsWith(INTERNAL_URL_PASSATTACHMENT) &&
45 m_root != nullptr &&
46 m_root->getParentServiceRoot()->downloadAttachmentOnMyOwn(url)) {
47 return;
48 }
49
50 if (!url.isEmpty()) {
51 bool open_externally_now = qApp->settings()->value(GROUP(Browser),
52 SETTING(Browser::OpenLinksInExternalBrowserRightAway)).toBool();
53
54 if (open_externally_now) {
55 qApp->web()->openUrlInExternalBrowser(url.toString());
56 }
57 else {
58 // User clicked some URL. Open it in external browser or download?
59 MessageBox box(qApp->mainFormWidget());
60 box.setText(tr("You clicked some link. You can download the link contents or open it in external web browser."));
61 box.setInformativeText(tr("What action do you want to take?"));
62 box.setDetailedText(url.toString());
63
64 QAbstractButton* btn_open = box.addButton(tr("Open in external browser"), QMessageBox::ButtonRole::ActionRole);
65 QAbstractButton* btn_download = box.addButton(tr("Download"), QMessageBox::ButtonRole::ActionRole);
66 QAbstractButton* btn_cancel = box.addButton(QMessageBox::StandardButton::Cancel);
67 bool always;
68 MessageBox::setCheckBox(&box, tr("Always open links in external browser."), &always);
69
70 box.setDefaultButton(QMessageBox::StandardButton::Cancel);
71 box.exec();
72
73 if (box.clickedButton() != box.button(QMessageBox::StandardButton::Cancel)) {
74 // Store selected checkbox value.
75 qApp->settings()->setValue(GROUP(Browser), Browser::OpenLinksInExternalBrowserRightAway, always);
76 }
77
78 if (box.clickedButton() == btn_open) {
79 qApp->web()->openUrlInExternalBrowser(url.toString());
80 }
81 else if (box.clickedButton() == btn_download) {
82 qApp->downloadManager()->download(url);
83 }
84
85 btn_download->deleteLater();
86 btn_open->deleteLater();
87 btn_cancel->deleteLater();
88 }
89 }
90 else {
91 MessageBox::show(qApp->mainFormWidget(), QMessageBox::Warning, tr("Incorrect link"), tr("Selected hyperlink is invalid."));
92 }
93 });
94 connect(m_txtBrowser,
95 QOverload<const QUrl&>::of(&QTextBrowser::highlighted),
96 [=](const QUrl& url) {
97 Q_UNUSED(url)
98 QToolTip::showText(QCursor::pos(), tr("Click this link to download it or open it with external browser."), this);
99 });
100
101 m_searchWidget->hide();
102 installEventFilter(this);
103
104 reloadFontSettings();
105 }
106
clear(bool also_hide)107 void MessageBrowser::clear(bool also_hide) {
108 m_txtBrowser->clear();
109 m_pictures.clear();
110 m_searchWidget->hide();
111
112 if (also_hide) {
113 hide();
114 }
115 }
116
prepareHtmlForMessage(const Message & message)117 QString MessageBrowser::prepareHtmlForMessage(const Message& message) {
118 QString html = QString("<h2 align=\"center\">%1</h2>").arg(message.m_title);
119
120 if (!message.m_url.isEmpty()) {
121 html += QString("[url] <a href=\"%1\">%1</a><br/>").arg(message.m_url);
122 }
123
124 for (const Enclosure& enc : message.m_enclosures) {
125 QString enc_url;
126
127 if (!enc.m_url.contains(QRegularExpression(QSL("^(http|ftp|\\/)")))) {
128 enc_url = QString(INTERNAL_URL_PASSATTACHMENT) + QL1S("/?") + enc.m_url;
129 }
130 else {
131 enc_url = enc.m_url;
132 }
133
134 html += QString("[%2] <a href=\"%1\">%1</a><br/>").arg(enc_url, enc.m_mimeType);
135 }
136
137 QRegularExpression imgTagRegex("\\<img[^\\>]*src\\s*=\\s*[\"\']([^\"\']*)[\"\'][^\\>]*\\>",
138 QRegularExpression::PatternOption::CaseInsensitiveOption |
139 QRegularExpression::PatternOption::InvertedGreedinessOption);
140 QRegularExpressionMatchIterator i = imgTagRegex.globalMatch(message.m_contents);
141 QString pictures_html;
142
143 while (i.hasNext()) {
144 QRegularExpressionMatch match = i.next();
145
146 m_pictures.append(match.captured(1));
147 pictures_html += QString("<br/>[%1] <a href=\"%2\">%2</a>").arg(tr("image"), match.captured(1));
148 }
149
150 if (qApp->settings()->value(GROUP(Messages), SETTING(Messages::DisplayImagePlaceholders)).toBool()) {
151 html += message.m_contents;
152 }
153 else {
154 QString cnts = message.m_contents;
155
156 html += cnts.replace(imgTagRegex, QString());
157 }
158
159 html += pictures_html;
160
161 /*html = html
162 .replace(QSL("\r\n"), QSL("\n"))
163 .replace(QL1C('\r'), QL1C('\n'))
164 .remove(QL1C('\n'));*/
165
166 return QSL("<html>"
167 "<head><style>"
168 "a { color: %2; }"
169 "</style></head>"
170 "<body>%1</body>"
171 "</html>").arg(html,
172 qApp->skins()->currentSkin().m_colorPalette[Skin::PaletteColors::Highlight].name());
173 }
174
eventFilter(QObject * watched,QEvent * event)175 bool MessageBrowser::eventFilter(QObject* watched, QEvent* event) {
176 Q_UNUSED(watched)
177
178 if (event->type() == QEvent::Type::KeyPress) {
179 auto* key_event = static_cast<QKeyEvent*>(event);
180
181 if (key_event->matches(QKeySequence::StandardKey::Find)) {
182
183 m_searchWidget->clear();
184 m_searchWidget->show();
185 m_searchWidget->setFocus();
186 return true;
187 }
188 else if (key_event->key() == Qt::Key::Key_Escape) {
189 m_searchWidget->cancelSearch();
190 return true;
191 }
192 }
193
194 return false;
195 }
196
reloadFontSettings()197 void MessageBrowser::reloadFontSettings() {
198 const Settings* settings = qApp->settings();
199 QFont fon;
200
201 fon.fromString(settings->value(GROUP(Messages), SETTING(Messages::PreviewerFontStandard)).toString());
202 m_txtBrowser->setFont(fon);
203 }
204
loadMessage(const Message & message,RootItem * root)205 void MessageBrowser::loadMessage(const Message& message, RootItem* root) {
206 Q_UNUSED(root)
207
208 auto html = prepareHtmlForMessage(message);
209
210 m_txtBrowser->setHtml(html);
211 m_txtBrowser->verticalScrollBar()->triggerAction(QScrollBar::SliderAction::SliderToMinimum);
212 m_searchWidget->hide();
213 }
214
verticalScrollBarPosition() const215 double MessageBrowser::verticalScrollBarPosition() const {
216 return m_txtBrowser->verticalScrollBar()->value();
217 }
218
setVerticalScrollBarPosition(double pos)219 void MessageBrowser::setVerticalScrollBarPosition(double pos) {
220 m_txtBrowser->verticalScrollBar()->setValue(pos);
221 }
222