/****************************************************************************** * QSkinny - Copyright (C) 2016 Uwe Rathmann * SPDX-License-Identifier: BSD-3-Clause *****************************************************************************/ #include "QskDrawer.h" #include "QskAspect.h" #include "QskAnimationHint.h" #include "QskQuick.h" #include "QskEvent.h" #include "QskPanGestureRecognizer.h" #include "QskGesture.h" #include #include QSK_QT_PRIVATE_BEGIN #include #include QSK_QT_PRIVATE_END /* Only used for the sliding in animation. Do we want to introduce a specific panel as background ??? */ QSK_SUBCONTROL( QskDrawer, Panel ) static inline qreal qskDefaultDragMargin() { // a skin hint ??? return QGuiApplication::styleHints()->startDragDistance(); } static inline void qskCatchMouseEvents( QQuickItem* item ) { #if 1 // manipulating other items - do we really want to do this ? item->setAcceptedMouseButtons( Qt::LeftButton ); item->setFiltersChildMouseEvents( true ); #endif } static bool qskCheckDirection( Qt::Edge edge, const QskPanGesture* gesture ) { const auto degrees = gesture->angle(); switch( edge ) { case Qt::LeftEdge: return ( degrees < 90.0 ) || ( degrees ) > 270.0; case Qt::RightEdge: return ( degrees > 90.0 ) && ( degrees < 270.0 ); case Qt::TopEdge: return degrees > 180.0; case Qt::BottomEdge: return degrees < 180.0; } return false; } static void qskLayoutDrawer( const QRectF& rect, QskDrawer* drawer ) { const auto size = qskSizeConstraint( drawer, Qt::PreferredSize ); QRectF r( 0.0, 0.0, size.width(), size.height() ); switch( drawer->edge() ) { case Qt::LeftEdge: { r.setHeight( rect.height() ); r.moveRight( rect.left() + size.width() ); break; } case Qt::RightEdge: { r.setHeight( rect.height() ); r.moveLeft( rect.right() - size.width() ); break; } case Qt::TopEdge: { r.setWidth( rect.width() ); r.moveBottom( rect.top() + size.height() ); break; } case Qt::BottomEdge: { r.setWidth( rect.width() ); r.moveTop( rect.bottom() - size.height() ); break; } } drawer->setGeometry( r ); } static inline QRectF qskSlidingRect( const QSizeF& size, Qt::Edge edge, qreal ratio ) { auto x = 0.0; auto y = 0.0; ratio = 1.0 - ratio; switch( edge ) { case Qt::LeftEdge: x = -ratio * size.width(); break; case Qt::RightEdge: x = ratio * size.width(); break; case Qt::TopEdge: y = -ratio * size.height(); break; case Qt::BottomEdge: y = ratio * size.height(); break; } return QRectF( x, y, size.width(), size.height() ); } namespace { class GeometryListener final : public QQuickItemChangeListener { public: GeometryListener( QQuickItem* item, QQuickItem* adjustedItem ) : m_item( item ) , m_adjustedItem( adjustedItem ) { adjust(); setEnabled( true ); } ~GeometryListener() { setEnabled( false ); } private: void itemGeometryChanged( QQuickItem*, QQuickGeometryChange, const QRectF& ) override { adjust(); } private: void adjust() { #if 0 const auto pos = m_adjustedItem->mapFromItem( m_item, QPointF() ); qskSetItemGeometry( m_adjustedItem, pos.x(), pos.y(), m_item->width(), m_item->height() ); #else qskLayoutDrawer( QRectF( QPointF(), m_item->size() ), qobject_cast< QskDrawer* >( m_adjustedItem ) ); #endif } void setEnabled( bool on ) { const auto changeTypes = QQuickItemPrivate::Geometry; auto d = QQuickItemPrivate::get( m_item ); if ( on ) d->addItemChangeListener( this, changeTypes ); else d->removeItemChangeListener( this, changeTypes ); } QQuickItem* m_item; QQuickItem* m_adjustedItem; }; } namespace { class GestureRecognizer : public QskPanGestureRecognizer { using Inherited = QskPanGestureRecognizer; public: GestureRecognizer( QskDrawer* drawer ) : QskPanGestureRecognizer( drawer ) { setWatchedItem( drawer->parentItem() ); setTargetItem( drawer ); } protected: bool isAcceptedPos( const QPointF& pos ) const override { auto drawer = qobject_cast< const QskDrawer* >( targetItem() ); if ( drawer->isTransitioning() ) return false; auto rect = qskItemRect( watchedItem() ); if ( !drawer->isOpen() ) { const auto dragMargin = drawer->dragMargin(); if ( dragMargin <= 0.0 ) return false; switch( drawer->edge() ) { case Qt::LeftEdge: rect.setRight( rect.left() + dragMargin ); break; case Qt::RightEdge: rect.setLeft( rect.right() - dragMargin ); break; case Qt::TopEdge: rect.setBottom( rect.top() + dragMargin ); break; case Qt::BottomEdge: rect.setTop( rect.bottom() - dragMargin ); break; } } return rect.contains( pos ); } }; } class QskDrawer::PrivateData { public: inline void resetListener( QskDrawer* drawer ) { delete listener; listener = nullptr; if ( drawer->parentItem() && drawer->isVisible() ) listener = new GeometryListener( drawer->parentItem(), drawer ); } Qt::Edge edge = Qt::LeftEdge; GestureRecognizer* gestureRecognizer = nullptr; GeometryListener* listener = nullptr; qreal dragMargin = qskDefaultDragMargin(); }; QskDrawer::QskDrawer( QQuickItem* parentItem ) : Inherited ( parentItem ) , m_data( new PrivateData ) { #if 1 setZ( 1 ); #endif setAutoLayoutChildren( true ); setInteractive( true ); setPopupFlag( PopupFlag::CloseOnPressOutside, true ); setTransitionAspect( Panel | QskAspect::Position | QskAspect::Metric ); /* The drawer wants to be on top of the parent - not being layouted into its layoutRect(). So we opt out and do the layout updates manually. */ setPlacementPolicy( QskPlacementPolicy::Ignore ); m_data->resetListener( this ); connect( this, &QskPopup::openChanged, this, &QskDrawer::setSliding ); connect( this, &QskPopup::transitioningChanged, this, &QskDrawer::setIntermediate ); } QskDrawer::~QskDrawer() { delete m_data->listener; } Qt::Edge QskDrawer::edge() const { return m_data->edge; } void QskDrawer::setEdge( Qt::Edge edge ) { if( m_data->edge == edge ) return; update(); m_data->edge = edge; edgeChanged( edge ); } void QskDrawer::setInteractive( bool on ) { if ( on == isInteractive() ) return; if ( on ) { m_data->gestureRecognizer = new GestureRecognizer( this ); if ( parentItem() ) qskCatchMouseEvents( parentItem() ); } else { // how to revert qskCatchMouseEvents properly ??? delete m_data->gestureRecognizer; m_data->gestureRecognizer = nullptr; } Q_EMIT interactiveChanged( on ); } bool QskDrawer::isInteractive() const { return m_data->gestureRecognizer != nullptr; } void QskDrawer::setDragMargin( qreal margin ) { margin = std::max( margin, 0.0 ); if ( margin != m_data->dragMargin ) { m_data->dragMargin = margin; Q_EMIT dragMarginChanged( margin ); } } void QskDrawer::resetDragMargin() { setDragMargin( qskDefaultDragMargin() ); } qreal QskDrawer::dragMargin() const { return m_data->dragMargin; } void QskDrawer::gestureEvent( QskGestureEvent* event ) { if ( event->gesture()->type() == QskGesture::Pan ) { /* For the moment we treat the gesture like a swipe gesture without dragging the drawer when moving the mouse. TODO ... */ const auto gesture = static_cast< const QskPanGesture* >( event->gesture().get() ); if ( gesture->state() == QskGesture::Finished ) { const auto forwards = qskCheckDirection( m_data->edge, gesture ); if ( forwards != isOpen() ) setOpen( forwards ); } return; } Inherited::gestureEvent( event ); } QRectF QskDrawer::layoutRectForSize( const QSizeF& size ) const { qreal ratio; if ( isTransitioning() ) ratio = metric( transitionAspect() ); else ratio = isOpen() ? 1.0 : 0.0; return qskSlidingRect( size, m_data->edge, ratio ); } void QskDrawer::itemChange( QQuickItem::ItemChange change, const QQuickItem::ItemChangeData& value ) { Inherited::itemChange( change, value ); switch( static_cast< int >( change ) ) { case QQuickItem::ItemParentHasChanged: { if ( parentItem() && isInteractive() ) qskCatchMouseEvents( parentItem() ); m_data->resetListener( this ); break; } case QQuickItem::ItemVisibleHasChanged: { m_data->resetListener( this ); break; } } } void QskDrawer::setSliding( bool on ) { const qreal from = on ? 0.0 : 1.0; const qreal to = on ? 1.0 : 0.0; const auto aspect = transitionAspect(); auto hint = animationHint( aspect ); hint.updateFlags = QskAnimationHint::UpdatePolish; startTransition( aspect, hint, from, to ); } QRectF QskDrawer::clipRect() const { if ( !isTransitioning() ) return Inherited::clipRect(); /* When fading we want to clip against the edge, where the drawer slides in/out. However the size of the drawer is often smaller than the one of the parent and we would clip the overlay node and all content, that is located outside the drawer geometry. So we expand the clip rectangle to "unbounded" at the other edges. Note, that clipping against "rounded" rectangles can't be done properly by overloading clipRect. We would have to manipulate the clip node manually - like it is done in QskScrollArea. TODO .. */ constexpr qreal d = std::numeric_limits< short >::max(); QRectF r( -d, -d, 2.0 * d, 2.0 * d ); switch( m_data->edge ) { case Qt::LeftEdge: r.setLeft( 0.0 ); break; case Qt::RightEdge: r.setRight( width() ); break; case Qt::TopEdge: r.setTop( 0.0 ); break; case Qt::BottomEdge: r.setBottom( height() ); break; } return r; } void QskDrawer::setIntermediate( bool on ) { setClip( on ); Q_EMIT focusIndicatorRectChanged(); } QRectF QskDrawer::focusIndicatorRect() const { if ( isTransitioning() ) return QRectF(); return Inherited::focusIndicatorRect(); } #include "moc_QskDrawer.cpp"