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