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