1
0
Fork 0
VideoPlayer/runtimeqml/runtimeqml.cpp
2018-10-27 16:11:29 +01:00

399 lines
10 KiB
C++

#include "runtimeqml.h"
#include <QXmlStreamReader>
#include <QFileInfo>
#include <QRegExp>
#include <QTimer>
/*!
* \brief Construct a RuntimeQML object with a path to the qrc file.
* \param engine App engine to reload.
* \param qrcFilename File name of the QRC file for auto reload.
* \param parent Pointer to a parent object.
*/
RuntimeQML::RuntimeQML(QQmlApplicationEngine* engine, const QString &qrcFilename, QObject *parent) :
QObject(parent),
m_engine(engine),
m_qrcFilename(qrcFilename),
m_mainQmlFilename("main.qml")
{
m_allowedSuffixList << "qml";
}
/*!
* \brief Returns the absolute path for the given qml file.
* \param qmlFile Qml filename
*/
QString RuntimeQML::adjustPath(QString qmlFile)
{
return m_selector.select(qrcAbsolutePath() + "/" + qmlFile);
}
/*!
* \brief Returns the absolute path to the QRC file.
*/
QString RuntimeQML::qrcAbsolutePath() const
{
return QFileInfo(m_qrcFilename).absolutePath();
}
/*!
* \brief Filename of the QRC file.
*/
QString RuntimeQML::qrcFilename() const
{
return m_qrcFilename;
}
/*!
* \brief If true, files are watched for changes and auto-reloaded.
* Otherwise, you need to trigger a reload manually from your code by calling reload().
* \sa reload
*/
bool RuntimeQML::autoReload() const
{
return m_autoReload;
}
/*!
* \brief If true, all open windows will be closed upon reload.
* \default true
*/
bool RuntimeQML::closeAllOnReload() const
{
return m_closeAllOnReload;
}
/*!
* \brief QRC prefixes that are ignored.
*/
const QList<QString>& RuntimeQML::prefixIgnoreList() const
{
return m_prefixIgnoreList;
}
/*!
* \brief Files that are ignored.
*/
const QList<QString> &RuntimeQML::fileIgnoreList() const
{
return m_fileIgnoreList;
}
/*!
* \brief Allowed suffixes to filter files to watch for changes.
* By default contains only "qml".
*/
const QList<QString> &RuntimeQML::allowedSuffixes() const
{
return m_allowedSuffixList;
}
/*!
* \brief Call it if you don't want debug outputs from this class.
*/
void RuntimeQML::noDebug()
{
if (m_noDebug)
return;
m_noDebug = true;
}
/*!
* \brief Reload the window.
*/
void RuntimeQML::reload()
{
QMetaObject::invokeMethod(this, "reloadQml", Qt::QueuedConnection);
}
/*!
* \brief Call it from QML to set the current QQuickWindow.
* You shouldn't need to call it as it is done automatically on reload.
* \param window
*/
void RuntimeQML::setWindow(QQuickWindow* window)
{
if (window == m_window)
return;
m_window = window;
}
/*!
* \brief Set the QRC filename for files to watch for changes.
* \param qrcFilename Path to a .qrc file.
*/
void RuntimeQML::setQrcFilename(QString qrcFilename)
{
if (m_qrcFilename == qrcFilename)
return;
m_qrcFilename = qrcFilename;
emit qrcFilenameChanged(qrcFilename);
loadQrcFiles();
}
/*!
* \brief Set the name of the main qml file.
* Default is "main.qml".
* \param filename The main qml filename.
*/
void RuntimeQML::setMainQmlFilename(QString filename)
{
if (m_mainQmlFilename == filename)
return;
m_mainQmlFilename = filename;
}
/*!
* \brief If true, files are watched for changes and auto-reloaded.
* Otherwise, you need to trigger a reload manually from your code by calling reload().
* \param reload True to auto-reload, false otherwise.
*/
void RuntimeQML::setAutoReload(bool autoReload)
{
if (m_autoReload == autoReload)
return;
m_autoReload = autoReload;
emit autoReloadChanged(autoReload);
if (autoReload)
loadQrcFiles();
else
unloadFileWatcher();
}
/*!
* \brief If true, all open windows are closed upon reload. Otherwise, might cause "link" errors with QML components.
* \param closeAllOnReload True to close all windows on reload, false otherwise.
*/
void RuntimeQML::setCloseAllOnReload(bool closeAllOnReload)
{
if (m_closeAllOnReload == closeAllOnReload)
return;
m_closeAllOnReload = closeAllOnReload;
emit closeAllOnReloadChanged(m_closeAllOnReload);
}
/*!
* \brief Add a QRC prefix to ignore.
* \note Relevant for auto-reload only.
* \param prefix Prefix to ignore.
*/
void RuntimeQML::ignoreQrcPrefix(const QString& prefix)
{
if (m_prefixIgnoreList.contains(prefix))
return;
m_prefixIgnoreList.append(prefix);
if (m_autoReload)
loadQrcFiles();
}
/*!
* \brief Add a filename to ignore from changes.
* Applies to the full filename in the QRC entry (i.e. the local "path"), with the prefix.
* Supports "file globbing" matching using wildcards.
* \note Relevant for auto-reload only.
* \param filename Filename to ignore.
*/
void RuntimeQML::ignoreFile(const QString &filename)
{
if (m_fileIgnoreList.contains(filename))
return;
m_fileIgnoreList.append(filename);
if (m_autoReload)
loadQrcFiles();
}
/*!
* \brief Allow a file suffix to be watched for changes.
* \note Relevant for auto-reload only.
* \param suffix
*/
void RuntimeQML::addSuffix(const QString &suffix)
{
if (m_allowedSuffixList.contains(suffix))
return;
m_allowedSuffixList.append(suffix);
if (m_autoReload)
loadQrcFiles();
}
/*!
* \brief Reload the QML. Do not call it directly, use reload() instead.
*/
void RuntimeQML::reloadQml()
{
if (m_mainQmlFilename.isEmpty()) {
qWarning("No QML file specified.");
return;
}
if (m_window) {
if (m_closeAllOnReload) {
// Find all child windows and close them
auto const allWindows = m_window->findChildren<QQuickWindow*>();
for (int i {0}; i < allWindows.size(); ++i) {
QQuickWindow* w = qobject_cast<QQuickWindow*>(allWindows.at(i));
if (w) {
w->close();
w->deleteLater();
}
}
}
m_window->close();
m_window->deleteLater();
}
m_engine->clearComponentCache();
// TODO: test with network files
// TODO: QString path to QUrl doesn't work under Windows with load() (load fail)
m_engine->load(m_selector.select(qrcAbsolutePath() + "/" + m_mainQmlFilename));
// NOTE: QQmlApplicationEngine::rootObjects() isn't cleared, should it be?
if (!m_engine->rootObjects().isEmpty()) {
QQuickWindow* w = qobject_cast<QQuickWindow*>(m_engine->rootObjects().last());
if (w) m_window = w;
}
// for (auto *o : m_engine->rootObjects()) {
// qDebug() << "> " << o;
// }
}
/*!
* \brief Called when a watched file changed, from QFileSystemWatcher.
* \param path Path/file that triggered the signal.
*/
void RuntimeQML::fileChanged(const QString& path)
{
if (!m_noDebug)
qDebug() << "Reloading qml:" << path;
reload();
#if defined(Q_OS_WIN)
// Deleted files are removed from the watcher, re-add the file for
// systems that delete files to update them
if (m_fileWatcher)
QTimer::singleShot(500, m_fileWatcher, [this,path](){
m_fileWatcher->addPath(path);
});
#endif
}
/*!
* \brief Load qml from the QRC file to watch them.
*/
void RuntimeQML::loadQrcFiles()
{
unloadFileWatcher();
m_fileWatcher = new QFileSystemWatcher(this);
connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &RuntimeQML::fileChanged);
QFile file(m_qrcFilename);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning("Unable to open resource file '%s', RuntimeQML will not work! Error: %s",
qPrintable(m_qrcFilename), qPrintable(file.errorString()));
return;
}
QString const basePath = qrcAbsolutePath() + "/";
QString currentPrefix;
// Read each entry
QXmlStreamReader inputStream(&file);
while (!inputStream.atEnd() && !inputStream.hasError()) {
inputStream.readNext();
if (inputStream.isStartElement()) {
QString name { inputStream.name().toString() };
// Check prefix
if (name == "qresource") {
if (inputStream.attributes().hasAttribute("prefix")) {
auto p = inputStream.attributes().value("prefix").toString();
if (m_prefixIgnoreList.contains(p)) {
// Ignore this prefix, loop through elements in this 'qresource' tag
while (!inputStream.atEnd() && !inputStream.hasError()) {
inputStream.readNext();
if (inputStream.isEndElement() && inputStream.name() == "qresource")
break;
}
continue;
}
currentPrefix = p;
}
}
// Check file name
if (name == "file") {
QString const filename { inputStream.readElementText() };
// Check ignore list
QString const fullFilename { currentPrefix + filename };
auto it = std::find_if(m_fileIgnoreList.cbegin(), m_fileIgnoreList.cend(), [&](QString const& pattern) {
QRegExp re(pattern);
re.setPatternSyntax(QRegExp::WildcardUnix);
return re.exactMatch(fullFilename);
});
if (it != m_fileIgnoreList.cend())
continue;
QFileInfo const file { filename };
// Add to the watch list if the file suffix is allowed
if (m_allowedSuffixList.contains(file.suffix())) {
QString fp { m_selector.select(basePath + filename) };
m_fileWatcher->addPath(fp);
//qDebug() << " " << file.absoluteFilePath() << fp;
}
}
}
}
if (!m_noDebug) {
qDebug("Watching QML files:");
int const fileCount = m_fileWatcher->files().size();
for (auto &f : m_fileWatcher->files()) {
qDebug() << " " << f;
}
if (fileCount > 0)
qDebug(" Total: %d", fileCount);
else
qDebug(" None.");
}
}
/*!
* \brief Unload the file watcher.
*/
void RuntimeQML::unloadFileWatcher()
{
if (m_fileWatcher) {
disconnect(m_fileWatcher);
delete m_fileWatcher;
m_fileWatcher = nullptr;
}
}