You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Framework/AzQtComponents/AzQtComponents/Components/StyleSheetCache.cpp

437 lines
13 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzQtComponents/Components/StyleSheetCache.h>
#include <AzQtComponents/Utilities/QtPluginPaths.h>
#include <AzCore/Debug/Trace.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
#include <QProxyStyle>
#include <QWidget>
#include <QDir>
#include <QFile>
#include <QFileSystemWatcher>
#include <QStringList>
#include <QRegExp>
#include <QDebug>
#include <QApplication>
#include <QQueue>
namespace AzQtComponents
{
StyleSheetCache::StyleSheetCache(QObject* parent)
: QObject(parent)
, m_fileWatcher(new QFileSystemWatcher(this))
, m_importExpression(new QRegExp("^\\s*@import\\s+\"?([^\"]+)\"?\\s*;(.*)$"))
{
connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &StyleSheetCache::fileOnDiskChanged);
}
StyleSheetCache::~StyleSheetCache()
{
}
const QString& StyleSheetCache::styleSheetExtension()
{
static const QString extension{QStringLiteral(".qss")};
return extension;
}
void StyleSheetCache::registerPathsFoundOnDisk(const QString& pathOnDisk, const QString& qrcPrefix)
{
// Do a sanity check to ensure there are no style-sheets on disk that don't exist in a qrc
QDir rootDirectory(pathOnDisk);
QQueue<QFileInfo> entriesToScan;
entriesToScan.push_back(QFileInfo(pathOnDisk));
while (!entriesToScan.empty())
{
QFileInfo entry = entriesToScan.front();
entriesToScan.pop_front();
if (!entry.exists())
{
continue;
}
if (entry.isDir())
{
for (auto subEntry : QDir(entry.absoluteFilePath()).entryInfoList({"*.qss"}, QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files))
{
entriesToScan.push_back(subEntry);
}
}
else
{
QString diskPath = entry.absoluteFilePath();
QString qrcPath = QStringLiteral("%1/%2")
.arg(qrcPrefix)
.arg(rootDirectory.relativeFilePath(diskPath));
QFileInfo qrcInfo(qrcPath);
if (qrcInfo.exists())
{
m_diskToQrcMap[diskPath] = qrcPath;
}
else
{
AZ_Warning("StyleSheetCache", false,
"No QRC entry found for style sheet found on disk. Disk path: \"%s\" Expected QRC path: \"%s\"",
diskPath.toUtf8().constData(), qrcPath.toUtf8().constData());
}
}
}
}
void StyleSheetCache::addSearchPaths(const QString& searchPrefix, const QString& pathOnDisk, const QString& qrcPrefix,
const AZ::IO::PathView& engineRootPath)
{
AZ_Warning("StyleSheetCache", m_prefixes.find(searchPrefix) == m_prefixes.end(),
"Style prefix \"%s\" was already registered, ignoring...", searchPrefix.constData());
// If pathOnDisk is a relative path, search from the engine root directory
QString diskPathToUse = pathOnDisk;
if (!QFileInfo(pathOnDisk).isAbsolute())
{
QDir rootDir = QString::fromUtf8(engineRootPath.Native().data(), aznumeric_cast<int>(engineRootPath.Native().size()));
diskPathToUse = rootDir.absoluteFilePath(pathOnDisk);
}
registerPathsFoundOnDisk(diskPathToUse, qrcPrefix);
// Specifying the path to the file on disk and the qrc prefix of the file in this order means
// that the style will be loaded from disk if it exists, otherwise the style in the Qt Resource
// file will be used. Styles loaded from disk will be automatically watched and re-applied if
// changes are detected, allowing much faster style iteration.
m_prefixes.insert(searchPrefix);
QDir::addSearchPath(searchPrefix, diskPathToUse);
QDir::addSearchPath(searchPrefix, qrcPrefix);
}
void StyleSheetCache::setFallbackSearchPaths(const QString& fallbackPrefix, const QString& pathOnDisk, const QString& qrcPrefix)
{
if (m_fallbackPrefix == fallbackPrefix)
{
return;
}
if (!m_fallbackPrefix.isEmpty())
{
QDir::setSearchPaths(m_fallbackPrefix, {});
}
m_fallbackPrefix = fallbackPrefix;
if (m_fallbackPrefix.isEmpty())
{
return;
}
registerPathsFoundOnDisk(pathOnDisk, qrcPrefix);
QDir::setSearchPaths(m_fallbackPrefix, {pathOnDisk, qrcPrefix});
}
void StyleSheetCache::clearCache()
{
m_styleSheetCache.clear();
}
void StyleSheetCache::fileOnDiskChanged(const QString& filePath)
{
qDebug() << "All styleSheets reloading, triggered by " << filePath << " changing on disk.";
// Much easier to just reload all stylesheets, instead of trying to figure out
// which ones are affected by this file.
// If we were to worry about just one file, we'd need to keep track of dependency
// info when preprocessing @import's
clearCache();
emit styleSheetsChanged();
}
QString StyleSheetCache::loadStyleSheet(QString styleFileName)
{
// include the file extension here; it'll make life easier when comparing file paths
if (!styleFileName.endsWith(styleSheetExtension()))
{
styleFileName.append(styleSheetExtension());
}
// check the cache
if (m_styleSheetCache.contains(styleFileName))
{
return m_styleSheetCache[styleFileName];
}
QString filePath = findStyleSheetPath(styleFileName);
if (filePath.isEmpty())
{
return QString();
}
QFileInfo filePathInfo(filePath);
// watch this file for changes now, if it's not loaded from resources
if (filePathInfo.exists() && filePathInfo.isNativePath())
{
m_fileWatcher->addPath(filePath);
QString absolutePath = filePathInfo.absoluteFilePath();
if (m_diskToQrcMap.find(absolutePath) == m_diskToQrcMap.end())
{
AZ_Error("StyleSheetCache", false, "No QRC entry was found for style-sheet loaded from disk, loading has been disabled: %s", absolutePath.toUtf8().constData());
return {};
}
}
QString loadedStyleSheet;
if (QFile::exists(filePath))
{
QFile styleSheetFile;
styleSheetFile.setFileName(filePath);
if (styleSheetFile.open(QFile::ReadOnly))
{
loadedStyleSheet = styleSheetFile.readAll();
}
}
// pre-process stylesheet
loadedStyleSheet = preprocess(styleFileName, loadedStyleSheet);
m_styleSheetCache.insert(styleFileName, loadedStyleSheet);
return loadedStyleSheet;
}
class MiniLessParser
{
public:
MiniLessParser(StyleSheetCache* cache);
QString process(const QString& styleSheet);
private:
enum class State
{
Default,
Comment,
};
void parseLine(const QString& line);
int parseForImportStatement(const QString& line);
State m_state = State::Default;
QRegExp m_importStatement;
QChar m_lastCharacter;
QString m_result;
int m_lineNumber = 0;
StyleSheetCache* m_cache = nullptr;
};
MiniLessParser::MiniLessParser(StyleSheetCache* cache)
: m_importStatement("@import\\s+\"?([^\"]+)\"?\\s*;")
, m_cache(cache)
{
}
QString MiniLessParser::process(const QString& styleSheet)
{
m_state = State::Default;
m_result.reserve(styleSheet.size());
m_lineNumber = 0;
m_lastCharacter = QChar();
QStringList lines = styleSheet.split(QRegExp("[\\n\\r]"), Qt::SkipEmptyParts);
for (QString& line : lines)
{
parseLine(line);
m_lineNumber++;
}
return m_result;
}
void MiniLessParser::parseLine(const QString& line)
{
for (int i = 0; i < line.size(); i++)
{
QChar currentChar = line[i];
switch (m_state)
{
case State::Default:
{
if (currentChar == '@')
{
// parse for import
i += parseForImportStatement(line.mid(i));
currentChar = QChar();
}
else
{
if ((m_lastCharacter == '/') && (currentChar == '*'))
{
m_state = State::Comment;
}
m_result += currentChar;
}
}
break;
case State::Comment:
{
if ((m_lastCharacter == '*') && (currentChar == '/'))
{
m_state = State::Default;
}
m_result += currentChar;
}
break;
}
m_lastCharacter = currentChar;
}
m_result += "\n";
// reset our state
m_lastCharacter = QChar();
m_state = State::Default;
}
int MiniLessParser::parseForImportStatement(const QString& line)
{
QString importName;
int ret = 0;
int pos = m_importStatement.indexIn(line, 0);
if (pos != -1)
{
importName = m_importStatement.cap(1);
ret = m_importStatement.cap(0).size();
if (!importName.isEmpty())
{
// attempt to import
QString subStyleSheet = m_cache->loadStyleSheet(importName);
if (!subStyleSheet.isEmpty())
{
// error?
}
m_result += subStyleSheet;
}
}
return ret;
}
QString StyleSheetCache::preprocess(QString styleFileName, QString loadedStyleSheet)
{
// Add in really basic support in here for less style @import statements
// This allows us to split up css chunks into other files, to group things
// in a much saner way
// check for dumb recursion
if (m_processingFiles.contains(styleFileName))
{
qDebug() << QString("Recursion found while processing styleSheets in the following order:");
for (QString& file : m_processingStack)
{
qDebug() << file;
}
return loadedStyleSheet;
}
m_processingStack.push_back(styleFileName);
m_processingFiles.insert(styleFileName);
// take a guess at the size of the string needed with imports
QString result;
result.reserve(loadedStyleSheet.size() * 3);
// run our mini less parser on the stylesheet, to process imports now
MiniLessParser lessParser(this);
result = lessParser.process(loadedStyleSheet);
m_processingStack.pop_back();
m_processingFiles.remove(styleFileName);
return result;
}
QString StyleSheetCache::findStyleSheetPath(const QString& styleFileName)
{
if (styleFileName.contains(':'))
{
// The file name is in a resource file (":/style.qss")
// or already has a prefix ("prefix:style.qss")
// or an absolute path on Windows
return styleFileName;
}
if (QFile::exists(styleFileName))
{
return styleFileName;
}
const auto stackSize = m_processingStack.size();
if (stackSize > 0)
{
// Resursively search ancestors of the file currently being processed
const auto ancestorStyleFileName = m_processingStack.pop();
const auto ancestorFilePath = findStyleSheetPath(ancestorStyleFileName);
m_processingStack.push(ancestorStyleFileName);
if (QFile::exists(ancestorFilePath))
{
auto prefix = ancestorFilePath.mid(0, ancestorFilePath.indexOf(':'));
if (ancestorFilePath.contains(':') && prefix.length() != 1)
{
// QDir::isAbsolutePath returns true for paths with a valid search prefix. The
// only way to distinguish between a prefix and absolute path on Windows is the
// length of the prefix. Prefix length must be at least two to avoid conflicting
// with Windows drive letters. However, files in Qt Resources files have a prefix
// length of zero.
const auto result = QString("%1:%2").arg(prefix, styleFileName);
if (QFile::exists(result))
{
return result;
}
}
else
{
// Assume the styleFileName is relative to the ancestorFilePath
const QFileInfo ancestorFileInfo(ancestorFilePath);
const auto result = ancestorFileInfo.absoluteDir().absoluteFilePath(styleFileName);
if (QFile::exists(result))
{
return result;
}
}
}
}
// If we didn't find it in the processing stack, search all known prefixes
for (const auto& prefix : m_prefixes)
{
const auto result = QString("%1:%2").arg(prefix, styleFileName);
if (QFile::exists(result))
{
return result;
}
}
// Finally, fall back to m_fallbackPrefix
return m_fallbackPrefix.isEmpty() ? QString() : QString("%1:%2").arg(m_fallbackPrefix, styleFileName);
}
} // namespace AzQtComponents
#include "Components/moc_StyleSheetCache.cpp"