aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard Moe Gustavsen <richard.gustavsen@qt.io>2023-11-01 12:59:14 +0100
committerRichard Moe Gustavsen <richard.gustavsen@qt.io>2023-11-06 11:51:44 +0100
commitd6b5289c7d88bf50d40aeb38f3f6ae6f9b28af5c (patch)
tree0cc44eae65a913825f168cd287169e702b81395d
parentd0d0d7435dd26fe1be126a603f810925e1e14183 (diff)
StyleGenerator: add support for theme icons
Generate the theme icons from the figma design. The icons are generated as theme icons, which means that the application will need to add the following line to main.cpp in order to use them: QIcon::setThemeName("NameOfStyle"); The icons can then be used in e.g Buttons by using the name of the icon: Button { icon.name: "navigation_settings" } The name used is the same name as the name of the variant in Figma, but with "_" between each category. Change-Id: I58110f0d74a95ae838271d28f56a2a912b82dac9 Reviewed-by: Doris Verria <doris.verria@qt.io>
-rw-r--r--tools/qqcstylegenerator/config.json31
-rw-r--r--tools/qqcstylegenerator/howto.txt8
-rw-r--r--tools/qqcstylegenerator/src/stylegenerator.h159
3 files changed, 185 insertions, 13 deletions
diff --git a/tools/qqcstylegenerator/config.json b/tools/qqcstylegenerator/config.json
index ea7300ed21..117e0b236b 100644
--- a/tools/qqcstylegenerator/config.json
+++ b/tools/qqcstylegenerator/config.json
@@ -6,11 +6,20 @@
{"atom": "background", "export": ["image"]},
{"atom": "contentItem", "export": ["layout"]},
{"atom": "indicator", "export": ["image"]},
- {"atom": "label", "export": ["text", "geometry"]}
+ {"atom": "label", "export": ["text", "geometry"]},
+ {"atom": "icon", "export": ["geometry"]}
],
"qml": {
"copy": [":/templates/*.qml", ":/templates/impl/*.qml"]
},
+ "icons": [
+ {
+ "name": "Icons",
+ "page": "Components",
+ "component set": "Icon",
+ "export": ["image"]
+ }
+ ],
"default controls": [
{
"name": "ApplicationWindow"
@@ -38,10 +47,12 @@
"name": "Button",
"page": "Components",
"component set": "ButtonTemplate${Theme}",
+ "contents": ["icon", "label"],
"atoms": [
{"atom": "background", "figmaPath": "Background"},
{"atom": "contentItem", "figmaPath": "Layout"},
- {"atom": "label", "figmaPath": "label"}
+ {"atom": "label", "figmaPath": "label"},
+ {"atom": "icon", "figmaPath": "Icon"}
],
"states": [
{"state": "normal", "figmaState": "normal"},
@@ -166,10 +177,12 @@
"name": "FlatButton",
"page": "Components",
"component set": "FlatButtonTemplate${Theme}",
+ "contents": ["icon", "label"],
"atoms": [
{"atom": "background", "figmaPath": "Background"},
{"atom": "contentItem", "figmaPath": "Layout"},
- {"atom": "label", "figmaPath": "label"}
+ {"atom": "label", "figmaPath": "label"},
+ {"atom": "icon", "figmaPath": "Icon"}
],
"states": [
{"state": "normal", "figmaState": "normal"},
@@ -328,10 +341,12 @@
"name": "RoundButton",
"page": "Components",
"component set": "RoundButtonTemplate${Theme}",
+ "contents": ["icon", "label"],
"atoms": [
{"atom": "background", "figmaPath": "Background"},
{"atom": "contentItem", "figmaPath": "Layout"},
- {"atom": "label", "figmaPath": "label"}
+ {"atom": "label", "figmaPath": "label"},
+ {"atom": "icon", "figmaPath": "Icon"}
],
"states": [
{"state": "normal", "figmaState": "normal"},
@@ -462,10 +477,12 @@
"name": "TabButton",
"page": "Components",
"component set": "TabButtonTemplate${Theme}",
+ "contents": ["icon", "label"],
"atoms": [
{"atom": "background", "figmaPath": "Background"},
{"atom": "contentItem", "figmaPath": "Layout"},
- {"atom": "label", "figmaPath": "label"}
+ {"atom": "label", "figmaPath": "label"},
+ {"atom": "icon", "figmaPath": "Icon"}
],
"states": [
{"state": "checked", "figmaState": "checked"},
@@ -532,10 +549,12 @@
"name": "ToolButton",
"page": "Components",
"component set": "ToolButtonTemplate${Theme}",
+ "contents": ["icon", "label"],
"atoms": [
{"atom": "background", "figmaPath": "Background"},
{"atom": "contentItem", "figmaPath": "Layout"},
- {"atom": "label", "figmaPath": "label"}
+ {"atom": "label", "figmaPath": "label"},
+ {"atom": "icon", "figmaPath": "Icon"}
],
"states": [
{"state": "checked", "figmaState": "checked"},
diff --git a/tools/qqcstylegenerator/howto.txt b/tools/qqcstylegenerator/howto.txt
index 30bd601188..31ffcce54d 100644
--- a/tools/qqcstylegenerator/howto.txt
+++ b/tools/qqcstylegenerator/howto.txt
@@ -13,3 +13,11 @@ If you want to use compile time selection of the style, import the style directl
<code>import "qrc:/qt/qml/@styleName@"</code>
+The style also contains theme icons. In order to use them, you need to add the following line to main.cpp:
+
+<code>QIcon::setThemeName("@styleName@");</code>
+
+You can then use the them from e.g a Button:
+
+<code>Button { icon.name: "navigation_settings" }</code>
+
diff --git a/tools/qqcstylegenerator/src/stylegenerator.h b/tools/qqcstylegenerator/src/stylegenerator.h
index 4d041928c9..ebbe44d04d 100644
--- a/tools/qqcstylegenerator/src/stylegenerator.h
+++ b/tools/qqcstylegenerator/src/stylegenerator.h
@@ -13,6 +13,8 @@
#include <QDir>
#include <QPixmap>
+#include <set>
+
#include "jsontools.h"
#include "bridge.h"
@@ -84,8 +86,10 @@ public:
if (!m_abort)
generateControls();
if (!m_abort)
+ generateIcons();
+ if (!m_abort)
downloadImages();
- progressTo(3);
+ progressTo(4);
progressLabel("Generating configuration files");
if (!m_abort)
generateConfiguration();
@@ -93,6 +97,8 @@ public:
generateQmlDir();
if (!m_abort)
generateQrcFile();
+ if (!m_abort)
+ generateIndexThemeFile();
} catch (std::exception &e) {
error(e.what());
}
@@ -540,6 +546,45 @@ private:
m_outputConfig[m_currentTheme].insert(controlNameModified, outputControlConfig);
}
+ void generateIcons()
+ {
+ // Note that we don't generate different icons per theme, since
+ // they will be colored with a shader in QML to follow the
+ // button icon color.
+ try {
+ QJsonArray iconGroupsArray = getArray("icons", m_inputConfig);
+ for (const auto iconGroupValue : iconGroupsArray) {
+ const QJsonObject iconGroupConfig = iconGroupValue.toObject();
+ const auto name = getString("name", iconGroupConfig);
+ progressLabel("Generating " + name);
+
+ QStringList exportList = getStringList("export", iconGroupConfig);
+ if (exportList.contains("image")) {
+ exportList.removeAll("image");
+ exportList += m_imageFormats;
+ }
+
+ const auto componentSetName = getThemeString("component set", iconGroupConfig);
+ const QJsonObject searchRoot = getComponentSearchRoot(iconGroupConfig);
+ const QJsonObject componentSet = getComponentSet(searchRoot, componentSetName);
+ const QString componentSetId = JsonTools::getString("id", componentSet);
+ const QString componentSetPath = JsonTools::resolvedPath(componentSetId);
+ debug("using component set: " + componentSetPath);
+
+ // All the children of the component represents an icon
+ const auto children = componentSet.value("children").toArray();
+ progressTo(children.count());
+
+ for (auto it = children.constBegin(); it != children.constEnd(); ++it) {
+ exportIcon(it->toObject(), exportList);
+ progress();
+ }
+ }
+ } catch (std::exception &e) {
+ warning("failed exporting icons: " + QString(e.what()));
+ }
+ }
+
QString generateQMLForJsonObject(const QJsonObject &object, const QString &objectName, QString &indent) {
QString qml;
@@ -707,6 +752,9 @@ private:
: imageName + '.' + imageFormat.format);
auto &figmaIdToFileNameMap = m_imagesToDownload[imageFormat.name];
+ if (figmaIdToFileNameMap.contains(figmaId))
+ warning("'" + figmaIdToFileNameMap[figmaId] + "' has the same figmaId '" + figmaId
+ + "' as '" + fileNameForWriting + "', and will be overwritten");
figmaIdToFileNameMap.insert(figmaId, fileNameForWriting);
m_imageCount++;
@@ -726,6 +774,46 @@ private:
exportBorderImageOffset(atom, outputConfig);
}
+ void exportIcon(const QJsonObject &iconObj, const QStringList &imageFormats)
+ {
+ const QString figmaId = getString("id", iconObj);
+ const QString figmaName = getString("name", iconObj);
+
+ QString imageName;
+ static QRegularExpression re(R"(Property.*=(.*))");
+ QRegularExpressionMatch match = re.match(figmaName);
+ if (match.hasMatch()) {
+ // The name might be a combination of many properties
+ QStringList propertyNames;
+ const auto properties = figmaName.split(',');
+ for (const auto &propertyName : properties) {
+ QRegularExpressionMatch match = re.match(propertyName);
+ propertyNames << match.captured(1);
+ }
+ imageName = propertyNames.join('_').toLower();
+ } else {
+ imageName = figmaName;
+ }
+ imageName.replace(' ', '_');
+ imageName.replace('-', '_');
+
+ for (const ImageFormat imageFormat : imageFormats) {
+ const QString imageFolder = "icons/icons"
+ + (imageFormat.hasScale ? + "@" + imageFormat.scale + "x" : "") + "/";
+ const QString fileName = imageFolder + imageName + "." + imageFormat.format;
+
+ auto &figmaIdToFileNameMap = m_imagesToDownload[imageFormat.name];
+ if (figmaIdToFileNameMap.contains(figmaId))
+ warning("'" + figmaIdToFileNameMap[figmaId] + "' has the same figmaId '" + figmaId
+ + "' as '" + fileName + "', and will be overwritten");
+ figmaIdToFileNameMap.insert(figmaId, fileName);
+ m_icons.insert(fileName);
+ m_imageCount++;
+
+ debug("exporting icon: " + fileName);
+ }
+ }
+
void exportJson(const QJsonObject &atom, QJsonObject &outputConfig)
{
const QString name = getString("name", outputConfig);
@@ -1008,12 +1096,14 @@ private:
{
debug("Generating Qt resource file");
const QString styleName = QFileInfo(m_bridge->m_targetDirectory).fileName();
- const QString targetPath = QFileInfo(m_bridge->m_targetDirectory).absolutePath();
+ const QString targetPath = QFileInfo(m_bridge->m_targetDirectory).absolutePath() + QDir::separator();
QString resources;
resources += "<RCC>\n";
- resources += "\t<qresource prefix=\"/qt/qml\">\n";
+ // Add the style into the prefix "/qt/qml", since this path is the
+ // default controls style search path, and therefore works out-of-the-box
+ resources += "\t<qresource prefix=\"/qt/qml\">\n";
QDirIterator it(styleName, QDirIterator::Subdirectories);
while (it.hasNext()) {
QString file = it.next();
@@ -1021,16 +1111,70 @@ private:
// QDir::NoDotAndDotDot
continue;
}
- resources += "\t\t<file alias=\"" + file + "\">"
- + targetPath + QDir::separator() + file
- + "</file>\n";
+ if (file.startsWith(styleName + "/icons/")) {
+ // icons go into a separate prefix below
+ continue;
+ }
+ resources += "\t\t<file alias=\"" + file + "\">" + targetPath + file + "</file>\n";
}
+ resources += "\t</qresource>\n";
+ // Add icons into the prefix "/icons", since this path is the
+ // default controls theme search path, and therefore works out-of-the-box
+ resources += "\t<qresource prefix=\"/icons\">\n";
+ resources += "\t\t<file alias=\"" + styleName + "/index.theme\">" + targetPath
+ + styleName + "/icons/index.theme</file>\n";
+
+ for (const QString &iconPath : m_icons) {
+ QString alias = iconPath;
+ alias.replace(QRegularExpression("^icons"), styleName);
+ resources += "\t\t<file alias=\"" + alias + "\">" + targetPath
+ + styleName + QDir::separator() + iconPath + "</file>\n";
+ }
resources += "\t</qresource>\n";
+
resources += "</RCC>\n";
+ createTextFileInStylefolder(styleName + ".qrc", resources);
+ progress();
+ }
+
+ void generateIndexThemeFile()
+ {
+ debug("Generating icons/index.theme");
+ const QString styleName = QFileInfo(m_bridge->m_targetDirectory).fileName();
+ const QString targetPath = QFileInfo(m_bridge->m_targetDirectory).absolutePath() + QDir::separator();
+ QString scaleDirectoriesConfig;
+ QStringList scaleDirectories;
+ QDirIterator it(styleName + QDir::separator() + "icons");
+ QRegularExpression reGetScale(R"(@(.*)x)");
- createTextFileInStylefolder(styleName + ".qrc", resources);
+ while (it.hasNext()) {
+ const QString file = it.next();
+ const QFileInfo fileInfo(file);
+ if (file.endsWith('.') || !fileInfo.isDir())
+ continue;
+
+ const QString directoryName = fileInfo.fileName();
+ scaleDirectories += directoryName;
+
+ auto scale = reGetScale.match(directoryName).captured(1);
+ if (scale.isEmpty())
+ scale = "1";
+
+ scaleDirectoriesConfig += "[" + directoryName + "]\n"
+ + "Scale=" + scale + "\n"
+ + "Size=32\n"
+ + "Type=Fixed\n\n";
+ }
+
+ const QString contents = QStringLiteral("[Icon Theme]\n")
+ + "Name=" + styleName + "\n"
+ + "Comment=Generated by Qt StyleGenerator\n\n"
+ + "Directories=" + scaleDirectories.join(',') + "\n\n"
+ + scaleDirectoriesConfig;
+
+ createTextFileInStylefolder("icons/index.theme", contents);
progress();
}
@@ -1210,6 +1354,7 @@ private:
QJsonDocument m_document;
QJsonObject m_inputConfig;
QMap<QString, QJsonObject> m_outputConfig;
+ std::set<QString> m_icons;
QStringList m_qmlDirControls;
QString m_cachedPageName;