FlightGear next
QQuickDrawable.cxx
Go to the documentation of this file.
1// QQuickDrawable.cxx - OSG Drawable using a QQuickRenderControl to draw
2//
3// Copyright (C) 2019 James Turner <james@flightgear.org>
4//
5// This program is free software; you can redistribute it and/or
6// modify it under the terms of the GNU General Public License as
7// published by the Free Software Foundation; either version 2 of the
8// License, or (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful, but
11// WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13// General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program; if not, write to the Free Software
17// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
19#include "config.h"
20
21#include <simgear/compiler.h>
22#include <atomic>
23
24#include <QOpenGLContext>
25
26#include "QQuickDrawable.hxx"
27
28#include <QQmlComponent>
29#include <QQmlContext>
30#include <QQmlEngine>
31#include <QQuickRenderControl>
32
33#include <QTimer>
34#include <QCoreApplication>
35#include <QOpenGLFunctions>
36#include <QQuickItem>
37#include <QQuickWindow>
38#include <QSurfaceFormat>
39#include <QThread>
40
41
42// private Qt headers, needed to make glue work between Qt and OSG
43// graphics window unfortunately.
44#include <private/qopenglcontext_p.h>
45
46#if defined(SG_MAC)
47# include "fake_qguiapp_p.h"
48#else
49# include <private/qguiapplication_p.h>
50#endif
51
52#include <osg/GraphicsContext>
53#include <osgGA/GUIEventAdapter>
54#include <osgGA/GUIEventHandler>
55#include <osgViewer/GraphicsWindow>
56#include <osgViewer/Viewer>
57
59#include <GUI/FGQQWindowManager.hxx>
60#include <GUI/FGQmlInstance.hxx>
62#include <Main/fg_props.hxx>
63#include <Main/globals.hxx>
64#include <GUI/OSGQtAdaption.hxx>
65#include <simgear/structure/commands.hxx>
66
67using namespace osgGA;
68
69struct QtKey {
70 QtKey(int _o, int _q, QString _s = {}) : osg(_o), qt(_q), s(_s) {}
71
72 int osg;
73 int qt;
74 QString s;
75};
76
77const std::initializer_list<QtKey> keymapInit = {
78 // contains all the key mappings except 0..9 and A..Z which are generated
79 // programatically
80
81 {GUIEventAdapter::KEY_Space, Qt::Key_Space, " "},
82 {GUIEventAdapter::KEY_Escape, Qt::Key_Escape, "\x1B"},
83 {GUIEventAdapter::KEY_Return, Qt::Key_Return, "\r"},
84 {GUIEventAdapter::KEY_Tab, Qt::Key_Tab, "\t"},
85 {GUIEventAdapter::KEY_BackSpace, Qt::Key_Backspace, "\x08"},
86 {GUIEventAdapter::KEY_Delete, Qt::Key_Delete, "\x7f"},
87
88 {GUIEventAdapter::KEY_Period, Qt::Key_Period, "."},
89 {GUIEventAdapter::KEY_Comma, Qt::Key_Comma, ","},
90 {GUIEventAdapter::KEY_Colon, Qt::Key_Colon, ":"},
91 {GUIEventAdapter::KEY_Quote, Qt::Key_QuoteLeft, "'"},
92 {GUIEventAdapter::KEY_Quotedbl, Qt::Key_QuoteDbl, "\""},
93 {GUIEventAdapter::KEY_Underscore, Qt::Key_Underscore, "_"},
94 {GUIEventAdapter::KEY_Plus, Qt::Key_Plus, "+"},
95 {GUIEventAdapter::KEY_Minus, Qt::Key_Minus, "-"},
96 {GUIEventAdapter::KEY_Asterisk, Qt::Key_Asterisk, "*"},
97 {GUIEventAdapter::KEY_Equals, Qt::Key_Equal, "="},
98 {GUIEventAdapter::KEY_Slash, Qt::Key_Slash, "/"},
99
100 {GUIEventAdapter::KEY_Left, Qt::Key_Left},
101 {GUIEventAdapter::KEY_Right, Qt::Key_Right},
102 {GUIEventAdapter::KEY_Up, Qt::Key_Up},
103 {GUIEventAdapter::KEY_Down, Qt::Key_Down},
104
105 {GUIEventAdapter::KEY_Shift_L, Qt::Key_Shift},
106 {GUIEventAdapter::KEY_Shift_R, Qt::Key_Shift},
107 {GUIEventAdapter::KEY_Control_L, Qt::Key_Control},
108 {GUIEventAdapter::KEY_Control_R, Qt::Key_Control},
109 {GUIEventAdapter::KEY_Meta_L, Qt::Key_Meta},
110 {GUIEventAdapter::KEY_Meta_R, Qt::Key_Meta},
111
112
113};
114
115std::vector<QtKey> global_keymap;
116
117class CustomRenderControl : public QQuickRenderControl
118{
119public:
121 : _qWindow(win)
122 {
123 // Q_ASSERT(win);
124 }
125
126 QWindow* renderWindow(QPoint* offset) override
127 {
128 if (offset) {
129 *offset = QPoint(0, 0);
130 }
131 return _qWindow;
132 }
133
134private:
135 QWindow* _qWindow = nullptr;
136};
137
138class QQuickDrawablePrivate : public QObject
139{
140 Q_OBJECT
141public:
144 {
145
146 }
147
149 {
150
151 }
152
154
155 QQmlComponent* qmlComponent = nullptr;
156 QQmlEngine* qmlEngine = nullptr;
157 bool syncRequired = true;
158
159 QQuickItem* rootItem = nullptr;
160
161 // this window is neither created()-ed nor shown but is needed by
162 // QQuickRenderControl for historical reasons, and gives us a place to
163 // inject events from the OSG side.
164 QQuickWindow* quickWindow = nullptr;
165
166 // window representing the OSG window, needed
167 // for making our adpoted context current
168 QWindow* foreignOSGWindow = nullptr;
169 QOpenGLContext* qtContext = nullptr;
170 osg::GraphicsContext* osgContext = nullptr;
171
172 std::atomic_int renderControlInited;
173 std::atomic_int syncPending;
174
176 {
177 if (syncRequired) {
178 renderControl->polishItems();
179 syncRequired = false;
180 syncPending = true;
181
182 osgContext->add(flightgear::makeGraphicsOp("Sync QQ2 Render control", [this](osg::GraphicsContext*) {
183 QOpenGLContextPrivate::setCurrentContext(qtContext);
184 renderControl->sync();
185 syncPending = false;
186 }));
187 }
188 }
189public slots:
191 {
192 if (qmlComponent->isError()) {
193 QList<QQmlError> errorList = qmlComponent->errors();
194 Q_FOREACH (const QQmlError& error, errorList) {
195 qWarning() << error.url() << error.line() << error;
196 }
197 return;
198 }
199
200 QObject* rootObject = qmlComponent->create();
201 if (qmlComponent->isError()) {
202 QList<QQmlError> errorList = qmlComponent->errors();
203 Q_FOREACH (const QQmlError& error, errorList) {
204 qWarning() << error.url() << error.line() << error;
205 }
206 return;
207 }
208
209 rootItem = qobject_cast<QQuickItem*>(rootObject);
210 if (!rootItem) {
211 qWarning() << Q_FUNC_INFO << "root object not a QQuickItem" << rootObject;
212 delete rootObject;
213 return;
214 }
215
216 // The root item is ready. Associate it with the window.
217 rootItem->setParentItem(quickWindow->contentItem());
218 syncRequired = true;
219 rootItem->setWidth(quickWindow->width());
220 rootItem->setHeight(quickWindow->height());
221 }
222
224 {
226 renderControl->prepareThread(QThread::currentThread());
227 QOpenGLContextPrivate::setCurrentContext(qtContext);
228 QOpenGLContextPrivate::get(qtContext)->surface = foreignOSGWindow;
229 renderControl->initialize(qtContext);
230
231 renderControlInited = true;
232 }
233
235 {
236 syncRequired = true;
237 }
238
240 {
241 qWarning() << Q_FUNC_INFO;
242 }
243
245 {
246 if (quickWindow->activeFocusItem())
247 qInfo() << Q_FUNC_INFO << "Active focus item is now:" << quickWindow->activeFocusItem();
248 else
249 qInfo() << Q_FUNC_INFO << "Active focus cleared";
250 }
251};
252
253
254static QObject* fgqmlinstance_provider(QQmlEngine* engine, QJSEngine* scriptEngine)
255{
256 Q_UNUSED(engine)
257 Q_UNUSED(scriptEngine)
258
259 FGQmlInstance* instance = new FGQmlInstance;
260 return instance;
261}
262
263static QObject* fgqq_windowManager_provider(QQmlEngine* engine, QJSEngine* scriptEngine)
264{
265 Q_UNUSED(scriptEngine)
266
267 FGQQWindowManager* instance = new FGQQWindowManager(engine);
268 return instance;
269}
270
271class ReloadCommand : public SGCommandMgr::Command
272{
273public:
274 ReloadCommand(QQuickDrawable* qq) : _drawable(qq)
275 {
276 }
277
278 bool operator()(const SGPropertyNode* aNode, SGPropertyNode* root) override
279 {
280 SG_UNUSED(aNode);
281 SG_UNUSED(root);
282
283 QTimer::singleShot(0, [this]() {
284 std::string rootQMLPath = fgGetString("/sim/gui/qml-root-path");
285 _drawable->reload(QUrl::fromLocalFile(QString::fromStdString(rootQMLPath)));
286 });
287 return true;
288 }
289
290private:
291 QQuickDrawable* _drawable;
292};
293
294class QuickEventHandler : public osgGA::GUIEventHandler
295{
296public:
298 {
299 populateKeymap();
300 }
301
302 bool handle(const GUIEventAdapter& ea, GUIActionAdapter& aa, osg::Object*, osg::NodeVisitor*) override
303 {
304 Q_UNUSED(aa);
305
306 if (ea.getHandled()) return false;
307
308 // frame event, ho hum ...
309 if (ea.getEventType() == GUIEventAdapter::FRAME) {
310 _drawable->frameEvent();
311 return false;
312 }
313
314 // Qt expects increasing downward mouse coords
315 const float fixedY = (ea.getMouseYOrientation() == GUIEventAdapter::Y_INCREASING_UPWARDS) ? ea.getWindowHeight() - ea.getY() : ea.getY();
316 const double pixelRatio = _drawable->foreignOSGWindow->devicePixelRatio();
317
318 QPointF pointInWindow{ea.getX() / pixelRatio, fixedY / pixelRatio};
319 QPointF screenPt = pointInWindow +
320 QPointF{ea.getWindowX() / pixelRatio, ea.getWindowY() / pixelRatio};
321
322 // const int scaledX = static_cast<int>(ea.getX() / static_pixelRatio);
323 // const int scaledY = static_cast<int>(fixedY / static_pixelRatio);
324
325 switch (ea.getEventType()) {
326 case (GUIEventAdapter::DRAG):
327 case (GUIEventAdapter::MOVE): {
328 QMouseEvent m(QEvent::MouseMove, pointInWindow, pointInWindow, screenPt,
329 Qt::NoButton,
330 osgButtonMaskToQt(ea), osgModifiersToQt(ea));
331 QCoreApplication::sendEvent(_drawable->quickWindow, &m);
332 return m.isAccepted();
333 }
334
335 case (GUIEventAdapter::PUSH):
336 case (GUIEventAdapter::RELEASE): {
337 const bool isUp = (ea.getEventType() == GUIEventAdapter::RELEASE);
338 QMouseEvent m(isUp ? QEvent::MouseButtonRelease : QEvent::MouseButtonPress,
339 pointInWindow, pointInWindow, screenPt,
340 osgButtonToQt(ea),
341 osgButtonMaskToQt(ea), osgModifiersToQt(ea));
342 QCoreApplication::sendEvent(_drawable->quickWindow, &m);
343
344 if (!isUp) {
345 if (m.isAccepted()) {
346 // deactivate PUI
347 auto active = puActiveWidget();
348 if (active) {
349 active->invokeDownCallback();
350 }
351 }
352
353 // on mouse downs which we don't accept, take focus back
354 // qInfo() << "Clearing QQ focus";
355 // auto focused = _drawable->quickWindow->activeFocusItem();
356 // if (focused) {
357 // focused->setFocus(false, Qt::MouseFocusReason);
358 // }
359 }
360
361 return m.isAccepted();
362 }
363
364 case (GUIEventAdapter::KEYDOWN):
365 case (GUIEventAdapter::KEYUP): {
366 if (!_drawable->quickWindow->activeFocusItem()) {
367 return false;
368 }
369
370 const bool isKeyRelease = (ea.getEventType() == GUIEventAdapter::KEYUP);
371 const auto& key = osgKeyToQt(ea.getKey());
372 QString s = key.s;
373
374 QKeyEvent k(isKeyRelease ? QEvent::KeyRelease : QEvent::KeyPress,
375 key.qt, osgModifiersToQt(ea), s);
376 QCoreApplication::sendEvent(_drawable->quickWindow, &k);
377 return k.isAccepted();
378 }
379
380 default:
381 return false;
382 }
383
384 return false;
385 }
386private:
387 Qt::MouseButtons osgButtonMaskToQt(const osgGA::GUIEventAdapter& ea) const
388 {
389 const int mask = ea.getButtonMask();
390 Qt::MouseButtons result = Qt::NoButton;
391 if (mask & osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON)
392 result |= Qt::LeftButton;
393
394 if (mask & osgGA::GUIEventAdapter::MIDDLE_MOUSE_BUTTON)
395 result |= Qt::MiddleButton;
396
397 if (mask & osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON)
398 result |= Qt::RightButton;
399
400 return result;
401 }
402
403 Qt::MouseButton osgButtonToQt(const osgGA::GUIEventAdapter& ea) const
404 {
405 switch (ea.getButton()) {
406 case osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON: return Qt::LeftButton;
407 case osgGA::GUIEventAdapter::MIDDLE_MOUSE_BUTTON: return Qt::MiddleButton;
408 case osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON: return Qt::RightButton;
409
410 default:
411 break;
412 }
413
414 return Qt::NoButton;
415 }
416
417 Qt::KeyboardModifiers osgModifiersToQt(const osgGA::GUIEventAdapter& ea) const
418 {
419 Qt::KeyboardModifiers result = Qt::NoModifier;
420 const int mask = ea.getModKeyMask();
421 if (mask & osgGA::GUIEventAdapter::MODKEY_ALT) result |= Qt::AltModifier;
422 if (mask & osgGA::GUIEventAdapter::MODKEY_CTRL) result |= Qt::ControlModifier;
423 if (mask & osgGA::GUIEventAdapter::MODKEY_META) result |= Qt::MetaModifier;
424 if (mask & osgGA::GUIEventAdapter::MODKEY_SHIFT) result |= Qt::ShiftModifier;
425 return result;
426 }
427
428 QtKey osgKeyToQt(int code) const
429 {
430 auto it = std::lower_bound(global_keymap.begin(), global_keymap.end(),
431 QtKey{code, 0, {}},
432 [](const QtKey& a, const QtKey& b) { return a.osg < b.osg; });
433 if ((it == global_keymap.end()) || (it->osg != code)) {
434 qWarning() << "no mapping defined for OSG key:" << code;
435 return {0, 0, ""};
436 }
437
438 return *it;
439 }
440
441 void populateKeymap()
442 {
443 if (!global_keymap.empty())
444 return;
445
446 // regular keymappsing for A..Z and 0..9
447 for (int i = 0; i < 10; ++i) {
448 global_keymap.emplace_back(GUIEventAdapter::KEY_0 + i, Qt::Key_0 + i, QString::number(i));
449 }
450
451 for (int i = 0; i < 26; ++i) {
452 global_keymap.emplace_back(GUIEventAdapter::KEY_A + i, Qt::Key_A + i, QChar::fromLatin1('a' + i));
453 }
454
455 for (int i = 0; i < 26; ++i) {
456 global_keymap.emplace_back('A' + i, Qt::Key_A + i, QChar::fromLatin1('A' + i));
457 }
458
459 // custom key mappsing
461
462 // sort by OSG code for fast lookups
463 std::sort(global_keymap.begin(), global_keymap.end(),
464 [](const QtKey& a, const QtKey& b) { return a.osg < b.osg; });
465 }
466
467 QQuickDrawablePrivate* _drawable;
468};
469
471{
472 setUseDisplayList(false);
473 setDataVariance(Object::DYNAMIC);
474
475 osg::StateSet* stateSet = getOrCreateStateSet();
476 stateSet->setRenderBinDetails(1001, "RenderBin");
477
478 QSurfaceFormat format;
479 format.setRenderableType(QSurfaceFormat::OpenGL);
480 QSurfaceFormat::setDefaultFormat(format);
481
482 static bool doneQmlRegistration = false;
483 if (!doneQmlRegistration) {
484 doneQmlRegistration = true;
485
486 // singleton system object
487 qmlRegisterSingletonType<FGQmlInstance>("FlightGear", 1, 0, "System", fgqmlinstance_provider);
488 qmlRegisterSingletonType<FGQmlInstance>("FlightGear", 1, 0, "WindowManager", fgqq_windowManager_provider);
489
490 // QML types
491 qmlRegisterType<FGQmlPropertyNode>("FlightGear", 1, 0, "Property");
492 qmlRegisterType<DialogStateController>("FlightGear", 1, 0, "DialogStateController");
493 }
494
495 globals->get_commands()->addCommandObject("reload-quick-gui", new ReloadCommand(this));
496}
497
499{
500 delete d->qmlEngine;
501 delete d->renderControl;
502}
503
504void QQuickDrawable::setup(osgViewer::GraphicsWindow *gw, osgViewer::Viewer *viewer)
505{
506 osg::GraphicsContext* gc = gw;
507
508 // none of this stuff needs the context current, so we can do it
509 // all safely on the main thread
510
511 d->foreignOSGWindow = flightgear::qtWindowFromOSG(gw);
512 // d->foreignOSGWindow->setFormat(format);
513 d->foreignOSGWindow->setSurfaceType(QSurface::OpenGLSurface);
514
515 // QWindow::requestActive would do QPA::makeKey, but on macOS this
516 // is a no-op for foreign windows. So we're going to manually set
517 // the focus window!
518 QGuiApplicationPrivate::focus_window = d->foreignOSGWindow;
519
520 d->osgContext = gc;
521 d->renderControl = new CustomRenderControl(d->foreignOSGWindow);
522 d->quickWindow = new QQuickWindow(d->renderControl);
523 d->quickWindow->setClearBeforeRendering(false);
524
525 d->qmlEngine = new QQmlEngine;
526
527 SGPath rootQMLPath = SGPath::fromUtf8(fgGetString("/sim/gui/qml-root-path"));
528 SG_LOG(SG_GENERAL, SG_INFO, "Root QML dir:" << rootQMLPath.dir());
529 d->qmlEngine->addImportPath(QString::fromStdString(rootQMLPath.dir()));
530 // d->qmlEngine->addImportPath(QStringLiteral("qrc:///"));
531
532 if (!d->qmlEngine->incubationController())
533 d->qmlEngine->setIncubationController(d->quickWindow->incubationController());
534
535 // QObject::connect(d->quickWindow, &QQuickWindow::activeFocusItemChanged,
536 // d.get(), &QQuickDrawablePrivate::onWindowActiveFocusItemChanged);
537
538 QObject::connect(d->renderControl, &QQuickRenderControl::sceneChanged,
540 QObject::connect(d->renderControl, &QQuickRenderControl::renderRequested,
542
543
544 viewer->getEventHandlers().push_front(new QuickEventHandler(d.get()));
545}
546
547void QQuickDrawable::drawImplementation(osg::RenderInfo& renderInfo) const
548{
549 if (!d->renderControlInited) {
550 d->initRenderControl();
551 }
552
553 if (QOpenGLContext::currentContext() != d->qtContext) {
554 QOpenGLContextPrivate::setCurrentContext(d->qtContext);
555 }
556
557 QOpenGLFunctions* glFuncs = d->qtContext->functions();
558 // prepare any state QQ2 needs
559 d->quickWindow->resetOpenGLState();
560
561 // and reset these manually
562 glFuncs->glPixelStorei(GL_PACK_ALIGNMENT, 4);
563 glFuncs->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
564 glFuncs->glPixelStorei(GL_PACK_ROW_LENGTH, 0);
565 glFuncs->glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
566
567 d->renderControl->render();
568
569 // otherwise the PUI camera gets confused
570 d->quickWindow->resetOpenGLState();
571}
572
573void QQuickDrawable::reload(const QUrl& url)
574{
575 d->qmlEngine->clearComponentCache();
576 setSource(url);
577}
578
579void QQuickDrawable::setSourcePath(const std::string& path)
580{
581 setSource(QUrl::fromLocalFile(QString::fromStdString(path)));
582}
583
584void QQuickDrawable::setSource(const QUrl& url)
585{
586 if (d->rootItem)
587 delete d->rootItem;
588 if (d->qmlComponent)
589 delete d->qmlComponent;
590 d->rootItem = nullptr;
591
592 d->qmlComponent = new QQmlComponent(d->qmlEngine, url);
593 if (d->qmlComponent->isLoading()) {
594 QObject::connect(d->qmlComponent, &QQmlComponent::statusChanged,
596 } else {
597 d->onComponentLoaded();
598 }
599}
600
601void QQuickDrawable::resize(int width, int height)
602{
603 // we need to unscale from physical pixels back to logical, otherwise we end up double-scaled.
604 const float currentPixelRatio = static_cast<float>(d->foreignOSGWindow->devicePixelRatio());
605 const int logicalWidth = static_cast<int>(width / currentPixelRatio);
606 const int logicalHeight = static_cast<int>(height / currentPixelRatio);
607
608 if (d->rootItem) {
609 d->rootItem->setWidth(logicalWidth);
610 d->rootItem->setHeight(logicalHeight);
611 }
612
613// SG_LOG(SG_GUI, SG_INFO, "Resize:, lw=" << logicalWidth << ", lh=" << logicalHeight);
614 d->quickWindow->setGeometry(0, 0, logicalWidth, logicalHeight);
615}
616
617#include "QQuickDrawable.moc"
#define p(x)
static QObject * fgqmlinstance_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
const std::initializer_list< QtKey > keymapInit
std::vector< QtKey > global_keymap
static QObject * fgqq_windowManager_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
#define i(x)
QWindow * renderWindow(QPoint *offset) override
CustomRenderControl(QWindow *win)
static QPointer< QWindow > focus_window
CustomRenderControl * renderControl
osg::GraphicsContext * osgContext
std::atomic_int renderControlInited
QOpenGLContext * qtContext
QQmlComponent * qmlComponent
std::atomic_int syncPending
void setSource(const QUrl &url)
virtual ~QQuickDrawable()
void reload(const QUrl &url)
void drawImplementation(osg::RenderInfo &renderInfo) const override
void setSourcePath(const std::string &path)
void resize(int width, int height)
void setup(osgViewer::GraphicsWindow *gw, osgViewer::Viewer *viewer)
QuickEventHandler(QQuickDrawablePrivate *p)
bool handle(const GUIEventAdapter &ea, GUIActionAdapter &aa, osg::Object *, osg::NodeVisitor *) override
bool operator()(const SGPropertyNode *aNode, SGPropertyNode *root) override
ReloadCommand(QQuickDrawable *qq)
std::string fgGetString(const char *name, const char *defaultValue)
Get a string value for a property.
Definition fg_props.cxx:556
FGGlobals * globals
Definition globals.cxx:142
QWindow * qtWindowFromOSG(osgViewer::GraphicsWindow *graphicsWindow)
osg::ref_ptr< osg::GraphicsOperation > makeGraphicsOp(const std::string &name, GraphicsFunctor func)
QOpenGLContext * qtContextFromOSG(osg::GraphicsContext *context)
Definition AIBase.hxx:25
QtKey(int _o, int _q, QString _s={})
QString s