dd530bd3a02485cb6b433b9f188fd18a9a2947c4
[quassel.git] / src / uisupport / uistyle.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2013 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program 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) version 3.                                           *
9  *                                                                         *
10  *   This program 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 this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include <QApplication>
22
23 #include "buffersettings.h"
24 #include "iconloader.h"
25 #include "qssparser.h"
26 #include "quassel.h"
27 #include "uistyle.h"
28 #include "uisettings.h"
29 #include "util.h"
30
31 QHash<QString, UiStyle::FormatType> UiStyle::_formatCodes;
32 QString UiStyle::_timestampFormatString;
33
34 UiStyle::UiStyle(QObject *parent)
35     : QObject(parent),
36     _channelJoinedIcon(SmallIcon("irc-channel-active")),
37     _channelPartedIcon(SmallIcon("irc-channel-inactive")),
38     _userOfflineIcon(SmallIcon("im-user-offline")),
39     _userOnlineIcon(SmallIcon("im-user")),
40     _userAwayIcon(SmallIcon("im-user-away")),
41     _categoryOpIcon(SmallIcon("irc-operator")),
42     _categoryVoiceIcon(SmallIcon("irc-voice")),
43     _opIconLimit(UserCategoryItem::categoryFromModes("o")),
44     _voiceIconLimit(UserCategoryItem::categoryFromModes("v"))
45 {
46     // register FormatList if that hasn't happened yet
47     // FIXME I don't think this actually avoids double registration... then again... does it hurt?
48     if (QVariant::nameToType("UiStyle::FormatList") == QVariant::Invalid) {
49         qRegisterMetaType<FormatList>("UiStyle::FormatList");
50         qRegisterMetaTypeStreamOperators<FormatList>("UiStyle::FormatList");
51         Q_ASSERT(QVariant::nameToType("UiStyle::FormatList") != QVariant::Invalid);
52     }
53
54     _uiStylePalette = QVector<QBrush>(NumRoles, QBrush());
55
56     // Now initialize the mapping between FormatCodes and FormatTypes...
57     _formatCodes["%O"] = Base;
58     _formatCodes["%B"] = Bold;
59     _formatCodes["%S"] = Italic;
60     _formatCodes["%U"] = Underline;
61     _formatCodes["%R"] = Reverse;
62
63     _formatCodes["%DN"] = Nick;
64     _formatCodes["%DH"] = Hostmask;
65     _formatCodes["%DC"] = ChannelName;
66     _formatCodes["%DM"] = ModeFlags;
67     _formatCodes["%DU"] = Url;
68
69     setTimestampFormatString("[hh:mm:ss]");
70
71     // BufferView / NickView settings
72     UiStyleSettings s;
73     _showBufferViewIcons = _showNickViewIcons = s.value("ShowItemViewIcons", true).toBool();
74     s.notify("ShowItemViewIcons", this, SLOT(showItemViewIconsChanged(QVariant)));
75
76     _allowMircColors = s.value("AllowMircColors", true).toBool();
77     s.notify("AllowMircColors", this, SLOT(allowMircColorsChanged(QVariant)));
78
79     loadStyleSheet();
80 }
81
82
83 UiStyle::~UiStyle()
84 {
85     qDeleteAll(_metricsCache);
86 }
87
88
89 void UiStyle::reload()
90 {
91     loadStyleSheet();
92 }
93
94
95 void UiStyle::loadStyleSheet()
96 {
97     qDeleteAll(_metricsCache);
98     _metricsCache.clear();
99     _formatCache.clear();
100     _formats.clear();
101
102     UiStyleSettings s;
103
104     QString styleSheet;
105     styleSheet += loadStyleSheet("file:///" + Quassel::findDataFilePath("stylesheets/default.qss"));
106     styleSheet += loadStyleSheet("file:///" + Quassel::configDirPath() + "settings.qss");
107     if (s.value("UseCustomStyleSheet", false).toBool())
108         styleSheet += loadStyleSheet("file:///" + s.value("CustomStyleSheetPath").toString(), true);
109     styleSheet += loadStyleSheet("file:///" + Quassel::optionValue("qss"), true);
110
111     if (!styleSheet.isEmpty()) {
112         QssParser parser;
113         parser.processStyleSheet(styleSheet);
114         QApplication::setPalette(parser.palette());
115
116         _uiStylePalette = parser.uiStylePalette();
117         _formats = parser.formats();
118         _listItemFormats = parser.listItemFormats();
119
120         styleSheet = styleSheet.trimmed();
121         if (!styleSheet.isEmpty())
122             qApp->setStyleSheet(styleSheet);  // pass the remaining sections to the application
123     }
124
125     emit changed();
126 }
127
128
129 QString UiStyle::loadStyleSheet(const QString &styleSheet, bool shouldExist)
130 {
131     QString ss = styleSheet;
132     if (ss.startsWith("file:///")) {
133         ss.remove(0, 8);
134         if (ss.isEmpty())
135             return QString();
136
137         QFile file(ss);
138         if (file.open(QFile::ReadOnly)) {
139             QTextStream stream(&file);
140             ss = stream.readAll();
141             file.close();
142         }
143         else {
144             if (shouldExist)
145                 qWarning() << "Could not open stylesheet file:" << file.fileName();
146             return QString();
147         }
148     }
149     return ss;
150 }
151
152
153 void UiStyle::setTimestampFormatString(const QString &format)
154 {
155     if (_timestampFormatString != format) {
156         _timestampFormatString = format;
157         // FIXME reload
158     }
159 }
160
161
162 void UiStyle::allowMircColorsChanged(const QVariant &v)
163 {
164     _allowMircColors = v.toBool();
165     emit changed();
166 }
167
168
169 /******** ItemView Styling *******/
170
171 void UiStyle::showItemViewIconsChanged(const QVariant &v)
172 {
173     _showBufferViewIcons = _showNickViewIcons = v.toBool();
174 }
175
176
177 QVariant UiStyle::bufferViewItemData(const QModelIndex &index, int role) const
178 {
179     BufferInfo::Type type = (BufferInfo::Type)index.data(NetworkModel::BufferTypeRole).toInt();
180     bool isActive = index.data(NetworkModel::ItemActiveRole).toBool();
181
182     if (role == Qt::DecorationRole) {
183         if (!_showBufferViewIcons)
184             return QVariant();
185
186         switch (type) {
187         case BufferInfo::ChannelBuffer:
188             if (isActive)
189                 return _channelJoinedIcon;
190             else
191                 return _channelPartedIcon;
192         case BufferInfo::QueryBuffer:
193             if (!isActive)
194                 return _userOfflineIcon;
195             if (index.data(NetworkModel::UserAwayRole).toBool())
196                 return _userAwayIcon;
197             else
198                 return _userOnlineIcon;
199         default:
200             return QVariant();
201         }
202     }
203
204     quint32 fmtType = BufferViewItem;
205     switch (type) {
206     case BufferInfo::StatusBuffer:
207         fmtType |= NetworkItem;
208         break;
209     case BufferInfo::ChannelBuffer:
210         fmtType |= ChannelBufferItem;
211         break;
212     case BufferInfo::QueryBuffer:
213         fmtType |= QueryBufferItem;
214         break;
215     default:
216         return QVariant();
217     }
218
219     QTextCharFormat fmt = _listItemFormats.value(BufferViewItem);
220     fmt.merge(_listItemFormats.value(fmtType));
221
222     BufferInfo::ActivityLevel activity = (BufferInfo::ActivityLevel)index.data(NetworkModel::BufferActivityRole).toInt();
223     if (activity & BufferInfo::Highlight) {
224         fmt.merge(_listItemFormats.value(BufferViewItem | HighlightedBuffer));
225         fmt.merge(_listItemFormats.value(fmtType | HighlightedBuffer));
226     }
227     else if (activity & BufferInfo::NewMessage) {
228         fmt.merge(_listItemFormats.value(BufferViewItem | UnreadBuffer));
229         fmt.merge(_listItemFormats.value(fmtType | UnreadBuffer));
230     }
231     else if (activity & BufferInfo::OtherActivity) {
232         fmt.merge(_listItemFormats.value(BufferViewItem | ActiveBuffer));
233         fmt.merge(_listItemFormats.value(fmtType | ActiveBuffer));
234     }
235     else if (!isActive) {
236         fmt.merge(_listItemFormats.value(BufferViewItem | InactiveBuffer));
237         fmt.merge(_listItemFormats.value(fmtType | InactiveBuffer));
238     }
239     else if (index.data(NetworkModel::UserAwayRole).toBool()) {
240         fmt.merge(_listItemFormats.value(BufferViewItem | UserAway));
241         fmt.merge(_listItemFormats.value(fmtType | UserAway));
242     }
243
244     return itemData(role, fmt);
245 }
246
247
248 QVariant UiStyle::nickViewItemData(const QModelIndex &index, int role) const
249 {
250     NetworkModel::ItemType type = (NetworkModel::ItemType)index.data(NetworkModel::ItemTypeRole).toInt();
251
252     if (role == Qt::DecorationRole) {
253         if (!_showNickViewIcons)
254             return QVariant();
255
256         switch (type) {
257         case NetworkModel::UserCategoryItemType:
258         {
259             int categoryId = index.data(TreeModel::SortRole).toInt();
260             if (categoryId <= _opIconLimit)
261                 return _categoryOpIcon;
262             if (categoryId <= _voiceIconLimit)
263                 return _categoryVoiceIcon;
264             return _userOnlineIcon;
265         }
266         case NetworkModel::IrcUserItemType:
267             if (index.data(NetworkModel::ItemActiveRole).toBool())
268                 return _userOnlineIcon;
269             else
270                 return _userAwayIcon;
271         default:
272             return QVariant();
273         }
274     }
275
276     QTextCharFormat fmt = _listItemFormats.value(NickViewItem);
277
278     switch (type) {
279     case NetworkModel::IrcUserItemType:
280         fmt.merge(_listItemFormats.value(NickViewItem | IrcUserItem));
281         if (!index.data(NetworkModel::ItemActiveRole).toBool()) {
282             fmt.merge(_listItemFormats.value(NickViewItem | UserAway));
283             fmt.merge(_listItemFormats.value(NickViewItem | IrcUserItem | UserAway));
284         }
285         break;
286     case NetworkModel::UserCategoryItemType:
287         fmt.merge(_listItemFormats.value(NickViewItem | UserCategoryItem));
288         break;
289     default:
290         return QVariant();
291     }
292
293     return itemData(role, fmt);
294 }
295
296
297 QVariant UiStyle::itemData(int role, const QTextCharFormat &format) const
298 {
299     switch (role) {
300     case Qt::FontRole:
301         return format.font();
302     case Qt::ForegroundRole:
303         return format.property(QTextFormat::ForegroundBrush);
304     case Qt::BackgroundRole:
305         return format.property(QTextFormat::BackgroundBrush);
306     default:
307         return QVariant();
308     }
309 }
310
311
312 /******** Caching *******/
313
314 QTextCharFormat UiStyle::format(quint64 key) const
315 {
316     return _formats.value(key, QTextCharFormat());
317 }
318
319
320 QTextCharFormat UiStyle::cachedFormat(quint32 formatType, quint32 messageLabel) const
321 {
322     return _formatCache.value(formatType | ((quint64)messageLabel << 32), QTextCharFormat());
323 }
324
325
326 void UiStyle::setCachedFormat(const QTextCharFormat &format, quint32 formatType, quint32 messageLabel) const
327 {
328     _formatCache[formatType | ((quint64)messageLabel << 32)] = format;
329 }
330
331
332 QFontMetricsF *UiStyle::fontMetrics(quint32 ftype, quint32 label) const
333 {
334     // QFontMetricsF is not assignable, so we need to store pointers :/
335     quint64 key = ftype | ((quint64)label << 32);
336
337     if (_metricsCache.contains(key))
338         return _metricsCache.value(key);
339
340     return (_metricsCache[key] = new QFontMetricsF(format(ftype, label).font()));
341 }
342
343
344 /******** Generate formats ********/
345
346 // NOTE: This and the following functions are intimately tied to the values in FormatType. Don't change this
347 //       until you _really_ know what you do!
348 QTextCharFormat UiStyle::format(quint32 ftype, quint32 label_) const
349 {
350     if (ftype == Invalid)
351         return QTextCharFormat();
352
353     quint64 label = (quint64)label_ << 32;
354
355     // check if we have exactly this format readily cached already
356     QTextCharFormat fmt = cachedFormat(ftype, label_);
357     if (fmt.properties().count())
358         return fmt;
359
360     mergeFormat(fmt, ftype, label & Q_UINT64_C(0xffff000000000000));
361
362     for (quint64 mask = Q_UINT64_C(0x0000000100000000); mask <= (quint64)Selected << 32; mask <<= 1) {
363         if (label & mask)
364             mergeFormat(fmt, ftype, mask | Q_UINT64_C(0xffff000000000000));
365     }
366
367     setCachedFormat(fmt, ftype, label_);
368     return fmt;
369 }
370
371
372 void UiStyle::mergeFormat(QTextCharFormat &fmt, quint32 ftype, quint64 label) const
373 {
374     mergeSubElementFormat(fmt, ftype & 0x00ff, label);
375
376     // TODO: allow combinations for mirc formats and colors (each), e.g. setting a special format for "bold and italic"
377     //       or "foreground 01 and background 03"
378     if ((ftype & 0xfff00)) { // element format
379         for (quint32 mask = 0x00100; mask <= 0x40000; mask <<= 1) {
380             if (ftype & mask) {
381                 mergeSubElementFormat(fmt, ftype & (mask | 0xff), label);
382             }
383         }
384     }
385
386     // Now we handle color codes
387     // We assume that those can't be combined with subelement and message types.
388     if (_allowMircColors) {
389         if (ftype & 0x00400000)
390             mergeSubElementFormat(fmt, ftype & 0x0f400000, label);  // foreground
391         if (ftype & 0x00800000)
392             mergeSubElementFormat(fmt, ftype & 0xf0800000, label);  // background
393         if ((ftype & 0x00c00000) == 0x00c00000)
394             mergeSubElementFormat(fmt, ftype & 0xffc00000, label);  // combination
395     }
396
397     // URL
398     if (ftype & Url)
399         mergeSubElementFormat(fmt, ftype & (Url | 0x000000ff), label);
400 }
401
402
403 // Merge a subelement format into an existing message format
404 void UiStyle::mergeSubElementFormat(QTextCharFormat &fmt, quint32 ftype, quint64 label) const
405 {
406     quint64 key = ftype | label;
407     fmt.merge(format(key & Q_UINT64_C(0x0000ffffffffff00))); // label + subelement
408     fmt.merge(format(key & Q_UINT64_C(0x0000ffffffffffff))); // label + subelement + msgtype
409     fmt.merge(format(key & Q_UINT64_C(0xffffffffffffff00))); // label + subelement + nickhash
410     fmt.merge(format(key & Q_UINT64_C(0xffffffffffffffff))); // label + subelement + nickhash + msgtype
411 }
412
413
414 UiStyle::FormatType UiStyle::formatType(Message::Type msgType)
415 {
416     switch (msgType) {
417     case Message::Plain:
418         return PlainMsg;
419     case Message::Notice:
420         return NoticeMsg;
421     case Message::Action:
422         return ActionMsg;
423     case Message::Nick:
424         return NickMsg;
425     case Message::Mode:
426         return ModeMsg;
427     case Message::Join:
428         return JoinMsg;
429     case Message::Part:
430         return PartMsg;
431     case Message::Quit:
432         return QuitMsg;
433     case Message::Kick:
434         return KickMsg;
435     case Message::Kill:
436         return KillMsg;
437     case Message::Server:
438         return ServerMsg;
439     case Message::Info:
440         return InfoMsg;
441     case Message::Error:
442         return ErrorMsg;
443     case Message::DayChange:
444         return DayChangeMsg;
445     case Message::Topic:
446         return TopicMsg;
447     case Message::NetsplitJoin:
448         return NetsplitJoinMsg;
449     case Message::NetsplitQuit:
450         return NetsplitQuitMsg;
451     case Message::Invite:
452         return InviteMsg;
453     }
454     //Q_ASSERT(false); // we need to handle all message types
455     qWarning() << Q_FUNC_INFO << "Unknown message type:" << msgType;
456     return ErrorMsg;
457 }
458
459
460 UiStyle::FormatType UiStyle::formatType(const QString &code)
461 {
462     if (_formatCodes.contains(code)) return _formatCodes.value(code);
463     return Invalid;
464 }
465
466
467 QString UiStyle::formatCode(FormatType ftype)
468 {
469     return _formatCodes.key(ftype);
470 }
471
472
473 QList<QTextLayout::FormatRange> UiStyle::toTextLayoutList(const FormatList &formatList, int textLength, quint32 messageLabel) const
474 {
475     QList<QTextLayout::FormatRange> formatRanges;
476     QTextLayout::FormatRange range;
477     int i = 0;
478     for (i = 0; i < formatList.count(); i++) {
479         range.format = format(formatList.at(i).second, messageLabel);
480         range.start = formatList.at(i).first;
481         if (i > 0) formatRanges.last().length = range.start - formatRanges.last().start;
482         formatRanges.append(range);
483     }
484     if (i > 0) formatRanges.last().length = textLength - formatRanges.last().start;
485     return formatRanges;
486 }
487
488
489 // This method expects a well-formatted string, there is no error checking!
490 // Since we create those ourselves, we should be pretty safe that nobody does something crappy here.
491 UiStyle::StyledString UiStyle::styleString(const QString &s_, quint32 baseFormat)
492 {
493     QString s = s_;
494     if (s.length() > 65535) {
495         qWarning() << QString("String too long to be styled: %1").arg(s);
496         return StyledString();
497     }
498     StyledString result;
499     result.formatList.append(qMakePair((quint16)0, baseFormat));
500     quint32 curfmt = baseFormat;
501     int pos = 0; quint16 length = 0;
502     for (;;) {
503         pos = s.indexOf('%', pos);
504         if (pos < 0) break;
505         if (s[pos+1] == '%') { // escaped %, we just remove one and continue
506             s.remove(pos, 1);
507             pos++;
508             continue;
509         }
510         if (s[pos+1] == 'D' && s[pos+2] == 'c') { // color code
511             if (s[pos+3] == '-') { // color off
512                 curfmt &= 0x003fffff;
513                 length = 4;
514             }
515             else {
516                 int color = 10 * s[pos+4].digitValue() + s[pos+5].digitValue();
517                 //TODO: use 99 as transparent color (re mirc color "standard")
518                 color &= 0x0f;
519                 if (s[pos+3] == 'f') {
520                     curfmt &= 0xf0ffffff;
521                     curfmt |= (quint32)(color << 24) | 0x00400000;
522                 }
523                 else {
524                     curfmt &= 0x0fffffff;
525                     curfmt |= (quint32)(color << 28) | 0x00800000;
526                 }
527                 length = 6;
528             }
529         }
530         else if (s[pos+1] == 'O') { // reset formatting
531             curfmt &= 0x000000ff; // we keep message type-specific formatting
532             length = 2;
533         }
534         else if (s[pos+1] == 'R') { // reverse
535             // TODO: implement reverse formatting
536
537             length = 2;
538         }
539         else { // all others are toggles
540             QString code = QString("%") + s[pos+1];
541             if (s[pos+1] == 'D') code += s[pos+2];
542             FormatType ftype = formatType(code);
543             if (ftype == Invalid) {
544                 pos++;
545                 qWarning() << (QString("Invalid format code in string: %1").arg(s));
546                 continue;
547             }
548             curfmt ^= ftype;
549             length = code.length();
550         }
551         s.remove(pos, length);
552         if (pos == result.formatList.last().first)
553             result.formatList.last().second = curfmt;
554         else
555             result.formatList.append(qMakePair((quint16)pos, curfmt));
556     }
557     result.plainText = s;
558     return result;
559 }
560
561
562 QString UiStyle::mircToInternal(const QString &mirc_)
563 {
564     QString mirc;
565     mirc.reserve(mirc_.size());
566     foreach (const QChar &c, mirc_) {
567         if ((c < '\x20' || c == '\x7f') && c != '\x03') {
568             switch (c.unicode()) {
569                 case '\x02':
570                     mirc += "%B";
571                     break;
572                 case '\x0f':
573                     mirc += "%O";
574                     break;
575                 case '\x09':
576                     mirc += "        ";
577                     break;
578                 case '\x12':
579                 case '\x16':
580                     mirc += "%R";
581                     break;
582                 case '\x1d':
583                     mirc += "%S";
584                     break;
585                 case '\x1f':
586                     mirc += "%U";
587                     break;
588                 case '\x7f':
589                     mirc += QChar(0x2421);
590                     break;
591                 default:
592                     mirc += QChar(0x2400 + c.unicode());
593             }
594         } else {
595             if (c == '%')
596                 mirc += c;
597             mirc += c;
598         }
599     }
600
601     // Now we bring the color codes (\x03) in a sane format that can be parsed more easily later.
602     // %Dcfxx is foreground, %Dcbxx is background color, where xx is a 2 digit dec number denoting the color code.
603     // %Dc- turns color off.
604     // Note: We use the "mirc standard" as described in <http://www.mirc.co.uk/help/color.txt>.
605     //       This means that we don't accept something like \x03,5 (even though others, like WeeChat, do).
606     int pos = 0;
607     for (;;) {
608         pos = mirc.indexOf('\x03', pos);
609         if (pos < 0) break;  // no more mirc color codes
610         QString ins, num;
611         int l = mirc.length();
612         int i = pos + 1;
613         // check for fg color
614         if (i < l && mirc[i].isDigit()) {
615             num = mirc[i++];
616             if (i < l && mirc[i].isDigit()) num.append(mirc[i++]);
617             else num.prepend('0');
618             ins = QString("%Dcf%1").arg(num);
619
620             if (i+1 < l && mirc[i] == ',' && mirc[i+1].isDigit()) {
621                 i++;
622                 num = mirc[i++];
623                 if (i < l && mirc[i].isDigit()) num.append(mirc[i++]);
624                 else num.prepend('0');
625                 ins += QString("%Dcb%1").arg(num);
626             }
627         }
628         else {
629             ins = "%Dc-";
630         }
631         mirc.replace(pos, i-pos, ins);
632     }
633     return mirc;
634 }
635
636
637 /***********************************************************************************/
638 UiStyle::StyledMessage::StyledMessage(const Message &msg)
639     : Message(msg)
640 {
641     if (type() == Message::Plain)
642         _senderHash = 0xff;
643     else
644         _senderHash = 0x00;  // this means we never compute the hash for msgs that aren't plain
645 }
646
647
648 void UiStyle::StyledMessage::style() const
649 {
650     QString user = userFromMask(sender());
651     QString host = hostFromMask(sender());
652     QString nick = nickFromMask(sender());
653     QString txt = UiStyle::mircToInternal(contents());
654     QString bufferName = bufferInfo().bufferName();
655     bufferName.replace('%', "%%"); // well, you _can_ have a % in a buffername apparently... -_-
656     host.replace('%', "%%");     // hostnames too...
657     user.replace('%', "%%");     // and the username...
658     nick.replace('%', "%%");     // ... and then there's totally RFC-violating servers like justin.tv m(
659     const int maxNetsplitNicks = 15;
660
661     QString t;
662     switch (type()) {
663     case Message::Plain:
664         //: Plain Message
665         t = tr("%1").arg(txt); break;
666     case Message::Notice:
667         //: Notice Message
668         t = tr("%1").arg(txt); break;
669     case Message::Action:
670         //: Action Message
671         t = tr("%DN%1%DN %2").arg(nick).arg(txt);
672         break;
673     case Message::Nick:
674         //: Nick Message
675         if (nick == contents()) t = tr("You are now known as %DN%1%DN").arg(txt);
676         else t = tr("%DN%1%DN is now known as %DN%2%DN").arg(nick, txt);
677         break;
678     case Message::Mode:
679         //: Mode Message
680         if (nick.isEmpty()) t = tr("User mode: %DM%1%DM").arg(txt);
681         else t = tr("Mode %DM%1%DM by %DN%2%DN").arg(txt, nick);
682         break;
683     case Message::Join:
684         //: Join Message
685         t = tr("%DN%1%DN %DH(%2@%3)%DH has joined %DC%4%DC").arg(nick, user, host, bufferName); break;
686     case Message::Part:
687         //: Part Message
688         t = tr("%DN%1%DN %DH(%2@%3)%DH has left %DC%4%DC").arg(nick, user, host, bufferName);
689         if (!txt.isEmpty()) t = QString("%1 (%2)").arg(t).arg(txt);
690         break;
691     case Message::Quit:
692         //: Quit Message
693         t = tr("%DN%1%DN %DH(%2@%3)%DH has quit").arg(nick, user, host);
694         if (!txt.isEmpty()) t = QString("%1 (%2)").arg(t).arg(txt);
695         break;
696     case Message::Kick:
697     {
698         QString victim = txt.section(" ", 0, 0);
699         QString kickmsg = txt.section(" ", 1);
700         //: Kick Message
701         t = tr("%DN%1%DN has kicked %DN%2%DN from %DC%3%DC").arg(nick).arg(victim).arg(bufferName);
702         if (!kickmsg.isEmpty()) t = QString("%1 (%2)").arg(t).arg(kickmsg);
703     }
704     break;
705     //case Message::Kill: FIXME
706
707     case Message::Server:
708         //: Server Message
709         t = tr("%1").arg(txt); break;
710     case Message::Info:
711         //: Info Message
712         t = tr("%1").arg(txt); break;
713     case Message::Error:
714         //: Error Message
715         t = tr("%1").arg(txt); break;
716     case Message::DayChange:
717     {
718         //: Day Change Message
719         t = tr("{Day changed to %1}").arg(timestamp().date().toString(Qt::DefaultLocaleLongDate));
720     }
721         break;
722     case Message::Topic:
723         //: Topic Message
724         t = tr("%1").arg(txt); break;
725     case Message::NetsplitJoin:
726     {
727         QStringList users = txt.split("#:#");
728         QStringList servers = users.takeLast().split(" ");
729
730         for (int i = 0; i < users.count() && i < maxNetsplitNicks; i++)
731             users[i] = nickFromMask(users.at(i));
732
733         t = tr("Netsplit between %DH%1%DH and %DH%2%DH ended. Users joined: ").arg(servers.at(0), servers.at(1));
734         if (users.count() <= maxNetsplitNicks)
735             t.append(QString("%DN%1%DN").arg(users.join(", ")));
736         else
737             t.append(tr("%DN%1%DN (%2 more)").arg(static_cast<QStringList>(users.mid(0, maxNetsplitNicks)).join(", ")).arg(users.count() - maxNetsplitNicks));
738     }
739     break;
740     case Message::NetsplitQuit:
741     {
742         QStringList users = txt.split("#:#");
743         QStringList servers = users.takeLast().split(" ");
744
745         for (int i = 0; i < users.count() && i < maxNetsplitNicks; i++)
746             users[i] = nickFromMask(users.at(i));
747
748         t = tr("Netsplit between %DH%1%DH and %DH%2%DH. Users quit: ").arg(servers.at(0), servers.at(1));
749
750         if (users.count() <= maxNetsplitNicks)
751             t.append(QString("%DN%1%DN").arg(users.join(", ")));
752         else
753             t.append(tr("%DN%1%DN (%2 more)").arg(static_cast<QStringList>(users.mid(0, maxNetsplitNicks)).join(", ")).arg(users.count() - maxNetsplitNicks));
754     }
755     break;
756     case Message::Invite:
757         //: Invite Message
758         t = tr("%1").arg(txt); break;
759     default:
760         t = tr("[%1]").arg(txt);
761     }
762     _contents = UiStyle::styleString(t, UiStyle::formatType(type()));
763 }
764
765
766 const QString &UiStyle::StyledMessage::plainContents() const
767 {
768     if (_contents.plainText.isNull())
769         style();
770
771     return _contents.plainText;
772 }
773
774
775 const UiStyle::FormatList &UiStyle::StyledMessage::contentsFormatList() const
776 {
777     if (_contents.plainText.isNull())
778         style();
779
780     return _contents.formatList;
781 }
782
783
784 QString UiStyle::StyledMessage::decoratedTimestamp() const
785 {
786     return timestamp().toLocalTime().toString(UiStyle::timestampFormatString());
787 }
788
789
790 QString UiStyle::StyledMessage::plainSender() const
791 {
792     switch (type()) {
793     case Message::Plain:
794     case Message::Notice:
795         return nickFromMask(sender());
796     default:
797         return QString();
798     }
799 }
800
801
802 QString UiStyle::StyledMessage::decoratedSender() const
803 {
804     switch (type()) {
805     case Message::Plain:
806         return tr("<%1>").arg(plainSender()); break;
807     case Message::Notice:
808         return tr("[%1]").arg(plainSender()); break;
809     case Message::Action:
810         return "-*-"; break;
811     case Message::Nick:
812         return "<->"; break;
813     case Message::Mode:
814         return "***"; break;
815     case Message::Join:
816         return "-->"; break;
817     case Message::Part:
818         return "<--"; break;
819     case Message::Quit:
820         return "<--"; break;
821     case Message::Kick:
822         return "<-*"; break;
823     case Message::Kill:
824         return "<-x"; break;
825     case Message::Server:
826         return "*"; break;
827     case Message::Info:
828         return "*"; break;
829     case Message::Error:
830         return "*"; break;
831     case Message::DayChange:
832         return "-"; break;
833     case Message::Topic:
834         return "*"; break;
835     case Message::NetsplitJoin:
836         return "=>"; break;
837     case Message::NetsplitQuit:
838         return "<="; break;
839     case Message::Invite:
840         return "->"; break;
841     default:
842         return QString("%1").arg(plainSender());
843     }
844 }
845
846
847 // FIXME hardcoded to 16 sender hashes
848 quint8 UiStyle::StyledMessage::senderHash() const
849 {
850     if (_senderHash != 0xff)
851         return _senderHash;
852
853     QString nick = nickFromMask(sender()).toLower();
854     if (!nick.isEmpty()) {
855         int chopCount = 0;
856         while (chopCount < nick.size() && nick.at(nick.count() - 1 - chopCount) == '_')
857             chopCount++;
858         if (chopCount < nick.size())
859             nick.chop(chopCount);
860     }
861     quint16 hash = qChecksum(nick.toAscii().data(), nick.toAscii().size());
862     return (_senderHash = (hash & 0xf) + 1);
863 }
864
865
866 /***********************************************************************************/
867
868 QDataStream &operator<<(QDataStream &out, const UiStyle::FormatList &formatList)
869 {
870     out << formatList.count();
871     UiStyle::FormatList::const_iterator it = formatList.begin();
872     while (it != formatList.end()) {
873         out << (*it).first << (*it).second;
874         ++it;
875     }
876     return out;
877 }
878
879
880 QDataStream &operator>>(QDataStream &in, UiStyle::FormatList &formatList)
881 {
882     quint16 cnt;
883     in >> cnt;
884     for (quint16 i = 0; i < cnt; i++) {
885         quint16 pos; quint32 ftype;
886         in >> pos >> ftype;
887         formatList.append(qMakePair((quint16)pos, ftype));
888     }
889     return in;
890 }