root/trunk/qt4-gui/src/config/emoticons.cpp

Revision 6427, 13.9 kB (checked in by eugene, 5 months ago)

Revised emoticon (un)parsing. It's not a hyperlink now. It removes annoying tab-switching and possibility to click on it.

  • Property svn:eol-style set to native
Line 
1/*
2 * This file is part of Licq, an instant messaging client for UNIX.
3 * Copyright (C) 2003-2006 Licq developers
4 *
5 * Licq 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 2 of the License, or
8 * (at your option) any later version.
9 *
10 * Licq 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 Licq; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18 */
19
20#include "emoticons.h"
21
22#include "config.h"
23
24#ifdef USE_KDE
25# include <kapplication.h>
26#else
27# include <QApplication>
28#endif
29
30//#define EMOTICON_DEBUG
31
32#ifdef EMOTICON_DEBUG
33# define TRACE(x...) qDebug(x)
34#else
35# define TRACE(x...) ((void)0)
36#endif
37
38#include <QDir>
39#include <QDomDocument>
40#include <QLinkedList>
41#include <QRegExp>
42#include <QTextDocument>
43
44#include <licq_log.h>
45
46
47using namespace LicqQtGui;
48
49const QString Emoticons::DEFAULT_THEME =
50  QString::fromLatin1(QT_TRANSLATE_NOOP("LicqQtGui::Emoticons", "Default"));
51const QString Emoticons::NO_THEME =
52  QString::fromLatin1(QT_TRANSLATE_NOOP("LicqQtGui::Emoticons", "None"));
53
54struct Emoticon
55{
56  QString file;
57  QString smiley;
58  QString escapedSmiley;
59};
60
61/// Private data and functions for Emotions
62class Emoticons::Impl
63{
64public:
65  QStringList basedirs;
66  QString currentTheme;
67
68  // Maps first char in smiley to its Emoticon instance.
69  QMap<QChar, QLinkedList<Emoticon> > emoticons;
70
71  // Maps an emoticon's filename to a smiley.
72  QMap<QString, QString> fileSmiley;
73
74  QString themeDir(const QString &theme) const;
75};
76
77/**
78 * @param theme the untranslated name of a theme
79 * @returns the full path to @a theme, or QString::null if no such theme was found.
80 */
81QString Emoticons::Impl::themeDir(const QString &theme) const
82{
83  QStringList::ConstIterator basedir = basedirs.begin();
84  for (; basedir != basedirs.end(); basedir++)
85  {
86    const QString dir = QString("%1/%2").arg(*basedir).arg(theme);
87    if (QFile::exists(QString("%1/emoticons.xml").arg(dir)))
88      return dir;
89  }
90
91  return QString::null;
92}
93
94
95// By making the application object parent, this instance will be
96// deleted when the application is closed.
97Emoticons::Emoticons()
98#ifdef USE_KDE
99  : QObject(kapp)
100#else
101  : QObject(qApp)
102#endif
103{
104  pimpl = new Impl;
105  pimpl->currentTheme = NO_THEME;
106}
107
108Emoticons::~Emoticons()
109{
110  delete pimpl;
111}
112
113Emoticons* Emoticons::m_self = 0L;
114Emoticons* Emoticons::self()
115{
116  if (!m_self)
117    m_self = new Emoticons;
118  return m_self;
119}
120
121QString Emoticons::translateThemeName(const QString &name)
122{
123  if (name == DEFAULT_THEME || name == NO_THEME)
124    return tr(name.toLatin1());
125  return name;
126}
127
128QString Emoticons::untranslateThemeName(const QString &name)
129{
130  if (name == tr(DEFAULT_THEME.toLatin1()))
131    return DEFAULT_THEME;
132  else if (name == tr(NO_THEME.toLatin1()))
133    return NO_THEME;
134  else
135    return name;
136}
137
138void Emoticons::setBasedirs(const QStringList &basedirs)
139{
140  pimpl->basedirs.clear();
141  QStringList::ConstIterator basedir = basedirs.begin();
142  for (; basedir != basedirs.end(); basedir++)
143    pimpl->basedirs += QDir(*basedir).absolutePath();
144}
145
146/**
147 * In every subdir in every basedir, we check for a file
148 * named emoticons.xml, and if we find one, subdir is added
149 * to the list of themes.
150 */
151QStringList Emoticons::themes() const
152{
153  QStringList themes;
154  bool defaultExists = false;
155
156  QStringList::ConstIterator basedir = pimpl->basedirs.begin();
157  for (; basedir != pimpl->basedirs.end(); basedir++)
158  {
159    QDir dir(*basedir, QString::null, QDir::Unsorted, QDir::Dirs);
160    const QStringList subdirs = dir.entryList();
161
162    QStringList::ConstIterator subdir = subdirs.begin();
163    for (; subdir != subdirs.end(); subdir++)
164    {
165      if (*subdir == "." || *subdir == "..")
166        continue;
167
168      if (*subdir == NO_THEME)
169        continue; // Add this later
170
171      if (QFile::exists(QString("%1/%2/emoticons.xml").arg(*basedir).arg(*subdir)))
172      {
173        if (*subdir == DEFAULT_THEME)
174        {
175          defaultExists = true;
176          continue; // Add this later
177        }
178
179        // Only add unique entires
180        if (themes.indexOf(*subdir) == -1)
181          themes += *subdir;
182      }
183    }
184  }
185
186  themes.sort();
187
188  // Adding these at the front so that they will be first in the list shown to the user.
189  if (defaultExists)
190    themes.push_front(translateThemeName(DEFAULT_THEME));
191  themes.push_front(translateThemeName(NO_THEME));
192
193  return themes;
194}
195
196/**
197 * @param dir directory to search in
198 * @param file filename (without extension) to search for
199 * @returns the full filename or QString::null if no such file exists.
200 */
201static QString fullFilename(const QString& dir, const QString& file)
202{
203  const QString base = QString("%1/%2").arg(dir).arg(file);
204
205  if (QFile::exists(base)) // First try without extension
206    return base;
207  else if (QFile::exists(base + ".png"))
208    return base + ".png";
209  else if (QFile::exists(base + ".jpg"))
210    return base + ".jpg";
211  else if (QFile::exists(base + ".gif"))
212    return base + ".gif";
213  else if (QFile::exists(base + ".mng"))
214    return base + ".mng";
215
216  gLog.Warn("%sUnknown file '%s'.\n", L_WARNxSTR, base.toLatin1().data());
217  return QString::null;
218}
219
220/**
221 * Parses the emoticons.xml file in @a dir.
222 * @param emoticons  For every smiley, the first character is added as a key
223 *                   and its Emoticon instance is appened to the list.
224 * @param fileSmiley Maps the filename of an emoticon to a smiley.
225 * @returns true on success; otherwise false.
226 *
227 * A short emoticons.xml file could look like this:
228 * <?xml version="1.0"?>
229 * <messaging-emoticon-map >
230 *
231 * <emoticon file="biggrin">
232 * <string>:-&lt;</string>
233 * <string>:D</string>
234 * </emoticon>
235 *
236 * <emoticon file="confused">
237 * <string>:-S</string>
238 * </emoticon>
239 *
240 * </messaging-emoticon-map>
241 */
242static bool parseXml(const QString& dir, QMap<QChar, QLinkedList<Emoticon> >* emoticons, QMap<QString, QString>* fileSmiley)
243{
244  QFile xmlfile(dir + QString::fromLatin1("/emoticons.xml"));
245  if (!xmlfile.open(QIODevice::ReadOnly))
246    return false;
247
248  QDomDocument doc("emoticons");
249  if (!doc.setContent(&xmlfile))
250  {
251    xmlfile.close();
252    return false;
253  }
254  xmlfile.close();
255
256  QDomElement docElem = doc.documentElement();
257
258  // Walk through all <emoticon> elements
259  QDomNode n = docElem.firstChild();
260  for (; !n.isNull(); n = n.nextSibling())
261  {
262    QDomElement e = n.toElement();
263    if (!e.isNull() && e.tagName() == QString::fromLatin1("emoticon"))
264    {
265      const QString file = fullFilename(dir, e.attribute("file"));
266      if (file.isNull())
267        continue;
268
269      bool first = true;
270      QDomNode stringNode = n.firstChild();
271      for (; !stringNode.isNull(); stringNode = stringNode.nextSibling())
272      {
273        // We extract all smileys from <string> elements (<string>smiley</string>).
274        // The first one is added to fileSmiley, so that when the user clicks
275        // on the icon, this is the smiley that is inserted into the document.
276        //
277        // All smileys are then indexed in the emoticons map on the first character
278        // in the escaped smiley.
279        QDomElement string = stringNode.toElement();
280        if (!string.isNull() && string.tagName() == QString::fromLatin1("string"))
281        {
282          Emoticon emo;
283          emo.smiley = string.text();
284          emo.escapedSmiley = Qt::escape(emo.smiley);
285          emo.file = file;
286
287          if (first)
288          {
289            (*fileSmiley)[emo.file] = emo.smiley;
290            first = false;
291          }
292
293          // Insert the smiley sorted by length with longest first. This way, if we have
294          // a smiley :) with image A and :)) with image B, the string :)) will always
295          // be replaced by image B.
296          QLinkedList<Emoticon>::iterator it = (*emoticons)[emo.escapedSmiley[0]].begin();
297          QLinkedList<Emoticon>::iterator end = (*emoticons)[emo.escapedSmiley[0]].end();
298          while (it != end)
299          {
300#ifdef EMOTICON_DEBUG
301            if ((*it).escapedSmiley == emo.escapedSmiley)
302              TRACE("The smiley '%s' (%s) is already mapped to %s",
303                  emo.smiley.toLatin1().data(),
304                  QFileInfo(file).fileName().toLatin1().data(),
305                  QFileInfo((*it).file).fileName().toLatin1().data());
306#endif
307            if ((*it).escapedSmiley.length() < emo.escapedSmiley.length())
308              break;
309            else
310              it++;
311          }
312          (*emoticons)[emo.escapedSmiley[0]].insert(it, emo);
313        }
314        else
315        {
316          gLog.Warn("%sElement '%s' in '%s' unknown.\n", L_WARNxSTR,
317                    string.tagName().toLatin1().data(), xmlfile.fileName().toLatin1().data());
318        }
319      }
320    }
321  }
322
323  return true;
324}
325
326QStringList Emoticons::fileList() const
327{
328  return pimpl->fileSmiley.keys();
329}
330
331// Similar to setTheme(const QString_&) but with the difference that
332// here we don't update currentTheme. We're just interested in getting
333// the filelist.
334QStringList Emoticons::fileList(const QString& theme_in) const
335{
336  const QString theme = untranslateThemeName(theme_in);
337
338  if (theme.isEmpty() || theme == NO_THEME)
339    return QStringList();
340
341  if (theme == pimpl->currentTheme)
342    return fileList();
343
344  const QString dir = pimpl->themeDir(theme);
345  if (dir.isNull())
346    return QStringList();
347
348  QMap<QChar, QLinkedList<Emoticon> > emoticons;
349  QMap<QString, QString> fileSmiley;
350
351  const bool parsed = parseXml(dir, &emoticons, &fileSmiley);
352  if (parsed)
353    return fileSmiley.keys();
354
355  return QStringList();
356}
357
358bool Emoticons::setTheme(const QString& theme_in)
359{
360  const QString theme = untranslateThemeName(theme_in);
361
362  if (theme.isEmpty() || theme == NO_THEME)
363  {
364    pimpl->currentTheme = NO_THEME;
365    pimpl->emoticons.clear();
366    pimpl->fileSmiley.clear();
367    return true;
368  }
369
370  if (theme == pimpl->currentTheme)
371    return true;
372
373  const QString dir = pimpl->themeDir(theme);
374  if (dir.isNull())
375    return false;
376
377  QMap<QChar, QLinkedList<Emoticon> > emoticons;
378  QMap<QString, QString> fileSmiley;
379
380  const bool parsed = parseXml(dir, &emoticons, &fileSmiley);
381  if (parsed)
382  {
383    pimpl->currentTheme = theme;
384    pimpl->emoticons = emoticons;
385    pimpl->fileSmiley = fileSmiley;
386    emit themeChanged();
387  }
388
389  return parsed;
390}
391
392QString Emoticons::theme() const
393{
394  return translateThemeName(pimpl->currentTheme);
395}
396
397QMap<QString, QString> Emoticons::emoticonsKeys() const
398{
399  return pimpl->fileSmiley;
400}
401
402/**
403 * @returns true if s1[start:start+s2.length] == s2
404 */
405static bool containsAt(const QString& s1, const QString& s2, const uint start)
406{
407  const uint end = start + s2.length();
408  const uint s1_length = static_cast<uint>(s1.length());
409  if (s1_length < end || start > s1_length)
410    return false;
411
412  for (uint pos = start; pos < end; pos++)
413  {
414    if (s1[pos] != s2[pos - start])
415      return false;
416  }
417  return true;
418}
419
420/**
421 * @param message is assumed to be in html, so that all \< is part of a tag
422 * @param mode the parsing mode
423 */
424void Emoticons::parseMessage(QString& message, ParseMode mode) const
425{
426  // Short-circuit if we don't have any emoticons
427  if (pimpl->emoticons.isEmpty())
428    return;
429
430  TRACE("message pre: '%s'", message.toLatin1().data());
431
432  QChar p(' '), c; // previous and current char
433  for (int pos = 0; pos < message.length(); pos++)
434  {
435    c = message[pos];
436
437    if (c == '<')
438    {
439      // If this is an a tag ("<a "), skip it completly
440      if (message[pos + 1] == 'a' && message[pos + 2].isSpace())
441      {
442        const int index = message.indexOf("</a>", pos);
443        if (index == -1)
444          return; // Bad html
445        pos = index + 3; // Fast-forward pos to point at '>'
446      }
447      else // Skip just the tag
448      {
449        const int index = message.indexOf('>', pos);
450        if (index == -1)
451          return; // Bad html
452        pos = index; // Fast-forward pos to point at '>'
453      }
454      p = '>';
455      continue;
456    }
457
458    // Only insert smileys after a space in strict and normal mode
459    if (mode == StrictMode || mode == NormalMode)
460    {
461      if (!p.isSpace() && !containsAt(message, QString::fromLatin1("<br />"), pos - 6))
462      {
463        p = c;
464        continue;
465      }
466    }
467
468    if (pimpl->emoticons.contains(c))
469    {
470      const QLinkedList<Emoticon> emolist = pimpl->emoticons[c];
471      QLinkedList<Emoticon>::ConstIterator it = emolist.begin();
472
473      for (; it != emolist.end(); it++)
474      {
475        const Emoticon& emo = *it;
476        if (containsAt(message, emo.escapedSmiley, pos))
477        {
478          // In strict and normal mode we need to check the char after the smiley
479          if (mode == StrictMode || mode == NormalMode)
480          {
481            const uint nextPos = pos + emo.escapedSmiley.length();
482            const QChar& n = message[nextPos];
483            if (!(n.isSpace() || n.isNull() || containsAt(message, QString::fromLatin1("<br"), nextPos)))
484            {
485              if (mode == StrictMode)
486                break;
487              else if (!n.isPunct()) // In normal mode we allow punct as well
488                break;
489            }
490          }
491
492          QString img = QString::fromLocal8Bit("<img src=\"file://%1#LICQ%2\">")
493            .arg(emo.file)
494            .arg(emo.escapedSmiley);
495          TRACE("Replacing '%s' with '%s'",
496              message.mid(pos, emo.escapedSmiley.length()).toLatin1().data(),
497              img.toLatin1().data());
498          message.replace(pos, emo.escapedSmiley.length(), img);
499          pos += img.length() - 1; // Point pos at '>'
500          c = '>';
501          break;
502        }
503      }
504    }
505
506    p = c;
507  }
508  TRACE("message post: '%s'", message.toLatin1().data());
509}
510
511/**
512 * "unparse" the message, removing all \<img\> tags and replacing them with the smiley.
513 */
514void Emoticons::unparseMessage(QString& message)
515{
516  QRegExp deicon("<img src=\"file://.*#LICQ(.*)\".*>");
517  deicon.setMinimal(true);
518  message.replace(deicon, "\\1");
519}
Note: See TracBrowser for help on using the browser.