1
0
Fork 0

[Dev] Added QML reloading.

This commit is contained in:
Kitteh 2018-10-27 16:11:29 +01:00
parent 9a119cb694
commit f3b3fc85ac
15 changed files with 757 additions and 16 deletions

1
.gitignore vendored
View file

@ -11,3 +11,4 @@ AppDir
Makefile
linuxdeploy*
other_libs
sffmpeg

37
CMakeLists.txt Normal file
View file

@ -0,0 +1,37 @@
cmake_minimum_required(VERSION 3.1.0)
project(KittehPlayer)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror -fstrict-aliasing -Wno-deprecated-declarations -Wno-unused-variable")
option(DEVELOP "Enable runtime QML reloading for developing." OFF)
find_package(Qt5Core REQUIRED)
find_package(Qt5 REQUIRED Qml Quick Gui Widgets Core)
find_package(Qt5QuickCompiler)
qtquick_compiler_add_resources(qml_QRC src/qml/qml.qrc)
set(SOURCES
src/main.cpp
src/mpvobject.cpp
)
if(DEVELOP)
set(SOURCES ${SOURCES} runtimeqml/runtimeqml.cpp)
add_definitions(-DQRC_SOURCE_PATH="${PROJECT_SOURCE_DIR}/src/qml")
endif(DEVELOP)
add_executable(KittehPlayer ${SOURCES} ${qml_QRC})
# Use the Qml/Quick modules from Qt 5.
target_link_libraries(KittehPlayer mpv)
qt5_use_modules(KittehPlayer Qml Quick Core Gui Widgets)

75
runtimeqml/.gitignore vendored Normal file
View file

@ -0,0 +1,75 @@
# This file is used to ignore files which are generated
# ----------------------------------------------------------------------------
*~
*.autosave
*.a
*.core
*.moc
*.o
*.obj
*.orig
*.rej
*.so
*.so.*
*_pch.h.cpp
*_resource.rc
*.qm
.#*
*.*#
core
!core/
tags
.DS_Store
.directory
*.debug
Makefile*
*.prl
*.app
moc_*.cpp
ui_*.h
qrc_*.cpp
Thumbs.db
*.res
*.rc
/.qmake.cache
/.qmake.stash
# qtcreator generated files
*.pro.user*
# qtcreator builds of test project
build-RuntimeQML-*
# xemacs temporary files
*.flc
# Vim temporary files
.*.swp
# Visual Studio generated files
*.ib_pdb_index
*.idb
*.ilk
*.pdb
*.sln
*.suo
*.vcproj
*vcproj.*.*.user
*.ncb
*.sdf
*.opensdf
*.vcxproj
*vcxproj.*
# MinGW generated files
*.Debug
*.Release
# Python byte code
*.pyc
# Binaries
# --------
*.dll
*.exe

24
runtimeqml/LICENSE Normal file
View file

@ -0,0 +1,24 @@
Copyright (c) 2018, Benjamin Balga
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

87
runtimeqml/README.md Normal file
View file

@ -0,0 +1,87 @@
# Runtime QML for Qt
**Written by**: *Benjamin Balga.*
**Copyright**: ***2018***, *Benjamin Balga*, released under BSD license.
## About
This is a base project to get runtime QML reload in your Qt project.
It allows you to reload all QML code at runtime, without recompiling or restarting your app, saving time.
With auto-reload, QML files are watched (based on the QRC file) and reloaded when you save them, or can trigger it manually.
On reload, all windows are closed, and the main window is reloaded. All "QML-only data" is lost, so use links to C++ models/properties as needed.
It only works with Window component as top object, or QQuickWindow subclasses.
### Examples
Example project is located here: https://github.com/GIPdA/runtimeqml_examples
## How to use it in your project
Clone the repo into your project (or copy-paste the ```runtimeqml``` folder) and import the ```.pri``` project file into your ```.pro``` file:
include(runtimeqml/runtimeqml.pri)
### With Qbs
The Qbs project file includes RuntimeQML as a static library. Check the example project to see how to include it in your project.
## Usage
Include ```runtimeqml.h``` header file, and create the RuntimeQML object (after the QML engine) :
RuntimeQML *rt = new RuntimeQML(&engine, QRC_SOURCE_PATH"/qml.qrc");
The second argument is the path to your qrc file listing all your QML files, needed for the auto-reload feature only. You can omit it if you don't want auto-reload.
```QRC_SOURCE_PATH``` is defined in the ```.pri/.qbs``` file to its parent path, just to not have to manually set an absolute path...
Set the "options" you want, or not:
rt->noDebug(); // Removes debug prints
rt->setAutoReload(true); // Enable auto-reload (begin to watch files)
//rt->setCloseAllOnReload(false); // Don't close all windows on reload. Not advised!
rt->setMainQmlFilename("main.qml"); // This is the file that loaded on reload, default is "main.qml"
For the auto-reload feature:
rt->addSuffix("conf"); // Adds a file suffix to the "white list" for watched files. "qml" is already in.
rt->ignorePrefix("/test"); // Ignore a prefix in the QRC file.
rt->ignoreFile("/Page2.qml"); // Ignore a file name with prefix (supports classic wildcard matching)
Then load the main QML file :
rt->reload();
And you're all set!
You can also check the test project. Beware, includes and defines differs a bit...
## Manual reload
Add the RuntimeQML object to the QML context:
engine.rootContext()->setContextProperty("RuntimeQML", rt);
Trigger the reload when and where you want, like with a button:
Button {
text: "Reload"
onClicked: {
RuntimeQML.reload();
}
}
You can do it in C++ too, of course.
## License
See LICENSE file.

