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.
567 lines
15 KiB
567 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 <tqdom.h>
|
|
#include <tqfile.h>
|
|
#include <tqstylesheet.h>
|
|
#include <tqimage.h>
|
|
#include <tqdatetime.h>
|
|
|
|
#include <kapplication.h>
|
|
#include <kdebug.h>
|
|
#include <kstandarddirs.h>
|
|
#include <tdeversion.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(); }
|
|
TQString matchText;
|
|
TQString matchTextEscaped;
|
|
TQString picPath;
|
|
TQString 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:
|
|
TQMap<TQChar, TQValueList<Emoticon> > emoticonMap;
|
|
TQMap<TQString, TQStringList> emoticonAndPicList;
|
|
|
|
/**
|
|
* The current icon theme from KopetePrefs
|
|
*/
|
|
TQString theme;
|
|
|
|
|
|
};
|
|
|
|
|
|
Emoticons *Emoticons::s_self = 0L;
|
|
|
|
Emoticons *Emoticons::self()
|
|
{
|
|
if( !s_self )
|
|
s_self = new Emoticons;
|
|
return s_self;
|
|
}
|
|
|
|
|
|
TQString Emoticons::parseEmoticons(const TQString& message, ParseMode mode ) //static
|
|
{
|
|
return self()->parse( message, mode );
|
|
}
|
|
|
|
TQValueList<Emoticons::Token> Emoticons::tokenizeEmoticons( const TQString& message, ParseMode mode ) // static
|
|
{
|
|
return self()->tokenize( message, mode );
|
|
}
|
|
|
|
TQValueList<Emoticons::Token> Emoticons::tokenize( const TQString& message, uint mode )
|
|
{
|
|
TQValueList<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 TQChar must be a space. */
|
|
TQChar p = ' ';
|
|
TQChar c; /* current char */
|
|
TQChar n; /* next character after a match candidate, if strict this should be TQChar::null or space */
|
|
|
|
/* This is the EmoticonNode container, it will represent each matched emoticon */
|
|
TQValueList<EmoticonNode> foundEmoticons;
|
|
TQValueList<EmoticonNode>::const_iterator found;
|
|
/* First-pass, store the matched emoticon locations in foundEmoticons */
|
|
TQValueList<Emoticon> emoticonList;
|
|
TQValueList<Emoticon>::const_iterator it;
|
|
size_t pos;
|
|
|
|
bool inHTMLTag = false;
|
|
bool inHTMLLink = false;
|
|
bool inHTMLEntity = false;
|
|
TQString 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 TQString &theme ) : TQObject( kapp, "KopeteEmoticons" )
|
|
{
|
|
// kdDebug(14010) << "KopeteEmoticons::KopeteEmoticons" << endl;
|
|
d=new Private;
|
|
if(theme.isNull())
|
|
{
|
|
initEmoticons();
|
|
connect( KopetePrefs::prefs(), TQT_SIGNAL(saved()), this, TQT_SLOT(initEmoticons()) );
|
|
}
|
|
else
|
|
{
|
|
initEmoticons( theme );
|
|
}
|
|
}
|
|
|
|
|
|
Emoticons::~Emoticons( )
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
|
|
|
|
void Emoticons::addIfPossible( const TQString& filenameNoExt, const TQStringList &emoticons )
|
|
{
|
|
KStandardDirs *dir = KGlobal::dirs();
|
|
TQString pic;
|
|
|
|
//maybe an extension was given, so try to find the exact file
|
|
pic = dir->findResource( "emoticons", d->theme + TQString::tqfromLatin1( "/" ) + filenameNoExt );
|
|
|
|
if( pic.isNull() )
|
|
pic = dir->findResource( "emoticons", d->theme + TQString::tqfromLatin1( "/" ) + filenameNoExt + TQString::tqfromLatin1( ".mng" ) );
|
|
if ( pic.isNull() )
|
|
pic = dir->findResource( "emoticons", d->theme + TQString::tqfromLatin1( "/" ) + filenameNoExt + TQString::tqfromLatin1( ".png" ) );
|
|
if ( pic.isNull() )
|
|
pic = dir->findResource( "emoticons", d->theme + TQString::tqfromLatin1( "/" ) + filenameNoExt + TQString::tqfromLatin1( ".gif" ) );
|
|
|
|
if( !pic.isNull() ) // only add if we found one file
|
|
{
|
|
TQPixmap p;
|
|
TQString result;
|
|
|
|
d->emoticonAndPicList.insert( pic, emoticons );
|
|
|
|
for ( TQStringList::const_iterator it = emoticons.constBegin(), end = emoticons.constEnd();
|
|
it != end; ++it )
|
|
{
|
|
TQString matchEscaped=TQStyleSheet::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 = TQString::tqfromLatin1( "<img align=\"center\" src=\"" ) +
|
|
e.picPath +
|
|
TQString::tqfromLatin1( "\" title=\"" ) +
|
|
matchEscaped +
|
|
TQString::tqfromLatin1( "\" width=\"" ) +
|
|
TQString::number( p.width() ) +
|
|
TQString::tqfromLatin1( "\" height=\"" ) +
|
|
TQString::number( p.height() ) +
|
|
TQString::tqfromLatin1( "\" />" );
|
|
|
|
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 TQString &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();
|
|
|
|
TQString filename= KGlobal::dirs()->findResource( "emoticons", d->theme + TQString::tqfromLatin1( "/emoticons.xml" ) );
|
|
if(!filename.isEmpty())
|
|
return initEmoticon_emoticonsxml( filename );
|
|
filename= KGlobal::dirs()->findResource( "emoticons", d->theme + TQString::tqfromLatin1( "/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 TQString & filename)
|
|
{
|
|
TQDomDocument emoticonMap( TQString::tqfromLatin1( "messaging-emoticon-map" ) );
|
|
|
|
TQFile mapFile( filename );
|
|
mapFile.open( IO_ReadOnly );
|
|
emoticonMap.setContent( &mapFile );
|
|
|
|
TQDomElement list = emoticonMap.documentElement();
|
|
TQDomNode node = list.firstChild();
|
|
while( !node.isNull() )
|
|
{
|
|
TQDomElement element = node.toElement();
|
|
if( !element.isNull() )
|
|
{
|
|
if( element.tagName() == TQString::tqfromLatin1( "emoticon" ) )
|
|
{
|
|
TQString emoticon_file = element.attribute(
|
|
TQString::tqfromLatin1( "file" ), TQString() );
|
|
TQStringList items;
|
|
|
|
TQDomNode emoticonNode = node.firstChild();
|
|
while( !emoticonNode.isNull() )
|
|
{
|
|
TQDomElement emoticonElement = emoticonNode.toElement();
|
|
if( !emoticonElement.isNull() )
|
|
{
|
|
if( emoticonElement.tagName() == TQString::tqfromLatin1( "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 TQString & filename)
|
|
{
|
|
TQDomDocument emoticonMap( TQString::tqfromLatin1( "icondef" ) );
|
|
|
|
TQFile mapFile( filename );
|
|
mapFile.open( IO_ReadOnly );
|
|
emoticonMap.setContent( &mapFile );
|
|
|
|
TQDomElement list = emoticonMap.documentElement();
|
|
TQDomNode node = list.firstChild();
|
|
while( !node.isNull() )
|
|
{
|
|
TQDomElement element = node.toElement();
|
|
if( !element.isNull() )
|
|
{
|
|
if( element.tagName() == TQString::tqfromLatin1( "icon" ) )
|
|
{
|
|
TQStringList items;
|
|
TQString emoticon_file;
|
|
|
|
TQDomNode emoticonNode = node.firstChild();
|
|
while( !emoticonNode.isNull() )
|
|
{
|
|
TQDomElement emoticonElement = emoticonNode.toElement();
|
|
if( !emoticonElement.isNull() )
|
|
{
|
|
if( emoticonElement.tagName() == TQString::tqfromLatin1( "text" ) )
|
|
{
|
|
//TODO xml:lang
|
|
items << emoticonElement.text();
|
|
}
|
|
else if( emoticonElement.tagName() == TQString::tqfromLatin1( "object" ) && emoticon_file.isEmpty() )
|
|
{
|
|
TQString mime= emoticonElement.attribute(
|
|
TQString::tqfromLatin1( "mime" ), TQString::tqfromLatin1("image/*") );
|
|
if(mime.startsWith(TQString::tqfromLatin1("image/")) && !mime.endsWith(TQString::tqfromLatin1("/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 */
|
|
TQValueList<TQChar> keys = d->emoticonMap.keys();
|
|
for ( TQValueList<TQChar>::const_iterator it = keys.begin(); it != keys.end(); ++it )
|
|
{
|
|
TQChar key = (*it);
|
|
TQValueList<Emoticon> keyValues = d->emoticonMap[key];
|
|
qHeapSort(keyValues.begin(), keyValues.end());
|
|
d->emoticonMap[key] = keyValues;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
TQMap<TQString, TQStringList> Emoticons::emoticonAndPicList()
|
|
{
|
|
return d->emoticonAndPicList;
|
|
}
|
|
|
|
|
|
TQString Emoticons::parse( const TQString &message, ParseMode mode )
|
|
{
|
|
if ( !KopetePrefs::prefs()->useEmoticons() )
|
|
return message;
|
|
|
|
TQValueList<Token> tokens = tokenize( message, mode );
|
|
TQValueList<Token>::const_iterator token;
|
|
TQString result;
|
|
TQPixmap 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;
|
|
}
|
|
|
|
void Emoticons::reload()
|
|
{
|
|
d->emoticonAndPicList.clear();
|
|
d->emoticonMap.clear();
|
|
initEmoticons( KopetePrefs::prefs()->iconTheme() );
|
|
}
|
|
|
|
} //END namesapce Kopete
|
|
|
|
#include "kopeteemoticons.moc"
|
|
|
|
|
|
|
|
// vim: set noet ts=4 sts=4 sw=4:
|
|
|