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::loadStyleSheet(const QString &styleSheet) {
55 QString ss = styleSheet;
56 ss = "file:////home/sputnick/devel/quassel/test.qss"; // FIXME
57 if(ss.startsWith("file:///")) {
60 if(file.open(QFile::ReadOnly)) {
61 QTextStream stream(&file);
62 ss = stream.readAll();
64 qWarning() << tr("Could not read stylesheet \"%1\"!").arg(file.fileName());
71 // Now we have the stylesheet itself in ss, start parsing
72 // Palette definitions first, so we can apply roles later on
73 QRegExp paletterx("(Palette[^{]*)\\{([^}]+)\\}");
75 while((pos = paletterx.indexIn(ss, pos)) >= 0) {
76 parsePaletteData(paletterx.cap(1).trimmed(), paletterx.cap(2).trimmed());
77 pos += paletterx.matchedLength();
80 // Now we can parse the rest of our custom blocks
81 QRegExp blockrx("((?:ChatLine|BufferList|NickList|TreeView)[^{]*)\\{([^}]+)\\}");
83 while((pos = blockrx.indexIn(ss, pos)) >= 0) {
84 //qDebug() << blockrx.cap(1) << blockrx.cap(2);
86 if(blockrx.cap(2) == "ChatLine")
87 parseChatLineData(blockrx.cap(1).trimmed(), blockrx.cap(2).trimmed());
89 // TODO: add moar here
91 pos += blockrx.matchedLength();
96 void QssParser::parseChatLineData(const QString &decl, const QString &contents) {
97 quint64 fmtType = parseFormatType(decl);
98 if(fmtType == UiStyle::Invalid)
101 QTextCharFormat format;
103 foreach(QString line, contents.split(';', QString::SkipEmptyParts)) {
104 int idx = line.indexOf(':');
106 qWarning() << Q_FUNC_INFO << tr("Invalid property declaration: %1").arg(line.trimmed());
109 QString property = line.left(idx).trimmed();
110 QString value = line.mid(idx + 1).trimmed();
112 if(property == "background" || property == "background-color")
113 format.setBackground(parseBrushValue(value));
114 else if(property == "foreground" || property == "color")
115 format.setForeground(parseBrushValue(value));
118 qWarning() << Q_FUNC_INFO << tr("Unknown ChatLine property: %1").arg(property);
122 _formats[fmtType] = format;
125 quint64 QssParser::parseFormatType(const QString &decl) {
126 QRegExp rx("ChatLine(?:::(\\w+))?(?:#(\\w+))?(?:\\[([=-,\\\"\\w\\s]+)\\])?\\s*");
127 // $1: subelement; $2: msgtype; $3: conditionals
128 if(!rx.exactMatch(decl)) {
129 qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
130 return UiStyle::Invalid;
132 QString subElement = rx.cap(1);
133 QString msgType = rx.cap(2);
134 QString conditions = rx.cap(3);
138 // First determine the subelement
139 if(!subElement.isEmpty()) {
140 if(subElement == "Timestamp")
141 fmtType |= UiStyle::Timestamp;
142 else if(subElement == "Sender")
143 fmtType |= UiStyle::Sender;
144 else if(subElement == "Nick")
145 fmtType |= UiStyle::Nick;
146 else if(subElement == "Hostmask")
147 fmtType |= UiStyle::Hostmask;
148 else if(subElement == "ModeFlags")
149 fmtType |= UiStyle::ModeFlags;
151 qWarning() << Q_FUNC_INFO << tr("Invalid subelement name in %1").arg(decl);
152 return UiStyle::Invalid;
156 // Now, figure out the message type
157 if(!msgType.isEmpty()) {
158 if(msgType == "Plain")
159 fmtType |= UiStyle::PlainMsg;
160 else if(msgType == "Notice")
161 fmtType |= UiStyle::NoticeMsg;
162 else if(msgType == "Server")
163 fmtType |= UiStyle::ServerMsg;
164 else if(msgType == "Error")
165 fmtType |= UiStyle::ErrorMsg;
166 else if(msgType == "Join")
167 fmtType |= UiStyle::JoinMsg;
168 else if(msgType == "Part")
169 fmtType |= UiStyle::PartMsg;
170 else if(msgType == "Quit")
171 fmtType |= UiStyle::QuitMsg;
172 else if(msgType == "Kick")
173 fmtType |= UiStyle::KickMsg;
174 else if(msgType == "Rename")
175 fmtType |= UiStyle::RenameMsg;
176 else if(msgType == "Mode")
177 fmtType |= UiStyle::ModeMsg;
178 else if(msgType == "Action")
179 fmtType |= UiStyle::ActionMsg;
181 qWarning() << Q_FUNC_INFO << tr("Invalid message type in %1").arg(decl);
185 // Next up: conditional (formats, labels, nickhash)
186 QRegExp condRx("\\s*(\\w+)\\s*=\\s*\"(\\w+)\"\\s*");
187 if(!conditions.isEmpty()) {
188 foreach(const QString &cond, conditions.split(',', QString::SkipEmptyParts)) {
189 if(!condRx.exactMatch(cond)) {
190 qWarning() << Q_FUNC_INFO << tr("Invalid condition %1").arg(cond);
191 return UiStyle::Invalid;
193 QString condName = condRx.cap(1);
194 QString condValue = condRx.cap(2);
195 if(condName == "label") {
196 quint64 labeltype = 0;
197 if(condValue == "highlight")
198 labeltype = UiStyle::Highlight;
200 qWarning() << Q_FUNC_INFO << tr("Invalid message label: %1").arg(condValue);
201 return UiStyle::Invalid;
203 fmtType |= (labeltype << 32);
204 } else if(condName == "sender") {
205 if(condValue == "self")
206 fmtType |= (quint64)UiStyle::OwnMsg << 32; // sender="self" is actually treated as a label
209 quint64 val = condValue.toUInt(&ok, 16);
211 qWarning() << Q_FUNC_INFO << tr("Invalid senderhash specification: %1").arg(condValue);
212 return UiStyle::Invalid;
215 qWarning() << Q_FUNC_INFO << tr("Senderhash can be at most \"fe\"!");
216 return UiStyle::Invalid;
218 fmtType |= val << 48;
228 // Palette { ... } specifies the application palette
229 // ColorGroups can be specified like pseudo states, chaining is OR (contrary to normal CSS handling):
230 // Palette:inactive:disabled { ... } applies to both the Inactive and the Disabled state
231 void QssParser::parsePaletteData(const QString &decl, const QString &contents) {
232 QList<QPalette::ColorGroup> colorGroups;
234 // Check if we want to apply this palette definition for particular ColorGroups
235 QRegExp rx("Palette((:(normal|active|inactive|disabled))*)");
236 if(!rx.exactMatch(decl)) {
237 qWarning() << Q_FUNC_INFO << tr("Invalid block declaration: %1").arg(decl);
240 if(!rx.cap(1).isEmpty()) {
241 QStringList groups = rx.cap(1).split(':', QString::SkipEmptyParts);
242 foreach(QString g, groups) {
243 if((g == "normal" || g == "active") && !colorGroups.contains(QPalette::Active))
244 colorGroups.append(QPalette::Active);
245 else if(g == "inactive" && !colorGroups.contains(QPalette::Inactive))
246 colorGroups.append(QPalette::Inactive);
247 else if(g == "disabled" && !colorGroups.contains(QPalette::Disabled))
248 colorGroups.append(QPalette::Disabled);
252 // Now let's go through the roles
253 foreach(QString line, contents.split(';', QString::SkipEmptyParts)) {
254 int idx = line.indexOf(':');
256 qWarning() << Q_FUNC_INFO << tr("Invalid palette role assignment: %1").arg(line.trimmed());
259 QString rolestr = line.left(idx).trimmed();
260 QString brushstr = line.mid(idx + 1).trimmed();
261 if(!_paletteColorRoles.contains(rolestr)) {
262 qWarning() << Q_FUNC_INFO << tr("Unknown palette role name: %1").arg(rolestr);
265 QBrush brush = parseBrushValue(brushstr);
266 if(colorGroups.count()) {
267 foreach(QPalette::ColorGroup group, colorGroups)
268 _palette.setBrush(group, _paletteColorRoles.value(rolestr), brush);
270 _palette.setBrush(_paletteColorRoles.value(rolestr), brush);
274 QBrush QssParser::parseBrushValue(const QString &str, bool *ok) {
277 QColor c = parseColorValue(str);
284 if(str.startsWith("palette")) { // Palette color role
285 QRegExp rx("palette\\s*\\(\\s*([a-z-]+)\\s*\\)");
286 if(!rx.exactMatch(str)) {
287 qWarning() << Q_FUNC_INFO << tr("Invalid palette color role specification: %1").arg(str);
290 if(!_paletteColorRoles.contains(rx.cap(1))) {
291 qWarning() << Q_FUNC_INFO << tr("Unknown palette color role: %1").arg(rx.cap(1));
294 return QBrush(_palette.brush(_paletteColorRoles.value(rx.cap(1))));
296 } else if(str.startsWith("qlineargradient")) {
297 static QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
298 QRegExp rx(QString("qlineargradient\\s*\\(\\s*x1:%1,\\s*y1:%1,\\s*x2:%1,\\s*y2:%1,(.+)\\)").arg(rxFloat));
299 if(!rx.exactMatch(str)) {
300 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
303 qreal x1 = rx.cap(1).toDouble();
304 qreal y1 = rx.cap(2).toDouble();
305 qreal x2 = rx.cap(3).toDouble();
306 qreal y2 = rx.cap(4).toDouble();
307 QGradientStops stops = parseGradientStops(rx.cap(5).trimmed());
309 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
312 QLinearGradient gradient(x1, y1, x2, y2);
313 gradient.setStops(stops);
316 return QBrush(gradient);
318 } else if(str.startsWith("qconicalgradient")) {
319 static QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
320 QRegExp rx(QString("qconicalgradient\\s*\\(\\s*cx:%1,\\s*cy:%1,\\s*angle:%1,(.+)\\)").arg(rxFloat));
321 if(!rx.exactMatch(str)) {
322 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
325 qreal cx = rx.cap(1).toDouble();
326 qreal cy = rx.cap(2).toDouble();
327 qreal angle = rx.cap(3).toDouble();
328 QGradientStops stops = parseGradientStops(rx.cap(4).trimmed());
330 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
333 QConicalGradient gradient(cx, cy, angle);
334 gradient.setStops(stops);
337 return QBrush(gradient);
339 } else if(str.startsWith("qradialgradient")) {
340 static QString rxFloat("\\s*(-?\\s*[0-9]*\\.?[0-9]+)\\s*");
341 QRegExp rx(QString("qradialgradient\\s*\\(\\s*cx:%1,\\s*cy:%1,\\s*radius:%1,\\s*fx:%1,\\s*fy:%1,(.+)\\)").arg(rxFloat));
342 if(!rx.exactMatch(str)) {
343 qWarning() << Q_FUNC_INFO << tr("Invalid gradient declaration: %1").arg(str);
346 qreal cx = rx.cap(1).toDouble();
347 qreal cy = rx.cap(2).toDouble();
348 qreal radius = rx.cap(3).toDouble();
349 qreal fx = rx.cap(4).toDouble();
350 qreal fy = rx.cap(5).toDouble();
351 QGradientStops stops = parseGradientStops(rx.cap(6).trimmed());
353 qWarning() << Q_FUNC_INFO << tr("Invalid gradient stops list: %1").arg(str);
356 QRadialGradient gradient(cx, cy, radius, fx, fy);
357 gradient.setStops(stops);
360 return QBrush(gradient);
366 QColor QssParser::parseColorValue(const QString &str) {
367 if(str.startsWith("rgba")) {
368 ColorTuple tuple = parseColorTuple(str.mid(4));
369 if(tuple.count() == 4)
370 return QColor(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
371 } else if(str.startsWith("rgb")) {
372 ColorTuple tuple = parseColorTuple(str.mid(3));
373 if(tuple.count() == 3)
374 return QColor(tuple.at(0), tuple.at(1), tuple.at(2));
375 } else if(str.startsWith("hsva")) {
376 ColorTuple tuple = parseColorTuple(str.mid(4));
377 if(tuple.count() == 4) {
379 c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2), tuple.at(3));
382 } else if(str.startsWith("hsv")) {
383 ColorTuple tuple = parseColorTuple(str.mid(3));
384 if(tuple.count() == 3) {
386 c.setHsvF(tuple.at(0), tuple.at(1), tuple.at(2));
390 QRegExp rx("#?[0-9A-Fa-z]+");
391 if(rx.exactMatch(str))
397 // get a list of comma-separated int values or percentages (rel to 0-255)
398 QssParser::ColorTuple QssParser::parseColorTuple(const QString &str) {
400 QRegExp rx("\\(((\\s*[0-9]{1,3}%?\\s*)(,\\s*[0-9]{1,3}%?\\s*)*)\\)");
401 if(!rx.exactMatch(str.trimmed())) {
404 QStringList values = rx.cap(1).split(',');
405 foreach(QString v, values) {
410 if(v.endsWith('%')) {
414 val = (qreal)v.toUInt(&ok);
424 QGradientStops QssParser::parseGradientStops(const QString &str_) {
426 QGradientStops result;
427 static QString rxFloat("(0?\\.[0-9]+|[01])"); // values between 0 and 1
428 QRegExp rx(QString("\\s*,?\\s*stop:\\s*(%1)\\s+([^:]+)(,\\s*stop:|$)").arg(rxFloat));
430 while((idx = rx.indexIn(str)) == 0) {
431 qreal x = rx.cap(1).toDouble();
432 QColor c = parseColorValue(rx.cap(3));
434 return QGradientStops();
435 result << QGradientStop(x, c);
436 str.remove(0, rx.matchedLength() - rx.cap(4).length());
438 if(!str.trimmed().isEmpty())
439 return QGradientStops();