80464d87d89d7ca12d3c43122d4a8be452264c75
[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 = mirc_;
565     mirc.replace('%', "%%");    // escape % just to be sure
566     mirc.replace('\t', "        ");    // tabs break layout, also this is italics in Konversation
567     mirc.replace('\x02', "%B");
568     mirc.replace('\x0f', "%O");
569     mirc.replace('\x12', "%R");
570     mirc.replace('\x16', "%R");
571     mirc.replace('\x1d', "%S");
572     mirc.replace('\x1f', "%U");
573
574     // Now we bring the color codes (\x03) in a sane format that can be parsed more easily later.
575     // %Dcfxx is foreground, %Dcbxx is background color, where xx is a 2 digit dec number denoting the color code.
576     // %Dc- turns color off.
577     // Note: We use the "mirc standard" as described in <http://www.mirc.co.uk/help/color.txt>.
578     //       This means that we don't accept something like \x03,5 (even though others, like WeeChat, do).
579     int pos = 0;
580     for (;;) {
581         pos = mirc.indexOf('\x03', pos);
582         if (pos < 0) break;  // no more mirc color codes
583         QString ins, num;
584         int l = mirc.length();
585         int i = pos + 1;
586         // check for fg color
587         if (i < l && mirc[i].isDigit()) {
588             num = mirc[i++];
589             if (i < l && mirc[i].isDigit()) num.append(mirc[i++]);
590             else num.prepend('0');
591             ins = QString("%Dcf%1").arg(num);
592
593             if (i+1 < l && mirc[i] == ',' && mirc[i+1].isDigit()) {
594                 i++;
595                 num = mirc[i++];
596                 if (i < l && mirc[i].isDigit()) num.append(mirc[i++]);
597                 else num.prepend('0');
598                 ins += QString("%Dcb%1").arg(num);
599             }
600         }
601         else {
602             ins = "%Dc-";
603         }
604         mirc.replace(pos, i-pos, ins);
605     }
606     return mirc;
607 }
608
609
610 /***********************************************************************************/
611 UiStyle::StyledMessage::StyledMessage(const Message &msg)
612     : Message(msg)
613 {
614     if (type() == Message::Plain)
615         _senderHash = 0xff;
616     else
617         _senderHash = 0x00;  // this means we never compute the hash for msgs that aren't plain
618 }
619
620
621 void UiStyle::StyledMessage::style() const
622 {
623     QString user = userFromMask(sender());
624     QString host = hostFromMask(sender());
625     QString nick = nickFromMask(sender());
626     QString txt = UiStyle::mircToInternal(contents());
627     QString bufferName = bufferInfo().bufferName();
628     bufferName.replace('%', "%%"); // well, you _can_ have a % in a buffername apparently... -_-
629     host.replace('%', "%%");     // hostnames too...
630     user.replace('%', "%%");     // and the username...
631     nick.replace('%', "%%");     // ... and then there's totally RFC-violating servers like justin.tv m(
632     const int maxNetsplitNicks = 15;
633
634     QString t;
635     switch (type()) {
636     case Message::Plain:
637         //: Plain Message
638         t = tr("%1").arg(txt); break;
639     case Message::Notice:
640         //: Notice Message
641         t = tr("%1").arg(txt); break;
642     case Message::Action:
643         //: Action Message
644         t = tr("%DN%1%DN %2").arg(nick).arg(txt);
645         break;
646     case Message::Nick:
647         //: Nick Message
648         if (nick == contents()) t = tr("You are now known as %DN%1%DN").arg(txt);
649         else t = tr("%DN%1%DN is now known as %DN%2%DN").arg(nick, txt);
650         break;
651     case Message::Mode:
652         //: Mode Message
653         if (nick.isEmpty()) t = tr("User mode: %DM%1%DM").arg(txt);
654         else t = tr("Mode %DM%1%DM by %DN%2%DN").arg(txt, nick);
655         break;
656     case Message::Join:
657         //: Join Message
658         t = tr("%DN%1%DN %DH(%2@%3)%DH has joined %DC%4%DC").arg(nick, user, host, bufferName); break;
659     case Message::Part:
660         //: Part Message
661         t = tr("%DN%1%DN %DH(%2@%3)%DH has left %DC%4%DC").arg(nick, user, host, bufferName);
662         if (!txt.isEmpty()) t = QString("%1 (%2)").arg(t).arg(txt);
663         break;
664     case Message::Quit:
665         //: Quit Message
666         t = tr("%DN%1%DN %DH(%2@%3)%DH has quit").arg(nick, user, host);
667         if (!txt.isEmpty()) t = QString("%1 (%2)").arg(t).arg(txt);
668         break;
669     case Message::Kick:
670     {
671         QString victim = txt.section(" ", 0, 0);
672         QString kickmsg = txt.section(" ", 1);
673         //: Kick Message
674         t = tr("%DN%1%DN has kicked %DN%2%DN from %DC%3%DC").arg(nick).arg(victim).arg(bufferName);
675         if (!kickmsg.isEmpty()) t = QString("%1 (%2)").arg(t).arg(kickmsg);
676     }
677     break;
678     //case Message::Kill: FIXME
679
680     case Message::Server:
681         //: Server Message
682         t = tr("%1").arg(txt); break;
683     case Message::Info:
684         //: Info Message
685         t = tr("%1").arg(txt); break;
686     case Message::Error:
687         //: Error Message
688         t = tr("%1").arg(txt); break;
689     case Message::DayChange:
690     {
691         //: Day Change Message
692         t = tr("{Day changed to %1}").arg(QLocale().toString(timestamp(), QLocale().dateFormat()));
693     }
694         break;
695     case Message::Topic:
696         //: Topic Message
697         t = tr("%1").arg(txt); break;
698     case Message::NetsplitJoin:
699     {
700         QStringList users = txt.split("#:#");
701         QStringList servers = users.takeLast().split(" ");
702
703         for (int i = 0; i < users.count() && i < maxNetsplitNicks; i++)
704             users[i] = nickFromMask(users.at(i));
705
706         t = tr("Netsplit between %DH%1%DH and %DH%2%DH ended. Users joined: ").arg(servers.at(0), servers.at(1));
707         if (users.count() <= maxNetsplitNicks)
708             t.append(QString("%DN%1%DN").arg(users.join(", ")));
709         else
710             t.append(tr("%DN%1%DN (%2 more)").arg(static_cast<QStringList>(users.mid(0, maxNetsplitNicks)).join(", ")).arg(users.count() - maxNetsplitNicks));
711     }
712     break;
713     case Message::NetsplitQuit:
714     {
715         QStringList users = txt.split("#:#");
716         QStringList servers = users.takeLast().split(" ");
717
718         for (int i = 0; i < users.count() && i < maxNetsplitNicks; i++)
719             users[i] = nickFromMask(users.at(i));
720
721         t = tr("Netsplit between %DH%1%DH and %DH%2%DH. Users quit: ").arg(servers.at(0), servers.at(1));
722
723         if (users.count() <= maxNetsplitNicks)
724             t.append(QString("%DN%1%DN").arg(users.join(", ")));
725         else
726             t.append(tr("%DN%1%DN (%2 more)").arg(static_cast<QStringList>(users.mid(0, maxNetsplitNicks)).join(", ")).arg(users.count() - maxNetsplitNicks));
727     }
728     break;
729     case Message::Invite:
730         //: Invite Message
731         t = tr("%1").arg(txt); break;
732     default:
733         t = tr("[%1]").arg(txt);
734     }
735     _contents = UiStyle::styleString(t, UiStyle::formatType(type()));
736 }
737
738
739 const QString &UiStyle::StyledMessage::plainContents() const
740 {
741     if (_contents.plainText.isNull())
742         style();
743
744     return _contents.plainText;
745 }
746
747
748 const UiStyle::FormatList &UiStyle::StyledMessage::contentsFormatList() const
749 {
750     if (_contents.plainText.isNull())
751         style();
752
753     return _contents.formatList;
754 }
755
756
757 QString UiStyle::StyledMessage::decoratedTimestamp() const
758 {
759     return timestamp().toLocalTime().toString(UiStyle::timestampFormatString());
760 }
761
762
763 QString UiStyle::StyledMessage::plainSender() const
764 {
765     switch (type()) {
766     case Message::Plain:
767     case Message::Notice:
768         return nickFromMask(sender());
769     default:
770         return QString();
771     }
772 }
773
774
775 QString UiStyle::StyledMessage::decoratedSender() const
776 {
777     switch (type()) {
778     case Message::Plain:
779         return tr("<%1>").arg(plainSender()); break;
780     case Message::Notice:
781         return tr("[%1]").arg(plainSender()); break;
782     case Message::Action:
783         return "-*-"; break;
784     case Message::Nick:
785         return "<->"; break;
786     case Message::Mode:
787         return "***"; break;
788     case Message::Join:
789         return "-->"; break;
790     case Message::Part:
791         return "<--"; break;
792     case Message::Quit:
793         return "<--"; break;
794     case Message::Kick:
795         return "<-*"; break;
796     case Message::Kill:
797         return "<-x"; break;
798     case Message::Server:
799         return "*"; break;
800     case Message::Info:
801         return "*"; break;
802     case Message::Error:
803         return "*"; break;
804     case Message::DayChange:
805         return "-"; break;
806     case Message::Topic:
807         return "*"; break;
808     case Message::NetsplitJoin:
809         return "=>"; break;
810     case Message::NetsplitQuit:
811         return "<="; break;
812     case Message::Invite:
813         return "->"; break;
814     default:
815         return QString("%1").arg(plainSender());
816     }
817 }
818
819
820 // FIXME hardcoded to 16 sender hashes
821 quint8 UiStyle::StyledMessage::senderHash() const
822 {
823     if (_senderHash != 0xff)
824         return _senderHash;
825
826     QString nick = nickFromMask(sender()).toLower();
827     if (!nick.isEmpty()) {
828         int chopCount = 0;
829         while (chopCount < nick.size() && nick.at(nick.count() - 1 - chopCount) == '_')
830             chopCount++;
831         if (chopCount < nick.size())
832             nick.chop(chopCount);
833     }
834     quint16 hash = qChecksum(nick.toAscii().data(), nick.toAscii().size());
835     return (_senderHash = (hash & 0xf) + 1);
836 }
837
838
839 /***********************************************************************************/
840
841 QDataStream &operator<<(QDataStream &out, const UiStyle::FormatList &formatList)
842 {
843     out << formatList.count();
844     UiStyle::FormatList::const_iterator it = formatList.begin();
845     while (it != formatList.end()) {
846         out << (*it).first << (*it).second;
847         ++it;
848     }
849     return out;
850 }
851
852
853 QDataStream &operator>>(QDataStream &in, UiStyle::FormatList &formatList)
854 {
855     quint16 cnt;
856     in >> cnt;
857     for (quint16 i = 0; i < cnt; i++) {
858         quint16 pos; quint32 ftype;
859         in >> pos >> ftype;
860         formatList.append(qMakePair((quint16)pos, ftype));
861     }
862     return in;
863 }