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