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