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