Fix expanding networks in Chat Monitor settings
[quassel.git] / src / uisupport / qssparser.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2016 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include <QApplication>
22
23 #include "qssparser.h"
24
25 QssParser::QssParser()
26 {
27     _palette = QApplication::palette();
28
29     // Init palette color roles
30     _paletteColorRoles["alternate-base"] = QPalette::AlternateBase;
31     _paletteColorRoles["background"] = QPalette::Background;
32     _paletteColorRoles["base"] = QPalette::Base;
33     _paletteColorRoles["bright-text"] = QPalette::BrightText;
34     _paletteColorRoles["button"] = QPalette::Button;
35     _paletteColorRoles["button-text"] = QPalette::ButtonText;
36     _paletteColorRoles["dark"] = QPalette::Dark;
37     _paletteColorRoles["foreground"] = QPalette::Foreground;
38     _paletteColorRoles["highlight"] = QPalette::Highlight;
39     _paletteColorRoles["highlighted-text"] = QPalette::HighlightedText;
40     _paletteColorRoles["light"] = QPalette::Light;
41     _paletteColorRoles["link"] = QPalette::Link;
42     _paletteColorRoles["link-visited"] = QPalette::LinkVisited;
43     _paletteColorRoles["mid"] = QPalette::Mid;
44     _paletteColorRoles["midlight"] = QPalette::Midlight;
45     _paletteColorRoles["shadow"] = QPalette::Shadow;
46     _paletteColorRoles["text"] = QPalette::Text;
47     _paletteColorRoles["tooltip-base"] = QPalette::ToolTipBase;
48     _paletteColorRoles["tooltip-text"] = QPalette::ToolTipText;
49     _paletteColorRoles["window"] = QPalette::Window;
50     _paletteColorRoles["window-text"] = QPalette::WindowText;
51
52     _uiStylePalette = QVector<QBrush>(UiStyle::NumRoles, QBrush());
53
54     _uiStyleColorRoles["marker-line"] = UiStyle::MarkerLine;
55     // Sender colors
56     _uiStyleColorRoles["sender-color-self"] = UiStyle::SenderColorSelf;
57     _uiStyleColorRoles["sender-color-00"] = UiStyle::SenderColor00;
58     _uiStyleColorRoles["sender-color-01"] = UiStyle::SenderColor01;
59     _uiStyleColorRoles["sender-color-02"] = UiStyle::SenderColor02;
60     _uiStyleColorRoles["sender-color-03"] = UiStyle::SenderColor03;
61     _uiStyleColorRoles["sender-color-04"] = UiStyle::SenderColor04;
62     _uiStyleColorRoles["sender-color-05"] = UiStyle::SenderColor05;
63     _uiStyleColorRoles["sender-color-06"] = UiStyle::SenderColor06;
64     _uiStyleColorRoles["sender-color-07"] = UiStyle::SenderColor07;
65     _uiStyleColorRoles["sender-color-08"] = UiStyle::SenderColor08;
66     _uiStyleColorRoles["sender-color-09"] = UiStyle::SenderColor09;
67     _uiStyleColorRoles["sender-color-0a"] = UiStyle::SenderColor0a;
68     _uiStyleColorRoles["sender-color-0b"] = UiStyle::SenderColor0b;
69     _uiStyleColorRoles["sender-color-0c"] = UiStyle::SenderColor0c;
70     _uiStyleColorRoles["sender-color-0d"] = UiStyle::SenderColor0d;
71     _uiStyleColorRoles["sender-color-0e"] = UiStyle::SenderColor0e;
72     _uiStyleColorRoles["sender-color-0f"] = UiStyle::SenderColor0f;
73 }
74
75
76 void QssParser::processStyleSheet(QString &ss)
77 {
78     if (ss.isEmpty())
79         return;
80
81     // Remove C-style comments /* */ or //
82     QRegExp commentRx("(//.*(\\n|$)|/\\*.*\\*/)");
83     commentRx.setMinimal(true);
84     ss.remove(commentRx);
85
86     // Palette definitions first, so we can apply roles later on
87     QRegExp paletterx("(Palette[^{]*)\\{([^}]+)\\}");
88     int pos = 0;
89     while ((pos = paletterx.indexIn(ss, pos)) >= 0) {
90         parsePaletteBlock(paletterx.cap(1).trimmed(), paletterx.cap(2).trimmed());
91         ss.remove(pos, paletterx.matchedLength());
92     }
93
94     // Now we can parse the rest of our custom blocks
95     QRegExp blockrx("((?:ChatLine|ChatListItem|NickListItem)[^{]*)\\{([^}]+)\\}");
96     pos = 0;
97     while ((pos = blockrx.indexIn(ss, pos)) >= 0) {
98         //qDebug() << blockrx.cap(1) << blockrx.cap(2);
99         QString declaration = blockrx.cap(1).trimmed();
100         QString contents = blockrx.cap(2).trimmed();
101
102         if (declaration.startsWith("ChatLine"))
103             parseChatLineBlock(declaration, contents);
104         else if (declaration.startsWith("ChatListItem") || declaration.startsWith("NickListItem"))
105             parseListItemBlock(declaration, contents);
106         //else
107         // TODO: add moar here
108
109         ss.remove(pos, blockrx.matchedLength());
110     }
111 }
112
113
114 /******** Parse a whole block: declaration { contents } *******/
115
116 void QssParser::parseChatLineBlock(const QString &decl, const QString &contents)
117 {
118     quint64 fmtType = parseFormatType(decl);
119     if (fmtType == UiStyle::Invalid)
120         return;
121
122     _formats[fmtType].merge(parseFormat(contents));
123 }
124
125
126 void QssParser::parseListItemBlock(const QString &decl, const QString &contents)
127 {
128     quint32 fmtType = parseItemFormatType(decl);
129     if (fmtType == UiStyle::Invalid)
130         return;
131
132     _listItemFormats[fmtType].merge(parseFormat(contents));
133 }
134
135
136 // Palette { ... } specifies the application palette
137 // ColorGroups can be specified like pseudo states, chaining is OR (contrary to normal CSS handling):
138 //   Palette:inactive:disabled { ... } applies to both the Inactive and the Disabled state
139 void QssParser::parsePaletteBlock(const QString &decl, const QString &contents)
140 {
141     QList<QPalette::ColorGroup> colorGroups;
142
143     // Check if we want to apply this palette definition for particular ColorGroups
144     QRegExp rx("Palette((:(normal|active|inactive|disabled))*)");
145     if (!rx.exactMatch(decl)) {
146         qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
147         return;
148     }
149     if (!rx.cap(1).isEmpty()) {
150         QStringList groups = rx.cap(1).split(':', QString::SkipEmptyParts);
151         foreach(QString g, groups) {
152             if ((g == "normal" || g == "active") && !colorGroups.contains(QPalette::Active))
153                 colorGroups.append(QPalette::Active);
154             else if (g == "inactive" && !colorGroups.contains(QPalette::Inactive))
155                 colorGroups.append(QPalette::Inactive);
156             else if (g == "disabled" && !colorGroups.contains(QPalette::Disabled))
157                 colorGroups.append(QPalette::Disabled);
158         }
159     }
160
161     // Now let's go through the roles
162     foreach(QString line, contents.split(';', QString::SkipEmptyParts)) {
163         int idx = line.indexOf(':');
164         if (idx <= 0) {
165             qWarning() << Q_FUNC_INFO << tr("Invalid palette role assignment: %1").arg(line.trimmed());
166             continue;
167         }
168         QString rolestr = line.left(idx).trimmed();
169         QString brushstr = line.mid(idx + 1).trimmed();
170
171         if (_paletteColorRoles.contains(rolestr)) {
172             QBrush brush = parseBrush(brushstr);
173             if (colorGroups.count()) {
174                 foreach(QPalette::ColorGroup group, colorGroups)
175                 _palette.setBrush(group, _paletteColorRoles.value(rolestr), brush);
176             }
177             else
178                 _palette.setBrush(_paletteColorRoles.value(rolestr), brush);
179         }
180         else if (_uiStyleColorRoles.contains(rolestr)) {
181             _uiStylePalette[_uiStyleColorRoles.value(rolestr)] = parseBrush(brushstr);
182         }
183         else
184             qWarning() << Q_FUNC_INFO << tr("Unknown palette role name: %1").arg(rolestr);
185     }
186 }
187
188
189 /******** Determine format types from a block declaration ********/
190
191 quint64 QssParser::parseFormatType(const QString &decl)
192 {
193     QRegExp rx("ChatLine(?:::(\\w+))?(?:#([\\w\\-]+))?(?:\\[([=-,\\\"\\w\\s]+)\\])?");
194     // $1: subelement; $2: msgtype; $3: conditionals
195     if (!rx.exactMatch(decl)) {
196         qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
197         return UiStyle::Invalid;
198     }
199     QString subElement = rx.cap(1);
200     QString msgType = rx.cap(2);
201     QString conditions = rx.cap(3);
202
203     quint64 fmtType = 0;
204
205     // First determine the subelement
206     if (!subElement.isEmpty()) {
207         if (subElement == "timestamp")
208             fmtType |= UiStyle::Timestamp;
209         else if (subElement == "sender")
210             fmtType |= UiStyle::Sender;
211         else if (subElement == "nick")
212             fmtType |= UiStyle::Nick;
213         else if (subElement == "contents")
214             fmtType |= UiStyle::Contents;
215         else if (subElement == "hostmask")
216             fmtType |= UiStyle::Hostmask;
217         else if (subElement == "modeflags")
218             fmtType |= UiStyle::ModeFlags;
219         else if (subElement == "url")
220             fmtType |= UiStyle::Url;
221         else {
222             qWarning() << Q_FUNC_INFO << tr("Invalid subelement name in %1").arg(decl);
223             return UiStyle::Invalid;
224         }
225     }
226
227     // Now, figure out the message type
228     if (!msgType.isEmpty()) {
229         if (msgType == "plain")
230             fmtType |= UiStyle::PlainMsg;
231         else if (msgType == "notice")
232             fmtType |= UiStyle::NoticeMsg;
233         else if (msgType == "action")
234             fmtType |= UiStyle::ActionMsg;
235         else if (msgType == "nick")
236             fmtType |= UiStyle::NickMsg;
237         else if (msgType == "mode")
238             fmtType |= UiStyle::ModeMsg;
239         else if (msgType == "join")
240             fmtType |= UiStyle::JoinMsg;
241         else if (msgType == "part")
242             fmtType |= UiStyle::PartMsg;
243         else if (msgType == "quit")
244             fmtType |= UiStyle::QuitMsg;
245         else if (msgType == "kick")
246             fmtType |= UiStyle::KickMsg;
247         else if (msgType == "kill")
248             fmtType |= UiStyle::KillMsg;
249         else if (msgType == "server")
250             fmtType |= UiStyle::ServerMsg;
251         else if (msgType == "info")
252             fmtType |= UiStyle::InfoMsg;
253         else if (msgType == "error")
254             fmtType |= UiStyle::ErrorMsg;
255         else if (msgType == "daychange")
256             fmtType |= UiStyle::DayChangeMsg;
257         else if (msgType == "topic")
258             fmtType |= UiStyle::TopicMsg;
259         else if (msgType == "netsplit-join")
260             fmtType |= UiStyle::NetsplitJoinMsg;
261         else if (msgType == "netsplit-quit")
262             fmtType |= UiStyle::NetsplitQuitMsg;
263         else if (msgType == "invite")
264             fmtType |= UiStyle::InviteMsg;
265         else {
266             qWarning() << Q_FUNC_INFO << tr("Invalid message type in %1").arg(decl);
267         }
268     }
269
270     // Next up: conditional (formats, labels, nickhash)
271     QRegExp condRx("\\s*([\\w\\-]+)\\s*=\\s*\"(\\w+)\"\\s*");
272     if (!conditions.isEmpty()) {
273         foreach(const QString &cond, conditions.split(',', QString::SkipEmptyParts)) {
274             if (!condRx.exactMatch(cond)) {
275                 qWarning() << Q_FUNC_INFO << tr("Invalid condition %1").arg(cond);
276                 return UiStyle::Invalid;
277             }
278             QString condName = condRx.cap(1);
279             QString condValue = condRx.cap(2);
280             if (condName == "label") {
281                 quint64 labeltype = 0;
282                 if (condValue == "highlight")
283                     labeltype = UiStyle::Highlight;
284                 else if (condValue == "selected")
285                     labeltype = UiStyle::Selected;
286                 else {
287                     qWarning() << Q_FUNC_INFO << tr("Invalid message label: %1").arg(condValue);
288                     return UiStyle::Invalid;
289                 }
290                 fmtType |= (labeltype << 32);
291             }
292             else if (condName == "sender") {
293                 if (condValue == "self")
294                     fmtType |= (quint64) UiStyle::OwnMsg << 32;  // sender="self" is actually treated as a label
295                 else {
296                     bool ok = true;
297                     quint64 val = condValue.toUInt(&ok, 16);
298                     if (!ok) {
299                         qWarning() << Q_FUNC_INFO << tr("Invalid senderhash specification: %1").arg(condValue);
300                         return UiStyle::Invalid;
301                     }
302                     if (val >= 16) {
303                         qWarning() << Q_FUNC_INFO << tr("Senderhash can be at most \"0x0f\"!");
304                         return UiStyle::Invalid;
305                     }
306                     fmtType |= ++val << 48;
307                 }
308             }
309             else if (condName == "format") {
310                 if (condValue == "bold")
311                     fmtType |= UiStyle::Bold;
312                 else if (condValue == "italic")
313                     fmtType |= UiStyle::Italic;
314                 else if (condValue == "underline")
315                     fmtType |= UiStyle::Underline;
316                 else if (condValue == "reverse")
317                     fmtType |= UiStyle::Reverse;
318                 else {
319                     qWarning() << Q_FUNC_INFO << tr("Invalid format name: %1").arg(condValue);
320                     return UiStyle::Invalid;
321                 }
322             }
323             else if (condName == "fg-color" || condName == "bg-color") {
324                 bool ok;
325                 quint8 col = condValue.toUInt(&ok, 16);
326                 if (!ok || col > 0x0f) {
327                     qWarning() << Q_FUNC_INFO << tr("Illegal IRC color specification (must be between 00 and 0f): %1").arg(condValue);
328                     return UiStyle::Invalid;
329                 }
330                 if (condName == "fg-color")
331                     fmtType |= 0x00400000 | (quint32)(col << 24);
332                 else
333                     fmtType |= 0x00800000 | (quint32)(col << 28);
334             }
335             else {
336                 qWarning() << Q_FUNC_INFO << tr("Unhandled condition: %1").arg(condName);
337                 return UiStyle::Invalid;
338             }
339         }
340     }
341
342     return fmtType;
343 }
344
345
346 // FIXME: Code duplication
347 quint32 QssParser::parseItemFormatType(const QString &decl)
348 {
349     QRegExp rx("(Chat|Nick)ListItem(?:\\[([=-,\\\"\\w\\s]+)\\])?");
350     // $1: item type; $2: properties
351     if (!rx.exactMatch(decl)) {
352         qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
353         return UiStyle::Invalid;
354     }
355     QString mainItemType = rx.cap(1);
356     QString properties = rx.cap(2);
357
358     quint32 fmtType = 0;
359
360     // Next up: properties
361     QString type, state;
362     if (!properties.isEmpty()) {
363         QHash<QString, QString> props;
364         QRegExp propRx("\\s*([\\w\\-]+)\\s*=\\s*\"([\\w\\-]+)\"\\s*");
365         foreach(const QString &prop, properties.split(',', QString::SkipEmptyParts)) {
366             if (!propRx.exactMatch(prop)) {
367                 qWarning() << Q_FUNC_INFO << tr("Invalid proplist %1").arg(prop);
368                 return UiStyle::Invalid;
369             }
370             props[propRx.cap(1)] = propRx.cap(2);
371         }
372         type = props.value("type");
373         state = props.value("state");
374     }
375
376     if (mainItemType == "Chat") {
377         fmtType |= UiStyle::BufferViewItem;
378         if (!type.isEmpty()) {
379             if (type == "network")
380                 fmtType |= UiStyle::NetworkItem;
381             else if (type == "channel")
382                 fmtType |= UiStyle::ChannelBufferItem;
383             else if (type == "query")
384                 fmtType |= UiStyle::QueryBufferItem;
385             else {
386                 qWarning() << Q_FUNC_INFO << tr("Invalid chatlist item type %1").arg(type);
387                 return UiStyle::Invalid;
388             }
389         }
390         if (!state.isEmpty()) {
391             if (state == "inactive")
392                 fmtType |= UiStyle::InactiveBuffer;
393             else if (state == "channel-event")
394                 fmtType |= UiStyle::ActiveBuffer;
395             else if (state == "unread-message")
396                 fmtType |= UiStyle::UnreadBuffer;
397             else if (state == "highlighted")
398                 fmtType |= UiStyle::HighlightedBuffer;
399             else if (state == "away")
400                 fmtType |= UiStyle::UserAway;
401             else {
402                 qWarning() << Q_FUNC_INFO << tr("Invalid chatlist state %1").arg(state);
403                 return UiStyle::Invalid;
404             }
405         }
406     }
407     else { // NickList
408         fmtType |= UiStyle::NickViewItem;
409         if (!type.isEmpty()) {
410             if (type == "user") {
411                 fmtType |= UiStyle::IrcUserItem;
412                 if (state == "away")
413                     fmtType |= UiStyle::UserAway;
414             }
415             else if (type == "category")
416                 fmtType |= UiStyle::UserCategoryItem;
417         }
418     }
419     return fmtType;
420 }
421
422
423 /******** Parse a whole format attribute block ********/
424
425 QTextCharFormat QssParser::parseFormat(const QString &qss)
426 {
427     QTextCharFormat format;
428
429     foreach(QString line, qss.split(';', QString::SkipEmptyParts)) {
430         int idx = line.indexOf(':');
431         if (idx <= 0) {
432             qWarning() << Q_FUNC_INFO << tr("Invalid property declaration: %1").arg(line.trimmed());
433             continue;
434         }
435         QString property = line.left(idx).trimmed();
436         QString value = line.mid(idx + 1).simplified();
437
438         if (property == "background" || property == "background-color")
439             format.setBackground(parseBrush(value));
440         else if (property == "foreground" || property == "color")
441             format.setForeground(parseBrush(value));
442
443         // font-related properties
444         else if (property.startsWith("font")) {
445             if (property == "font")
446                 parseFont(value, &format);
447             else if (property == "font-style")
448                 parseFontStyle(value, &format);
449             else if (property == "font-weight")
450                 parseFontWeight(value, &format);
451             else if (property == "font-size")
452                 parseFontSize(value, &format);
453             else if (property == "font-family")
454                 parseFontFamily(value, &format);
455             else {
456                 qWarning() << Q_FUNC_INFO << tr("Invalid font property: %1").arg(line);
457                 continue;
458             }
459         }
460
461         else {
462             qWarning() << Q_FUNC_INFO << tr("Unknown ChatLine property: %1").arg(property);
463         }
464     }
465
466     return format;
467 }
468
469
470 /******** Brush ********/
471
472 QBrush QssParser::parseBrush(const QString &str, bool *ok)
473 {
474     if (ok)
475         *ok = false;
476     QColor c = parseColor(str);
477     if (c.isValid()) {
478         if (ok)
479             *ok = true;
480         return QBrush(c);
481     }
482
483     if (str.startsWith("palette")) { // Palette color role
484         // Does the palette follow the expected format?  For example:
485         // palette(marker-line)
486         // palette    ( system-color-0f  )
487         //
488         // Match the palette marker, grabbing the name inside in  case-sensitive manner
489         //   palette\s*\(\s*([a-z-0-9]+)\s*\)
490         //   palette   Match the string 'palette'
491         //   \s*       Match any amount of whitespace
492         //   \(, \)    Match literal '(' or ')' marks
493         //   (...+)    Match contents between 1 and unlimited number of times
494         //   [a-z-]    Match any character from a-z, case sensitive
495         //   [0-9]     Match any digit from 0-9
496         // Note that '\' must be escaped as '\\'
497         // Helpful interactive website for debugging and explaining:  https://regex101.com/
498         QRegExp rx("palette\\s*\\(\\s*([a-z-0-9]+)\\s*\\)");
499         if (!rx.exactMatch(str)) {
500             qWarning() << Q_FUNC_INFO << tr("Invalid palette color role specification: %1").arg(str);
501             return QBrush();
502         }
503         if (_paletteColorRoles.contains(rx.cap(1)))
504             return QBrush(_palette.brush(_paletteColorRoles.value(rx.cap(1))));
505         if (_uiStyleColorRoles.contains(rx.cap(1)))
506             return QBrush(_uiStylePalette.at(_uiStyleColorRoles.value(rx.cap(1))));
507         qWarning() << Q_FUNC_INFO << tr("Unknown palette color role: %1").arg(rx.cap(1));
508         return QBrush();
509     }
510     else if (str.startsWith("qlineargradient")) {
511         static QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
512         QRegExp rx(QString("qlineargradient\\s*\\(\\s*x1:%1,\\s*y1:%1,\\s*x2:%1,\\s*y2:%1,(.+)\\)").arg(rxFloat));
513         if (!rx.exactMatch(str)) {
514             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
515             return QBrush();
516         }
517         qreal x1 = rx.cap(1).toDouble();
518         qreal y1 = rx.cap(2).toDouble();
519         qreal x2 = rx.cap(3).toDouble();
520         qreal y2 = rx.cap(4).toDouble();
521         QGradientStops stops = parseGradientStops(rx.cap(5).trimmed());
522         if (!stops.count()) {
523             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
524             return QBrush();
525         }
526         QLinearGradient gradient(x1, y1, x2, y2);
527         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
528         gradient.setStops(stops);
529         if (ok)
530             *ok = true;
531         return QBrush(gradient);
532     }
533     else if (str.startsWith("qconicalgradient")) {
534         static QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
535         QRegExp rx(QString("qconicalgradient\\s*\\(\\s*cx:%1,\\s*cy:%1,\\s*angle:%1,(.+)\\)").arg(rxFloat));
536         if (!rx.exactMatch(str)) {
537             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
538             return QBrush();
539         }
540         qreal cx = rx.cap(1).toDouble();
541         qreal cy = rx.cap(2).toDouble();
542         qreal angle = rx.cap(3).toDouble();
543         QGradientStops stops = parseGradientStops(rx.cap(4).trimmed());
544         if (!stops.count()) {
545             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
546             return QBrush();
547         }
548         QConicalGradient gradient(cx, cy, angle);
549         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
550         gradient.setStops(stops);
551         if (ok)
552             *ok = true;
553         return QBrush(gradient);
554     }
555     else if (str.startsWith("qradialgradient")) {
556         static QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
557         QRegExp rx(QString("qradialgradient\\s*\\(\\s*cx:%1,\\s*cy:%1,\\s*radius:%1,\\s*fx:%1,\\s*fy:%1,(.+)\\)").arg(rxFloat));
558         if (!rx.exactMatch(str)) {
559             qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
560             return QBrush();
561         }
562         qreal cx = rx.cap(1).toDouble();
563         qreal cy = rx.cap(2).toDouble();
564         qreal radius = rx.cap(3).toDouble();
565         qreal fx = rx.cap(4).toDouble();
566         qreal fy = rx.cap(5).toDouble();
567         QGradientStops stops = parseGradientStops(rx.cap(6).trimmed());
568         if (!stops.count()) {
569             qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
570             return QBrush();
571         }
572         QRadialGradient gradient(cx, cy, radius, fx, fy);
573         gradient.setCoordinateMode(QGradient::ObjectBoundingMode);
574         gradient.setStops(stops);
575         if (ok)
576             *ok = true;
577         return QBrush(gradient);
578     }
579
580     return QBrush();
581 }
582
583
584 QColor QssParser::parseColor(const QString &str)
585 {
586     if (str.startsWith("rgba")) {
587         ColorTuple tuple = parseColorTuple(str.mid(4));
588         if (tuple.count() == 4)
589             return QColor(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
590     }
591     else if (str.startsWith("rgb")) {
592         ColorTuple tuple = parseColorTuple(str.mid(3));
593         if (tuple.count() == 3)
594             return QColor(tuple.at(0), tuple.at(1), tuple.at(2));
595     }
596     else if (str.startsWith("hsva")) {
597         ColorTuple tuple = parseColorTuple(str.mid(4));
598         if (tuple.count() == 4) {
599             QColor c;
600             c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
601             return c;
602         }
603     }
604     else if (str.startsWith("hsv")) {
605         ColorTuple tuple = parseColorTuple(str.mid(3));
606         if (tuple.count() == 3) {
607             QColor c;
608             c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2));
609             return c;
610         }
611     }
612     else {
613         QRegExp rx("#?[0-9A-Fa-z]+");
614         if (rx.exactMatch(str))
615             return QColor(str);
616     }
617     return QColor();
618 }
619
620
621 // get a list of comma-separated int values or percentages (rel to 0-255)
622 QssParser::ColorTuple QssParser::parseColorTuple(const QString &str)
623 {
624     ColorTuple result;
625     QRegExp rx("\\(((\\s*[0-9]{1,3}%?\\s*)(,\\s*[0-9]{1,3}%?\\s*)*)\\)");
626     if (!rx.exactMatch(str.trimmed())) {
627         return ColorTuple();
628     }
629     QStringList values = rx.cap(1).split(',');
630     foreach(QString v, values) {
631         qreal val;
632         bool perc = false;
633         bool ok;
634         v = v.trimmed();
635         if (v.endsWith('%')) {
636             perc = true;
637             v.chop(1);
638         }
639         val = (qreal)v.toUInt(&ok);
640         if (!ok)
641             return ColorTuple();
642         if (perc)
643             val = 255 * val/100;
644         result.append(val);
645     }
646     return result;
647 }
648
649
650 QGradientStops QssParser::parseGradientStops(const QString &str_)
651 {
652     QString str = str_;
653     QGradientStops result;
654     static QString rxFloat("(0?\\.[0-9]+|[01])"); // values between 0 and 1
655     QRegExp rx(QString("\\s*,?\\s*stop:\\s*(%1)\\s+([^:]+)(,\\s*stop:|$)").arg(rxFloat));
656     int idx;
657     while ((idx = rx.indexIn(str)) == 0) {
658         qreal x = rx.cap(1).toDouble();
659         QColor c = parseColor(rx.cap(3));
660         if (!c.isValid())
661             return QGradientStops();
662         result << QGradientStop(x, c);
663         str.remove(0, rx.matchedLength() - rx.cap(4).length());
664     }
665     if (!str.trimmed().isEmpty())
666         return QGradientStops();
667
668     return result;
669 }
670
671
672 /******** Font Properties ********/
673
674 void QssParser::parseFont(const QString &value, QTextCharFormat *format)
675 {
676     QRegExp rx("((?:(?:normal|italic|oblique|underline|bold|100|200|300|400|500|600|700|800|900) ){0,2}) ?(\\d+)(pt|px)? \"(.*)\"");
677     if (!rx.exactMatch(value)) {
678         qWarning() << Q_FUNC_INFO << tr("Invalid font specification: %1").arg(value);
679         return;
680     }
681     format->setFontItalic(false);
682     format->setFontWeight(QFont::Normal);
683     QStringList proplist = rx.cap(1).split(' ', QString::SkipEmptyParts);
684     foreach(QString prop, proplist) {
685         if (prop == "italic")
686             format->setFontItalic(true);
687         else if (prop == "underline")
688             format->setFontUnderline(true);
689         //else if(prop == "oblique")
690         //  format->setStyle(QFont::StyleOblique);
691         else if (prop == "bold")
692             format->setFontWeight(QFont::Bold);
693         else { // number
694             int w = prop.toInt();
695             format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
696         }
697     }
698
699     if (rx.cap(3) == "px")
700         format->setProperty(QTextFormat::FontPixelSize, rx.cap(2).toInt());
701     else
702         format->setFontPointSize(rx.cap(2).toInt());
703
704     format->setFontFamily(rx.cap(4));
705 }
706
707
708 void QssParser::parseFontStyle(const QString &value, QTextCharFormat *format)
709 {
710     if (value == "normal")
711         format->setFontItalic(false);
712     else if (value == "italic")
713         format->setFontItalic(true);
714     else if (value == "underline")
715         format->setFontUnderline(true);
716     //else if(value == "oblique")
717     //  format->setStyle(QFont::StyleOblique);
718     else {
719         qWarning() << Q_FUNC_INFO << tr("Invalid font style specification: %1").arg(value);
720     }
721 }
722
723
724 void QssParser::parseFontWeight(const QString &value, QTextCharFormat *format)
725 {
726     if (value == "normal")
727         format->setFontWeight(QFont::Normal);
728     else if (value == "bold")
729         format->setFontWeight(QFont::Bold);
730     else {
731         bool ok;
732         int w = value.toInt(&ok);
733         if (!ok) {
734             qWarning() << Q_FUNC_INFO << tr("Invalid font weight specification: %1").arg(value);
735             return;
736         }
737         format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
738     }
739 }
740
741
742 void QssParser::parseFontSize(const QString &value, QTextCharFormat *format)
743 {
744     QRegExp rx("(\\d+)(pt|px)");
745     if (!rx.exactMatch(value)) {
746         qWarning() << Q_FUNC_INFO << tr("Invalid font size specification: %1").arg(value);
747         return;
748     }
749     if (rx.cap(2) == "px")
750         format->setProperty(QTextFormat::FontPixelSize, rx.cap(1).toInt());
751     else
752         format->setFontPointSize(rx.cap(1).toInt());
753 }
754
755
756 void QssParser::parseFontFamily(const QString &value, QTextCharFormat *format)
757 {
758     QString family = value;
759     if (family.startsWith('"') && family.endsWith('"')) {
760         family = family.mid(1, family.length() - 2);
761     }
762     format->setFontFamily(family);
763 }