/* * Copyright 2007 by Matias Valdenegro T. * Copyright 2007 by Robert Knight * Copyright 2008 by Olivier Goffart * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "boxlayout.h" #include #include #include #include #include "layoutanimator.h" namespace Plasma { class BoxLayout::Private { public: BoxLayout *const q; Direction direction; QList children; bool multiRow; int rowCount; int colCount() const { return ((children.count()-1)/rowCount)+1; } Private(BoxLayout *parent) : q(parent) , direction(LeftToRight), multiRow(false) , rowCount(1) { } // returns the component of 'size' in the expanding direction // of this layout qreal size(const QSizeF& size) const { switch (direction) { case LeftToRight: case RightToLeft: return size.width(); case TopToBottom: case BottomToTop: return size.height(); default: Q_ASSERT(false); return 0; } } // returns the component of 'size' in the other direction qreal size_o(const QSizeF& size) const { switch (direction) { case LeftToRight: case RightToLeft: return size.height(); case TopToBottom: case BottomToTop: return size.width(); default: Q_ASSERT(false); return 0; } } // returns the direction in which this layout expands // or shrinks Qt::Orientation expandingDirection() const { switch ( direction ) { case LeftToRight: case RightToLeft: return Qt::Horizontal; case TopToBottom: case BottomToTop: return Qt::Vertical; default: Q_ASSERT(false); return Qt::Horizontal; } } // returns the position from which layouting should // begin depending on the direction of this layout qreal startPos(const QRectF& geometry) const { switch ( direction ) { case LeftToRight: return geometry.left() + q->margin(LeftMargin); case TopToBottom: return geometry.top() + q->margin(TopMargin); case RightToLeft: return geometry.right() - q->margin(RightMargin); case BottomToTop: return geometry.bottom() - q->margin(BottomMargin); default: Q_ASSERT(false); return 0; } } // lays out an item // // 'geometry' the geometry of the layout // 'item' the item whoose geometry should be altered // 'pos' the position of the item (in the expanding direction of the layout) // 'size' the size of the item (in the expanding direction of the layout) // // returns the position for the next item in the layout // qreal layoutItem(const QRectF& geometry , LayoutItem *item , const qreal pos , qreal size, int row) { //qDebug() << "layoutItem: " << direction << "item size" << size; QRectF newGeometry; qreal newPos = 0; qreal top = 0; qreal height = 0; QSizeF minSize = item->minimumSize(); QSizeF maxSize = item->maximumSize(); switch ( direction ) { case LeftToRight: case RightToLeft: height = (geometry.height()-q->spacing()*(rowCount-1))/rowCount; top = geometry.top()+row*(height+q->spacing()); height = qBound(minSize.height(), height, maxSize.height()); break; case TopToBottom: case BottomToTop: height = (geometry.width()-q->spacing()*(rowCount-1))/rowCount; top = geometry.left()+row*(height+q->spacing()); height = qBound(minSize.width(), height, maxSize.width()); break; } switch ( direction ) { case LeftToRight: newGeometry = QRectF(pos,top,size,height); newPos = pos+size+q->spacing(); break; case RightToLeft: newGeometry = QRectF(geometry.width()-pos-size,top, size,height); newPos = pos-size-q->spacing(); break; case TopToBottom: newGeometry = QRectF(top,pos,height,size); newPos = pos+size+q->spacing(); break; case BottomToTop: newGeometry = QRectF(top,geometry.height()-pos-size, height,size); newPos = pos-size-q->spacing(); break; } // qDebug() << "Item geometry: " << newGeometry; if (q->animator()) { q->animator()->setGeometry(item, newGeometry); } else { item->setGeometry(newGeometry); } return newPos; } enum SizeType { MinSize, MaxSize, HintSize }; // this provides a + function which can be passed as the 'op' // argument to calculateSize static qreal sum(const qreal a , const qreal b) { return a+b; } // calcualtes a size hint or value for this layout // 'sizeType' - The item size ( minimum , maximum , hint ) to use // 'dir' - The direction component of the item size to use // 'op' - A function to apply to the size of each item in the layout // , usually qMax,qMin or sum template qreal calculateSize(SizeType sizeType , Qt::Orientation dir , T (*op)(const T,const T)) const { qreal value = 0; for ( int i = 0 ; i < children.count() ; i++ ) { QSizeF itemSize; switch ( sizeType ) { case MinSize: itemSize = children[i]->minimumSize(); break; case MaxSize: itemSize = children[i]->maximumSize(); break; case HintSize: itemSize = children[i]->sizeHint(); break; } if ( dir == Qt::Horizontal ) { value = op(value,itemSize.width()); } else { value = op(value,itemSize.height()); } } return value; } // calculates a size hint or value for this layout // 'calculateSizeType' specifies the value to be calculated QSizeF calculateSize(SizeType calculateSizeType) const { QSizeF result; const qreal totalSpacingC = q->spacing() * colCount()-1; const qreal totalSpacingR = q->spacing() * rowCount-1; switch ( direction ) { case LeftToRight: case RightToLeft: result = QSizeF(calculateSize(calculateSizeType,Qt::Horizontal,sum)/rowCount, calculateSize(calculateSizeType,Qt::Vertical,qMax)*rowCount); result.rwidth() += q->margin(LeftMargin) + q->margin(RightMargin) + totalSpacingC; result.rheight() += q->margin(TopMargin) + q->margin(BottomMargin) + totalSpacingR; break; case TopToBottom: case BottomToTop: result = QSizeF(calculateSize(calculateSizeType,Qt::Horizontal,qMax)/rowCount, calculateSize(calculateSizeType,Qt::Vertical,sum)*rowCount); result.rheight() += q->margin(TopMargin) + q->margin(BottomMargin) + totalSpacingC; result.rwidth() += q->margin(LeftMargin) + q->margin(RightMargin) + totalSpacingR; break; } return result; } }; BoxLayout::BoxLayout(Direction direction , LayoutItem *parent) : Layout(parent), d(new Private(this)) { d->direction = direction; } void BoxLayout::setDirection(Direction direction) { d->direction = direction; updateGeometry(); } BoxLayout::Direction BoxLayout::direction() const { return d->direction; } BoxLayout::~BoxLayout() { foreach (LayoutItem* item, d->children) { item->unsetManagingLayout(this); } delete d; } Qt::Orientations BoxLayout::expandingDirections() const { switch ( d->direction ) { case LeftToRight: case RightToLeft: return Qt::Horizontal; case TopToBottom: case BottomToTop: return Qt::Vertical; default: Q_ASSERT(false); return 0; } } int BoxLayout::count() const { return d->children.count(); } void BoxLayout::setAnimator(LayoutAnimator *animator) { Layout::setAnimator(animator); if (animator) { foreach (LayoutItem *item, d->children) { animator->setGeometry(item, item->geometry()); animator->setCurrentState(item, LayoutAnimator::StandardState); } } } void BoxLayout::insertItem(int index, LayoutItem *item) { if (!item || d->children.contains(item)) { return; } item->setManagingLayout(this); if (index == -1) { index = d->children.size(); } d->children.insert(index, item); if (animator()) { animator()->setCurrentState(item, LayoutAnimator::InsertedState); } updateGeometry(); } void BoxLayout::addItem(LayoutItem *item) { if (!item) { return; } insertItem(-1, item); } void BoxLayout::removeItem(LayoutItem *item) { if (!item) { return; } item->unsetManagingLayout(this); d->children.removeAll(item); if (animator()) { animator()->setCurrentState(item, LayoutAnimator::RemovedState); } updateGeometry(); } int BoxLayout::indexOf(LayoutItem *l) const { return d->children.indexOf(l); } LayoutItem *BoxLayout::itemAt(int i) const { if (i >= d->children.count()) { return 0; } return d->children[i]; } LayoutItem *BoxLayout::takeAt(int i) { if (i >= d->children.count()) { return 0; } return d->children.takeAt(i); // FIXME: This is never reached. Should it be called? updateGeometry(); } void BoxLayout::relayout() { QRectF margined = geometry().adjusted(margin(LeftMargin), margin(TopMargin), -margin(RightMargin), -margin(BottomMargin)); //qDebug() << "geo before " << geo << "and with margins" << margined << "margins" << margin(LeftMargin) // << margin(TopMargin) << -margin(RightMargin) << -margin(BottomMargin); //qDebug() << "Box layout beginning with geo" << geometry; //qDebug() << "This box max size" << maximumSize(); d->rowCount = 1; if(d->multiRow) { qreal minRowSize=1; qreal minWidth=0; for(int i = 0; i < d->children.count(); i++) { minRowSize = qMax(minRowSize, d->size_o(d->children[i]->minimumSize())); minWidth += d->size(d->children[i]->minimumSize()); } const qreal ratio = 2.25; //maybe this should not be hardcoded //we want the height of items be larger than the minimum size int maxRow = (d->size_o(margined.size()) + spacing()) / (minRowSize + spacing()); //we want enough rows to be able to fit each items width. int minRow = (minWidth + d->children.count() * spacing()) / (d->size(margined.size()) + spacing() + 0.1); //FIXME: this formula doesn't take the cellspacing in account // it should also try to "fill" before adding a row d->rowCount = 1 + sqrt(ratio * count() * d->size_o(margined.size()) / (d->size(margined.size())+1)); d->rowCount = qMax(minRow,d->rowCount); d->rowCount = qMin(maxRow,d->rowCount); d->rowCount = qMax(1,d->rowCount); } int colCount = d->colCount(); QVector sizes(colCount,0); QVector expansionSpace(colCount,0); qreal available = d->size(margined.size()) - spacing() * colCount; qreal perItemSize = available / colCount; // initial distribution of space to items for ( int i = 0 ; i < colCount ; i++ ) { qreal minItemSize=0; qreal maxItemSize=65536; bool isExpanding=true; qreal hint = 0; for(int f = i*d->rowCount; f < (i+1)*d->rowCount; f++) { if(f>=count()) { break; } const LayoutItem *item = d->children[f]; const bool itemExp = (item->expandingDirections() & d->expandingDirection()); isExpanding = isExpanding && itemExp; minItemSize = qMax(minItemSize,d->size(item->minimumSize())); maxItemSize = qMin(maxItemSize,d->size(item->maximumSize())); if(!itemExp) hint = qMax(hint,d->size(item->sizeHint())); } if ( isExpanding ) { sizes[i] = perItemSize; } else { sizes[i] = hint; } // qDebug() << "Layout max item " << i << "size: " << maxItemSize; sizes[i] = qMin( sizes[i] , maxItemSize ); sizes[i] = qMax( sizes[i] , minItemSize ); // qDebug() << "Available: " << available << "per item:" << perItemSize << // "Initial size: " << sizes[i]; if ( isExpanding ) { expansionSpace[i] = maxItemSize-sizes[i]; } else { expansionSpace[i] = 0; } available -= sizes[i]; // adjust the per-item size if the space was over or under used if ( sizes[i] != perItemSize && i != sizes.count()-1 ) { perItemSize = available / (sizes.count()-i-1); } } // distribute out any remaining space to items which can still expand // // space is distributed equally amongst remaining items until we run // out of space or items to expand int expandable = sizes.count(); const qreal threshold = 1.0; while ( available > threshold && expandable > 0 ) { qreal extraSpace = available / expandable; for ( int i = 0 ; i < colCount ; i++ ) { if ( expansionSpace[i] > threshold ) { qreal oldSize = sizes[i]; sizes[i] += qMin(extraSpace,expansionSpace[i]); expansionSpace[i] -= sizes[i]-oldSize; available -= sizes[i]-oldSize; } else { expandable--; } } } // set items' geometry according to new sizes qreal pos = d->startPos(geometry()); for ( int col = 0 ; col < colCount ; col++ ) { int newPos = pos; for ( int row = 0 ; row < d->rowCount ; row++ ) { int i = col*d->rowCount+row; if(i>=count()) break; //QObject *obj = dynamic_cast(d->children[i]); //if ( obj ) //qDebug() << "Item " << i << obj->metaObject()->className() << "size:" << sizes[i]; int p = d->layoutItem(margined, d->children[i], pos , sizes[col], row); newPos = (row != 0 && p < pos) ? qMin(p,newPos) : qMax(p,newPos); } pos=newPos; } startAnimation(); } QSizeF BoxLayout::maximumSize() const { return Layout::maximumSize(); } QSizeF BoxLayout::minimumSize() const { return d->calculateSize(Private::MinSize); } QSizeF BoxLayout::sizeHint() const { return d->calculateSize(Private::HintSize); } void BoxLayout::setMultiRow(bool b) { d->multiRow = b; } HBoxLayout::HBoxLayout(LayoutItem *parent) : BoxLayout(LeftToRight,parent) { } VBoxLayout::VBoxLayout(LayoutItem *parent) : BoxLayout(TopToBottom,parent) { } } // Plasma namespace