File indexing completed on 2024-05-26 04:30:45

0001 /*
0002  *  SPDX-FileCopyrightText: 2015 Dmitry Kazakov <dimula73@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "kis_color_filter_combo.h"
0008 
0009 #include <klocalizedstring.h>
0010 #include <QStylePainter>
0011 #include <QtCore/qmath.h>
0012 #include <QApplication>
0013 #include <QProxyStyle>
0014 #include <QStyleOption>
0015 #include <QSortFilterProxyModel>
0016 #include <QStandardItemModel>
0017 #include <QListView>
0018 #include <QMouseEvent>
0019 
0020 #include <QStyleFactory>
0021 
0022 
0023 
0024 #include "kis_node_view_color_scheme.h"
0025 #include "kis_debug.h"
0026 #include "kis_icon_utils.h"
0027 #include "krita_utils.h"
0028 #include "kis_node.h"
0029 
0030 enum AdditionalRoles {
0031     OriginalLabelIndex = Qt::UserRole + 1000
0032 };
0033 
0034 
0035 struct LabelFilteringModel : public QSortFilterProxyModel
0036 {
0037     LabelFilteringModel(QObject *parent) : QSortFilterProxyModel(parent) {}
0038 
0039     bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override {
0040         const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
0041         const int labelIndex = index.data(OriginalLabelIndex).toInt();
0042 
0043 
0044         return labelIndex < 0 || m_acceptedLabels.contains(labelIndex);
0045     }
0046 
0047     void setAcceptedLabels(const QSet<int> &value) {
0048         m_acceptedLabels = value;
0049         invalidateFilter();
0050     }
0051 
0052 private:
0053     QSet<int> m_acceptedLabels;
0054 };
0055 
0056 
0057 class ComboEventFilter : public QObject
0058 {
0059 public:
0060     ComboEventFilter(KisColorFilterCombo *parent) : m_parent(parent), m_buttonPressed(false) {}
0061 
0062 protected:
0063     bool eventFilter(QObject *obj, QEvent *event) override {
0064         if (event->type() == QEvent::Leave) {
0065             m_buttonPressed = false;
0066 
0067         } else if (event->type() == QEvent::MouseButtonPress) {
0068             QMouseEvent *mevent = static_cast<QMouseEvent*>(event);
0069             m_buttonPressed = mevent->button() == Qt::LeftButton;
0070 
0071         } else if (event->type() == QEvent::MouseButtonRelease) {
0072             QMouseEvent *mevent = static_cast<QMouseEvent*>(event);
0073             QModelIndex index = m_parent->view()->indexAt(mevent->pos());
0074             if (!index.isValid()) return false;
0075 
0076             /**
0077              * We should eat the first event that arrives exactly when
0078              * the drop down appears on screen.
0079              */
0080             if (!m_buttonPressed) return true;
0081 
0082             const bool toUncheckedState = index.data(Qt::CheckStateRole) == Qt::Checked;
0083 
0084             if (toUncheckedState) {
0085                 m_parent->model()->setData(index, Qt::Unchecked, Qt::CheckStateRole);
0086             } else {
0087                 m_parent->model()->setData(index, Qt::Checked, Qt::CheckStateRole);
0088             }
0089 
0090             if (index.data(OriginalLabelIndex).toInt() == -1) {
0091                 for (int i = 0; i < m_parent->model()->rowCount(); i++) {
0092                     const QModelIndex &other = m_parent->model()->index(i, 0);
0093                     if (other.data(OriginalLabelIndex) != -1) {
0094                         m_parent->model()->setData(other, toUncheckedState ? Qt::Unchecked : Qt::Checked, Qt::CheckStateRole);
0095                     }
0096                 }
0097             } else {
0098                 bool prevChecked = false;
0099                 bool checkedVaries = false;
0100                 QModelIndex allLabelsIndex;
0101 
0102                 for (int i = 0; i < m_parent->model()->rowCount(); i++) {
0103                     const QModelIndex &other = m_parent->model()->index(i, 0);
0104                     if (other.data(OriginalLabelIndex) != -1) {
0105                         const bool currentChecked = other.data(Qt::CheckStateRole) == Qt::Checked;
0106 
0107                         if (i == 0) {
0108                             prevChecked = currentChecked;
0109                         } else {
0110                             if (prevChecked != currentChecked) {
0111                                 checkedVaries = true;
0112                                 break;
0113                             }
0114                         }
0115                     } else {
0116                         allLabelsIndex = other;
0117                     }
0118                 }
0119 
0120                 const bool allLabelsIndexShouldBeChecked =
0121                     prevChecked && !checkedVaries;
0122 
0123                 if (allLabelsIndexShouldBeChecked !=
0124                     (allLabelsIndex.data(Qt::CheckStateRole) == Qt::Checked)) {
0125 
0126                     m_parent->model()->setData(allLabelsIndex, allLabelsIndexShouldBeChecked ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
0127                 }
0128             }
0129 
0130             emit m_parent->selectedColorsChanged();
0131 
0132             m_buttonPressed = false;
0133             return true;
0134         }
0135 
0136         return QObject::eventFilter(obj, event);
0137     }
0138 
0139 private:
0140     KisColorFilterCombo *m_parent;
0141     bool m_buttonPressed;
0142 };
0143 
0144 class FullSizedListView : public QListView
0145 {
0146 public:
0147     QSize sizeHint() const override {
0148         return contentsSize();
0149     }
0150 };
0151 
0152 class PopupComboBoxStyle : public QProxyStyle
0153 {
0154 public:
0155     PopupComboBoxStyle(QStyle *baseStyle = nullptr) : QProxyStyle(baseStyle) {}
0156 
0157     int styleHint(QStyle::StyleHint hint, const QStyleOption *option, const QWidget *widget, QStyleHintReturn *returnData) const override
0158     {
0159         // This flag makes ComboBox popup float on top of its parent ComboBox, like in Fusion style.
0160         // Only when this hint is set will Qt respect combobox popup size hints, otherwise the popup
0161         // can never exceed the width of its parent ComboBox, like in Breeze style.
0162         if (hint == QStyle::SH_ComboBox_Popup) {
0163             return true;
0164         }
0165 
0166         return QProxyStyle::styleHint(hint, option, widget, returnData);
0167     }
0168 };
0169 
0170 struct KisColorFilterCombo::Private
0171 {
0172     LabelFilteringModel *filteringModel;
0173     /**
0174       * if the combobox is in the filter mode
0175       *     (when no colors are selected)
0176       *     it will show the filter icon ("view-filter")
0177       *     otherwise it will show tag icon ("tag")
0178       */
0179     bool filterMode {true};
0180     /**
0181       * If the combobox is in the circle mode,
0182       *     it will show the selected colors as circle
0183       *     otherwise it will show it in a rectangle
0184       */
0185     bool circleMode {true};
0186 };
0187 
0188 KisColorFilterCombo::KisColorFilterCombo(QWidget *parent, bool filterMode, bool circleMode)
0189     : QComboBox(parent),
0190       m_d(new Private)
0191 {
0192     m_d->filterMode = filterMode;
0193     m_d->circleMode = circleMode;
0194 
0195 
0196     QStandardItemModel *newModel = new QStandardItemModel(this);
0197     setModel(newModel);
0198 
0199     QStyle* newStyle = QStyleFactory::create(style()->objectName());
0200     // proxy style steals the ownership of the style and deletes it later
0201     PopupComboBoxStyle *proxyStyle = new PopupComboBoxStyle(newStyle);
0202 
0203     proxyStyle->setParent(this);
0204     setStyle(proxyStyle);
0205 
0206     setView(new FullSizedListView);
0207     m_eventFilters.append(new ComboEventFilter(this));
0208     m_eventFilters.append(new ComboEventFilter(this));
0209 
0210     view()->installEventFilter(m_eventFilters[0]);
0211     view()->viewport()->installEventFilter(m_eventFilters[1]);
0212 
0213     KisNodeViewColorScheme scm;
0214 
0215     QStandardItem* item = new QStandardItem(i18nc("combo box: show all layers", "All"));
0216     item->setCheckable(true);
0217     item->setCheckState(Qt::Unchecked);
0218     item->setData(QColor(Qt::transparent), Qt::BackgroundRole);
0219     item->setData(int(-1), OriginalLabelIndex);
0220     item->setData(QSize(30, scm.rowHeight()), Qt::SizeHintRole);
0221     newModel->appendRow(item);
0222 
0223     int labelIndex = 0;
0224     foreach (const QColor &color, scm.allColorLabels()) {
0225         const QString title = color.alpha() > 0 ? "" : i18nc("combo box: select all layers without a label", "No Label");
0226 
0227         QStandardItem* item = new QStandardItem(title);
0228         item->setCheckable(true);
0229         item->setCheckState(Qt::Unchecked);
0230         item->setData(color, Qt::BackgroundRole);
0231         item->setData(labelIndex, OriginalLabelIndex);
0232         item->setData(QSize(30, scm.rowHeight()), Qt::SizeHintRole);
0233         newModel->appendRow(item);
0234 
0235         labelIndex++;
0236     }
0237 
0238     m_d->filteringModel = new LabelFilteringModel(this);
0239     QAbstractItemModel *originalModel = model();
0240     originalModel->setParent(m_d->filteringModel);
0241 
0242     m_d->filteringModel->setSourceModel(originalModel);
0243     setModel(m_d->filteringModel);
0244 }
0245 
0246 KisColorFilterCombo::~KisColorFilterCombo()
0247 {
0248     qDeleteAll(m_eventFilters);
0249 }
0250 
0251 void collectAvailableLabels(KisNodeSP root, QSet<int> *labels)
0252 {
0253     labels->insert(root->colorLabelIndex());
0254 
0255     KisNodeSP node = root->firstChild();
0256     while (node) {
0257         collectAvailableLabels(node, labels);
0258         node = node->nextSibling();
0259     }
0260 }
0261 
0262 void KisColorFilterCombo::updateAvailableLabels(KisNodeSP rootNode)
0263 {
0264     QSet<int> labels;
0265     if (!rootNode.isNull()) {
0266         collectAvailableLabels(rootNode, &labels);
0267     }
0268 
0269     updateAvailableLabels(labels);
0270 }
0271 
0272 void KisColorFilterCombo::updateAvailableLabels(const QSet<int> &labels)
0273 {
0274     m_d->filteringModel->setAcceptedLabels(labels);
0275 }
0276 
0277 void KisColorFilterCombo::setModes(bool filterMode, bool circleMode)
0278 {
0279     m_d->filterMode = filterMode;
0280     m_d->circleMode = circleMode;
0281 }
0282 
0283 QList<int> KisColorFilterCombo::selectedColors() const
0284 {
0285     QList<int> colors;
0286     for (int i = 0; i < model()->rowCount(); i++) {
0287         const QModelIndex &other = model()->index(i, 0);
0288         const int label = other.data(OriginalLabelIndex).toInt();
0289 
0290         if (label != -1 &&
0291             other.data(Qt::CheckStateRole) == Qt::Checked) {
0292 
0293             colors << label;
0294         }
0295     }
0296     return colors;
0297 }
0298 
0299 void KisColorFilterCombo::paintColorPie(QStylePainter &painter, const QPalette& palette, const QList<int> &selectedColors, const QRect &rect, const int &baseSize)
0300 {
0301     KisNodeViewColorScheme scm;
0302     const QPen oldPen = painter.pen();
0303     const QBrush oldBrush = painter.brush();
0304     const int border = 0;
0305     QColor shadowColor = palette.shadow().color();
0306     shadowColor.setAlpha(64);
0307 
0308     QRect pieRect(0, 0, baseSize - 2 * border, baseSize - 2 * border);
0309     pieRect.moveCenter(rect.center());
0310 
0311     if (selectedColors.size() == 1) {
0312         const int currentLabel = selectedColors.first();
0313         const QColor currentColor = scm.colorFromLabelIndex(currentLabel);
0314         const QBrush brush = QBrush(currentColor);
0315         painter.setBrush(brush);
0316         painter.setPen(QPen(shadowColor, 1));
0317 
0318         if (currentColor.alpha() > 0) {
0319             painter.drawEllipse(rect);
0320         } else if (currentLabel == 0) {
0321             QColor white = Qt::white;
0322             QColor grey = QColor(220,220,220);
0323             painter.setBrush(QBrush(shadowColor));
0324             painter.setRenderHint(QPainter::Antialiasing);
0325             painter.drawEllipse(rect);
0326             const int step = 16 * 360 / 4;
0327             const int checkerSteps = 4;
0328 
0329             for (int i = 0; i < checkerSteps; i++) {
0330                 QBrush checkerBrush = QBrush((i % 2) ? grey : white);
0331                 painter.setPen(Qt::NoPen);
0332                 painter.setBrush(checkerBrush);
0333                 painter.drawPie(pieRect, step * i, step);
0334             }
0335 
0336         }
0337     } else {
0338         const int numColors = selectedColors.size();
0339         const int step = 16 * 360 / numColors;
0340 
0341         painter.setPen(QPen(shadowColor, 1));
0342         painter.setBrush(QColor(0,0,0,0));
0343         painter.setRenderHint(QPainter::Antialiasing);
0344         painter.drawEllipse(rect);
0345         for (int i = 0; i < numColors; i++) {
0346             QColor color = scm.colorFromLabelIndex(selectedColors[i]);
0347             QBrush brush = color.alpha() > 0 ? QBrush(color) : QBrush(Qt::black, Qt::Dense4Pattern);
0348             painter.setPen(Qt::NoPen);
0349             painter.setBrush(brush);
0350 
0351             painter.drawPie(pieRect, step * i, step);
0352         }
0353     }
0354 
0355     painter.setPen(oldPen);
0356     painter.setBrush(oldBrush);
0357 }
0358 
0359 
0360 void KisColorFilterCombo::paintEvent(QPaintEvent *event)
0361 {
0362     Q_UNUSED(event);
0363 
0364     QStylePainter painter(this);
0365     painter.setPen(palette().color(QPalette::Text));
0366 
0367     // draw the combobox frame, focusrect and selected etc.
0368     QStyleOptionComboBox opt;
0369     initStyleOption(&opt);
0370     painter.drawComplexControl(QStyle::CC_ComboBox, opt);
0371 
0372 
0373     {
0374         const QRect editRect = style()->subControlRect(QStyle::CC_ComboBox, &opt, QStyle::SC_ComboBoxEditField, this);
0375         const int size = qMin(editRect.width(), editRect.height());
0376 
0377         const QList<int> selectedColors = this->selectedColors();
0378 
0379         if (selectedColors.size() == 0 || selectedColors.size() == model()->rowCount() - 1) {
0380             QIcon icon = KisIconUtils::loadIcon(m_d->filterMode ? "view-filter" : "tag");
0381             QPixmap pixmap = icon.pixmap(QSize(size, size), !isEnabled() ? QIcon::Disabled : QIcon::Normal);
0382             painter.drawPixmap(editRect.right() - size, editRect.top(), pixmap);
0383 
0384         } else {
0385             const int numColors = selectedColors.size();
0386             if (m_d->circleMode) {
0387                 KisColorFilterCombo::paintColorPie(painter, opt.palette, selectedColors, editRect, size );
0388             } else {
0389                 // show all colors in a rectangle
0390                 KisNodeViewColorScheme scm;
0391 
0392                 int oneColorWidth = editRect.width()/numColors;
0393                 int currentWidth = 0;
0394                 for (int i = 0; i < numColors; i++) {
0395                     QColor color = scm.colorFromLabelIndex(selectedColors[i]);
0396                     QBrush brush = color.alpha() > 0 ? QBrush(color) : QBrush(Qt::black, Qt::Dense4Pattern);
0397                     painter.setPen(color);
0398                     painter.setBrush(brush);
0399                     if (i == numColors - 1) {
0400                         // last color; let's fill up
0401                         painter.fillRect(currentWidth, editRect.top(), editRect.width() - currentWidth, editRect.height(), brush);
0402                     } else {
0403                         painter.fillRect(currentWidth, editRect.top(), oneColorWidth, editRect.height(), brush);
0404                     }
0405 
0406                     currentWidth += oneColorWidth;
0407                 }
0408 
0409 
0410 
0411 
0412             }
0413         }
0414     }
0415 
0416     // draw the icon and text
0417     //painter.drawControl(QStyle::CE_ComboBoxLabel, opt);
0418 }
0419 
0420 QSize KisColorFilterCombo::minimumSizeHint() const
0421 {
0422     return sizeHint();
0423 }
0424 
0425 QSize KisColorFilterCombo::sizeHint() const
0426 {
0427     QStyleOptionComboBox opt;
0428     initStyleOption(&opt);
0429 
0430     const QStyleOption *baseOption = qstyleoption_cast<const QStyleOption *>(&opt);
0431     const int arrowSize = style()->pixelMetric(QStyle::PM_ScrollBarExtent, baseOption, this);
0432 
0433     const QSize originalHint = QComboBox::sizeHint();
0434     QSize sh(3 * arrowSize, originalHint.height());
0435 
0436     return sh;
0437 }