qTox  Version: nightly | Commit: bc751c8e1cac455f9690654fcfe0f560d2d7dfdd
smileypack.cpp
Go to the documentation of this file.
1 /*
2  Copyright © 2014-2019 by The qTox Project Contributors
3 
4  This file is part of qTox, a Qt-based graphical interface for Tox.
5 
6  qTox is libre software: you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation, either version 3 of the License, or
9  (at your option) any later version.
10 
11  qTox is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with qTox. If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "smileypack.h"
22 
23 #include <QDir>
24 #include <QDomElement>
25 #include <QRegularExpression>
26 #include <QStandardPaths>
27 #include <QStringBuilder>
28 #include <QtConcurrent/QtConcurrentRun>
29 #include <QTimer>
30 
31 #if defined(Q_OS_FREEBSD)
32 #include <locale.h>
33 #endif
34 
55 QStringList loadDefaultPaths();
56 
57 static const QStringList DEFAULT_PATHS = loadDefaultPaths();
58 
59 static const QString RICH_TEXT_PATTERN = QStringLiteral("<img title=\"%1\" src=\"key:%1\"\\>");
60 
61 static const QString EMOTICONS_FILE_NAME = QStringLiteral("emoticons.xml");
62 
63 static constexpr int CLEANUP_TIMEOUT = 5 * 60 * 1000; // 5 minutes
64 
70 QStringList loadDefaultPaths()
71 {
72 #if defined(Q_OS_FREEBSD)
73  // TODO: Remove when will be fixed.
74  // Workaround to fix https://bugreports.qt.io/browse/QTBUG-57522
75  setlocale(LC_ALL, "");
76 #endif
77  const QString EMOTICONS_SUB_PATH = QDir::separator() + QStringLiteral("emoticons");
78  QStringList paths{":/smileys", "~/.kde4/share/emoticons", "~/.kde/share/emoticons",
79  EMOTICONS_SUB_PATH};
80 
81  // qTox exclusive emoticons
82  QStandardPaths::StandardLocation location;
83  location = QStandardPaths::AppDataLocation;
84 
85  QStringList locations = QStandardPaths::standardLocations(location);
86  // system wide emoticons
87  locations.append(QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation));
88  for (QString qtoxPath : locations) {
89  qtoxPath.append(EMOTICONS_SUB_PATH);
90  if (!paths.contains(qtoxPath)) {
91  paths.append(qtoxPath);
92  }
93  }
94 
95  return paths;
96 }
97 
103 QString getAsRichText(const QString& key)
104 {
105  return RICH_TEXT_PATTERN.arg(key);
106 }
107 
108 bool isAscii(const QString& string)
109 {
110  constexpr auto asciiExtMask = 0x80;
111 
112  return (string.toUtf8()[0] & asciiExtMask) == 0;
113 }
114 
116  : cleanupTimer{new QTimer(this)}
117 {
118  loadingMutex.lock();
119  QtConcurrent::run(this, &SmileyPack::load, Settings::getInstance().getSmileyPack());
122  connect(cleanupTimer, &QTimer::timeout, this, &SmileyPack::cleanupIconsCache);
123  cleanupTimer->start(CLEANUP_TIMEOUT);
124 }
125 
127 {
128  delete cleanupTimer;
129 }
130 
132 {
133  QMutexLocker locker(&loadingMutex);
134  for (auto it = cachedIcon.begin(); it != cachedIcon.end();) {
135  std::shared_ptr<QIcon>& icon = it->second;
136  if (icon.use_count() == 1) {
137  it = cachedIcon.erase(it);
138  } else {
139  ++it;
140  }
141  }
142 }
143 
148 {
149  static SmileyPack smileyPack;
150  return smileyPack;
151 }
152 
157 {
158  return listSmileyPacks(DEFAULT_PATHS);
159 }
160 
167 {
168  QList<QPair<QString, QString>> smileyPacks;
169  const QString homePath = QDir::homePath();
170  for (QString path : paths) {
171  if (path.startsWith('~')) {
172  path.replace(0, 1, homePath);
173  }
174 
175  QDir dir(path);
176  if (!dir.exists()) {
177  continue;
178  }
179 
180  for (const QString& subdirectory : dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
181  dir.cd(subdirectory);
182  if (dir.exists(EMOTICONS_FILE_NAME)) {
183  QString absPath = dir.absolutePath() + QDir::separator() + EMOTICONS_FILE_NAME;
184  QPair<QString, QString> p{dir.dirName(), absPath};
185  if (!smileyPacks.contains(p)) {
186  smileyPacks.append(p);
187  }
188  }
189 
190  dir.cdUp();
191  }
192  }
193 
194  return smileyPacks;
195 }
196 
203 bool SmileyPack::load(const QString& filename)
204 {
205  QFile xmlFile(filename);
206  if (!xmlFile.exists() || !xmlFile.open(QIODevice::ReadOnly)) {
207  loadingMutex.unlock();
208  return false;
209  }
210 
211  QDomDocument doc;
212  doc.setContent(xmlFile.readAll());
213  xmlFile.close();
214 
215  /* parse the cfg file
216  * sample:
217  * <?xml version='1.0'?>
218  * <messaging-emoticon-map>
219  * <emoticon file="smile.png" >
220  * <string>:)</string>
221  * <string>:-)</string>
222  * </emoticon>
223  * <emoticon file="sad.png" >
224  * <string>:(</string>
225  * <string>:-(</string>
226  * </emoticon>
227  * </messaging-emoticon-map>
228  */
229 
230  path = QFileInfo(filename).absolutePath();
231  QDomNodeList emoticonElements = doc.elementsByTagName("emoticon");
232  const QString itemName = QStringLiteral("file");
233  const QString childName = QStringLiteral("string");
234  const int iconsCount = emoticonElements.size();
235  emoticons.clear();
236  emoticonToPath.clear();
237  cachedIcon.clear();
238 
239  for (int i = 0; i < iconsCount; ++i) {
240  QDomNode node = emoticonElements.at(i);
241  QString iconName = node.attributes().namedItem(itemName).nodeValue();
242  QString iconPath = QDir{path}.filePath(iconName);
243  QDomElement stringElement = node.firstChildElement(childName);
244  QStringList emoticonList;
245  while (!stringElement.isNull()) {
246  QString emoticon = stringElement.text().replace("<", "&lt;").replace(">", "&gt;");
247  emoticonToPath.insert(emoticon, iconPath);
248  emoticonList.append(emoticon);
249  stringElement = stringElement.nextSibling().toElement();
250  }
251 
252  emoticons.append(emoticonList);
253  }
254 
255  constructRegex();
256 
257  loadingMutex.unlock();
258  return true;
259 }
260 
265 {
266  QString allPattern = QStringLiteral("(");
267  QString regularPatterns;
268  QString multiCharacterEmojiPatterns;
269 
270  // construct one big regex that matches on every emoticon
271  for (const QString& emote : emoticonToPath.keys()) {
272  if (!isAscii(emote)) {
273  if (emote.toUcs4().length() == 1) {
274  regularPatterns.append(emote);
275  regularPatterns.append(QStringLiteral("|"));
276  }
277  else {
278  multiCharacterEmojiPatterns.append(emote);
279  multiCharacterEmojiPatterns.append(QStringLiteral("|"));
280  }
281  } else {
282  // patterns like ":)" or ":smile:", don't match inside a word or else will hit punctuation and html tags
283  regularPatterns.append(QStringLiteral(R"((?<=^|\s))") % QRegularExpression::escape(emote) % QStringLiteral(R"((?=$|\s))"));
284  regularPatterns.append(QStringLiteral("|"));
285  }
286  }
287 
288  // Regexps are evaluated from left to right, insert multichar emojis first so they are evaluated first
289  allPattern.append(multiCharacterEmojiPatterns);
290  allPattern.append(regularPatterns);
291  allPattern[allPattern.size() - 1] = QChar(')');
292 
293  // compile and optimize regex
294  smilify.setPattern(allPattern);
295  smilify.optimize();
296 }
297 
303 QString SmileyPack::smileyfied(const QString& msg)
304 {
305  QMutexLocker locker(&loadingMutex);
306  QString result(msg);
307 
308  int replaceDiff = 0;
309  QRegularExpressionMatchIterator iter = smilify.globalMatch(result);
310  while (iter.hasNext()) {
311  QRegularExpressionMatch match = iter.next();
312  int startPos = match.capturedStart();
313  int keyLength = match.capturedLength();
314  QString imgRichText = getAsRichText(match.captured());
315  result.replace(startPos + replaceDiff, keyLength, imgRichText);
316  replaceDiff += imgRichText.length() - keyLength;
317  }
318 
319  return result;
320 }
321 
326 {
327  QMutexLocker locker(&loadingMutex);
328  return emoticons;
329 }
330 
336 std::shared_ptr<QIcon> SmileyPack::getAsIcon(const QString& emoticon) const
337 {
338  QMutexLocker locker(&loadingMutex);
339  if (cachedIcon.find(emoticon) != cachedIcon.end()) {
340  return cachedIcon[emoticon];
341  }
342 
343  const auto iconPathIt = emoticonToPath.find(emoticon);
344  if (iconPathIt == emoticonToPath.end()) {
345  return std::make_shared<QIcon>();
346  }
347 
348  const QString& iconPath = iconPathIt.value();
349  auto icon = std::make_shared<QIcon>(iconPath);
350  cachedIcon[emoticon] = icon;
351  return icon;
352 }
353 
355 {
356  loadingMutex.lock();
357  QtConcurrent::run(this, &SmileyPack::load, Settings::getInstance().getSmileyPack());
358 }
SmileyPack::getAsIcon
std::shared_ptr< QIcon > getAsIcon(const QString &key) const
Gets icon accoring to passed emoticon.
Definition: smileypack.cpp:336
SmileyPack::cachedIcon
std::map< QString, std::shared_ptr< QIcon > > cachedIcon
Definition: smileypack.h:57
SmileyPack::loadingMutex
QMutex loadingMutex
Definition: smileypack.h:63
settings.h
SmileyPack::~SmileyPack
~SmileyPack() override
Definition: smileypack.cpp:126
QList
Definition: friendlist.h:25
SmileyPack::cleanupTimer
QTimer * cleanupTimer
Definition: smileypack.h:61
SmileyPack::getEmoticons
QList< QStringList > getEmoticons() const
Returns all emoticons that was extracted from files, grouped by according icon file.
Definition: smileypack.cpp:325
SmileyPack::constructRegex
void constructRegex()
Creates the regex for replacing emoticons with the path to their pictures.
Definition: smileypack.cpp:264
SmileyPack::load
bool load(const QString &filename)
Load smile pack.
Definition: smileypack.cpp:203
SmileyPack::SmileyPack
SmileyPack()
Definition: smileypack.cpp:115
Settings::smileyPackChanged
void smileyPackChanged(const QString &name)
SmileyPack::path
QString path
directory containing the cfg and image files
Definition: smileypack.h:60
SmileyPack::smilify
QRegularExpression smilify
Definition: smileypack.h:62
smileypack.h
SmileyPack::getInstance
static SmileyPack & getInstance()
Returns the singleton instance.
Definition: smileypack.cpp:147
SmileyPack::onSmileyPackChanged
void onSmileyPackChanged()
Definition: smileypack.cpp:354
SmileyPack::smileyfied
QString smileyfied(const QString &msg)
Replaces all found text emoticons to HTML reference with its according icon filename.
Definition: smileypack.cpp:303
SmileyPack::cleanupIconsCache
void cleanupIconsCache()
Definition: smileypack.cpp:131
SmileyPack::emoticonToPath
QHash< QString, QString > emoticonToPath
Definition: smileypack.h:58
getAsRichText
QString getAsRichText(const QString &key)
Wraps passed string into smiley HTML image reference.
Definition: smileypack.cpp:103
isAscii
bool isAscii(const QString &string)
Definition: smileypack.cpp:108
Settings::getInstance
static Settings & getInstance()
Returns the singleton instance.
Definition: settings.cpp:88
SmileyPack::listSmileyPacks
static QList< QPair< QString, QString > > listSmileyPacks()
Does the same as listSmileyPaths, but with default paths.
Definition: smileypack.cpp:156
SmileyPack::emoticons
QList< QStringList > emoticons
{{ ":)", ":-)" }, {":(", ...}, ... }
Definition: smileypack.h:59
loadDefaultPaths
QStringList loadDefaultPaths()
Construct list of standard directories with "emoticons" sub dir, whether these directories exist or n...
Definition: smileypack.cpp:70
SmileyPack
Maps emoticons to smileys.
Definition: smileypack.h:31