From d6b5289c7d88bf50d40aeb38f3f6ae6f9b28af5c Mon Sep 17 00:00:00 2001 From: Richard Moe Gustavsen Date: Wed, 1 Nov 2023 12:59:14 +0100 Subject: 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 --- tools/qqcstylegenerator/config.json | 31 +++++- tools/qqcstylegenerator/howto.txt | 8 ++ tools/qqcstylegenerator/src/stylegenerator.h | 159 +++++++++++++++++++++++++-- 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 import "qrc:/qt/qml/@styleName@" +The style also contains theme icons. In order to use them, you need to add the following line to main.cpp: + +QIcon::setThemeName("@styleName@"); + +You can then use the them from e.g a Button: + +Button { icon.name: "navigation_settings" } + 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 #include +#include + #include "jsontools.h" #include "bridge.h" @@ -83,9 +85,11 @@ public: copyFiles(); 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 += "\n"; - resources += "\t\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\n"; QDirIterator it(styleName, QDirIterator::Subdirectories); while (it.hasNext()) { QString file = it.next(); @@ -1021,16 +1111,70 @@ private: // QDir::NoDotAndDotDot continue; } - resources += "\t\t" - + targetPath + QDir::separator() + file - + "\n"; + if (file.startsWith(styleName + "/icons/")) { + // icons go into a separate prefix below + continue; + } + resources += "\t\t" + targetPath + file + "\n"; } + resources += "\t\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\n"; + resources += "\t\t" + targetPath + + styleName + "/icons/index.theme\n"; + + for (const QString &iconPath : m_icons) { + QString alias = iconPath; + alias.replace(QRegularExpression("^icons"), styleName); + resources += "\t\t" + targetPath + + styleName + QDir::separator() + iconPath + "\n"; + } resources += "\t\n"; + resources += "\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 m_outputConfig; + std::set m_icons; QStringList m_qmlDirControls; QString m_cachedPageName; -- cgit v1.2.3