diff --git a/src/filewidgets/kfileplacesitem.cpp b/src/filewidgets/kfileplacesitem.cpp --- a/src/filewidgets/kfileplacesitem.cpp +++ b/src/filewidgets/kfileplacesitem.cpp @@ -108,6 +108,25 @@ } else { m_text = bookmark.text(); } + + const GroupType type = groupType(); + switch (type) { + case PlacesType: + m_groupName = i18nc("@item", "Places"); + break; + case RecentlySavedType: + m_groupName = i18nc("@item", "Recently Saved"); + break; + case SearchForType: + m_groupName = i18nc("@item", "Search For"); + break; + case DevicesType: + m_groupName = i18nc("@item", "Devices"); + break; + default: + Q_ASSERT(false); + break; + } } Solid::Device KFilePlacesItem::device() const @@ -131,8 +150,11 @@ QVariant KFilePlacesItem::data(int role) const { - QVariant returnData; + if (role == KFilePlacesModel::GroupRole) { + return QVariant(m_groupName); + } + QVariant returnData; if (role != KFilePlacesModel::HiddenRole && role != Qt::BackgroundRole && isDevice()) { returnData = deviceData(role); } else { @@ -142,6 +164,30 @@ return returnData; } +KFilePlacesItem::GroupType KFilePlacesItem::groupType() const +{ + if (!isDevice()) { + const QString protocol = bookmark().url().scheme(); + if (protocol == QLatin1String("timeline")) { + return RecentlySavedType; + } + + if (protocol.contains(QLatin1String("search"))) { + return SearchForType; + } + + if (protocol == QLatin1String("bluetooth") || + protocol == QLatin1String("obexftp") || + protocol == QLatin1String("kdeconnect")) { + return DevicesType; + } + + return PlacesType; + } + + return DevicesType; +} + QVariant KFilePlacesItem::bookmarkData(int role) const { KBookmark b = bookmark(); diff --git a/src/filewidgets/kfileplacesitem_p.h b/src/filewidgets/kfileplacesitem_p.h --- a/src/filewidgets/kfileplacesitem_p.h +++ b/src/filewidgets/kfileplacesitem_p.h @@ -41,6 +41,14 @@ { Q_OBJECT public: + enum GroupType + { + PlacesType, + SearchForType, + RecentlySavedType, + DevicesType + }; + KFilePlacesItem(KBookmarkManager *manager, const QString &address, const QString &udi = QString()); @@ -53,6 +61,7 @@ void setBookmark(const KBookmark &bookmark); Solid::Device device() const; QVariant data(int role) const; + GroupType groupType() const; static KBookmark createBookmark(KBookmarkManager *manager, const QString &label, @@ -94,6 +103,7 @@ mutable QPointer m_mtp; QString m_iconPath; QStringList m_emblems; + QString m_groupName; }; #endif diff --git a/src/filewidgets/kfileplacesmodel.h b/src/filewidgets/kfileplacesmodel.h --- a/src/filewidgets/kfileplacesmodel.h +++ b/src/filewidgets/kfileplacesmodel.h @@ -47,7 +47,8 @@ HiddenRole = 0x0741CAAC, SetupNeededRole = 0x059A935D, FixedDeviceRole = 0x332896C1, - CapacityBarRecommendedRole = 0x1548C5C4 + CapacityBarRecommendedRole = 0x1548C5C4, + GroupRole = 0x0a5b64ee }; KFilePlacesModel(QObject *parent = nullptr); diff --git a/src/filewidgets/kfileplacesmodel.cpp b/src/filewidgets/kfileplacesmodel.cpp --- a/src/filewidgets/kfileplacesmodel.cpp +++ b/src/filewidgets/kfileplacesmodel.cpp @@ -470,6 +470,12 @@ } } + // return a sorted list based on groups + qStableSort(items.begin(), items.end(), + [](KFilePlacesItem *itemA, KFilePlacesItem *itemB) { + return (itemA->groupType() < itemB->groupType()); + }); + return items; } diff --git a/src/filewidgets/kfileplacesview.h b/src/filewidgets/kfileplacesview.h --- a/src/filewidgets/kfileplacesview.h +++ b/src/filewidgets/kfileplacesview.h @@ -78,6 +78,8 @@ void dragMoveEvent(QDragMoveEvent *event) Q_DECL_OVERRIDE; void dropEvent(QDropEvent *event) Q_DECL_OVERRIDE; void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; + void startDrag(Qt::DropActions supportedActions) Q_DECL_OVERRIDE; + void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE; protected Q_SLOTS: void rowsInserted(const QModelIndex &parent, int start, int end) Q_DECL_OVERRIDE; diff --git a/src/filewidgets/kfileplacesview.cpp b/src/filewidgets/kfileplacesview.cpp --- a/src/filewidgets/kfileplacesview.cpp +++ b/src/filewidgets/kfileplacesview.cpp @@ -84,6 +84,10 @@ qreal contentsOpacity(const QModelIndex &index) const; + bool pointIsHeaderArea(const QPoint &pos); + + void setDragModeCount(int count); + private: KFilePlacesView *m_view; int m_iconSize; @@ -100,6 +104,22 @@ QMap m_timeLineMap; QMap m_timeLineInverseMap; + + mutable int m_sectionHeaderHeight; + + mutable int m_dragModeCount; + + QString groupNameFromIndex(const QModelIndex &index) const; + QModelIndex previousVisibleIndex(const QModelIndex &index) const; + bool indexIsSectionHeader(const QModelIndex &index) const; + void drawSectionHeader(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const; + + QColor textColor(const QStyleOption &option) const; + QColor baseColor(const QStyleOption &option) const; + QColor mixedColor(const QColor& c1, const QColor& c2, int c1Percent) const; + }; KFilePlacesViewDelegate::KFilePlacesViewDelegate(KFilePlacesView *parent) : @@ -110,7 +130,9 @@ m_appearingOpacity(0.0), m_disappearingIconSize(0), m_disappearingOpacity(0.0), - m_showHoverIndication(true) + m_showHoverIndication(true), + m_sectionHeaderHeight(-1), + m_dragModeCount(0) { } @@ -128,26 +150,48 @@ iconSize = m_disappearingIconSize; } - const KFilePlacesModel *filePlacesModel = static_cast(index.model()); - Solid::Device device = filePlacesModel->deviceForIndex(index); + int height = option.fontMetrics.height() / 2 + qMax(iconSize, option.fontMetrics.height()); + + if (indexIsSectionHeader(index)) { + m_sectionHeaderHeight = option.fontMetrics.height(); + height += m_sectionHeaderHeight; + } - return QSize(option.rect.width(), option.fontMetrics.height() / 2 + qMax(iconSize, option.fontMetrics.height())); + return QSize(option.rect.width(), height); } void KFilePlacesViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { painter->save(); + QStyleOptionViewItem opt = option; + + // draw header when necessary + if (indexIsSectionHeader(index)) { + // if is drawing the floating element used by drag/drop, does not draw the header + if (m_dragModeCount == 0) { + drawSectionHeader(painter, opt, index); + } + painter->translate(0, option.fontMetrics.height() / 2); + opt.rect.translate(0, option.fontMetrics.height() / 2); + opt.rect.setHeight(opt.rect.height() - option.fontMetrics.height()); + } + + if (m_dragModeCount > 0) { + m_dragModeCount--; + } + + // draw item if (m_appearingItems.contains(index)) { painter->setOpacity(m_appearingOpacity); } else if (m_disappearingItems.contains(index)) { painter->setOpacity(m_disappearingOpacity); } - QStyleOptionViewItem opt = option; if (!m_showHoverIndication) { opt.state &= ~QStyle::State_MouseOver; } + QApplication::style()->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter); const KFilePlacesModel *placesModel = static_cast(index.model()); @@ -307,6 +351,118 @@ return 0; } +bool KFilePlacesViewDelegate::pointIsHeaderArea(const QPoint &pos) +{ + // we only accept drag events starting from item boddy, ignore drag request from header + QModelIndex index = m_view->indexAt(pos); + if (!index.isValid()) { + return false; + } + + if (indexIsSectionHeader(index)) { + const QRect vRect = m_view->visualRect(index); + const int delegateY = pos.y() - vRect.y(); + if (delegateY <= m_sectionHeaderHeight) { + return true; + } + } + return false; +} + +void KFilePlacesViewDelegate::setDragModeCount(int count) +{ + m_dragModeCount = count; +} + +QString KFilePlacesViewDelegate::groupNameFromIndex(const QModelIndex &index) const +{ + if (index.isValid()) { + return index.data(KFilePlacesModel::GroupRole).toString(); + } else { + return QString(); + } +} + +QModelIndex KFilePlacesViewDelegate::previousVisibleIndex(const QModelIndex &index) const +{ + if (index.row() == 0) { + return QModelIndex(); + } + + const QAbstractItemModel *model = index.model(); + QModelIndex prevIndex = model->index(index.row() - 1, index.column(), index.parent()); + + while (m_view->isRowHidden(prevIndex.row())) { + if (prevIndex.row() == 0) { + return QModelIndex(); + } + prevIndex = model->index(prevIndex.row() - 1, index.column(), index.parent()); + } + + return prevIndex; +} + +bool KFilePlacesViewDelegate::indexIsSectionHeader(const QModelIndex &index) const +{ + if (m_view->isRowHidden(index.row())) { + return false; + } + + if (index.row() == 0) { + return true; + } + + const auto groupName = groupNameFromIndex(index); + const auto previousGroupName = groupNameFromIndex(previousVisibleIndex(index)); + return groupName != previousGroupName; +} + +void KFilePlacesViewDelegate::drawSectionHeader(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + const QString category = index.data(KFilePlacesModel::GroupRole).toString(); + if (m_sectionHeaderHeight == -1) { + m_sectionHeaderHeight = option.fontMetrics.height(); + } + + QRect textRect(option.rect); + textRect.setLeft(textRect.left() + 3); + textRect.setHeight(m_sectionHeaderHeight); + + painter->save(); + + // based on dolphoin colors + const QColor c1 = textColor(option); + const QColor c2 = baseColor(option); + QColor penColor = mixedColor(c1, c2, 60); + + painter->setPen(penColor); + painter->drawText(textRect, Qt::AlignLeft | Qt::AlignBottom, category); + painter->restore(); +} + +QColor KFilePlacesViewDelegate::textColor(const QStyleOption &option) const +{ + const QPalette::ColorGroup group = m_view->isActiveWindow() ? QPalette::Active : QPalette::Inactive; + return option.palette.color(group, QPalette::WindowText); +} + +QColor KFilePlacesViewDelegate::baseColor(const QStyleOption &option) const +{ + const QPalette::ColorGroup group = m_view->isActiveWindow() ? QPalette::Active : QPalette::Inactive; + return option.palette.color(group, QPalette::Window); +} + +QColor KFilePlacesViewDelegate::mixedColor(const QColor& c1, const QColor& c2, int c1Percent) const +{ + Q_ASSERT(c1Percent >= 0 && c1Percent <= 100); + + const int c2Percent = 100 - c1Percent; + return QColor((c1.red() * c1Percent + c2.red() * c2Percent) / 100, + (c1.green() * c1Percent + c2.green() * c2Percent) / 100, + (c1.blue() * c1Percent + c2.blue() * c2Percent) / 100); +} + + class KFilePlacesView::Private { public: @@ -337,6 +493,7 @@ bool insertBelow(const QRect &itemRect, const QPoint &pos) const; int insertIndicatorHeight(int itemHeight) const; void fadeCapacityBar(const QModelIndex &index, FadeType fadeType); + int sectionsCount() const; void _k_placeClicked(const QModelIndex &index); void _k_placeEntered(const QModelIndex &index); @@ -577,7 +734,7 @@ QAction *add = nullptr; QAction *mainSeparator = nullptr; - if (index.isValid()) { + if (!delegate->pointIsHeaderArea(event->pos()) && index.isValid()) { if (!placesModel->isDevice(index)) { if (placesModel->url(index).toString() == QLatin1String("trash:/")) { emptyTrash = menu.addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18nc("@action:inmenu", "Empty Trash")); @@ -835,6 +992,26 @@ } } +void KFilePlacesView::startDrag(Qt::DropActions supportedActions) +{ + KFilePlacesViewDelegate *delegate = dynamic_cast(itemDelegate()); + + delegate->setDragModeCount(selectedIndexes().size()); + QListView::startDrag(supportedActions); +} + +void KFilePlacesView::mousePressEvent(QMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + KFilePlacesViewDelegate *delegate = dynamic_cast(itemDelegate()); + // does not accept drags from section header area + if (delegate->pointIsHeaderArea(event->pos())) { + return; + } + } + QListView::mousePressEvent(event); +} + void KFilePlacesView::setModel(QAbstractItemModel *model) { QListView::setModel(model); @@ -968,7 +1145,8 @@ const int margin = q->style()->pixelMetric(QStyle::PM_FocusFrameHMargin, nullptr, q) + 1; const int maxWidth = q->viewport()->width() - textWidth - 4 * margin - 1; - const int maxHeight = ((q->height() - (fm.height() / 2) * rowCount) / rowCount) - 1; + int maxHeight = ((q->height() - (fm.height() / 2) * rowCount) / rowCount) - 1; + maxHeight += sectionsCount() * fm.height(); int size = qMin(maxHeight, maxWidth); @@ -1070,6 +1248,20 @@ timeLine->start(); } +int KFilePlacesView::Private::sectionsCount() const +{ + QSet sections; + + for(int i = 0; i < q->model()->rowCount(); i++) { + if (!q->isRowHidden(i)) { + QModelIndex index = q->model()->index(i, 0); + sections << index.data(KFilePlacesModel::GroupRole).toString(); + } + } + + return sections.size(); +} + void KFilePlacesView::Private::_k_placeClicked(const QModelIndex &index) { KFilePlacesModel *placesModel = qobject_cast(q->model()); @@ -1203,4 +1395,4 @@ #include "moc_kfileplacesview.cpp" #include "moc_kfileplacesview_p.cpp" -#include "kfileplacesview.moc" \ No newline at end of file +#include "kfileplacesview.moc"