1 /* === This file is part of Calamares - <https://calamares.io> ===
2  *
3  *   SPDX-FileCopyrightText: 2020 Adriaan de Groot <groot@kde.org>
4  *   SPDX-License-Identifier: GPL-3.0-or-later
5  *
6  *   Calamares is Free Software: see the License-Identifier above.
7  *
8  */
9 
10 #include "QmlViewStep.h"
11 
12 #include "Branding.h"
13 #include "ViewManager.h"
14 
15 #include "utils/Dirs.h"
16 #include "utils/Logger.h"
17 #include "utils/NamedEnum.h"
18 #include "utils/Qml.h"
19 #include "utils/Variant.h"
20 #include "widgets/WaitingWidget.h"
21 
22 #include <QQmlComponent>
23 #include <QQmlContext>
24 #include <QQmlEngine>
25 #include <QQuickItem>
26 #include <QQuickWidget>
27 #include <QVBoxLayout>
28 #include <QWidget>
29 
30 
31 /// @brief State-change of the QML, for changeQMLState()
32 enum class QMLAction
33 {
34     Start,
35     Stop
36 };
37 
38 /** @brief Tells the QML we activated or left it.
39  *
40  * If @p action is @c QMLAction::Start, calls onActivate in the QML.
41  * If @p action is @c QMLAction::Stop, calls onLeave in the QML.
42  *
43  * Sets *activatedInCalamares* property on the QML as well (to true
44  * if @p action is @c QMLAction::Start, false otherwise).
45  */
46 static void
changeQMLState(QMLAction action,QQuickItem * item)47 changeQMLState( QMLAction action, QQuickItem* item )
48 {
49     static const char propertyName[] = "activatedInCalamares";
50 
51     bool activate = action == QMLAction::Start;
52     CalamaresUtils::callQmlFunction( item, activate ? "onActivate" : "onLeave" );
53 
54     auto property = item->property( propertyName );
55     if ( property.isValid() && ( property.type() == QVariant::Bool ) && ( property.toBool() != activate ) )
56     {
57         item->setProperty( propertyName, activate );
58     }
59 }
60 
61 namespace Calamares
62 {
63 
QmlViewStep(QObject * parent)64 QmlViewStep::QmlViewStep( QObject* parent )
65     : ViewStep( parent )
66     , m_widget( new QWidget )
67     , m_spinner( new WaitingWidget( tr( "Loading ..." ) ) )
68     , m_qmlWidget( new QQuickWidget )
69 {
70     CalamaresUtils::registerQmlModels();
71 
72     QVBoxLayout* layout = new QVBoxLayout( m_widget );
73     layout->addWidget( m_spinner );
74 
75     m_qmlWidget->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
76     m_qmlWidget->setResizeMode( QQuickWidget::SizeRootObjectToView );
77     m_qmlWidget->engine()->addImportPath( CalamaresUtils::qmlModulesDir().absolutePath() );
78 
79     // QML Loading starts when the configuration for the module is set.
80 }
81 
~QmlViewStep()82 QmlViewStep::~QmlViewStep() {}
83 
84 QString
prettyName() const85 QmlViewStep::prettyName() const
86 {
87     // TODO: query the QML itself
88     return tr( "QML Step <i>%1</i>." ).arg( moduleInstanceKey().module() );
89 }
90 
91 
92 bool
isAtBeginning() const93 QmlViewStep::isAtBeginning() const
94 {
95     return true;
96 }
97 
98 bool
isAtEnd() const99 QmlViewStep::isAtEnd() const
100 {
101     return true;
102 }
103 bool
isBackEnabled() const104 QmlViewStep::isBackEnabled() const
105 {
106     return true;
107 }
108 
109 bool
isNextEnabled() const110 QmlViewStep::isNextEnabled() const
111 {
112     return true;
113 }
114 
115 Calamares::JobList
jobs() const116 QmlViewStep::jobs() const
117 {
118     return JobList();
119 }
120 
121 void
onActivate()122 QmlViewStep::onActivate()
123 {
124     if ( m_qmlObject )
125     {
126         changeQMLState( QMLAction::Start, m_qmlObject );
127     }
128 }
129 
130 void
onLeave()131 QmlViewStep::onLeave()
132 {
133     if ( m_qmlObject )
134     {
135         changeQMLState( QMLAction::Stop, m_qmlObject );
136     }
137 }
138 
139 QWidget*
widget()140 QmlViewStep::widget()
141 {
142     return m_widget;
143 }
144 
145 QSize
widgetMargins(Qt::Orientations panelSides)146 QmlViewStep::widgetMargins( Qt::Orientations panelSides )
147 {
148     // If any panels around it, use the standard, but if all the
149     // panels are hidden, like on full-screen with subsumed navigation,
150     // then no margins.
151     if ( panelSides )
152     {
153         return ViewStep::widgetMargins( panelSides );
154     }
155     else
156     {
157         return QSize( 0, 0 );
158     }
159 }
160 
161 void
loadComplete()162 QmlViewStep::loadComplete()
163 {
164     cDebug() << "QML component" << m_qmlFileName << m_qmlComponent->status();
165     if ( m_qmlComponent->status() == QQmlComponent::Error )
166     {
167         showFailedQml();
168     }
169     if ( m_qmlComponent->isReady() && !m_qmlObject )
170     {
171         cDebug() << Logger::SubEntry << "QML component complete" << m_qmlFileName << "creating object";
172         // Don't do this again
173         disconnect( m_qmlComponent, &QQmlComponent::statusChanged, this, &QmlViewStep::loadComplete );
174 
175         QObject* o = m_qmlComponent->create();
176         m_qmlObject = qobject_cast< QQuickItem* >( o );
177         if ( !m_qmlObject )
178         {
179             cError() << Logger::SubEntry << "Could not create QML from" << m_qmlFileName;
180             delete o;
181         }
182         else
183         {
184             // setContent() is public API, but not documented publicly.
185             // It is marked \internal in the Qt sources, but does exactly
186             // what is needed: sets up visual parent by replacing the root
187             // item, and handling resizes.
188             m_qmlWidget->setContent( QUrl( m_qmlFileName ), m_qmlComponent, m_qmlObject );
189             showQml();
190         }
191     }
192 }
193 
194 void
showQml()195 QmlViewStep::showQml()
196 {
197     if ( !m_qmlWidget || !m_qmlObject )
198     {
199         cWarning() << "showQml() called but no QML object";
200         return;
201     }
202     if ( m_spinner )
203     {
204         m_widget->layout()->removeWidget( m_spinner );
205         m_widget->layout()->addWidget( m_qmlWidget );
206         delete m_spinner;
207         m_spinner = nullptr;
208     }
209     else
210     {
211         cWarning() << "showQml() called twice";
212     }
213 
214     if ( ViewManager::instance()->currentStep() == this )
215     {
216         // We're alreay visible! Must have been slow QML loading, and we
217         // passed onActivate already.
218         changeQMLState( QMLAction::Start, m_qmlObject );
219     }
220 }
221 
222 
223 void
setConfigurationMap(const QVariantMap & configurationMap)224 QmlViewStep::setConfigurationMap( const QVariantMap& configurationMap )
225 {
226     bool ok = false;
227     m_searchMethod
228         = CalamaresUtils::qmlSearchNames().find( CalamaresUtils::getString( configurationMap, "qmlSearch" ), ok );
229     if ( !ok )
230     {
231         cWarning() << "Bad QML search mode set for" << moduleInstanceKey();
232     }
233 
234     QString qmlFile = CalamaresUtils::getString( configurationMap, "qmlFilename" );
235     if ( !m_qmlComponent )
236     {
237         m_qmlFileName = searchQmlFile( m_searchMethod, qmlFile, moduleInstanceKey() );
238 
239         QObject* config = this->getConfig();
240         if ( config )
241         {
242             setContextProperty( "config", config );
243         }
244 
245         cDebug() << "QmlViewStep" << moduleInstanceKey() << "loading" << m_qmlFileName;
246         m_qmlComponent = new QQmlComponent(
247             m_qmlWidget->engine(), QUrl( m_qmlFileName ), QQmlComponent::CompilationMode::Asynchronous );
248         connect( m_qmlComponent, &QQmlComponent::statusChanged, this, &QmlViewStep::loadComplete );
249         if ( m_qmlComponent->status() == QQmlComponent::Error )
250         {
251             showFailedQml();
252         }
253     }
254     else
255     {
256         cWarning() << "QML configuration set after component" << moduleInstanceKey() << "has loaded.";
257     }
258 }
259 
260 void
showFailedQml()261 QmlViewStep::showFailedQml()
262 {
263     cWarning() << "QmlViewStep" << moduleInstanceKey() << "loading failed.";
264     if ( m_qmlComponent )
265     {
266         cDebug() << Logger::SubEntry << "QML error:" << m_qmlComponent->errorString();
267     }
268     m_spinner->setText( prettyName() + ' ' + tr( "Loading failed." ) );
269 }
270 
271 QObject*
getConfig()272 QmlViewStep::getConfig()
273 {
274     return nullptr;
275 }
276 
277 void
setContextProperty(const char * name,QObject * property)278 QmlViewStep::setContextProperty( const char* name, QObject* property )
279 {
280     m_qmlWidget->engine()->rootContext()->setContextProperty( name, property );
281 }
282 
283 }  // namespace Calamares
284