473 lines
12 KiB
C++
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"
|