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