#include "QskSkinTransition.h" #include "QskControl.h" #include "QskSkin.h" #include "QskSkinHintTable.h" #include "QskColorFilter.h" #include "QskHintAnimator.h" #include #include #include #include #include #include #include #include namespace { class UpdateInfo { public: enum UpdateMode { Polish = 1, Update = 2 }; static inline bool compare( const UpdateInfo& i1, const UpdateInfo& i2 ) { return i1.control.data() < i2.control.data(); } QPointer< QskControl > control; int updateModes; }; class AnimatorCandidate { public: AnimatorCandidate() = default; inline AnimatorCandidate( QskAspect::Aspect aspect, QVariant from, QVariant to ): aspect( aspect ), from( from ), to( to ) { } QskAspect::Aspect aspect; QVariant from; QVariant to; }; } static QVector< AnimatorCandidate > qskAnimatorCandidates( QskSkinTransition::Type mask, const QskSkinHintTable& oldTable, const std::unordered_map< int, QskColorFilter >& oldFilters, const QskSkinHintTable& newTable, const std::unordered_map< int, QskColorFilter >& newFilters ) { // building a list of candidates for animations by comparing // the old/new set of skin hints const QskColorFilter noFilter; QVector< AnimatorCandidate > candidates; if ( !oldTable.hasHints() ) return candidates; for ( const auto& entry : newTable.hints() ) { const auto aspect = entry.first; if ( aspect.isAnimator() ) continue; const auto type = aspect.type(); if ( type == QskAspect::Flag ) { switch( aspect.flagPrimitive() ) { case QskAspect::GraphicRole: { if ( !( mask & QskSkinTransition::Color ) ) continue; int role1 = 0; const auto value = oldTable.resolvedHint( aspect ); if ( value ) role1 = value->toInt(); const int role2 = entry.second.toInt(); /* When the role is the same we already have the animators for the graphic filter table running */ if ( role1 != role2 ) { const auto it1 = oldFilters.find( role1 ); const auto it2 = newFilters.find( role2 ); if ( it1 != oldFilters.end() || it2 != newFilters.end() ) { const auto& f1 = ( it1 != oldFilters.end() ) ? it1->second : noFilter; const auto& f2 = ( it2 != newFilters.end() ) ? it2->second : noFilter; if ( f1 != f2 ) { candidates += AnimatorCandidate( aspect, QVariant::fromValue( f1 ), QVariant::fromValue( f2 ) ); } } } break; } case QskAspect::FontRole: { if ( !( mask & QskSkinTransition::Metric ) ) continue; break; } default: ; } } else { if ( ( ( type == QskAspect::Color ) && ( mask & QskSkinTransition::Color ) ) || ( ( type == QskAspect::Metric ) && ( mask & QskSkinTransition::Metric ) ) ) { auto value = oldTable.resolvedHint( aspect ); if ( value == nullptr && aspect.subControl() != QskAspect::Control ) { auto a = aspect; a.setSubControl( QskAspect::Control ); a.clearStates(); value = oldTable.resolvedHint( a ); } /* We are missing transitions, when a hint in newTable gets resolved from QskControl. TODO ... */ if ( value && *value != entry.second ) candidates += AnimatorCandidate( aspect, *value, entry.second ); } } } return candidates; } namespace { class AnimatorGroup final : public QObject { Q_OBJECT public: AnimatorGroup() { } void start() { m_notifyConnection = QskAnimator::addAdvanceHandler( this, SLOT( notify( QQuickWindow* ) ) ); for ( auto& it : m_hintAnimatorMap ) it.second.start(); for ( auto& it : m_graphicFilterAnimatorMap ) it.second.start(); } bool isRunning() const { return !m_hintAnimatorMap.empty(); } void reset() { disconnect( m_notifyConnection ); m_hintAnimatorMap.clear(); m_graphicFilterAnimatorMap.clear(); m_updateInfos.clear(); } inline QVariant animatedHint( QskAspect::Aspect aspect ) const { auto it = m_hintAnimatorMap.find( aspect ); if ( it != m_hintAnimatorMap.cend() ) { const auto& animator = it->second; if ( animator.isRunning() ) return animator.currentValue(); } return QVariant(); } inline QVariant animatedGraphicFilter( int graphicRole ) const { auto it = m_graphicFilterAnimatorMap.find( graphicRole ); if ( it != m_graphicFilterAnimatorMap.cend() ) { const auto& animator = it->second; if ( animator.isRunning() ) return animator.currentValue(); } return QVariant(); } void addGraphicFilterAnimators( QQuickWindow* window, const QskAnimationHint& animatorHint, const std::unordered_map< int, QskColorFilter >& oldFilters, const std::unordered_map< int, QskColorFilter >& newFilters ) { const QskColorFilter noFilter; for ( auto it2 = newFilters.begin(); it2 != newFilters.end(); ++it2 ) { auto it1 = oldFilters.find( it2->first ); if ( it1 == oldFilters.cend() ) it1 = oldFilters.find( 0 ); const auto& f1 = ( it1 != oldFilters.cend() ) ? it1->second : noFilter; const auto& f2 = it2->second; if ( f1 != f2 ) { QskVariantAnimator animator; animator.setWindow( window ); animator.setDuration( animatorHint.duration ); animator.setEasingCurve( animatorHint.type ); animator.setStartValue( QVariant::fromValue( f1 ) ); animator.setEndValue( QVariant::fromValue( f2 ) ); m_graphicFilterAnimatorMap.emplace( it2->first, animator ); } } } void addAnimators( QQuickItem* item, const QskAnimationHint& animatorHint, const QVector< AnimatorCandidate >& candidates, QskSkin* skin ) { if ( !item->isVisible() ) return; if ( auto control = qobject_cast< QskControl* >( item ) ) { if ( control->isInitiallyPainted() && ( skin == control->effectiveSkin() ) ) { addControlAnimators( control, animatorHint, candidates ); #if 1 /* As it is hard to identify which controls depend on the animated graphic filters we schedule an initial update and let the controls do the rest: see QskSkinnable::effectiveGraphicFilter */ control->update(); #endif } } const auto children = item->childItems(); for ( auto child : children ) addAnimators( child, animatorHint, candidates, skin ); } private Q_SLOTS: void notify( QQuickWindow* window ) { if ( m_updateInfos.empty() ) return; for ( auto& info : m_updateInfos ) { QskControl* control = info.control; if ( control && control->window() == window ) { if ( info.updateModes & UpdateInfo::Polish ) { control->resetImplicitSize(); control->polish(); } if ( info.updateModes & UpdateInfo::Update ) control->update(); } } if ( !m_hintAnimatorMap.empty() ) { if ( !m_hintAnimatorMap.begin()->second.isRunning() ) reset(); } } private: void addControlAnimators( QskControl* control, const QskAnimationHint& animatorHint, const QVector< AnimatorCandidate >& candidates ) { const auto subControls = control->subControls(); for ( const auto& candidate : candidates ) { using namespace QskAspect; if ( candidate.aspect.type() != Metric ) { if ( !( control->flags() & QQuickItem::ItemHasContents ) ) { // while metrics might have an effect on layouts, we // can safely ignore others for controls without content continue; } } const Subcontrol subControl = candidate.aspect.subControl(); if ( subControl != control->effectiveSubcontrol( subControl ) ) { // The control uses subcontrol redirection, so we can assume it // is not interested in this subcontrol. continue; } if ( subControl != QskAspect::Control ) { if ( !subControls.contains( subControl ) ) { // the control is not interested in the aspect continue; } } else { if ( !control->autoFillBackground() ) { // no need to animate the background unless we show it continue; } } QskAspect::Aspect a = candidate.aspect; a.clearStates(); a.addState( control->skinState() ); const QskSkinHintStatus requestState = control->hintStatus( a ); if ( requestState.source != QskSkinHintStatus::Skin ) { // The control does not resolve the aspect from the skin. continue; } if ( candidate.aspect != requestState.aspect ) { // the aspect was resolved to something else continue; } addAnimator( control->window(), candidate, animatorHint ); storeUpdateInfo( control, candidate.aspect ); } } void addAnimator( QQuickWindow* window, const AnimatorCandidate& candidate, QskAnimationHint animationHint ) { auto it = m_hintAnimatorMap.find( candidate.aspect ); if ( it != m_hintAnimatorMap.end() ) return; // already there it = m_hintAnimatorMap.emplace( candidate.aspect, QskHintAnimator() ).first; auto& animator = it->second; animator.setAspect( candidate.aspect ); animator.setStartValue( candidate.from ); animator.setEndValue( candidate.to ); animator.setDuration( animationHint.duration ); animator.setEasingCurve( animationHint.type ); animator.setControl( nullptr ); animator.setWindow( window ); } inline void storeUpdateInfo( QskControl* control, QskAspect::Aspect aspect ) { UpdateInfo info; info.control = control; info.updateModes = UpdateInfo::Update; if ( aspect.type() == QskAspect::Metric ) info.updateModes |= UpdateInfo::Polish; auto it = std::lower_bound( m_updateInfos.begin(), m_updateInfos.end(), info, UpdateInfo::compare ); if ( ( it != m_updateInfos.end() ) && ( it->control == info.control ) ) it->updateModes |= info.updateModes; else m_updateInfos.insert( it, info ); } std::unordered_map< QskAspect::Aspect, QskHintAnimator > m_hintAnimatorMap; std::unordered_map< int, QskVariantAnimator > m_graphicFilterAnimatorMap; std::vector< UpdateInfo > m_updateInfos; // vector: for fast iteration QMetaObject::Connection m_notifyConnection; }; } Q_GLOBAL_STATIC( AnimatorGroup, qskSkinAnimator ) QskSkinTransition::QskSkinTransition(): m_mask( QskSkinTransition::AllTypes ) { m_skins[0] = m_skins[1] = nullptr; } QskSkinTransition::~QskSkinTransition() { } void QskSkinTransition::setMask( Type type ) { m_mask = type; } QskSkinTransition::Type QskSkinTransition::mask() const { return m_mask; } void QskSkinTransition::setSourceSkin( QskSkin* skin ) { m_skins[0] = skin; } QskSkin* QskSkinTransition::sourceSkin() const { return m_skins[0]; } void QskSkinTransition::setTargetSkin( QskSkin* skin ) { m_skins[1] = skin; } QskSkin* QskSkinTransition::targetSkin() const { return m_skins[1]; } void QskSkinTransition::setAnimation( QskAnimationHint animationHint ) { m_animationHint = animationHint; } QskAnimationHint QskSkinTransition::animation() const { return m_animationHint; } void QskSkinTransition::updateSkin( QskSkin*, QskSkin* ) { // nop } void QskSkinTransition::process() { if ( ( m_skins[0] == nullptr ) || ( m_skins[1] == nullptr ) ) { // do nothing return; } qskSkinAnimator->reset(); if ( ( m_animationHint.duration <= 0 ) || ( m_mask == 0 ) ) { // no animations, we can apply the changes updateSkin( m_skins[0], m_skins[1] ); return; } QVector< AnimatorCandidate > candidates; const auto oldFilters = m_skins[0]->graphicFilters(); { // copy out all hints before updating the skin // - would be good to have Copy on Write here const auto oldTable = m_skins[0]->hintTable(); // apply the changes updateSkin( m_skins[0], m_skins[1] ); candidates = qskAnimatorCandidates( m_mask, oldTable, oldFilters, m_skins[1]->hintTable(), m_skins[1]->graphicFilters() ); } if ( !candidates.isEmpty() ) { bool doGraphicFilter = m_mask & QskSkinTransition::Color; const auto windows = qGuiApp->topLevelWindows(); for ( const auto window : windows ) { if ( auto quickWindow = qobject_cast< QQuickWindow* >( window ) ) { if ( doGraphicFilter ) { qskSkinAnimator->addGraphicFilterAnimators( quickWindow, m_animationHint, oldFilters, m_skins[1]->graphicFilters() ); doGraphicFilter = false; } /* finally we schedule the animators the hard way by running over the the item trees. */ qskSkinAnimator->addAnimators( quickWindow->contentItem(), m_animationHint, candidates, m_skins[1] ); } } qskSkinAnimator->start(); } } bool QskSkinTransition::isRunning() { return qskSkinAnimator.exists() && qskSkinAnimator->isRunning(); } QVariant QskSkinTransition::animatedHint( QskAspect::Aspect aspect ) { if ( !qskSkinAnimator.exists() ) return QVariant(); return qskSkinAnimator->animatedHint( aspect ); } QVariant QskSkinTransition::animatedGraphicFilter( int graphicRole ) { if ( !qskSkinAnimator.exists() ) return QVariant(); return qskSkinAnimator->animatedGraphicFilter( graphicRole ); } #include "QskSkinTransition.moc"