diff --git a/CHANGES b/CHANGES index 9f72f515c..7e9fc22be 100644 --- a/CHANGES +++ b/CHANGES @@ -7,6 +7,10 @@ website will have to do for older versions. This release contains contributions from (alphabetically by first name): +Distributions are **advised** to check the slideshow they use for the +installation step; changes in loading and translation mechanisms may +require changes in the slideshow. + ## Core ## - With this release, option *WITH_PYTHONQT* changes default to **off**. @@ -18,6 +22,14 @@ This release contains contributions from (alphabetically by first name): configured after the last *exec* section of the sequence has been solved. The *finished* page can be left out (but then you don't get the restart-now functionality). #1168 + - The *slideshow* which is run during installation is now loaded on + startup (while requirements checking is being done). This should + improve responsiveness when the slideshow starts. If the slideshow + has methods `onActivate()` and `onLeave()` those will be called + when the installation step is activated (e.g. the installation + starts and the slideshow becomes visible) or is finished (and the + slideshow becomes hidden). + - The example slideshow now starts its timers when it becomes visible. ## Modules ## diff --git a/src/branding/default/branding.desc b/src/branding/default/branding.desc index 1dd4de03a..1bd76cd29 100644 --- a/src/branding/default/branding.desc +++ b/src/branding/default/branding.desc @@ -93,6 +93,17 @@ images: # The slideshow is displayed during execution steps (e.g. when the # installer is actually writing to disk and doing other slow things). slideshow: "show.qml" +# There are two available APIs for the slideshow: +# - 1 (the default) loads the entire slideshow when the installation- +# slideshow page is shown and starts the QML then. The QML +# is never stopped (after installation is done, times etc. +# continue to fire). +# - 2 loads the slideshow on startup and calls onActivate() and +# onLeave() in the root object. After the installation is done, +# the show is stopped (first by calling onLeave(), then destroying +# the QML components). +slideshowAPI: 2 + # Colors for text and background components. # diff --git a/src/branding/default/show.qml b/src/branding/default/show.qml index 83d47d9e7..43b407283 100644 --- a/src/branding/default/show.qml +++ b/src/branding/default/show.qml @@ -68,8 +68,9 @@ Presentation centeredText: qsTr("This is a third Slide element.") } - Component.onCompleted: { - advanceTimer.running = true; - console.log("Component complete"); + function onActivate() { + presentation.currentSlide = 0; + advanceTimer.running = true + console.log("Component activated"); } } diff --git a/src/libcalamaresui/Branding.cpp b/src/libcalamaresui/Branding.cpp index 48917f1ba..876fdfa80 100644 --- a/src/libcalamaresui/Branding.cpp +++ b/src/libcalamaresui/Branding.cpp @@ -127,6 +127,7 @@ Branding::Branding( const QString& brandingFilePath, , m_descriptorPath( brandingFilePath ) , m_welcomeStyleCalamares( false ) , m_welcomeExpandingLogo( true ) + , m_slideshowAPI( 1 ) { cDebug() << "Using Calamares branding file at" << brandingFilePath; @@ -234,6 +235,14 @@ Branding::Branding( const QString& brandingFilePath, } else bail( "Syntax error in slideshow sequence." ); + + int api = doc[ "slideshowAPI" ].IsScalar() ? doc[ "slideshowAPI" ].as() : -1; + if ( ( api < 1 ) || ( api > 2 ) ) + { + cWarning() << "Invalid or missing *slideshowAPI* in branding file."; + api = 1; + } + m_slideshowAPI = api; } catch ( YAML::Exception& e ) { diff --git a/src/libcalamaresui/Branding.h b/src/libcalamaresui/Branding.h index 23a7a7a49..a3909bc00 100644 --- a/src/libcalamaresui/Branding.h +++ b/src/libcalamaresui/Branding.h @@ -118,6 +118,7 @@ public: /** @brief Path to the slideshow QML file, if any. */ QString slideshowPath() const { return m_slideshowPath; } + int slideshowAPI() const { return m_slideshowAPI; } QString string( Branding::StringEntry stringEntry ) const; QString styleString( Branding::StyleEntry styleEntry ) const; @@ -172,6 +173,7 @@ private: QMap< QString, QString > m_images; QMap< QString, QString > m_style; QString m_slideshowPath; + int m_slideshowAPI; QString m_translationsPathPrefix; /** @brief Initialize the simple settings below */ diff --git a/src/libcalamaresui/ExecutionViewStep.cpp b/src/libcalamaresui/ExecutionViewStep.cpp index a65ab3a1c..4d28a24b2 100644 --- a/src/libcalamaresui/ExecutionViewStep.cpp +++ b/src/libcalamaresui/ExecutionViewStep.cpp @@ -36,10 +36,40 @@ #include #include #include -#include -#include +#include #include +#include +#include +#include + +/** @brief Calls the QML method @p method() + * + * Pass in only the name of the method (e.g. onActivate). This function + * checks if the method exists (with no arguments) before trying to + * call it, so that no warnings are printed due to missing methods. + * + * If there is a return value from the QML method, it is logged (but not otherwise used). + */ +static void +callQMLFunction( QQuickItem* qmlObject, const char* method ) +{ + QByteArray methodSignature( method ); + methodSignature.append( "()" ); + if ( qmlObject && qmlObject->metaObject()->indexOfMethod( methodSignature ) >= 0 ) + { + QVariant returnValue; + QMetaObject::invokeMethod( qmlObject, method, Q_RETURN_ARG( QVariant, returnValue ) ); + if ( !returnValue.isNull() ) + { + cDebug() << "QML" << methodSignature << "returned" << returnValue; + } + } + else if ( qmlObject ) + { + cDebug() << "QML" << methodSignature << "is missing."; + } +} namespace Calamares { @@ -47,31 +77,38 @@ namespace Calamares ExecutionViewStep::ExecutionViewStep( QObject* parent ) : ViewStep( parent ) , m_widget( new QWidget ) + , m_progressBar( new QProgressBar ) + , m_label( new QLabel ) + , m_qmlShow( new QQuickWidget ) + , m_qmlComponent( nullptr ) + , m_qmlObject( nullptr ) { - m_progressBar = new QProgressBar; - m_progressBar->setMaximum( 10000 ); - m_label = new QLabel; QVBoxLayout* layout = new QVBoxLayout( m_widget ); QVBoxLayout* innerLayout = new QVBoxLayout; - m_slideShow = new QQuickWidget; - layout->addWidget( m_slideShow ); - CalamaresUtils::unmarginLayout( layout ); + m_progressBar->setMaximum( 10000 ); - layout->addLayout( innerLayout ); - m_slideShow->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ); - m_slideShow->setResizeMode( QQuickWidget::SizeRootObjectToView ); + m_qmlShow->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ); + m_qmlShow->setResizeMode( QQuickWidget::SizeRootObjectToView ); + m_qmlShow->engine()->addImportPath( CalamaresUtils::qmlModulesDir().absolutePath() ); - m_slideShow->engine()->addImportPath( CalamaresUtils::qmlModulesDir().absolutePath() ); + layout->addWidget( m_qmlShow ); + CalamaresUtils::unmarginLayout( layout ); + layout->addLayout( innerLayout ); innerLayout->addSpacing( CalamaresUtils::defaultFontHeight() / 2 ); innerLayout->addWidget( m_progressBar ); innerLayout->addWidget( m_label ); - cDebug() << "QML import paths:" << Logger::DebugList( m_slideShow->engine()->importPathList() ); + cDebug() << "QML import paths:" << Logger::DebugList( m_qmlShow->engine()->importPathList() ); + if ( Branding::instance()->slideshowAPI() == 2 ) + { + cDebug() << "QML load on startup, API 2."; + loadQmlV2(); + } - connect( JobQueue::instance(), &JobQueue::progress, - this, &ExecutionViewStep::updateFromJobQueue ); + connect( JobQueue::instance(), &JobQueue::progress, this, &ExecutionViewStep::updateFromJobQueue ); + CALAMARES_RETRANSLATE( m_qmlShow->engine()->retranslate(); ) } @@ -130,22 +167,67 @@ ExecutionViewStep::isAtEnd() const return true; } +void +ExecutionViewStep::loadQmlV2() +{ + if ( !m_qmlComponent && !Calamares::Branding::instance()->slideshowPath().isEmpty() ) + { + m_qmlComponent = new QQmlComponent( m_qmlShow->engine(), + QUrl::fromLocalFile( Calamares::Branding::instance()->slideshowPath() ), + QQmlComponent::CompilationMode::Asynchronous + ); + connect( m_qmlComponent, &QQmlComponent::statusChanged, this, &ExecutionViewStep::loadQmlV2Complete ); + } +} + +void +ExecutionViewStep::loadQmlV2Complete() +{ + if ( m_qmlComponent && m_qmlComponent->isReady() && !m_qmlObject ) + { + cDebug() << "QML loading complete, API 2"; + // Don't do this again + disconnect( m_qmlComponent, &QQmlComponent::statusChanged, this, &ExecutionViewStep::loadQmlV2Complete ); + + QObject* o = m_qmlComponent->create(); + m_qmlObject = qobject_cast< QQuickItem* >( o ); + if ( !m_qmlObject ) + delete o; + else + { + // setContent() is public API, but not documented publicly. + // It is marked \internal in the Qt sources, but does exactly + // what is needed: sets up visual parent by replacing the root + // item, and handling resizes. + m_qmlShow->setContent( QUrl::fromLocalFile( Calamares::Branding::instance()->slideshowPath() ), m_qmlComponent, m_qmlObject ); + if ( ViewManager::instance()->currentStep() == this ) + { + // We're alreay visible! Must have been slow QML loading, and we + // passed onActivate already. + callQMLFunction( m_qmlObject, "onActivate" ); + } + } + } +} void ExecutionViewStep::onActivate() { - CALAMARES_RETRANSLATE_WIDGET( m_widget, - if ( !Calamares::Branding::instance()->slideshowPath().isEmpty() ) - m_slideShow->setSource( QUrl::fromLocalFile( Calamares::Branding::instance() - ->slideshowPath() ) ); - ) - + if ( Branding::instance()->slideshowAPI() == 2 ) + { + // The QML was already loaded in the constructor, need to start it + callQMLFunction( m_qmlObject, "onActivate" ); + } + else if ( !Calamares::Branding::instance()->slideshowPath().isEmpty() ) + { + // API version 1 assumes onCompleted is the trigger + m_qmlShow->setSource( QUrl::fromLocalFile( Calamares::Branding::instance()->slideshowPath() ) ); + } JobQueue* queue = JobQueue::instance(); foreach ( const QString& instanceKey, m_jobInstanceKeys ) { - Calamares::Module* module = Calamares::ModuleManager::instance() - ->moduleInstance( instanceKey ); + Calamares::Module* module = Calamares::ModuleManager::instance()->moduleInstance( instanceKey ); if ( module ) { auto jl = module->jobs(); @@ -183,4 +265,16 @@ ExecutionViewStep::updateFromJobQueue( qreal percent, const QString& message ) m_label->setText( message ); } +void +ExecutionViewStep::onLeave() +{ + // API version 2 is explicitly stopped; version 1 keeps running + if ( Branding::instance()->slideshowAPI() == 2 ) + { + callQMLFunction( m_qmlObject, "onLeave" ); + delete m_qmlObject; + m_qmlObject = nullptr; + } +} + } // namespace diff --git a/src/libcalamaresui/ExecutionViewStep.h b/src/libcalamaresui/ExecutionViewStep.h index ed6de4382..c1183e110 100644 --- a/src/libcalamaresui/ExecutionViewStep.h +++ b/src/libcalamaresui/ExecutionViewStep.h @@ -25,7 +25,10 @@ #include class QLabel; +class QObject; class QProgressBar; +class QQmlComponent; +class QQuickItem; class QQuickWidget; namespace Calamares @@ -51,19 +54,26 @@ public: bool isAtEnd() const override; void onActivate() override; + void onLeave() override; JobList jobs() const override; void appendJobModuleInstanceKey( const QString& instanceKey ); +public slots: + void loadQmlV2Complete(); + private: QWidget* m_widget; QProgressBar* m_progressBar; QLabel* m_label; - QQuickWidget* m_slideShow; + QQuickWidget* m_qmlShow; + QQmlComponent* m_qmlComponent; + QQuickItem* m_qmlObject; //< The actual show QStringList m_jobInstanceKeys; + void loadQmlV2(); //< Loads the slideshow QML (from branding) for API version 2 void updateFromJobQueue( qreal percent, const QString& message ); };