/****************************************************************************** * QSkinny - Copyright (C) 2016 Uwe Rathmann * This file may be used under the terms of the QSkinny License, Version 1.0 *****************************************************************************/ #include "QskGridLayoutEngine.h" #include "QskLayoutHint.h" #include "QskLayoutChain.h" #include "QskSizePolicy.h" #include "QskQuick.h" #include #include #include static inline qreal qskSegmentLength( const QskLayoutChain::Segments& s, int start, int end ) { return s[ end ].start - s[ start ].start + s[ end ].length; } namespace { class Settings { public: class Setting { public: inline bool isDefault() const { return m_stretch < 0 && m_hint.isDefault(); } bool setStretch( int stretch ) { if ( stretch != m_stretch ) { m_stretch = stretch; return true; } return false; } bool setHint( Qt::SizeHint which, qreal size ) { if ( size != m_hint.size( which ) ) { m_hint.setSize( which, size ); return true; } return false; } QskLayoutChain::CellData cell() const { QskLayoutChain::CellData cell; cell.hint = m_hint.normalized(); cell.stretch = m_stretch; cell.canGrow = m_stretch != 0; cell.isValid = true; return cell; } inline int stretch() const { return m_stretch; } inline QskLayoutHint hint() const { return m_hint; } int position = -1; private: int m_stretch = -1; QskLayoutHint m_hint; }; void clear() { m_settings.clear(); } bool setStretchAt( int index, int stretch ) { auto setStretch = [stretch]( Setting& s ) { return s.setStretch( stretch ); }; return setValueAt( index, setStretch ); } bool setHintAt( int index, Qt::SizeHint which, qreal size ) { auto setHint = [which, size]( Setting& s ) { return s.setHint( which, size ); }; return setValueAt( index, setHint ); } Setting settingAt( int index ) const { auto it = lowerBound( index ); if ( it != m_settings.end() ) return *it; return Setting(); } const std::vector< Setting >& settings() const { return m_settings; } private: inline bool setValueAt( int pos, const std::function< bool( Setting& ) > modify ) { if ( pos < 0 ) return false; bool isModified; auto it = lowerBound( pos ); if ( it != m_settings.end() && it->position == pos ) { isModified = modify( *it ); if ( isModified && it->isDefault() ) m_settings.erase( it ); } else { Setting setting; isModified = modify( setting ); if ( isModified ) { setting.position = pos; m_settings.insert( it, setting ); } } return isModified; } inline std::vector< Setting >::iterator lowerBound( int index ) const { auto cmp = []( const Setting& setting, const int& pos ) { return setting.position < pos; }; auto& settings = const_cast< std::vector< Setting >& >( m_settings ); return std::lower_bound( settings.begin(), settings.end(), index, cmp ); } std::vector< Setting > m_settings; // a flat map }; } namespace { class Element { public: Element( QQuickItem*, const QRect& ); Element( const QSizeF& spacing, const QRect& ); Element& operator=( const Element& ); QSizeF spacing() const; QQuickItem* item() const; QRect grid() const; void setGrid( const QRect& ); QRect minimumGrid() const; bool isIgnored() const; QskLayoutChain::CellData cell( Qt::Orientation ) const; void transpose(); private: union { QQuickItem* m_item; QSizeF m_spacing; }; QRect m_grid; bool m_isSpacer; }; } Element::Element( QQuickItem* item, const QRect& grid ) : m_item( item ) , m_grid( grid ) , m_isSpacer( false ) { } Element::Element( const QSizeF& spacing, const QRect& grid ) : m_spacing( spacing ) , m_grid( grid ) , m_isSpacer( true ) { } Element& Element::operator=( const Element& other ) { m_isSpacer = other.m_isSpacer; if ( other.m_isSpacer ) m_spacing = other.m_spacing; else m_item = other.m_item; m_grid = other.m_grid; return *this; } inline QSizeF Element::spacing() const { return m_isSpacer ? m_spacing : QSizeF(); } inline QQuickItem* Element::item() const { return m_isSpacer ? nullptr : m_item; } QRect Element::grid() const { return m_grid; } void Element::setGrid( const QRect& grid ) { m_grid = grid; } QRect Element::minimumGrid() const { return QRect( m_grid.left(), m_grid.top(), qMax( m_grid.width(), 1 ), qMax( m_grid.height(), 1 ) ); } bool Element::isIgnored() const { return !( m_isSpacer || qskIsVisibleToLayout( m_item ) ); } QskLayoutChain::CellData Element::cell( Qt::Orientation orientation ) const { QskLayoutChain::CellData cell; cell.isValid = true; if ( m_isSpacer ) { const qreal value = ( orientation == Qt::Horizontal ) ? m_spacing.width() : m_spacing.height(); cell.hint.setMinimum( value ); cell.hint.setPreferred( value ); cell.hint.setMaximum( value ); } else { const auto policy = qskSizePolicy( m_item ).policy( orientation ); cell.canGrow = policy & QskSizePolicy::GrowFlag; if ( policy & QskSizePolicy::ExpandFlag ) cell.stretch = 1; } return cell; } void Element::transpose() { m_grid.setRect( m_grid.top(), m_grid.left(), m_grid.height(), m_grid.width() ); } class QskGridLayoutEngine::PrivateData { public: inline Element* elementAt( int index ) const { const int count = this->elements.size(); if ( index < 0 || index >= count ) return nullptr; return const_cast< Element* >( &this->elements[index] ); } int insertElement( QQuickItem* item, QSizeF spacing, QRect grid ) { // -1 means unlimited, while 0 does not make any sense if ( grid.width() == 0 ) grid.setWidth( 1 ); if ( grid.height() == 0 ) grid.setHeight( 1 ); if ( item ) { elements.push_back( Element( item, grid ) ); } else { if ( spacing.width() < 0.0 ) spacing.setWidth( 0.0 ); if ( spacing.height() < 0.0 ) spacing.setHeight( 0.0 ); elements.push_back( Element( spacing, grid ) ); } grid = effectiveGrid( elements.back() ); rowCount = qMax( rowCount, grid.bottom() + 1 ); columnCount = qMax( columnCount, grid.right() + 1 ); return this->elements.size() - 1; } QRect effectiveGrid( const Element& element ) const { QRect r = element.grid(); if ( r.width() <= 0 ) r.setRight( qMax( this->columnCount - 1, r.left() ) ); if ( r.height() <= 0 ) r.setBottom( qMax( this->rowCount - 1, r.top() ) ); return r; } Settings& settings( Qt::Orientation orientation ) const { auto that = const_cast< PrivateData* >( this ); return ( orientation == Qt::Horizontal ) ? that->columnSettings : that->rowSettings; } std::vector< Element > elements; Settings rowSettings; Settings columnSettings; int rowCount = 0; int columnCount = 0; }; QskGridLayoutEngine::QskGridLayoutEngine() : m_data( new PrivateData() ) { } QskGridLayoutEngine::~QskGridLayoutEngine() { } int QskGridLayoutEngine::count() const { return m_data->elements.size(); } bool QskGridLayoutEngine::setStretchFactor( int pos, int stretch, Qt::Orientation orientation ) { if ( pos < 0 ) return false; if ( stretch < 0 ) stretch = -1; if ( m_data->settings( orientation ).setStretchAt( pos, stretch ) ) { invalidate(); return true; } return false; } int QskGridLayoutEngine::stretchFactor( int pos, Qt::Orientation orientation ) const { const auto setting = m_data->settings( orientation ).settingAt( pos ); return ( setting.position == pos ) ? setting.stretch() : 0; } bool QskGridLayoutEngine::setRowSizeHint( int row, Qt::SizeHint which, qreal height ) { if ( m_data->rowSettings.setHintAt( row, which, height ) ) { invalidate(); return true; } return false; } qreal QskGridLayoutEngine::rowSizeHint( int row, Qt::SizeHint which ) const { const auto& settings = m_data->rowSettings; return settings.settingAt( row ).hint().size( which ); } bool QskGridLayoutEngine::setColumnSizeHint( int column, Qt::SizeHint which, qreal width ) { if ( m_data->columnSettings.setHintAt( column, which, width ) ) { invalidate(); return true; } return false; } qreal QskGridLayoutEngine::columnSizeHint( int column, Qt::SizeHint which ) const { const auto& settings = m_data->columnSettings; return settings.settingAt( column ).hint().size( which ); } int QskGridLayoutEngine::insertItem( QQuickItem* item, const QRect& grid ) { invalidate(); return m_data->insertElement( item, QSizeF(), grid ); } int QskGridLayoutEngine::insertSpacer( const QSizeF& spacing, const QRect& grid ) { return m_data->insertElement( nullptr, spacing, grid ); } bool QskGridLayoutEngine::removeAt( int index ) { const auto element = m_data->elementAt( index ); if ( element == nullptr ) return false; const auto grid = element->minimumGrid(); auto& elements = m_data->elements; elements.erase( elements.begin() + index ); // doing a lazy recalculation instead ?? if ( grid.bottom() >= m_data->rowCount || grid.right() >= m_data->columnCount ) { int maxRow = -1; int maxColumn = -1; for ( const auto& element : elements ) { const auto grid = element.minimumGrid(); maxRow = qMax( maxRow, grid.bottom() ); maxColumn = qMax( maxColumn, grid.right() ); } m_data->rowCount = maxRow + 1; m_data->columnCount = maxColumn + 1; } invalidate(); return true; } bool QskGridLayoutEngine::clear() { m_data->elements.clear(); m_data->rowSettings.clear(); m_data->columnSettings.clear(); invalidate(); return true; } int QskGridLayoutEngine::indexAt( int row, int column ) const { if ( row < m_data->rowCount && column < m_data->columnCount ) { for ( uint i = 0; i < m_data->elements.size(); i++ ) { const auto grid = m_data->effectiveGrid( m_data->elements[i] ); if ( grid.contains( column, row ) ) return i; } } return -1; } QQuickItem* QskGridLayoutEngine::itemAt( int index ) const { if ( const auto element = m_data->elementAt( index ) ) return element->item(); return nullptr; } QSizeF QskGridLayoutEngine::spacerAt( int index ) const { if ( const auto element = m_data->elementAt( index ) ) return element->spacing(); return QSizeF(); } QQuickItem* QskGridLayoutEngine::itemAt( int row, int column ) const { return itemAt( indexAt( row, column ) ); } bool QskGridLayoutEngine::setGridAt( int index, const QRect& grid ) { if ( auto element = m_data->elementAt( index ) ) { if ( element->grid() != grid ) { element->setGrid( grid ); invalidate(); return true; } } return false; } QRect QskGridLayoutEngine::gridAt( int index ) const { if ( auto element = m_data->elementAt( index ) ) return element->grid(); return QRect(); } QRect QskGridLayoutEngine::effectiveGridAt( int index ) const { if ( auto element = m_data->elementAt( index ) ) return m_data->effectiveGrid( *element ); return QRect(); } void QskGridLayoutEngine::invalidateElementCache() { } void QskGridLayoutEngine::layoutItems() { for ( const auto& element : m_data->elements ) { auto item = element.item(); if ( item && qskIsVisibleToParent( item ) ) { const auto grid = m_data->effectiveGrid( element ); layoutItem( item, grid ); } } } void QskGridLayoutEngine::transpose() { for ( auto& element : m_data->elements ) element.transpose(); qSwap( m_data->columnSettings, m_data->rowSettings ); invalidate(); } int QskGridLayoutEngine::effectiveCount( Qt::Orientation orientation ) const { return ( orientation == Qt::Horizontal ) ? m_data->columnCount : m_data->rowCount; } void QskGridLayoutEngine::setupChain( Qt::Orientation orientation, const QskLayoutChain::Segments& constraints, QskLayoutChain& chain ) const { /* We collect all information from the simple elements first before adding those that occupy more than one cell */ QVarLengthArray< const Element* > postponed; postponed.reserve( m_data->elements.size() ); for ( const auto& element : m_data->elements ) { if ( element.isIgnored() ) continue; auto grid = m_data->effectiveGrid( element ); if ( orientation == Qt::Horizontal ) grid.setRect( grid.y(), grid.x(), grid.height(), grid.width() ); if ( grid.height() == 1 ) { qreal constraint = -1.0; if ( !constraints.isEmpty() ) constraint = qskSegmentLength( constraints, grid.left(), grid.right() ); auto cell = element.cell( orientation ); if ( element.item() ) cell.hint = layoutHint( element.item(), orientation, constraint ); chain.expandCell( grid.top(), cell ); } else { postponed += &element; } } const auto& settings = m_data->settings( orientation ); for ( const auto& setting : settings.settings() ) { if ( setting.position >= chain.count() ) { qWarning() << "Extra settings for exceeding rows/columns not yet implemented."; continue; } chain.shrinkCell( setting.position, setting.cell() ); } for ( const auto element : postponed ) { auto grid = m_data->effectiveGrid( *element ); if ( orientation == Qt::Horizontal ) grid.setRect( grid.y(), grid.x(), grid.height(), grid.width() ); qreal constraint = -1.0; if ( !constraints.isEmpty() ) constraint = qskSegmentLength( constraints, grid.left(), grid.right() ); auto cell = element->cell( orientation ); cell.hint = layoutHint( element->item(), orientation, constraint ); chain.expandCells( grid.top(), grid.height(), cell ); } }