QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskFocusIndicator.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskFocusIndicator.h"
7#include "QskAspect.h"
8#include "QskAnimationHint.h"
9#include "QskEvent.h"
10#include "QskQuick.h"
11
12#include <qpointer.h>
13#include <qquickwindow.h>
14#include <qbasictimer.h>
15
16QSK_QT_PRIVATE_BEGIN
17#include <private/qquickitem_p.h>
18QSK_QT_PRIVATE_END
19
20QSK_SUBCONTROL( QskFocusIndicator, Panel )
21
22static inline QRectF qskFocusIndicatorRect( const QQuickItem* item )
23{
24 if ( auto control = qskControlCast( item ) )
25 return control->focusIndicatorRect();
26
27 const QVariant v = item->property( "focusIndicatorRect" );
28 if ( v.canConvert< QRectF >() )
29 return v.toRectF();
30
31 return qskItemRect( item );
32}
33
34static inline QRectF qskFocusIndicatorClipRect( const QQuickItem* item )
35{
36 QRectF rect( 0.0, 0.0, -1.0, -1.0 );
37
38 if ( item )
39 {
40 if ( auto control = qskControlCast( item ) )
41 rect = control->focusIndicatorClipRect();
42 else
43 rect = item->clipRect();
44 }
45
46 return rect;
47}
48
49static inline QskAspect::Section qskItemSection( const QQuickItem* item )
50{
51 if ( auto control = qskControlCast( item ) )
52 return control->section();
53
54 return QskAspect::Body;
55}
56
57static inline bool qskIsEnablingKey( const QKeyEvent* event )
58{
59 // what keys do we want have here ???
60 return qskIsButtonPressKey( event ) || qskFocusChainIncrement( event );
61}
62
63class QskFocusIndicator::PrivateData
64{
65 public:
66 void resetConnections()
67 {
68 for ( const auto& connection : std::as_const( connections ) )
69 QObject::disconnect( connection );
70
71 connections.clear();
72 }
73
74 inline bool isAutoDisabling() const { return duration > 0; }
75 inline bool isAutoEnabling() const { return false; }
76
77 QPointer< QQuickItem > clippingItem;
78 QVector< QMetaObject::Connection > connections;
79
80 int duration = 0;
81 QBasicTimer timer;
82
83 bool blockAutoRepeatKeyEvents = false;
84};
85
86QskFocusIndicator::QskFocusIndicator( QQuickItem* parent )
87 : Inherited( parent ) // parentItem() might change, but parent() stays
88 , m_data( new PrivateData() )
89{
90 setPlacementPolicy( QskPlacementPolicy::Ignore );
91 connectWindow( window(), true );
92
93 setDuration( 4500 );
94}
95
96QskFocusIndicator::~QskFocusIndicator()
97{
98}
99
100void QskFocusIndicator::setDuration( int ms )
101{
102 ms = std::max( ms, 0 );
103 if ( ms == m_data->duration )
104 return;
105
106 m_data->duration = ms;
107
108 if ( m_data->isAutoDisabling() )
109 {
110 if ( auto w = window() )
111 w->installEventFilter( this );
112
113 if ( isEnabled() )
114 {
115 if ( isInitiallyPainted() )
116 m_data->timer.start( m_data->duration, this );
117 else
118 setEnabled( false );
119 }
120
121 connect( this, &QQuickItem::enabledChanged,
122 this, &QskFocusIndicator::resetTimer );
123 }
124 else
125 {
126 if ( auto w = window() )
127 w->removeEventFilter( this );
128
129 setEnabled( true );
130
131 disconnect( this, &QQuickItem::enabledChanged,
132 this, &QskFocusIndicator::resetTimer );
133 }
134
135 Q_EMIT durationChanged( ms );
136}
137
138int QskFocusIndicator::duration() const
139{
140 return m_data->duration;
141}
142
143void QskFocusIndicator::maybeEnable( bool on )
144{
145 if ( !m_data->isAutoEnabling() )
146 return;
147
148 if ( on )
149 {
150 if ( auto w = window() )
151 {
152 if ( w->isExposed() && w->isActive() )
153 setEnabled( true );
154 }
155 }
156 else
157 {
158 setEnabled( false );
159 }
160}
161
162void QskFocusIndicator::resetTimer()
163{
164 if ( m_data->isAutoDisabling() )
165 {
166 if ( isEnabled() )
167 {
168 const auto hint = animationHint( Panel | QskAspect::Color );
169 m_data->timer.start( m_data->duration + hint.duration, this );
170 }
171 else
172 {
173 m_data->timer.stop();
174 }
175 }
176}
177
178bool QskFocusIndicator::eventFilter( QObject* object, QEvent* event )
179{
180 if( ( object != window() ) || !m_data->isAutoDisabling() )
181 return Inherited::eventFilter( object, event );
182
183 switch( static_cast< int >( event->type() ) )
184 {
185 case QEvent::KeyPress:
186 case QEvent::KeyRelease:
187 case QEvent::ShortcutOverride:
188 {
189 if ( m_data->timer.isActive() )
190 {
191 // renew the exposed period
192 m_data->timer.start( m_data->duration, this );
193 }
194 break;
195 }
196 }
197
198 switch( static_cast< int >( event->type() ) )
199 {
200 case QEvent::KeyPress:
201 {
202 const auto keyEvent = static_cast< QKeyEvent* >( event );
203
204 if( keyEvent->isAutoRepeat() && m_data->blockAutoRepeatKeyEvents )
205 {
206 /*
207 We swallow all auto repeated events to avoid running along
208 the tab chain by accident.
209 */
210 return true;
211 }
212
213 if ( !isEnabled() && qskIsEnablingKey( keyEvent ) )
214 {
215 setEnabled( true );
216 m_data->blockAutoRepeatKeyEvents = true;
217 return true;
218 }
219
220 m_data->blockAutoRepeatKeyEvents = false;
221 break;
222 }
223
224 case QEvent::KeyRelease:
225 {
226 if( m_data->blockAutoRepeatKeyEvents )
227 {
228 if( !static_cast< QKeyEvent* >( event )->isAutoRepeat() )
229 m_data->blockAutoRepeatKeyEvents = false;
230
231 return true;
232 }
233
234 break;
235 }
236
237 case QEvent::Expose:
238 case QEvent::FocusIn:
239 case QEvent::FocusOut:
240 {
241 maybeEnable( event->type() != QEvent::FocusOut );
242 break;
243 }
244 }
245
246 return Inherited::eventFilter( object, event );
247}
248
249void QskFocusIndicator::timerEvent( QTimerEvent* event )
250{
251 if ( m_data->isAutoDisabling() )
252 {
253 if( event->timerId() == m_data->timer.timerId() )
254 {
255 setEnabled( false );
256 return;
257 }
258 }
259
260 Inherited::timerEvent( event );
261}
262
263bool QskFocusIndicator::contains( const QPointF& ) const
264{
265 // so that tools like Squish do not see it
266 return false;
267}
268
269QRectF QskFocusIndicator::clipRect() const
270{
271 if ( m_data->clippingItem )
272 {
273 auto rect = qskFocusIndicatorClipRect( m_data->clippingItem );
274 rect = mapRectFromItem( m_data->clippingItem, rect );
275
276 return rect;
277 }
278
279 return Inherited::clipRect();
280}
281
282void QskFocusIndicator::onFocusItemGeometryChanged()
283{
284 updateFocusFrame();
285}
286
287void QskFocusIndicator::onWindowSizeChanged( int )
288{
289 updateFocusFrame();
290}
291
292void QskFocusIndicator::onFocusItemDestroyed()
293{
294 m_data->resetConnections();
295 setVisible( false );
296}
297
298void QskFocusIndicator::onFocusItemChanged()
299{
300 m_data->resetConnections();
301
302 if ( !( window() && window()->contentItem() ) )
303 return;
304
305 // We want to be on top, but do we cover all corner cases ???
306 setParentItem( window()->contentItem() );
307 setZ( 10e-6 );
308
309 const auto focusItem = window()->activeFocusItem();
310 QQuickItem* clippingItem = nullptr;
311
312 if ( focusItem && ( focusItem != window()->contentItem() ) )
313 {
314 setSection( qskItemSection( focusItem ) );
315
316 auto item = focusItem;
317 m_data->connections += connectItem( item );
318
319 while ( auto itemParent = item->parentItem() )
320 {
321 m_data->connections += connectItem( itemParent );
322
323 if ( clippingItem == nullptr && itemParent->clip() )
324 clippingItem = itemParent;
325
326 item = itemParent;
327 }
328 }
329
330 m_data->clippingItem = clippingItem;
331 updateFocusFrame();
332}
333
334void QskFocusIndicator::updateFocusFrame()
335{
336 QRectF r = focusRect();
337 setVisible( !r.isEmpty() );
338
339 if ( !r.isEmpty() )
340 {
341 r = r.marginsAdded( paddingHint( Panel ) );
342
343 if ( auto w = window() )
344 {
345 QRectF clipRect( 0, 0, w->width(), w->height() );
346 clipRect = parentItem()->mapRectFromScene( clipRect );
347
348 r = r.intersected( clipRect );
349 }
350
351 setGeometry( r );
352
353 const auto clipRect = qskFocusIndicatorClipRect( m_data->clippingItem );
354 setClip( !clipRect.isEmpty() );
355
356 if ( clip() )
357 {
358 /*
359 The clip node is updated on QQuickItemPrivate::Size
360 So we need to set it here even in situations, where
361 the size did not change. For now we always trigger an
362 update of the clipNode, but we could limit it to
363 changes of the clipRect(). TODO ...
364 */
365 QQuickItemPrivate::get( this )->dirty( QQuickItemPrivate::Size );
366 }
367 }
368
369 update();
370}
371
372QRectF QskFocusIndicator::focusRect() const
373{
374 if ( window() && parentItem() )
375 {
376 const auto item = window()->activeFocusItem();
377
378 if ( item && ( item != this ) && item->isVisible() &&
379 ( item != window()->contentItem() ) )
380 {
381 const auto rect = qskFocusIndicatorRect( item );
382 return parentItem()->mapRectFromItem( item, rect );
383 }
384 }
385
386 return QRectF();
387}
388
390{
392
393 connectWindow( event->oldWindow(), false );
394 connectWindow( event->window(), true );
395
396 onFocusItemChanged();
397
398 if ( m_data->isAutoDisabling() )
399 {
400 if ( auto w = event->oldWindow() )
401 w->removeEventFilter( this );
402
403 if( auto w = event->window() )
404 {
405 w->installEventFilter( this );
406 maybeEnable( true );
407 }
408 }
409}
410
411void QskFocusIndicator::connectWindow( const QQuickWindow* window, bool on )
412{
413 if ( window == nullptr )
414 return;
415
416 if ( on )
417 {
418 connect( window, &QQuickWindow::activeFocusItemChanged,
419 this, &QskFocusIndicator::onFocusItemChanged );
420
421 connect( window, &QQuickWindow::widthChanged,
422 this, &QskFocusIndicator::onWindowSizeChanged );
423
424 connect( window, &QQuickWindow::heightChanged,
425 this, &QskFocusIndicator::onWindowSizeChanged );
426 }
427 else
428 {
429 disconnect( window, &QQuickWindow::activeFocusItemChanged,
430 this, &QskFocusIndicator::onFocusItemChanged );
431
432 disconnect( window, &QQuickWindow::widthChanged,
433 this, &QskFocusIndicator::onWindowSizeChanged );
434
435 disconnect( window, &QQuickWindow::heightChanged,
436 this, &QskFocusIndicator::onWindowSizeChanged );
437 }
438}
439
440QVector< QMetaObject::Connection > QskFocusIndicator::connectItem( const QQuickItem* sender )
441{
442 QVector< QMetaObject::Connection > c;
443 c.reserve( 7 );
444
445 c += QObject::connect( sender, &QObject::destroyed,
446 this, &QskFocusIndicator::onFocusItemDestroyed );
447
448 const auto method = &QskFocusIndicator::onFocusItemGeometryChanged;
449
450 c += QObject::connect( sender, &QQuickItem::xChanged, this, method );
451 c += QObject::connect( sender, &QQuickItem::yChanged, this, method );
452 c += QObject::connect( sender, &QQuickItem::widthChanged, this, method );
453 c += QObject::connect( sender, &QQuickItem::heightChanged, this, method );
454 c += QObject::connect( sender, &QQuickItem::visibleChanged, this, method );
455
456 if ( const auto control = qskControlCast( sender ) )
457 {
458 c += QObject::connect( control, &QskControl::focusIndicatorRectChanged, this, method );
459 }
460 else
461 {
462 if ( sender->metaObject()->indexOfSignal( "focusIndicatorRectChanged()" ) >= 0 )
463 {
464 c += QObject::connect( sender, SIGNAL(focusIndicatorRectChanged()),
465 this, SLOT(onFocusItemGeometryChanged()) );
466 }
467 }
468
469 return c;
470}
471
472#include "moc_QskFocusIndicator.cpp"
void focusIndicatorRectChanged()
void windowChangeEvent(QskWindowChangeEvent *) override
QRectF rect
Definition QskItem.h:21
bool isInitiallyPainted() const
Definition QskItem.cpp:578
virtual void windowChangeEvent(QskWindowChangeEvent *)
Definition QskItem.cpp:851
void setGeometry(qreal x, qreal y, qreal width, qreal height)
Definition QskItem.cpp:330
QMarginsF paddingHint(QskAspect, QskSkinHintStatus *=nullptr) const
Retrieves a padding hint.
QskAnimationHint animationHint(QskAspect, QskSkinHintStatus *=nullptr) const