Akonadi Contacts

standardcontactformatter.cpp
1/*
2 This file is part of Akonadi Contact.
3
4 SPDX-FileCopyrightText: 2010 Tobias Koenig <tokoe@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "standardcontactformatter.h"
10
11#include <Akonadi/Item>
12#include <KColorScheme>
13#include <KConfigGroup>
14
15#include <KContacts/Addressee>
16
17#include <KLocalizedString>
18#include <KStringHandler>
19
20#include <QLocale>
21#include <QRegularExpression>
22#include <QSet>
23
24using namespace Akonadi;
25
26class Akonadi::StandardContactFormatterPrivate
27{
28public:
29 bool displayQRcode = true;
30};
31
33 : d(new StandardContactFormatterPrivate())
34{
35}
36
38
39static int contactAge(const QDate &date)
40{
41 const QDate now = QDate::currentDate();
42 int age = now.year() - date.year();
43 if (date > now.addYears(-age)) {
44 age--;
45 }
46 return age;
47}
48
50{
51 KContacts::Addressee rawContact;
52 const Akonadi::Item localItem = item();
53 if (localItem.isValid() && localItem.hasPayload<KContacts::Addressee>()) {
54 rawContact = localItem.payload<KContacts::Addressee>();
55 } else {
56 rawContact = contact();
57 }
58
59 if (rawContact.isEmpty()) {
60 return {};
61 }
62
63 // We'll be building a table to display the vCard in.
64 // Each row of the table will be built using one of these strings for its HTML.
65
66 // single data item:
67 // %1 is the item name
68 // %2 is the item value
69 QString rowFmtStr1 = QStringLiteral(
70 "<tr valign=\"top\">"
71 "<td align=\"right\" valign=\"top\" width=\"30%\"><b><font color=\"grey\">%1</font></b></td>\n"
72 "<td colspan=\"2\" align=\"left\" valign=\"top\" width=\"70%\"><font>%2</font></td>\n"
73 "</tr>\n");
74
75 // data item plus additional icon(s):
76 // %1 is the item name
77 // %2 is the item value
78 // %3 is the icon(s), each as a HTML <a><img> tag
79 QString rowFmtStr2 = QStringLiteral(
80 "<tr valign=\"top\">"
81 "<td align=\"right\" valign=\"top\" width=\"30%\"><b><font color=\"grey\">%1</font></b></td>\n"
82 "<td align=\"left\" valign=\"top\"><font>%2</font></td>\n"
83 "<td align=\"left\" valign=\"top\">%3</td>\n"
84 "</tr>\n");
85
86 // Build the table's rows here
87 QString dynamicPart;
88
89 // Birthday
90 const QDate date = rawContact.birthday().date();
91 const int years = contactAge(date);
92
93 if (date.isValid()) {
94 dynamicPart += rowFmtStr1.arg(KContacts::Addressee::birthdayLabel(),
95 QLocale().toString(date) + QLatin1StringView("&nbsp;&nbsp;") + i18np("(One year old)", "(%1 years old)", years));
96 }
97
98 // Phone Numbers
99 const auto phoneNumbers = rawContact.phoneNumbers();
100 for (const KContacts::PhoneNumber &number : phoneNumbers) {
101 QString dispLabel = number.typeLabel().replace(QLatin1Char(' '), QStringLiteral("&nbsp;"));
102 QString dispValue = QStringLiteral("<a href=\"tel:%1\">%2</a>").arg(number.normalizedNumber()).arg(number.number().toHtmlEscaped());
103 if (number.type() & KContacts::PhoneNumber::Cell) {
104 QString dispIcon = QStringLiteral("<a href=\"sms:%1\" title=\"%2\"><img src=\"sms_icon\" align=\"top\"/>")
105 .arg(number.normalizedNumber())
106 .arg(i18nc("@info:tooltip", "Send SMS"));
107 dynamicPart += rowFmtStr2.arg(dispLabel, dispValue, dispIcon);
108 } else {
109 dynamicPart += rowFmtStr1.arg(dispLabel, dispValue);
110 }
111 }
112
113 // EMails
114 for (const QString &email : rawContact.emails()) {
115 const QString type = i18nc("a contact's email address", "Email");
116
117 const QString fullEmail = QString::fromLatin1(QUrl::toPercentEncoding(rawContact.fullEmail(email)));
118
119 dynamicPart += rowFmtStr1.arg(type, QStringLiteral("<a href=\"mailto:%1\">%2</a>").arg(fullEmail, email));
120 }
121
122 // Homepage
123 if (rawContact.url().isValid()) {
124 QString url = rawContact.url().url().url();
125 if (!url.startsWith(QLatin1StringView("http://")) && !url.startsWith(QLatin1StringView("https://"))) {
126 url = QLatin1StringView("http://") + url;
127 }
128
130 dynamicPart += rowFmtStr1.arg(i18n("Homepage"), url);
131 }
132
133 // Blog Feed
134 const QString blog = rawContact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("BlogFeed"));
135 if (!blog.isEmpty()) {
136 dynamicPart += rowFmtStr1.arg(i18n("Blog Feed"), KStringHandler::tagUrls(blog.toHtmlEscaped()));
137 }
138
139 // Addresses
140 const auto addresses = rawContact.addresses();
141 for (const KContacts::Address &address : addresses) {
142 QString formattedAddress;
143
144 if (address.label().isEmpty()) {
145 formattedAddress = address.formatted(KContacts::AddressFormatStyle::Postal).trimmed().toHtmlEscaped();
146 } else {
147 formattedAddress = address.label().toHtmlEscaped();
148 }
149
150 formattedAddress.replace(QRegularExpression(QStringLiteral("\n+")), QStringLiteral("<br>"));
151
152 const QString url = QStringLiteral("<a href=\"%1\" title=\"%2\"><img src=\"map_icon\" alt=\"%2\"/></a>")
153 .arg(address.geoUri().toString())
154 .arg(i18nc("@info:tooltip", "Show address on map"));
155 dynamicPart += rowFmtStr2.arg(KContacts::Address::typeLabel(address.type()), formattedAddress, url);
156 }
157
158 // Note
159 QString notes;
160 if (!rawContact.note().isEmpty()) {
161 notes = rowFmtStr1.arg(i18n("Notes"), rawContact.note().toHtmlEscaped().replace(QLatin1Char('\n'), QLatin1StringView("<br>")));
162 }
163
164 // Custom Data
165 QString customData;
166 static QMap<QString, QString> titleMap;
167 if (titleMap.isEmpty()) {
168 titleMap.insert(QStringLiteral("Department"), i18n("Department"));
169 titleMap.insert(QStringLiteral("Profession"), i18n("Profession"));
170 titleMap.insert(QStringLiteral("AssistantsName"), i18n("Assistant's Name"));
171 titleMap.insert(QStringLiteral("ManagersName"), i18n("Manager's Name"));
172 titleMap.insert(QStringLiteral("SpousesName"), i18nc("Wife/Husband/...", "Partner's Name"));
173 titleMap.insert(QStringLiteral("Office"), i18n("Office"));
174 titleMap.insert(QStringLiteral("IMAddress"), i18n("IM Address"));
175 titleMap.insert(QStringLiteral("Anniversary"), i18n("Anniversary"));
176 titleMap.insert(QStringLiteral("AddressBook"), i18n("Address Book"));
177 const QMap<QString, QString> tmpTitleMap = titleMap;
178
179 QMap<QString, QString>::const_iterator iterator = tmpTitleMap.constBegin();
180 while (iterator != tmpTitleMap.constEnd()) {
181 titleMap.insert(iterator.key().toUpper(), iterator.value());
182 ++iterator;
183 }
184 }
185
186 static QSet<QString> blacklistedKeys;
187 if (blacklistedKeys.isEmpty()) {
188 blacklistedKeys.insert(QStringLiteral("CRYPTOPROTOPREF"));
189 blacklistedKeys.insert(QStringLiteral("OPENPGPFP"));
190 blacklistedKeys.insert(QStringLiteral("SMIMEFP"));
191 blacklistedKeys.insert(QStringLiteral("CRYPTOSIGNPREF"));
192 blacklistedKeys.insert(QStringLiteral("CRYPTOENCRYPTPREF"));
193 blacklistedKeys.insert(QStringLiteral("MailPreferedFormatting"));
194 blacklistedKeys.insert(QStringLiteral("MailAllowToRemoteContent"));
195 blacklistedKeys.insert(QStringLiteral("MAILPREFEREDFORMATTING"));
196 blacklistedKeys.insert(QStringLiteral("MAILALLOWTOREMOTECONTENT"));
197 }
198
199 if (!rawContact.customs().empty()) {
200 const QStringList customs = rawContact.customs();
201 for (QString custom : customs) {
202 if (custom.startsWith(QLatin1StringView("KADDRESSBOOK-"))) {
203 custom.remove(QStringLiteral("KADDRESSBOOK-X-"));
204 custom.remove(QStringLiteral("KADDRESSBOOK-"));
205
206 int pos = custom.indexOf(QLatin1Char(':'));
207 QString key = custom.left(pos);
208 QString value = custom.mid(pos + 1);
209
210 // convert anniversary correctly
211 if (key == QLatin1StringView("Anniversary") || key == QLatin1StringView("ANNIVERSARY")) {
212 const QDateTime dateTime = QDateTime::fromString(value, Qt::ISODate);
213 value = QLocale().toString(dateTime.date());
214 } else if (key == QLatin1StringView("BlogFeed") || key == QLatin1StringView("BLOGFEED")) { // blog is handled separated
215 continue;
216 } else if (blacklistedKeys.contains(key)) {
217 continue;
218 }
219
220 // check whether we have a mapping for the title
221 const QMap<QString, QString>::ConstIterator keyIt = titleMap.constFind(key);
222 bool needToEscape = true;
223 if (keyIt != titleMap.constEnd()) {
224 key = keyIt.value();
225 } else {
226 // check whether it is a custom local field
227 for (const QVariantMap &description : customFieldDescriptions()) {
228 if (description.value(QStringLiteral("key")).toString() == key) {
229 key = description.value(QStringLiteral("title")).toString();
230 const QString descriptionType = description.value(QStringLiteral("type")).toString();
231 if (descriptionType == QLatin1StringView("boolean")) {
232 if (value == QLatin1StringView("true")) {
233 value = i18nc("Boolean value", "yes");
234 } else {
235 value = i18nc("Boolean value", "no");
236 }
237 } else if (descriptionType == QLatin1StringView("date")) {
238 const QDate date = QDate::fromString(value, Qt::ISODate);
239 value = QLocale().toString(date, QLocale::ShortFormat);
240 } else if (descriptionType == QLatin1StringView("time")) {
241 const QTime time = QTime::fromString(value, Qt::ISODate);
242 value = QLocale().toString(time);
243 } else if (descriptionType == QLatin1StringView("datetime")) {
244 const QDateTime dateTime = QDateTime::fromString(value, Qt::ISODate);
245 value = QLocale().toString(dateTime, QLocale::ShortFormat);
246 } else if (descriptionType == QLatin1StringView("url")) {
247 value = KStringHandler::tagUrls(value.toHtmlEscaped());
248 needToEscape = false;
249 }
250
251 break;
252 }
253 }
254 }
255 if (needToEscape) {
256 value = value.toHtmlEscaped();
257 }
258 customData += rowFmtStr1.arg(key, value);
259 }
260 }
261 }
262
263 // Assemble all parts
264 QString role = rawContact.title();
265 if (role.isEmpty()) {
266 role = rawContact.role();
267 }
268 if (role.isEmpty()) {
269 role = rawContact.custom(QStringLiteral("KADDRESSBOOK"), QStringLiteral("X-Profession"));
270 }
271
272 QString strAddr =
273 QStringLiteral(
274 "<div align=\"center\">"
275 "<table cellpadding=\"3\" cellspacing=\"1\">"
276 "<tr>"
277 "<td align=\"right\" valign=\"top\" width=\"30%\" rowspan=\"3\">"
278 "<img src=\"%1\" width=\"100\" vspace=\"1\"/>" // image
279 "</td>"
280 "<td colspan=\"2\" align=\"left\" width=\"70%\"><font size=\"+2\"><b>%2</b></font></td>" // name
281 "</tr>"
282 "<tr>"
283 "<td colspan=\"2\" align=\"left\" width=\"70%\">%3</td>" // role
284 "</tr>"
285 "<tr>"
286 "<td colspan=\"2\" align=\"left\" width=\"70%\">%4</td>" // organization
287 "</tr>")
288 .arg(QStringLiteral("contact_photo"), rawContact.realName().toHtmlEscaped(), role.toHtmlEscaped(), rawContact.organization().toHtmlEscaped());
289
290 strAddr.append(dynamicPart);
291 strAddr.append(notes);
292 strAddr.append(customData);
293 strAddr.append(QLatin1StringView("</table>"));
294
295 if (d->displayQRcode) {
296 KConfig config(QStringLiteral("akonadi_contactrc"));
297 KConfigGroup group(&config, QStringLiteral("View"));
298 if (group.readEntry("QRCodes", true)) {
299 strAddr.append(QString::fromLatin1("<p align=\"center\">"
300 "<img src=\"%1\" vspace=\"1\"/>"
301 "</p>")
302 .arg(QStringLiteral("qrcode")));
303 }
304 }
305
306 strAddr.append(QLatin1StringView("</div>\n"));
307
308 if (form == EmbeddableForm) {
309 return strAddr;
310 }
311
312 const QString document = QStringLiteral(
313 "<html>"
314 "<head>"
315 " <style type=\"text/css\">"
316 " a {text-decoration:none; color:%1}"
317 " </style>"
318 "</head>"
319 "<body text=\"%1\" bgcolor=\"%2\">" // text and background color
320 "%3" // contact part
321 "</body>"
322 "</html>")
323 .arg(KColorScheme(QPalette::Active, KColorScheme::View).foreground().color().name(),
324 KColorScheme(QPalette::Active, KColorScheme::View).background().color().name(),
325 strAddr);
326
327 return document;
328}
329
330void StandardContactFormatter::setDisplayQRCode(bool show)
331{
332 d->displayQRcode = show;
333}
334
335bool StandardContactFormatter::displayQRCode() const
336{
337 return d->displayQRcode;
338}
KContacts::Addressee contact() const
Returns the contact that will be formatted.
Akonadi::Item item() const
Returns the item who's payload will be formatted.
QList< QVariantMap > customFieldDescriptions() const
Returns the custom field descriptions that will be used.
HtmlForm
Describes the form of the HTML that is created.
@ EmbeddableForm
Creates a div HTML element that can be embedded.
T payload() const
bool isValid() const
bool hasPayload() const
StandardContactFormatter()
Creates a new standard contact formatter.
~StandardContactFormatter() override
Destroys the standard contact formatter.
QString toHtml(HtmlForm form=SelfcontainedForm) const override
Returns the contact formatted as HTML.
QString readEntry(const char *key, const char *aDefault=nullptr) const
QString typeLabel() const
QStringList emails() const
QString custom(const QString &app, const QString &name) const
QString organization() const
QStringList customs() const
QString note() const
QDateTime birthday() const
bool isEmpty() const
QString role() const
QString realName() const
QString title() const
QString fullEmail(const QString &email=QString()) const
ResourceLocatorUrl url() const
static QString birthdayLabel()
Address::List addresses() const
PhoneNumber::List phoneNumbers() const
QString i18np(const char *singular, const char *plural, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
A widget for editing the display name of a contact.
KCOREADDONS_EXPORT QString tagUrls(const QString &text)
QDate addYears(int nyears) const const
QDate currentDate()
QDate fromString(QStringView string, QStringView format, QCalendar cal)
bool isValid(int year, int month, int day)
int year() const const
QDate date() const const
QDateTime fromString(QStringView string, QStringView format, QCalendar cal)
bool empty() const const
void remove(qsizetype i, qsizetype n)
QString toString(QDate date, FormatType format) const const
const_iterator constBegin() const const
const_iterator constEnd() const const
const_iterator constFind(const Key &key) const const
iterator insert(const Key &key, const T &value)
bool isEmpty() const const
T value(const Key &key, const T &defaultValue) const const
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
bool isEmpty() const const
QString & append(QChar ch)
QString arg(Args &&... args) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString left(qsizetype n) const const
QString mid(qsizetype position, qsizetype n) const const
QString number(double n, char format, int precision)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toHtmlEscaped() const const
QTime fromString(QStringView string, QStringView format)
QByteArray toPercentEncoding(const QString &input, const QByteArray &exclude, const QByteArray &include)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:13:20 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.