diff options
| author | Peter Penz <[email protected]> | 2011-07-30 20:13:09 +0200 |
|---|---|---|
| committer | Peter Penz <[email protected]> | 2011-07-30 20:13:41 +0200 |
| commit | f23e9496f303995557b744c03178f5dbd9b35016 (patch) | |
| tree | 1139c4340ac173718d1fa847e0124d6175781fd9 /src/kitemviews | |
| parent | 69e4007e5e330f2ca87c0176a186967b5ca156e8 (diff) | |
Merged very early alpha-version of Dolphin 2.0
Dolphin 2.0 will get a new view-engine with the
following improvements:
- Better performance
- Animated transitions
- No clipped filenames due to dynamic item-sizes
- Grouping support for all view-modes
- Non-rectangular selection areas
- Simplified code for better maintenance
More details will be provided in a blog-entry during
the next days.
Please note that the code is in a very
early alpha-stage and although the most tricky parts
have been implemented already very basic things like
drag and drop or selections have not been pushed yet.
Those things are rather trivial to implement but this
still will take some time.
Diffstat (limited to 'src/kitemviews')
32 files changed, 7982 insertions, 0 deletions
diff --git a/src/kitemviews/kfileitemlistview.cpp b/src/kitemviews/kfileitemlistview.cpp new file mode 100644 index 000000000..a7217d30c --- /dev/null +++ b/src/kitemviews/kfileitemlistview.cpp @@ -0,0 +1,423 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kfileitemlistview.h" + +#include "kitemlistgroupheader.h" +#include "kfileitemmodelrolesupdater.h" +#include "kfileitemlistwidget.h" +#include "kfileitemmodel.h" +#include <KLocale> +#include <KStringHandler> + +#include <KDebug> + +#include <QTextLine> +#include <QTimer> + +#define KFILEITEMLISTVIEW_DEBUG + +namespace { + const int ShortInterval = 50; + const int LongInterval = 300; +} + +KFileItemListView::KFileItemListView(QGraphicsWidget* parent) : + KItemListView(parent), + m_itemLayout(IconsLayout), + m_modelRolesUpdater(0), + m_updateVisibleIndexRangeTimer(0), + m_updateIconSizeTimer(0), + m_minimumRolesWidths() +{ + setScrollOrientation(Qt::Vertical); + setWidgetCreator(new KItemListWidgetCreator<KFileItemListWidget>()); + setGroupHeaderCreator(new KItemListGroupHeaderCreator<KItemListGroupHeader>()); + + m_updateVisibleIndexRangeTimer = new QTimer(this); + m_updateVisibleIndexRangeTimer->setSingleShot(true); + m_updateVisibleIndexRangeTimer->setInterval(ShortInterval); + connect(m_updateVisibleIndexRangeTimer, SIGNAL(timeout()), this, SLOT(updateVisibleIndexRange())); + + m_updateIconSizeTimer = new QTimer(this); + m_updateIconSizeTimer->setSingleShot(true); + m_updateIconSizeTimer->setInterval(ShortInterval); + connect(m_updateIconSizeTimer, SIGNAL(timeout()), this, SLOT(updateIconSize())); + + updateMinimumRolesWidths(); +} + +KFileItemListView::~KFileItemListView() +{ + delete widgetCreator(); + delete groupHeaderCreator(); + + delete m_modelRolesUpdater; + m_modelRolesUpdater = 0; +} + +void KFileItemListView::setPreviewsShown(bool show) +{ + if (m_modelRolesUpdater) { + m_modelRolesUpdater->setPreviewShown(show); + } +} + +bool KFileItemListView::previewsShown() const +{ + return m_modelRolesUpdater->isPreviewShown(); +} + +void KFileItemListView::setItemLayout(Layout layout) +{ + if (m_itemLayout != layout) { + m_itemLayout = layout; + updateLayoutOfVisibleItems(); + } +} + +KFileItemListView::Layout KFileItemListView::itemLayout() const +{ + return m_itemLayout; +} + +QSizeF KFileItemListView::itemSizeHint(int index) const +{ + const QHash<QByteArray, QVariant> values = model()->data(index); + const KItemListStyleOption& option = styleOption(); + const int additionalRolesCount = qMax(visibleRoles().count() - 1, 0); + + switch (m_itemLayout) { + case IconsLayout: { + const QString text = KStringHandler::preProcessWrap(values["name"].toString()); + + const qreal maxWidth = itemSize().width() - 2 * option.margin; + int textLinesCount = 0; + QTextLine line; + + // Calculate the number of lines required for wrapping the name + QTextOption textOption(Qt::AlignHCenter); + textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + + QTextLayout layout(text, option.font); + layout.setTextOption(textOption); + layout.beginLayout(); + while ((line = layout.createLine()).isValid()) { + line.setLineWidth(maxWidth); + line.naturalTextWidth(); + ++textLinesCount; + } + layout.endLayout(); + + // Add one line for each additional information + textLinesCount += additionalRolesCount; + + const qreal height = textLinesCount * option.fontMetrics.height() + + option.iconSize + + option.margin * 4; + return QSizeF(itemSize().width(), height); + } + + case CompactLayout: { + // For each row exactly one role is shown. Calculate the maximum required width that is necessary + // to show all roles without horizontal clipping. + qreal maximumRequiredWidth = 0.0; + QHashIterator<QByteArray, int> it(visibleRoles()); + while (it.hasNext()) { + it.next(); + const QByteArray& role = it.key(); + const QString text = values[role].toString(); + const qreal requiredWidth = option.fontMetrics.width(text); + maximumRequiredWidth = qMax(maximumRequiredWidth, requiredWidth); + } + + const qreal width = option.margin * 4 + option.iconSize + maximumRequiredWidth; + const qreal height = option.margin * 2 + qMax(option.iconSize, (1 + additionalRolesCount) * option.fontMetrics.height()); + return QSizeF(width, height); + } + + case DetailsLayout: { + // The width will be determined dynamically by KFileItemListView::visibleRoleSizes() + const qreal height = option.margin * 2 + qMax(option.iconSize, option.fontMetrics.height()); + return QSizeF(-1, height); + } + + default: + Q_ASSERT(false); + break; + } + + return QSize(); +} + +QHash<QByteArray, QSizeF> KFileItemListView::visibleRoleSizes() const +{ + QElapsedTimer timer; + timer.start(); + + QHash<QByteArray, QSizeF> sizes; + + const int itemCount = model()->count(); + for (int i = 0; i < itemCount; ++i) { + QHashIterator<QByteArray, int> it(visibleRoles()); + while (it.hasNext()) { + it.next(); + const QByteArray& visibleRole = it.key(); + + QSizeF maxSize = sizes.value(visibleRole, QSizeF(0, 0)); + + const QSizeF itemSize = visibleRoleSizeHint(i, visibleRole); + maxSize = maxSize.expandedTo(itemSize); + sizes.insert(visibleRole, maxSize); + } + + if (i > 100 && timer.elapsed() > 200) { + // When having several thousands of items calculating the sizes can get + // very expensive. We accept a possibly too small role-size in favour + // of having no blocking user interface. + #ifdef KFILEITEMLISTVIEW_DEBUG + kDebug() << "Timer exceeded, stopped after" << i << "items"; + #endif + break; + } + } + +#ifdef KFILEITEMLISTVIEW_DEBUG + kDebug() << "[TIME] Calculated dynamic item size for " << itemCount << "items:" << timer.elapsed(); +#endif + return sizes; +} + +void KFileItemListView::initializeItemListWidget(KItemListWidget* item) +{ + KFileItemListWidget* fileItemListWidget = static_cast<KFileItemListWidget*>(item); + + switch (m_itemLayout) { + case IconsLayout: fileItemListWidget->setLayout(KFileItemListWidget::IconsLayout); break; + case CompactLayout: fileItemListWidget->setLayout(KFileItemListWidget::CompactLayout); break; + case DetailsLayout: fileItemListWidget->setLayout(KFileItemListWidget::DetailsLayout); break; + default: Q_ASSERT(false); break; + } +} + +void KFileItemListView::onModelChanged(KItemModelBase* current, KItemModelBase* previous) +{ + Q_UNUSED(previous); + Q_ASSERT(qobject_cast<KFileItemModel*>(current)); + + if (m_modelRolesUpdater) { + delete m_modelRolesUpdater; + } + + m_modelRolesUpdater = new KFileItemModelRolesUpdater(static_cast<KFileItemModel*>(current), this); + const int size = styleOption().iconSize; + m_modelRolesUpdater->setIconSize(QSize(size, size)); +} + +void KFileItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); + updateLayoutOfVisibleItems(); +} + +void KFileItemListView::onItemSizeChanged(const QSizeF& current, const QSizeF& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); + triggerVisibleIndexRangeUpdate(); +} + +void KFileItemListView::onOffsetChanged(qreal current, qreal previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); + triggerVisibleIndexRangeUpdate(); +} + +void KFileItemListView::onVisibleRolesChanged(const QHash<QByteArray, int>& current, const QHash<QByteArray, int>& previous) +{ + Q_UNUSED(previous); + + Q_ASSERT(qobject_cast<KFileItemModel*>(model())); + KFileItemModel* fileItemModel = static_cast<KFileItemModel*>(model()); + + // KFileItemModel does not distinct between "visible" and "invisible" roles. + // Add all roles that are mandatory for having a working KFileItemListView: + QSet<QByteArray> keys = current.keys().toSet(); + QSet<QByteArray> roles = keys; + roles.insert("iconPixmap"); + roles.insert("iconName"); + roles.insert("name"); // TODO: just don't allow to disable it + roles.insert("isDir"); + if (m_itemLayout == DetailsLayout) { + roles.insert("isExpanded"); + roles.insert("expansionLevel"); + } + + fileItemModel->setRoles(roles); + + m_modelRolesUpdater->setRoles(keys); +} + +void KFileItemListView::onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); + triggerIconSizeUpdate(); +} + +void KFileItemListView::onTransactionBegin() +{ + m_modelRolesUpdater->setPaused(true); +} + +void KFileItemListView::onTransactionEnd() +{ + // Only unpause the model-roles-updater if no timer is active. If one + // timer is still active the model-roles-updater will be unpaused later as + // soon as the timer has been exceeded. + const bool timerActive = m_updateVisibleIndexRangeTimer->isActive() || + m_updateIconSizeTimer->isActive(); + if (!timerActive) { + m_modelRolesUpdater->setPaused(false); + } +} + +void KFileItemListView::resizeEvent(QGraphicsSceneResizeEvent* event) +{ + KItemListView::resizeEvent(event); + triggerVisibleIndexRangeUpdate(); +} + +void KFileItemListView::slotItemsRemoved(const KItemRangeList& itemRanges) +{ + KItemListView::slotItemsRemoved(itemRanges); + updateTimersInterval(); +} + +void KFileItemListView::triggerVisibleIndexRangeUpdate() +{ + m_modelRolesUpdater->setPaused(true); + m_updateVisibleIndexRangeTimer->start(); +} + +void KFileItemListView::updateVisibleIndexRange() +{ + if (!m_modelRolesUpdater) { + return; + } + + const int index = firstVisibleIndex(); + const int count = lastVisibleIndex() - index + 1; + m_modelRolesUpdater->setVisibleIndexRange(index, count); + + if (m_updateIconSizeTimer->isActive()) { + // If the icon-size update is pending do an immediate update + // of the icon-size before unpausing m_modelRolesUpdater. This prevents + // an unnecessary expensive recreation of all previews afterwards. + m_updateIconSizeTimer->stop(); + const KItemListStyleOption& option = styleOption(); + m_modelRolesUpdater->setIconSize(QSize(option.iconSize, option.iconSize)); + } + + m_modelRolesUpdater->setPaused(isTransactionActive()); + updateTimersInterval(); +} + +void KFileItemListView::triggerIconSizeUpdate() +{ + m_modelRolesUpdater->setPaused(true); + m_updateIconSizeTimer->start(); +} + +void KFileItemListView::updateIconSize() +{ + if (!m_modelRolesUpdater) { + return; + } + + const KItemListStyleOption& option = styleOption(); + m_modelRolesUpdater->setIconSize(QSize(option.iconSize, option.iconSize)); + + if (m_updateVisibleIndexRangeTimer->isActive()) { + // If the visibility-index-range update is pending do an immediate update + // of the range before unpausing m_modelRolesUpdater. This prevents + // an unnecessary expensive recreation of all previews afterwards. + m_updateVisibleIndexRangeTimer->stop(); + const int index = firstVisibleIndex(); + const int count = lastVisibleIndex() - index + 1; + m_modelRolesUpdater->setVisibleIndexRange(index, count); + } + + m_modelRolesUpdater->setPaused(isTransactionActive()); + updateTimersInterval(); +} + +QSizeF KFileItemListView::visibleRoleSizeHint(int index, const QByteArray& role) const +{ + const KItemListStyleOption& option = styleOption(); + + qreal width = m_minimumRolesWidths.value(role, 0); + const qreal height = option.margin * 2 + option.fontMetrics.height(); + + const QVariant value = model()->data(index).value(role); + const QString text = value.toString(); + if (!text.isEmpty()) { + width = qMax(width, qreal(option.margin * 2 + option.fontMetrics.width(text))); + } + + return QSizeF(width, height); +} + +void KFileItemListView::updateLayoutOfVisibleItems() +{ + foreach (KItemListWidget* widget, visibleItemListWidgets()) { + initializeItemListWidget(widget); + } + triggerVisibleIndexRangeUpdate(); +} + +void KFileItemListView::updateTimersInterval() +{ + if (!model()) { + return; + } + + // The ShortInterval is used for cases like switching the directory: If the + // model is empty and filled later the creation of the previews should be done + // as soon as possible. The LongInterval is used when the model already contains + // items and assures that operations like zooming don't result in too many temporary + // recreations of the previews. + + const int interval = (model()->count() <= 0) ? ShortInterval : LongInterval; + m_updateVisibleIndexRangeTimer->setInterval(interval); + m_updateIconSizeTimer->setInterval(interval); +} + +void KFileItemListView::updateMinimumRolesWidths() +{ + m_minimumRolesWidths.clear(); + + const KItemListStyleOption& option = styleOption(); + const QString sizeText = QLatin1String("888888") + i18nc("@item:intable", "items"); + m_minimumRolesWidths.insert("size", option.fontMetrics.width(sizeText)); +} + +#include "kfileitemlistview.moc" diff --git a/src/kitemviews/kfileitemlistview.h b/src/kitemviews/kfileitemlistview.h new file mode 100644 index 000000000..acd03ed22 --- /dev/null +++ b/src/kitemviews/kfileitemlistview.h @@ -0,0 +1,97 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KFILEITEMLISTVIEW_H +#define KFILEITEMLISTVIEW_H + +#include <libdolphin_export.h> + +#include <kitemviews/kitemlistview.h> + +class KFileItemModelRolesUpdater; +class QTimer; + +class LIBDOLPHINPRIVATE_EXPORT KFileItemListView : public KItemListView +{ + Q_OBJECT + +public: + enum Layout + { + IconsLayout, + CompactLayout, + DetailsLayout + }; + + KFileItemListView(QGraphicsWidget* parent = 0); + virtual ~KFileItemListView(); + + void setPreviewsShown(bool show); + bool previewsShown() const; + + void setItemLayout(Layout layout); + Layout itemLayout() const; + + virtual QSizeF itemSizeHint(int index) const; + virtual QHash<QByteArray, QSizeF> visibleRoleSizes() const; + +protected: + virtual void initializeItemListWidget(KItemListWidget* item); + virtual void onModelChanged(KItemModelBase* current, KItemModelBase* previous); + virtual void onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous); + virtual void onItemSizeChanged(const QSizeF& current, const QSizeF& previous); + virtual void onOffsetChanged(qreal current, qreal previous); + virtual void onVisibleRolesChanged(const QHash<QByteArray, int>& current, const QHash<QByteArray, int>& previous); + virtual void onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous); + virtual void onTransactionBegin(); + virtual void onTransactionEnd(); + virtual void resizeEvent(QGraphicsSceneResizeEvent* event); + +protected slots: + virtual void slotItemsRemoved(const KItemRangeList& itemRanges); + +private slots: + void triggerVisibleIndexRangeUpdate(); + void updateVisibleIndexRange(); + + void triggerIconSizeUpdate(); + void updateIconSize(); + +private: + QSizeF visibleRoleSizeHint(int index, const QByteArray& role) const; + void updateLayoutOfVisibleItems(); + void updateTimersInterval(); + void updateMinimumRolesWidths(); + +private: + Layout m_itemLayout; + + KFileItemModelRolesUpdater* m_modelRolesUpdater; + QTimer* m_updateVisibleIndexRangeTimer; + QTimer* m_updateIconSizeTimer; + + // Cache for calculating visibleRoleSizes() in a fast way + QHash<QByteArray, int> m_minimumRolesWidths; + + friend class KFileItemListViewTest; // For unit testing +}; + +#endif + + diff --git a/src/kitemviews/kfileitemlistwidget.cpp b/src/kitemviews/kfileitemlistwidget.cpp new file mode 100644 index 000000000..4abc4d115 --- /dev/null +++ b/src/kitemviews/kfileitemlistwidget.cpp @@ -0,0 +1,728 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kfileitemlistwidget.h" + +#include "kfileitemmodel.h" +#include "kitemlistview.h" +#include "kpixmapmodifier_p.h" + +#include <KIcon> +#include <KIconEffect> +#include <KIconLoader> +#include <KLocale> +#include <KStringHandler> +#include <KDebug> + +#include <QFontMetricsF> +#include <QGraphicsSceneResizeEvent> +#include <QPainter> +#include <QTextLayout> +#include <QTextLine> + +//#define KFILEITEMLISTWIDGET_DEBUG + +KFileItemListWidget::KFileItemListWidget(QGraphicsItem* parent) : + KItemListWidget(parent), + m_isDir(false), + m_dirtyLayout(true), + m_dirtyContent(true), + m_dirtyContentRoles(), + m_layout(IconsLayout), + m_pixmapPos(), + m_pixmap(), + m_scaledPixmapSize(), + m_hoverPixmapRect(), + m_hoverPixmap(), + m_textPos(), + m_text(), + m_textsBoundingRect(), + m_sortedVisibleRoles(), + m_expansionArea(), + m_additionalInfoTextColor() +{ + for (int i = 0; i < TextIdCount; ++i) { + m_text[i].setTextFormat(Qt::PlainText); + m_text[i].setPerformanceHint(QStaticText::AggressiveCaching); + } +} + +KFileItemListWidget::~KFileItemListWidget() +{ +} + +void KFileItemListWidget::setLayout(Layout layout) +{ + if (m_layout != layout) { + m_layout = layout; + m_dirtyLayout = true; + update(); + } +} + +KFileItemListWidget::Layout KFileItemListWidget::layout() const +{ + return m_layout; +} + +void KFileItemListWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + KItemListWidget::paint(painter, option, widget); + + painter->setRenderHint(QPainter::Antialiasing); + + if (m_dirtyContent || m_dirtyLayout) { + const_cast<KFileItemListWidget*>(this)->updateCache(); + } + + if (m_isDir && !m_expansionArea.isEmpty()) { + QStyleOption arrowOption; + arrowOption.rect = m_expansionArea.toRect(); + const QStyle::PrimitiveElement arrow = data()["isExpanded"].toBool() + ? QStyle::PE_IndicatorArrowDown : QStyle::PE_IndicatorArrowRight; + style()->drawPrimitive(arrow, &arrowOption, painter); + } + + const bool isHovered = (hoverOpacity() > 0.0); + + const KItemListStyleOption& itemListStyleOption = styleOption(); + if (isHovered) { + // Blend the unhovered and hovered pixmap if the hovering + // animation is ongoing + if (hoverOpacity() < 1.0) { + drawPixmap(painter, m_pixmap); + } + + const qreal opacity = painter->opacity(); + painter->setOpacity(hoverOpacity() * opacity); + drawPixmap(painter, m_hoverPixmap); + + // Draw the hover background for the text + QRectF textsBoundingRect = m_textsBoundingRect; + const qreal marginDiff = itemListStyleOption.margin / 2; + textsBoundingRect.adjust(marginDiff, marginDiff, -marginDiff, -marginDiff); + painter->setOpacity(hoverOpacity() * opacity * 0.1); + painter->setPen(Qt::NoPen); + painter->setBrush(itemListStyleOption.palette.text()); + painter->drawRoundedRect(textsBoundingRect, 4, 4); + + painter->setOpacity(opacity); + } else { + drawPixmap(painter, m_pixmap); + } + + painter->setFont(itemListStyleOption.font); + painter->setPen(itemListStyleOption.palette.text().color()); + painter->drawStaticText(m_textPos[Name], m_text[Name]); + + painter->setPen(m_additionalInfoTextColor); + for (int i = Name + 1; i < TextIdCount; ++i) { + painter->drawStaticText(m_textPos[i], m_text[i]); + } + +#ifdef KFILEITEMLISTWIDGET_DEBUG + painter->setPen(Qt::red); + painter->setBrush(Qt::NoBrush); + painter->drawText(QPointF(0, itemListStyleOption.fontMetrics.height()), QString::number(index())); + painter->drawRect(rect()); +#endif +} + +bool KFileItemListWidget::contains(const QPointF& point) const +{ + return KItemListWidget::contains(point) || m_textsBoundingRect.contains(point); +} + +QRectF KFileItemListWidget::hoverBoundingRect() const +{ + QRectF bounds = m_hoverPixmapRect; + const qreal margin = styleOption().margin; + bounds.adjust(-margin, -margin, margin, margin); + return bounds; +} + +QRectF KFileItemListWidget::expansionToggleRect() const +{ + return m_isDir ? m_expansionArea : QRectF(); +} + +void KFileItemListWidget::dataChanged(const QHash<QByteArray, QVariant>& current, + const QSet<QByteArray>& roles) +{ + KItemListWidget::dataChanged(current, roles); + m_dirtyContent = true; + + QSet<QByteArray> dirtyRoles; + if (roles.isEmpty()) { + dirtyRoles = visibleRoles().keys().toSet(); + dirtyRoles.insert("iconPixmap"); + dirtyRoles.insert("iconName"); + } else { + dirtyRoles = roles; + } + + QSetIterator<QByteArray> it(dirtyRoles); + while (it.hasNext()) { + const QByteArray& role = it.next(); + m_dirtyContentRoles.insert(role); + } +} + +void KFileItemListWidget::visibleRolesChanged(const QHash<QByteArray, int>& current, + const QHash<QByteArray, int>& previous) +{ + KItemListWidget::visibleRolesChanged(current, previous); + m_dirtyLayout = true; + + // Cache the roles sorted into m_sortedVisibleRoles: + const int visibleRolesCount = current.count(); + m_sortedVisibleRoles.clear(); + m_sortedVisibleRoles.reserve(visibleRolesCount); + for (int i = 0; i < visibleRolesCount; ++i) { + m_sortedVisibleRoles.append(QByteArray()); + } + + QHashIterator<QByteArray, int> it(current); + while (it.hasNext()) { + it.next(); + + const int index = it.value(); + if (index < 0 || index >= visibleRolesCount || !m_sortedVisibleRoles.at(index).isEmpty()) { + kWarning() << "The visible roles have an invalid sort order."; + break; + } + + const QByteArray& role = it.key(); + m_sortedVisibleRoles[index] = role; + } +} + +void KFileItemListWidget::visibleRolesSizesChanged(const QHash<QByteArray, QSizeF>& current, + const QHash<QByteArray, QSizeF>& previous) +{ + KItemListWidget::visibleRolesSizesChanged(current, previous); + m_dirtyLayout = true; +} + +void KFileItemListWidget::styleOptionChanged(const KItemListStyleOption& current, + const KItemListStyleOption& previous) +{ + KItemListWidget::styleOptionChanged(current, previous); + + // For the color of the additional info the inactive text color + // is not used as this might lead to unreadable text for some color schemes. Instead + // the text color is slightly mixed with the background color. + const QColor c1 = current.palette.text().color(); + const QColor c2 = current.palette.background().color(); + const int p1 = 70; + const int p2 = 100 - p1; + m_additionalInfoTextColor = QColor((c1.red() * p1 + c2.red() * p2) / 100, + (c1.green() * p1 + c2.green() * p2) / 100, + (c1.blue() * p1 + c2.blue() * p2) / 100); + + m_dirtyLayout = true; +} + +void KFileItemListWidget::resizeEvent(QGraphicsSceneResizeEvent* event) +{ + KItemListWidget::resizeEvent(event); + m_dirtyLayout = true; +} + +void KFileItemListWidget::updateCache() +{ + if (index() < 0) { + return; + } + + m_isDir = data()["isDir"].toBool(); + + updateExpansionArea(); + updateTextsCache(); + updatePixmapCache(); + + m_dirtyLayout = false; + m_dirtyContent = false; + m_dirtyContentRoles.clear(); +} + +void KFileItemListWidget::updateExpansionArea() +{ + if (m_layout == DetailsLayout) { + const QHash<QByteArray, QVariant> values = data(); + Q_ASSERT(values.contains("expansionLevel")); + const KItemListStyleOption& option = styleOption(); + const int expansionLevel = values.value("expansionLevel", 0).toInt(); + + const qreal widgetHeight = size().height(); + const qreal expansionLevelSize = KIconLoader::SizeSmall; + const qreal x = option.margin + expansionLevel * widgetHeight; + const qreal y = (widgetHeight - expansionLevelSize) / 2; + m_expansionArea = QRectF(x, y, expansionLevelSize, expansionLevelSize); + } else { + m_expansionArea = QRectF(); + } +} + +void KFileItemListWidget::updatePixmapCache() +{ + // Precondition: Requires already updated m_textPos values to calculate + // the remaining height when the alignment is vertical. + + const bool iconOnTop = (m_layout == IconsLayout); + const KItemListStyleOption& option = styleOption(); + const int iconHeight = option.iconSize; + + const QHash<QByteArray, QVariant> values = data(); + const QSizeF widgetSize = size(); + + int scaledIconHeight = 0; + if (iconOnTop) { + scaledIconHeight = static_cast<int>(m_textPos[Name].y() - 3 * option.margin); + } else { + const int textRowsCount = (m_layout == CompactLayout) ? visibleRoles().count() : 1; + const qreal requiredTextHeight = textRowsCount * option.fontMetrics.height(); + scaledIconHeight = (requiredTextHeight < iconHeight) ? widgetSize.height() - 2 * option.margin : iconHeight; + } + + bool updatePixmap = (iconHeight != m_pixmap.height()); + if (!updatePixmap && m_dirtyContent) { + updatePixmap = m_dirtyContentRoles.isEmpty() + || m_dirtyContentRoles.contains("iconPixmap") + || m_dirtyContentRoles.contains("iconName"); + } + + if (updatePixmap) { + m_pixmap = values["iconPixmap"].value<QPixmap>(); + if (m_pixmap.isNull()) { + // Use the icon that fits to the MIME-type + QString iconName = values["iconName"].toString(); + if (iconName.isEmpty()) { + // The icon-name has not been not resolved by KFileItemModelRolesUpdater, + // use a generic icon as fallback + iconName = QLatin1String("unknown"); + } + m_pixmap = pixmapForIcon(iconName, iconHeight); + m_hoverPixmapRect.setSize(m_pixmap.size()); + } else if (m_pixmap.size() != QSize(iconHeight, iconHeight)) { + // A custom pixmap has been applied. Assure that the pixmap + // is scaled to the available size. + const bool scale = m_pixmap.width() > iconHeight || m_pixmap.height() > iconHeight || + (m_pixmap.width() < iconHeight && m_pixmap.height() < iconHeight); + if (scale) { + KPixmapModifier::scale(m_pixmap, QSize(iconHeight, iconHeight)); + } + m_hoverPixmapRect.setSize(m_pixmap.size()); + + // To simplify the handling of scaling the original pixmap + // will be embedded into a square pixmap. + QPixmap squarePixmap(iconHeight, iconHeight); + squarePixmap.fill(Qt::transparent); + + QPainter painter(&squarePixmap); + if (iconOnTop) { + const int x = (iconHeight - m_pixmap.width()) / 2; // Center horizontally + const int y = iconHeight - m_pixmap.height(); // Align on bottom + painter.drawPixmap(x, y, m_pixmap); + } else { + const int x = iconHeight - m_pixmap.width(); // Align right + const int y = (iconHeight - m_pixmap.height()) / 2; // Center vertically + painter.drawPixmap(x, y, m_pixmap); + } + + m_pixmap = squarePixmap; + } else { + m_hoverPixmapRect.setSize(m_pixmap.size()); + } + + Q_ASSERT(m_pixmap.height() == iconHeight); + } + + m_scaledPixmapSize = QSize(scaledIconHeight, scaledIconHeight); + + if (iconOnTop) { + m_pixmapPos.setX((widgetSize.width() - m_scaledPixmapSize.width()) / 2); + } else { + m_pixmapPos.setX(m_textPos[Name].x() - 2 * option.margin - scaledIconHeight); + } + m_pixmapPos.setY(option.margin); + + // Center the hover rectangle horizontally and align it on bottom + const qreal x = m_pixmapPos.x() + (m_scaledPixmapSize.width() - m_hoverPixmapRect.width()) / 2.0; + const qreal y = m_pixmapPos.y() + m_scaledPixmapSize.height() - m_hoverPixmapRect.height(); + m_hoverPixmapRect.moveTopLeft(QPointF(x, y)); + + // Prepare the pixmap that is used when the item gets hovered + if (option.state & QStyle::State_MouseOver) { + m_hoverPixmap = m_pixmap; + KIconEffect* effect = KIconLoader::global()->iconEffect(); + // In the KIconLoader terminology, active = hover. + if (effect->hasEffect(KIconLoader::Desktop, KIconLoader::ActiveState)) { + m_hoverPixmap = effect->apply(m_pixmap, KIconLoader::Desktop, KIconLoader::ActiveState); + } else { + m_hoverPixmap = m_pixmap; + } + } else if (hoverOpacity() <= 0.0) { + // No hover animation is ongoing. Clear m_hoverPixmap to save memory. + m_hoverPixmap = QPixmap(); + } +} + +void KFileItemListWidget::updateTextsCache() +{ + QTextOption textOption; + switch (m_layout) { + case IconsLayout: + textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + textOption.setAlignment(Qt::AlignHCenter); + break; + case CompactLayout: + case DetailsLayout: + textOption.setAlignment(Qt::AlignLeft); + textOption.setWrapMode(QTextOption::NoWrap); + break; + default: + Q_ASSERT(false); + break; + } + + for (int i = 0; i < TextIdCount; ++i) { + m_text[i].setText(QString()); + m_text[i].setTextOption(textOption); + } + + switch (m_layout) { + case IconsLayout: updateIconsLayoutTextCache(); break; + case CompactLayout: updateCompactLayoutTextCache(); break; + case DetailsLayout: updateDetailsLayoutTextCache(); break; + default: Q_ASSERT(false); break; + } +} + +void KFileItemListWidget::updateIconsLayoutTextCache() +{ + // +------+ + // | Icon | + // +------+ + // + // Name role that + // might get wrapped above + // several lines. + // Additional role 1 + // Additional role 2 + + const QHash<QByteArray, QVariant> values = data(); + + const KItemListStyleOption& option = styleOption(); + const qreal maxWidth = size().width() - 2 * option.margin; + const qreal widgetHeight = size().height(); + const qreal fontHeight = option.fontMetrics.height(); + + // Initialize properties for the "name" role. It will be used as anchor + // for initializing the position of the other roles. + m_text[Name].setText(KStringHandler::preProcessWrap(values["name"].toString())); + + // Calculate the number of lines required for the name and the required width + int textLinesCountForName = 0; + qreal requiredWidthForName = 0; + QTextLine line; + + QTextLayout layout(m_text[Name].text(), option.font); + layout.setTextOption(m_text[Name].textOption()); + layout.beginLayout(); + while ((line = layout.createLine()).isValid()) { + line.setLineWidth(maxWidth); + requiredWidthForName = qMax(requiredWidthForName, line.naturalTextWidth()); + ++textLinesCountForName; + } + layout.endLayout(); + + // Use one line for each additional information + int textLinesCount = textLinesCountForName; + const int additionalRolesCount = qMax(visibleRoles().count() - 1, 0); + textLinesCount += additionalRolesCount; + + m_text[Name].setTextWidth(maxWidth); + m_textPos[Name] = QPointF(option.margin, widgetHeight - textLinesCount * fontHeight - option.margin); + m_textsBoundingRect = QRectF(option.margin + (maxWidth - requiredWidthForName) / 2, + m_textPos[Name].y(), + requiredWidthForName, + m_text[Name].size().height()); + + // Calculate the position for each additional information + qreal y = m_textPos[Name].y() + textLinesCountForName * fontHeight; + foreach (const QByteArray& role, m_sortedVisibleRoles) { + const TextId textId = roleTextId(role); + if (textId == Name) { + continue; + } + + const QString text = roleText(textId, values[role]); + m_text[textId].setText(text); + + qreal requiredWidth = 0; + + QTextLayout layout(text, option.font); + layout.setTextOption(m_text[textId].textOption()); + layout.beginLayout(); + QTextLine textLine = layout.createLine(); + if (textLine.isValid()) { + textLine.setLineWidth(maxWidth); + requiredWidth = textLine.naturalTextWidth(); + if (textLine.textLength() < text.length()) { + // TODO: QFontMetrics::elidedText() works different regarding the given width + // in comparison to QTextLine::setLineWidth(). It might happen that the text does + // not get elided although it does not fit into the given width. As workaround + // the margin is substracted. + const QString elidedText = option.fontMetrics.elidedText(text, Qt::ElideRight, maxWidth - option.margin); + m_text[textId].setText(elidedText); + } + } + layout.endLayout(); + + m_textPos[textId] = QPointF(option.margin, y); + m_text[textId].setTextWidth(maxWidth); + + const QRectF textBoundingRect(option.margin + (maxWidth - requiredWidth) / 2, y, requiredWidth, fontHeight); + m_textsBoundingRect |= textBoundingRect; + + y += fontHeight; + } + + // Add a margin to the text bounding rectangle + const qreal margin = option.margin; + m_textsBoundingRect.adjust(-margin, -margin, margin, margin); +} + +void KFileItemListWidget::updateCompactLayoutTextCache() +{ + // +------+ Name role + // | Icon | Additional role 1 + // +------+ Additional role 2 + + const QHash<QByteArray, QVariant> values = data(); + + const KItemListStyleOption& option = styleOption(); + const qreal widgetHeight = size().height(); + const qreal fontHeight = option.fontMetrics.height(); + const qreal textLinesHeight = qMax(visibleRoles().count(), 1) * fontHeight; + const int scaledIconSize = (textLinesHeight < option.iconSize) ? widgetHeight - 2 * option.margin : option.iconSize; + + qreal maximumRequiredTextWidth = 0; + const qreal x = option.margin * 3 + scaledIconSize; + qreal y = (widgetHeight - textLinesHeight) / 2; + const qreal maxWidth = size().width() - x - option.margin; + foreach (const QByteArray& role, m_sortedVisibleRoles) { + const TextId textId = roleTextId(role); + + const QString text = roleText(textId, values[role]); + m_text[textId].setText(text); + + qreal requiredWidth = option.fontMetrics.width(text); + if (requiredWidth > maxWidth) { + requiredWidth = maxWidth; + const QString elidedText = option.fontMetrics.elidedText(text, Qt::ElideRight, maxWidth); + m_text[textId].setText(elidedText); + } + + m_textPos[textId] = QPointF(x, y); + m_text[textId].setTextWidth(maxWidth); + + maximumRequiredTextWidth = qMax(maximumRequiredTextWidth, requiredWidth); + + y += fontHeight; + } + + m_textsBoundingRect = QRectF(x - option.margin, 0, maximumRequiredTextWidth + 2 * option.margin, widgetHeight); +} + +void KFileItemListWidget::updateDetailsLayoutTextCache() +{ + // Precondition: Requires already updated m_expansionArea + // to determine the left position. + + // +------+ + // | Icon | Name role Additional role 1 Additional role 2 + // +------+ + m_textsBoundingRect = QRectF(); + + const KItemListStyleOption& option = styleOption(); + const QHash<QByteArray, QVariant> values = data(); + + const qreal widgetHeight = size().height(); + const int scaledIconSize = widgetHeight - 2 * option.margin; + const int fontHeight = option.fontMetrics.height(); + + qreal x = m_expansionArea.right() + option.margin * 3 + scaledIconSize; + const qreal y = qMax(qreal(option.margin), (widgetHeight - fontHeight) / 2); + + foreach (const QByteArray& role, m_sortedVisibleRoles) { + const TextId textId = roleTextId(role); + + const QString text = roleText(textId, values[role]); + m_text[textId].setText(text); + + const qreal requiredWidth = option.fontMetrics.width(text); + m_textPos[textId] = QPointF(x, y); + + const qreal columnWidth = visibleRolesSizes().value(role, QSizeF(0, 0)).width(); + x += columnWidth; + + switch (textId) { + case Name: { + m_textsBoundingRect = QRectF(m_textPos[textId].x() - option.margin, 0, + requiredWidth + 2 * option.margin, size().height()); + + // The column after the name should always be aligned on the same x-position independent + // from the expansion-level shown in the name column + x -= m_expansionArea.right(); + break; + } + case Size: + // The values for the size should be right aligned + m_textPos[textId].rx() += columnWidth - requiredWidth - 2 * option.margin; + break; + + default: + break; + } + } +} + +QString KFileItemListWidget::roleText(TextId textId, const QVariant& roleValue) const +{ + QString text; + + switch (textId) { + case Name: + case Permissions: + case Owner: + case Group: + case Type: + case Destination: + case Path: + text = roleValue.toString(); + break; + + case Size: { + if (data().value("isDir").toBool()) { + // The item represents a directory. Show the number of sub directories + // instead of the file size of the directory. + if (!roleValue.isNull()) { + const KIO::filesize_t size = roleValue.value<KIO::filesize_t>(); + text = i18ncp("@item:intable", "%1 item", "%1 items", size); + } + } else { + const KIO::filesize_t size = roleValue.value<KIO::filesize_t>(); + text = KIO::convertSize(size); + } + break; + } + + case Date: { + const QDateTime dateTime = roleValue.toDateTime(); + text = KGlobal::locale()->formatDateTime(dateTime); + break; + } + + default: + Q_ASSERT(false); + break; + } + + return text; +} + +void KFileItemListWidget::drawPixmap(QPainter* painter, const QPixmap& pixmap) +{ + const bool isHiddenItem = m_text[Name].text().startsWith(QLatin1Char('.')); + qreal opacity; + if (isHiddenItem) { + opacity = painter->opacity(); + painter->setOpacity(opacity * 0.3); + } + + if (m_scaledPixmapSize != pixmap.size()) { + QPixmap scaledPixmap = pixmap; + KPixmapModifier::scale(scaledPixmap, m_scaledPixmapSize); + painter->drawPixmap(m_pixmapPos, scaledPixmap); + +#ifdef KFILEITEMLISTWIDGET_DEBUG + painter->setPen(Qt::green); + painter->drawRect(QRectF(m_pixmapPos, QSizeF(scaledPixmap.size()))); +#endif + } else { + painter->drawPixmap(m_pixmapPos, pixmap); + } + + if (isHiddenItem) { + painter->setOpacity(opacity); + } +} + +QPixmap KFileItemListWidget::pixmapForIcon(const QString& name, int size) +{ + const KIcon icon(name); + + int requestedSize; + if (size <= KIconLoader::SizeSmall) { + requestedSize = KIconLoader::SizeSmall; + } else if (size <= KIconLoader::SizeSmallMedium) { + requestedSize = KIconLoader::SizeSmallMedium; + } else if (size <= KIconLoader::SizeMedium) { + requestedSize = KIconLoader::SizeMedium; + } else if (size <= KIconLoader::SizeLarge) { + requestedSize = KIconLoader::SizeLarge; + } else if (size <= KIconLoader::SizeHuge) { + requestedSize = KIconLoader::SizeHuge; + } else if (size <= KIconLoader::SizeEnormous) { + requestedSize = KIconLoader::SizeEnormous; + } else if (size <= KIconLoader::SizeEnormous * 2) { + requestedSize = KIconLoader::SizeEnormous * 2; + } else { + requestedSize = size; + } + + QPixmap pixmap = icon.pixmap(requestedSize, requestedSize); + if (requestedSize != size) { + KPixmapModifier::scale(pixmap, QSize(size, size)); + } + + return pixmap; +} + +KFileItemListWidget::TextId KFileItemListWidget::roleTextId(const QByteArray& role) +{ + static QHash<QByteArray, TextId> rolesHash; + if (rolesHash.isEmpty()) { + rolesHash.insert("name", Name); + rolesHash.insert("size", Size); + rolesHash.insert("date", Date); + rolesHash.insert("permissions", Permissions); + rolesHash.insert("owner", Owner); + rolesHash.insert("group", Group); + rolesHash.insert("type", Type); + rolesHash.insert("destination", Destination); + rolesHash.insert("path", Path); + } + + return rolesHash.value(role); +} + +#include "kfileitemlistwidget.moc" diff --git a/src/kitemviews/kfileitemlistwidget.h b/src/kitemviews/kfileitemlistwidget.h new file mode 100644 index 000000000..3ce953106 --- /dev/null +++ b/src/kitemviews/kfileitemlistwidget.h @@ -0,0 +1,118 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KFILEITEMLISTWIDGET_H +#define KFILEITEMLISTWIDGET_H + +#include <libdolphin_export.h> + +#include <kitemviews/kitemlistwidget.h> + +#include <QPixmap> +#include <QPointF> +#include <QStaticText> + +class LIBDOLPHINPRIVATE_EXPORT KFileItemListWidget : public KItemListWidget +{ + Q_OBJECT + +public: + enum Layout + { + IconsLayout, + CompactLayout, + DetailsLayout + }; + + KFileItemListWidget(QGraphicsItem* parent); + virtual ~KFileItemListWidget(); + + void setLayout(Layout layout); + Layout layout() const; + + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = 0); + + virtual bool contains(const QPointF& point) const; + virtual QRectF hoverBoundingRect() const; + virtual QRectF expansionToggleRect() const; + +protected: + virtual void dataChanged(const QHash<QByteArray, QVariant>& current, const QSet<QByteArray>& roles = QSet<QByteArray>()); + virtual void visibleRolesChanged(const QHash<QByteArray, int>& current, const QHash<QByteArray, int>& previous); + virtual void visibleRolesSizesChanged(const QHash<QByteArray, QSizeF>& current, const QHash<QByteArray, QSizeF>& previous); + virtual void styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous); + virtual void resizeEvent(QGraphicsSceneResizeEvent* event); + +private: + enum TextId { + Name, + Size, + Date, + Permissions, + Owner, + Group, + Type, + Destination, + Path, + TextIdCount // Mandatory last entry + }; + + void updateCache(); + void updateExpansionArea(); + void updatePixmapCache(); + + void updateTextsCache(); + void updateIconsLayoutTextCache(); + void updateCompactLayoutTextCache(); + void updateDetailsLayoutTextCache(); + + QString roleText(TextId textId, const QVariant& roleValue) const; + + void drawPixmap(QPainter* painter, const QPixmap& pixmap); + + static QPixmap pixmapForIcon(const QString& name, int size); + static TextId roleTextId(const QByteArray& role); + +private: + bool m_isDir; + bool m_dirtyLayout; + bool m_dirtyContent; + QSet<QByteArray> m_dirtyContentRoles; + + Layout m_layout; + QPointF m_pixmapPos; + QPixmap m_pixmap; + QSize m_scaledPixmapSize; + + QRectF m_hoverPixmapRect; + QPixmap m_hoverPixmap; + + QPointF m_textPos[TextIdCount]; + QStaticText m_text[TextIdCount]; + QRectF m_textsBoundingRect; + + QList<QByteArray> m_sortedVisibleRoles; + + QRectF m_expansionArea; + QColor m_additionalInfoTextColor; +}; + +#endif + + diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp new file mode 100644 index 000000000..b191abab6 --- /dev/null +++ b/src/kitemviews/kfileitemmodel.cpp @@ -0,0 +1,868 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kfileitemmodel.h" + +#include <KDirLister> +#include <KLocale> +#include <KStringHandler> +#include <KDebug> + +#include <QTimer> + +#define KFILEITEMMODEL_DEBUG + +KFileItemModel::KFileItemModel(KDirLister* dirLister, QObject* parent) : + KItemModelBase(QByteArray(), "name", parent), + m_dirLister(dirLister), + m_naturalSorting(true), + m_sortFoldersFirst(true), + m_groupRole(NoRole), + m_sortRole(NameRole), + m_caseSensitivity(Qt::CaseInsensitive), + m_sortedItems(), + m_items(), + m_data(), + m_requestRole(), + m_minimumUpdateIntervalTimer(0), + m_maximumUpdateIntervalTimer(0), + m_pendingItemsToInsert(), + m_pendingItemsToDelete(), + m_rootExpansionLevel(-1) +{ + resetRoles(); + m_requestRole[NameRole] = true; + m_requestRole[IsDirRole] = true; + + Q_ASSERT(dirLister); + + connect(dirLister, SIGNAL(canceled()), this, SLOT(slotCanceled())); + connect(dirLister, SIGNAL(completed()), this, SLOT(slotCompleted())); + connect(dirLister, SIGNAL(newItems(KFileItemList)), this, SLOT(slotNewItems(KFileItemList))); + connect(dirLister, SIGNAL(itemsDeleted(KFileItemList)), this, SLOT(slotItemsDeleted(KFileItemList))); + connect(dirLister, SIGNAL(clear()), this, SLOT(slotClear())); + connect(dirLister, SIGNAL(clear(KUrl)), this, SLOT(slotClear(KUrl))); + + // Although the layout engine of KItemListView is fast it is very inefficient to e.g. + // emit 50 itemsInserted()-signals each 100 ms. m_minimumUpdateIntervalTimer assures that updates + // are done in 1 second intervals for equal operations. + m_minimumUpdateIntervalTimer = new QTimer(this); + m_minimumUpdateIntervalTimer->setInterval(1000); + m_minimumUpdateIntervalTimer->setSingleShot(true); + connect(m_minimumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItems())); + + // For slow KIO-slaves like used for searching it makes sense to show results periodically even + // before the completed() or canceled() signal has been emitted. + m_maximumUpdateIntervalTimer = new QTimer(this); + m_maximumUpdateIntervalTimer->setInterval(2000); + m_maximumUpdateIntervalTimer->setSingleShot(true); + connect(m_maximumUpdateIntervalTimer, SIGNAL(timeout()), this, SLOT(dispatchPendingItems())); + + Q_ASSERT(m_minimumUpdateIntervalTimer->interval() <= m_maximumUpdateIntervalTimer->interval()); +} + +KFileItemModel::~KFileItemModel() +{ +} + +int KFileItemModel::count() const +{ + return m_data.count(); +} + +QHash<QByteArray, QVariant> KFileItemModel::data(int index) const +{ + if (index >= 0 && index < count()) { + return m_data.at(index); + } + return QHash<QByteArray, QVariant>(); +} + +bool KFileItemModel::setData(int index, const QHash<QByteArray, QVariant>& values) +{ + if (index >= 0 && index < count()) { + QHash<QByteArray, QVariant> currentValue = m_data.at(index); + + QSet<QByteArray> changedRoles; + QHashIterator<QByteArray, QVariant> it(values); + while (it.hasNext()) { + it.next(); + const QByteArray role = it.key(); + const QVariant value = it.value(); + + if (currentValue[role] != value) { + currentValue[role] = value; + changedRoles.insert(role); + } + } + + if (!changedRoles.isEmpty()) { + m_data[index] = currentValue; + emit itemsChanged(KItemRangeList() << KItemRange(index, 1), changedRoles); + } + + return true; + } + return false; +} + +bool KFileItemModel::supportsGrouping() const +{ + return true; +} + +bool KFileItemModel::supportsSorting() const +{ + return true; +} + +KFileItem KFileItemModel::fileItem(int index) const +{ + if (index >= 0 && index < count()) { + return m_sortedItems.at(index); + } + + return KFileItem(); +} + +int KFileItemModel::index(const KFileItem& item) const +{ + if (item.isNull()) { + return -1; + } + + return m_items.value(item, -1); +} + +void KFileItemModel::clear() +{ + slotClear(); +} + +void KFileItemModel::setRoles(const QSet<QByteArray>& roles) +{ + if (count() > 0) { + const bool supportedExpanding = m_requestRole[IsExpandedRole] && m_requestRole[ExpansionLevelRole]; + const bool willSupportExpanding = roles.contains("isExpanded") && roles.contains("expansionLevel"); + if (supportedExpanding && !willSupportExpanding) { + // No expanding is supported anymore. Take care to delete all items that have an expansion level + // that is not 0 (and hence are part of an expanded item). + removeExpandedItems(); + } + } + + resetRoles(); + QSetIterator<QByteArray> it(roles); + while (it.hasNext()) { + const QByteArray& role = it.next(); + m_requestRole[roleIndex(role)] = true; + } + + if (count() > 0) { + // Update m_data with the changed requested roles + const int maxIndex = count() - 1; + for (int i = 0; i <= maxIndex; ++i) { + m_data[i] = retrieveData(m_sortedItems.at(i)); + } + + kWarning() << "TODO: Emitting itemsChanged() with no information what has changed!"; + emit itemsChanged(KItemRangeList() << KItemRange(0, count()), QSet<QByteArray>()); + } +} + +QSet<QByteArray> KFileItemModel::roles() const +{ + QSet<QByteArray> roles; + for (int i = 0; i < RolesCount; ++i) { + if (m_requestRole[i]) { + switch (i) { + case NoRole: break; + case NameRole: roles.insert("name"); break; + case SizeRole: roles.insert("size"); break; + case DateRole: roles.insert("date"); break; + case PermissionsRole: roles.insert("permissions"); break; + case OwnerRole: roles.insert("owner"); break; + case GroupRole: roles.insert("group"); break; + case TypeRole: roles.insert("type"); break; + case DestinationRole: roles.insert("destination"); break; + case PathRole: roles.insert("path"); break; + case IsDirRole: roles.insert("isDir"); break; + case IsExpandedRole: roles.insert("isExpanded"); break; + case ExpansionLevelRole: roles.insert("expansionLevel"); break; + default: Q_ASSERT(false); break; + } + } + } + return roles; +} + +bool KFileItemModel::setExpanded(int index, bool expanded) +{ + if (isExpanded(index) == expanded || index < 0 || index >= count()) { + return false; + } + + QHash<QByteArray, QVariant> values; + values.insert("isExpanded", expanded); + if (!setData(index, values)) { + return false; + } + + if (expanded) { + const KUrl url = m_sortedItems.at(index).url(); + KDirLister* dirLister = m_dirLister.data(); + if (dirLister) { + dirLister->openUrl(url, KDirLister::Keep); + return true; + } + } else { + KFileItemList itemsToRemove; + const int expansionLevel = data(index)["expansionLevel"].toInt(); + ++index; + while (index < count() && data(index)["expansionLevel"].toInt() > expansionLevel) { + itemsToRemove.append(m_sortedItems.at(index)); + ++index; + } + removeItems(itemsToRemove); + return true; + } + + return false; +} + +bool KFileItemModel::isExpanded(int index) const +{ + if (index >= 0 && index < count()) { + return m_data.at(index).value("isExpanded").toBool(); + } + return false; +} + +bool KFileItemModel::isExpandable(int index) const +{ + if (index >= 0 && index < count()) { + return m_sortedItems.at(index).isDir(); + } + return false; +} + +void KFileItemModel::onGroupRoleChanged(const QByteArray& current, const QByteArray& previous) +{ + Q_UNUSED(previous); + m_groupRole = roleIndex(current); +} + +void KFileItemModel::onSortRoleChanged(const QByteArray& current, const QByteArray& previous) +{ + Q_UNUSED(previous); + const int itemCount = count(); + if (itemCount <= 0) { + return; + } + + m_sortRole = roleIndex(current); + + KFileItemList sortedItems = m_sortedItems; + m_sortedItems.clear(); + m_items.clear(); + m_data.clear(); + emit itemsRemoved(KItemRangeList() << KItemRange(0, itemCount)); + + sort(sortedItems.begin(), sortedItems.end()); + int index = 0; + foreach (const KFileItem& item, sortedItems) { + m_sortedItems.append(item); + m_items.insert(item, index); + m_data.append(retrieveData(item)); + + ++index; + } + + emit itemsInserted(KItemRangeList() << KItemRange(0, itemCount)); +} + +void KFileItemModel::slotCompleted() +{ + if (m_minimumUpdateIntervalTimer->isActive()) { + // dispatchPendingItems() will be called when the timer + // has been expired. + return; + } + + dispatchPendingItems(); + m_minimumUpdateIntervalTimer->start(); +} + +void KFileItemModel::slotCanceled() +{ + m_minimumUpdateIntervalTimer->stop(); + m_maximumUpdateIntervalTimer->stop(); + dispatchPendingItems(); +} + +void KFileItemModel::slotNewItems(const KFileItemList& items) +{ + if (!m_pendingItemsToDelete.isEmpty()) { + removeItems(m_pendingItemsToDelete); + m_pendingItemsToDelete.clear(); + } + m_pendingItemsToInsert.append(items); + + if (useMaximumUpdateInterval() && !m_maximumUpdateIntervalTimer->isActive()) { + // Assure that items get dispatched if no completed() or canceled() signal is + // emitted during the maximum update interval. + m_maximumUpdateIntervalTimer->start(); + } +} + +void KFileItemModel::slotItemsDeleted(const KFileItemList& items) +{ + if (!m_pendingItemsToInsert.isEmpty()) { + insertItems(m_pendingItemsToInsert); + m_pendingItemsToInsert.clear(); + } + m_pendingItemsToDelete.append(items); +} + +void KFileItemModel::slotClear() +{ +#ifdef KFILEITEMMODEL_DEBUG + kDebug() << "Clearing all items"; +#endif + + m_minimumUpdateIntervalTimer->stop(); + m_maximumUpdateIntervalTimer->stop(); + m_pendingItemsToInsert.clear(); + m_pendingItemsToDelete.clear(); + + m_rootExpansionLevel = -1; + + const int removedCount = m_data.count(); + if (removedCount > 0) { + m_sortedItems.clear(); + m_items.clear(); + m_data.clear(); + emit itemsRemoved(KItemRangeList() << KItemRange(0, removedCount)); + } +} + +void KFileItemModel::slotClear(const KUrl& url) +{ + Q_UNUSED(url); +} + +void KFileItemModel::dispatchPendingItems() +{ + if (!m_pendingItemsToInsert.isEmpty()) { + Q_ASSERT(m_pendingItemsToDelete.isEmpty()); + insertItems(m_pendingItemsToInsert); + m_pendingItemsToInsert.clear(); + } else if (!m_pendingItemsToDelete.isEmpty()) { + Q_ASSERT(m_pendingItemsToInsert.isEmpty()); + removeItems(m_pendingItemsToDelete); + m_pendingItemsToDelete.clear(); + } +} + +void KFileItemModel::insertItems(const KFileItemList& items) +{ + if (items.isEmpty()) { + return; + } + +#ifdef KFILEITEMMODEL_DEBUG + QElapsedTimer timer; + timer.start(); + kDebug() << "==========================================================="; + kDebug() << "Inserting" << items.count() << "items"; +#endif + + KFileItemList sortedItems = items; + sort(sortedItems.begin(), sortedItems.end()); + +#ifdef KFILEITEMMODEL_DEBUG + kDebug() << "[TIME] Sorting:" << timer.elapsed(); +#endif + + KItemRangeList itemRanges; + int targetIndex = 0; + int sourceIndex = 0; + int insertedAtIndex = -1; + int insertedCount = 0; + while (sourceIndex < sortedItems.count()) { + // Find target index from m_items to insert the current item + // in a sorted order + const int previousTargetIndex = targetIndex; + while (targetIndex < m_sortedItems.count()) { + if (!lessThan(m_sortedItems.at(targetIndex), sortedItems.at(sourceIndex))) { + break; + } + ++targetIndex; + } + + if (targetIndex - previousTargetIndex > 0 && insertedAtIndex >= 0) { + itemRanges << KItemRange(insertedAtIndex, insertedCount); + insertedAtIndex = targetIndex; + insertedCount = 0; + } + + // Insert item at the position targetIndex + const KFileItem item = sortedItems.at(sourceIndex); + m_sortedItems.insert(targetIndex, item); + m_data.insert(targetIndex, retrieveData(item)); + // m_items will be inserted after the loop (see comment below) + ++insertedCount; + + if (insertedAtIndex < 0) { + insertedAtIndex = targetIndex; + } + ++targetIndex; + ++sourceIndex; + } + + // The indexes of all m_items must be adjusted, not only the index + // of the new items + for (int i = 0; i < m_sortedItems.count(); ++i) { + m_items.insert(m_sortedItems.at(i), i); + } + + itemRanges << KItemRange(insertedAtIndex, insertedCount); + emit itemsInserted(itemRanges); + +#ifdef KFILEITEMMODEL_DEBUG + kDebug() << "[TIME] Inserting of" << items.count() << "items:" << timer.elapsed(); +#endif +} + +void KFileItemModel::removeItems(const KFileItemList& items) +{ + if (items.isEmpty()) { + return; + } + +#ifdef KFILEITEMMODEL_DEBUG + kDebug() << "Removing " << items.count() << "items"; +#endif + + KFileItemList sortedItems = items; + sort(sortedItems.begin(), sortedItems.end()); + + QList<int> indexesToRemove; + indexesToRemove.reserve(items.count()); + + // Calculate the item ranges that will get deleted + KItemRangeList itemRanges; + int removedAtIndex = -1; + int removedCount = 0; + int targetIndex = 0; + foreach (const KFileItem& itemToRemove, sortedItems) { + const int previousTargetIndex = targetIndex; + while (targetIndex < m_sortedItems.count()) { + if (m_sortedItems.at(targetIndex) == itemToRemove) { + break; + } + ++targetIndex; + } + if (targetIndex >= m_sortedItems.count()) { + kWarning() << "Item that should be deleted has not been found!"; + return; + } + + if (targetIndex - previousTargetIndex > 0 && removedAtIndex >= 0) { + itemRanges << KItemRange(removedAtIndex, removedCount); + removedAtIndex = targetIndex; + removedCount = 0; + } + + indexesToRemove.append(targetIndex); + if (removedAtIndex < 0) { + removedAtIndex = targetIndex; + } + ++removedCount; + ++targetIndex; + } + + // Delete the items + for (int i = indexesToRemove.count() - 1; i >= 0; --i) { + const int indexToRemove = indexesToRemove.at(i); + m_items.remove(m_sortedItems.at(indexToRemove)); + m_sortedItems.removeAt(indexToRemove); + m_data.removeAt(indexToRemove); + } + + // The indexes of all m_items must be adjusted, not only the index + // of the removed items + for (int i = 0; i < m_sortedItems.count(); ++i) { + m_items.insert(m_sortedItems.at(i), i); + } + + if (count() <= 0) { + m_rootExpansionLevel = -1; + } + + itemRanges << KItemRange(removedAtIndex, removedCount); + emit itemsRemoved(itemRanges); +} + +void KFileItemModel::removeExpandedItems() +{ + + KFileItemList expandedItems; + + const int maxIndex = m_data.count() - 1; + for (int i = 0; i <= maxIndex; ++i) { + if (m_data.at(i).value("expansionLevel").toInt() > 0) { + const KFileItem fileItem = m_sortedItems.at(i); + expandedItems.append(fileItem); + } + } + + // The m_rootExpansionLevel may not get reset before all items with + // a bigger expansionLevel have been removed. + Q_ASSERT(m_rootExpansionLevel >= 0); + removeItems(expandedItems); + + m_rootExpansionLevel = -1; +} + +void KFileItemModel::resetRoles() +{ + for (int i = 0; i < RolesCount; ++i) { + m_requestRole[i] = false; + } +} + +KFileItemModel::Role KFileItemModel::roleIndex(const QByteArray& role) const +{ + static QHash<QByteArray, Role> rolesHash; + if (rolesHash.isEmpty()) { + rolesHash.insert("name", NameRole); + rolesHash.insert("size", SizeRole); + rolesHash.insert("date", DateRole); + rolesHash.insert("permissions", PermissionsRole); + rolesHash.insert("owner", OwnerRole); + rolesHash.insert("group", GroupRole); + rolesHash.insert("type", TypeRole); + rolesHash.insert("destination", DestinationRole); + rolesHash.insert("path", PathRole); + rolesHash.insert("isDir", IsDirRole); + rolesHash.insert("isExpanded", IsExpandedRole); + rolesHash.insert("expansionLevel", ExpansionLevelRole); + } + return rolesHash.value(role, NoRole); +} + +QHash<QByteArray, QVariant> KFileItemModel::retrieveData(const KFileItem& item) const +{ + // It is important to insert only roles that are fast to retrieve. E.g. + // KFileItem::iconName() can be very expensive if the MIME-type is unknown + // and hence will be retrieved asynchronously by KFileItemModelRolesUpdater. + QHash<QByteArray, QVariant> data; + data.insert("iconPixmap", QPixmap()); + + const bool isDir = item.isDir(); + if (m_requestRole[IsDirRole]) { + data.insert("isDir", isDir); + } + + if (m_requestRole[NameRole]) { + data.insert("name", item.name()); + } + + if (m_requestRole[SizeRole]) { + if (isDir) { + data.insert("size", QVariant()); + } else { + data.insert("size", item.size()); + } + } + + if (m_requestRole[DateRole]) { + // Don't use KFileItem::timeString() as this is too expensive when + // having several thousands of items. Instead the formatting of the + // date-time will be done on-demand by the view when the date will be shown. + const KDateTime dateTime = item.time(KFileItem::ModificationTime); + data.insert("date", dateTime.dateTime()); + } + + if (m_requestRole[PermissionsRole]) { + data.insert("permissions", item.permissionsString()); + } + + if (m_requestRole[OwnerRole]) { + data.insert("owner", item.user()); + } + + if (m_requestRole[GroupRole]) { + data.insert("group", item.group()); + } + + if (m_requestRole[DestinationRole]) { + QString destination = item.linkDest(); + if (destination.isEmpty()) { + destination = i18nc("@item:intable", "No destination"); + } + data.insert("destination", destination); + } + + if (m_requestRole[PathRole]) { + data.insert("path", item.localPath()); + } + + if (m_requestRole[IsExpandedRole]) { + data.insert("isExpanded", false); + } + + if (m_requestRole[ExpansionLevelRole]) { + if (m_rootExpansionLevel < 0) { + KDirLister* dirLister = m_dirLister.data(); + if (dirLister) { + const QString rootDir = dirLister->url().directory(KUrl::AppendTrailingSlash); + m_rootExpansionLevel = rootDir.count('/'); + } + } + const QString dir = item.url().directory(KUrl::AppendTrailingSlash); + const int level = dir.count('/') - m_rootExpansionLevel - 1; + data.insert("expansionLevel", level); + } + + if (item.isMimeTypeKnown()) { + data.insert("iconName", item.iconName()); + + if (m_requestRole[TypeRole]) { + data.insert("type", item.mimeComment()); + } + } + + return data; +} + +bool KFileItemModel::lessThan(const KFileItem& a, const KFileItem& b) const +{ + int result = 0; + + if (m_rootExpansionLevel >= 0) { + result = expansionLevelsCompare(a, b); + if (result != 0) { + // The items have parents with different expansion levels + return result < 0; + } + } + + if (m_sortFoldersFirst) { + const bool isDirA = a.isDir(); + const bool isDirB = b.isDir(); + if (isDirA && !isDirB) { + return true; + } else if (!isDirA && isDirB) { + return false; + } + } + + switch (m_sortRole) { + case NameRole: { + result = stringCompare(a.text(), b.text()); + if (result == 0) { + // KFileItem::text() may not be unique in case UDS_DISPLAY_NAME is used + result = stringCompare(a.name(m_caseSensitivity == Qt::CaseInsensitive), + b.name(m_caseSensitivity == Qt::CaseInsensitive)); + } + break; + } + + case DateRole: { + const KDateTime dateTimeA = a.time(KFileItem::ModificationTime); + const KDateTime dateTimeB = b.time(KFileItem::ModificationTime); + if (dateTimeA < dateTimeB) { + result = -1; + } else if (dateTimeA > dateTimeB) { + result = +1; + } + break; + } + + default: + break; + } + + if (result == 0) { + // It must be assured that the sort order is always unique even if two values have been + // equal. In this case a comparison of the URL is done which is unique in all cases + // within KDirLister. + result = QString::compare(a.url().url(), b.url().url(), Qt::CaseSensitive); + } + + return result < 0; +} + +void KFileItemModel::sort(const KFileItemList::iterator& startIterator, const KFileItemList::iterator& endIterator) +{ + KFileItemList::iterator start = startIterator; + KFileItemList::iterator end = endIterator; + + // The implementation is based on qSortHelper() from qalgorithms.h + // Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). + // In opposite to qSort() it allows to use a member-function for the comparison of elements. + while (1) { + int span = int(end - start); + if (span < 2) { + return; + } + + --end; + KFileItemList::iterator low = start, high = end - 1; + KFileItemList::iterator pivot = start + span / 2; + + if (lessThan(*end, *start)) { + qSwap(*end, *start); + } + if (span == 2) { + return; + } + + if (lessThan(*pivot, *start)) { + qSwap(*pivot, *start); + } + if (lessThan(*end, *pivot)) { + qSwap(*end, *pivot); + } + if (span == 3) { + return; + } + + qSwap(*pivot, *end); + + while (low < high) { + while (low < high && lessThan(*low, *end)) { + ++low; + } + + while (high > low && lessThan(*end, *high)) { + --high; + } + if (low < high) { + qSwap(*low, *high); + ++low; + --high; + } else { + break; + } + } + + if (lessThan(*low, *end)) { + ++low; + } + + qSwap(*end, *low); + sort(start, low); + + start = low + 1; + ++end; + } +} + +int KFileItemModel::stringCompare(const QString& a, const QString& b) const +{ + // Taken from KDirSortFilterProxyModel (kdelibs/kfile/kdirsortfilterproxymodel.*) + // Copyright (C) 2006 by Peter Penz <[email protected]> + // Copyright (C) 2006 by Dominic Battre <[email protected]> + // Copyright (C) 2006 by Martin Pool <[email protected]> + + if (m_caseSensitivity == Qt::CaseInsensitive) { + const int result = m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseInsensitive) + : QString::compare(a, b, Qt::CaseInsensitive); + if (result != 0) { + // Only return the result, if the strings are not equal. If they are equal by a case insensitive + // comparison, still a deterministic sort order is required. A case sensitive + // comparison is done as fallback. + return result; + } + } + + return m_naturalSorting ? KStringHandler::naturalCompare(a, b, Qt::CaseSensitive) + : QString::compare(a, b, Qt::CaseSensitive); +} + +int KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& b) const +{ + const KUrl urlA = a.url(); + const KUrl urlB = b.url(); + if (urlA.directory() == urlB.directory()) { + // Both items have the same directory as parent + return 0; + } + + // Check whether one item is the parent of the other item + if (urlA.isParentOf(urlB)) { + return -1; + } else if (urlB.isParentOf(urlA)) { + return +1; + } + + // Determine the maximum common path of both items and + // remember the index in 'index' + const QString pathA = urlA.path(); + const QString pathB = urlB.path(); + + const int maxIndex = qMin(pathA.length(), pathB.length()) - 1; + int index = 0; + while (index <= maxIndex && pathA.at(index) == pathB.at(index)) { + ++index; + } + if (index > maxIndex) { + index = maxIndex; + } + while (pathA.at(index) != QLatin1Char('/') && index > 0) { + --index; + } + + // Determine the first sub-path after the common path and + // check whether it represents a directory or already a file + bool isDirA = true; + const QString subPathA = subPath(a, pathA, index, &isDirA); + bool isDirB = true; + const QString subPathB = subPath(b, pathB, index, &isDirB); + + if (isDirA && !isDirB) { + return -1; + } else if (!isDirA && isDirB) { + return +1; + } + + return stringCompare(subPathA, subPathB); +} + +QString KFileItemModel::subPath(const KFileItem& item, + const QString& itemPath, + int start, + bool* isDir) const +{ + Q_ASSERT(isDir); + const int pathIndex = itemPath.indexOf('/', start + 1); + *isDir = (pathIndex > 0) || item.isDir(); + return itemPath.mid(start, pathIndex - start); +} + +bool KFileItemModel::useMaximumUpdateInterval() const +{ + const KDirLister* dirLister = m_dirLister.data(); + return dirLister && !dirLister->url().isLocalFile(); +} + +#include "kfileitemmodel.moc" diff --git a/src/kitemviews/kfileitemmodel.h b/src/kitemviews/kfileitemmodel.h new file mode 100644 index 000000000..e6c89d744 --- /dev/null +++ b/src/kitemviews/kfileitemmodel.h @@ -0,0 +1,192 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KFILEITEMMODEL_H +#define KFILEITEMMODEL_H + +#include <libdolphin_export.h> +#include <KFileItemList> +#include <KUrl> +#include <kitemviews/kitemmodelbase.h> + +#include <QHash> + +class KDirLister; +class QTimer; + +/** + * @brief KItemModelBase implementation for KFileItems. + * + * KFileItemModel is connected with one KDirLister. Each time the KDirLister + * emits new items, removes items or changes items the model gets synchronized. + * + * KFileItemModel supports sorting and grouping of items. Additional roles that + * are not part of KFileItem can be added with KFileItemModel::setData(). + * + * Also the recursive expansion of sub-directories is supported by + * KFileItemModel::setExpanded(). + */ +class LIBDOLPHINPRIVATE_EXPORT KFileItemModel : public KItemModelBase +{ + Q_OBJECT + +public: + explicit KFileItemModel(KDirLister* dirLister, QObject* parent = 0); + virtual ~KFileItemModel(); + + virtual int count() const; + virtual QHash<QByteArray, QVariant> data(int index) const; + virtual bool setData(int index, const QHash<QByteArray, QVariant> &values); + + /** + * @return True + * @reimp + */ + virtual bool supportsGrouping() const; + + /** + * @return True + * @reimp + */ + virtual bool supportsSorting() const; + + /** + * @return The file-item for the index \a index. If the index is in a valid + * range it is assured that the file-item is not null. The runtime + * complexity of this call is O(1). + */ + KFileItem fileItem(int index) const; + + /** + * @return The index for the file-item \a item. -1 is returned if no file-item + * is found or if the file-item is null. The runtime + * complexity of this call is O(1). + */ + int index(const KFileItem& item) const; + + /** + * Clears all items of the model. + */ + void clear(); + + // TODO: "name" + "isDir" is default in ctor + void setRoles(const QSet<QByteArray>& roles); + QSet<QByteArray> roles() const; + + bool setExpanded(int index, bool expanded); + bool isExpanded(int index) const; + bool isExpandable(int index) const; + +protected: + virtual void onGroupRoleChanged(const QByteArray& current, const QByteArray& previous); + virtual void onSortRoleChanged(const QByteArray& current, const QByteArray& previous); + +private slots: + void slotCompleted(); + void slotCanceled(); + void slotNewItems(const KFileItemList& items); + void slotItemsDeleted(const KFileItemList& items); + void slotClear(); + void slotClear(const KUrl& url); + + void dispatchPendingItems(); + +private: + void insertItems(const KFileItemList& items); + void removeItems(const KFileItemList& items); + + void removeExpandedItems(); + + enum Role { + NoRole, + NameRole, + SizeRole, + DateRole, + PermissionsRole, + OwnerRole, + GroupRole, + TypeRole, + DestinationRole, + PathRole, + IsDirRole, + IsExpandedRole, + ExpansionLevelRole, + RolesCount // Mandatory last entry + }; + + void resetRoles(); + + Role roleIndex(const QByteArray& role) const; + + QHash<QByteArray, QVariant> retrieveData(const KFileItem& item) const; + + bool lessThan(const KFileItem& a, const KFileItem& b) const; + void sort(const KFileItemList::iterator& start, const KFileItemList::iterator& end); + int stringCompare(const QString& a, const QString& b) const; + + /** + * Compares the expansion level of both items. The "expansion level" is defined + * by the number of parent directories. However simply comparing just the numbers + * is not sufficient, it is also important to check the hierarchy for having + * a correct order like shown in a tree. + */ + int expansionLevelsCompare(const KFileItem& a, const KFileItem& b) const; + + /** + * Helper method for expansionLevelCompare(). + */ + QString subPath(const KFileItem& item, + const QString& itemPath, + int start, + bool* isDir) const; + + bool useMaximumUpdateInterval() const; + +private: + QWeakPointer<KDirLister> m_dirLister; + + bool m_naturalSorting; + bool m_sortFoldersFirst; + + Role m_groupRole; + Role m_sortRole; + Qt::CaseSensitivity m_caseSensitivity; + + KFileItemList m_sortedItems; // Allows O(1) access for KFileItemModel::fileItem(int index) + QHash<KFileItem, int> m_items; // Allows O(1) access for KFileItemModel::index(const KFileItem& item) + QList<QHash<QByteArray, QVariant> > m_data; + + bool m_requestRole[RolesCount]; + + QTimer* m_minimumUpdateIntervalTimer; + QTimer* m_maximumUpdateIntervalTimer; + KFileItemList m_pendingItemsToInsert; + KFileItemList m_pendingItemsToDelete; + + // Stores the smallest expansion level of the root-URL. Is required to calculate + // the "expansionLevel" role in an efficient way. A value < 0 indicates that + // it has not been initialized yet. + mutable int m_rootExpansionLevel; + + friend class KFileItemModelTest; // For unit testing +}; + +#endif + + diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp new file mode 100644 index 000000000..325d08e18 --- /dev/null +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@ -0,0 +1,765 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kfileitemmodelrolesupdater.h" + +#include "kfileitemmodel.h" +#include "kpixmapmodifier_p.h" + +#include <KConfig> +#include <KConfigGroup> +#include <KDebug> +#include <KFileItem> +#include <KGlobal> +#include <KIO/PreviewJob> +#include <QPainter> +#include <QPixmap> +#include <QElapsedTimer> +#include <QTimer> + +// Required includes for subDirectoriesCount(): +#ifdef Q_WS_WIN + #include <QDir> +#else + #include <dirent.h> + #include <QFile> +#endif + +#define KFILEITEMMODELROLESUPDATER_DEBUG + +namespace { + const int MaxResolveItemsCount = 100; +} + +KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QObject* parent) : + QObject(parent), + m_paused(false), + m_previewChangedDuringPausing(false), + m_iconSizeChangedDuringPausing(false), + m_rolesChangedDuringPausing(false), + m_previewShown(false), + m_clearPreviews(false), + m_model(model), + m_iconSize(), + m_firstVisibleIndex(0), + m_lastVisibleIndex(-1), + m_roles(), + m_enabledPlugins(), + m_pendingVisibleItems(), + m_pendingInvisibleItems(), + m_previewJobs(), + m_resolvePendingRolesTimer(0) +{ + Q_ASSERT(model); + + const KConfigGroup globalConfig(KGlobal::config(), "PreviewSettings"); + m_enabledPlugins = globalConfig.readEntry("Plugins", QStringList() + << "directorythumbnail" + << "imagethumbnail" + << "jpegthumbnail"); + + connect(m_model, SIGNAL(itemsInserted(KItemRangeList)), + this, SLOT(slotItemsInserted(KItemRangeList))); + connect(m_model, SIGNAL(itemsRemoved(KItemRangeList)), + this, SLOT(slotItemsRemoved(KItemRangeList))); + connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + + // A timer with a minimal timeout is used to merge several triggerPendingRolesResolving() calls + // to only one call of resolvePendingRoles(). + m_resolvePendingRolesTimer = new QTimer(this); + m_resolvePendingRolesTimer->setInterval(1); + m_resolvePendingRolesTimer->setSingleShot(true); + connect(m_resolvePendingRolesTimer, SIGNAL(timeout()), this, SLOT(resolvePendingRoles())); +} + +KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater() +{ +} + +void KFileItemModelRolesUpdater::setIconSize(const QSize& size) +{ + if (size != m_iconSize) { + m_iconSize = size; + if (m_paused) { + m_iconSizeChangedDuringPausing = true; + } else if (m_previewShown) { + // An icon size change requires the regenerating of + // all previews + sortAndResolveAllRoles(); + } else { + sortAndResolvePendingRoles(); + } + } +} + +QSize KFileItemModelRolesUpdater::iconSize() const +{ + return m_iconSize; +} + +void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count) +{ + if (index < 0) { + index = 0; + } + if (count < 0) { + count = 0; + } + + if (index == m_firstVisibleIndex && count == m_lastVisibleIndex - m_firstVisibleIndex + 1) { + // The range has not been changed + return; + } + + m_firstVisibleIndex = index; + m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1); + + if (hasPendingRoles() && !m_paused) { + sortAndResolvePendingRoles(); + } +} + +void KFileItemModelRolesUpdater::setPreviewShown(bool show) +{ + if (show == m_previewShown) { + return; + } + + m_previewShown = show; + if (!show) { + m_clearPreviews = true; + } + + if (m_paused) { + m_previewChangedDuringPausing = true; + } else { + sortAndResolveAllRoles(); + } +} + +bool KFileItemModelRolesUpdater::isPreviewShown() const +{ + return m_previewShown; +} + +void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList& list) +{ + m_enabledPlugins = list; +} + +void KFileItemModelRolesUpdater::setPaused(bool paused) +{ + if (paused == m_paused) { + return; + } + + m_paused = paused; + if (paused) { + if (hasPendingRoles()) { + foreach (KJob* job, m_previewJobs) { + job->kill(); + } + Q_ASSERT(m_previewJobs.isEmpty()); + } + } else { + const bool resolveAll = (m_iconSizeChangedDuringPausing && m_previewShown) || + (m_previewChangedDuringPausing && !m_previewShown) || + m_rolesChangedDuringPausing; + if (resolveAll) { + sortAndResolveAllRoles(); + } else { + sortAndResolvePendingRoles(); + } + + m_iconSizeChangedDuringPausing = false; + m_previewChangedDuringPausing = false; + m_rolesChangedDuringPausing = false; + } +} + +void KFileItemModelRolesUpdater::setRoles(const QSet<QByteArray>& roles) +{ + if (roles.count() == m_roles.count()) { + bool isEqual = true; + foreach (const QByteArray& role, roles) { + if (!m_roles.contains(role)) { + isEqual = false; + break; + } + } + if (isEqual) { + return; + } + } + + m_roles = roles; + + if (m_paused) { + m_rolesChangedDuringPausing = true; + } else { + sortAndResolveAllRoles(); + } +} + +QSet<QByteArray> KFileItemModelRolesUpdater::roles() const +{ + return m_roles; +} + +bool KFileItemModelRolesUpdater::isPaused() const +{ + return m_paused; +} + +QStringList KFileItemModelRolesUpdater::enabledPlugins() const +{ + return m_enabledPlugins; +} + +void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList& itemRanges) +{ + // If no valid index range is given assume that all items are visible. + // A cleanup will be done later as soon as the index range has been set. + const bool hasValidIndexRange = (m_lastVisibleIndex >= 0); + + if (hasValidIndexRange) { + // Move all current pending visible items that are not visible anymore + // to the pending invisible items. + QSetIterator<KFileItem> it(m_pendingVisibleItems); + while (it.hasNext()) { + const KFileItem item = it.next(); + const int index = m_model->index(item); + if (index < m_firstVisibleIndex || index > m_lastVisibleIndex) { + m_pendingVisibleItems.remove(item); + m_pendingInvisibleItems.insert(item); + } + } + } + + int rangesCount = 0; + + foreach (const KItemRange& range, itemRanges) { + rangesCount += range.count; + + // Add the inserted items to the pending visible and invisible items + const int lastIndex = range.index + range.count - 1; + for (int i = range.index; i <= lastIndex; ++i) { + const KFileItem item = m_model->fileItem(i); + if (!hasValidIndexRange || (i >= m_firstVisibleIndex && i <= m_lastVisibleIndex)) { + m_pendingVisibleItems.insert(item); + } else { + m_pendingInvisibleItems.insert(item); + } + } + } + + triggerPendingRolesResolving(rangesCount); +} + +void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRanges) +{ + Q_UNUSED(itemRanges); + m_firstVisibleIndex = 0; + m_lastVisibleIndex = -1; + if (hasPendingRoles() && m_model->count() <= 0) { + resetPendingRoles(); + } +} + +void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRanges, + const QSet<QByteArray>& roles) +{ + Q_UNUSED(itemRanges); + Q_UNUSED(roles); + // TODO +} + +void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPixmap& pixmap) +{ + m_pendingVisibleItems.remove(item); + m_pendingInvisibleItems.remove(item); + + const int index = m_model->index(item); + if (index < 0) { + return; + } + + QPixmap scaledPixmap = pixmap; + + const QString mimeType = item.mimetype(); + const int slashIndex = mimeType.indexOf(QLatin1Char('/')); + const QString mimeTypeGroup = mimeType.left(slashIndex); + if (mimeTypeGroup == QLatin1String("image")) { + KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); + } else { + KPixmapModifier::scale(scaledPixmap, m_iconSize); + } + + QHash<QByteArray, QVariant> data = rolesData(item); + data.insert("iconPixmap", scaledPixmap); + + disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + m_model->setData(index, data); + connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); +} + +void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem& item) +{ + m_pendingVisibleItems.remove(item); + m_pendingInvisibleItems.remove(item); + + const bool clearPreviews = m_clearPreviews; + m_clearPreviews = true; + applyResolvedRoles(item, ResolveAll); + m_clearPreviews = clearPreviews; +} + +void KFileItemModelRolesUpdater::slotPreviewJobFinished(KJob* job) +{ +#ifdef KFILEITEMMODELROLESUPDATER_DEBUG + kDebug() << "Preview job finished. Pending visible:" << m_pendingVisibleItems.count() << "invisible:" << m_pendingInvisibleItems.count(); +#endif + + m_previewJobs.removeOne(job); + if (!m_previewJobs.isEmpty() || !hasPendingRoles()) { + return; + } + + const KFileItemList visibleItems = sortedItems(m_pendingVisibleItems); + const KFileItemList invisibleItems = itemSubSet(m_pendingInvisibleItems, MaxResolveItemsCount - visibleItems.count()); + startPreviewJob(visibleItems + invisibleItems); +} + +void KFileItemModelRolesUpdater::resolvePendingRoles() +{ + int resolvedCount = 0; + + const bool hasSlowRoles = m_previewShown + || m_roles.contains("size") + || m_roles.contains("type"); + const ResolveHint resolveHint = hasSlowRoles ? ResolveFast : ResolveAll; + + // Resolving the MIME type can be expensive. Assure that not more than 200 ms are + // spend for resolving them synchronously. Usually this is more than enough to determine + // all visible items, but there are corner cases where this limit gets easily exceeded. + const int MaxTime = 200; + QElapsedTimer timer; + timer.start(); + + // Resolve the MIME type of all visible items + QSetIterator<KFileItem> visibleIt(m_pendingVisibleItems); + while (visibleIt.hasNext()) { + const KFileItem item = visibleIt.next(); + applyResolvedRoles(item, resolveHint); + if (!hasSlowRoles) { + Q_ASSERT(!m_pendingInvisibleItems.contains(item)); + // All roles have been resolved already by applyResolvedRoles() + m_pendingVisibleItems.remove(item); + } + ++resolvedCount; + + if (timer.elapsed() > MaxTime) { + break; + } + } + + // Resolve the MIME type of the invisible items at least until the timeout + // has been exceeded or the maximum number of items has been reached + KFileItemList invisibleItems; + if (m_lastVisibleIndex >= 0) { + // The visible range is valid, don't care about the order how the MIME + // type of invisible items get resolved + invisibleItems = m_pendingInvisibleItems.toList(); + } else { + // The visible range is temporary invalid (e.g. happens when loading + // a directory) so take care to sort the currently invisible items where + // a part will get visible later + invisibleItems = sortedItems(m_pendingInvisibleItems); + } + + int index = 0; + while (resolvedCount < MaxResolveItemsCount && index < invisibleItems.count() && timer.elapsed() <= MaxTime) { + const KFileItem item = invisibleItems.at(index); + applyResolvedRoles(item, resolveHint); + + if (!hasSlowRoles) { + // All roles have been resolved already by applyResolvedRoles() + m_pendingInvisibleItems.remove(item); + } + ++index; + ++resolvedCount; + } + + if (m_previewShown) { + KFileItemList items = sortedItems(m_pendingVisibleItems); + items += invisibleItems; + startPreviewJob(items); + } else { + QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles())); + } + +#ifdef KFILEITEMMODELROLESUPDATER_DEBUG + if (timer.elapsed() > MaxTime) { + kDebug() << "Maximum time exceeded, skipping items... Remaining visible:" << m_pendingVisibleItems.count() + << "invisible:" << m_pendingInvisibleItems.count(); + } + kDebug() << "[TIME] Resolved pending roles:" << timer.elapsed(); +#endif +} + +void KFileItemModelRolesUpdater::resolveNextPendingRoles() +{ + if (m_paused) { + return; + } + + if (m_previewShown) { + // The preview has been turned on since the last run. Skip + // resolving further pending roles as this is done as soon + // as a preview has been received. + return; + } + + int resolvedCount = 0; + bool changed = false; + for (int i = 0; i <= 1; ++i) { + QSet<KFileItem>& pendingItems = (i == 0) ? m_pendingVisibleItems : m_pendingInvisibleItems; + QSetIterator<KFileItem> it(pendingItems); + while (it.hasNext() && !changed && resolvedCount < MaxResolveItemsCount) { + const KFileItem item = it.next(); + pendingItems.remove(item); + changed = applyResolvedRoles(item, ResolveAll); + ++resolvedCount; + } + } + + if (hasPendingRoles()) { + QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles())); + } else { + m_clearPreviews = false; + } + +#ifdef KFILEITEMMODELROLESUPDATER_DEBUG + static int callCount = 0; + ++callCount; + if (callCount % 100 == 0) { + kDebug() << "Remaining visible roles to resolve:" << m_pendingVisibleItems.count() + << "invisible:" << m_pendingInvisibleItems.count(); + } +#endif +} + +void KFileItemModelRolesUpdater::startPreviewJob(const KFileItemList& items) +{ + if (items.count() <= 0 || m_paused) { + return; + } + + // PreviewJob internally caches items always with the size of + // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done + // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must + // do a downscaling anyhow because of the frame, so in this case only the provided + // cache sizes are requested. + const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) + ? QSize(256, 256) : QSize(128, 128); + + KJob* job; + if (items.count() <= MaxResolveItemsCount) { + job = KIO::filePreview(items, cacheSize, &m_enabledPlugins); + } else { + KFileItemList itemsSubSet; + for (int i = 0; i <= MaxResolveItemsCount; ++i) { + itemsSubSet.append(items.at(i)); + } + job = KIO::filePreview(itemsSubSet, cacheSize, &m_enabledPlugins); + } + + connect(job, SIGNAL(gotPreview(const KFileItem&, const QPixmap&)), + this, SLOT(slotGotPreview(const KFileItem&, const QPixmap&))); + connect(job, SIGNAL(failed(KFileItem)), + this, SLOT(slotPreviewFailed(KFileItem))); + connect(job, SIGNAL(finished(KJob*)), + this, SLOT(slotPreviewJobFinished(KJob*))); + + m_previewJobs.append(job); +} + + +bool KFileItemModelRolesUpdater::hasPendingRoles() const +{ + return !m_pendingVisibleItems.isEmpty() || !m_pendingInvisibleItems.isEmpty(); +} + +void KFileItemModelRolesUpdater::resetPendingRoles() +{ + m_pendingVisibleItems.clear(); + m_pendingInvisibleItems.clear(); + + foreach (KJob* job, m_previewJobs) { + job->kill(); + } + Q_ASSERT(m_previewJobs.isEmpty()); +} + +void KFileItemModelRolesUpdater::triggerPendingRolesResolving(int count) +{ + Q_ASSERT(count <= m_model->count()); + if (count == m_model->count()) { + // When initially loading a directory a synchronous resolving prevents a minor + // flickering when opening directories. This is also fine from a performance point + // of view as it is assured in resolvePendingRoles() to never block the event-loop + // for more than 200 ms. + resolvePendingRoles(); + } else { + // Items have been added. This can be done in several small steps within one loop + // because of the sorting and hence may not trigger any expensive operation. + m_resolvePendingRolesTimer->start(); + } +} + +void KFileItemModelRolesUpdater::sortAndResolveAllRoles() +{ + if (m_paused) { + return; + } + + resetPendingRoles(); + Q_ASSERT(m_pendingVisibleItems.isEmpty()); + Q_ASSERT(m_pendingInvisibleItems.isEmpty()); + + if (m_model->count() <= 0) { + return; + } + + // Determine all visible items + Q_ASSERT(m_firstVisibleIndex >= 0); + for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) { + const KFileItem item = m_model->fileItem(i); + if (!item.isNull()) { + m_pendingVisibleItems.insert(item); + } + } + + // Determine all invisible items + for (int i = 0; i < m_firstVisibleIndex; ++i) { + const KFileItem item = m_model->fileItem(i); + if (!item.isNull()) { + m_pendingInvisibleItems.insert(item); + } + } + for (int i = m_lastVisibleIndex + 1; i < m_model->count(); ++i) { + const KFileItem item = m_model->fileItem(i); + if (!item.isNull()) { + m_pendingInvisibleItems.insert(item); + } + } + + triggerPendingRolesResolving(m_pendingVisibleItems.count() + + m_pendingInvisibleItems.count()); +} + +void KFileItemModelRolesUpdater::sortAndResolvePendingRoles() +{ + Q_ASSERT(!m_paused); + if (m_model->count() <= 0) { + return; + } + + // If no valid index range is given assume that all items are visible. + // A cleanup will be done later as soon as the index range has been set. + const bool hasValidIndexRange = (m_lastVisibleIndex >= 0); + + // Trigger a preview generation of all pending items. Assure that the visible + // pending items get generated first. + QSet<KFileItem> pendingItems; + pendingItems += m_pendingVisibleItems; + pendingItems += m_pendingInvisibleItems; + + resetPendingRoles(); + Q_ASSERT(m_pendingVisibleItems.isEmpty()); + Q_ASSERT(m_pendingInvisibleItems.isEmpty()); + + QSetIterator<KFileItem> it(pendingItems); + while (it.hasNext()) { + const KFileItem item = it.next(); + if (item.isNull()) { + continue; + } + + const int index = m_model->index(item); + if (!hasValidIndexRange || (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex)) { + m_pendingVisibleItems.insert(item); + } else { + m_pendingInvisibleItems.insert(item); + } + } + + triggerPendingRolesResolving(m_pendingVisibleItems.count() + + m_pendingInvisibleItems.count()); +} + +bool KFileItemModelRolesUpdater::applyResolvedRoles(const KFileItem& item, ResolveHint hint) +{ + const bool resolveAll = (hint == ResolveAll); + + bool mimeTypeChanged = false; + if (!item.isMimeTypeKnown()) { + item.determineMimeType(); + mimeTypeChanged = true; + } + + if (mimeTypeChanged || resolveAll || m_clearPreviews) { + const int index = m_model->index(item); + if (index < 0) { + return false; + } + + QHash<QByteArray, QVariant> data; + if (resolveAll) { + data = rolesData(item); + } + + if (mimeTypeChanged || m_clearPreviews) { + data.insert("iconName", item.iconName()); + } + if (m_clearPreviews) { + data.insert("iconPixmap", QString()); + } + + disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + m_model->setData(index, data); + connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + return true; + } + + return false; +} + +QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileItem& item) const +{ + QHash<QByteArray, QVariant> data; + + if (m_roles.contains("size")) { + if (item.isDir() && item.isLocalFile()) { + const QString path = item.localPath(); + const int count = subDirectoriesCount(path); + if (count >= 0) { + data.insert("size", KIO::filesize_t(count)); + } + } + } + + if (m_roles.contains("type")) { + data.insert("type", item.mimeComment()); + } + + return data; +} + +KFileItemList KFileItemModelRolesUpdater::sortedItems(const QSet<KFileItem>& items) const +{ + KFileItemList itemList; + if (items.isEmpty()) { + return itemList; + } + +#ifdef KFILEITEMMODELROLESUPDATER_DEBUG + QElapsedTimer timer; + timer.start(); +#endif + + QList<int> indexes; + indexes.reserve(items.count()); + + QSetIterator<KFileItem> it(items); + while (it.hasNext()) { + const KFileItem item = it.next(); + const int index = m_model->index(item); + indexes.append(index); + } + qSort(indexes); + + itemList.reserve(items.count()); + foreach (int index, indexes) { + itemList.append(m_model->fileItem(index)); + } + +#ifdef KFILEITEMMODELROLESUPDATER_DEBUG + kDebug() << "[TIME] Sorting of items:" << timer.elapsed(); +#endif + return itemList; +} + +KFileItemList KFileItemModelRolesUpdater::itemSubSet(const QSet<KFileItem>& items, int count) +{ + KFileItemList itemList; + + int index = 0; + QSetIterator<KFileItem> it(items); + while (it.hasNext() && index < count) { + const KFileItem item = it.next(); + if (item.isNull()) { + continue; + } + itemList.append(item); + ++index; + } + + return itemList; +} + +int KFileItemModelRolesUpdater::subDirectoriesCount(const QString& path) +{ +#ifdef Q_WS_WIN + QDir dir(path); + return dir.entryList(QDir::AllEntries|QDir::NoDotAndDotDot|QDir::System).count(); +#else + // Taken from kdelibs/kio/kio/kdirmodel.cpp + // Copyright (C) 2006 David Faure <[email protected]> + + int count = -1; + DIR* dir = ::opendir(QFile::encodeName(path)); + if (dir) { + count = 0; + struct dirent *dirEntry = 0; + while ((dirEntry = ::readdir(dir))) { + if (dirEntry->d_name[0] == '.') { + if (dirEntry->d_name[1] == '\0') { + // Skip "." + continue; + } + if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') { + // Skip ".." + continue; + } + } + ++count; + } + ::closedir(dir); + } + return count; +#endif +} + +#include "kfileitemmodelrolesupdater.moc" diff --git a/src/kitemviews/kfileitemmodelrolesupdater.h b/src/kitemviews/kfileitemmodelrolesupdater.h new file mode 100644 index 000000000..4931f9d64 --- /dev/null +++ b/src/kitemviews/kfileitemmodelrolesupdater.h @@ -0,0 +1,177 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KFILEITEMMODELROLESUPDATER_H +#define KFILEITEMMODELROLESUPDATER_H + +#include <libdolphin_export.h> + +#include <KFileItem> +#include <kitemviews/kitemmodelbase.h> + +#include <QObject> +#include <QSet> +#include <QSize> +#include <QStringList> + +class KFileItemModel; +class KJob; +class QPixmap; +class QTimer; + +/** + * @brief Resolves expensive roles asynchronously and applies them to the KFileItemModel. + * + * KFileItemModel only resolves roles that are inexpensive like e.g. the file name or + * the permissions. Creating previews or determining the MIME-type can be quite expensive + * and KFileItemModelRolesUpdater takes care to update such roles asynchronously. + */ +class LIBDOLPHINPRIVATE_EXPORT KFileItemModelRolesUpdater : public QObject +{ + Q_OBJECT + +public: + KFileItemModelRolesUpdater(KFileItemModel* model, QObject* parent = 0); + virtual ~KFileItemModelRolesUpdater(); + + void setIconSize(const QSize& size); + QSize iconSize() const; + + /** + * Sets the range of items that are visible currently. The roles + * of visible items are resolved first. + */ + void setVisibleIndexRange(int index, int count); + + /** + * If \a show is set to true, the "iconPixmap" role will be filled with a preview + * of the file. If \a show is false the MIME type icon will be used for the "iconPixmap" + * role. + */ + void setPreviewShown(bool show); + bool isPreviewShown() const; + + /** + * If \a paused is set to true the asynchronous resolving of roles will be paused. + * State changes during pauses like changing the icon size or the preview-shown + * will be remembered and handled after unpausing. + */ + void setPaused(bool paused); + bool isPaused() const; + + /** + * Sets the roles that should be resolved asynchronously. + */ + void setRoles(const QSet<QByteArray>& roles); + QSet<QByteArray> roles() const; + + /** + * Sets the list of enabled thumbnail plugins. + * Per default all plugins enabled in the KConfigGroup "PreviewSettings" + * are used. + * + * Note that this method doesn't cause already generated previews + * to be regenerated. + * + * For a list of available plugins, call KServiceTypeTrader::self()->query("ThumbCreator"). + * + * @see enabledPlugins + */ + void setEnabledPlugins(const QStringList& list); + + /** + * Returns the list of enabled thumbnail plugins. + * @see setEnabledPlugins + */ + QStringList enabledPlugins() const; + +private slots: + void slotItemsInserted(const KItemRangeList& itemRanges); + void slotItemsRemoved(const KItemRangeList& itemRanges); + void slotItemsChanged(const KItemRangeList& itemRanges, + const QSet<QByteArray>& roles); + + void slotGotPreview(const KFileItem& item, const QPixmap& pixmap); + void slotPreviewFailed(const KFileItem& item); + + /** + * Is invoked when the preview job has been finished and + * removes the job from the m_previewJobs list. + */ + void slotPreviewJobFinished(KJob* job); + + void resolvePendingRoles(); + void resolveNextPendingRoles(); + +private: + void startPreviewJob(const KFileItemList& items); + + bool hasPendingRoles() const; + void resetPendingRoles(); + void triggerPendingRolesResolving(int count); + void sortAndResolveAllRoles(); + void sortAndResolvePendingRoles(); + + enum ResolveHint { + ResolveFast, + ResolveAll + }; + bool applyResolvedRoles(const KFileItem& item, ResolveHint hint); + QHash<QByteArray, QVariant> rolesData(const KFileItem& item) const; + + KFileItemList sortedItems(const QSet<KFileItem>& items) const; + + static KFileItemList itemSubSet(const QSet<KFileItem>& items, int count); + static int subDirectoriesCount(const QString& path); + +private: + // Property for setPaused()/isPaused(). + bool m_paused; + + // Property changes during pausing must be remembered to be able + // to react when unpausing again: + bool m_previewChangedDuringPausing; + bool m_iconSizeChangedDuringPausing; + bool m_rolesChangedDuringPausing; + + // Property for setPreviewShown()/previewShown(). + bool m_previewShown; + + // True if the role "iconPixmap" should be cleared when resolving the next + // role with resolveRole(). Is necessary if the preview gets disabled + // during the roles-updater has been paused by setPaused(). + bool m_clearPreviews; + + KFileItemModel* m_model; + QSize m_iconSize; + int m_firstVisibleIndex; + int m_lastVisibleIndex; + QSet<QByteArray> m_roles; + QStringList m_enabledPlugins; + + QSet<KFileItem> m_pendingVisibleItems; + QSet<KFileItem> m_pendingInvisibleItems; + QList<KJob*> m_previewJobs; + + QTimer* m_resolvePendingRolesTimer; +}; + +#endif + + diff --git a/src/kitemviews/kitemlistcontainer.cpp b/src/kitemviews/kitemlistcontainer.cpp new file mode 100644 index 000000000..09fb505d6 --- /dev/null +++ b/src/kitemviews/kitemlistcontainer.cpp @@ -0,0 +1,191 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemlistcontainer.h" + +#include "kitemlistcontroller.h" +#include "kitemlistview.h" +#include "kitemmodelbase.h" + +#include <QGraphicsScene> +#include <QGraphicsView> +#include <QScrollBar> +#include <QStyle> + +#include <KDebug> + +class KItemListContainerViewport : public QGraphicsView +{ +public: + KItemListContainerViewport(QGraphicsScene* scene, QWidget* parent) + : QGraphicsView(scene, parent) + { + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setViewportMargins(0, 0, 0, 0); + setFrameShape(QFrame::NoFrame); + } + + void scrollContentsBy(int dx, int dy) + { + Q_UNUSED(dx); + Q_UNUSED(dy); + // Do nothing. This prevents that e.g. the wheel-event + // results in a moving of the scene items. + } +}; + +KItemListContainer::KItemListContainer(KItemListController* controller, QWidget* parent) : + QAbstractScrollArea(parent), + m_controller(controller) +{ + Q_ASSERT(controller); + controller->setParent(this); + initialize(); +} + +KItemListContainer::KItemListContainer(QWidget* parent) : + QAbstractScrollArea(parent), + m_controller(0) +{ + initialize(); +} + +KItemListContainer::~KItemListContainer() +{ +} + +KItemListController* KItemListContainer::controller() const +{ + return m_controller; +} + +void KItemListContainer::showEvent(QShowEvent* event) +{ + QAbstractScrollArea::showEvent(event); + updateGeometries(); +} + +void KItemListContainer::resizeEvent(QResizeEvent* event) +{ + QAbstractScrollArea::resizeEvent(event); + updateGeometries(); +} + +void KItemListContainer::scrollContentsBy(int dx, int dy) +{ + KItemListView* view = m_controller->view(); + if (!view) { + return; + } + + const qreal currentOffset = view->offset(); + const qreal offsetDiff = (view->scrollOrientation() == Qt::Vertical) ? dy : dx; + view->setOffset(currentOffset - offsetDiff); +} + +void KItemListContainer::slotModelChanged(KItemModelBase* current, KItemModelBase* previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +void KItemListContainer::slotViewChanged(KItemListView* current, KItemListView* previous) +{ + QGraphicsScene* scene = static_cast<QGraphicsView*>(viewport())->scene(); + if (previous) { + scene->removeItem(previous); + disconnect(previous, SIGNAL(offsetChanged(int,int)), this, SLOT(updateScrollBars())); + disconnect(previous, SIGNAL(maximumOffsetChanged(int,int)), this, SLOT(updateScrollBars())); + } + if (current) { + scene->addItem(current); + connect(previous, SIGNAL(offsetChanged(int,int)), this, SLOT(updateScrollBars())); + connect(current, SIGNAL(maximumOffsetChanged(int,int)), this, SLOT(updateScrollBars())); + } +} + +void KItemListContainer::updateScrollBars() +{ + const QSizeF size = m_controller->view()->size(); + + if (m_controller->view()->scrollOrientation() == Qt::Vertical) { + QScrollBar* scrollBar = verticalScrollBar(); + const int value = m_controller->view()->offset(); + const int maximum = qMax(0, int(m_controller->view()->maximumOffset() - size.height())); + scrollBar->setPageStep(size.height()); + scrollBar->setMinimum(0); + scrollBar->setMaximum(maximum); + scrollBar->setValue(value); + horizontalScrollBar()->setMaximum(0); + } else { + QScrollBar* scrollBar = horizontalScrollBar(); + const int value = m_controller->view()->offset(); + const int maximum = qMax(0, int(m_controller->view()->maximumOffset() - size.width())); + scrollBar->setPageStep(size.width()); + scrollBar->setMinimum(0); + scrollBar->setMaximum(maximum); + scrollBar->setValue(value); + verticalScrollBar()->setMaximum(0); + } +} + +void KItemListContainer::updateGeometries() +{ + QRect rect = geometry(); + + int widthDec = frameWidth() * 2; + if (verticalScrollBar()->isVisible()) { + widthDec += style()->pixelMetric(QStyle::PM_ScrollBarExtent); + } + + int heightDec = frameWidth() * 2; + if (horizontalScrollBar()->isVisible()) { + heightDec += style()->pixelMetric(QStyle::PM_ScrollBarExtent); + } + + rect.adjust(0, 0, -widthDec, -heightDec); + + m_controller->view()->setGeometry(QRect(0, 0, rect.width(), rect.height())); + + static_cast<KItemListContainerViewport*>(viewport())->scene()->setSceneRect(0, 0, rect.width(), rect.height()); + static_cast<KItemListContainerViewport*>(viewport())->viewport()->setGeometry(QRect(0, 0, rect.width(), rect.height())); + + updateScrollBars(); +} + +void KItemListContainer::initialize() +{ + if (!m_controller) { + m_controller = new KItemListController(this); + } + + connect(m_controller, SIGNAL(modelChanged(KItemModelBase*,KItemModelBase*)), + this, SLOT(slotModelChanged(KItemModelBase*,KItemModelBase*))); + connect(m_controller, SIGNAL(viewChanged(KItemListView*,KItemListView*)), + this, SLOT(slotViewChanged(KItemListView*,KItemListView*))); + + QGraphicsView* graphicsView = new KItemListContainerViewport(new QGraphicsScene(this), this); + setViewport(graphicsView); +} + +#include "kitemlistcontainer.moc" diff --git a/src/kitemviews/kitemlistcontainer.h b/src/kitemviews/kitemlistcontainer.h new file mode 100644 index 000000000..83044c4f8 --- /dev/null +++ b/src/kitemviews/kitemlistcontainer.h @@ -0,0 +1,70 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMLISTCONTAINER_H +#define KITEMLISTCONTAINER_H + +#include <libdolphin_export.h> + +#include <QAbstractScrollArea> + +class KItemListController; +class KItemListView; +class KItemModelBase; + +/** + * @brief Provides a QWidget based scrolling view for a KItemListController. + * + * @see KItemListController + */ +class LIBDOLPHINPRIVATE_EXPORT KItemListContainer : public QAbstractScrollArea +{ + Q_OBJECT + +public: + explicit KItemListContainer(KItemListController* controller, QWidget* parent = 0); + KItemListContainer(QWidget* parent = 0); + virtual ~KItemListContainer(); + + KItemListController* controller() const; + +protected: + virtual void showEvent(QShowEvent* event); + virtual void resizeEvent(QResizeEvent* event); + virtual void scrollContentsBy(int dx, int dy); + +private slots: + void slotModelChanged(KItemModelBase* current, KItemModelBase* previous); + void slotViewChanged(KItemListView* current, KItemListView* previous); + void updateScrollBars(); + +private: + void initialize(); + void updateGeometries(); + +private: + KItemListController* m_controller; +}; + +#endif + + diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp new file mode 100644 index 000000000..b709e5606 --- /dev/null +++ b/src/kitemviews/kitemlistcontroller.cpp @@ -0,0 +1,281 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemlistcontroller.h" + +#include "kitemlistview.h" +#include "kitemlistselectionmanager.h" + +#include <QEvent> +#include <QGraphicsSceneEvent> +#include <QTransform> + +#include <KDebug> + +KItemListController::KItemListController(QObject* parent) : + QObject(parent), + m_selectionBehavior(NoSelection), + m_model(0), + m_view(0), + m_selectionManager(new KItemListSelectionManager(this)) +{ +} + +KItemListController::~KItemListController() +{ +} + +void KItemListController::setModel(KItemModelBase* model) +{ + if (m_model == model) { + return; + } + + KItemModelBase* oldModel = m_model; + m_model = model; + + if (m_view) { + m_view->setModel(m_model); + } + + emit modelChanged(m_model, oldModel); +} + +KItemModelBase* KItemListController::model() const +{ + return m_model; +} + +KItemListSelectionManager* KItemListController::selectionManager() const +{ + return m_selectionManager; +} + +void KItemListController::setView(KItemListView* view) +{ + if (m_view == view) { + return; + } + + KItemListView* oldView = m_view; + m_view = view; + + if (m_view) { + m_view->setController(this); + m_view->setModel(m_model); + } + + emit viewChanged(m_view, oldView); +} + +KItemListView* KItemListController::view() const +{ + return m_view; +} + +void KItemListController::setSelectionBehavior(SelectionBehavior behavior) +{ + m_selectionBehavior = behavior; +} + +KItemListController::SelectionBehavior KItemListController::selectionBehavior() const +{ + return m_selectionBehavior; +} + +bool KItemListController::showEvent(QShowEvent* event) +{ + Q_UNUSED(event); + return false; +} + +bool KItemListController::hideEvent(QHideEvent* event) +{ + Q_UNUSED(event); + return false; +} + +bool KItemListController::keyPressEvent(QKeyEvent* event) +{ + Q_UNUSED(event); + return false; +} + +bool KItemListController::inputMethodEvent(QInputMethodEvent* event) +{ + Q_UNUSED(event); + return false; +} + +bool KItemListController::mousePressEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::mouseMoveEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::mouseReleaseEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) +{ + if (m_view) { + const QPointF pos = transform.map(event->pos()); + const int index = m_view->itemAt(pos); + if (index >= 0) { + bool emitItemClicked = true; + if (event->button() & Qt::LeftButton) { + if (m_view->isAboveExpansionToggle(index, pos)) { + emit itemExpansionToggleClicked(index); + emitItemClicked = false; + } + } + + if (emitItemClicked) { + emit itemClicked(index, event->button()); + } + } + } + + return false; +} + +bool KItemListController::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::dragEnterEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::dragLeaveEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::dragMoveEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::dropEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::hoverEnterEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::hoverMoveEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::hoverLeaveEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::wheelEvent(QGraphicsSceneWheelEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::resizeEvent(QGraphicsSceneResizeEvent* event, const QTransform& transform) +{ + Q_UNUSED(event); + Q_UNUSED(transform); + return false; +} + +bool KItemListController::processEvent(QEvent* event, const QTransform& transform) +{ + if (!event) { + return false; + } + + switch (event->type()) { +// case QEvent::FocusIn: +// case QEvent::FocusOut: +// return focusEvent(static_cast<QFocusEvent*>(event)); + case QEvent::KeyPress: + return keyPressEvent(static_cast<QKeyEvent*>(event)); + case QEvent::InputMethod: + return inputMethodEvent(static_cast<QInputMethodEvent*>(event)); + case QEvent::GraphicsSceneMousePress: + return mousePressEvent(static_cast<QGraphicsSceneMouseEvent*>(event), QTransform()); + case QEvent::GraphicsSceneMouseMove: + return mouseMoveEvent(static_cast<QGraphicsSceneMouseEvent*>(event), QTransform()); + case QEvent::GraphicsSceneMouseRelease: + return mouseReleaseEvent(static_cast<QGraphicsSceneMouseEvent*>(event), QTransform()); + case QEvent::GraphicsSceneWheel: + return wheelEvent(static_cast<QGraphicsSceneWheelEvent*>(event), QTransform()); + case QEvent::GraphicsSceneDragEnter: + return dragEnterEvent(static_cast<QGraphicsSceneDragDropEvent*>(event), QTransform()); + case QEvent::GraphicsSceneDragLeave: + return dragLeaveEvent(static_cast<QGraphicsSceneDragDropEvent*>(event), QTransform()); + case QEvent::GraphicsSceneDragMove: + return dragMoveEvent(static_cast<QGraphicsSceneDragDropEvent*>(event), QTransform()); + case QEvent::GraphicsSceneDrop: + return dropEvent(static_cast<QGraphicsSceneDragDropEvent*>(event), QTransform()); + case QEvent::GraphicsSceneHoverEnter: + return hoverEnterEvent(static_cast<QGraphicsSceneHoverEvent*>(event), QTransform()); + case QEvent::GraphicsSceneHoverMove: + return hoverMoveEvent(static_cast<QGraphicsSceneHoverEvent*>(event), QTransform()); + case QEvent::GraphicsSceneHoverLeave: + return hoverLeaveEvent(static_cast<QGraphicsSceneHoverEvent*>(event), QTransform()); + case QEvent::GraphicsSceneResize: + return resizeEvent(static_cast<QGraphicsSceneResizeEvent*>(event), transform); + default: + break; + } + + return false; +} + +#include "kitemlistcontroller.moc" diff --git a/src/kitemviews/kitemlistcontroller.h b/src/kitemviews/kitemlistcontroller.h new file mode 100644 index 000000000..4407e3445 --- /dev/null +++ b/src/kitemviews/kitemlistcontroller.h @@ -0,0 +1,118 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMLISTCONTROLLER_H +#define KITEMLISTCONTROLLER_H + +#include <libdolphin_export.h> + +#include <QObject> + +class KItemModelBase; +class KItemListSelectionManager; +class KItemListView; +class QGraphicsSceneHoverEvent; +class QGraphicsSceneDragDropEvent; +class QGraphicsSceneMouseEvent; +class QGraphicsSceneResizeEvent; +class QGraphicsSceneWheelEvent; +class QHideEvent; +class QInputMethodEvent; +class QKeyEvent; +class QShowEvent; +class QTransform; + +/** + * @brief Controls the view, model and selection of an item-list. + * + * For a working item-list it is mandatory to set a compatible view and model + * with KItemListController::setView() and KItemListController::setModel(). + * + * @see KItemListView + * @see KItemModelBase + * @see KItemListSelectionManager + */ +class LIBDOLPHINPRIVATE_EXPORT KItemListController : public QObject +{ + Q_OBJECT + Q_ENUMS(SelectionBehavior) + Q_PROPERTY(KItemModelBase* model READ model WRITE setModel) + Q_PROPERTY(KItemListView *view READ view WRITE setView) + Q_PROPERTY(SelectionBehavior selectionBehavior READ selectionBehavior WRITE setSelectionBehavior) + +public: + enum SelectionBehavior { + NoSelection, + SingleSelection, + MultiSelection + }; + + KItemListController(QObject* parent = 0); + virtual ~KItemListController(); + + void setModel(KItemModelBase* model); + KItemModelBase* model() const; + + void setView(KItemListView* view); + KItemListView* view() const; + + KItemListSelectionManager* selectionManager() const; + + void setSelectionBehavior(SelectionBehavior behavior); + SelectionBehavior selectionBehavior() const; + + virtual bool showEvent(QShowEvent* event); + virtual bool hideEvent(QHideEvent* event); + virtual bool keyPressEvent(QKeyEvent* event); + virtual bool inputMethodEvent(QInputMethodEvent* event); + virtual bool mousePressEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform); + virtual bool mouseMoveEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform); + virtual bool mouseReleaseEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform); + virtual bool mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event, const QTransform& transform); + virtual bool dragEnterEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform); + virtual bool dragLeaveEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform); + virtual bool dragMoveEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform); + virtual bool dropEvent(QGraphicsSceneDragDropEvent* event, const QTransform& transform); + virtual bool hoverEnterEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform); + virtual bool hoverMoveEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform); + virtual bool hoverLeaveEvent(QGraphicsSceneHoverEvent* event, const QTransform& transform); + virtual bool wheelEvent(QGraphicsSceneWheelEvent* event, const QTransform& transform); + virtual bool resizeEvent(QGraphicsSceneResizeEvent* event, const QTransform& transform); + virtual bool processEvent(QEvent* event, const QTransform& transform); + +signals: + void itemClicked(int index, Qt::MouseButton button); + void itemExpansionToggleClicked(int index); + + void modelChanged(KItemModelBase* current, KItemModelBase* previous); + void viewChanged(KItemListView* current, KItemListView* previous); + +private: + SelectionBehavior m_selectionBehavior; + KItemModelBase* m_model; + KItemListView* m_view; + KItemListSelectionManager* m_selectionManager; +}; + +#endif + + diff --git a/src/kitemviews/kitemlistgroupheader.cpp b/src/kitemviews/kitemlistgroupheader.cpp new file mode 100644 index 000000000..4db253293 --- /dev/null +++ b/src/kitemviews/kitemlistgroupheader.cpp @@ -0,0 +1,58 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemlistgroupheader.h" + +#include "kitemlistview.h" + +#include <QPainter> + +#include <KDebug> + +KItemListGroupHeader::KItemListGroupHeader(QGraphicsWidget* parent) : + QGraphicsWidget(parent, 0) +{ +} + +KItemListGroupHeader::~KItemListGroupHeader() +{ +} + +QSizeF KItemListGroupHeader::sizeHint(Qt::SizeHint which, const QSizeF& constraint) const +{ + Q_UNUSED(which); + Q_UNUSED(constraint); + return QSizeF(); +} + +void KItemListGroupHeader::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + painter->setPen(Qt::darkGreen); + painter->setBrush(QColor(0, 255, 0, 50)); + painter->drawRect(rect()); + + //painter->drawText(rect(), QString::number(m_index)); +} + +#include "kitemlistgroupheader.moc" diff --git a/src/kitemviews/kitemlistgroupheader.h b/src/kitemviews/kitemlistgroupheader.h new file mode 100644 index 000000000..135fd5e5f --- /dev/null +++ b/src/kitemviews/kitemlistgroupheader.h @@ -0,0 +1,45 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMLISTGROUPHEADER_H +#define KITEMLISTGROUPHEADER_H + +#include <libdolphin_export.h> + +#include <QGraphicsWidget> + +class KItemListView; + +class LIBDOLPHINPRIVATE_EXPORT KItemListGroupHeader : public QGraphicsWidget +{ + Q_OBJECT + +public: + KItemListGroupHeader(QGraphicsWidget* parent = 0); + virtual ~KItemListGroupHeader(); + + void setIndex(int index); + int index() const; + + virtual QSizeF sizeHint(Qt::SizeHint which = Qt::PreferredSize, const QSizeF& constraint = QSizeF()) const; + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = 0); +}; +#endif + + diff --git a/src/kitemviews/kitemlistselectionmanager.cpp b/src/kitemviews/kitemlistselectionmanager.cpp new file mode 100644 index 000000000..6fe9ed818 --- /dev/null +++ b/src/kitemviews/kitemlistselectionmanager.cpp @@ -0,0 +1,87 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemlistselectionmanager.h" + +#include "kitemmodelbase.h" + +KItemListSelectionManager::KItemListSelectionManager(QObject* parent) : + QObject(parent), + m_currentItem(-1), + m_anchorItem(-1), + m_model(0) +{ +} + +KItemListSelectionManager::~KItemListSelectionManager() +{ +} + +void KItemListSelectionManager::setCurrentItem(int current) +{ + const int previous = m_currentItem; + if (m_model && current < m_model->count()) { + m_currentItem = current; + } else { + m_currentItem = -1; + } + + if (m_currentItem != previous) { + emit currentChanged(m_currentItem, previous); + } +} + +int KItemListSelectionManager::currentItem() const +{ + return m_currentItem; +} + +void KItemListSelectionManager::setAnchorItem(int anchor) +{ + const int previous = m_anchorItem; + if (m_model && anchor < m_model->count()) { + m_anchorItem = anchor; + } else { + m_anchorItem = -1; + } + + if (m_anchorItem != previous) { + emit anchorChanged(m_anchorItem, previous); + } +} + +int KItemListSelectionManager::anchorItem() const +{ + return m_anchorItem; +} + +KItemModelBase* KItemListSelectionManager::model() const +{ + return m_model; +} + +void KItemListSelectionManager::setModel(KItemModelBase* model) +{ + m_model = model; +} + +#include "kitemlistselectionmanager.moc" diff --git a/src/kitemviews/kitemlistselectionmanager.h b/src/kitemviews/kitemlistselectionmanager.h new file mode 100644 index 000000000..5c8e84614 --- /dev/null +++ b/src/kitemviews/kitemlistselectionmanager.h @@ -0,0 +1,69 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMLISTSELECTIONMANAGER_H +#define KITEMLISTSELECTIONMANAGER_H + +#include <libdolphin_export.h> + +#include <QObject> + +class KItemModelBase; + +class LIBDOLPHINPRIVATE_EXPORT KItemListSelectionManager : public QObject +{ + Q_OBJECT + +public: + enum SelectionMode { + Select, + Deselect, + Toggle + }; + + KItemListSelectionManager(QObject* parent = 0); + virtual ~KItemListSelectionManager(); + + void setCurrentItem(int current); + int currentItem() const; + + void setAnchorItem(int anchor); + int anchorItem() const; + + KItemModelBase* model() const; + +signals: + void currentChanged(int current, int previous); + void anchorChanged(int anchor, int previous); + +protected: + void setModel(KItemModelBase* model); + +private: + int m_currentItem; + int m_anchorItem; + KItemModelBase* m_model; + + friend class KItemListController; +}; + +#endif diff --git a/src/kitemviews/kitemlistsizehintresolver.cpp b/src/kitemviews/kitemlistsizehintresolver.cpp new file mode 100644 index 000000000..00eb79bdd --- /dev/null +++ b/src/kitemviews/kitemlistsizehintresolver.cpp @@ -0,0 +1,86 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemlistsizehintresolver_p.h" + +#include <kitemviews/kitemlistview.h> +#include <KDebug> + +KItemListSizeHintResolver::KItemListSizeHintResolver(const KItemListView* itemListView) : + m_itemListView(itemListView), + m_sizeHintCache() +{ +} + +KItemListSizeHintResolver::~KItemListSizeHintResolver() +{ +} + +QSizeF KItemListSizeHintResolver::sizeHint(int index) const +{ + QSizeF size = m_sizeHintCache.at(index); + if (size.isEmpty()) { + size = m_itemListView->itemSizeHint(index); + m_sizeHintCache[index] = size; + } + return size; +} + +void KItemListSizeHintResolver::itemsInserted(int index, int count) +{ + const int currentCount = m_sizeHintCache.count(); + m_sizeHintCache.reserve(currentCount + count); + while (count > 0) { + m_sizeHintCache.insert(index, QSizeF()); + ++index; + --count; + } +} + +void KItemListSizeHintResolver::itemsRemoved(int index, int count) +{ + const QList<QSizeF>::iterator begin = m_sizeHintCache.begin() + index; + const QList<QSizeF>::iterator end = begin + count; + m_sizeHintCache.erase(begin, end); +} + +void KItemListSizeHintResolver::itemsMoved(int from, int to, int count) +{ + Q_UNUSED(from); + Q_UNUSED(to); + Q_UNUSED(count); +} + +void KItemListSizeHintResolver::itemsChanged(int index, int count, const QSet<QByteArray>& roles) +{ + Q_UNUSED(roles); + while (count) { + m_sizeHintCache[index] = QSizeF(); + ++index; + --count; + } +} + +void KItemListSizeHintResolver::clearCache() +{ + const int count = m_sizeHintCache.count(); + for (int i = 0; i < count; ++i) { + m_sizeHintCache[i] = QSizeF(); + } +} diff --git a/src/kitemviews/kitemlistsizehintresolver_p.h b/src/kitemviews/kitemlistsizehintresolver_p.h new file mode 100644 index 000000000..f4f65eccb --- /dev/null +++ b/src/kitemviews/kitemlistsizehintresolver_p.h @@ -0,0 +1,50 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMLISTSIZEHINTRESOLVER_H +#define KITEMLISTSIZEHINTRESOLVER_H + +#include <libdolphin_export.h> + +#include <QByteArray> +#include <QList> +#include <QSizeF> + +class KItemListView; + +class LIBDOLPHINPRIVATE_EXPORT KItemListSizeHintResolver +{ +public: + KItemListSizeHintResolver(const KItemListView* itemListView); + virtual ~KItemListSizeHintResolver(); + QSizeF sizeHint(int index) const; + + void itemsInserted(int index, int count); + void itemsRemoved(int index, int count); + void itemsMoved(int from, int to, int count); + void itemsChanged(int index, int count, const QSet<QByteArray>& roles); + + void clearCache(); + +private: + const KItemListView* m_itemListView; + mutable QList<QSizeF> m_sizeHintCache; +}; + +#endif diff --git a/src/kitemviews/kitemliststyleoption.cpp b/src/kitemviews/kitemliststyleoption.cpp new file mode 100644 index 000000000..261dfc07b --- /dev/null +++ b/src/kitemviews/kitemliststyleoption.cpp @@ -0,0 +1,37 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemliststyleoption.h" + +KItemListStyleOption::KItemListStyleOption() : + QStyleOption(QStyleOption::Version, QStyleOption::SO_CustomBase + 1) +{ +} + +KItemListStyleOption::KItemListStyleOption(const KItemListStyleOption& other) : + QStyleOption(other) +{ + margin = other.margin; + iconSize = other.iconSize; + font = other.font; +} + +KItemListStyleOption::~KItemListStyleOption() +{ +} diff --git a/src/kitemviews/kitemliststyleoption.h b/src/kitemviews/kitemliststyleoption.h new file mode 100644 index 000000000..d181204d7 --- /dev/null +++ b/src/kitemviews/kitemliststyleoption.h @@ -0,0 +1,41 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMLISTSTYLEOPTION_H +#define KITEMLISTSTYLEOPTION_H + +#include <libdolphin_export.h> + +#include <QFont> +#include <QStyleOption> + +class LIBDOLPHINPRIVATE_EXPORT KItemListStyleOption : public QStyleOption +{ +public: + KItemListStyleOption(); + KItemListStyleOption(const KItemListStyleOption& other); + virtual ~KItemListStyleOption(); + + int margin; + int iconSize; + QFont font; +}; +#endif + + diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp new file mode 100644 index 000000000..a0897420c --- /dev/null +++ b/src/kitemviews/kitemlistview.cpp @@ -0,0 +1,1124 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemlistview.h" + +#include "kitemlistcontroller.h" +#include "kitemlistgroupheader.h" +#include "kitemlistselectionmanager.h" +#include "kitemlistsizehintresolver_p.h" +#include "kitemlistviewlayouter_p.h" +#include "kitemlistviewanimation_p.h" +#include "kitemlistwidget.h" + +#include <KDebug> + +#include <QGraphicsSceneMouseEvent> +#include <QPropertyAnimation> +#include <QStyle> +#include <QTimer> + +KItemListView::KItemListView(QGraphicsWidget* parent) : + QGraphicsWidget(parent), + m_grouped(false), + m_activeTransactions(0), + m_itemSize(), + m_controller(0), + m_model(0), + m_visibleRoles(), + m_visibleRolesSizes(), + m_widgetCreator(0), + m_groupHeaderCreator(0), + m_styleOption(), + m_visibleItems(), + m_visibleGroups(), + m_sizeHintResolver(0), + m_layouter(0), + m_animation(0), + m_layoutTimer(0), + m_oldOffset(0), + m_oldMaximumOffset(0) +{ + setAcceptHoverEvents(true); + + m_sizeHintResolver = new KItemListSizeHintResolver(this); + + m_layouter = new KItemListViewLayouter(this); + m_layouter->setSizeHintResolver(m_sizeHintResolver); + + m_animation = new KItemListViewAnimation(this); + connect(m_animation, SIGNAL(finished(QGraphicsWidget*, KItemListViewAnimation::AnimationType)), + this, SLOT(slotAnimationFinished(QGraphicsWidget*, KItemListViewAnimation::AnimationType))); + + m_layoutTimer = new QTimer(this); + m_layoutTimer->setInterval(300); + m_layoutTimer->setSingleShot(true); + connect(m_layoutTimer, SIGNAL(timeout()), this, SLOT(slotLayoutTimerFinished())); +} + +KItemListView::~KItemListView() +{ + delete m_sizeHintResolver; + m_sizeHintResolver = 0; +} + +void KItemListView::setScrollOrientation(Qt::Orientation orientation) +{ + const Qt::Orientation previousOrientation = m_layouter->scrollOrientation(); + if (orientation == previousOrientation) { + return; + } + + m_layouter->setScrollOrientation(orientation); + m_animation->setScrollOrientation(orientation); + m_sizeHintResolver->clearCache(); + updateLayout(); + onScrollOrientationChanged(orientation, previousOrientation); +} + +Qt::Orientation KItemListView::scrollOrientation() const +{ + return m_layouter->scrollOrientation(); +} + +void KItemListView::setItemSize(const QSizeF& itemSize) +{ + const QSizeF previousSize = m_itemSize; + if (itemSize == previousSize) { + return; + } + + m_itemSize = itemSize; + + if (!markVisibleRolesSizesAsDirty()) { + if (itemSize.width() < previousSize.width() || itemSize.height() < previousSize.height()) { + prepareLayoutForIncreasedItemCount(itemSize, ItemSize); + } else { + m_layouter->setItemSize(itemSize); + } + } + + m_sizeHintResolver->clearCache(); + updateLayout(); + onItemSizeChanged(itemSize, previousSize); +} + +QSizeF KItemListView::itemSize() const +{ + return m_itemSize; +} + +void KItemListView::setOffset(qreal offset) +{ + if (offset < 0) { + offset = 0; + } + + const qreal previousOffset = m_layouter->offset(); + if (offset == previousOffset) { + return; + } + + m_layouter->setOffset(offset); + m_animation->setOffset(offset); + if (!m_layoutTimer->isActive()) { + doLayout(NoAnimation, 0, 0); + update(); + } + onOffsetChanged(offset, previousOffset); +} + +qreal KItemListView::offset() const +{ + return m_layouter->offset(); +} + +qreal KItemListView::maximumOffset() const +{ + return m_layouter->maximumOffset(); +} + +void KItemListView::setVisibleRoles(const QHash<QByteArray, int>& roles) +{ + const QHash<QByteArray, int> previousRoles = m_visibleRoles; + m_visibleRoles = roles; + + QHashIterator<int, KItemListWidget*> it(m_visibleItems); + while (it.hasNext()) { + it.next(); + KItemListWidget* widget = it.value(); + widget->setVisibleRoles(roles); + widget->setVisibleRolesSizes(m_visibleRolesSizes); + } + + m_sizeHintResolver->clearCache(); + m_layouter->markAsDirty(); + onVisibleRolesChanged(roles, previousRoles); + + markVisibleRolesSizesAsDirty(); + updateLayout(); +} + +QHash<QByteArray, int> KItemListView::visibleRoles() const +{ + return m_visibleRoles; +} + +KItemListController* KItemListView::controller() const +{ + return m_controller; +} + +KItemModelBase* KItemListView::model() const +{ + return m_model; +} + +void KItemListView::setWidgetCreator(KItemListWidgetCreatorBase* widgetCreator) +{ + m_widgetCreator = widgetCreator; +} + +KItemListWidgetCreatorBase* KItemListView::widgetCreator() const +{ + return m_widgetCreator; +} + +void KItemListView::setGroupHeaderCreator(KItemListGroupHeaderCreatorBase* groupHeaderCreator) +{ + m_groupHeaderCreator = groupHeaderCreator; +} + +KItemListGroupHeaderCreatorBase* KItemListView::groupHeaderCreator() const +{ + return m_groupHeaderCreator; +} + +void KItemListView::setStyleOption(const KItemListStyleOption& option) +{ + const KItemListStyleOption previousOption = m_styleOption; + m_styleOption = option; + + QHashIterator<int, KItemListWidget*> it(m_visibleItems); + while (it.hasNext()) { + it.next(); + it.value()->setStyleOption(option); + } + + m_sizeHintResolver->clearCache(); + updateLayout(); + onStyleOptionChanged(option, previousOption); +} + +const KItemListStyleOption& KItemListView::styleOption() const +{ + return m_styleOption; +} + +void KItemListView::setGeometry(const QRectF& rect) +{ + QGraphicsWidget::setGeometry(rect); + if (!m_model) { + return; + } + + if (m_itemSize.isEmpty()) { + m_layouter->setItemSize(QSizeF()); + } + + if (m_model->count() > 0) { + prepareLayoutForIncreasedItemCount(rect.size(), LayouterSize); + } else { + m_layouter->setSize(rect.size()); + } + + m_layoutTimer->start(); +} + +int KItemListView::itemAt(const QPointF& pos) const +{ + if (!m_model) { + return -1; + } + + QHashIterator<int, KItemListWidget*> it(m_visibleItems); + while (it.hasNext()) { + it.next(); + + const KItemListWidget* widget = it.value(); + const QPointF mappedPos = widget->mapFromItem(this, pos); + if (widget->contains(mappedPos)) { + return it.key(); + } + } + + return -1; +} + +bool KItemListView::isAboveSelectionToggle(int index, const QPointF& pos) const +{ + Q_UNUSED(index); + Q_UNUSED(pos); + return false; +} + +bool KItemListView::isAboveExpansionToggle(int index, const QPointF& pos) const +{ + const KItemListWidget* widget = m_visibleItems.value(index); + if (widget) { + const QRectF expansionToggleRect = widget->expansionToggleRect(); + if (!expansionToggleRect.isEmpty()) { + const QPointF mappedPos = widget->mapFromItem(this, pos); + return expansionToggleRect.contains(mappedPos); + } + } + return false; +} + +int KItemListView::firstVisibleIndex() const +{ + return m_layouter->firstVisibleIndex(); +} + +int KItemListView::lastVisibleIndex() const +{ + return m_layouter->lastVisibleIndex(); +} + +QSizeF KItemListView::itemSizeHint(int index) const +{ + Q_UNUSED(index); + return itemSize(); +} + +QHash<QByteArray, QSizeF> KItemListView::visibleRoleSizes() const +{ + return QHash<QByteArray, QSizeF>(); +} + +void KItemListView::beginTransaction() +{ + ++m_activeTransactions; + if (m_activeTransactions == 1) { + onTransactionBegin(); + } +} + +void KItemListView::endTransaction() +{ + --m_activeTransactions; + if (m_activeTransactions < 0) { + m_activeTransactions = 0; + kWarning() << "Mismatch between beginTransaction()/endTransaction()"; + } + + if (m_activeTransactions == 0) { + onTransactionEnd(); + updateLayout(); + } +} + +bool KItemListView::isTransactionActive() const +{ + return m_activeTransactions > 0; +} + +void KItemListView::initializeItemListWidget(KItemListWidget* item) +{ + Q_UNUSED(item); +} + +void KItemListView::onControllerChanged(KItemListController* current, KItemListController* previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +void KItemListView::onModelChanged(KItemModelBase* current, KItemModelBase* previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +void KItemListView::onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +void KItemListView::onItemSizeChanged(const QSizeF& current, const QSizeF& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +void KItemListView::onOffsetChanged(qreal current, qreal previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +void KItemListView::onVisibleRolesChanged(const QHash<QByteArray, int>& current, const QHash<QByteArray, int>& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +void KItemListView::onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +void KItemListView::onTransactionBegin() +{ +} + +void KItemListView::onTransactionEnd() +{ +} + +bool KItemListView::event(QEvent* event) +{ + // Forward all events to the controller and handle them there + if (m_controller && m_controller->processEvent(event, transform())) { + event->accept(); + return true; + } + return QGraphicsWidget::event(event); +} + +void KItemListView::mousePressEvent(QGraphicsSceneMouseEvent* event) +{ + event->accept(); +} + +void KItemListView::hoverMoveEvent(QGraphicsSceneHoverEvent* event) +{ + if (!m_model) { + return; + } + + QHashIterator<int, KItemListWidget*> it(m_visibleItems); + while (it.hasNext()) { + it.next(); + + KItemListWidget* widget = it.value(); + KItemListStyleOption styleOption = widget->styleOption(); + const QPointF mappedPos = widget->mapFromItem(this, event->pos()); + + const bool hovered = widget->contains(mappedPos) && + !widget->expansionToggleRect().contains(mappedPos) && + !widget->selectionToggleRect().contains(mappedPos); + if (hovered) { + if (!(styleOption.state & QStyle::State_MouseOver)) { + styleOption.state |= QStyle::State_MouseOver; + widget->setStyleOption(styleOption); + } + } else if (styleOption.state & QStyle::State_MouseOver) { + styleOption.state &= ~QStyle::State_MouseOver; + widget->setStyleOption(styleOption); + } + } +} + +void KItemListView::hoverLeaveEvent(QGraphicsSceneHoverEvent* event) +{ + Q_UNUSED(event); + + if (!m_model) { + return; + } + + QHashIterator<int, KItemListWidget*> it(m_visibleItems); + while (it.hasNext()) { + it.next(); + + KItemListWidget* widget = it.value(); + KItemListStyleOption styleOption = widget->styleOption(); + if (styleOption.state & QStyle::State_MouseOver) { + styleOption.state &= ~QStyle::State_MouseOver; + widget->setStyleOption(styleOption); + } + } +} + +QList<KItemListWidget*> KItemListView::visibleItemListWidgets() const +{ + return m_visibleItems.values(); +} + +void KItemListView::slotItemsInserted(const KItemRangeList& itemRanges) +{ + markVisibleRolesSizesAsDirty(); + + const bool hasMultipleRanges = (itemRanges.count() > 1); + if (hasMultipleRanges) { + beginTransaction(); + } + + foreach (const KItemRange& range, itemRanges) { + const int index = range.index; + const int count = range.count; + if (index < 0 || count <= 0) { + kWarning() << "Invalid item range (index:" << index << ", count:" << count << ")"; + continue; + } + + m_sizeHintResolver->itemsInserted(index, count); + + // Determine which visible items must be moved + QList<int> itemsToMove; + QHashIterator<int, KItemListWidget*> it(m_visibleItems); + while (it.hasNext()) { + it.next(); + const int visibleItemIndex = it.key(); + if (visibleItemIndex >= index) { + itemsToMove.append(visibleItemIndex); + } + } + + // Update the indexes of all KItemListWidget instances that are located + // after the inserted items. It is important to adjust the indexes in the order + // from the highest index to the lowest index to prevent overlaps when setting the new index. + qSort(itemsToMove); + for (int i = itemsToMove.count() - 1; i >= 0; --i) { + KItemListWidget* widget = m_visibleItems.value(itemsToMove[i]); + Q_ASSERT(widget); + setWidgetIndex(widget, widget->index() + count); + } + + m_layouter->markAsDirty(); + if (m_model->count() == count && maximumOffset() > size().height()) { + kDebug() << "Scrollbar required, skipping layout"; + const int scrollBarExtent = style()->pixelMetric(QStyle::PM_ScrollBarExtent); + QSizeF layouterSize = m_layouter->size(); + if (scrollOrientation() == Qt::Vertical) { + layouterSize.rwidth() -= scrollBarExtent; + } else { + layouterSize.rheight() -= scrollBarExtent; + } + m_layouter->setSize(layouterSize); + } + + if (!hasMultipleRanges) { + doLayout(Animation, index, count); + update(); + } + + if (m_controller) { + KItemListSelectionManager* selectionManager = m_controller->selectionManager(); + const int current = selectionManager->currentItem(); + if (current < 0) { + selectionManager->setCurrentItem(0); + } else if (current >= index) { + selectionManager->setCurrentItem(current + count); + } + } + } + + if (hasMultipleRanges) { + endTransaction(); + } +} + +void KItemListView::slotItemsRemoved(const KItemRangeList& itemRanges) +{ + markVisibleRolesSizesAsDirty(); + + const bool hasMultipleRanges = (itemRanges.count() > 1); + if (hasMultipleRanges) { + beginTransaction(); + } + + for (int i = itemRanges.count() - 1; i >= 0; --i) { + const KItemRange& range = itemRanges.at(i); + const int index = range.index; + const int count = range.count; + if (index < 0 || count <= 0) { + kWarning() << "Invalid item range (index:" << index << ", count:" << count << ")"; + continue; + } + + m_sizeHintResolver->itemsRemoved(index, count); + + const int firstRemovedIndex = index; + const int lastRemovedIndex = index + count - 1; + const int lastIndex = m_model->count() + count - 1; + + // Remove all KItemListWidget instances that got deleted + for (int i = firstRemovedIndex; i <= lastRemovedIndex; ++i) { + KItemListWidget* widget = m_visibleItems.value(i); + if (!widget) { + continue; + } + + m_animation->stop(widget); + // Stopping the animation might lead to recycling the widget if + // it is invisible (see slotAnimationFinished()). + // Check again whether it is still visible: + if (!m_visibleItems.contains(i)) { + continue; + } + + if (m_model->count() == 0) { + // For performance reasons no animation is done when all items have + // been removed. + recycleWidget(widget); + } else { + // Animate the removing of the items. Special case: When removing an item there + // is no valid model index available anymore. For the + // remove-animation the item gets removed from m_visibleItems but the widget + // will stay alive until the animation has been finished and will + // be recycled (deleted) in KItemListView::slotAnimationFinished(). + m_visibleItems.remove(i); + widget->setIndex(-1); + m_animation->start(widget, KItemListViewAnimation::DeleteAnimation); + } + } + + // Update the indexes of all KItemListWidget instances that are located + // after the deleted items + for (int i = lastRemovedIndex + 1; i <= lastIndex; ++i) { + KItemListWidget* widget = m_visibleItems.value(i); + if (widget) { + const int newIndex = i - count; + setWidgetIndex(widget, newIndex); + } + } + + m_layouter->markAsDirty(); + if (!hasMultipleRanges) { + doLayout(Animation, index, -count); + update(); + } + + /*KItemListSelectionManager* selectionManager = m_controller->selectionManager(); + const int current = selectionManager->currentItem(); + if (count() <= 0) { + selectionManager->setCurrentItem(-1); + } else if (current >= index) { + selectionManager->setCurrentItem(current + count); + }*/ + } + + if (hasMultipleRanges) { + endTransaction(); + } +} + +void KItemListView::slotItemsChanged(const KItemRangeList& itemRanges, + const QSet<QByteArray>& roles) +{ + foreach (const KItemRange& itemRange, itemRanges) { + const int index = itemRange.index; + const int count = itemRange.count; + + m_sizeHintResolver->itemsChanged(index, count, roles); + + const int lastIndex = index + count - 1; + for (int i = index; i <= lastIndex; ++i) { + KItemListWidget* widget = m_visibleItems.value(i); + if (widget) { + widget->setData(m_model->data(i), roles); + } + } + } +} + +void KItemListView::slotAnimationFinished(QGraphicsWidget* widget, + KItemListViewAnimation::AnimationType type) +{ + KItemListWidget* itemListWidget = qobject_cast<KItemListWidget*>(widget); + Q_ASSERT(itemListWidget); + + switch (type) { + case KItemListViewAnimation::DeleteAnimation: { + // As we recycle the widget in this case it is important to assure that no + // other animation has been started. This is a convention in KItemListView and + // not a requirement defined by KItemListViewAnimation. + Q_ASSERT(!m_animation->isStarted(itemListWidget)); + + // All KItemListWidgets that are animated by the DeleteAnimation are not maintained + // by m_visibleWidgets and must be deleted manually after the animation has + // been finished. + KItemListGroupHeader* header = m_visibleGroups.value(itemListWidget); + if (header) { + m_groupHeaderCreator->recycle(header); + m_visibleGroups.remove(itemListWidget); + } + m_widgetCreator->recycle(itemListWidget); + break; + } + + case KItemListViewAnimation::CreateAnimation: + case KItemListViewAnimation::MovingAnimation: + case KItemListViewAnimation::ResizeAnimation: { + const int index = itemListWidget->index(); + const bool invisible = (index < m_layouter->firstVisibleIndex()) || + (index > m_layouter->lastVisibleIndex()); + if (invisible && !m_animation->isStarted(itemListWidget)) { + recycleWidget(itemListWidget); + } + break; + } + + default: break; + } +} + +void KItemListView::slotLayoutTimerFinished() +{ + m_layouter->setSize(geometry().size()); + doLayout(Animation, 0, 0); +} + +void KItemListView::setController(KItemListController* controller) +{ + if (m_controller != controller) { + KItemListController* previous = m_controller; + m_controller = controller; + onControllerChanged(controller, previous); + } +} + +void KItemListView::setModel(KItemModelBase* model) +{ + if (m_model == model) { + return; + } + + KItemModelBase* previous = m_model; + + if (m_model) { + disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + disconnect(m_model, SIGNAL(itemsInserted(KItemRangeList)), + this, SLOT(slotItemsInserted(KItemRangeList))); + disconnect(m_model, SIGNAL(itemsRemoved(KItemRangeList)), + this, SLOT(slotItemsRemoved(KItemRangeList))); + } + + m_model = model; + m_layouter->setModel(model); + m_grouped = !model->groupRole().isEmpty(); + + if (m_model) { + connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet<QByteArray>)), + this, SLOT(slotItemsChanged(KItemRangeList,QSet<QByteArray>))); + connect(m_model, SIGNAL(itemsInserted(KItemRangeList)), + this, SLOT(slotItemsInserted(KItemRangeList))); + connect(m_model, SIGNAL(itemsRemoved(KItemRangeList)), + this, SLOT(slotItemsRemoved(KItemRangeList))); + } + + onModelChanged(model, previous); +} + +void KItemListView::updateLayout() +{ + doLayout(Animation, 0, 0); + update(); +} + +void KItemListView::doLayout(LayoutAnimationHint hint, int changedIndex, int changedCount) +{ + if (m_layoutTimer->isActive()) { + kDebug() << "Stopping layout timer, synchronous layout requested"; + m_layoutTimer->stop(); + } + + if (m_model->count() < 0 || m_activeTransactions > 0) { + return; + } + + applyDynamicItemSize(); + + const int firstVisibleIndex = m_layouter->firstVisibleIndex(); + const int lastVisibleIndex = m_layouter->lastVisibleIndex(); + if (firstVisibleIndex < 0) { + emitOffsetChanges(); + return; + } + + // Do a sanity check of the offset-property: When properties of the itemlist-view have been changed + // it might be possible that the maximum offset got changed too. Assure that the full visible range + // is still shown if the maximum offset got decreased. + const qreal visibleOffsetRange = (scrollOrientation() == Qt::Horizontal) ? size().width() : size().height(); + const qreal maxOffsetToShowFullRange = maximumOffset() - visibleOffsetRange; + if (offset() > maxOffsetToShowFullRange) { + m_layouter->setOffset(qMax(qreal(0), maxOffsetToShowFullRange)); + } + + // Determine all items that are completely invisible and might be + // reused for items that just got (at least partly) visible. + // Items that do e.g. an animated moving of their position are not + // marked as invisible: This assures that a scrolling inside the view + // can be done without breaking an animation. + QList<int> reusableItems; + QHashIterator<int, KItemListWidget*> it(m_visibleItems); + while (it.hasNext()) { + it.next(); + KItemListWidget* widget = it.value(); + const int index = widget->index(); + const bool invisible = (index < firstVisibleIndex) || (index > lastVisibleIndex); + if (invisible && !m_animation->isStarted(widget)) { + widget->setVisible(false); + reusableItems.append(index); + } + } + + // Assure that for each visible item a KItemListWidget is available. KItemListWidget + // instances from invisible items are reused. If no reusable items are + // found then new KItemListWidget instances get created. + const bool animate = (hint == Animation); + for (int i = firstVisibleIndex; i <= lastVisibleIndex; ++i) { + bool applyNewPos = true; + bool wasHidden = false; + + const QRectF itemBounds = m_layouter->itemBoundingRect(i); + const QPointF newPos = itemBounds.topLeft(); + KItemListWidget* widget = m_visibleItems.value(i); + if (!widget) { + wasHidden = true; + if (!reusableItems.isEmpty()) { + // Reuse a KItemListWidget instance from an invisible item + const int oldIndex = reusableItems.takeLast(); + widget = m_visibleItems.value(oldIndex); + setWidgetIndex(widget, i); + } else { + // No reusable KItemListWidget instance is available, create a new one + widget = createWidget(i); + } + widget->resize(itemBounds.size()); + + if (animate && changedCount < 0) { + // Items have been deleted, move the created item to the + // imaginary old position. + const QRectF itemBoundingRect = m_layouter->itemBoundingRect(i - changedCount); + if (itemBoundingRect.isEmpty()) { + const QPointF invisibleOldPos = (scrollOrientation() == Qt::Vertical) + ? QPointF(0, size().height()) : QPointF(size().width(), 0); + widget->setPos(invisibleOldPos); + } else { + widget->setPos(itemBoundingRect.topLeft()); + } + applyNewPos = false; + } + } else if (m_animation->isStarted(widget, KItemListViewAnimation::MovingAnimation)) { + applyNewPos = false; + } + + if (animate) { + const bool itemsRemoved = (changedCount < 0); + const bool itemsInserted = (changedCount > 0); + + if (itemsRemoved && (i >= changedIndex + changedCount + 1)) { + // The item is located after the removed items. Animate the moving of the position. + m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos); + applyNewPos = false; + } else if (itemsInserted && i >= changedIndex) { + // The item is located after the first inserted item + if (i <= changedIndex + changedCount - 1) { + // The item is an inserted item. Animate the appearing of the item. + // For performance reasons no animation is done when changedCount is equal + // to all available items. + if (changedCount < m_model->count()) { + m_animation->start(widget, KItemListViewAnimation::CreateAnimation); + } + } else if (!m_animation->isStarted(widget, KItemListViewAnimation::CreateAnimation)) { + // The item was already there before, so animate the moving of the position. + // No moving animation is done if the item is animated by a create animation: This + // prevents a "move animation mess" when inserting several ranges in parallel. + m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos); + applyNewPos = false; + } + } else if (!itemsRemoved && !itemsInserted && !wasHidden) { + // The size of the view might have been changed. Animate the moving of the position. + m_animation->start(widget, KItemListViewAnimation::MovingAnimation, newPos); + applyNewPos = false; + } + } + + if (applyNewPos) { + widget->setPos(newPos); + } + + Q_ASSERT(widget->index() == i); + widget->setVisible(true); + + if (widget->size() != itemBounds.size()) { + m_animation->start(widget, KItemListViewAnimation::ResizeAnimation, itemBounds.size()); + } + } + + // Delete invisible KItemListWidget instances that have not been reused + foreach (int index, reusableItems) { + recycleWidget(m_visibleItems.value(index)); + } + + emitOffsetChanges(); +} + +void KItemListView::emitOffsetChanges() +{ + const int newOffset = m_layouter->offset(); + if (m_oldOffset != newOffset) { + emit offsetChanged(newOffset, m_oldOffset); + m_oldOffset = newOffset; + } + + const int newMaximumOffset = m_layouter->maximumOffset(); + if (m_oldMaximumOffset != newMaximumOffset) { + emit maximumOffsetChanged(newMaximumOffset, m_oldMaximumOffset); + m_oldMaximumOffset = newMaximumOffset; + } +} + +KItemListWidget* KItemListView::createWidget(int index) +{ + KItemListWidget* widget = m_widgetCreator->create(this); + widget->setVisibleRoles(m_visibleRoles); + widget->setVisibleRolesSizes(m_visibleRolesSizes); + widget->setStyleOption(m_styleOption); + widget->setIndex(index); + widget->setData(m_model->data(index)); + m_visibleItems.insert(index, widget); + + if (m_grouped) { + if (m_layouter->isFirstGroupItem(index)) { + KItemListGroupHeader* header = m_groupHeaderCreator->create(widget); + header->setPos(0, -50); + header->resize(50, 50); + m_visibleGroups.insert(widget, header); + } + } + + initializeItemListWidget(widget); + return widget; +} + +void KItemListView::recycleWidget(KItemListWidget* widget) +{ + if (m_grouped) { + KItemListGroupHeader* header = m_visibleGroups.value(widget); + if (header) { + m_groupHeaderCreator->recycle(header); + m_visibleGroups.remove(widget); + } + } + + m_visibleItems.remove(widget->index()); + m_widgetCreator->recycle(widget); +} + +void KItemListView::setWidgetIndex(KItemListWidget* widget, int index) +{ + if (m_grouped) { + bool createHeader = m_layouter->isFirstGroupItem(index); + KItemListGroupHeader* header = m_visibleGroups.value(widget); + if (header) { + if (createHeader) { + createHeader = false; + } else { + m_groupHeaderCreator->recycle(header); + m_visibleGroups.remove(widget); + } + } + + if (createHeader) { + KItemListGroupHeader* header = m_groupHeaderCreator->create(widget); + header->setPos(0, -50); + header->resize(50, 50); + m_visibleGroups.insert(widget, header); + } + } + + const int oldIndex = widget->index(); + m_visibleItems.remove(oldIndex); + widget->setVisibleRoles(m_visibleRoles); + widget->setVisibleRolesSizes(m_visibleRolesSizes); + widget->setStyleOption(m_styleOption); + widget->setIndex(index); + widget->setData(m_model->data(index)); + m_visibleItems.insert(index, widget); + + initializeItemListWidget(widget); +} + +void KItemListView::prepareLayoutForIncreasedItemCount(const QSizeF& size, SizeType sizeType) +{ + // Calculate the first visible index and last visible index for the current size + const int currentFirst = m_layouter->firstVisibleIndex(); + const int currentLast = m_layouter->lastVisibleIndex(); + + const QSizeF currentSize = (sizeType == LayouterSize) ? m_layouter->size() : m_layouter->itemSize(); + + // Calculate the first visible index and last visible index for the new size + setLayouterSize(size, sizeType); + const int newFirst = m_layouter->firstVisibleIndex(); + const int newLast = m_layouter->lastVisibleIndex(); + + if ((currentFirst != newFirst) || (currentLast != newLast)) { + // At least one index has been changed. Assure that widgets for all possible + // visible items get created so that a move-animation can be started later. + const int maxVisibleItems = m_layouter->maximumVisibleItems(); + int minFirst = qMin(newFirst, currentFirst); + const int maxLast = qMax(newLast, currentLast); + + if (maxLast - minFirst + 1 < maxVisibleItems) { + // Increasing the size might result in a smaller KItemListView::offset(). + // Decrease the first visible index in a way that at least the maximum + // visible items are shown. + minFirst = qMax(0, maxLast - maxVisibleItems + 1); + } + + if (maxLast - minFirst > maxVisibleItems + maxVisibleItems / 2) { + // The creating of widgets is quite expensive. Assure that never more + // than 50 % of the maximum visible items get created for the animations. + return; + } + + setLayouterSize(currentSize, sizeType); + for (int i = minFirst; i <= maxLast; ++i) { + if (!m_visibleItems.contains(i)) { + KItemListWidget* widget = createWidget(i); + const QPointF pos = m_layouter->itemBoundingRect(i).topLeft(); + widget->setPos(pos); + } + } + setLayouterSize(size, sizeType); + } +} + +void KItemListView::setLayouterSize(const QSizeF& size, SizeType sizeType) +{ + switch (sizeType) { + case LayouterSize: m_layouter->setSize(size); break; + case ItemSize: m_layouter->setItemSize(size); break; + default: break; + } +} + +bool KItemListView::markVisibleRolesSizesAsDirty() +{ + const bool dirty = m_itemSize.isEmpty(); + if (dirty) { + m_visibleRolesSizes.clear(); + m_layouter->setItemSize(QSizeF()); + } + return dirty; +} + +void KItemListView::applyDynamicItemSize() +{ + if (!m_itemSize.isEmpty()) { + return; + } + + if (m_visibleRolesSizes.isEmpty()) { + m_visibleRolesSizes = visibleRoleSizes(); + foreach (KItemListWidget* widget, visibleItemListWidgets()) { + widget->setVisibleRolesSizes(m_visibleRolesSizes); + } + } + + if (m_layouter->itemSize().isEmpty()) { + qreal requiredWidth = 0; + qreal requiredHeight = 0; + + QHashIterator<QByteArray, QSizeF> it(m_visibleRolesSizes); + while (it.hasNext()) { + it.next(); + const QSizeF& visibleRoleSize = it.value(); + requiredWidth += visibleRoleSize.width(); + requiredHeight += visibleRoleSize.height(); + } + + QSizeF dynamicItemSize = m_itemSize; + if (dynamicItemSize.width() <= 0) { + dynamicItemSize.setWidth(qMax(requiredWidth, size().width())); + } + if (dynamicItemSize.height() <= 0) { + dynamicItemSize.setHeight(qMax(requiredHeight, size().height())); + } + + m_layouter->setItemSize(dynamicItemSize); + } +} + +KItemListCreatorBase::~KItemListCreatorBase() +{ + qDeleteAll(m_recycleableWidgets); + qDeleteAll(m_createdWidgets); +} + +void KItemListCreatorBase::addCreatedWidget(QGraphicsWidget* widget) +{ + m_createdWidgets.insert(widget); +} + +void KItemListCreatorBase::pushRecycleableWidget(QGraphicsWidget* widget) +{ + Q_ASSERT(m_createdWidgets.contains(widget)); + m_createdWidgets.remove(widget); + + if (m_recycleableWidgets.count() < 100) { + m_recycleableWidgets.append(widget); + widget->setVisible(false); + } else { + delete widget; + } +} + +QGraphicsWidget* KItemListCreatorBase::popRecycleableWidget() +{ + if (m_recycleableWidgets.isEmpty()) { + return 0; + } + + QGraphicsWidget* widget = m_recycleableWidgets.takeLast(); + m_createdWidgets.insert(widget); + return widget; +} + +KItemListWidgetCreatorBase::~KItemListWidgetCreatorBase() +{ +} + +void KItemListWidgetCreatorBase::recycle(KItemListWidget* widget) +{ + widget->setOpacity(1.0); + pushRecycleableWidget(widget); +} + +KItemListGroupHeaderCreatorBase::~KItemListGroupHeaderCreatorBase() +{ +} + +void KItemListGroupHeaderCreatorBase::recycle(KItemListGroupHeader* header) +{ + header->setOpacity(1.0); + pushRecycleableWidget(header); +} + +#include "kitemlistview.moc" diff --git a/src/kitemviews/kitemlistview.h b/src/kitemviews/kitemlistview.h new file mode 100644 index 000000000..829736a09 --- /dev/null +++ b/src/kitemviews/kitemlistview.h @@ -0,0 +1,360 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMLISTVIEW_H +#define KITEMLISTVIEW_H + +#include <libdolphin_export.h> + +#include <kitemviews/kitemliststyleoption.h> +#include <kitemviews/kitemlistviewanimation_p.h> +#include <kitemviews/kitemlistwidget.h> +#include <kitemviews/kitemmodelbase.h> +#include <QGraphicsWidget> + +class KItemListController; +class KItemListWidgetCreatorBase; +class KItemListGroupHeader; +class KItemListGroupHeaderCreatorBase; +class KItemListSizeHintResolver; +class KItemListViewAnimation; +class KItemListViewLayouter; +class KItemListWidget; +class KItemListViewCreatorBase; +class QTimer; + +/** + * @brief Represents the view of an item-list. + * + * The view is responsible for showing the items of the model within + * a GraphicsItem. Each visible item is represented by a KItemListWidget. + * + * The created view must be applied to the KItemListController with + * KItemListController::setView(). For showing a custom model it is not + * mandatory to derive from KItemListView, all that is necessary is + * to set a widget-creator that is capable to create KItemListWidgets + * showing the model items. A widget-creator can be set with + * KItemListView::setWidgetCreator(). + * + * @see KItemListWidget + * @see KItemModelBase + */ +class LIBDOLPHINPRIVATE_EXPORT KItemListView : public QGraphicsWidget +{ + Q_OBJECT + +public: + KItemListView(QGraphicsWidget* parent = 0); + virtual ~KItemListView(); + + void setScrollOrientation(Qt::Orientation orientation); + Qt::Orientation scrollOrientation() const; + + void setItemSize(const QSizeF& size); + QSizeF itemSize() const; + + // TODO: add note that offset is not checked against maximumOffset, only against 0. + void setOffset(qreal offset); + qreal offset() const; + + qreal maximumOffset() const; + + /** + * Sets the visible roles to \p roles. The integer-value defines + * the order of the visible role: Smaller values are ordered first. + */ + void setVisibleRoles(const QHash<QByteArray, int>& roles); + QHash<QByteArray, int> visibleRoles() const; + + /** + * @return Controller of the item-list. The controller gets + * initialized by KItemListController::setView() and will + * result in calling KItemListController::onControllerChanged(). + */ + KItemListController* controller() const; + + /** + * @return Model of the item-list. The model gets + * initialized by KItemListController::setView() and will + * result in calling KItemListController::onModelChanged(). + */ + KItemModelBase* model() const; + + /** + * Sets the creator that creates a widget showing the + * content of one model-item. Usually it is sufficient + * to implement a custom widget X derived from KItemListWidget and + * set the creator by: + * <code> + * itemListView->setWidgetCreator(new KItemListWidgetCreator<X>()); + * </code> + * Note that the ownership of the widget creator is not transferred to + * the item-list view: One instance of a widget creator might get shared + * by several item-list view instances. + **/ + void setWidgetCreator(KItemListWidgetCreatorBase* widgetCreator); + KItemListWidgetCreatorBase* widgetCreator() const; + + void setGroupHeaderCreator(KItemListGroupHeaderCreatorBase* groupHeaderCreator); + KItemListGroupHeaderCreatorBase* groupHeaderCreator() const; + + void setStyleOption(const KItemListStyleOption& option); + const KItemListStyleOption& styleOption() const; + + virtual void setGeometry(const QRectF& rect); + + int itemAt(const QPointF& pos) const; + bool isAboveSelectionToggle(int index, const QPointF& pos) const; + bool isAboveExpansionToggle(int index, const QPointF& pos) const; + + int firstVisibleIndex() const; + int lastVisibleIndex() const; + + virtual QSizeF itemSizeHint(int index) const; + virtual QHash<QByteArray, QSizeF> visibleRoleSizes() const; + + void beginTransaction(); + void endTransaction(); + bool isTransactionActive() const; + +signals: + void offsetChanged(int current, int previous); + void maximumOffsetChanged(int current, int previous); + +protected: + virtual void initializeItemListWidget(KItemListWidget* item); + + virtual void onControllerChanged(KItemListController* current, KItemListController* previous); + virtual void onModelChanged(KItemModelBase* current, KItemModelBase* previous); + + virtual void onScrollOrientationChanged(Qt::Orientation current, Qt::Orientation previous); + virtual void onItemSizeChanged(const QSizeF& current, const QSizeF& previous); + virtual void onOffsetChanged(qreal current, qreal previous); + virtual void onVisibleRolesChanged(const QHash<QByteArray, int>& current, const QHash<QByteArray, int>& previous); + virtual void onStyleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous); + + virtual void onTransactionBegin(); + virtual void onTransactionEnd(); + + virtual bool event(QEvent* event); + virtual void mousePressEvent(QGraphicsSceneMouseEvent* event); + virtual void hoverMoveEvent(QGraphicsSceneHoverEvent* event); + virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent* event); + + QList<KItemListWidget*> visibleItemListWidgets() const; + +protected slots: + virtual void slotItemsInserted(const KItemRangeList& itemRanges); + virtual void slotItemsRemoved(const KItemRangeList& itemRanges); + virtual void slotItemsChanged(const KItemRangeList& itemRanges, + const QSet<QByteArray>& roles); + +private slots: + void slotAnimationFinished(QGraphicsWidget* widget, + KItemListViewAnimation::AnimationType type); + void slotLayoutTimerFinished(); + +private: + enum LayoutAnimationHint + { + NoAnimation, + Animation + }; + + enum SizeType + { + LayouterSize, + ItemSize + }; + + void setController(KItemListController* controller); + void setModel(KItemModelBase* model); + + void updateLayout(); + void doLayout(LayoutAnimationHint hint, int changedIndex, int changedCount); + void doGroupHeadersLayout(LayoutAnimationHint hint, int changedIndex, int changedCount); + void emitOffsetChanges(); + + KItemListWidget* createWidget(int index); + void recycleWidget(KItemListWidget* widget); + void setWidgetIndex(KItemListWidget* widget, int index); + + /** + * Helper method for setGeometry() and setItemSize(): Calling both methods might result + * in a changed number of visible items. To assure that currently invisible items can + * get animated from the old position to the new position prepareLayoutForIncreasedItemCount() + * takes care to create all item widgets that are visible with the old or the new size. + * @param size Size of the layouter or the item dependent on \p sizeType. + * @param sizeType LayouterSize: KItemListLayouter::setSize() is used. + * ItemSize: KItemListLayouter::setItemSize() is used. + */ + void prepareLayoutForIncreasedItemCount(const QSizeF& size, SizeType sizeType); + + /** + * Helper method for prepareLayoutForIncreasedItemCount(). + */ + void setLayouterSize(const QSizeF& size, SizeType sizeType); + + /** + * Marks the visible roles as dirty so that they will get updated when doing the next + * layout. The visible roles will only get marked as dirty if an empty item-size is + * given. + * @return True if the visible roles have been marked as dirty. + */ + bool markVisibleRolesSizesAsDirty(); + + /** + * Updates the m_visibleRoleSizes property and applies the dynamic + * size to the layouter. + */ + void applyDynamicItemSize(); + +private: + bool m_grouped; + int m_activeTransactions; // Counter for beginTransaction()/endTransaction() + + QSizeF m_itemSize; + KItemListController* m_controller; + KItemModelBase* m_model; + QHash<QByteArray, int> m_visibleRoles; + QHash<QByteArray, QSizeF> m_visibleRolesSizes; + KItemListWidgetCreatorBase* m_widgetCreator; + KItemListGroupHeaderCreatorBase* m_groupHeaderCreator; + KItemListStyleOption m_styleOption; + + QHash<int, KItemListWidget*> m_visibleItems; + QHash<KItemListWidget*, KItemListGroupHeader*> m_visibleGroups; + + int m_scrollBarExtent; + KItemListSizeHintResolver* m_sizeHintResolver; + KItemListViewLayouter* m_layouter; + KItemListViewAnimation* m_animation; + + QTimer* m_layoutTimer; // Triggers an asynchronous doLayout() call. + int m_oldOffset; + int m_oldMaximumOffset; + + friend class KItemListController; +}; + +/** + * Allows to do a fast logical creation and deletion of QGraphicsWidgets + * by recycling existing QGraphicsWidgets instances. Is used by + * KItemListWidgetCreatorBase and KItemListGroupHeaderCreatorBase. + * @internal + */ +class LIBDOLPHINPRIVATE_EXPORT KItemListCreatorBase +{ +public: + virtual ~KItemListCreatorBase(); + +protected: + void addCreatedWidget(QGraphicsWidget* widget); + void pushRecycleableWidget(QGraphicsWidget* widget); + QGraphicsWidget* popRecycleableWidget(); + +private: + QSet<QGraphicsWidget*> m_createdWidgets; + QList<QGraphicsWidget*> m_recycleableWidgets; +}; + +/** + * @brief Base class for creating KItemListWidgets. + * + * It is recommended that applications simply use the KItemListWidgetCreator-template class. + * For a custom implementation the methods create() and recyle() must be reimplemented. + * The intention of the widget creator is to prevent repetitive and expensive instantiations and + * deletions of KItemListWidgets by recycling existing widget instances. + */ +class LIBDOLPHINPRIVATE_EXPORT KItemListWidgetCreatorBase : public KItemListCreatorBase +{ +public: + virtual ~KItemListWidgetCreatorBase(); + virtual KItemListWidget* create(KItemListView* view) = 0; + virtual void recycle(KItemListWidget* widget); +}; + +template <class T> +class LIBDOLPHINPRIVATE_EXPORT KItemListWidgetCreator : public KItemListWidgetCreatorBase +{ +public: + virtual ~KItemListWidgetCreator(); + virtual KItemListWidget* create(KItemListView* view); +}; + +template <class T> +KItemListWidgetCreator<T>::~KItemListWidgetCreator() +{ +} + +template <class T> +KItemListWidget* KItemListWidgetCreator<T>::create(KItemListView* view) +{ + KItemListWidget* widget = static_cast<KItemListWidget*>(popRecycleableWidget()); + if (!widget) { + widget = new T(view); + addCreatedWidget(widget); + } + return widget; +} + +/** + * @brief Base class for creating KItemListGroupHeaders. + * + * It is recommended that applications simply use the KItemListGroupHeaderCreator-template class. + * For a custom implementation the methods create() and recyle() must be reimplemented. + * The intention of the group-header creator is to prevent repetitive and expensive instantiations and + * deletions of KItemListGroupHeaders by recycling existing header instances. + */ +class LIBDOLPHINPRIVATE_EXPORT KItemListGroupHeaderCreatorBase : public KItemListCreatorBase +{ +public: + virtual ~KItemListGroupHeaderCreatorBase(); + virtual KItemListGroupHeader* create(QGraphicsWidget* parent) = 0; + virtual void recycle(KItemListGroupHeader* header); +}; + +template <class T> +class LIBDOLPHINPRIVATE_EXPORT KItemListGroupHeaderCreator : public KItemListGroupHeaderCreatorBase +{ +public: + virtual ~KItemListGroupHeaderCreator(); + virtual KItemListGroupHeader* create(QGraphicsWidget* parent); +}; + +template <class T> +KItemListGroupHeaderCreator<T>::~KItemListGroupHeaderCreator() +{ +} + +template <class T> +KItemListGroupHeader* KItemListGroupHeaderCreator<T>::create(QGraphicsWidget* parent) +{ + KItemListGroupHeader* widget = static_cast<KItemListGroupHeader*>(popRecycleableWidget()); + if (!widget) { + widget = new T(parent); + addCreatedWidget(widget); + } + return widget; +} + +#endif diff --git a/src/kitemviews/kitemlistviewanimation.cpp b/src/kitemviews/kitemlistviewanimation.cpp new file mode 100644 index 000000000..449d557f9 --- /dev/null +++ b/src/kitemviews/kitemlistviewanimation.cpp @@ -0,0 +1,241 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemlistviewanimation_p.h" + +#include "kitemlistview.h" + +#include <KDebug> + +#include <QGraphicsWidget> +#include <QPropertyAnimation> + +KItemListViewAnimation::KItemListViewAnimation(QObject* parent) : + QObject(parent), + m_scrollOrientation(Qt::Vertical), + m_offset(0), + m_animation() +{ +} + +KItemListViewAnimation::~KItemListViewAnimation() +{ + for (int type = 0; type < AnimationTypeCount; ++type) { + qDeleteAll(m_animation[type]); + } +} + +void KItemListViewAnimation::setScrollOrientation(Qt::Orientation orientation) +{ + m_scrollOrientation = orientation; +} + +Qt::Orientation KItemListViewAnimation::scrollOrientation() const +{ + return m_scrollOrientation; +} + +void KItemListViewAnimation::setOffset(qreal offset) +{ + const qreal diff = m_offset - offset; + m_offset = offset; + + // The change of the offset requires that the position of all + // animated QGraphicsWidgets get adjusted. An exception is made + // for the delete animation that should just fade away on the + // existing position. + for (int type = 0; type < AnimationTypeCount; ++type) { + if (type == DeleteAnimation) { + continue; + } + + QHashIterator<QGraphicsWidget*, QPropertyAnimation*> it(m_animation[type]); + while (it.hasNext()) { + it.next(); + + QGraphicsWidget* widget = it.key(); + QPropertyAnimation* propertyAnim = it.value(); + + QPointF currentPos = widget->pos(); + if (m_scrollOrientation == Qt::Vertical) { + currentPos.ry() += diff; + } else { + currentPos.rx() += diff; + } + + if (type == MovingAnimation) { + // Stop the animation, calculate the moved start- and end-value + // and restart the animation for the remaining duration. + const int remainingDuration = propertyAnim->duration() + - propertyAnim->currentTime(); + + const bool block = propertyAnim->signalsBlocked(); + propertyAnim->blockSignals(true); + propertyAnim->stop(); + + QPointF endPos = propertyAnim->endValue().toPointF(); + if (m_scrollOrientation == Qt::Vertical) { + endPos.ry() += diff; + } else { + endPos.rx() += diff; + } + + propertyAnim->setDuration(remainingDuration); + propertyAnim->setStartValue(currentPos); + propertyAnim->setEndValue(endPos); + propertyAnim->start(); + propertyAnim->blockSignals(block); + } else { + widget->setPos(currentPos); + } + } + } +} + +qreal KItemListViewAnimation::offset() const +{ + return m_offset; +} + +void KItemListViewAnimation::start(QGraphicsWidget* widget, AnimationType type, const QVariant& endValue) +{ + stop(widget, type); + + const int duration = 200; + QPropertyAnimation* propertyAnim = 0; + + switch (type) { + case MovingAnimation: { + const QPointF newPos = endValue.toPointF(); + if (newPos == widget->pos()) { + return; + } + + propertyAnim = new QPropertyAnimation(widget, "pos"); + propertyAnim->setDuration(duration); + propertyAnim->setEndValue(newPos); + break; + } + + case CreateAnimation: { + propertyAnim = new QPropertyAnimation(widget, "opacity"); + propertyAnim->setEasingCurve(QEasingCurve::InQuart); + propertyAnim->setDuration(duration); + propertyAnim->setStartValue(0.0); + propertyAnim->setEndValue(1.0); + break; + } + + case DeleteAnimation: { + propertyAnim = new QPropertyAnimation(widget, "opacity"); + propertyAnim->setEasingCurve(QEasingCurve::OutQuart); + propertyAnim->setDuration(duration); + propertyAnim->setStartValue(1.0); + propertyAnim->setEndValue(0.0); + break; + } + + case ResizeAnimation: { + const QSizeF newSize = endValue.toSizeF(); + if (newSize == widget->size()) { + return; + } + + propertyAnim = new QPropertyAnimation(widget, "size"); + propertyAnim->setDuration(duration); + propertyAnim->setEndValue(newSize); + break; + } + + default: + break; + } + + Q_ASSERT(propertyAnim); + connect(propertyAnim, SIGNAL(finished()), this, SLOT(slotFinished())); + m_animation[type].insert(widget, propertyAnim); + + propertyAnim->start(); +} + +void KItemListViewAnimation::stop(QGraphicsWidget* widget, AnimationType type) +{ + QPropertyAnimation* propertyAnim = m_animation[type].value(widget); + if (propertyAnim) { + propertyAnim->stop(); + + switch (type) { + case MovingAnimation: break; + case CreateAnimation: widget->setOpacity(1.0); break; + case DeleteAnimation: widget->setOpacity(0.0); break; + case ResizeAnimation: break; + default: break; + } + + m_animation[type].remove(widget); + delete propertyAnim; + + emit finished(widget, type); + } +} + +void KItemListViewAnimation::stop(QGraphicsWidget* widget) +{ + for (int type = 0; type < AnimationTypeCount; ++type) { + stop(widget, static_cast<AnimationType>(type)); + } +} + +bool KItemListViewAnimation::isStarted(QGraphicsWidget *widget, AnimationType type) const +{ + return m_animation[type].value(widget); +} + +bool KItemListViewAnimation::isStarted(QGraphicsWidget* widget) const +{ + for (int type = 0; type < AnimationTypeCount; ++type) { + if (isStarted(widget, static_cast<AnimationType>(type))) { + return true; + } + } + return false; +} + +void KItemListViewAnimation::slotFinished() +{ + QPropertyAnimation* finishedAnim = qobject_cast<QPropertyAnimation*>(sender()); + for (int type = 0; type < AnimationTypeCount; ++type) { + QHashIterator<QGraphicsWidget*, QPropertyAnimation*> it(m_animation[type]); + while (it.hasNext()) { + it.next(); + QPropertyAnimation* propertyAnim = it.value(); + if (propertyAnim == finishedAnim) { + QGraphicsWidget* widget = it.key(); + m_animation[type].remove(widget); + finishedAnim->deleteLater(); + + emit finished(widget, static_cast<AnimationType>(type)); + return; + } + } + } + Q_ASSERT(false); +} + +#include "kitemlistviewanimation_p.moc" diff --git a/src/kitemviews/kitemlistviewanimation_p.h b/src/kitemviews/kitemlistviewanimation_p.h new file mode 100644 index 000000000..0bf54d296 --- /dev/null +++ b/src/kitemviews/kitemlistviewanimation_p.h @@ -0,0 +1,79 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMLISTVIEWANIMATION_H +#define KITEMLISTVIEWANIMATION_H + +#include <libdolphin_export.h> + +#include <QHash> +#include <QObject> +#include <QVariant> + +class KItemListView; +class QGraphicsWidget; +class QPointF; +class QPropertyAnimation; + +class LIBDOLPHINPRIVATE_EXPORT KItemListViewAnimation : public QObject +{ + Q_OBJECT + +public: + enum AnimationType { + MovingAnimation, + CreateAnimation, + DeleteAnimation, + ResizeAnimation + }; + + KItemListViewAnimation(QObject* parent = 0); + virtual ~KItemListViewAnimation(); + + void setScrollOrientation(Qt::Orientation orientation); + Qt::Orientation scrollOrientation() const; + + void setOffset(qreal offset); + qreal offset() const; + + void start(QGraphicsWidget* widget, AnimationType type, const QVariant& endValue = QVariant()); + + void stop(QGraphicsWidget* widget, AnimationType type); + void stop(QGraphicsWidget* widget); + + bool isStarted(QGraphicsWidget *widget, AnimationType type) const; + bool isStarted(QGraphicsWidget* widget) const; + +signals: + void finished(QGraphicsWidget* widget, KItemListViewAnimation::AnimationType type); + +private slots: + void slotFinished(); + +private: + enum { AnimationTypeCount = 4 }; + + Qt::Orientation m_scrollOrientation; + qreal m_offset; + QHash<QGraphicsWidget*, QPropertyAnimation*> m_animation[AnimationTypeCount]; +}; + +#endif + + diff --git a/src/kitemviews/kitemlistviewlayouter.cpp b/src/kitemviews/kitemlistviewlayouter.cpp new file mode 100644 index 000000000..7d420e093 --- /dev/null +++ b/src/kitemviews/kitemlistviewlayouter.cpp @@ -0,0 +1,474 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemlistviewlayouter_p.h" + +#include "kitemmodelbase.h" +#include "kitemlistsizehintresolver_p.h" + +#include <KDebug> + +#define KITEMLISTVIEWLAYOUTER_DEBUG + +namespace { + // TODO + const int HeaderHeight = 50; +}; + +KItemListViewLayouter::KItemListViewLayouter(QObject* parent) : + QObject(parent), + m_dirty(true), + m_visibleIndexesDirty(true), + m_grouped(false), + m_scrollOrientation(Qt::Vertical), + m_size(), + m_itemSize(128, 128), + m_model(0), + m_sizeHintResolver(0), + m_offset(0), + m_maximumOffset(0), + m_firstVisibleIndex(-1), + m_lastVisibleIndex(-1), + m_firstVisibleGroupIndex(-1), + m_columnWidth(0), + m_xPosInc(0), + m_columnCount(0), + m_groups(), + m_groupIndexes(), + m_itemBoundingRects() +{ +} + +KItemListViewLayouter::~KItemListViewLayouter() +{ +} + +void KItemListViewLayouter::setScrollOrientation(Qt::Orientation orientation) +{ + if (m_scrollOrientation != orientation) { + m_scrollOrientation = orientation; + m_dirty = true; + } +} + +Qt::Orientation KItemListViewLayouter::scrollOrientation() const +{ + return m_scrollOrientation; +} + +void KItemListViewLayouter::setSize(const QSizeF& size) +{ + if (m_size != size) { + m_size = size; + m_dirty = true; + } +} + +QSizeF KItemListViewLayouter::size() const +{ + return m_size; +} + +void KItemListViewLayouter::setItemSize(const QSizeF& size) +{ + if (m_itemSize != size) { + m_itemSize = size; + m_dirty = true; + } +} + +QSizeF KItemListViewLayouter::itemSize() const +{ + return m_itemSize; +} + +void KItemListViewLayouter::setOffset(qreal offset) +{ + if (m_offset != offset) { + m_offset = offset; + m_visibleIndexesDirty = true; + } +} + +qreal KItemListViewLayouter::offset() const +{ + return m_offset; +} + +void KItemListViewLayouter::setModel(const KItemModelBase* model) +{ + if (m_model != model) { + m_model = model; + m_dirty = true; + } +} + +const KItemModelBase* KItemListViewLayouter::model() const +{ + return m_model; +} + +void KItemListViewLayouter::setSizeHintResolver(const KItemListSizeHintResolver* sizeHintResolver) +{ + if (m_sizeHintResolver != sizeHintResolver) { + m_sizeHintResolver = sizeHintResolver; + m_dirty = true; + } +} + +const KItemListSizeHintResolver* KItemListViewLayouter::sizeHintResolver() const +{ + return m_sizeHintResolver; +} + +qreal KItemListViewLayouter::maximumOffset() const +{ + const_cast<KItemListViewLayouter*>(this)->doLayout(); + return m_maximumOffset; +} + +int KItemListViewLayouter::firstVisibleIndex() const +{ + const_cast<KItemListViewLayouter*>(this)->doLayout(); + return m_firstVisibleIndex; +} + +int KItemListViewLayouter::lastVisibleIndex() const +{ + const_cast<KItemListViewLayouter*>(this)->doLayout(); + return m_lastVisibleIndex; +} + +QRectF KItemListViewLayouter::itemBoundingRect(int index) const +{ + const_cast<KItemListViewLayouter*>(this)->doLayout(); + if (index < 0 || index >= m_itemBoundingRects.count()) { + return QRectF(); + } + + if (m_scrollOrientation == Qt::Horizontal) { + // Rotate the logical direction which is always vertical by 90° + // to get the physical horizontal direction + const QRectF& b = m_itemBoundingRects[index]; + QRectF bounds(b.y(), b.x(), b.height(), b.width()); + QPointF pos = bounds.topLeft(); + pos.rx() -= m_offset; + bounds.moveTo(pos); + return bounds; + } + + QRectF bounds = m_itemBoundingRects[index]; + QPointF pos = bounds.topLeft(); + pos.ry() -= m_offset; + bounds.moveTo(pos); + return bounds; +} + +int KItemListViewLayouter::maximumVisibleItems() const +{ + const_cast<KItemListViewLayouter*>(this)->doLayout(); + + const int height = static_cast<int>(m_size.height()); + const int rowHeight = static_cast<int>(m_itemSize.height()); + int rows = height / rowHeight; + if (height % rowHeight != 0) { + ++rows; + } + + return rows * m_columnCount; +} + +bool KItemListViewLayouter::isFirstGroupItem(int itemIndex) const +{ + return m_groupIndexes.contains(itemIndex); +} + +void KItemListViewLayouter::markAsDirty() +{ + m_dirty = true; +} + +void KItemListViewLayouter::doLayout() +{ + if (m_dirty) { +#ifdef KITEMLISTVIEWLAYOUTER_DEBUG + QElapsedTimer timer; + timer.start(); +#endif + + m_visibleIndexesDirty = true; + + QSizeF itemSize = m_itemSize; + QSizeF size = m_size; + + const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal); + if (horizontalScrolling) { + itemSize.setWidth(m_itemSize.height()); + itemSize.setHeight(m_itemSize.width()); + size.setWidth(m_size.height()); + size.setHeight(m_size.width()); + } + + m_columnWidth = itemSize.width(); + m_columnCount = qMax(1, int(size.width() / m_columnWidth)); + m_xPosInc = 0; + + const int itemCount = m_model->count(); + if (itemCount > m_columnCount) { + // Apply the unused width equally to each column + const qreal unusedWidth = size.width() - m_columnCount * m_columnWidth; + const qreal columnInc = unusedWidth / (m_columnCount + 1); + m_columnWidth += columnInc; + m_xPosInc += columnInc; + } + + int rowCount = itemCount / m_columnCount; + if (itemCount % m_columnCount != 0) { + ++rowCount; + } + + m_itemBoundingRects.reserve(itemCount); + + qreal y = 0; + int rowIndex = 0; + + int index = 0; + while (index < itemCount) { + qreal x = m_xPosInc; + qreal maxItemHeight = itemSize.height(); + + int column = 0; + while (index < itemCount && column < m_columnCount) { + qreal requiredItemHeight = itemSize.height(); + if (m_sizeHintResolver) { + const QSizeF sizeHint = m_sizeHintResolver->sizeHint(index); + const qreal sizeHintHeight = horizontalScrolling ? sizeHint.width() : sizeHint.height(); + if (sizeHintHeight > requiredItemHeight) { + requiredItemHeight = sizeHintHeight; + } + } + + const QRectF bounds(x, y, itemSize.width(), requiredItemHeight); + if (index < m_itemBoundingRects.count()) { + m_itemBoundingRects[index] = bounds; + } else { + m_itemBoundingRects.append(bounds); + } + + maxItemHeight = qMax(maxItemHeight, requiredItemHeight); + x += m_columnWidth; + ++index; + ++column; + } + + y += maxItemHeight; + ++rowIndex; + } + if (m_itemBoundingRects.count() > itemCount) { + m_itemBoundingRects.erase(m_itemBoundingRects.begin() + itemCount, + m_itemBoundingRects.end()); + } + + m_maximumOffset = (itemCount > 0) ? m_itemBoundingRects.last().bottom() : 0; + + m_grouped = !m_model->groupRole().isEmpty(); + /*if (m_grouped) { + createGroupHeaders(); + + const int lastGroupItemCount = m_model->count() - m_groups.last().firstItemIndex; + m_maximumOffset = m_groups.last().y + (lastGroupItemCount / m_columnCount) * m_rowHeight; + if (lastGroupItemCount % m_columnCount != 0) { + m_maximumOffset += m_rowHeight; + } + } else {*/ + // m_maximumOffset = m_minimumRowHeight * rowCount; + //} + +#ifdef KITEMLISTVIEWLAYOUTER_DEBUG + kDebug() << "[TIME] doLayout() for " << m_model->count() << "items:" << timer.elapsed(); +#endif + m_dirty = false; + } + + if (m_grouped) { + updateGroupedVisibleIndexes(); + } else { + updateVisibleIndexes(); + } +} + +void KItemListViewLayouter::updateVisibleIndexes() +{ + if (!m_visibleIndexesDirty) { + return; + } + + Q_ASSERT(!m_grouped); + Q_ASSERT(!m_dirty); + + if (m_model->count() <= 0) { + m_firstVisibleIndex = -1; + m_lastVisibleIndex = -1; + m_visibleIndexesDirty = false; + return; + } + + const bool horizontalScrolling = (m_scrollOrientation == Qt::Horizontal); + const int minimumHeight = horizontalScrolling ? m_itemSize.width() + : m_itemSize.height(); + + // Calculate the first visible index: + // 1. Guess the index by using the minimum row height + const int maxIndex = m_model->count() - 1; + m_firstVisibleIndex = int(m_offset / minimumHeight) * m_columnCount; + + // 2. Decrease the index by checking the real row heights + int prevRowIndex = m_firstVisibleIndex - m_columnCount; + while (prevRowIndex > maxIndex) { + prevRowIndex -= m_columnCount; + } + + while (prevRowIndex >= 0 && m_itemBoundingRects[prevRowIndex].bottom() >= m_offset) { + m_firstVisibleIndex = prevRowIndex; + prevRowIndex -= m_columnCount; + } + m_firstVisibleIndex = qBound(0, m_firstVisibleIndex, maxIndex); + + // Calculate the last visible index + const int visibleHeight = horizontalScrolling ? m_size.width() : m_size.height(); + const qreal bottom = m_offset + visibleHeight; + m_lastVisibleIndex = m_firstVisibleIndex; // first visible row, first column + int nextRowIndex = m_lastVisibleIndex + m_columnCount; + while (nextRowIndex <= maxIndex && m_itemBoundingRects[nextRowIndex].y() <= bottom) { + m_lastVisibleIndex = nextRowIndex; + nextRowIndex += m_columnCount; + } + m_lastVisibleIndex += m_columnCount - 1; // move it to the last column + m_lastVisibleIndex = qBound(0, m_lastVisibleIndex, maxIndex); + + m_visibleIndexesDirty = false; +} + +void KItemListViewLayouter::updateGroupedVisibleIndexes() +{ + if (!m_visibleIndexesDirty) { + return; + } + + Q_ASSERT(m_grouped); + Q_ASSERT(!m_dirty); + + if (m_model->count() <= 0) { + m_firstVisibleIndex = -1; + m_lastVisibleIndex = -1; + m_visibleIndexesDirty = false; + return; + } + + // Find the first visible group + const int lastGroupIndex = m_groups.count() - 1; + int groupIndex = lastGroupIndex; + for (int i = 1; i < m_groups.count(); ++i) { + if (m_groups[i].y >= m_offset) { + groupIndex = i - 1; + break; + } + } + + // Calculate the first visible index + qreal groupY = m_groups[groupIndex].y; + m_firstVisibleIndex = m_groups[groupIndex].firstItemIndex; + const int invisibleRowCount = int(m_offset - groupY) / int(m_itemSize.height()); + m_firstVisibleIndex += invisibleRowCount * m_columnCount; + if (groupIndex + 1 <= lastGroupIndex) { + // Check whether the calculated first visible index remains inside the current + // group. If this is not the case let the first element of the next group be the first + // visible index. + const int nextGroupIndex = m_groups[groupIndex + 1].firstItemIndex; + if (m_firstVisibleIndex > nextGroupIndex) { + m_firstVisibleIndex = nextGroupIndex; + } + } + + m_firstVisibleGroupIndex = groupIndex; + + const int maxIndex = m_model->count() - 1; + m_firstVisibleIndex = qBound(0, m_firstVisibleIndex, maxIndex); + + // Calculate the last visible index: Find group where the last visible item is shown. + const qreal visibleBottom = m_offset + m_size.height(); // TODO: respect Qt::Horizontal alignment + while ((groupIndex < lastGroupIndex) && (m_groups[groupIndex + 1].y < visibleBottom)) { + ++groupIndex; + } + + groupY = m_groups[groupIndex].y; + m_lastVisibleIndex = m_groups[groupIndex].firstItemIndex; + const int availableHeight = static_cast<int>(visibleBottom - groupY); + int visibleRowCount = availableHeight / int(m_itemSize.height()); + if (availableHeight % int(m_itemSize.height()) != 0) { + ++visibleRowCount; + } + m_lastVisibleIndex += visibleRowCount * m_columnCount - 1; + + if (groupIndex + 1 <= lastGroupIndex) { + // Check whether the calculate last visible index remains inside the current group. + // If this is not the case let the last element of this group be the last visible index. + const int nextGroupIndex = m_groups[groupIndex + 1].firstItemIndex; + if (m_lastVisibleIndex >= nextGroupIndex) { + m_lastVisibleIndex = nextGroupIndex - 1; + } + } + //Q_ASSERT(m_lastVisibleIndex < m_model->count()); + m_lastVisibleIndex = qBound(0, m_lastVisibleIndex, maxIndex); + + m_visibleIndexesDirty = false; +} + +void KItemListViewLayouter::createGroupHeaders() +{ + m_groups.clear(); + m_groupIndexes.clear(); + + // TODO: + QList<int> numbers; + numbers << 0 << 5 << 6 << 13 << 20 << 25 << 30 << 35 << 50; + + qreal y = 0; + for (int i = 0; i < numbers.count(); ++i) { + if (i > 0) { + const int previousGroupItemCount = numbers[i] - m_groups.last().firstItemIndex; + int previousGroupRowCount = previousGroupItemCount / m_columnCount; + if (previousGroupItemCount % m_columnCount != 0) { + ++previousGroupRowCount; + } + const qreal previousGroupHeight = previousGroupRowCount * m_itemSize.height(); + y += previousGroupHeight; + } + y += HeaderHeight; + + ItemGroup itemGroup; + itemGroup.firstItemIndex = numbers[i]; + itemGroup.y = y; + + m_groups.append(itemGroup); + m_groupIndexes.insert(itemGroup.firstItemIndex); + } +} + +#include "kitemlistviewlayouter_p.moc" diff --git a/src/kitemviews/kitemlistviewlayouter_p.h b/src/kitemviews/kitemlistviewlayouter_p.h new file mode 100644 index 000000000..775e9ff85 --- /dev/null +++ b/src/kitemviews/kitemlistviewlayouter_p.h @@ -0,0 +1,126 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMLISTVIEWLAYOUTER_H +#define KITEMLISTVIEWLAYOUTER_H + +#include <libdolphin_export.h> + +#include <QObject> +#include <QRectF> +#include <QSet> +#include <QSizeF> + +class KItemModelBase; +class KItemListSizeHintResolver; + +class LIBDOLPHINPRIVATE_EXPORT KItemListViewLayouter : public QObject +{ + Q_OBJECT + +public: + KItemListViewLayouter(QObject* parent = 0); + virtual ~KItemListViewLayouter(); + + void setScrollOrientation(Qt::Orientation orientation); + Qt::Orientation scrollOrientation() const; + + void setSize(const QSizeF& size); + QSizeF size() const; + + void setItemSize(const QSizeF& size); + QSizeF itemSize() const; + + // TODO: add note that offset can be < 0 or > maximumOffset! + void setOffset(qreal offset); + qreal offset() const; + + void setModel(const KItemModelBase* model); + const KItemModelBase* model() const; + + void setSizeHintResolver(const KItemListSizeHintResolver* sizeHintResolver); + const KItemListSizeHintResolver* sizeHintResolver() const; + + qreal maximumOffset() const; + + // TODO: mention that return value is -1 if count == 0 + int firstVisibleIndex() const; + + // TODO: mention that return value is -1 if count == 0 + int lastVisibleIndex() const; + + QRectF itemBoundingRect(int index) const; + + int maximumVisibleItems() const; + + /** + * @return True if the item with the index \p itemIndex + * is the first item within a group. + */ + bool isFirstGroupItem(int itemIndex) const; + + void markAsDirty(); + +private: + void doLayout(); + + void updateVisibleIndexes(); + void updateGroupedVisibleIndexes(); + void createGroupHeaders(); + +private: + bool m_dirty; + bool m_visibleIndexesDirty; + bool m_grouped; + + Qt::Orientation m_scrollOrientation; + QSizeF m_size; + + QSizeF m_itemSize; + const KItemModelBase* m_model; + const KItemListSizeHintResolver* m_sizeHintResolver; + + qreal m_offset; + qreal m_maximumOffset; + + int m_firstVisibleIndex; + int m_lastVisibleIndex; + + int m_firstVisibleGroupIndex; + + qreal m_columnWidth; + qreal m_xPosInc; + int m_columnCount; + + struct ItemGroup { + int firstItemIndex; + qreal y; + }; + QList<ItemGroup> m_groups; + + // Stores all item indexes that are the first item of a group. + // Assures fast access for KItemListViewLayouter::isFirstGroupItem(). + QSet<int> m_groupIndexes; + + QList<QRectF> m_itemBoundingRects; +}; + +#endif + + diff --git a/src/kitemviews/kitemlistwidget.cpp b/src/kitemviews/kitemlistwidget.cpp new file mode 100644 index 000000000..3f08d9f7a --- /dev/null +++ b/src/kitemviews/kitemlistwidget.cpp @@ -0,0 +1,260 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemlistwidget.h" + +#include "kitemlistview.h" +#include "kitemmodelbase.h" + +#include <KDebug> + +#include <QPainter> +#include <QPropertyAnimation> +#include <QStyle> + +KItemListWidget::KItemListWidget(QGraphicsItem* parent) : + QGraphicsWidget(parent, 0), + m_index(-1), + m_data(), + m_visibleRoles(), + m_visibleRolesSizes(), + m_styleOption(), + m_hoverOpacity(0), + m_hoverCache(0), + m_hoverAnimation(0) +{ +} + +KItemListWidget::~KItemListWidget() +{ + clearCache(); +} + +void KItemListWidget::setIndex(int index) +{ + if (m_index != index) { + if (m_hoverAnimation) { + m_hoverAnimation->stop(); + m_hoverOpacity = 0; + } + clearCache(); + + m_index = index; + } +} + +int KItemListWidget::index() const +{ + return m_index; +} + +void KItemListWidget::setData(const QHash<QByteArray, QVariant>& data, + const QSet<QByteArray>& roles) +{ + clearCache(); + if (roles.isEmpty()) { + m_data = data; + dataChanged(m_data); + } else { + foreach (const QByteArray& role, roles) { + m_data[role] = data[role]; + } + dataChanged(m_data, roles); + } +} + +QHash<QByteArray, QVariant> KItemListWidget::data() const +{ + return m_data; +} + +void KItemListWidget::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) +{ + Q_UNUSED(option); + if (m_hoverOpacity <= 0.0) { + return; + } + + const QRect hoverBounds = hoverBoundingRect().toRect(); + if (!m_hoverCache) { + m_hoverCache = new QPixmap(hoverBounds.size()); + m_hoverCache->fill(Qt::transparent); + + QPainter pixmapPainter(m_hoverCache); + + QStyleOptionViewItemV4 viewItemOption; + viewItemOption.initFrom(widget); + viewItemOption.rect = QRect(0, 0, hoverBounds.width(), hoverBounds.height()); + viewItemOption.state = QStyle::State_Enabled | QStyle::State_MouseOver; + viewItemOption.viewItemPosition = QStyleOptionViewItemV4::OnlyOne; + + widget->style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &viewItemOption, &pixmapPainter, widget); + } + + const qreal opacity = painter->opacity(); + painter->setOpacity(m_hoverOpacity * opacity); + painter->drawPixmap(hoverBounds.topLeft(), *m_hoverCache); + painter->setOpacity(opacity); +} + +void KItemListWidget::setVisibleRoles(const QHash<QByteArray, int>& roles) +{ + const QHash<QByteArray, int> previousRoles = m_visibleRoles; + m_visibleRoles = roles; + visibleRolesChanged(roles, previousRoles); +} + +QHash<QByteArray, int> KItemListWidget::visibleRoles() const +{ + return m_visibleRoles; +} + +void KItemListWidget::setVisibleRolesSizes(const QHash<QByteArray, QSizeF> rolesSizes) +{ + const QHash<QByteArray, QSizeF> previousRolesSizes = m_visibleRolesSizes; + m_visibleRolesSizes = rolesSizes; + visibleRolesSizesChanged(rolesSizes, previousRolesSizes); +} + +QHash<QByteArray, QSizeF> KItemListWidget::visibleRolesSizes() const +{ + return m_visibleRolesSizes; +} + +void KItemListWidget::setStyleOption(const KItemListStyleOption& option) +{ + const KItemListStyleOption previous = m_styleOption; + if (m_index >= 0) { + clearCache(); + + const bool wasHovered = (previous.state & QStyle::State_MouseOver); + m_styleOption = option; + const bool isHovered = (m_styleOption.state & QStyle::State_MouseOver); + + if (wasHovered != isHovered) { + // The hovering state has been changed. Assure that a fade-animation + // is done to the new state. + if (!m_hoverAnimation) { + m_hoverAnimation = new QPropertyAnimation(this, "hoverOpacity", this); + m_hoverAnimation->setDuration(200); + } + m_hoverAnimation->stop(); + + if (!wasHovered && isHovered) { + m_hoverAnimation->setEndValue(1.0); + } else { + Q_ASSERT(wasHovered && !isHovered); + m_hoverAnimation->setEndValue(0.0); + } + + m_hoverAnimation->start(); + } + } else { + m_styleOption = option; + } + + styleOptionChanged(option, previous); +} + +const KItemListStyleOption& KItemListWidget::styleOption() const +{ + return m_styleOption; +} + +bool KItemListWidget::contains(const QPointF& point) const +{ + return hoverBoundingRect().contains(point) || + expansionToggleRect().contains(point) || + selectionToggleRect().contains(point); +} + +QRectF KItemListWidget::hoverBoundingRect() const +{ + return QRectF(QPointF(0, 0), size()); +} + +QRectF KItemListWidget::selectionToggleRect() const +{ + return QRectF(); +} + +QRectF KItemListWidget::expansionToggleRect() const +{ + return QRectF(); +} + +void KItemListWidget::dataChanged(const QHash<QByteArray, QVariant>& current, + const QSet<QByteArray>& roles) +{ + Q_UNUSED(current); + Q_UNUSED(roles); + update(); +} + +void KItemListWidget::visibleRolesChanged(const QHash<QByteArray, int>& current, + const QHash<QByteArray, int>& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); + update(); +} + +void KItemListWidget::visibleRolesSizesChanged(const QHash<QByteArray, QSizeF>& current, + const QHash<QByteArray, QSizeF>& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); + update(); +} + +void KItemListWidget::styleOptionChanged(const KItemListStyleOption& current, + const KItemListStyleOption& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); + update(); +} + +void KItemListWidget::resizeEvent(QGraphicsSceneResizeEvent* event) +{ + QGraphicsWidget::resizeEvent(event); + clearCache(); +} + +qreal KItemListWidget::hoverOpacity() const +{ + return m_hoverOpacity; +} + +void KItemListWidget::setHoverOpacity(qreal opacity) +{ + m_hoverOpacity = opacity; + update(); +} + +void KItemListWidget::clearCache() +{ + delete m_hoverCache; + m_hoverCache = 0; +} + +#include "kitemlistwidget.moc" diff --git a/src/kitemviews/kitemlistwidget.h b/src/kitemviews/kitemlistwidget.h new file mode 100644 index 000000000..eb2ebf455 --- /dev/null +++ b/src/kitemviews/kitemlistwidget.h @@ -0,0 +1,134 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMLISTWIDGET_H +#define KITEMLISTWIDGET_H + +#include <libdolphin_export.h> + +#include <kitemviews/kitemliststyleoption.h> + +#include <QGraphicsWidget> + +class QPropertyAnimation; + +/** + * @brief Widget that shows a visible item from the model. + * + * For showing an item from a custom model it is required to at least overwrite KItemListWidget::paint(). + * All properties are set by KItemListView, for each property there is a corresponding + * virtual protected method that allows to react on property changes. + */ +class LIBDOLPHINPRIVATE_EXPORT KItemListWidget : public QGraphicsWidget +{ + Q_OBJECT + +public: + KItemListWidget(QGraphicsItem* parent); + virtual ~KItemListWidget(); + + void setIndex(int index); + int index() const; + + void setData(const QHash<QByteArray, QVariant>& data, const QSet<QByteArray>& roles = QSet<QByteArray>()); + QHash<QByteArray, QVariant> data() const; + + /** + * Draws the hover-bounding-rectangle if the item is hovered. Overwrite this method + * to show the data of the custom model provided by KItemListWidget::data(). + * @reimp + */ + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = 0); + + /** + * Sets the visible roles to \p roles. The integer-value defines + * the order of the visible role: Smaller values are ordered first. + */ + void setVisibleRoles(const QHash<QByteArray, int>& roles); + QHash<QByteArray, int> visibleRoles() const; + + void setVisibleRolesSizes(const QHash<QByteArray, QSizeF> rolesSizes); + QHash<QByteArray, QSizeF> visibleRolesSizes() const; + + void setStyleOption(const KItemListStyleOption& option); + const KItemListStyleOption& styleOption() const; + + /** + * @return True if \a point is inside KItemListWidget::hoverBoundingRect(), + * KItemListWidget::selectionToggleRect() or KItemListWidget::expansionToggleRect(). + * @reimp + */ + virtual bool contains(const QPointF& point) const; + + /** + * @return Bounding rectangle for the area that acts as hovering-area. Per default + * the bounding rectangle of the KItemListWidget is returned. + */ + virtual QRectF hoverBoundingRect() const; + + /** + * @return Rectangle for the selection-toggle that is used to select or deselect an item. + * Per default an empty rectangle is returned which means that no selection-toggle + * is available. + */ + virtual QRectF selectionToggleRect() const; + + /** + * @return Rectangle for the expansion-toggle that is used to open a sub-tree of the model. + * Per default an empty rectangle is returned which means that no opening of sub-trees + * is supported. + */ + virtual QRectF expansionToggleRect() const; + +protected: + virtual void dataChanged(const QHash<QByteArray, QVariant>& current, const QSet<QByteArray>& roles = QSet<QByteArray>()); + virtual void visibleRolesChanged(const QHash<QByteArray, int>& current, const QHash<QByteArray, int>& previous); + virtual void visibleRolesSizesChanged(const QHash<QByteArray, QSizeF>& current, const QHash<QByteArray, QSizeF>& previous); + virtual void styleOptionChanged(const KItemListStyleOption& current, const KItemListStyleOption& previous); + virtual void resizeEvent(QGraphicsSceneResizeEvent* event); + + /** + * @return The current opacity of the hover-animation. When implementing a custom painting-code for a hover-state + * this opacity value should be respected. + */ + qreal hoverOpacity() const; + +private: + void setHoverOpacity(qreal opacity); + void clearCache(); + +private: + Q_PROPERTY(qreal hoverOpacity READ hoverOpacity WRITE setHoverOpacity) + + int m_index; + QHash<QByteArray, QVariant> m_data; + QHash<QByteArray, int> m_visibleRoles; + QHash<QByteArray, QSizeF> m_visibleRolesSizes; + KItemListStyleOption m_styleOption; + + qreal m_hoverOpacity; + mutable QPixmap* m_hoverCache; + QPropertyAnimation* m_hoverAnimation; +}; +#endif + + diff --git a/src/kitemviews/kitemmodelbase.cpp b/src/kitemviews/kitemmodelbase.cpp new file mode 100644 index 000000000..63e771ef7 --- /dev/null +++ b/src/kitemviews/kitemmodelbase.cpp @@ -0,0 +1,113 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 "kitemmodelbase.h" + +KItemRange::KItemRange(int index, int count) : + index(index), + count(count) +{ +} + +KItemModelBase::KItemModelBase(QObject* parent) : + QObject(parent), + m_groupRole(), + m_sortRole() +{ +} + +KItemModelBase::KItemModelBase(const QByteArray& groupRole, const QByteArray& sortRole, QObject* parent) : + QObject(parent), + m_groupRole(groupRole), + m_sortRole(sortRole) +{ +} + +KItemModelBase::~KItemModelBase() +{ +} + +bool KItemModelBase::setData(int index, const QHash<QByteArray, QVariant> &values) +{ + Q_UNUSED(index); + Q_UNUSED(values); + return false; +} + +bool KItemModelBase::supportsGrouping() const +{ + return false; +} + +void KItemModelBase::setGroupRole(const QByteArray& role) +{ + if (supportsGrouping() && role != m_groupRole) { + const QByteArray previous = m_groupRole; + m_groupRole = role; + onGroupRoleChanged(role, previous); + emit groupRoleChanged(role, previous); + } +} + +QByteArray KItemModelBase::groupRole() const +{ + return m_groupRole; +} + +bool KItemModelBase::supportsSorting() const +{ + return false; +} + +void KItemModelBase::setSortRole(const QByteArray& role) +{ + if (supportsSorting() && role != m_sortRole) { + const QByteArray previous = m_sortRole; + m_sortRole = role; + onSortRoleChanged(role, previous); + emit sortRoleChanged(role, previous); + } +} + +QByteArray KItemModelBase::sortRole() const +{ + return m_sortRole; +} + +QString KItemModelBase::roleDescription(const QByteArray& role) const +{ + return role; +} + +void KItemModelBase::onGroupRoleChanged(const QByteArray& current, const QByteArray& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +void KItemModelBase::onSortRoleChanged(const QByteArray& current, const QByteArray& previous) +{ + Q_UNUSED(current); + Q_UNUSED(previous); +} + +#include "kitemmodelbase.moc" diff --git a/src/kitemviews/kitemmodelbase.h b/src/kitemviews/kitemmodelbase.h new file mode 100644 index 000000000..4eb96c8fd --- /dev/null +++ b/src/kitemviews/kitemmodelbase.h @@ -0,0 +1,145 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * Based on the Itemviews NG project from Trolltech Labs: * + * http://qt.gitorious.org/qt-labs/itemviews-ng * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KITEMMODELBASE_H +#define KITEMMODELBASE_H + +#include <libdolphin_export.h> + +#include <QHash> +#include <QObject> +#include <QSet> +#include <QVariant> + +struct KItemRange +{ + KItemRange(int index, int count); + int index; + int count; +}; +typedef QList<KItemRange> KItemRangeList; + +/** + * @brief Base class for model implementations used by KItemListView and KItemListController. + * + * A item-model consists of a variable number of items. The number of items + * is given by KItemModelBase::count(). The data of an item is accessed by a unique index + * with KItemModelBase::data(). The indexes are integer-values counting from 0 to the + * KItemModelBase::count() - 1. + * + * One item consists of a variable number of role/value-pairs. + * + * A model can optionally provide sorting- and/or grouping-capabilities. + */ +class LIBDOLPHINPRIVATE_EXPORT KItemModelBase : public QObject +{ + Q_OBJECT + +public: + KItemModelBase(QObject* parent = 0); + KItemModelBase(const QByteArray& groupRole, const QByteArray& sortRole, QObject* parent = 0); + virtual ~KItemModelBase(); + + /** @return The number of items. */ + virtual int count() const = 0; + + virtual QHash<QByteArray, QVariant> data(int index) const = 0; + + /** + * Sets the data for the item at \a index to the given \a values. Returns true + * if the data was set on the item; returns false otherwise. + * + * The default implementation does not set the data, and will always return + * false. + */ + virtual bool setData(int index, const QHash<QByteArray, QVariant>& values); + + /** + * @return True if the model supports grouping of data. Per default false is returned. + * If the model should support grouping it is necessary to overwrite + * this method to return true and to implement KItemModelBase::onGroupRoleChanged(). + */ + virtual bool supportsGrouping() const; + + /** + * Sets the group-role to \a role. The method KItemModelBase::onGroupRoleChanged() will be + * called so that model-implementations can react on the group-role change. Afterwards the + * signal groupRoleChanged() will be emitted. + */ + void setGroupRole(const QByteArray& role); + QByteArray groupRole() const; + + /** + * @return True if the model supports sorting of data. Per default false is returned. + * If the model should support sorting it is necessary to overwrite + * this method to return true and to implement KItemModelBase::onSortRoleChanged(). + */ + virtual bool supportsSorting() const; + + /** + * Sets the sor-role to \a role. The method KItemModelBase::onSortRoleChanged() will be + * called so that model-implementations can react on the sort-role change. Afterwards the + * signal sortRoleChanged() will be emitted. + */ + void setSortRole(const QByteArray& role); + QByteArray sortRole() const; + + virtual QString roleDescription(const QByteArray& role) const; + +signals: + void itemsInserted(const KItemRangeList& itemRanges); + void itemsRemoved(const KItemRangeList& itemRanges); + void itemsMoved(const KItemRangeList& itemRanges); + void itemsChanged(const KItemRangeList& itemRanges, const QSet<QByteArray>& roles); + + void groupRoleChanged(const QByteArray& current, const QByteArray& previous); + void sortRoleChanged(const QByteArray& current, const QByteArray& previous); + +protected: + /** + * Is invoked if the group role has been changed by KItemModelBase::setGroupRole(). Allows + * to react on the changed group role before the signal groupRoleChanged() will be emitted. + * The implementation must assure that the items are sorted in a way that they are grouped + * by the role given by \a current. Usually the most efficient way is to emit a + * itemsRemoved() signal for all items, reorder the items internally and to emit a + * itemsInserted() signal afterwards. + */ + virtual void onGroupRoleChanged(const QByteArray& current, const QByteArray& previous); + + /** + * Is invoked if the sort role has been changed by KItemModelBase::setSortRole(). Allows + * to react on the changed sort role before the signal sortRoleChanged() will be emitted. + * The implementation must assure that the items are sorted by the role given by \a current. + * Usually the most efficient way is to emit a + * itemsRemoved() signal for all items, reorder the items internally and to emit a + * itemsInserted() signal afterwards. + */ + virtual void onSortRoleChanged(const QByteArray& current, const QByteArray& previous); + +private: + QByteArray m_groupRole; + QByteArray m_sortRole; +}; + +#endif + + diff --git a/src/kitemviews/kpixmapmodifier.cpp b/src/kitemviews/kpixmapmodifier.cpp new file mode 100644 index 000000000..e210f0bf5 --- /dev/null +++ b/src/kitemviews/kpixmapmodifier.cpp @@ -0,0 +1,388 @@ +//krazy:exclude=copyright (email of Maxim is missing) +/* + This file is a part of the KDE project + + Copyright © 2006 Zack Rusin <[email protected]> + Copyright © 2006-2007, 2008 Fredrik Höglund <[email protected]> + + The stack blur algorithm was invented by Mario Klingemann <[email protected]> + + This implementation is based on the version in Anti-Grain Geometry Version 2.4, + Copyright © 2002-2005 Maxim Shemanarev (http://www.antigrain.com) + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include "kpixmapmodifier_p.h" + +#include <QImage> +#include <QPainter> +#include <QPixmap> +#include <QSize> + +#include <KDebug> + +#include <config-X11.h> // for HAVE_XRENDER +#if defined(Q_WS_X11) && defined(HAVE_XRENDER) +# include <QX11Info> +# include <X11/Xlib.h> +# include <X11/extensions/Xrender.h> +#endif + +static const quint32 stackBlur8Mul[255] = +{ + 512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512, + 454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512, + 482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456, + 437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512, + 497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328, + 320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456, + 446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335, + 329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512, + 505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405, + 399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328, + 324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271, + 268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456, + 451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388, + 385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335, + 332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292, + 289,287,285,282,280,278,275,273,271,269,267,265,263,261,259 +}; + +static const quint32 stackBlur8Shr[255] = +{ + 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, + 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 +}; + +static void blurHorizontal(QImage& image, unsigned int* stack, int div, int radius) +{ + int stackindex; + int stackstart; + + quint32 * const pixels = reinterpret_cast<quint32 *>(image.bits()); + quint32 pixel; + + int w = image.width(); + int h = image.height(); + int wm = w - 1; + + unsigned int mulSum = stackBlur8Mul[radius]; + unsigned int shrSum = stackBlur8Shr[radius]; + + unsigned int sum, sumIn, sumOut; + + for (int y = 0; y < h; y++) { + sum = 0; + sumIn = 0; + sumOut = 0; + + const int yw = y * w; + pixel = pixels[yw]; + for (int i = 0; i <= radius; i++) { + stack[i] = qAlpha(pixel); + + sum += stack[i] * (i + 1); + sumOut += stack[i]; + } + + for (int i = 1; i <= radius; i++) { + pixel = pixels[yw + qMin(i, wm)]; + + unsigned int* stackpix = &stack[i + radius]; + *stackpix = qAlpha(pixel); + + sum += *stackpix * (radius + 1 - i); + sumIn += *stackpix; + } + + stackindex = radius; + for (int x = 0, i = yw; x < w; x++) { + pixels[i++] = (((sum * mulSum) >> shrSum) << 24) & 0xff000000; + + sum -= sumOut; + + stackstart = stackindex + div - radius; + if (stackstart >= div) { + stackstart -= div; + } + + unsigned int* stackpix = &stack[stackstart]; + + sumOut -= *stackpix; + + pixel = pixels[yw + qMin(x + radius + 1, wm)]; + + *stackpix = qAlpha(pixel); + + sumIn += *stackpix; + sum += sumIn; + + if (++stackindex >= div) { + stackindex = 0; + } + + stackpix = &stack[stackindex]; + + sumOut += *stackpix; + sumIn -= *stackpix; + } + } +} + +static void blurVertical(QImage& image, unsigned int* stack, int div, int radius) +{ + int stackindex; + int stackstart; + + quint32 * const pixels = reinterpret_cast<quint32 *>(image.bits()); + quint32 pixel; + + int w = image.width(); + int h = image.height(); + int hm = h - 1; + + int mul_sum = stackBlur8Mul[radius]; + int shr_sum = stackBlur8Shr[radius]; + + unsigned int sum, sumIn, sumOut; + + for (int x = 0; x < w; x++) { + sum = 0; + sumIn = 0; + sumOut = 0; + + pixel = pixels[x]; + for (int i = 0; i <= radius; i++) { + stack[i] = qAlpha(pixel); + + sum += stack[i] * (i + 1); + sumOut += stack[i]; + } + + for (int i = 1; i <= radius; i++) { + pixel = pixels[qMin(i, hm) * w + x]; + + unsigned int* stackpix = &stack[i + radius]; + *stackpix = qAlpha(pixel); + + sum += *stackpix * (radius + 1 - i); + sumIn += *stackpix; + } + + stackindex = radius; + for (int y = 0, i = x; y < h; y++, i += w) { + pixels[i] = (((sum * mul_sum) >> shr_sum) << 24) & 0xff000000; + + sum -= sumOut; + + stackstart = stackindex + div - radius; + if (stackstart >= div) + stackstart -= div; + + unsigned int* stackpix = &stack[stackstart]; + + sumOut -= *stackpix; + + pixel = pixels[qMin(y + radius + 1, hm) * w + x]; + + *stackpix = qAlpha(pixel); + + sumIn += *stackpix; + sum += sumIn; + + if (++stackindex >= div) { + stackindex = 0; + } + + stackpix = &stack[stackindex]; + + sumOut += *stackpix; + sumIn -= *stackpix; + } + } +} + +static void stackBlur(QImage& image, float radius) +{ + radius = qRound(radius); + + int div = int(radius * 2) + 1; + unsigned int* stack = new unsigned int[div]; + + blurHorizontal(image, stack, div, radius); + blurVertical(image, stack, div, radius); + + delete [] stack; +} + +static void shadowBlur(QImage& image, float radius, const QColor& color) +{ + if (radius < 0) { + return; + } + + if (radius > 0) { + stackBlur(image, radius); + } + + // Correct the color and opacity of the shadow + QPainter p(&image); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + p.fillRect(image.rect(), color); +} + +namespace { + /** Helper class for drawing frames for KPixmapModifier::applyFrame(). */ + class TileSet + { + public: + enum { LeftMargin = 3, TopMargin = 2, RightMargin = 3, BottomMargin = 4 }; + + enum Tile { TopLeftCorner = 0, TopSide, TopRightCorner, LeftSide, + RightSide, BottomLeftCorner, BottomSide, BottomRightCorner, + NumTiles }; + + TileSet() + { + QImage image(8 * 3, 8 * 3, QImage::Format_ARGB32_Premultiplied); + + QPainter p(&image); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.fillRect(image.rect(), Qt::transparent); + p.fillRect(image.rect().adjusted(3, 3, -3, -3), Qt::black); + p.end(); + + shadowBlur(image, 3, Qt::black); + + QPixmap pixmap = QPixmap::fromImage(image); + m_tiles[TopLeftCorner] = pixmap.copy(0, 0, 8, 8); + m_tiles[TopSide] = pixmap.copy(8, 0, 8, 8); + m_tiles[TopRightCorner] = pixmap.copy(16, 0, 8, 8); + m_tiles[LeftSide] = pixmap.copy(0, 8, 8, 8); + m_tiles[RightSide] = pixmap.copy(16, 8, 8, 8); + m_tiles[BottomLeftCorner] = pixmap.copy(0, 16, 8, 8); + m_tiles[BottomSide] = pixmap.copy(8, 16, 8, 8); + m_tiles[BottomRightCorner] = pixmap.copy(16, 16, 8, 8); + } + + void paint(QPainter* p, const QRect& r) + { + p->drawPixmap(r.topLeft(), m_tiles[TopLeftCorner]); + if (r.width() - 16 > 0) { + p->drawTiledPixmap(r.x() + 8, r.y(), r.width() - 16, 8, m_tiles[TopSide]); + } + p->drawPixmap(r.right() - 8 + 1, r.y(), m_tiles[TopRightCorner]); + if (r.height() - 16 > 0) { + p->drawTiledPixmap(r.x(), r.y() + 8, 8, r.height() - 16, m_tiles[LeftSide]); + p->drawTiledPixmap(r.right() - 8 + 1, r.y() + 8, 8, r.height() - 16, m_tiles[RightSide]); + } + p->drawPixmap(r.x(), r.bottom() - 8 + 1, m_tiles[BottomLeftCorner]); + if (r.width() - 16 > 0) { + p->drawTiledPixmap(r.x() + 8, r.bottom() - 8 + 1, r.width() - 16, 8, m_tiles[BottomSide]); + } + p->drawPixmap(r.right() - 8 + 1, r.bottom() - 8 + 1, m_tiles[BottomRightCorner]); + + const QRect contentRect = r.adjusted(LeftMargin + 1, TopMargin + 1, + -(RightMargin + 1), -(BottomMargin + 1)); + p->fillRect(contentRect, Qt::transparent); + } + + QPixmap m_tiles[NumTiles]; + }; +} + +void KPixmapModifier::scale(QPixmap& pixmap, const QSize& scaledSize) +{ +#if defined(Q_WS_X11) && defined(HAVE_XRENDER) + // Assume that the texture size limit is 2048x2048 + if ((pixmap.width() <= 2048) && (pixmap.height() <= 2048) && pixmap.x11PictureHandle()) { + QSize scaledPixmapSize = pixmap.size(); + scaledPixmapSize.scale(scaledSize, Qt::KeepAspectRatio); + + const qreal factor = scaledPixmapSize.width() / qreal(pixmap.width()); + + XTransform xform = {{ + { XDoubleToFixed(1 / factor), 0, 0 }, + { 0, XDoubleToFixed(1 / factor), 0 }, + { 0, 0, XDoubleToFixed(1) } + }}; + + QPixmap scaledPixmap(scaledPixmapSize); + scaledPixmap.fill(Qt::transparent); + + Display* dpy = QX11Info::display(); + + XRenderPictureAttributes attr; + attr.repeat = RepeatPad; + XRenderChangePicture(dpy, pixmap.x11PictureHandle(), CPRepeat, &attr); + + XRenderSetPictureFilter(dpy, pixmap.x11PictureHandle(), FilterBilinear, 0, 0); + XRenderSetPictureTransform(dpy, pixmap.x11PictureHandle(), &xform); + XRenderComposite(dpy, PictOpOver, pixmap.x11PictureHandle(), None, scaledPixmap.x11PictureHandle(), + 0, 0, 0, 0, 0, 0, scaledPixmap.width(), scaledPixmap.height()); + pixmap = scaledPixmap; + } else { + pixmap = pixmap.scaled(scaledSize, Qt::KeepAspectRatio, Qt::FastTransformation); + } +#else + pixmap = pixmap.scaled(scaledSize, Qt::KeepAspectRatio, Qt::FastTransformation); +#endif +} + +void KPixmapModifier::applyFrame(QPixmap& icon, const QSize& scaledSize) +{ + static TileSet tileSet; + + // Resize the icon to the maximum size minus the space required for the frame + const QSize size(scaledSize.width() - TileSet::LeftMargin - TileSet::RightMargin, + scaledSize.height() - TileSet::TopMargin - TileSet::BottomMargin); + scale(icon, size); + + QPixmap framedIcon(icon.size().width() + TileSet::LeftMargin + TileSet::RightMargin, + icon.size().height() + TileSet::TopMargin + TileSet::BottomMargin); + framedIcon.fill(Qt::transparent); + + QPainter painter; + painter.begin(&framedIcon); + painter.setCompositionMode(QPainter::CompositionMode_Source); + tileSet.paint(&painter, framedIcon.rect()); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + painter.drawPixmap(TileSet::LeftMargin, TileSet::TopMargin, icon); + + icon = framedIcon; +} + diff --git a/src/kitemviews/kpixmapmodifier_p.h b/src/kitemviews/kpixmapmodifier_p.h new file mode 100644 index 000000000..bca5e442a --- /dev/null +++ b/src/kitemviews/kpixmapmodifier_p.h @@ -0,0 +1,37 @@ +/*************************************************************************** + * Copyright (C) 2011 by Peter Penz <[email protected]> * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, 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 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 * + ***************************************************************************/ + +#ifndef KPIXMAPMODIFIER_H +#define KPIXMAPMODIFIER_H + +#include <libdolphin_export.h> + +class QPixmap; +class QSize; + +class LIBDOLPHINPRIVATE_EXPORT KPixmapModifier +{ +public: + static void scale(QPixmap& pixmap, const QSize& scaledSize); + static void applyFrame(QPixmap& icon, const QSize& scaledSize); +}; + +#endif + + |
