diff options
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 + + |
