+ result.formatList.emplace_back(std::make_pair(pos, curfmt));
+ }
+ result.plainText = s;
+ return result;
+}
+
+
+QString UiStyle::mircToInternal(const QString &mirc_)
+{
+ QString mirc;
+ mirc.reserve(mirc_.size());
+ foreach (const QChar &c, mirc_) {
+ if ((c < '\x20' || c == '\x7f') && c != '\x03' && c != '\x04') {
+ switch (c.unicode()) {
+ case '\x02':
+ mirc += "%B";
+ break;
+ case '\x0f':
+ mirc += "%O";
+ break;
+ case '\x09':
+ mirc += " ";
+ break;
+ case '\x11':
+ // Monospace not supported yet
+ break;
+ case '\x12':
+ case '\x16':
+ mirc += "%R";
+ break;
+ case '\x1d':
+ mirc += "%I";
+ break;
+ case '\x1e':
+ mirc += "%S";
+ break;
+ case '\x1f':
+ mirc += "%U";
+ break;
+ case '\x7f':
+ mirc += QChar(0x2421);
+ break;
+ default:
+ mirc += QChar(0x2400 + c.unicode());
+ }
+ } else {
+ if (c == '%')
+ mirc += c;
+ mirc += c;
+ }
+ }
+
+ // Now we bring the color codes (\x03) in a sane format that can be parsed more easily later.
+ // %Dcfxx is foreground, %Dcbxx is background color, where xx is a 2 digit dec number denoting the color code.
+ // %Dc- turns color off.
+ // Note: We use the "mirc standard" as described in <http://www.mirc.co.uk/help/color.txt>.
+ // This means that we don't accept something like \x03,5 (even though others, like WeeChat, do).
+ {
+ int pos = 0;
+ while (true) {
+ pos = mirc.indexOf('\x03', pos);
+ if (pos < 0)
+ break; // no more mirc color codes
+ QString ins, num;
+ int l = mirc.length();
+ int i = pos + 1;
+ // check for fg color
+ if (i < l && mirc[i].isDigit()) {
+ num = mirc[i++];
+ if (i < l && mirc[i].isDigit())
+ num.append(mirc[i++]);
+ else
+ num.prepend('0');
+ ins = QString("%Dcf%1").arg(num);
+
+ if (i+1 < l && mirc[i] == ',' && mirc[i+1].isDigit()) {
+ i++;
+ num = mirc[i++];
+ if (i < l && mirc[i].isDigit())
+ num.append(mirc[i++]);
+ else
+ num.prepend('0');
+ ins += QString("%Dcb%1").arg(num);
+ }
+ }
+ else {
+ ins = "%Dc-";
+ }
+ mirc.replace(pos, i-pos, ins);
+ }
+ }
+
+ // Hex colors, as specified in https://modern.ircdocs.horse/formatting.html#hex-color
+ // %Dhf#rrggbb is foreground, %Dhb#rrggbb is background
+ {
+ static const QRegExp rx{"[\\da-fA-F]{6}"};
+ int pos = 0;
+ while (true) {
+ if (pos >= mirc.length())
+ break;
+ pos = mirc.indexOf('\x04', pos);
+ if (pos < 0)
+ break;
+ int i = pos + 1;
+ QString ins;
+ auto num = mirc.mid(i, 6);
+ if (!num.isEmpty() && rx.exactMatch(num)) {
+ ins = "%Dhf#" + num.toLower();
+ i += 6;
+ if (i < mirc.length() && mirc[i] == ',' && !(num = mirc.mid(i + 1, 6)).isEmpty() && rx.exactMatch(num)) {
+ ins += "%Dhb#" + num.toLower();
+ i += 7;
+ }
+ }
+ else {
+ ins = "%Dc-";
+ }
+ mirc.replace(pos, i - pos, ins);
+ pos += ins.length();
+ }
+ }
+
+ return mirc;
+}
+
+
+QString UiStyle::systemTimestampFormatString()
+{
+ if (_systemTimestampFormatString.isEmpty()) {
+ // Calculate and cache the system timestamp format string
+ updateSystemTimestampFormat();
+ }
+ return _systemTimestampFormatString;
+}
+
+
+QString UiStyle::timestampFormatString()
+{
+ if (useCustomTimestampFormat()) {
+ return _timestampFormatString;
+ } else {
+ return systemTimestampFormatString();
+ }
+}
+
+
+/***********************************************************************************/
+UiStyle::StyledMessage::StyledMessage(const Message &msg)
+ : Message(msg)
+{
+ switch (type()) {
+ // Don't compute the sender hash for message types without a nickname embedded
+ case Message::Server:
+ case Message::Info:
+ case Message::Error:
+ case Message::DayChange:
+ case Message::Topic:
+ case Message::Invite:
+ // Don't compute the sender hash for messages with multiple nicks
+ // Fixing this without breaking themes would be.. complex.
+ case Message::NetsplitJoin:
+ case Message::NetsplitQuit:
+ case Message::Kick:
+ // Don't compute the sender hash for message types that are not yet completed elsewhere
+ case Message::Kill:
+ _senderHash = 0x00;
+ break;
+ default:
+ // Compute the sender hash for all other message types
+ _senderHash = 0xff;
+ break;
+ }
+}
+
+
+void UiStyle::StyledMessage::style() const
+{
+ QString user = userFromMask(sender());
+ QString host = hostFromMask(sender());
+ QString nick = nickFromMask(sender());
+ QString txt = UiStyle::mircToInternal(contents());
+ QString bufferName = bufferInfo().bufferName();
+ bufferName.replace('%', "%%"); // well, you _can_ have a % in a buffername apparently... -_-
+ host.replace('%', "%%"); // hostnames too...
+ user.replace('%', "%%"); // and the username...
+ nick.replace('%', "%%"); // ... and then there's totally RFC-violating servers like justin.tv m(
+ const int maxNetsplitNicks = 15;
+
+ QString t;
+ switch (type()) {
+ case Message::Plain:
+ t = QString("%1").arg(txt); break;
+ case Message::Notice:
+ t = QString("%1").arg(txt); break;
+ case Message::Action:
+ t = QString("%DN%1%DN %2").arg(nick).arg(txt);
+ break;
+ case Message::Nick:
+ //: Nick Message
+ if (nick == contents()) t = tr("You are now known as %DN%1%DN").arg(txt);
+ else t = tr("%DN%1%DN is now known as %DN%2%DN").arg(nick, txt);
+ break;
+ case Message::Mode:
+ //: Mode Message
+ if (nick.isEmpty()) t = tr("User mode: %DM%1%DM").arg(txt);
+ else t = tr("Mode %DM%1%DM by %DN%2%DN").arg(txt, nick);
+ break;
+ case Message::Join:
+ //: Join Message
+ t = tr("%DN%1%DN %DH(%2@%3)%DH has joined %DC%4%DC").arg(nick, user, host, bufferName); break;
+ case Message::Part:
+ //: Part Message
+ t = tr("%DN%1%DN %DH(%2@%3)%DH has left %DC%4%DC").arg(nick, user, host, bufferName);
+ if (!txt.isEmpty()) t = QString("%1 (%2)").arg(t).arg(txt);
+ break;
+ case Message::Quit:
+ //: Quit Message
+ t = tr("%DN%1%DN %DH(%2@%3)%DH has quit").arg(nick, user, host);
+ if (!txt.isEmpty()) t = QString("%1 (%2)").arg(t).arg(txt);
+ break;
+ case Message::Kick:
+ {
+ QString victim = txt.section(" ", 0, 0);
+ QString kickmsg = txt.section(" ", 1);
+ //: Kick Message
+ t = tr("%DN%1%DN has kicked %DN%2%DN from %DC%3%DC").arg(nick).arg(victim).arg(bufferName);
+ if (!kickmsg.isEmpty()) t = QString("%1 (%2)").arg(t).arg(kickmsg);
+ }
+ break;
+ //case Message::Kill: FIXME
+
+ case Message::Server:
+ t = QString("%1").arg(txt); break;
+ case Message::Info:
+ t = QString("%1").arg(txt); break;
+ case Message::Error:
+ t = QString("%1").arg(txt); break;
+ case Message::DayChange:
+ {
+ //: Day Change Message
+ t = tr("{Day changed to %1}").arg(timestamp().date().toString(Qt::DefaultLocaleLongDate));
+ }
+ break;
+ case Message::Topic:
+ t = QString("%1").arg(txt); break;
+ case Message::NetsplitJoin:
+ {
+ QStringList users = txt.split("#:#");
+ QStringList servers = users.takeLast().split(" ");
+
+ for (int i = 0; i < users.count() && i < maxNetsplitNicks; i++)
+ users[i] = nickFromMask(users.at(i));
+
+ t = tr("Netsplit between %DH%1%DH and %DH%2%DH ended. Users joined: ").arg(servers.at(0), servers.at(1));
+ if (users.count() <= maxNetsplitNicks)
+ t.append(QString("%DN%1%DN").arg(users.join(", ")));
+ else
+ t.append(tr("%DN%1%DN (%2 more)").arg(static_cast<QStringList>(users.mid(0, maxNetsplitNicks)).join(", ")).arg(users.count() - maxNetsplitNicks));
+ }
+ break;
+ case Message::NetsplitQuit:
+ {
+ QStringList users = txt.split("#:#");
+ QStringList servers = users.takeLast().split(" ");
+
+ for (int i = 0; i < users.count() && i < maxNetsplitNicks; i++)
+ users[i] = nickFromMask(users.at(i));
+
+ t = tr("Netsplit between %DH%1%DH and %DH%2%DH. Users quit: ").arg(servers.at(0), servers.at(1));
+
+ if (users.count() <= maxNetsplitNicks)
+ t.append(QString("%DN%1%DN").arg(users.join(", ")));
+ else
+ t.append(tr("%DN%1%DN (%2 more)").arg(static_cast<QStringList>(users.mid(0, maxNetsplitNicks)).join(", ")).arg(users.count() - maxNetsplitNicks));
+ }
+ break;
+ case Message::Invite:
+ t = QString("%1").arg(txt); break;
+ default:
+ t = QString("[%1]").arg(txt);
+ }
+ _contents = UiStyle::styleString(t, UiStyle::formatType(type()));
+}
+
+
+const QString &UiStyle::StyledMessage::plainContents() const
+{
+ if (_contents.plainText.isNull())
+ style();
+
+ return _contents.plainText;
+}
+
+
+const UiStyle::FormatList &UiStyle::StyledMessage::contentsFormatList() const
+{
+ if (_contents.plainText.isNull())
+ style();
+
+ return _contents.formatList;
+}
+
+
+QString UiStyle::StyledMessage::decoratedTimestamp() const
+{
+ return timestamp().toLocalTime().toString(UiStyle::timestampFormatString());
+}
+
+
+QString UiStyle::StyledMessage::plainSender() const
+{
+ switch (type()) {
+ case Message::Plain:
+ case Message::Notice:
+ return nickFromMask(sender());
+ default:
+ return QString();
+ }
+}
+
+
+QString UiStyle::StyledMessage::decoratedSender() const
+{
+ QString _senderPrefixes;
+ if (_showSenderPrefixes) {
+ _senderPrefixes = senderPrefixes();
+ }
+
+ switch (type()) {
+ case Message::Plain:
+ if (_showSenderBrackets)
+ return QString("<%1%2>").arg(_senderPrefixes, plainSender());
+ else
+ return QString("%1%2").arg(_senderPrefixes, plainSender());
+ case Message::Notice:
+ return QString("[%1%2]").arg(_senderPrefixes, plainSender());
+ case Message::Action:
+ return "-*-";
+ case Message::Nick:
+ return "<->";
+ case Message::Mode:
+ return "***";
+ case Message::Join:
+ return "-->";
+ case Message::Part:
+ return "<--";
+ case Message::Quit:
+ return "<--";
+ case Message::Kick:
+ return "<-*";
+ case Message::Kill:
+ return "<-x";
+ case Message::Server:
+ return "*";
+ case Message::Info:
+ return "*";
+ case Message::Error:
+ return "*";
+ case Message::DayChange:
+ return "-";
+ case Message::Topic:
+ return "*";
+ case Message::NetsplitJoin:
+ return "=>";
+ case Message::NetsplitQuit:
+ return "<=";
+ case Message::Invite:
+ return "->";
+ }
+
+ return QString("%1%2").arg(_senderPrefixes, plainSender());
+}
+
+
+// FIXME hardcoded to 16 sender hashes
+quint8 UiStyle::StyledMessage::senderHash() const
+{
+ if (_senderHash != 0xff)
+ return _senderHash;
+
+ QString nick;
+
+ // HACK: Until multiple nicknames with different colors can be solved in the theming engine,
+ // for /nick change notifications, use the color of the new nickname (if possible), not the old
+ // nickname.
+ if (type() == Message::Nick) {
+ // New nickname is given as contents. Change to that.
+ nick = stripFormatCodes(contents()).toLower();
+ } else {
+ // Just use the sender directly
+ nick = nickFromMask(sender()).toLower();
+ }
+
+ if (!nick.isEmpty()) {
+ int chopCount = 0;
+ while (chopCount < nick.size() && nick.at(nick.count() - 1 - chopCount) == '_')
+ chopCount++;
+ if (chopCount < nick.size())
+ nick.chop(chopCount);
+ }
+ quint16 hash = qChecksum(nick.toLatin1().data(), nick.toLatin1().size());
+ return (_senderHash = (hash & 0xf) + 1);
+}
+
+/***********************************************************************************/
+
+#if QT_VERSION < 0x050000
+uint qHash(UiStyle::ItemFormatType key)
+{
+ return qHash(static_cast<quint32>(key));
+}
+
+#else
+
+uint qHash(UiStyle::ItemFormatType key, uint seed)
+{
+ return qHash(static_cast<quint32>(key), seed);
+}
+#endif
+
+UiStyle::FormatType operator|(UiStyle::FormatType lhs, UiStyle::FormatType rhs)
+{
+ return static_cast<UiStyle::FormatType>(static_cast<quint32>(lhs) | static_cast<quint32>(rhs));
+}
+
+UiStyle::FormatType& operator|=(UiStyle::FormatType& lhs, UiStyle::FormatType rhs)
+{
+ lhs = static_cast<UiStyle::FormatType>(static_cast<quint32>(lhs) | static_cast<quint32>(rhs));
+ return lhs;
+}
+
+
+UiStyle::FormatType operator|(UiStyle::FormatType lhs, quint32 rhs)
+{
+ return static_cast<UiStyle::FormatType>(static_cast<quint32>(lhs) | rhs);
+}
+
+
+UiStyle::FormatType& operator|=(UiStyle::FormatType &lhs, quint32 rhs)
+{
+ lhs = static_cast<UiStyle::FormatType>(static_cast<quint32>(lhs) | rhs);
+ return lhs;
+}
+
+
+UiStyle::FormatType operator&(UiStyle::FormatType lhs, UiStyle::FormatType rhs)
+{
+ return static_cast<UiStyle::FormatType>(static_cast<quint32>(lhs) & static_cast<quint32>(rhs));
+}
+
+
+UiStyle::FormatType& operator&=(UiStyle::FormatType &lhs, UiStyle::FormatType rhs)
+{
+ lhs = static_cast<UiStyle::FormatType>(static_cast<quint32>(lhs) & static_cast<quint32>(rhs));
+ return lhs;
+}
+
+
+UiStyle::FormatType operator&(UiStyle::FormatType lhs, quint32 rhs)
+{
+ return static_cast<UiStyle::FormatType>(static_cast<quint32>(lhs) & rhs);
+}
+
+
+UiStyle::FormatType& operator&=(UiStyle::FormatType &lhs, quint32 rhs)
+{
+ lhs = static_cast<UiStyle::FormatType>(static_cast<quint32>(lhs) & rhs);
+ return lhs;
+}
+
+
+UiStyle::FormatType& operator^=(UiStyle::FormatType &lhs, UiStyle::FormatType rhs)
+{
+ lhs = static_cast<UiStyle::FormatType>(static_cast<quint32>(lhs) ^ static_cast<quint32>(rhs));
+ return lhs;
+}
+
+
+UiStyle::MessageLabel operator|(UiStyle::MessageLabel lhs, UiStyle::MessageLabel rhs)
+{
+ return static_cast<UiStyle::MessageLabel>(static_cast<quint32>(lhs) | static_cast<quint32>(rhs));
+}
+
+
+UiStyle::MessageLabel& operator|=(UiStyle::MessageLabel &lhs, UiStyle::MessageLabel rhs)
+{
+ lhs = static_cast<UiStyle::MessageLabel>(static_cast<quint32>(lhs) | static_cast<quint32>(rhs));
+ return lhs;
+}
+
+
+UiStyle::MessageLabel operator&(UiStyle::MessageLabel lhs, quint32 rhs)
+{
+ return static_cast<UiStyle::MessageLabel>(static_cast<quint32>(lhs) & rhs);
+}
+
+
+UiStyle::MessageLabel& operator&=(UiStyle::MessageLabel &lhs, UiStyle::MessageLabel rhs)
+{
+ lhs = static_cast<UiStyle::MessageLabel>(static_cast<quint32>(lhs) & static_cast<quint32>(rhs));
+ return lhs;
+}
+
+
+quint64 operator|(UiStyle::FormatType lhs, UiStyle::MessageLabel rhs)
+{
+ return static_cast<quint64>(lhs) | (static_cast<quint64>(rhs) << 32ull);
+}
+
+
+UiStyle::ItemFormatType operator|(UiStyle::ItemFormatType lhs, UiStyle::ItemFormatType rhs)
+{
+ return static_cast<UiStyle::ItemFormatType>(static_cast<quint32>(lhs) | static_cast<quint32>(rhs));
+}
+
+
+UiStyle::ItemFormatType& operator|=(UiStyle::ItemFormatType &lhs, UiStyle::ItemFormatType rhs)
+{
+ lhs = static_cast<UiStyle::ItemFormatType>(static_cast<quint32>(lhs) | static_cast<quint32>(rhs));
+ return lhs;
+}
+
+/***********************************************************************************/
+
+QDataStream &operator<<(QDataStream &out, const UiStyle::FormatList &formatList)
+{
+ out << static_cast<quint16>(formatList.size());
+ UiStyle::FormatList::const_iterator it = formatList.begin();
+ while (it != formatList.end()) {
+ out << it->first
+ << static_cast<quint32>(it->second.type)
+ << it->second.foreground
+ << it->second.background;
+ ++it;
+ }
+ return out;
+}
+
+
+QDataStream &operator>>(QDataStream &in, UiStyle::FormatList &formatList)
+{
+ quint16 cnt;
+ in >> cnt;
+ for (quint16 i = 0; i < cnt; i++) {
+ quint16 pos;
+ quint32 ftype;
+ QColor foreground;
+ QColor background;
+ in >> pos >> ftype >> foreground >> background;
+ formatList.emplace_back(std::make_pair(quint16{pos}, UiStyle::Format{static_cast<UiStyle::FormatType>(ftype), foreground, background}));
+ }
+ return in;