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