#include "runtimeqml.h" #include #include #include #include /*! * \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& RuntimeQML::prefixIgnoreList() const { return m_prefixIgnoreList; } /*! * \brief Files that are ignored. */ const QList &RuntimeQML::fileIgnoreList() const { return m_fileIgnoreList; } /*! * \brief Allowed suffixes to filter files to watch for changes. * By default contains only "qml". */ const QList &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(); for (int i {0}; i < allWindows.size(); ++i) { QQuickWindow* w = qobject_cast(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(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; } }