398
runtimeqml/runtimeqml.cpp Normal file
View file

@ -0,0 +1,398 @@
#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;
}
}

82
runtimeqml/runtimeqml.h Normal file
View file

@ -0,0 +1,82 @@
#ifndef RUNTIMEQML_H
#define RUNTIMEQML_H
#include <QObject>
#include <QQmlApplicationEngine>
#include <QQuickWindow>
#include <QFileSelector>
#include <QFileSystemWatcher>
#include <QDebug>
class RuntimeQML : public QObject
{
Q_OBJECT
Q_PROPERTY(QString qrcFilename READ qrcFilename WRITE setQrcFilename NOTIFY qrcFilenameChanged)
Q_PROPERTY(bool autoReload READ autoReload WRITE setAutoReload NOTIFY autoReloadChanged)
Q_PROPERTY(bool closeAllOnReload READ closeAllOnReload WRITE setCloseAllOnReload NOTIFY closeAllOnReloadChanged)
public:
explicit RuntimeQML(QQmlApplicationEngine *engine, QString const& qrcFilename = QString(), QObject *parent = 0);
// If using QQmlFileSelector with Loader
Q_INVOKABLE QString adjustPath(QString qmlFile);
Q_INVOKABLE QString qrcAbsolutePath() const;
QString qrcFilename() const;
bool autoReload() const;
bool closeAllOnReload() const;
QList<QString> const & prefixIgnoreList() const;
QList<QString> const & fileIgnoreList() const;
QList<QString> const & allowedSuffixes() const;
void noDebug();
signals:
void autoReloadChanged(bool autoReload);
void qrcFilenameChanged(QString qrcFilename);
void closeAllOnReloadChanged(bool closeAllOnReload);
public slots:
void reload();
void setWindow(QQuickWindow* window);
void setQrcFilename(QString qrcFilename);
void setMainQmlFilename(QString filename);
void setAutoReload(bool autoReload);
void setCloseAllOnReload(bool closeAllOnReload);
void ignoreQrcPrefix(QString const& prefix);
void ignoreFile(QString const& filename);
void addSuffix(QString const& suffix);
private slots:
void reloadQml();
void fileChanged(const QString &path);
private:
void loadQrcFiles();
void unloadFileWatcher();
QQmlApplicationEngine *m_engine {nullptr};
QQuickWindow *m_window {nullptr};
QFileSelector m_selector;
QString m_qrcFilename;
QString m_mainQmlFilename;
bool m_autoReload {false};
QFileSystemWatcher* m_fileWatcher {nullptr};
QList<QString> m_prefixIgnoreList;
QList<QString> m_fileIgnoreList;
QList<QString> m_allowedSuffixList;
bool m_noDebug {false};
bool m_closeAllOnReload {true};
};
#endif // RUNTIMEQML_H

13
runtimeqml/runtimeqml.pri Normal file
View file

@ -0,0 +1,13 @@
# Qt Quick Runtime Reloader
QT += core qml quick
INCLUDEPATH += $$PWD
DEFINES += "QRC_SOURCE_PATH=\\\"$$PWD/..\\\""
SOURCES += \
$$PWD/runtimeqml.cpp
HEADERS += \
$$PWD/runtimeqml.h

18
runtimeqml/runtimeqml.qbs Normal file
View file

