QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskPopup.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskPopup.h"
7#include "QskAspect.h"
8#include "QskInputGrabber.h"
9#include "QskQuick.h"
10#include "QskWindow.h"
11#include "QskEvent.h"
12#include "QskPlatform.h"
13#include "QskHintAnimator.h"
14
15#include <qpa/qplatformintegration.h>
16
17QSK_QT_PRIVATE_BEGIN
18#include <private/qquickwindow_p.h>
19#include <private/qquickitem_p.h>
20QSK_QT_PRIVATE_END
21
22QSK_SUBCONTROL( QskPopup, Overlay )
23QSK_SYSTEM_STATE( QskPopup, Closed, QskAspect::FirstSystemState << 1 )
24
25static void qskSetFocus( QQuickItem* item, bool on )
26{
27 if ( item->window() == nullptr )
28 return;
29
30 /*
31 For unknown reasons Qt::PopupFocusReason is blocked inside of
32 QQuickItem::setFocus. So let's bypass it calling
33 QQuickWindowPrivate::setFocusInScope/clearFocusInScope directly,
34 */
35
36 if ( const auto scope = qskNearestFocusScope( item ) )
37 {
38#if QT_VERSION >= QT_VERSION_CHECK( 6, 1, 0 )
39 auto dw = QQuickItemPrivate::get( item )->deliveryAgentPrivate();
40#else
41 auto dw = QQuickWindowPrivate::get( item->window() );
42#endif
43 if ( on )
44 {
45 dw->setFocusInScope( scope, item, Qt::PopupFocusReason );
46 }
47 else
48 {
49 // to avoid an assertion we have to check if we have the subFocus
50 if ( QQuickItemPrivate::get( scope )->subFocusItem == item )
51 dw->clearFocusInScope( scope, item, Qt::PopupFocusReason );
52 }
53 }
54}
55
56static inline void qskSendPopupEvent(
57 QQuickWindow* window, QskPopup* popup, bool on )
58{
59 if ( window )
60 {
61 const auto type = on ? QskEvent::PopupAdded : QskEvent::PopupRemoved;
62
63 QskPopupEvent event( type, popup );
64 QCoreApplication::sendEvent( window, &event );
65 }
66}
67
68static bool qskReplayMousePress()
69{
70 if ( const auto pf = qskPlatformIntegration() )
71 {
72 const auto styleHint = QPlatformIntegration::ReplayMousePressOutsidePopup;
73 return pf->styleHint( styleHint ).toBool();
74 }
75
76 return false;
77}
78
79static inline QskAspect qskEffectiveFadingAspect( const QskPopup* popup )
80{
81 auto aspect = popup->fadingAspect();
82 aspect.setSubcontrol( popup->effectiveSubcontrol( aspect.subControl() ) );
83
84 return aspect;
85}
86
87static void qskStartFading( QskPopup* popup, bool on )
88{
89 const auto aspect = qskEffectiveFadingAspect( popup );
90
91 auto hint = popup->animationHint( aspect );
92
93 if ( hint.isValid() )
94 {
95 hint.updateFlags = QskAnimationHint::UpdatePolish | QskAnimationHint::UpdateNode;
96
97 const qreal from = on ? 0.0 : 1.0;
98 const qreal to = on ? 1.0 : 0.0;
99
100 popup->startTransition( aspect, hint, from, to );
101 }
102}
103
104namespace
105{
106 class InputGrabber final : public QskInputGrabber
107 {
108 using Inherited = QskInputGrabber;
109
110 public:
111 InputGrabber( QskPopup* parent )
112 : Inherited( parent )
113 {
114 }
115
116 protected:
117 bool event( QEvent* event ) override
118 {
119 bool ok = Inherited::event( event );
120
121 const int eventType = event->type();
122
123 if ( eventType == QEvent::MouseButtonPress )
124 {
125 if ( auto popup = static_cast< QskPopup* >( parentItem() ) )
126 {
127 if ( event->isAccepted() &&
128 ( popup->popupFlags() & QskPopup::CloseOnPressOutside ) )
129 {
130 popup->close();
131
132 if ( qskReplayMousePress() )
133 {
134 // TODO ...
135 }
136 }
137 }
138 }
139 else if ( eventType == QskEvent::GeometryChange )
140 {
141 if ( auto popup = static_cast< QskPopup* >( parentItem() ) )
142 {
143 if ( popup->hasOverlay() )
144 popup->update();
145 }
146 }
147
148 return ok;
149 }
150 };
151}
152
153class QskPopup::PrivateData
154{
155 public:
156 PrivateData()
157 : flags( 0 )
158 , isModal( false )
159 , autoGrabFocus( true )
160 , handoverFocus( true )
161 {
162 }
163
164 InputGrabber* inputGrabber = nullptr;
165
166 uint priority = 0;
167
168 int flags : 4;
169 bool isModal : 1;
170
171 const bool autoGrabFocus : 1;
172 const bool handoverFocus : 1;
173};
174
175QskPopup::QskPopup( QQuickItem* parent )
176 : Inherited( parent )
177 , m_data( new PrivateData() )
178{
179 // initially the popup is closed and invisible
180 Inherited::setVisible( false );
181 setSkinStateFlag( QskPopup::Closed );
182
183 /*
184 We need to stop event propagation.
185
186 Unfortunatly derived classes can't use setAcceptedMouseButtons anymore.
187 Need to think about a solution TODO ...
188 */
189
190 setAcceptedMouseButtons( Qt::AllButtons );
191 setWheelEnabled( true );
192
193 // we don't want to be resized by layout code
194 setPlacementPolicy( QskPlacementPolicy::Ignore );
195
196 setFlag( ItemIsFocusScope, true );
197 setTabFence( true );
198 setFocusPolicy( Qt::StrongFocus );
199
200 /*
201 sending a notification, that can be used to register popups
202 for some sort of popup/window management
203 */
204 qskSendPopupEvent( window(), this, true );
205}
206
207QskPopup::~QskPopup()
208{
209 qskSendPopupEvent( window(), this, false );
210}
211
212void QskPopup::open()
213{
214 setOpen( true );
215}
216
217void QskPopup::close()
218{
219 setOpen( false );
220}
221
222void QskPopup::toggle()
223{
224 setOpen( !isOpen() );
225}
226
227void QskPopup::setOpen( bool on )
228{
229 if ( on == isOpen() )
230 return;
231
232 if ( on )
233 QskControl::setVisible( on );
234
235 setSkinStateFlag( QskPopup::Closed, !on );
236
237 Q_EMIT openChanged( on );
238
239 if ( on )
240 Q_EMIT opened();
241 else
242 Q_EMIT closed();
243
244 if ( !on )
245 grabFocus( false );
246
247 qskStartFading( this, on );
248
249 if ( isFading() )
250 {
251 Q_EMIT fadingChanged( true );
252 }
253 else
254 {
255 if ( !on )
256 {
257 Inherited::setVisible( false );
258
259 if ( testPopupFlag( QskPopup::DeleteOnClose ) )
260 deleteLater();
261 }
262 }
263}
264
265bool QskPopup::isOpen() const
266{
267 return !hasSkinState( QskPopup::Closed );
268}
269
270QskAspect QskPopup::fadingAspect() const
271{
272 return QskAspect();
273}
274
275bool QskPopup::isFading() const
276{
277 const auto aspect = qskEffectiveFadingAspect( this );
278 return runningHintAnimator( aspect ) != nullptr;
279}
280
281qreal QskPopup::fadingFactor() const
282{
283 const auto aspect = qskEffectiveFadingAspect( this );
284 if ( auto animator = runningHintAnimator( aspect ) )
285 return animator->currentValue().value< qreal >();
286
287 return isOpen() ? 1.0 : 0.0;
288}
289
290QRectF QskPopup::overlayRect() const
291{
292 if ( hasOverlay() && m_data->inputGrabber )
293 return m_data->inputGrabber->grabberRect();
294
295 return QRectF();
296}
297
298void QskPopup::updateInputGrabber()
299{
300 if ( parentItem() && isVisible() &&
301 ( isModal() || testPopupFlag( CloseOnPressOutside ) ) )
302 {
303 if ( m_data->inputGrabber == nullptr )
304 {
305 const auto children = childItems();
306 m_data->inputGrabber = new InputGrabber( this );
307
308 if ( !children.isEmpty() )
309 {
310 /*
311 Even if the input grabber has no content it has an effect
312 on QQuickItem::childAt. Also tools like Squish struggle with
313 sorting out items without content.
314 So let's better move the grabber to the beginning.
315 */
316 m_data->inputGrabber->stackBefore( children.first() );
317 }
318 }
319 }
320 else
321 {
322 if ( m_data->inputGrabber )
323 {
324 /*
325 In QQuickWindowPrivate::deliverPressOrReleaseEvent ( 5.12 )
326 might crash, when we delete the grabber as a result of a
327 mouse event somewehere below the popup.
328 */
329 m_data->inputGrabber->setParentItem( nullptr );
330 m_data->inputGrabber->setParent( nullptr );
331 m_data->inputGrabber->deleteLater();
332
333 m_data->inputGrabber = nullptr;
334 }
335 }
336}
337
339{
340 if ( isVisible() && !isInitiallyPainted() )
341 {
342 /*
343 Usually we suppress transitions, when a control has never been
344 painted before as there is no valid starting point. Popups are
345 different as we want to have smooth fade/slide appearances.
346 */
347 if ( ( aspect.value() == 0 ) )
348 return true;
349
350 if ( aspect.subControl() == qskEffectiveFadingAspect( this ).subControl() )
351 return true;
352
353 if ( aspect.subControl() == effectiveSubcontrol( QskPopup::Overlay ) )
354 return true;
355 }
356
357 return Inherited::isTransitionAccepted( aspect );
358}
359
360void QskPopup::setPriority( uint priority )
361{
362 if ( m_data->priority != priority )
363 {
364 m_data->priority = priority;
365 Q_EMIT priorityChanged( priority );
366 }
367}
368
369uint QskPopup::priority() const
370{
371 return m_data->priority;
372}
373
374void QskPopup::setModal( bool on )
375{
376 if ( on == m_data->isModal )
377 return;
378
379 m_data->isModal = on;
380 updateInputGrabber();
381
382 Q_EMIT modalChanged( on );
383}
384
385bool QskPopup::isModal() const
386{
387 return m_data->isModal;
388}
389
390void QskPopup::setPopupFlags( PopupFlags flags )
391{
392 const auto newFlags = static_cast< int >( flags );
393
394 if ( newFlags != m_data->flags )
395 {
396 m_data->flags = newFlags;
397 updateInputGrabber();
398 }
399}
400
401QskPopup::PopupFlags QskPopup::popupFlags() const
402{
403 return static_cast< PopupFlags >( m_data->flags );
404}
405
406void QskPopup::setPopupFlag( PopupFlag flag, bool on )
407{
408 auto flags = m_data->flags;
409
410 if ( on )
411 flags |= flag;
412 else
413 flags &= ~flag;
414
415 setPopupFlags( PopupFlags( flags ) );
416}
417
418bool QskPopup::testPopupFlag( PopupFlag flag ) const
419{
420 return m_data->flags & flag;
421}
422
423void QskPopup::setOverlay( bool on )
424{
425 if ( setFlagHint( Overlay | QskAspect::Style, on ) )
426 Q_EMIT overlayChanged( on );
427}
428
429void QskPopup::resetOverlay()
430{
431 if ( resetSkinHint( Overlay | QskAspect::Style ) )
432 Q_EMIT overlayChanged( hasOverlay() );
433}
434
435bool QskPopup::hasOverlay() const
436{
437 return flagHint< bool >( QskPopup::Overlay | QskAspect::Style, true );
438}
439
440void QskPopup::grabFocus( bool on )
441{
442 if ( on == hasFocus() )
443 return;
444
445 if ( on )
446 {
447 qskSetFocus( this, true );
448 }
449 else
450 {
451 QQuickItem* successor = nullptr;
452
453 if ( m_data->handoverFocus )
454 {
455 /*
456 Qt/Quick does not handover the focus to another item,
457 when the active focus gets lost. For the situation of
458 a popup being closed we try to do it.
459 */
460 successor = focusSuccessor();
461 }
462
463 if ( successor )
464 qskSetFocus( successor, true );
465
466 if ( hasFocus() )
467 qskSetFocus( this, false );
468 }
469}
470
471bool QskPopup::event( QEvent* event )
472{
473 bool ok = Inherited::event( event );
474
475 switch ( static_cast< int >( event->type() ) )
476 {
477 case QEvent::KeyPress:
478 case QEvent::KeyRelease:
479
480 case QEvent::Wheel:
481
482 case QEvent::MouseButtonPress:
483 case QEvent::MouseMove:
484 case QEvent::MouseButtonRelease:
485
486 case QEvent::HoverEnter:
487 case QEvent::HoverLeave:
488 {
489 // swallow the event
490 event->accept();
491 if ( auto w = qobject_cast< QskWindow* >( window() ) )
492 w->setEventAcceptance( QskWindow::EventPropagationStopped );
493
494 break;
495 }
496 case QskEvent::Animator:
497 {
498 const auto animatorEvent = static_cast< QskAnimatorEvent* >( event );
499
500 if ( ( animatorEvent->state() == QskAnimatorEvent::Terminated )
501 && ( animatorEvent->aspect() == qskEffectiveFadingAspect( this ) ) )
502 {
503 if ( !isOpen() )
504 {
505 Inherited::setVisible( false );
506
507 if ( testPopupFlag( QskPopup::DeleteOnClose ) )
508 deleteLater();
509 }
510
511 Q_EMIT fadingChanged( false );
512 }
513
514 break;
515 }
516 default:
517 {
518 /*
519 Don't accept touch events otherwise we don't receive the
520 synthesized mouse events and need to handle both type of
521 events from now on.
522 But by accepting the mouse event propagation of the touch
523 events also stops.
524 */
525
526 break;
527 }
528 }
529
530 return ok;
531}
532
533void QskPopup::keyPressEvent( QKeyEvent* event )
534{
535 if ( qskIsStandardKeyInput( event, QKeySequence::Cancel ) )
536 {
537 close();
538 return;
539 }
540
541 return Inherited::keyPressEvent( event );
542}
543
544void QskPopup::focusInEvent( QFocusEvent* event )
545{
546 Inherited::focusInEvent( event );
547
548 if ( isFocusScope() && isTabFence() && ( scopedFocusItem() == nullptr ) )
549 {
550 if ( event->reason() == Qt::PopupFocusReason )
551 {
552 /*
553 When receiving the focus we need to have a focused
554 item, so that the tab focus chain has a starting point.
555
556 But we only do it when the reason is Qt::PopupFocusReason
557 as we also receive focus events during the process of reparenting
558 children and setting the focus there can leave the item tree
559 in an invalid state.
560 */
561
562 if ( auto focusItem = nextItemInFocusChain( true ) )
563 {
564 if ( !qskIsItemInDestructor( focusItem )
565 && qskIsAncestorOf( this, focusItem ) )
566 {
567 focusItem->setFocus( true );
568 }
569 }
570 }
571 }
572}
573
574void QskPopup::focusOutEvent( QFocusEvent* event )
575{
576 Inherited::focusOutEvent( event );
577}
578
579QQuickItem* QskPopup::focusSuccessor() const
580{
581 if ( const auto scope = qskNearestFocusScope( this ) )
582 {
583 const auto children = qskPaintOrderChildItems( scope );
584 for ( auto it = children.crbegin(); it != children.crend(); ++it )
585 {
586 auto child = *it;
587
588 if ( ( child != this ) && child->isFocusScope() &&
589 child->activeFocusOnTab() && child->isVisible() )
590 {
591 return child;
592 }
593 }
594 }
595
596 return nullptr;
597}
598
600{
601 if ( m_data->autoGrabFocus )
602 {
603 // What to do, when we are hidden below another popup ??
604 grabFocus( true );
605 }
606
608}
609
610void QskPopup::itemChange( QQuickItem::ItemChange change,
611 const QQuickItem::ItemChangeData& value )
612{
613 Inherited::itemChange( change, value );
614
615 if ( change == QQuickItem::ItemVisibleHasChanged )
616 {
617 updateInputGrabber();
618
619 if ( value.boolValue )
620 {
621 polish(); // so that aboutToShow is called. TODO ...
622 }
623 else
624 {
625 grabFocus( false );
626 }
627 }
628 else if ( change == QQuickItem::ItemParentHasChanged )
629 {
630 delete m_data->inputGrabber;
631 m_data->inputGrabber = nullptr;
632
633 updateInputGrabber();
634 }
635}
636
638{
639 qskSendPopupEvent( event->oldWindow(), this, false );
640 qskSendPopupEvent( event->window(), this, true );
641
643}
644
645int QskPopup::execPopup()
646{
647 class EventLoop : public QEventLoop
648 {
649 public:
650 EventLoop( QskPopup* popup )
651 : QEventLoop( popup )
652 {
653 /*
654 We want popup being the parent, so that the loop can be found
655 by popup->findChild< QEventLoop* >()
656 */
657
658 connect( popup, &QObject::destroyed, this, &EventLoop::reject );
659 connect( popup, &QskPopup::fadingChanged, this, &EventLoop::maybeQuit );
660 connect( popup, &QskPopup::openChanged, this, &EventLoop::maybeQuit );
661 }
662
663 private:
664 void reject()
665 {
666 setParent( nullptr );
667 QEventLoop::exit( 1 );
668 }
669
670 void maybeQuit()
671 {
672 if ( auto popup = qobject_cast< const QskPopup* >( parent() ) )
673 {
674 if ( popup->isOpen() || popup->isFading() )
675 return;
676 }
677
678 QEventLoop::exit( 0 );
679 }
680 };
681
682 if ( isOpen() || isFading() )
683 {
684 qWarning() << "QskPopup::exec: popup is already opened";
685 return -1;
686 }
687
688 if ( priority() > 0 )
689 {
690 qWarning( "QskPopup::exec for a popup with non default priority." );
691 }
692
693 open();
694
695 if ( window() == nullptr )
696 {
697 qWarning( "trying to exec a popup without window." );
698 return -1;
699 }
700
701 if ( auto mouseGrabber = window()->mouseGrabberItem() )
702 {
703 // when being called from QQuickWindow::mouseReleaseEvent
704 // the mouse grabber has not yet been released.
705
706 if( !qskIsAncestorOf( this, mouseGrabber ) )
707 qskUngrabMouse( mouseGrabber );
708 }
709
710 return EventLoop( this ).exec( QEventLoop::DialogExec );
711}
712
713#include "moc_QskPopup.cpp"
Lookup key for a QskSkinHintTable.
Definition QskAspect.h:15
@ FirstSystemState
Definition QskAspect.h:115
constexpr quint64 value() const noexcept
Definition QskAspect.h:402
constexpr Subcontrol subControl() const noexcept
Definition QskAspect.h:417
void itemChange(ItemChange, const ItemChangeData &) override
virtual void aboutToShow()
Definition QskItem.cpp:1091
bool isTabFence() const
Definition QskItem.cpp:410
bool isInitiallyPainted() const
Definition QskItem.cpp:578
virtual void windowChangeEvent(QskWindowChangeEvent *)
Definition QskItem.cpp:851
void aboutToShow() override
Definition QskPopup.cpp:599
bool isTransitionAccepted(QskAspect) const override
Additional check if an transition should be started.
Definition QskPopup.cpp:338
void windowChangeEvent(QskWindowChangeEvent *) override
Definition QskPopup.cpp:637
bool resetSkinHint(QskAspect)
Remove a hint from the local hint table.
virtual bool isTransitionAccepted(QskAspect) const
Additional check if an transition should be started.
QskAnimationHint animationHint(QskAspect, QskSkinHintStatus *=nullptr) const
void setSkinStateFlag(QskAspect::State, bool on=true)
bool setFlagHint(QskAspect, int flag)
Sets a flag hint.
QskAspect::Subcontrol effectiveSubcontrol(QskAspect::Subcontrol) const