You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
560 lines
15 KiB
560 lines
15 KiB
/*
|
|
kopeteemoticons.cpp - Kopete Preferences Container-Class
|
|
|
|
Copyright (c) 2002 by Stefan Gehn <metz AT gehn.net>
|
|
Copyright (c) 2002-2006 by Olivier Goffart <ogoffart @ kde.org>
|
|
Copyright (c) 2005 by Engin AYDOGAN <engin@bzzzt.biz>
|
|
|
|
Kopete (c) 2002-2005 by the Kopete developers <kopete-devel@kde.org>
|
|
|
|
*************************************************************************
|
|
* *
|
|
* This library is free software; you can redistribute it and/or *
|
|
* modify it under the terms of the GNU Lesser General Public *
|
|
* License as published by the Free Software Foundation; either *
|
|
* version 2 of the License, or (at your option) any later version. *
|
|
* *
|
|
*************************************************************************
|
|
*/
|
|
|
|
#include "kopeteemoticons.h"
|
|
|
|
#include "kopeteprefs.h"
|
|
|
|
#include <qdom.h>
|
|
#include <qfile.h>
|
|
#include <qstylesheet.h>
|
|
#include <qimage.h>
|
|
#include <qdatetime.h>
|
|
|
|
#include <kapplication.h>
|
|
#include <kdebug.h>
|
|
#include <kstandarddirs.h>
|
|
#include <kdeversion.h>
|
|
|
|
#include <set>
|
|
#include <algorithm>
|
|
#include <iterator>
|
|
|
|
|
|
/*
|
|
* Testcases can be found in the kopeteemoticontest app in the tests/ directory.
|
|
*/
|
|
|
|
|
|
namespace Kopete {
|
|
|
|
|
|
struct Emoticons::Emoticon
|
|
{
|
|
Emoticon(){}
|
|
/* sort by longest to shortest matchText */
|
|
bool operator< (const Emoticon &e){ return matchText.length() > e.matchText.length(); }
|
|
QString matchText;
|
|
QString matchTextEscaped;
|
|
QString picPath;
|
|
QString picHTMLCode;
|
|
};
|
|
|
|
/* This is the object we will store each emoticon match in */
|
|
struct Emoticons::EmoticonNode {
|
|
const Emoticon emoticon;
|
|
int pos;
|
|
EmoticonNode() : emoticon(), pos( -1 ) {}
|
|
EmoticonNode( const Emoticon e, int p ) : emoticon( e ), pos( p ) {}
|
|
};
|
|
|
|
class Emoticons::Private
|
|
{
|
|
public:
|
|
QMap<QChar, QValueList<Emoticon> > emoticonMap;
|
|
QMap<QString, QStringList> emoticonAndPicList;
|
|
|
|
/**
|
|
* The current icon theme from KopetePrefs
|
|
*/
|
|
QString theme;
|
|
|
|
|
|
};
|
|
|
|
|
|
Emoticons *Emoticons::s_self = 0L;
|
|
|
|
Emoticons *Emoticons::self()
|
|
{
|
|
if( !s_self )
|
|
s_self = new Emoticons;
|
|
return s_self;
|
|
}
|
|
|
|
|
|
QString Emoticons::parseEmoticons(const QString& message, ParseMode mode ) //static
|
|
{
|
|
return self()->parse( message, mode );
|
|
}
|
|
|
|
QValueList<Emoticons::Token> Emoticons::tokenizeEmoticons( const QString& message, ParseMode mode ) // static
|
|
{
|
|
return self()->tokenize( message, mode );
|
|
}
|
|
|
|
QValueList<Emoticons::Token> Emoticons::tokenize( const QString& message, uint mode )
|
|
{
|
|
QValueList<Token> result;
|
|
if ( !KopetePrefs::prefs()->useEmoticons() )
|
|
{
|
|
result.append( Token( Text, message ) );
|
|
return result;
|
|
}
|
|
|
|
if( ! ( mode & (StrictParse|RelaxedParse) ) )
|
|
{
|
|
//if none of theses two mode are selected, use the mode from the config
|
|
mode |= KopetePrefs::prefs()->emoticonsRequireSpaces() ? StrictParse : RelaxedParse ;
|
|
}
|
|
|
|
/* previous char, in the firs iteration assume that it is space since we want
|
|
* to let emoticons at the beginning, the very first previous QChar must be a space. */
|
|
QChar p = ' ';
|
|
QChar c; /* current char */
|
|
QChar n; /* next character after a match candidate, if strict this should be QChar::null or space */
|
|
|
|
/* This is the EmoticonNode container, it will represent each matched emoticon */
|
|
QValueList<EmoticonNode> foundEmoticons;
|
|
QValueList<EmoticonNode>::const_iterator found;
|
|
/* First-pass, store the matched emoticon locations in foundEmoticons */
|
|
QValueList<Emoticon> emoticonList;
|
|
QValueList<Emoticon>::const_iterator it;
|
|
size_t pos;
|
|
|
|
bool inHTMLTag = false;
|
|
bool inHTMLLink = false;
|
|
bool inHTMLEntity = false;
|
|
QString needle; // search for this
|
|
for ( pos = 0; pos < message.length(); pos++ )
|
|
{
|
|
c = message[ pos ];
|
|
|
|
if ( mode & SkipHTML ) // Shall we skip HTML ?
|
|
{
|
|
if ( !inHTMLTag ) // Are we already in an HTML tag ?
|
|
{
|
|
if ( c == '<' ) { // If not check if are going into one
|
|
inHTMLTag = true; // If we are, change the state to inHTML
|
|
p = c;
|
|
continue;
|
|
}
|
|
}
|
|
else // We are already in a HTML tag
|
|
{
|
|
if ( c == '>' ) { // Check if it ends
|
|
inHTMLTag = false; // If so, change the state
|
|
if ( p == 'a' )
|
|
{
|
|
inHTMLLink = false;
|
|
}
|
|
}
|
|
else if ( c == 'a' && p == '<' ) // check if we just entered an achor tag
|
|
{
|
|
inHTMLLink = true; // don't put smileys in urls
|
|
}
|
|
p = c;
|
|
continue;
|
|
}
|
|
|
|
if( !inHTMLEntity )
|
|
{ // are we
|
|
if( c == '&' )
|
|
{
|
|
inHTMLEntity = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( inHTMLLink ) // i can't think of any situation where a link adress might need emoticons
|
|
{
|
|
p = c;
|
|
continue;
|
|
}
|
|
|
|
if ( (mode & StrictParse) && !p.isSpace() && p != '>')
|
|
{ // '>' may mark the end of an html tag
|
|
p = c;
|
|
continue;
|
|
} /* strict requires space before the emoticon */
|
|
if ( d->emoticonMap.contains( c ) )
|
|
{
|
|
emoticonList = d->emoticonMap[ c ];
|
|
bool found = false;
|
|
for ( it = emoticonList.begin(); it != emoticonList.end(); ++it )
|
|
{
|
|
// If this is an HTML, then search for the HTML form of the emoticon.
|
|
// For instance <o) => >o)
|
|
needle = ( mode & SkipHTML ) ? (*it).matchTextEscaped : (*it).matchText;
|
|
if ( ( pos == (size_t)message.find( needle, pos ) ) )
|
|
{
|
|
if( mode & StrictParse )
|
|
{
|
|
/* check if the character after this match is space or end of string*/
|
|
n = message[ pos + needle.length() ];
|
|
//<br/> marks the end of a line
|
|
if( n != '<' && !n.isSpace() && !n.isNull() && n!= '&')
|
|
break;
|
|
}
|
|
/* Perfect match */
|
|
foundEmoticons.append( EmoticonNode( (*it), pos ) );
|
|
found = true;
|
|
/* Skip the matched emoticon's matchText */
|
|
pos += needle.length() - 1;
|
|
break;
|
|
}
|
|
}
|
|
if( !found )
|
|
{
|
|
if( inHTMLEntity ){
|
|
// If we are in an HTML entitiy such as >
|
|
int htmlEnd = message.find( ';', pos );
|
|
// Search for where it ends
|
|
if( htmlEnd == -1 )
|
|
{
|
|
// Apparently this HTML entity isn't ended, something is wrong, try skip the '&'
|
|
// and continue
|
|
kdDebug( 14000 ) << k_funcinfo << "Broken HTML entity, trying to recover." << endl;
|
|
inHTMLEntity = false;
|
|
pos++;
|
|
}
|
|
else
|
|
{
|
|
pos = htmlEnd;
|
|
inHTMLEntity = false;
|
|
}
|
|
}
|
|
}
|
|
} /* else no emoticons begin with this character, so don't do anything */
|
|
p = c;
|
|
}
|
|
|
|
/* if no emoticons found just return the text */
|
|
if ( foundEmoticons.isEmpty() )
|
|
{
|
|
result.append( Token( Text, message ) );
|
|
return result;
|
|
}
|
|
|
|
/* Second-pass, generate tokens based on the matches */
|
|
|
|
pos = 0;
|
|
int length;
|
|
|
|
for ( found = foundEmoticons.begin(); found != foundEmoticons.end(); ++found )
|
|
{
|
|
needle = ( mode & SkipHTML ) ? (*found).emoticon.matchTextEscaped : (*found).emoticon.matchText;
|
|
if ( ( length = ( (*found).pos - pos ) ) )
|
|
{
|
|
result.append( Token( Text, message.mid( pos, length ) ) );
|
|
result.append( Token( Image, (*found).emoticon.matchTextEscaped, (*found).emoticon.picPath, (*found).emoticon.picHTMLCode ) );
|
|
pos += length + needle.length();
|
|
}
|
|
else
|
|
{
|
|
result.append( Token( Image, (*found).emoticon.matchTextEscaped, (*found).emoticon.picPath, (*found).emoticon.picHTMLCode ) );
|
|
pos += needle.length();
|
|
}
|
|
}
|
|
|
|
if ( message.length() - pos ) // if there is remaining regular text
|
|
{
|
|
result.append( Token( Text, message.mid( pos ) ) );
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Emoticons::Emoticons( const QString &theme ) : QObject( kapp, "KopeteEmoticons" )
|
|
{
|
|
// kdDebug(14010) << "KopeteEmoticons::KopeteEmoticons" << endl;
|
|
d=new Private;
|
|
if(theme.isNull())
|
|
{
|
|
initEmoticons();
|
|
connect( KopetePrefs::prefs(), SIGNAL(saved()), this, SLOT(initEmoticons()) );
|
|
}
|
|
else
|
|
{
|
|
initEmoticons( theme );
|
|
}
|
|
}
|
|
|
|
|
|
Emoticons::~Emoticons( )
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
|
|
|
|
void Emoticons::addIfPossible( const QString& filenameNoExt, const QStringList &emoticons )
|
|
{
|
|
KStandardDirs *dir = KGlobal::dirs();
|
|
QString pic;
|
|
|
|
//maybe an extension was given, so try to find the exact file
|
|
pic = dir->findResource( "emoticons", d->theme + QString::fromLatin1( "/" ) + filenameNoExt );
|
|
|
|
if( pic.isNull() )
|
|
pic = dir->findResource( "emoticons", d->theme + QString::fromLatin1( "/" ) + filenameNoExt + QString::fromLatin1( ".mng" ) );
|
|
if ( pic.isNull() )
|
|
pic = dir->findResource( "emoticons", d->theme + QString::fromLatin1( "/" ) + filenameNoExt + QString::fromLatin1( ".png" ) );
|
|
if ( pic.isNull() )
|
|
pic = dir->findResource( "emoticons", d->theme + QString::fromLatin1( "/" ) + filenameNoExt + QString::fromLatin1( ".gif" ) );
|
|
|
|
if( !pic.isNull() ) // only add if we found one file
|
|
{
|
|
QPixmap p;
|
|
QString result;
|
|
|
|
d->emoticonAndPicList.insert( pic, emoticons );
|
|
|
|
for ( QStringList::const_iterator it = emoticons.constBegin(), end = emoticons.constEnd();
|
|
it != end; ++it )
|
|
{
|
|
QString matchEscaped=QStyleSheet::escape(*it);
|
|
|
|
Emoticon e;
|
|
e.picPath = pic;
|
|
|
|
// We need to include size (width, height attributes) hints in the emoticon HTML code
|
|
// Unless we do so, ChatMessagePart::slotScrollView does not work properly and causing
|
|
// HTMLPart not to be scrolled to the very last message.
|
|
p.load( e.picPath );
|
|
result = QString::fromLatin1( "<img align=\"center\" src=\"" ) +
|
|
e.picPath +
|
|
QString::fromLatin1( "\" title=\"" ) +
|
|
matchEscaped +
|
|
QString::fromLatin1( "\" width=\"" ) +
|
|
QString::number( p.width() ) +
|
|
QString::fromLatin1( "\" height=\"" ) +
|
|
QString::number( p.height() ) +
|
|
QString::fromLatin1( "\" />" );
|
|
|
|
e.picHTMLCode = result;
|
|
e.matchTextEscaped = matchEscaped;
|
|
e.matchText = *it;
|
|
d->emoticonMap[ matchEscaped[0] ].append( e );
|
|
d->emoticonMap[ (*it)[0] ].append( e );
|
|
}
|
|
}
|
|
}
|
|
|
|
void Emoticons::initEmoticons( const QString &theme )
|
|
{
|
|
if(theme.isNull())
|
|
{
|
|
if ( d->theme == KopetePrefs::prefs()->iconTheme() )
|
|
return;
|
|
|
|
d->theme = KopetePrefs::prefs()->iconTheme();
|
|
}
|
|
else
|
|
{
|
|
d->theme = theme;
|
|
}
|
|
|
|
// kdDebug(14010) << k_funcinfo << "Called" << endl;
|
|
d->emoticonAndPicList.clear();
|
|
d->emoticonMap.clear();
|
|
|
|
QString filename= KGlobal::dirs()->findResource( "emoticons", d->theme + QString::fromLatin1( "/emoticons.xml" ) );
|
|
if(!filename.isEmpty())
|
|
return initEmoticon_emoticonsxml( filename );
|
|
filename= KGlobal::dirs()->findResource( "emoticons", d->theme + QString::fromLatin1( "/icondef.xml" ) );
|
|
if(!filename.isEmpty())
|
|
return initEmoticon_JEP0038( filename );
|
|
kdWarning(14010) << k_funcinfo << "emotiucon XML theme description not found" <<endl;
|
|
}
|
|
|
|
void Emoticons::initEmoticon_emoticonsxml( const QString & filename)
|
|
{
|
|
QDomDocument emoticonMap( QString::fromLatin1( "messaging-emoticon-map" ) );
|
|
|
|
QFile mapFile( filename );
|
|
mapFile.open( IO_ReadOnly );
|
|
emoticonMap.setContent( &mapFile );
|
|
|
|
QDomElement list = emoticonMap.documentElement();
|
|
QDomNode node = list.firstChild();
|
|
while( !node.isNull() )
|
|
{
|
|
QDomElement element = node.toElement();
|
|
if( !element.isNull() )
|
|
{
|
|
if( element.tagName() == QString::fromLatin1( "emoticon" ) )
|
|
{
|
|
QString emoticon_file = element.attribute(
|
|
QString::fromLatin1( "file" ), QString::null );
|
|
QStringList items;
|
|
|
|
QDomNode emoticonNode = node.firstChild();
|
|
while( !emoticonNode.isNull() )
|
|
{
|
|
QDomElement emoticonElement = emoticonNode.toElement();
|
|
if( !emoticonElement.isNull() )
|
|
{
|
|
if( emoticonElement.tagName() == QString::fromLatin1( "string" ) )
|
|
{
|
|
items << emoticonElement.text();
|
|
}
|
|
else
|
|
{
|
|
kdDebug(14010) << k_funcinfo <<
|
|
"Warning: Unknown element '" << element.tagName() <<
|
|
"' in emoticon data" << endl;
|
|
}
|
|
}
|
|
emoticonNode = emoticonNode.nextSibling();
|
|
}
|
|
|
|
addIfPossible ( emoticon_file, items );
|
|
}
|
|
else
|
|
{
|
|
kdDebug(14010) << k_funcinfo << "Warning: Unknown element '" <<
|
|
element.tagName() << "' in map file" << endl;
|
|
}
|
|
}
|
|
node = node.nextSibling();
|
|
}
|
|
mapFile.close();
|
|
sortEmoticons();
|
|
}
|
|
|
|
|
|
void Emoticons::initEmoticon_JEP0038( const QString & filename)
|
|
{
|
|
QDomDocument emoticonMap( QString::fromLatin1( "icondef" ) );
|
|
|
|
QFile mapFile( filename );
|
|
mapFile.open( IO_ReadOnly );
|
|
emoticonMap.setContent( &mapFile );
|
|
|
|
QDomElement list = emoticonMap.documentElement();
|
|
QDomNode node = list.firstChild();
|
|
while( !node.isNull() )
|
|
{
|
|
QDomElement element = node.toElement();
|
|
if( !element.isNull() )
|
|
{
|
|
if( element.tagName() == QString::fromLatin1( "icon" ) )
|
|
{
|
|
QStringList items;
|
|
QString emoticon_file;
|
|
|
|
QDomNode emoticonNode = node.firstChild();
|
|
while( !emoticonNode.isNull() )
|
|
{
|
|
QDomElement emoticonElement = emoticonNode.toElement();
|
|
if( !emoticonElement.isNull() )
|
|
{
|
|
if( emoticonElement.tagName() == QString::fromLatin1( "text" ) )
|
|
{
|
|
//TODO xml:lang
|
|
items << emoticonElement.text();
|
|
}
|
|
else if( emoticonElement.tagName() == QString::fromLatin1( "object" ) && emoticon_file.isEmpty() )
|
|
{
|
|
QString mime= emoticonElement.attribute(
|
|
QString::fromLatin1( "mime" ), QString::fromLatin1("image/*") );
|
|
if(mime.startsWith(QString::fromLatin1("image/")) && !mime.endsWith(QString::fromLatin1("/svg+xml")))
|
|
{
|
|
emoticon_file = emoticonElement.text();
|
|
}
|
|
else
|
|
{
|
|
kdDebug(14010) << k_funcinfo << "Warning: Unsupported format '" << mime << endl;
|
|
}
|
|
}
|
|
/*else
|
|
{
|
|
kdDebug(14010) << k_funcinfo <<
|
|
"Warning: Unknown element '" << element.tagName() <<
|
|
"' in emoticon data" << endl;
|
|
}*/
|
|
}
|
|
emoticonNode = emoticonNode.nextSibling();
|
|
}
|
|
if( !items.isEmpty() && !emoticon_file.isEmpty() )
|
|
addIfPossible ( emoticon_file, items );
|
|
}
|
|
else
|
|
{
|
|
kdDebug(14010) << k_funcinfo << "Warning: Unknown element '" <<
|
|
element.tagName() << "' in map file" << endl;
|
|
}
|
|
}
|
|
node = node.nextSibling();
|
|
}
|
|
mapFile.close();
|
|
sortEmoticons();
|
|
}
|
|
|
|
|
|
void Emoticons::sortEmoticons()
|
|
{
|
|
/* sort strings in order of longest to shortest to provide convenient input for
|
|
greedy matching in the tokenizer */
|
|
QValueList<QChar> keys = d->emoticonMap.keys();
|
|
for ( QValueList<QChar>::const_iterator it = keys.begin(); it != keys.end(); ++it )
|
|
{
|
|
QChar key = (*it);
|
|
QValueList<Emoticon> keyValues = d->emoticonMap[key];
|
|
qHeapSort(keyValues.begin(), keyValues.end());
|
|
d->emoticonMap[key] = keyValues;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
QMap<QString, QStringList> Emoticons::emoticonAndPicList()
|
|
{
|
|
return d->emoticonAndPicList;
|
|
}
|
|
|
|
|
|
QString Emoticons::parse( const QString &message, ParseMode mode )
|
|
{
|
|
if ( !KopetePrefs::prefs()->useEmoticons() )
|
|
return message;
|
|
|
|
QValueList<Token> tokens = tokenize( message, mode );
|
|
QValueList<Token>::const_iterator token;
|
|
QString result;
|
|
QPixmap p;
|
|
for ( token = tokens.begin(); token != tokens.end(); ++token )
|
|
{
|
|
switch ( (*token).type )
|
|
{
|
|
case Text:
|
|
result += (*token).text;
|
|
break;
|
|
case Image:
|
|
result += (*token).picHTMLCode;
|
|
kdDebug( 14010 ) << k_funcinfo << "Emoticon html code: " << result << endl;
|
|
break;
|
|
default:
|
|
kdDebug( 14010 ) << k_funcinfo << "Unknown token type. Something's broken." << endl;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
} //END namesapce Kopete
|
|
|
|
#include "kopeteemoticons.moc"
|
|
|
|
|
|
|
|
// vim: set noet ts=4 sts=4 sw=4:
|
|
|