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