-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
QML-based components API for controllers #13459
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is roughly going into the direction I'm thinking. Keep it up.
res/controllers/Denon-DN-S3700.qml
Outdated
@@ -0,0 +1,17 @@ | |||
import QtQuick |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't actually want QtQuick (since thats the GUI framework). You instead just want the language basics QtQml
. You may also add a version (not sure whats appropriate, in the preexisting qml files we have in mixxx we use 2.12, but thats quite old (I think from Qt 5.12 still)).
import QtQuick | |
import QtQml |
Ok, so the QML loading logic is really entangled with the screen handling logic. So now I'm thinking: Why not dropt the XML entirely and handle all the metadata in QML? That could be done using a |
d051ebb
to
39d2020
Compare
src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp
Outdated
Show resolved
Hide resolved
That should be the target! One semantic syntax, with use of a programming language, where it is not possible to express it in general semantic language. Important is, that our MIDI learning wizard can read all necessary information from this semantic information. |
The potential problem I have with that is that loading a QML module just to read its metadata may be too slow considering you need to parse >100 mappings on startup. I also don't like that it couples the metadata to a particular execution engine, making it impossible to easily try out new execution engines. The biggest issue is that there is no public QML (de-)serialization library. So manipulating the document as it would be required for a controller mapping wizard would not be possible. All in all, I'm not in favor of dropping the separate metadata manifest for now. |
What if We create a custom C++ class? That should be well faster, no? All QML object have a C++ class counterpart, I can't believe Qt would release a new language if its heavy to parse. Edti: Also @Swiftb0y, you said on Zulip:
Maybe this can be the occasion to do so. Can someone point me in the correct direction to do it? |
Sure, we can't be sure until we measured, but the other two concerns I've voiced still apply.
Not sure, what you trying to achieve? I think these type definitions don't give us much more than better editor integration. Edit: if you still want to explore that, from my limited research, you'll want to generate a qmldir file, which could be done automatically using CMake. |
The parsing of the mappings at every startup (we do it 2x at for each XML file at each startup btw.) seems to be wrong to me anyway. As we need only very few data from these files at startup. |
Sure... filehash should be sufficient to determine if something changed. But thats a separate topic IMO. |
Since it would solve all the performance related concerns, it should be addressed first (not in this PR of cause). |
sure... but again, performance wasn't my main concern. It was actually the fact that
|
It looks like we have conflicting or unclear requirements here. We need to decide the way forward to not end up with a blocked PR or double work. Let's summarize (please correct me): User Requirements
Mapping Developer
Did I miss something? Please complete.
That is correct if we use only one parser. I can image to workaround that by a stripped Mixxx QML parser. reading a file hash, is in this context the minimum version of such parser, but we can read also other info.
I can imagine QT does exactly that. Maybe we can copy from them and use it for our second parseing. The other topic is how the data stream is handled. I like to keep the connection factory and the dispatcher in the C++ domain, controlled by QML XML or JS to share concepts between the mapping types. I can also imagine to read the mapping by XML and write them as QML or such for an easy transition. Another point that bugs me is the weak scratching stability and the programming skills required to map the jogg wheel. Both can be targeted by handling the jog in wheel in the C++ domain and configure it from QML XML or JS. Does that make sense? |
I'm sorry but I don't think it makes sense to discuss this feature further in text form. The problem at hand is simply to complicated and widening the scope by bringing discussing the metadata format and currently irrelevant features such as scratching does not help! I will try to find some time and write down my thoughts in a sorted fashion for https://github.com/mixxxdj/proposals/. |
It's better if I continue to work on this, which has the benefit to familiarise myself with the code. |
Sure... just be prepared that it probably won't get merged until we all agree on a design. |
Or else: as a starting point, I can keep the XML and just design a rough QML API based on componentsJS. Then we can iterate and refine over this. Then only, we can discuss dropping the XML. Is that acceptable? |
Yeah. I would keep the XML for now. Changing the metadata format is orthogonal to a potential QML API.
I don't think it makes sense to prototype a QML API outside a QML environment since the fact that that reactive bindings are available would change the API a lot. Or am I misunderstanding you here? |
I mean: I will keep the main design concepts of ComponentsJS to design the QML API, changing a few things for more flexibility. |
Yes, that would work. I think the main challenge is that with QML, we would need to insert a layer that adapts between the explicit message sending model to the implicit reactive binding model. For instance we would need to differentiate between when a binding was triggered (which can happen pretty much spuriously in a reactive system) for no good reason and when we actually do want to send a message. For that, I would introduce the concept of idempotence (each component should be by default idempotent, meaning sending the same message again would not introduce a visual difference and thus spontanous messages that are the same as the one before can be ignored). In the long-term I also want to experiment with abstracting the MIDI-onlyness out of the components, but thats a slightly different topic, though the idempotence concept would be added to the midi part of that abstraction). TLDR; in order to properly work with reactive bindings and reduce overhead, components should swallow duplicate messages by default (inbound and outbound; separably configurable). Does that make sense? |
I took the momentum and put my post into a proposals document: I think we need a separate documents to define single aspects. The requirements document shall make sure that all changes contributes to a common goal. From this point of view @christophehenry can carry on implementing a POC and also keeps his momentum. Does the current list of requirements match your ideas by the way?
That does not always work, because we do not always receive a release event to track double tabs. I think the CO system already covers all aspects we need to consider with these reactive bindings. mixxx/src/control/controlobject.cpp Line 13 in d61841c
|
I fully agree.
Not yet. I will prepare a proposal as well today.
Yes, I'm aware that it won't always work, but it should in the majority of cases and it should be easy to turn of on a per-component basis. I also don't think relying on / referring to COs works here because components may not always be bound to components. It also doesn't always work (for example the |
6bbc49b
to
2a03ebf
Compare
So following r1677677964, I propose we drop MixxxController {
screens: {
screenId: Item {
…
}
}
} Or using a dedicated class like this: MixxxController {
screens: [
Screen {
screenId: …
Item {
…
}
}
]
} I'm rather in favor of a dedicated class so that all the screen-related code can be extracted from Edit: or even: MixxxController {
Screen {
screenId: …
Item {
…
}
}
} I think it's possible using a signal on |
res/controllers/Denon-DN-S3700.qml
Outdated
function init() { | ||
console.error(controller.controllerId, controller.debugMode); | ||
} | ||
function shutdown() { | ||
console.error(`Shutting down ${controller.controllerId} with debug mode ${controller.debugMode}`); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually… Is that even needed? In QML, there's Component.onCompleted
and Component.onDestruction
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These hooks will be invoked on component instantiation and destruction. Assuming this was inspired from the existing screen rendering, init
and shutdown
are called at different time.
init
is called once the controller has started. It is the direct equivalent to the same hook in Javascript. Unlike theconstructor
(equivalent toComponent.onCompleted
), it is invoked once the engine is running.shutdown
is called when the engine is about to shutdown. This allows the controller to perform some animation or screen clean up, which is required for some devices (e.g restoring the splash screen, playing a shutdown animation, clearing the screen to a static colour, ...). In case ofComponent.onDestruction
, as the name suggest, you don't have a component to render anymore.
In case of "pure" controller (without UI rendering), I think it makes sense to ditch these function, but if we want to eventually merge the engines together, they will have to remain.
I believe you might be able to replace them with signals instead tho (e.g MixxxController::onStart
, MixxxController::onShutdown
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest you use the Dummy Device Screen.hid.xml
(Doc for Linux) provided as part of the code to make sure you aren't breaking the UI rendering. I haven't tested it yet, but from a quick code review, I believe there is a few things that won't work.
res/controllers/Denon-DN-S3700.qml
Outdated
function init() { | ||
console.error(controller.controllerId, controller.debugMode); | ||
} | ||
function shutdown() { | ||
console.error(`Shutting down ${controller.controllerId} with debug mode ${controller.debugMode}`); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These hooks will be invoked on component instantiation and destruction. Assuming this was inspired from the existing screen rendering, init
and shutdown
are called at different time.
init
is called once the controller has started. It is the direct equivalent to the same hook in Javascript. Unlike theconstructor
(equivalent toComponent.onCompleted
), it is invoked once the engine is running.shutdown
is called when the engine is about to shutdown. This allows the controller to perform some animation or screen clean up, which is required for some devices (e.g restoring the splash screen, playing a shutdown animation, clearing the screen to a static colour, ...). In case ofComponent.onDestruction
, as the name suggest, you don't have a component to render anymore.
In case of "pure" controller (without UI rendering), I think it makes sense to ditch these function, but if we want to eventually merge the engines together, they will have to remain.
I believe you might be able to replace them with signals instead tho (e.g MixxxController::onStart
, MixxxController::onShutdown
)
QListIterator<std::shared_ptr<mixxx::qml::MixxxController>> controllers(m_mixxxController); | ||
bool controllersSuccess = true; | ||
while (controllers.hasNext()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use a for-loop instead
QListIterator<std::shared_ptr<mixxx::qml::MixxxController>> controllers(m_mixxxController); | |
bool controllersSuccess = true; | |
while (controllers.hasNext()) { | |
bool controllersSuccess = true; | |
for (auto controller: m_mixxxController) { |
QFile file = QFile(qmlScript.file.absoluteFilePath()); | ||
if (!file.exists()) { | ||
qCWarning(m_logger) << "Unable to load the QML file:" << qmlScript.file.absoluteFilePath() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe scene
is the right terminology. I would rather we keep it instead of the cryptic file
@@ -863,34 +867,40 @@ bool ControllerScriptEngineLegacy::evaluateScriptFile(const QFileInfo& scriptFil | |||
} | |||
|
|||
#ifdef MIXXX_USE_QML | |||
std::shared_ptr<QQuickItem> ControllerScriptEngineLegacy::loadQMLFile( | |||
bool ControllerScriptEngineLegacy::instanciateQMLComponent( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure moving the scene binding logic in this function helps much to be honest. Perhaps you should move the logic from line 460
to a new function, but I would suggest not making loadQMLFile
more complex.
QString identifier = scriptFile.attribute( | ||
"identifier", scriptFile.attribute("functionprefix", "")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case of the screens, it makes no sense to default to functionprefix
c751ebf
to
edd2254
Compare
I'm pretty sure there is a few things that won't work. This is me right now: But I believe the screens code can be greatly simplified if separated in a QML component which is what I started to do. There's no logic yet bit res/controllers/Denon-DN-S3700.qml shows how it would look like. I'd like your opinion. @JoergAtGithub and @Swiftb0y too. |
I'm not sure this will change much since there is close to no logic with the root item (thus why interpreted as a QQuickItem) The reason why all of this is like this is... legacy :) I would suggest to watch the proposals currently getting discussed, as I'm not entirely sure it makes sense to make QML support in the legacy engine, since there is a lot of limitations there, and it is likely not going to yield to great result because of those limitations. |
Damn. I was hoping to move that code into the |
Uh… Can I have some help on the build error? |
src/qml/mixxxscreen.h
Outdated
#include <QImage> | ||
#include <QObject> | ||
#include <QSize> | ||
#include <QtQmlIntegration> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this header doesn't exist. Where did you get this from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember. It provides QML_ELEMENT
. But it appears <QtQml/qqmlregistration.h>
does the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is described here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I think QtQmlIntegration
is only a folder containing more headers. I think QtQml/qqmlregistration.h
is better.
51fea62
to
1d248a2
Compare
CMakeLists.txt
Outdated
@@ -2776,6 +2776,9 @@ if(QML) | |||
src/qml/qmlplayerproxy.cpp | |||
src/qml/qmlvisibleeffectsmodel.cpp | |||
src/qml/qmlwaveformoverview.cpp | |||
src/qml/mixxcomponentlifecycle.cpp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this here is useful. MixxxComponentLifecycle
has no implementation since it's an interface but compilation fails without it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, I think thats due to the MOC. Just keep it.
1d248a2
to
2e34045
Compare
src/qml/mixxxscreen.cpp
Outdated
if (typedIdx >= 0) { | ||
auto transformMethod = meta->method(typedIdx); | ||
if (!transformMethod.isValid()) { | ||
// TODO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How should I handle logging here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do I get the engine from a QML element? I can see that QQuickView
has an engine()
which I suppose is because it takes the QQmlEngine
has ctor argument but I don't understand where it gets this argument from. In my code, if I try put any other ctor than QObject
's default, element stops being creatable.
src/qml/mixxxscreen.cpp
Outdated
transform = [transformMethod, this](const QByteArray input, | ||
const QDateTime& timestamp) -> QVariant { | ||
return transform(transformMethod, input, timestamp, true); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a bit more efficient than previous implementation since transform function is searched only once trhough relfection. But then again, I know very few C++ so I may awefully wrong.
src/qml/mixxxscreen.h
Outdated
// Transform function | ||
std::function<QVariant(const QByteArray, const QDateTime&)> transform = | ||
[](const QByteArray input, const QDateTime& timestamp) { | ||
return QVariant(input); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd better use std::expected
here with an error type enum instead here.
…rmFrame and call method
}; | ||
Q_ENUM(ColorEndian) | ||
|
||
explicit MixxxScreen(MixxxController* parent = nullptr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I did the test and parent
here is nullptr
. How can I force a QML component to be instanciated inside a MixxxController
.
So code in the last commit is not good, but it's a glance of the solution I have in mind to allow If someone has a more elegant solution, I take it. |
Let's put this on pause for now. |
Ref: #13440