File indexing completed on 2024-05-19 05:21:23

0001 /*
0002   This file is part of KDE Kontact.
0003 
0004   SPDX-FileCopyrightText: 2003 Cornelius Schumacher <schumacher@kde.org>
0005   SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
0006 
0007   SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include "iconsidepane.h"
0011 #include "mainwindow.h"
0012 #include "prefs.h"
0013 using namespace Kontact;
0014 
0015 #include <KontactInterface/Core>
0016 #include <KontactInterface/Plugin>
0017 
0018 #include <KIconLoader>
0019 #include <KLocalizedString>
0020 #include <QAction>
0021 #include <QActionGroup>
0022 #include <QApplication>
0023 #include <QIcon>
0024 
0025 #include <QCollator>
0026 #include <QDragEnterEvent>
0027 #include <QDragMoveEvent>
0028 #include <QDropEvent>
0029 #include <QLayout>
0030 #include <QPainter>
0031 #include <QSortFilterProxyModel>
0032 #include <QStringListModel>
0033 #include <QStyledItemDelegate>
0034 #include <QTimer>
0035 
0036 namespace Kontact
0037 {
0038 class SelectionModel : public QItemSelectionModel
0039 {
0040     Q_OBJECT
0041 public:
0042     SelectionModel(QAbstractItemModel *model, QObject *parent)
0043         : QItemSelectionModel(model, parent)
0044     {
0045     }
0046 
0047 public Q_SLOTS:
0048     void clear() override
0049     {
0050         // Don't allow the current selection to be cleared. QListView doesn't call to this method
0051         // nowadays, but just to cover of future change of implementation, since QTreeView does call
0052         // to this one when clearing the selection.
0053     }
0054 
0055     void select(const QModelIndex &index, QItemSelectionModel::SelectionFlags command) override
0056     {
0057         // Don't allow the current selection to be cleared
0058         if (!index.isValid() && (command & QItemSelectionModel::Clear)) {
0059             return;
0060         }
0061         QItemSelectionModel::select(index, command);
0062     }
0063 
0064     void select(const QItemSelection &selection, QItemSelectionModel::SelectionFlags command) override
0065     {
0066         // Don't allow the current selection to be cleared
0067         if (selection.isEmpty() && (command & QItemSelectionModel::Clear)) {
0068             return;
0069         }
0070         QItemSelectionModel::select(selection, command);
0071     }
0072 };
0073 
0074 class Model : public QStringListModel
0075 {
0076     Q_OBJECT
0077 public:
0078     enum SpecialRoles { PluginName = Qt::UserRole };
0079 
0080     explicit Model(Navigator *parentNavigator = nullptr)
0081         : QStringListModel(parentNavigator)
0082         , mNavigator(parentNavigator)
0083     {
0084     }
0085 
0086     void setPluginList(const QList<KontactInterface::Plugin *> &list)
0087     {
0088         pluginList = list;
0089     }
0090 
0091     [[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override
0092     {
0093         Qt::ItemFlags flags = QStringListModel::flags(index);
0094 
0095         flags &= ~Qt::ItemIsEditable;
0096 
0097         if (index.isValid()) {
0098             if (static_cast<KontactInterface::Plugin *>(index.internalPointer())->disabled()) {
0099                 flags &= ~Qt::ItemIsEnabled;
0100                 flags &= ~Qt::ItemIsSelectable;
0101                 flags &= ~Qt::ItemIsDropEnabled;
0102             } else {
0103                 flags |= Qt::ItemIsDropEnabled;
0104             }
0105         } else {
0106             flags &= ~Qt::ItemIsDropEnabled;
0107         }
0108 
0109         return flags;
0110     }
0111 
0112     [[nodiscard]] QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
0113     {
0114         Q_UNUSED(parent)
0115         if (row < 0 || row >= pluginList.count()) {
0116             return {};
0117         }
0118         return createIndex(row, column, pluginList[row]);
0119     }
0120 
0121     [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
0122     {
0123         if (!index.isValid() || !index.internalPointer()) {
0124             return {};
0125         }
0126 
0127         if (role == Qt::DisplayRole) {
0128             if (!mNavigator->showText()) {
0129                 return {};
0130             }
0131             return static_cast<KontactInterface::Plugin *>(index.internalPointer())->title();
0132         } else if (role == Qt::DecorationRole) {
0133             if (!mNavigator->showIcons()) {
0134                 return {};
0135             }
0136             return QIcon::fromTheme(static_cast<KontactInterface::Plugin *>(index.internalPointer())->icon());
0137         } else if (role == Qt::TextAlignmentRole) {
0138             return Qt::AlignCenter;
0139         } else if (role == Qt::ToolTipRole) {
0140             if (!mNavigator->showText()) {
0141                 return static_cast<KontactInterface::Plugin *>(index.internalPointer())->title();
0142             }
0143             return {};
0144         } else if (role == PluginName) {
0145             return static_cast<KontactInterface::Plugin *>(index.internalPointer())->identifier();
0146         }
0147         return QStringListModel::data(index, role);
0148     }
0149 
0150 private:
0151     QList<KontactInterface::Plugin *> pluginList;
0152     Navigator *const mNavigator;
0153 };
0154 
0155 class SortFilterProxyModel : public QSortFilterProxyModel
0156 {
0157     Q_OBJECT
0158 public:
0159     explicit SortFilterProxyModel(QObject *parent = nullptr)
0160         : QSortFilterProxyModel(parent)
0161     {
0162         setDynamicSortFilter(true);
0163         sort(0);
0164     }
0165 
0166 protected:
0167     [[nodiscard]] bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
0168     {
0169         auto leftPlugin = static_cast<KontactInterface::Plugin *>(left.internalPointer());
0170         auto rightPlugin = static_cast<KontactInterface::Plugin *>(right.internalPointer());
0171 
0172         if (leftPlugin->weight() == rightPlugin->weight()) {
0173             // Optimize it
0174             QCollator col;
0175             return col.compare(leftPlugin->title(), rightPlugin->title()) < 0;
0176         }
0177 
0178         return leftPlugin->weight() < rightPlugin->weight();
0179     }
0180 };
0181 
0182 class Delegate : public QStyledItemDelegate
0183 {
0184     Q_OBJECT
0185 public:
0186     explicit Delegate(Navigator *parentNavigator = nullptr)
0187         : QStyledItemDelegate(parentNavigator)
0188         , mNavigator(parentNavigator)
0189     {
0190     }
0191 
0192     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0193     {
0194         if (!index.isValid() || !index.internalPointer()) {
0195             return;
0196         }
0197 
0198         QStyleOptionViewItem opt(*static_cast<const QStyleOptionViewItem *>(&option));
0199         // optionCopy.state |= QStyle::State_Active;
0200         opt.decorationPosition = QStyleOptionViewItem::Top;
0201         const int height = 0;
0202         painter->save();
0203 
0204         mNavigator->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter);
0205 
0206         if (mNavigator->showIcons() && mNavigator->showText()) {
0207             opt.icon = index.data(Qt::DecorationRole).value<QIcon>();
0208             const int size = mNavigator->iconSize();
0209             const auto spacing = mNavigator->style()->pixelMetric(QStyle::PM_FocusFrameHMargin);
0210             const int textHeight = painter->fontMetrics().height();
0211 
0212             const int y = opt.rect.y() + (opt.rect.height() - size - spacing - textHeight) / 2;
0213 
0214             opt.icon.paint(painter, QRect(opt.rect.x(), y, opt.rect.width(), size), Qt::AlignCenter, QIcon::Normal, QIcon::On);
0215             painter->drawText(QRect(opt.rect.x(), y + size + spacing, opt.rect.width(), textHeight), index.data(Qt::DisplayRole).toString(), {Qt::AlignCenter});
0216         } else if (mNavigator->showIcons()) {
0217             opt.icon = index.data(Qt::DecorationRole).value<QIcon>();
0218             const int size = mNavigator->iconSize() + height;
0219             opt.decorationSize = QSize(size, size);
0220             opt.icon.paint(painter, opt.rect, Qt::AlignCenter, QIcon::Normal, QIcon::On);
0221         } else if (mNavigator->showText()) {
0222             painter->drawText(opt.rect, index.data(Qt::DisplayRole).toString(), {Qt::AlignCenter});
0223         }
0224         painter->restore();
0225     }
0226 
0227     [[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
0228     {
0229         if (!index.isValid() || !index.internalPointer()) {
0230             return {};
0231         }
0232 
0233         QStyleOptionViewItem optionCopy(*static_cast<const QStyleOptionViewItem *>(&option));
0234         optionCopy.decorationPosition = QStyleOptionViewItem::Top;
0235 
0236         optionCopy.decorationSize = mNavigator->showIcons() ? QSize(mNavigator->iconSize(), mNavigator->iconSize()) : QSize();
0237         optionCopy.textElideMode = Qt::ElideNone;
0238         return QStyledItemDelegate::sizeHint(optionCopy, index);
0239     }
0240 
0241 private:
0242     Navigator *const mNavigator;
0243 };
0244 }
0245 
0246 Navigator::Navigator(SidePaneBase *parent)
0247     : QListView(parent)
0248     , mSidePane(parent)
0249 {
0250     setViewport(new QWidget(this));
0251 
0252     setVerticalScrollMode(ScrollPerPixel);
0253     setHorizontalScrollMode(ScrollPerPixel);
0254     setFrameShape(QFrame::NoFrame);
0255 
0256     mIconSize = Prefs::self()->sidePaneIconSize();
0257     mShowIcons = Prefs::self()->sidePaneShowIcons();
0258     mShowText = Prefs::self()->sidePaneShowText();
0259 
0260     auto viewMode = new QActionGroup(this);
0261     connect(viewMode, &QActionGroup::triggered, this, &Navigator::slotActionTriggered);
0262 
0263     mShowIconsAction = new QAction(i18nc("@action:inmenu", "Show Icons Only"), this);
0264     mShowIconsAction->setCheckable(true);
0265     mShowIconsAction->setActionGroup(viewMode);
0266     mShowIconsAction->setChecked(!mShowText && mShowIcons);
0267     setHelpText(mShowIconsAction, i18nc("@info:status", "Show sidebar items with icons and without text"));
0268     mShowIconsAction->setWhatsThis(i18nc("@info:whatsthis", "Choose this option if you want the sidebar items to have icons without text."));
0269 
0270     mShowTextAction = new QAction(i18nc("@action:inmenu", "Show Text Only"), this);
0271     mShowTextAction->setCheckable(true);
0272     mShowTextAction->setActionGroup(viewMode);
0273     mShowTextAction->setChecked(mShowText && !mShowIcons);
0274     setHelpText(mShowTextAction, i18nc("@info:status", "Show sidebar items with text and without icons"));
0275     mShowTextAction->setWhatsThis(i18nc("@info:whatsthis", "Choose this option if you want the sidebar items to have text without icons."));
0276 
0277     mShowBothAction = new QAction(i18nc("@action:inmenu", "Show Icons && Text"), this);
0278     mShowBothAction->setCheckable(true);
0279     mShowBothAction->setActionGroup(viewMode);
0280     mShowBothAction->setChecked(mShowText && mShowIcons);
0281     setHelpText(mShowBothAction, i18nc("@info:status", "Show sidebar items with icons and text"));
0282     mShowBothAction->setWhatsThis(i18nc("@info:whatsthis", "Choose this option if you want the sidebar items to have icons and text."));
0283 
0284     auto iconSizeActionGroup = new QActionGroup(this);
0285     connect(iconSizeActionGroup, &QActionGroup::triggered, this, &Navigator::slotActionTriggered);
0286 
0287     mBigIconsAction = new QAction(i18nc("@action:inmenu", "Big Icons"), this);
0288     mBigIconsAction->setCheckable(true);
0289     mBigIconsAction->setActionGroup(iconSizeActionGroup);
0290     mBigIconsAction->setChecked(mIconSize == KIconLoader::SizeLarge);
0291     setHelpText(mBigIconsAction, i18nc("@info:status", "Show large size sidebar icons"));
0292     mBigIconsAction->setWhatsThis(i18nc("@info:whatsthis", "Choose this option if you want the sidebar icons to be extra big."));
0293 
0294     mNormalIconsAction = new QAction(i18nc("@action:inmenu", "Normal Icons"), this);
0295     mNormalIconsAction->setCheckable(true);
0296     mNormalIconsAction->setActionGroup(iconSizeActionGroup);
0297     mNormalIconsAction->setChecked(mIconSize == KIconLoader::SizeMedium);
0298     setHelpText(mNormalIconsAction, i18nc("@info:status", "Show normal size sidebar icons"));
0299     mNormalIconsAction->setWhatsThis(i18nc("@info:whatsthis", "Choose this option if you want the sidebar icons to be normal size."));
0300 
0301     mSmallIconsAction = new QAction(i18nc("@action:inmenu", "Small Icons"), this);
0302     mSmallIconsAction->setCheckable(true);
0303     mSmallIconsAction->setActionGroup(iconSizeActionGroup);
0304     mSmallIconsAction->setChecked(mIconSize == KIconLoader::SizeSmallMedium);
0305     setHelpText(mSmallIconsAction, i18nc("@info:status", "Show small size sidebar icons"));
0306     mSmallIconsAction->setWhatsThis(i18nc("@info:whatsthis", "Choose this option if you want the sidebar icons to be extra small."));
0307 
0308     mHideSideBarAction = new QAction(i18nc("@action:inmenu", "Hide Sidebar"), this);
0309     mHideSideBarAction->setCheckable(true);
0310     mHideSideBarAction->setChecked(false);
0311     setHelpText(mHideSideBarAction, i18nc("@info:status", "Hide the icon sidebar"));
0312     mHideSideBarAction->setWhatsThis(i18nc("@info:whatsthis", "Choose this option if you want to hide the icon sidebar. Press F9 to unhide."));
0313     connect(mHideSideBarAction, &QAction::triggered, this, &Navigator::slotHideSideBarTriggered);
0314 
0315     auto sep = new QAction(this);
0316     sep->setSeparator(true);
0317     auto sep2 = new QAction(this);
0318     sep2->setSeparator(true);
0319 
0320     QList<QAction *> actionList;
0321     actionList << mShowIconsAction << mShowTextAction << mShowBothAction << sep << mBigIconsAction << mNormalIconsAction << mSmallIconsAction << sep2
0322                << mHideSideBarAction;
0323 
0324     insertActions(nullptr, actionList);
0325 
0326     setContextMenuPolicy(Qt::ActionsContextMenu);
0327     setViewMode(ListMode);
0328     setItemDelegate(new Delegate(this));
0329     mModel = new Model(this);
0330     auto sortFilterProxyModel = new SortFilterProxyModel(this);
0331     sortFilterProxyModel->setSourceModel(mModel);
0332     setModel(sortFilterProxyModel);
0333     setSelectionModel(new SelectionModel(sortFilterProxyModel, this));
0334 
0335     setDragDropMode(DropOnly);
0336     viewport()->setAcceptDrops(true);
0337     setDropIndicatorShown(true);
0338 
0339     connect(selectionModel(), &QItemSelectionModel::currentChanged, this, &Navigator::slotCurrentChanged);
0340 }
0341 
0342 void Navigator::updatePlugins(const QList<KontactInterface::Plugin *> &plugins_)
0343 {
0344     QString currentPlugin;
0345     if (currentIndex().isValid()) {
0346         currentPlugin = currentIndex().model()->data(currentIndex(), Model::PluginName).toString();
0347     }
0348 
0349     QList<KontactInterface::Plugin *> pluginsToShow;
0350     for (KontactInterface::Plugin *plugin : plugins_) {
0351         if (plugin->showInSideBar()) {
0352             pluginsToShow << plugin;
0353         }
0354     }
0355 
0356     mModel->setPluginList(pluginsToShow);
0357 
0358     mModel->removeRows(0, mModel->rowCount());
0359     mModel->insertRows(0, pluginsToShow.count());
0360 
0361     // Restore the previous selected index, if any
0362     if (!currentPlugin.isEmpty()) {
0363         setCurrentPlugin(currentPlugin);
0364     }
0365 }
0366 
0367 void Navigator::setCurrentPlugin(const QString &plugin)
0368 {
0369     const int numberOfRows(model()->rowCount());
0370     for (int i = 0; i < numberOfRows; ++i) {
0371         const QModelIndex index = model()->index(i, 0);
0372         const QString pluginName = model()->data(index, Model::PluginName).toString();
0373 
0374         if (plugin == pluginName) {
0375             selectionModel()->setCurrentIndex(index, QItemSelectionModel::SelectCurrent);
0376             break;
0377         }
0378     }
0379 }
0380 
0381 QSize Navigator::sizeHint() const
0382 {
0383     //### TODO: We can cache this value, so this reply is faster. Since here we won't
0384     //          have too many elements, it is not that important. When caching this value
0385     //          make sure it is updated correctly when new rows have been added or
0386     //          removed. (ereslibre)
0387 
0388     int maxWidth = 0;
0389     const int numberOfRows(model()->rowCount());
0390     for (int i = 0; i < numberOfRows; ++i) {
0391         const QModelIndex index = model()->index(i, 0);
0392         maxWidth = qMax(maxWidth, sizeHintForIndex(index).width());
0393     }
0394 
0395     // Take vertical scrollbar into account
0396     maxWidth += qApp->style()->pixelMetric(QStyle::PM_ScrollBarExtent);
0397 
0398     const int viewHeight = QListView::sizeHint().height();
0399 
0400     const QSize size(maxWidth + rect().width() - contentsRect().width(), viewHeight);
0401     return size;
0402 }
0403 
0404 void Navigator::dragEnterEvent(QDragEnterEvent *event)
0405 {
0406     if (event->proposedAction() == Qt::IgnoreAction) {
0407         return;
0408     }
0409     event->acceptProposedAction();
0410 }
0411 
0412 void Navigator::dragMoveEvent(QDragMoveEvent *event)
0413 {
0414     if (event->proposedAction() == Qt::IgnoreAction) {
0415         return;
0416     }
0417     const QModelIndex dropIndex = indexAt(event->position().toPoint());
0418 
0419     if (!dropIndex.isValid() || !(dropIndex.model()->flags(dropIndex) & Qt::ItemIsEnabled)) {
0420         event->setAccepted(false);
0421         return;
0422     } else {
0423         const QModelIndex sourceIndex = static_cast<const QSortFilterProxyModel *>(model())->mapToSource(dropIndex);
0424         auto plugin = static_cast<KontactInterface::Plugin *>(sourceIndex.internalPointer());
0425         if (!plugin->canDecodeMimeData(event->mimeData())) {
0426             event->setAccepted(false);
0427             return;
0428         }
0429     }
0430 
0431     event->acceptProposedAction();
0432 }
0433 
0434 void Navigator::dropEvent(QDropEvent *event)
0435 {
0436     if (event->proposedAction() == Qt::IgnoreAction) {
0437         return;
0438     }
0439 
0440     const QModelIndex dropIndex = indexAt(event->position().toPoint());
0441 
0442     if (!dropIndex.isValid()) {
0443         return;
0444     } else {
0445         const QModelIndex sourceIndex = static_cast<const QSortFilterProxyModel *>(model())->mapToSource(dropIndex);
0446         auto plugin = static_cast<KontactInterface::Plugin *>(sourceIndex.internalPointer());
0447         plugin->processDropEvent(event);
0448     }
0449 }
0450 
0451 void Navigator::setHelpText(QAction *act, const QString &text)
0452 {
0453     act->setStatusTip(text);
0454     act->setToolTip(text);
0455     if (act->whatsThis().isEmpty()) {
0456         act->setWhatsThis(text);
0457     }
0458 }
0459 
0460 void Navigator::showEvent(QShowEvent *event)
0461 {
0462     parentWidget()->setMaximumWidth(sizeHint().width());
0463     parentWidget()->setMinimumWidth(sizeHint().width());
0464 
0465     QListView::showEvent(event);
0466 }
0467 
0468 void Navigator::slotCurrentChanged(const QModelIndex &current)
0469 {
0470     if (!current.isValid() || !current.internalPointer() || !(current.model()->flags(current) & Qt::ItemIsEnabled)) {
0471         return;
0472     }
0473 
0474     QModelIndex source = static_cast<const QSortFilterProxyModel *>(current.model())->mapToSource(current);
0475 
0476     Q_EMIT pluginActivated(static_cast<KontactInterface::Plugin *>(source.internalPointer()));
0477 }
0478 
0479 void Navigator::slotActionTriggered(QAction *object)
0480 {
0481     bool checked = object->isChecked();
0482     if (object == mShowIconsAction) {
0483         mShowIcons = checked;
0484         mShowText = !checked;
0485     } else if (object == mShowTextAction) {
0486         mShowIcons = !checked;
0487         mShowText = checked;
0488     } else if (object == mShowBothAction) {
0489         mShowIcons = checked;
0490         mShowText = checked;
0491     } else if (object == mBigIconsAction) {
0492         mIconSize = KIconLoader::SizeLarge;
0493     } else if (object == mNormalIconsAction) {
0494         mIconSize = KIconLoader::SizeMedium;
0495     } else if (object == mSmallIconsAction) {
0496         mIconSize = KIconLoader::SizeSmallMedium;
0497     }
0498 
0499     Prefs::self()->setSidePaneIconSize(mIconSize);
0500     Prefs::self()->setSidePaneShowIcons(mShowIcons);
0501     Prefs::self()->setSidePaneShowText(mShowText);
0502 
0503     reset();
0504 
0505     QTimer::singleShot(0, this, &Navigator::updateNavigatorSize);
0506 }
0507 
0508 void Navigator::slotHideSideBarTriggered()
0509 {
0510     if (mainWindow()) {
0511         mainWindow()->showHideSideBar(false);
0512         mHideSideBarAction->setChecked(false);
0513     }
0514 }
0515 
0516 void Navigator::updateNavigatorSize()
0517 {
0518     parentWidget()->setMaximumWidth(sizeHint().width());
0519     parentWidget()->setMinimumWidth(sizeHint().width());
0520     update();
0521 }
0522 
0523 IconSidePane::IconSidePane(KontactInterface::Core *core, QWidget *parent)
0524     : SidePaneBase(core, parent)
0525 {
0526     mNavigator = new Navigator(this);
0527     layout()->addWidget(mNavigator);
0528     mNavigator->setFocusPolicy(Qt::NoFocus);
0529     mNavigator->setMainWindow(qobject_cast<MainWindow *>(core));
0530     connect(mNavigator, &Navigator::pluginActivated, this, &IconSidePane::pluginSelected);
0531 }
0532 
0533 IconSidePane::~IconSidePane() = default;
0534 
0535 void IconSidePane::setCurrentPlugin(const QString &plugin)
0536 {
0537     mNavigator->setCurrentPlugin(plugin);
0538 }
0539 
0540 void IconSidePane::updatePlugins()
0541 {
0542     mNavigator->updatePlugins(core()->pluginList());
0543 }
0544 
0545 void IconSidePane::resizeEvent(QResizeEvent *event)
0546 {
0547     Q_UNUSED(event)
0548     const int newWidth(mNavigator->sizeHint().width());
0549     setFixedWidth(newWidth);
0550     mNavigator->setFixedWidth(newWidth);
0551 }
0552 
0553 #include "iconsidepane.moc"
0554 
0555 #include "moc_iconsidepane.cpp"