Merge pull request #54 from Bombe/issue-1227
[quassel.git] / src / uisupport / uistyle.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2014 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     StyledString result;
495     result.formatList.append(qMakePair((quint16)0, baseFormat));
496
497     if (s.length() > 65535) {
498         // We use quint16 for indexes
499         qWarning() << QString("String too long to be styled: %1").arg(s);
500         result.plainText = s;
501         return result;
502     }
503
504     quint32 curfmt = baseFormat;
505     int pos = 0; quint16 length = 0;
506     for (;;) {
507         pos = s.indexOf('%', pos);
508         if (pos < 0) break;
509         if (s[pos+1] == '%') { // escaped %, we just remove one and continue
510             s.remove(pos, 1);
511             pos++;
512             continue;
513         }
514         if (s[pos+1] == 'D' && s[pos+2] == 'c') { // color code
515             if (s[pos+3] == '-') { // color off
516                 curfmt &= 0x003fffff;
517                 length = 4;
518             }
519             else {
520                 int color = 10 * s[pos+4].digitValue() + s[pos+5].digitValue();
521                 //TODO: use 99 as transparent color (re mirc color "standard")
522                 color &= 0x0f;
523                 if (s[pos+3] == 'f') {
524                     curfmt &= 0xf0ffffff;
525                     curfmt |= (quint32)(color << 24) | 0x00400000;
526                 }
527                 else {
528                     curfmt &= 0x0fffffff;
529                     curfmt |= (quint32)(color << 28) | 0x00800000;
530                 }
531                 length = 6;
532             }
533         }
534         else if (s[pos+1] == 'O') { // reset formatting
535             curfmt &= 0x000000ff; // we keep message type-specific formatting
536             length = 2;
537         }
538         else if (s[pos+1] == 'R') { // reverse
539             // TODO: implement reverse formatting
540
541             length = 2;
542         }
543         else { // all others are toggles
544             QString code = QString("%") + s[pos+1];
545             if (s[pos+1] == 'D') code += s[pos+2];
546             FormatType ftype = formatType(code);
547             if (ftype == Invalid) {
548                 pos++;
549                 qWarning() << (QString("Invalid format code in string: %1").arg(s));
550                 continue;
551             }
552             curfmt ^= ftype;
553             length = code.length();
554         }
555         s.remove(pos, length);
556         if (pos == result.formatList.last().first)
557             result.formatList.last().second = curfmt;
558         else
559             result.formatList.append(qMakePair((quint16)pos, curfmt));
560     }
561     result.plainText = s;
562     return result;
563 }
564
565
566 QString UiStyle::mircToInternal(const QString &mirc_)
567 {
568     QString mirc;
569     mirc.reserve(mirc_.size());
570     foreach (const QChar &c, mirc_) {
571         if ((c < '\x20' || c == '\x7f') && c != '\x03') {
572             switch (c.unicode()) {
573                 case '\x02':
574                     mirc += "%B";
575                     break;
576                 case '\x0f':
577                     mirc += "%O";
578                     break;
579                 case '\x09':
580                     mirc += "        ";
581                     break;
582                 case '\x12':
583                 case '\x16':
584                     mirc += "%R";
585                     break;
586                 case '\x1d':
587                     mirc += "%S";
588                     break;
589                 case '\x1f':
590                     mirc += "%U";
591                     break;
592                 case '\x7f':
593                     mirc += QChar(0x2421);
594                     break;
595                 default:
596                     mirc += QChar(0x2400 + c.unicode());
597             }
598         } else {
599             if (c == '%')
600                 mirc += c;
601             mirc += c;
602         }
603     }
604
605     // Now we bring the color codes (\x03) in a sane format that can be parsed more easily later.
606     // %Dcfxx is foreground, %Dcbxx is background color, where xx is a 2 digit dec number denoting the color code.
607     // %Dc- turns color off.
608     // Note: We use the "mirc standard" as described in <http://www.mirc.co.uk/help/color.txt>.
609     //       This means that we don't accept something like \x03,5 (even though others, like WeeChat, do).
610     int pos = 0;
611     for (;;) {
612         pos = mirc.indexOf('\x03', pos);
613         if (pos < 0) break;  // no more mirc color codes
614         QString ins, num;
615         int l = mirc.length();
616         int i = pos + 1;
617         // check for fg color
618         if (i < l && mirc[i].isDigit()) {
619             num = mirc[i++];
620             if (i < l && mirc[i].isDigit()) num.append(mirc[i++]);
621             else num.prepend('0');
622             ins = QString("%Dcf%1").arg(num);
623
624             if (i+1 < l && mirc[i] == ',' && mirc[i+1].isDigit()) {
625                 i++;
626                 num = mirc[i++];
627                 if (i < l && mirc[i].isDigit()) num.append(mirc[i++]);
628                 else num.prepend('0');
629                 ins += QString("%Dcb%1").arg(num);
630             }
631         }
632         else {
633             ins = "%Dc-";
634         }
635         mirc.replace(pos, i-pos, ins);
636     }
637     return mirc;
638 }
639
640
641 /***********************************************************************************/
642 UiStyle::StyledMessage::StyledMessage(const Message &msg)
643     : Message(msg)
644 {
645     if (type() == Message::Plain)
646         _senderHash = 0xff;
647     else
648         _senderHash = 0x00;  // this means we never compute the hash for msgs that aren't plain
649 }
650
651
652 void UiStyle::StyledMessage::style() const
653 {
654     QString user = userFromMask(sender());
655     QString host = hostFromMask(sender());
656     QString nick = nickFromMask(sender());
657     QString txt = UiStyle::mircToInternal(contents());
658     QString bufferName = bufferInfo().bufferName();
659     bufferName.replace('%', "%%"); // well, you _can_ have a % in a buffername apparently... -_-
660     host.replace('%', "%%");     // hostnames too...
661     user.replace('%', "%%");     // and the username...
662     nick.replace('%', "%%");     // ... and then there's totally RFC-violating servers like justin.tv m(
663     const int maxNetsplitNicks = 15;
664
665     QString t;
666     switch (type()) {
667     case Message::Plain:
668         //: Plain Message
669         t = tr("%1").arg(txt); break;
670     case Message::Notice:
671         //: Notice Message
672         t = tr("%1").arg(txt); break;
673     case Message::Action:
674         //: Action Message
675         t = tr("%DN%1%DN %2").arg(nick).arg(txt);
676         break;
677     case Message::Nick:
678         //: Nick Message
679         if (nick == contents()) t = tr("You are now known as %DN%1%DN").arg(txt);
680         else t = tr("%DN%1%DN is now known as %DN%2%DN").arg(nick, txt);
681         break;
682     case Message::Mode:
683         //: Mode Message
684         if (nick.isEmpty()) t = tr("User mode: %DM%1%DM").arg(txt);
685         else t = tr("Mode %DM%1%DM by %DN%2%DN").arg(txt, nick);
686         break;
687     case Message::Join:
688         //: Join Message
689         t = tr("%DN%1%DN %DH(%2@%3)%DH has joined %DC%4%DC").arg(nick, user, host, bufferName); break;
690     case Message::Part:
691         //: Part Message
692         t = tr("%DN%1%DN %DH(%2@%3)%DH has left %DC%4%DC").arg(nick, user, host, bufferName);
693         if (!txt.isEmpty()) t = QString("%1 (%2)").arg(t).arg(txt);
694         break;
695     case Message::Quit:
696         //: Quit Message
697         t = tr("%DN%1%DN %DH(%2@%3)%DH has quit").arg(nick, user, host);
698         if (!txt.isEmpty()) t = QString("%1 (%2)").arg(t).arg(txt);
699         break;
700     case Message::Kick:
701     {
702         QString victim = txt.section(" ", 0, 0);
703         QString kickmsg = txt.section(" ", 1);
704         //: Kick Message
705         t = tr("%DN%1%DN has kicked %DN%2%DN from %DC%3%DC").arg(nick).arg(victim).arg(bufferName);
706         if (!kickmsg.isEmpty()) t = QString("%1 (%2)").arg(t).arg(kickmsg);
707     }
708     break;
709     //case Message::Kill: FIXME
710
711     case Message::Server:
712         //: Server Message
713         t = tr("%1").arg(txt); break;
714     case Message::Info:
715         //: Info Message
716         t = tr("%1").arg(txt); break;
717     case Message::Error:
718         //: Error Message
719         t = tr("%1").arg(txt); break;
720     case Message::DayChange:
721     {
722         //: Day Change Message
723         t = tr("{Day changed to %1}").arg(timestamp().date().toString(Qt::DefaultLocaleLongDate));
724     }
725         break;
726     case Message::Topic:
727         //: Topic Message
728         t = tr("%1").arg(txt); break;
729     case Message::NetsplitJoin:
730     {
731         QStringList users = txt.split("#:#");
732         QStringList servers = users.takeLast().split(" ");
733
734         for (int i = 0; i < users.count() && i < maxNetsplitNicks; i++)
735             users[i] = nickFromMask(users.at(i));
736
737         t = tr("Netsplit between %DH%1%DH and %DH%2%DH ended. Users joined: ").arg(servers.at(0), servers.at(1));
738         if (users.count() <= maxNetsplitNicks)
739             t.append(QString("%DN%1%DN").arg(users.join(", ")));
740         else
741             t.append(tr("%DN%1%DN (%2 more)").arg(static_cast<QStringList>(users.mid(0, maxNetsplitNicks)).join(", ")).arg(users.count() - maxNetsplitNicks));
742     }
743     break;
744     case Message::NetsplitQuit:
745     {
746         QStringList users = txt.split("#:#");
747         QStringList servers = users.takeLast().split(" ");
748
749         for (int i = 0; i < users.count() && i < maxNetsplitNicks; i++)
750             users[i] = nickFromMask(users.at(i));
751
752         t = tr("Netsplit between %DH%1%DH and %DH%2%DH. Users quit: ").arg(servers.at(0), servers.at(1));
753
754         if (users.count() <= maxNetsplitNicks)
755             t.append(QString("%DN%1%DN").arg(users.join(", ")));
756         else
757             t.append(tr("%DN%1%DN (%2 more)").arg(static_cast<QStringList>(users.mid(0, maxNetsplitNicks)).join(", ")).arg(users.count() - maxNetsplitNicks));
758     }
759     break;
760     case Message::Invite:
761         //: Invite Message
762         t = tr("%1").arg(txt); break;
763     default:
764         t = tr("[%1]").arg(txt);
765     }
766     _contents = UiStyle::styleString(t, UiStyle::formatType(type()));
767 }
768
769
770 const QString &UiStyle::StyledMessage::plainContents() const
771 {
772     if (_contents.plainText.isNull())
773         style();
774
775     return _contents.plainText;
776 }
777
778
779 const UiStyle::FormatList &UiStyle::StyledMessage::contentsFormatList() const
780 {
781     if (_contents.plainText.isNull())
782         style();
783
784     return _contents.formatList;
785 }
786
787
788 QString UiStyle::StyledMessage::decoratedTimestamp() const
789 {
790     return timestamp().toLocalTime().toString(UiStyle::timestampFormatString());
791 }
792
793
794 QString UiStyle::StyledMessage::plainSender() const
795 {
796     switch (type()) {
797     case Message::Plain:
798     case Message::Notice:
799         return nickFromMask(sender());
800     default:
801         return QString();
802     }
803 }
804
805
806 QString UiStyle::StyledMessage::decoratedSender() const
807 {
808     switch (type()) {
809     case Message::Plain:
810         return tr("<%1>").arg(plainSender()); break;
811     case Message::Notice:
812         return tr("[%1]").arg(plainSender()); break;
813     case Message::Action:
814         return "-*-"; break;
815     case Message::Nick:
816         return "<->"; break;
817     case Message::Mode:
818         return "***"; break;
819     case Message::Join:
820         return "-->"; break;
821     case Message::Part:
822         return "<--"; break;
823     case Message::Quit:
824         return "<--"; break;
825     case Message::Kick:
826         return "<-*"; break;
827     case Message::Kill:
828         return "<-x"; break;
829     case Message::Server:
830         return "*"; break;
831     case Message::Info:
832         return "*"; break;
833     case Message::Error:
834         return "*"; break;
835     case Message::DayChange:
836         return "-"; break;
837     case Message::Topic:
838         return "*"; break;
839     case Message::NetsplitJoin:
840         return "=>"; break;
841     case Message::NetsplitQuit:
842         return "<="; break;
843     case Message::Invite:
844         return "->"; break;
845     default:
846         return QString("%1").arg(plainSender());
847     }
848 }
849
850
851 // FIXME hardcoded to 16 sender hashes
852 quint8 UiStyle::StyledMessage::senderHash() const
853 {
854     if (_senderHash != 0xff)
855         return _senderHash;
856
857     QString nick = nickFromMask(sender()).toLower();
858     if (!nick.isEmpty()) {
859         int chopCount = 0;
860         while (chopCount < nick.size() && nick.at(nick.count() - 1 - chopCount) == '_')
861             chopCount++;
862         if (chopCount < nick.size())
863             nick.chop(chopCount);
864     }
865     quint16 hash = qChecksum(nick.toAscii().data(), nick.toAscii().size());
866     return (_senderHash = (hash & 0xf) + 1);
867 }
868
869
870 /***********************************************************************************/
871
872 QDataStream &operator<<(QDataStream &out, const UiStyle::FormatList &formatList)
873 {
874     out << formatList.count();
875     UiStyle::FormatList::const_iterator it = formatList.begin();
876     while (it != formatList.end()) {
877         out << (*it).first << (*it).second;
878         ++it;
879     }
880     return out;
881 }
882
883
884 QDataStream &operator>>(QDataStream &in, UiStyle::FormatList &formatList)
885 {
886     quint16 cnt;
887     in >> cnt;
888     for (quint16 i = 0; i < cnt; i++) {
889         quint16 pos; quint32 ftype;
890         in >> pos >> ftype;
891         formatList.append(qMakePair((quint16)pos, ftype));
892     }
893     return in;
894 }