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