qskinny/src/controls/QskSkinTransition.cpp

713 lines
18 KiB
C++

/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the QSkinny License, Version 1.0
*****************************************************************************/
#include "QskSkinTransition.h"
#include "QskColorFilter.h"
#include "QskControl.h"
#include "QskWindow.h"
#include "QskAnimationHint.h"
#include "QskHintAnimator.h"
#include "QskSkin.h"
#include "QskSkinHintTable.h"
#include <qglobalstatic.h>
#include <qguiapplication.h>
#include <qobject.h>
#include <qvector.h>
#include <unordered_map>
#include <vector>
static void qskSendStyleEventRecursive( QQuickItem* item )
{
QEvent event( QEvent::StyleChange );
QCoreApplication::sendEvent( item, &event );
const auto children = item->childItems();
for ( auto child : children )
qskSendStyleEventRecursive( child );
}
static void qskAddCandidates( const QskSkinTransition::Type mask,
const QskSkin* skin, QSet< QskAspect >& candidates )
{
for ( const auto& entry : skin->hintTable().hints() )
{
const auto aspect = entry.first.trunk();
if ( aspect.isAnimator() )
continue;
bool isCandidate = false;
switch( aspect.type() )
{
case QskAspect::Flag:
{
if ( aspect.flagPrimitive() == QskAspect::GraphicRole )
{
isCandidate = mask & QskSkinTransition::Color;
}
#if 0
else if ( aspect.flagPrimitive() == QskAspect::FontRole )
{
isCandidate = mask & QskSkinTransition::Metric;
}
#endif
break;
}
case QskAspect::Color:
{
isCandidate = mask & QskSkinTransition::Color;
break;
}
case QskAspect::Metric:
{
isCandidate = mask & QskSkinTransition::Metric;
break;
}
}
if ( isCandidate )
candidates += aspect;
}
}
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 HintAnimator : public QskHintAnimator
{
public:
inline HintAnimator( const QskControl* control, const QskAspect aspect,
const QVariant& value1, const QVariant& value2, QskAnimationHint hint )
{
setAspect( aspect );
setStartValue( value1 );
setEndValue( value2 );
setDuration( hint.duration );
setEasingCurve( hint.type );
setUpdateFlags( hint.updateFlags );
setWindow( control->window() );
}
};
class WindowAnimator
{
public:
WindowAnimator( QQuickWindow* = nullptr );
const QQuickWindow* window() const;
void start();
bool isRunning() const;
QVariant animatedHint( QskAspect ) const;
QVariant animatedGraphicFilter( int graphicRole ) const;
void addGraphicFilterAnimators( const QskAnimationHint&,
const QskSkin*, const QskSkin* );
void addItemAspects( QQuickItem*,
const QskAnimationHint&, const QSet< QskAspect >&,
const QskSkin*, const QskSkin* );
void update();
private:
bool isControlAffected( const QskControl*,
const QVector< QskAspect::Subcontrol >&, QskAspect ) const;
void addHints( const QskControl*,
const QskAnimationHint&, const QSet< QskAspect >& candidates,
const QskSkin* skin1, const QskSkin* skin2 );
void storeAnimator( const QskControl*, const QskAspect,
const QVariant&, const QVariant&, QskAnimationHint );
void storeUpdateInfo( const QskControl*, QskAspect );
QQuickWindow* m_window;
std::unordered_map< QskAspect, HintAnimator > m_animatorMap;
std::unordered_map< int, QskVariantAnimator > m_graphicFilterAnimatorMap;
std::vector< UpdateInfo > m_updateInfos; // vector: for fast iteration
};
class ApplicationAnimator : public QObject
{
Q_OBJECT
public:
~ApplicationAnimator();
WindowAnimator* windowAnimator( const QQuickWindow* );
void add( WindowAnimator* );
void start();
void reset();
bool isRunning() const;
private Q_SLOTS:
// using functor slots ?
void notify( QQuickWindow* );
void cleanup( QQuickWindow* );
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< WindowAnimator* > m_windowAnimators;
QMetaObject::Connection m_connections[2];
};
}
Q_GLOBAL_STATIC( ApplicationAnimator, qskApplicationAnimator )
WindowAnimator::WindowAnimator( QQuickWindow* window )
: m_window( window )
{
}
inline const QQuickWindow* WindowAnimator::window() const
{
return m_window;
}
void WindowAnimator::start()
{
for ( auto& it : m_animatorMap )
it.second.start();
for ( auto& it : m_graphicFilterAnimatorMap )
it.second.start();
}
bool WindowAnimator::isRunning() const
{
if ( !m_animatorMap.empty() )
{
const auto& animator = m_animatorMap.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 WindowAnimator::animatedHint( QskAspect aspect ) const
{
auto it = m_animatorMap.find( aspect );
if ( it != m_animatorMap.cend() )
{
const auto& animator = it->second;
if ( animator.isRunning() )
return animator.currentValue();
}
return QVariant();
}
inline QVariant WindowAnimator::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 WindowAnimator::addGraphicFilterAnimators( const QskAnimationHint& animatorHint,
const QskSkin* skin1, const QskSkin* skin2 )
{
const QskColorFilter noFilter;
const auto& filter1 = skin1->graphicFilters();
const auto& filter2 = skin2->graphicFilters();
for ( auto it2 = filter2.begin(); it2 != filter2.end(); ++it2 )
{
auto it1 = filter1.find( it2->first );
if ( it1 == filter1.cend() )
it1 = filter1.find( 0 );
const auto& f1 = ( it1 != filter1.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 WindowAnimator::addItemAspects( QQuickItem* item,
const QskAnimationHint& animatorHint, const QSet< QskAspect >& candidates,
const QskSkin* skin1, const QskSkin* skin2 )
{
if ( !item->isVisible() )
return;
if ( auto control = qskControlCast( item ) )
{
if ( control->isInitiallyPainted() && ( control->effectiveSkin() == skin2 ) )
{
addHints( control, animatorHint, candidates, skin1, skin2 );
#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 )
addItemAspects( child, animatorHint, candidates, skin1, skin2 );
}
void WindowAnimator::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();
}
}
}
void WindowAnimator::addHints( const QskControl* control,
const QskAnimationHint& animatorHint, const QSet< QskAspect >& candidates,
const QskSkin* skin1, const QskSkin* skin2 )
{
const auto subControls = control->subControls();
const auto& localTable = control->hintTable();
const auto& table1 = skin1->hintTable();
const auto& table2 = skin2->hintTable();
for ( auto aspect : candidates )
{
if ( !isControlAffected( control, subControls, aspect ) )
continue;
aspect.setPlacement( control->effectivePlacement() );
aspect.setStates( control->skinStates() );
if ( localTable.resolvedHint( aspect ) )
{
// value is not from the skin - ignored
continue;
}
QskAspect r1, r2;
const auto v1 = table1.resolvedHint( aspect, &r1 );
const auto v2 = table2.resolvedHint( aspect, &r2 );
if ( v1 && v2 )
{
if ( QskVariantAnimator::maybeInterpolate( *v1, *v2 ) )
{
if ( r1.placement() == r2.placement() )
aspect.setPlacement( r2.placement() );
if ( r1.states() == r2.states() )
aspect.setStates( r2.states() );
storeAnimator( control, aspect, *v1, *v2, animatorHint );
storeUpdateInfo( control, aspect );
}
}
else if ( v1 )
{
aspect.setPlacement( r1.placement() );
aspect.setStates( r1.states() );
storeAnimator( control, aspect, *v1, QVariant(), animatorHint );
storeUpdateInfo( control, aspect );
}
else if ( v2 )
{
aspect.setPlacement( r1.placement() );
aspect.setStates( r1.states() );
storeAnimator( control, aspect, QVariant(), *v2, animatorHint );
storeUpdateInfo( control, aspect );
}
}
}
inline bool WindowAnimator::isControlAffected( const QskControl* control,
const QVector< QskAspect::Subcontrol >& subControls, const QskAspect aspect ) const
{
if ( !aspect.isMetric() )
{
if ( !( control->flags() & QQuickItem::ItemHasContents ) )
{
// while metrics might have an effect on layouts, we
// ignore all others for controls without content
return false;
}
}
const auto subControl = aspect.subControl();
if ( subControl != control->effectiveSubcontrol( subControl ) )
{
// The control uses subcontrol redirection, so we can assume it
// is not interested in this subcontrol.
return false;
}
if ( subControl == QskAspect::Control )
{
if ( !control->autoFillBackground() )
{
// no need to animate the background unless we show it
return false;
}
}
else
{
if ( !subControls.contains( subControl ) )
{
// the control is not interested in the aspect
return false;
}
}
return true;
}
inline void WindowAnimator::storeAnimator( const QskControl* control, const QskAspect aspect,
const QVariant& value1, const QVariant& value2, QskAnimationHint hint )
{
if ( m_animatorMap.find( aspect ) == m_animatorMap.cend() )
{
m_animatorMap.emplace( aspect,
HintAnimator( control, aspect, value1, value2, hint ) );
}
}
inline void WindowAnimator::storeUpdateInfo( const QskControl* control, QskAspect aspect )
{
UpdateInfo info;
info.control = const_cast< QskControl* >( control );
info.updateModes = UpdateInfo::Update;
if ( aspect.isMetric() )
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 );
}
ApplicationAnimator::~ApplicationAnimator()
{
reset();
}
inline WindowAnimator* ApplicationAnimator::windowAnimator( const QQuickWindow* window )
{
if ( window )
{
for ( auto animator : m_windowAnimators )
{
if ( animator->window() == window )
return animator;
}
}
return nullptr;
}
void ApplicationAnimator::add( WindowAnimator* animator )
{
m_windowAnimators.push_back( animator );
}
void ApplicationAnimator::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& animator : m_windowAnimators )
animator->start();
}
void ApplicationAnimator::reset()
{
qDeleteAll( m_windowAnimators );
m_windowAnimators.clear();
disconnect( m_connections[0] );
disconnect( m_connections[1] );
}
inline bool ApplicationAnimator::isRunning() const
{
return !m_windowAnimators.empty();
}
void ApplicationAnimator::notify( QQuickWindow* window )
{
for ( auto& animator : m_windowAnimators )
{
if ( animator->window() == window )
{
animator->update();
return;
}
}
}
void ApplicationAnimator::cleanup( QQuickWindow* window )
{
for ( auto it = m_windowAnimators.begin();
it != m_windowAnimators.end(); ++it )
{
auto animator = *it;
if ( animator->window() == window )
{
if ( !animator->isRunning() )
{
// The notification might be for other animators
m_windowAnimators.erase( it );
delete animator;
}
// let the items know, that we are done
qskSendStyleEventRecursive( window->contentItem() );
break;
}
}
if ( m_windowAnimators.empty() )
reset();
}
class QskSkinTransition::PrivateData
{
public:
QskSkin* skins[ 2 ] = {};
QskAnimationHint animationHint;
Type mask = QskSkinTransition::AllTypes;
};
QskSkinTransition::QskSkinTransition()
: m_data( new PrivateData() )
{
}
QskSkinTransition::~QskSkinTransition()
{
}
void QskSkinTransition::setMask( Type type )
{
m_data->mask = type;
}
QskSkinTransition::Type QskSkinTransition::mask() const
{
return m_data->mask;
}
void QskSkinTransition::setSourceSkin( QskSkin* skin )
{
m_data->skins[ 0 ] = skin;
}
QskSkin* QskSkinTransition::sourceSkin() const
{
return m_data->skins[ 0 ];
}
void QskSkinTransition::setTargetSkin( QskSkin* skin )
{
m_data->skins[ 1 ] = skin;
}
QskSkin* QskSkinTransition::targetSkin() const
{
return m_data->skins[ 1 ];
}
void QskSkinTransition::setAnimation( QskAnimationHint animationHint )
{
m_data->animationHint = animationHint;
}
QskAnimationHint QskSkinTransition::animation() const
{
return m_data->animationHint;
}
void QskSkinTransition::updateSkin( QskSkin*, QskSkin* )
{
// nop
}
void QskSkinTransition::process()
{
qskApplicationAnimator->reset();
auto skin1 = m_data->skins[ 0 ];
auto skin2 = m_data->skins[ 1 ];
QSet< QskAspect > candidates;
if ( skin1 && skin2 )
{
if ( ( m_data->animationHint.duration > 0 ) && ( m_data->mask != 0 ) )
{
qskAddCandidates( m_data->mask, skin1, candidates );
qskAddCandidates( m_data->mask, skin2, candidates );
}
}
if ( !candidates.isEmpty() )
{
bool doGraphicFilter = m_data->mask & QskSkinTransition::Color;
const auto windows = qGuiApp->topLevelWindows();
for ( const auto window : windows )
{
if ( auto w = qobject_cast< QQuickWindow* >( window ) )
{
if ( !w->isVisible() || ( qskEffectiveSkin( w ) != skin2 ) )
{
continue;
}
auto animator = new WindowAnimator( w );
if ( doGraphicFilter )
{
animator->addGraphicFilterAnimators(
m_data->animationHint, skin1, skin2 );
doGraphicFilter = false;
}
/*
finally we schedule the animators the hard way by running
over the the item trees.
*/
animator->addItemAspects( w->contentItem(),
m_data->animationHint, candidates, skin1, skin2 );
qskApplicationAnimator->add( animator );
}
}
qskApplicationAnimator->start();
}
// apply the changes
updateSkin( skin1, skin2 );
}
bool QskSkinTransition::isRunning()
{
if ( qskApplicationAnimator.exists() )
return qskApplicationAnimator->isRunning();
return false;
}
QVariant QskSkinTransition::animatedHint(
const QQuickWindow* window, QskAspect aspect )
{
if ( qskApplicationAnimator.exists() )
{
if ( const auto animator = qskApplicationAnimator->windowAnimator( window ) )
return animator->animatedHint( aspect );
}
return QVariant();
}
QVariant QskSkinTransition::animatedGraphicFilter(
const QQuickWindow* window, int graphicRole )
{
if ( qskApplicationAnimator.exists() )
{
if ( const auto animator = qskApplicationAnimator->windowAnimator( window ) )
return animator->animatedGraphicFilter( graphicRole );
}
return QVariant();
}
#include "QskSkinTransition.moc"