qskinny/src/controls/QskFocusIndicator.cpp
2024-01-17 14:31:45 +01:00

473 lines
12 KiB
C++

/******************************************************************************
* QSkinny - Copyright (C) The authors
* SPDX-License-Identifier: BSD-3-Clause
*****************************************************************************/
#include "QskFocusIndicator.h"
#include "QskAspect.h"
#include "QskAnimationHint.h"
#include "QskEvent.h"
#include "QskQuick.h"
#include <qpointer.h>
#include <qquickwindow.h>
#include <qbasictimer.h>
QSK_QT_PRIVATE_BEGIN
#include <private/qquickitem_p.h>
QSK_QT_PRIVATE_END
QSK_SUBCONTROL( QskFocusIndicator, Panel )
static inline QRectF qskFocusIndicatorRect( const QQuickItem* item )
{
if ( auto control = qskControlCast( item ) )
return control->focusIndicatorRect();
const QVariant v = item->property( "focusIndicatorRect" );
if ( v.canConvert< QRectF >() )
return v.toRectF();
return qskItemRect( item );
}
static inline QRectF qskFocusIndicatorClipRect( const QQuickItem* item )
{
QRectF rect( 0.0, 0.0, -1.0, -1.0 );
if ( item )
{
if ( auto control = qskControlCast( item ) )
rect = control->focusIndicatorClipRect();
else
rect = item->clipRect();
}
return rect;
}
static inline QskAspect::Section qskItemSection( const QQuickItem* item )
{
if ( auto control = qskControlCast( item ) )
return control->section();
return QskAspect::Body;
}
static inline bool qskIsEnablingKey( const QKeyEvent* event )
{
// what keys do we want have here ???
return qskIsButtonPressKey( event ) || qskFocusChainIncrement( event );
}
class QskFocusIndicator::PrivateData
{
public:
void resetConnections()
{
for ( const auto& connection : std::as_const( connections ) )
QObject::disconnect( connection );
connections.clear();
}
inline bool isAutoDisabling() const { return duration > 0; }
inline bool isAutoEnabling() const { return false; }
QPointer< QQuickItem > clippingItem;
QVector< QMetaObject::Connection > connections;
int duration = 0;
QBasicTimer timer;
bool blockAutoRepeatKeyEvents = false;
};
QskFocusIndicator::QskFocusIndicator( QQuickItem* parent )
: Inherited( parent ) // parentItem() might change, but parent() stays
, m_data( new PrivateData() )
{
setPlacementPolicy( QskPlacementPolicy::Ignore );
connectWindow( window(), true );
setDuration( 4500 );
}
QskFocusIndicator::~QskFocusIndicator()
{
}
void QskFocusIndicator::setDuration( int ms )
{
ms = std::max( ms, 0 );
if ( ms == m_data->duration )
return;
m_data->duration = ms;
if ( m_data->isAutoDisabling() )
{
if ( auto w = window() )
w->installEventFilter( this );
if ( isEnabled() )
{
if ( isInitiallyPainted() )
m_data->timer.start( m_data->duration, this );
else
setEnabled( false );
}
connect( this, &QQuickItem::enabledChanged,
this, &QskFocusIndicator::resetTimer );
}
else
{
if ( auto w = window() )
w->removeEventFilter( this );
setEnabled( true );
disconnect( this, &QQuickItem::enabledChanged,
this, &QskFocusIndicator::resetTimer );
}
Q_EMIT durationChanged( ms );
}
int QskFocusIndicator::duration() const
{
return m_data->duration;
}
void QskFocusIndicator::maybeEnable( bool on )
{
if ( !m_data->isAutoEnabling() )
return;
if ( on )
{
if ( auto w = window() )
{
if ( w->isExposed() && w->isActive() )
setEnabled( true );
}
}
else
{
setEnabled( false );
}
}
void QskFocusIndicator::resetTimer()
{
if ( m_data->isAutoDisabling() )
{
if ( isEnabled() )
{
const auto hint = animationHint( Panel | QskAspect::Color );
m_data->timer.start( m_data->duration + hint.duration, this );
}
else
{
m_data->timer.stop();
}
}
}
bool QskFocusIndicator::eventFilter( QObject* object, QEvent* event )
{
if( ( object != window() ) || !m_data->isAutoDisabling() )
return Inherited::eventFilter( object, event );
switch( static_cast< int >( event->type() ) )
{
case QEvent::KeyPress:
case QEvent::KeyRelease:
case QEvent::ShortcutOverride:
{
if ( m_data->timer.isActive() )
{
// renew the exposed period
m_data->timer.start( m_data->duration, this );
}
break;
}
}
switch( static_cast< int >( event->type() ) )
{
case QEvent::KeyPress:
{
const auto keyEvent = static_cast< QKeyEvent* >( event );
if( keyEvent->isAutoRepeat() && m_data->blockAutoRepeatKeyEvents )
{
/*
We swallow all auto repeated events to avoid running along
the tab chain by accident.
*/
return true;
}
if ( !isEnabled() && qskIsEnablingKey( keyEvent ) )
{
setEnabled( true );
m_data->blockAutoRepeatKeyEvents = true;
return true;
}
m_data->blockAutoRepeatKeyEvents = false;
break;
}
case QEvent::KeyRelease:
{
if( m_data->blockAutoRepeatKeyEvents )
{
if( !static_cast< QKeyEvent* >( event )->isAutoRepeat() )
m_data->blockAutoRepeatKeyEvents = false;
return true;
}
break;
}
case QEvent::Expose:
case QEvent::FocusIn:
case QEvent::FocusOut:
{
maybeEnable( event->type() != QEvent::FocusOut );
break;
}
}
return Inherited::eventFilter( object, event );
}
void QskFocusIndicator::timerEvent( QTimerEvent* event )
{
if ( m_data->isAutoDisabling() )
{
if( event->timerId() == m_data->timer.timerId() )
{
setEnabled( false );
return;
}
}
Inherited::timerEvent( event );
}
bool QskFocusIndicator::contains( const QPointF& ) const
{
// so that tools like Squish do not see it
return false;
}
QRectF QskFocusIndicator::clipRect() const
{
if ( m_data->clippingItem )
{
auto rect = qskFocusIndicatorClipRect( m_data->clippingItem );
rect = mapRectFromItem( m_data->clippingItem, rect );
return rect;
}
return Inherited::clipRect();
}
void QskFocusIndicator::onFocusItemGeometryChanged()
{
updateFocusFrame();
}
void QskFocusIndicator::onWindowSizeChanged( int )
{
updateFocusFrame();
}
void QskFocusIndicator::onFocusItemDestroyed()
{
m_data->resetConnections();
setVisible( false );
}
void QskFocusIndicator::onFocusItemChanged()
{
m_data->resetConnections();
if ( !( window() && window()->contentItem() ) )
return;
// We want to be on top, but do we cover all corner cases ???
setParentItem( window()->contentItem() );
setZ( 10e-6 );
const auto focusItem = window()->activeFocusItem();
QQuickItem* clippingItem = nullptr;
if ( focusItem && ( focusItem != window()->contentItem() ) )
{
setSection( qskItemSection( focusItem ) );
auto item = focusItem;
m_data->connections += connectItem( item );
while ( auto itemParent = item->parentItem() )
{
m_data->connections += connectItem( itemParent );
if ( clippingItem == nullptr && itemParent->clip() )
clippingItem = itemParent;
item = itemParent;
}
}
m_data->clippingItem = clippingItem;
updateFocusFrame();
}
void QskFocusIndicator::updateFocusFrame()
{
QRectF r = focusRect();
setVisible( !r.isEmpty() );
if ( !r.isEmpty() )
{
r = r.marginsAdded( paddingHint( Panel ) );
if ( auto w = window() )
{
QRectF clipRect( 0, 0, w->width(), w->height() );
clipRect = parentItem()->mapRectFromScene( clipRect );
r = r.intersected( clipRect );
}
setGeometry( r );
const auto clipRect = qskFocusIndicatorClipRect( m_data->clippingItem );
setClip( !clipRect.isEmpty() );
if ( clip() )
{
/*
The clip node is updated on QQuickItemPrivate::Size
So we need to set it here even in situations, where
the size did not change. For now we always trigger an
update of the clipNode, but we could limit it to
changes of the clipRect(). TODO ...
*/
QQuickItemPrivate::get( this )->dirty( QQuickItemPrivate::Size );
}
}
update();
}
QRectF QskFocusIndicator::focusRect() const
{
if ( window() && parentItem() )
{
const auto item = window()->activeFocusItem();
if ( item && ( item != this ) && item->isVisible() &&
( item != window()->contentItem() ) )
{
const auto rect = qskFocusIndicatorRect( item );
return parentItem()->mapRectFromItem( item, rect );
}
}
return QRectF();
}
void QskFocusIndicator::windowChangeEvent( QskWindowChangeEvent* event )
{
Inherited::windowChangeEvent( event );
connectWindow( event->oldWindow(), false );
connectWindow( event->window(), true );
onFocusItemChanged();
if ( m_data->isAutoDisabling() )
{
if ( auto w = event->oldWindow() )
w->removeEventFilter( this );
if( auto w = event->window() )
{
w->installEventFilter( this );
maybeEnable( true );
}
}
}
void QskFocusIndicator::connectWindow( const QQuickWindow* window, bool on )
{
if ( window == nullptr )
return;
if ( on )
{
connect( window, &QQuickWindow::activeFocusItemChanged,
this, &QskFocusIndicator::onFocusItemChanged );
connect( window, &QQuickWindow::widthChanged,
this, &QskFocusIndicator::onWindowSizeChanged );
connect( window, &QQuickWindow::heightChanged,
this, &QskFocusIndicator::onWindowSizeChanged );
}
else
{
disconnect( window, &QQuickWindow::activeFocusItemChanged,
this, &QskFocusIndicator::onFocusItemChanged );
disconnect( window, &QQuickWindow::widthChanged,
this, &QskFocusIndicator::onWindowSizeChanged );
disconnect( window, &QQuickWindow::heightChanged,
this, &QskFocusIndicator::onWindowSizeChanged );
}
}
QVector< QMetaObject::Connection > QskFocusIndicator::connectItem( const QQuickItem* sender )
{
QVector< QMetaObject::Connection > c;
c.reserve( 7 );
c += QObject::connect( sender, &QObject::destroyed,
this, &QskFocusIndicator::onFocusItemDestroyed );
const auto method = &QskFocusIndicator::onFocusItemGeometryChanged;
c += QObject::connect( sender, &QQuickItem::xChanged, this, method );
c += QObject::connect( sender, &QQuickItem::yChanged, this, method );
c += QObject::connect( sender, &QQuickItem::widthChanged, this, method );
c += QObject::connect( sender, &QQuickItem::heightChanged, this, method );
c += QObject::connect( sender, &QQuickItem::visibleChanged, this, method );
if ( const auto control = qskControlCast( sender ) )
{
c += QObject::connect( control, &QskControl::focusIndicatorRectChanged, this, method );
}
else
{
if ( sender->metaObject()->indexOfSignal( "focusIndicatorRectChanged()" ) >= 0 )
{
c += QObject::connect( sender, SIGNAL(focusIndicatorRectChanged()),
this, SLOT(onFocusItemGeometryChanged()) );
}
}
return c;
}
#include "moc_QskFocusIndicator.cpp"