@ -0,0 +1,18 @@
import qbs 1.0
StaticLibrary {
name: "runtimeqml"
files: [
"runtimeqml.cpp",
"runtimeqml.h",
]
Depends { name: 'cpp' }
Depends { name: "Qt.core" }
Depends { name: "Qt.quick" }
Export {
Depends { name: "cpp" }
cpp.includePaths: [product.sourceDirectory]
cpp.defines: ['QRC_SOURCE_PATH="'+path+'/.."']
}
}

View file

@ -6,7 +6,9 @@
#include "config.h"
#include "mpvobject.h"
#ifdef QRC_SOURCE_PATH
#include "runtimeqml/runtimeqml.h"
#endif
#include <QApplication>
@ -59,6 +61,15 @@ view->setSource(QUrl("qrc:///player/main.qml"));
view->show();*/
QQmlApplicationEngine engine;
#ifdef QRC_SOURCE_PATH
RuntimeQML *rt = new RuntimeQML(&engine, QRC_SOURCE_PATH"/qml.qrc");
rt->setAutoReload(true);
rt->setMainQmlFilename("main.qml");
rt->reload();
#else
engine.load(QUrl(QStringLiteral("qrc:///player/main.qml")));
#endif
return app.exec();
}

View file

@ -118,7 +118,7 @@ MpvObject::MpvObject(QQuickItem * parent)
throw std::runtime_error("could not create mpv context");
mpv_set_option_string(mpv, "terminal", "yes");
mpv_set_option_string(mpv, "msg-level", "all=v");
//mpv_set_option_string(mpv, "msg-level", "all=v");
// Fix?
mpv_set_option_string(mpv, "ytdl", "yes");

BIN
src/qml/CustomMenuItem.qmlc Normal file

Binary file not shown.

BIN
src/qml/codes.jsc Normal file

Binary file not shown.

View file

@ -43,7 +43,6 @@ ApplicationWindow {
var sid = player.getProperty("sid")
var vid = player.getProperty("vid")
console.log("Updating Track Menu, Total Tracks: " + tracks)
for (track = 0; track <= tracks; track++) {
var trackID = player.getProperty("track-list/" + track + "/id")
var trackType = player.getProperty("track-list/" + track + "/type")
@ -155,12 +154,10 @@ ApplicationWindow {
}
function skipToNinth(val) {
console.log(val)
var skipto = 0
if (val != 0) {
skipto = Math.floor(progressBar.to / 9 * val)
}
console.log(skipto)
player.command(["seek", skipto, "absolute"])
}
@ -175,7 +172,6 @@ ApplicationWindow {
}
function updateVolume(volume) {
console.log(volume)
var muted = player.getProperty("mute")
if (muted || volume === 0) {
@ -405,7 +401,7 @@ ApplicationWindow {
}
background: Rectangle {
implicitWidth: parent.width
width: parent.width
implicitHeight: 10
color: "black"
opacity: 0.6
@ -468,7 +464,7 @@ ApplicationWindow {
Menu {
id: playbackMenuBarItem
title: "Playback"
width: 100
width: 150
background: Rectangle {
implicitWidth: parent.width
implicitHeight: 10
@ -476,7 +472,7 @@ ApplicationWindow {
opacity: 0.6
}
delegate: CustomMenuItem {
width: 100
width: parent.width
}
Action {
@ -569,7 +565,7 @@ ApplicationWindow {
Menu {
id: tracksMenuBarItem
title: "Tracks"
width: 150
width: 140
background: Rectangle {
implicitWidth: parent.width
implicitHeight: 10
@ -577,7 +573,7 @@ ApplicationWindow {
opacity: 0.6
}
delegate: CustomMenuItem {
width: 100
width: parent.width
}
Action {
text: "Track Menu"
@ -621,7 +617,7 @@ ApplicationWindow {
Menu {
id: viewMenuBarItem
title: "View"
width: 100
width: 120
background: Rectangle {
implicitWidth: parent.width
implicitHeight: 10
@ -629,6 +625,7 @@ ApplicationWindow {
opacity: 0.6
}
delegate: CustomMenuItem {
width: parent.width
}
Action {
@ -716,8 +713,6 @@ ApplicationWindow {
height: childrenRect.height
visible: false
anchors.centerIn: player
anchors.right: player.right
anchors.bottom: progressBar.top
border.color: "black"
border.width: 2

BIN
src/qml/main.qmlc Normal file

Binary file not shown.