1 /***************************************************************************
2 * Copyright (C) 2005-09 by the Quassel Project *
3 * devel@quassel-irc.org *
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. *
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. *
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 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
19 ***************************************************************************/
21 #include <QApplication>
23 #include "qssparser.h"
25 QssParser::QssParser()
28 _palette = QApplication::palette();
30 // Init palette color roles
31 _paletteColorRoles["alternate-base"] = QPalette::AlternateBase;
32 _paletteColorRoles["background"] = QPalette::Background;
33 _paletteColorRoles["base"] = QPalette::Base;
34 _paletteColorRoles["bright-text"] = QPalette::BrightText;
35 _paletteColorRoles["button"] = QPalette::Button;
36 _paletteColorRoles["button-text"] = QPalette::ButtonText;
37 _paletteColorRoles["dark"] = QPalette::Dark;
38 _paletteColorRoles["foreground"] = QPalette::Foreground;
39 _paletteColorRoles["highlight"] = QPalette::Highlight;
40 _paletteColorRoles["highlighted-text"] = QPalette::HighlightedText;
41 _paletteColorRoles["light"] = QPalette::Light;
42 _paletteColorRoles["link"] = QPalette::Link;
43 _paletteColorRoles["link-visited"] = QPalette::LinkVisited;
44 _paletteColorRoles["mid"] = QPalette::Mid;
45 _paletteColorRoles["midlight"] = QPalette::Midlight;
46 _paletteColorRoles["shadow"] = QPalette::Shadow;
47 _paletteColorRoles["text"] = QPalette::Text;
48 _paletteColorRoles["tooltip-base"] = QPalette::ToolTipBase;
49 _paletteColorRoles["tooltip-text"] = QPalette::ToolTipText;
50 _paletteColorRoles["window"] = QPalette::Window;
51 _paletteColorRoles["window-text"] = QPalette::WindowText;
54 void QssParser::processStyleSheet(QString &ss) {
58 // Remove C-style comments /* */ or //
59 QRegExp commentRx("(//.*(\\n|$)|/\\*.*\\*/)");
60 commentRx.setMinimal(true);
63 // Palette definitions first, so we can apply roles later on
64 QRegExp paletterx("(Palette[^{]*)\\{([^}]+)\\}");
66 while((pos = paletterx.indexIn(ss, pos)) >= 0) {
67 parsePaletteData(paletterx.cap(1).trimmed(), paletterx.cap(2).trimmed());
68 ss.remove(pos, paletterx.matchedLength());
71 // Now we can parse the rest of our custom blocks
72 QRegExp blockrx("((?:ChatLine|BufferList|NickList|TreeView)[^{]*)\\{([^}]+)\\}");
74 while((pos = blockrx.indexIn(ss, pos)) >= 0) {
75 //qDebug() << blockrx.cap(1) << blockrx.cap(2);
77 if(blockrx.cap(1).startsWith("ChatLine"))
78 parseChatLineData(blockrx.cap(1).trimmed(), blockrx.cap(2).trimmed());
80 // TODO: add moar here
82 ss.remove(pos, blockrx.matchedLength());
86 void QssParser::parseChatLineData(const QString &decl, const QString &contents) {
87 quint64 fmtType = parseFormatType(decl);
88 if(fmtType == UiStyle::Invalid)
91 QTextCharFormat format;
93 foreach(QString line, contents.split(';', QString::SkipEmptyParts)) {
94 int idx = line.indexOf(':');
96 qWarning() << Q_FUNC_INFO << tr("Invalid property declaration: %1").arg(line.trimmed());
99 QString property = line.left(idx).trimmed();
100 QString value = line.mid(idx + 1).simplified();
102 if(property == "background" || property == "background-color")
103 format.setBackground(parseBrush(value));
104 else if(property == "foreground" || property == "color")
105 format.setForeground(parseBrush(value));
107 // font-related properties
108 else if(property.startsWith("font")) {
109 if(property == "font")
110 parseFont(value, &format);
111 else if(property == "font-style")
112 parseFontStyle(value, &format);
113 else if(property == "font-weight")
114 parseFontWeight(value, &format);
115 else if(property == "font-size")
116 parseFontSize(value, &format);
117 else if(property == "font-family")
118 parseFontFamily(value, &format);
120 qWarning() << Q_FUNC_INFO << tr("Invalid font property: %1").arg(line);
126 qWarning() << Q_FUNC_INFO << tr("Unknown ChatLine property: %1").arg(property);
130 _formats[fmtType].merge(format);
133 quint64 QssParser::parseFormatType(const QString &decl) {
134 QRegExp rx("ChatLine(?:::(\\w+))?(?:#(\\w+))?(?:\\[([=-,\\\"\\w\\s]+)\\])?\\s*");
135 // $1: subelement; $2: msgtype; $3: conditionals
136 if(!rx.exactMatch(decl)) {
137 qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
138 return UiStyle::Invalid;
140 QString subElement = rx.cap(1);
141 QString msgType = rx.cap(2);
142 QString conditions = rx.cap(3);
146 // First determine the subelement
147 if(!subElement.isEmpty()) {
148 if(subElement == "timestamp")
149 fmtType |= UiStyle::Timestamp;
150 else if(subElement == "sender")
151 fmtType |= UiStyle::Sender;
152 else if(subElement == "nick")
153 fmtType |= UiStyle::Nick;
154 else if(subElement == "contents")
155 fmtType |= UiStyle::Contents;
156 else if(subElement == "hostmask")
157 fmtType |= UiStyle::Hostmask;
158 else if(subElement == "modeflags")
159 fmtType |= UiStyle::ModeFlags;
161 qWarning() << Q_FUNC_INFO << tr("Invalid subelement name in %1").arg(decl);
162 return UiStyle::Invalid;
166 // Now, figure out the message type
167 if(!msgType.isEmpty()) {
168 if(msgType == "plain")
169 fmtType |= UiStyle::PlainMsg;
170 else if(msgType == "notice")
171 fmtType |= UiStyle::NoticeMsg;
172 else if(msgType == "action")
173 fmtType |= UiStyle::ActionMsg;
174 else if(msgType == "nick")
175 fmtType |= UiStyle::NickMsg;
176 else if(msgType == "mode")
177 fmtType |= UiStyle::ModeMsg;
178 else if(msgType == "join")
179 fmtType |= UiStyle::JoinMsg;
180 else if(msgType == "part")
181 fmtType |= UiStyle::PartMsg;
182 else if(msgType == "quit")
183 fmtType |= UiStyle::QuitMsg;
184 else if(msgType == "kick")
185 fmtType |= UiStyle::KickMsg;
186 else if(msgType == "kill")
187 fmtType |= UiStyle::KillMsg;
188 else if(msgType == "server")
189 fmtType |= UiStyle::ServerMsg;
190 else if(msgType == "info")
191 fmtType |= UiStyle::InfoMsg;
192 else if(msgType == "error")
193 fmtType |= UiStyle::ErrorMsg;
194 else if(msgType == "daychange")
195 fmtType |= UiStyle::DayChangeMsg;
197 qWarning() << Q_FUNC_INFO << tr("Invalid message type in %1").arg(decl);
201 // Next up: conditional (formats, labels, nickhash)
202 QRegExp condRx("\\s*([\\w\\-]+)\\s*=\\s*\"(\\w+)\"\\s*");
203 if(!conditions.isEmpty()) {
204 foreach(const QString &cond, conditions.split(',', QString::SkipEmptyParts)) {
205 if(!condRx.exactMatch(cond)) {
206 qWarning() << Q_FUNC_INFO << tr("Invalid condition %1").arg(cond);
207 return UiStyle::Invalid;
209 QString condName = condRx.cap(1);
210 QString condValue = condRx.cap(2);
211 if(condName == "label") {
212 quint64 labeltype = 0;
213 if(condValue == "highlight")
214 labeltype = UiStyle::Highlight;
215 else if(condValue == "selected")
216 labeltype = UiStyle::Selected;
218 qWarning() << Q_FUNC_INFO << tr("Invalid message label: %1").arg(condValue);
219 return UiStyle::Invalid;
221 fmtType |= (labeltype << 32);
222 } else if(condName == "sender") {
223 if(condValue == "self")
224 fmtType |= (quint64)UiStyle::OwnMsg << 32; // sender="self" is actually treated as a label
227 quint64 val = condValue.toUInt(&ok, 16);
229 qWarning() << Q_FUNC_INFO << tr("Invalid senderhash specification: %1").arg(condValue);
230 return UiStyle::Invalid;
233 qWarning() << Q_FUNC_INFO << tr("Senderhash can be at most \"0x0f\"!");
234 return UiStyle::Invalid;
236 fmtType |= val << 48;
238 } else if(condName == "format") {
239 if(condValue == "bold")
240 fmtType |= UiStyle::Bold;
241 else if(condValue == "italic")
242 fmtType |= UiStyle::Italic;
243 else if(condValue == "underline")
244 fmtType |= UiStyle::Underline;
245 else if(condValue == "reverse")
246 fmtType |= UiStyle::Reverse;
248 qWarning() << Q_FUNC_INFO << tr("Invalid format name: %1").arg(condValue);
249 return UiStyle::Invalid;
251 } else if(condName == "fg-color" || condName == "bg-color") {
253 quint8 col = condValue.toUInt(&ok, 16);
254 if(!ok || col > 0x0f) {
255 qWarning() << Q_FUNC_INFO << tr("Illegal IRC color specification (must be between 00 and 0f): %1").arg(condValue);
256 return UiStyle::Invalid;
258 if(condName == "fg-color")
259 fmtType |= 0x00400000 | (col << 24);
261 fmtType |= 0x00800000 | (col << 28);
263 qWarning() << Q_FUNC_INFO << tr("Unhandled condition: %1").arg(condName);
264 return UiStyle::Invalid;
272 // Palette { ... } specifies the application palette
273 // ColorGroups can be specified like pseudo states, chaining is OR (contrary to normal CSS handling):
274 // Palette:inactive:disabled { ... } applies to both the Inactive and the Disabled state
275 void QssParser::parsePaletteData(const QString &decl, const QString &contents) {
276 QList<QPalette::ColorGroup> colorGroups;
278 // Check if we want to apply this palette definition for particular ColorGroups
279 QRegExp rx("Palette((:(normal|active|inactive|disabled))*)");
280 if(!rx.exactMatch(decl)) {
281 qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
284 if(!rx.cap(1).isEmpty()) {
285 QStringList groups = rx.cap(1).split(':', QString::SkipEmptyParts);
286 foreach(QString g, groups) {
287 if((g == "normal" || g == "active") && !colorGroups.contains(QPalette::Active))
288 colorGroups.append(QPalette::Active);
289 else if(g == "inactive" && !colorGroups.contains(QPalette::Inactive))
290 colorGroups.append(QPalette::Inactive);
291 else if(g == "disabled" && !colorGroups.contains(QPalette::Disabled))
292 colorGroups.append(QPalette::Disabled);
296 // Now let's go through the roles
297 foreach(QString line, contents.split(';', QString::SkipEmptyParts)) {
298 int idx = line.indexOf(':');
300 qWarning() << Q_FUNC_INFO << tr("Invalid palette role assignment: %1").arg(line.trimmed());
303 QString rolestr = line.left(idx).trimmed();
304 QString brushstr = line.mid(idx + 1).trimmed();
305 if(!_paletteColorRoles.contains(rolestr)) {
306 qWarning() << Q_FUNC_INFO << tr("Unknown palette role name: %1").arg(rolestr);
309 QBrush brush = parseBrush(brushstr);
310 if(colorGroups.count()) {
311 foreach(QPalette::ColorGroup group, colorGroups)
312 _palette.setBrush(group, _paletteColorRoles.value(rolestr), brush);
314 _palette.setBrush(_paletteColorRoles.value(rolestr), brush);
318 QBrush QssParser::parseBrush(const QString &str, bool *ok) {
321 QColor c = parseColor(str);
328 if(str.startsWith("palette")) { // Palette color role
329 QRegExp rx("palette\\s*\\(\\s*([a-z-]+)\\s*\\)");
330 if(!rx.exactMatch(str)) {
331 qWarning() << Q_FUNC_INFO << tr("Invalid palette color role specification: %1").arg(str);
334 if(!_paletteColorRoles.contains(rx.cap(1))) {
335 qWarning() << Q_FUNC_INFO << tr("Unknown palette color role: %1").arg(rx.cap(1));
338 return QBrush(_palette.brush(_paletteColorRoles.value(rx.cap(1))));
340 } else if(str.startsWith("qlineargradient")) {
341 static QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
342 QRegExp rx(QString("qlineargradient\\s*\\(\\s*x1:%1,\\s*y1:%1,\\s*x2:%1,\\s*y2:%1,(.+)\\)").arg(rxFloat));
343 if(!rx.exactMatch(str)) {
344 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
347 qreal x1 = rx.cap(1).toDouble();
348 qreal y1 = rx.cap(2).toDouble();
349 qreal x2 = rx.cap(3).toDouble();
350 qreal y2 = rx.cap(4).toDouble();
351 QGradientStops stops = parseGradientStops(rx.cap(5).trimmed());
353 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
356 QLinearGradient gradient(x1, y1, x2, y2);
357 gradient.setStops(stops);
360 return QBrush(gradient);
362 } else if(str.startsWith("qconicalgradient")) {
363 static QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
364 QRegExp rx(QString("qconicalgradient\\s*\\(\\s*cx:%1,\\s*cy:%1,\\s*angle:%1,(.+)\\)").arg(rxFloat));
365 if(!rx.exactMatch(str)) {
366 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
369 qreal cx = rx.cap(1).toDouble();
370 qreal cy = rx.cap(2).toDouble();
371 qreal angle = rx.cap(3).toDouble();
372 QGradientStops stops = parseGradientStops(rx.cap(4).trimmed());
374 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
377 QConicalGradient gradient(cx, cy, angle);
378 gradient.setStops(stops);
381 return QBrush(gradient);
383 } else if(str.startsWith("qradialgradient")) {
384 static QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
385 QRegExp rx(QString("qradialgradient\\s*\\(\\s*cx:%1,\\s*cy:%1,\\s*radius:%1,\\s*fx:%1,\\s*fy:%1,(.+)\\)").arg(rxFloat));
386 if(!rx.exactMatch(str)) {
387 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
390 qreal cx = rx.cap(1).toDouble();
391 qreal cy = rx.cap(2).toDouble();
392 qreal radius = rx.cap(3).toDouble();
393 qreal fx = rx.cap(4).toDouble();
394 qreal fy = rx.cap(5).toDouble();
395 QGradientStops stops = parseGradientStops(rx.cap(6).trimmed());
397 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
400 QRadialGradient gradient(cx, cy, radius, fx, fy);
401 gradient.setStops(stops);
404 return QBrush(gradient);
410 QColor QssParser::parseColor(const QString &str) {
411 if(str.startsWith("rgba")) {
412 ColorTuple tuple = parseColorTuple(str.mid(4));
413 if(tuple.count() == 4)
414 return QColor(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
415 } else if(str.startsWith("rgb")) {
416 ColorTuple tuple = parseColorTuple(str.mid(3));
417 if(tuple.count() == 3)
418 return QColor(tuple.at(0), tuple.at(1), tuple.at(2));
419 } else if(str.startsWith("hsva")) {
420 ColorTuple tuple = parseColorTuple(str.mid(4));
421 if(tuple.count() == 4) {
423 c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
426 } else if(str.startsWith("hsv")) {
427 ColorTuple tuple = parseColorTuple(str.mid(3));
428 if(tuple.count() == 3) {
430 c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2));
434 QRegExp rx("#?[0-9A-Fa-z]+");
435 if(rx.exactMatch(str))
441 // get a list of comma-separated int values or percentages (rel to 0-255)
442 QssParser::ColorTuple QssParser::parseColorTuple(const QString &str) {
444 QRegExp rx("\\(((\\s*[0-9]{1,3}%?\\s*)(,\\s*[0-9]{1,3}%?\\s*)*)\\)");
445 if(!rx.exactMatch(str.trimmed())) {
448 QStringList values = rx.cap(1).split(',');
449 foreach(QString v, values) {
454 if(v.endsWith('%')) {
458 val = (qreal)v.toUInt(&ok);
468 QGradientStops QssParser::parseGradientStops(const QString &str_) {
470 QGradientStops result;
471 static QString rxFloat("(0?\\.[0-9]+|[01])"); // values between 0 and 1
472 QRegExp rx(QString("\\s*,?\\s*stop:\\s*(%1)\\s+([^:]+)(,\\s*stop:|$)").arg(rxFloat));
474 while((idx = rx.indexIn(str)) == 0) {
475 qreal x = rx.cap(1).toDouble();
476 QColor c = parseColor(rx.cap(3));
478 return QGradientStops();
479 result << QGradientStop(x, c);
480 str.remove(0, rx.matchedLength() - rx.cap(4).length());
482 if(!str.trimmed().isEmpty())
483 return QGradientStops();
488 /******** Font Properties ********/
490 void QssParser::parseFont(const QString& value, QTextCharFormat* format) {
491 QRegExp rx("((?:(?:normal|italic|oblique|underline|bold|100|200|300|400|500|600|700|800|900) ){0,2}) ?(\\d+)(pt|px)? \"(.*)\"");
492 if(!rx.exactMatch(value)) {
493 qWarning() << Q_FUNC_INFO << tr("Invalid font specification: %1").arg(value);
496 format->setFontItalic(false);
497 format->setFontWeight(QFont::Normal);
498 QStringList proplist = rx.cap(1).split(' ', QString::SkipEmptyParts);
499 foreach(QString prop, proplist) {
501 format->setFontItalic(true);
502 else if(prop == "underline")
503 format->setFontUnderline(true);
504 //else if(prop == "oblique")
505 // format->setStyle(QFont::StyleOblique);
506 else if(prop == "bold")
507 format->setFontWeight(QFont::Bold);
509 int w = prop.toInt();
510 format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
514 if(rx.cap(3) == "px")
515 format->setProperty(QTextFormat::FontPixelSize, rx.cap(2).toInt());
517 format->setFontPointSize(rx.cap(2).toInt());
519 format->setFontFamily(rx.cap(4));
522 void QssParser::parseFontStyle(const QString& value, QTextCharFormat* format) {
523 if(value == "normal")
524 format->setFontItalic(false);
525 else if(value == "italic")
526 format->setFontItalic(true);
527 else if(value == "underline")
528 format->setFontUnderline(true);
529 //else if(value == "oblique")
530 // format->setStyle(QFont::StyleOblique);
532 qWarning() << Q_FUNC_INFO << tr("Invalid font style specification: %1").arg(value);
536 void QssParser::parseFontWeight(const QString& value, QTextCharFormat* format) {
537 if(value == "normal")
538 format->setFontWeight(QFont::Normal);
539 else if(value == "bold")
540 format->setFontWeight(QFont::Bold);
543 int w = value.toInt(&ok);
545 qWarning() << Q_FUNC_INFO << tr("Invalid font weight specification: %1").arg(value);
548 format->setFontWeight(qMin(w / 8, 99)); // taken from Qt's qss parser
552 void QssParser::parseFontSize(const QString& value, QTextCharFormat* format) {
553 QRegExp rx("\\(d+)(pt|px)");
554 if(!rx.exactMatch(value)) {
555 qWarning() << Q_FUNC_INFO << tr("Invalid font size specification: %1").arg(value);
558 if(rx.cap(2) == "px")
559 format->setProperty(QTextFormat::FontPixelSize, rx.cap(1).toInt());
561 format->setFontPointSize(rx.cap(1).toInt());
564 void QssParser::parseFontFamily(const QString& value, QTextCharFormat* format) {
565 QString family = value;
566 if(family.startsWith('"') && family.endsWith('"')) {
567 family = family.mid(1, family.length() - 2);
569 format->setFontFamily(family);