/****************************************************************************** * QSkinny - Copyright (C) 2016 Uwe Rathmann * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ #include "QskFocusIndicator.h" #include "QskAspect.h" #include "QskAnimationHint.h" #include "QskEvent.h" #include "QskQuick.h" #include #include #include QSK_QT_PRIVATE_BEGIN #include 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"