qskinny/src/controls/QskSkinTransition.cpp
2019-04-26 11:56:09 +02:00

681 lines
20 KiB
C++

#include "QskSkinTransition.h"
#include "QskColorFilter.h"
#include "QskControl.h"
#include "QskHintAnimator.h"
#include "QskSkin.h"
#include "QskSkinHintTable.h"
#include <qglobalstatic.h>
#include <qguiapplication.h>
#include <qobject.h>
#include <qquickwindow.h>
#include <qvector.h>
#include <unordered_map>
#include <vector>
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
{
public:
AnimatorGroup( QQuickWindow* window = nullptr )
: m_window( window )
{
}
inline const QQuickWindow* window() const
{
return m_window;
}
void start()
{
for ( auto& it : m_hintAnimatorMap )
it.second.start();
for ( auto& it : m_graphicFilterAnimatorMap )
it.second.start();
}
bool isRunning() const
{
if ( !m_hintAnimatorMap.empty() )
{
const auto& animator = m_hintAnimatorMap.begin()->second;
if ( animator.isRunning() )
return true;
}
if ( !m_graphicFilterAnimatorMap.empty() )
{
const auto& animator = m_graphicFilterAnimatorMap.begin()->second;
if ( animator.isRunning() )
return true;
}
return false;
}
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(
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( m_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 = qskControlCast( 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 );
}
void update()
{
for ( auto& info : m_updateInfos )
{
if ( auto control = info.control )
{
if ( info.updateModes & UpdateInfo::Polish )
{
control->resetImplicitSize();
control->polish();
}
if ( info.updateModes & UpdateInfo::Update )
control->update();
}
}
}
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 );
}
QQuickWindow* m_window;
std::unordered_map< QskAspect::Aspect, QskHintAnimator > m_hintAnimatorMap;
std::unordered_map< int, QskVariantAnimator > m_graphicFilterAnimatorMap;
std::vector< UpdateInfo > m_updateInfos; // vector: for fast iteration
};
class AnimatorGroups : public QObject
{
Q_OBJECT
public:
~AnimatorGroups()
{
reset();
}
inline AnimatorGroup* animatorGroup( const QQuickWindow* window )
{
if ( !m_animatorGroups.empty() && window )
{
for ( auto group : m_animatorGroups )
{
if ( group->window() == window )
return group;
}
}
return nullptr;
}
void add( AnimatorGroup* group )
{
m_animatorGroups.push_back( group );
}
void start()
{
m_connections[0] = QskAnimator::addAdvanceHandler(
this, SLOT(notify(QQuickWindow*)), Qt::UniqueConnection );
m_connections[1] = QskAnimator::addCleanupHandler(
this, SLOT(cleanup(QQuickWindow*)), Qt::UniqueConnection );
for ( auto& group : m_animatorGroups )
group->start();
}
void reset()
{
qDeleteAll( m_animatorGroups );
m_animatorGroups.clear();
disconnect( m_connections[0] );
disconnect( m_connections[1] );
}
inline bool isRunning() const
{
return !m_animatorGroups.empty();
}
private Q_SLOTS:
void notify( QQuickWindow* window )
{
for ( auto& group : m_animatorGroups )
{
if ( group->window() == window )
{
group->update();
return;
}
}
}
void cleanup( QQuickWindow* window )
{
for ( auto it = m_animatorGroups.begin();
it != m_animatorGroups.end(); ++it )
{
auto group = *it;
if ( group->window() == window )
{
if ( !group->isRunning() )
{
// The notification might be for other animators
m_animatorGroups.erase( it );
delete group;
}
break;
}
}
if ( m_animatorGroups.empty() )
reset();
}
private:
/*
It should be possible to find an implementation, that interpolates
a skin hint only once for all windows. But as our animtors are driven by
QQuickWindow::afterAnimating the code will have to be somehow tricky.
But as skin transitions are no operations, that happen often, we can accept
the overhaed of the current implementation and do the finetuning later.
*/
std::vector< AnimatorGroup* > m_animatorGroups;
QMetaObject::Connection m_connections[2];
};
}
Q_GLOBAL_STATIC( AnimatorGroups, 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 ) )
{
auto* group = new AnimatorGroup( quickWindow );
if ( doGraphicFilter )
{
group->addGraphicFilterAnimators(
m_animationHint, oldFilters,
m_skins[ 1 ]->graphicFilters() );
doGraphicFilter = false;
}
/*
finally we schedule the animators the hard way by running
over the the item trees.
*/
group->addAnimators( quickWindow->contentItem(),
m_animationHint, candidates, m_skins[ 1 ] );
qskSkinAnimator->add( group );
}
}
qskSkinAnimator->start();
}
}
bool QskSkinTransition::isRunning()
{
if ( qskSkinAnimator.exists() )
return qskSkinAnimator->isRunning();
return false;
}
QVariant QskSkinTransition::animatedHint(
const QQuickWindow* window, QskAspect::Aspect aspect )
{
if ( qskSkinAnimator.exists() )
{
if ( const auto group = qskSkinAnimator->animatorGroup( window ) )
return group->animatedHint( aspect );
}
return QVariant();
}
QVariant QskSkinTransition::animatedGraphicFilter(
const QQuickWindow* window, int graphicRole )
{
if ( qskSkinAnimator.exists() )
{
if ( const auto group = qskSkinAnimator->animatorGroup( window ) )
return group->animatedGraphicFilter( graphicRole );
}
return QVariant();
}
#include "QskSkinTransition.moc"