File indexing completed on 2024-05-12 16:01:41

0001 /* This file is part of the KDE project
0002    SPDX-FileCopyrightText: 2009 Vera Lukman <shicmap@gmail.com>
0003    SPDX-FileCopyrightText: 2011 Sven Langkamp <sven.langkamp@gmail.com>
0004    SPDX-FileCopyrightText: 2016 Scott Petrovic <scottpetrovic@gmail.com>
0005 
0006    SPDX-License-Identifier: LGPL-2.0-only
0007 
0008 */
0009 #include <QtGui>
0010 #include <QMenu>
0011 #include <QWhatsThis>
0012 #include <QVBoxLayout>
0013 
0014 #include <KisTagModel.h>
0015 
0016 #include "kis_canvas2.h"
0017 #include "kis_config.h"
0018 #include "kis_popup_palette.h"
0019 #include "kis_favorite_resource_manager.h"
0020 #include "kis_icon_utils.h"
0021 #include <kis_canvas_resource_provider.h>
0022 #include <KoTriangleColorSelector.h>
0023 #include "KoColorDisplayRendererInterface.h"
0024 #include <KisVisualColorSelector.h>
0025 #include <kis_config_notifier.h>
0026 #include "kis_signal_compressor.h"
0027 #include "brushhud/kis_brush_hud.h"
0028 #include "brushhud/kis_round_hud_button.h"
0029 #include "kis_signals_blocker.h"
0030 #include "kis_canvas_controller.h"
0031 #include "kis_acyclic_signal_connector.h"
0032 #include <kis_paintop_preset.h>
0033 #include "KisMouseClickEater.h"
0034 
0035 static const int WIDGET_MARGIN = 16;
0036 static const qreal BORDER_WIDTH = 3.0;
0037 
0038 class PopupColorTriangle : public KoTriangleColorSelector
0039 {
0040 public:
0041     PopupColorTriangle(const KoColorDisplayRendererInterface *displayRenderer, QWidget* parent)
0042         : KoTriangleColorSelector(displayRenderer, parent)
0043         , m_dragging(false)
0044     {
0045     }
0046 
0047     ~PopupColorTriangle() override {}
0048 
0049     void tabletEvent(QTabletEvent* event) override {
0050         QMouseEvent* mouseEvent = 0;
0051 
0052         // ignore any tablet events that are done with the right click
0053         // Tablet move events don't return a "button", so catch that too
0054         if(event->button() == Qt::LeftButton || event->type() == QEvent::TabletMove)
0055         {
0056             switch (event->type()) {
0057                 case QEvent::TabletPress:
0058                     mouseEvent = new QMouseEvent(QEvent::MouseButtonPress, event->pos(),
0059                                                  Qt::LeftButton, Qt::LeftButton, event->modifiers());
0060                     m_dragging = true;
0061                     mousePressEvent(mouseEvent);
0062                     event->accept();
0063                     break;
0064                 case QEvent::TabletMove:
0065                     mouseEvent = new QMouseEvent(QEvent::MouseMove, event->pos(),
0066                                                  (m_dragging) ? Qt::LeftButton : Qt::NoButton,
0067                                                  (m_dragging) ? Qt::LeftButton : Qt::NoButton, event->modifiers());
0068                     mouseMoveEvent(mouseEvent);
0069                     event->accept();
0070                     break;
0071                 case QEvent::TabletRelease:
0072                     mouseEvent = new QMouseEvent(QEvent::MouseButtonRelease, event->pos(),
0073                                                  Qt::LeftButton,
0074                                                  Qt::LeftButton,
0075                                                  event->modifiers());
0076                     m_dragging = false;
0077                     mouseReleaseEvent(mouseEvent);
0078                     event->accept();
0079                     break;
0080                 default: break;
0081             }
0082         }
0083 
0084         delete mouseEvent;
0085     }
0086 
0087 private:
0088     bool m_dragging;
0089 };
0090 
0091 KisPopupPalette::KisPopupPalette(KisViewManager* viewManager, KisCoordinatesConverter* coordinatesConverter ,KisFavoriteResourceManager* manager,
0092                                  const KoColorDisplayRendererInterface *displayRenderer, KisCanvasResourceProvider *provider, QWidget *parent)
0093     : QWidget(parent, Qt::FramelessWindowHint)
0094     , m_coordinatesConverter(coordinatesConverter)
0095     , m_viewManager(viewManager)
0096     , m_actionManager(viewManager->actionManager())
0097     , m_resourceManager(manager)
0098     , m_displayRenderer(displayRenderer)
0099     , m_colorChangeCompressor(new KisSignalCompressor(50, KisSignalCompressor::POSTPONE))
0100     , m_actionCollection(viewManager->actionCollection())
0101     , m_acyclicConnector(new KisAcyclicSignalConnector(this))
0102     , m_clicksEater(new KisMouseClickEater(Qt::RightButton, 1, this))
0103 {
0104     connect(m_colorChangeCompressor.data(), SIGNAL(timeout()),
0105             SLOT(slotEmitColorChanged()));
0106 
0107     connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), this, SLOT(slotConfigurationChanged()));
0108     connect(m_displayRenderer,  SIGNAL(displayConfigurationChanged()), this, SLOT(slotDisplayConfigurationChanged()));
0109 
0110     m_acyclicConnector->connectForwardKoColor(m_resourceManager, SIGNAL(sigChangeFGColorSelector(KoColor)),
0111                                               this, SLOT(slotExternalFgColorChanged(KoColor)));
0112 
0113     m_acyclicConnector->connectBackwardKoColor(this, SIGNAL(sigChangefGColor(KoColor)),
0114                                                m_resourceManager, SIGNAL(sigSetFGColor(KoColor)));
0115     // just update() to repaint color labels on external background color change
0116     connect(viewManager->canvasResourceProvider(), SIGNAL(sigBGColorChanged(KoColor)), SLOT(update()));
0117 
0118     connect(this, SIGNAL(sigChangeActivePaintop(int)), m_resourceManager, SLOT(slotChangeActivePaintop(int)));
0119     connect(this, SIGNAL(sigUpdateRecentColor(int)), m_resourceManager, SLOT(slotUpdateRecentColor(int)));
0120 
0121     connect(m_resourceManager, SIGNAL(setSelectedColor(int)), this, SLOT(slotSetSelectedColor(int)));
0122     connect(m_resourceManager, SIGNAL(updatePalettes()), this, SLOT(slotUpdate()));
0123     connect(m_resourceManager, SIGNAL(hidePalettes()), this, SIGNAL(finished()));
0124 
0125     // Instances of `this` rely on finished() to be detached and its lifetime is associated with `parent`
0126     connect(parent, SIGNAL(destroyed(QObject *)), this, SIGNAL(finished()), Qt::DirectConnection);
0127 
0128     setCursor(Qt::ArrowCursor);
0129     setMouseTracking(true);
0130     setHoveredPreset(-1);
0131     setHoveredColor(-1);
0132     setSelectedColor(-1);
0133 
0134     m_brushHud = new KisBrushHud(provider, this);
0135 
0136     m_tagsButton = new KisRoundHudButton(this);
0137 
0138     connect(m_tagsButton, SIGNAL(clicked()), SLOT(slotShowTagsPopup()));
0139 
0140     m_brushHudButton = new KisRoundHudButton(this);
0141     m_brushHudButton->setCheckable(true);
0142 
0143     connect(m_brushHudButton, SIGNAL(toggled(bool)), SLOT(showHudWidget(bool)));
0144     
0145     m_bottomBarWidget = new QWidget(this);
0146     
0147     m_bottomBarButton = new KisRoundHudButton(this);
0148     m_bottomBarButton->setCheckable(true);
0149 
0150     connect( m_bottomBarButton, SIGNAL(toggled(bool)), SLOT(showBottomBarWidget(bool)));
0151 
0152     m_clearColorHistoryButton = new KisRoundHudButton(this);
0153     m_clearColorHistoryButton->setToolTip(i18n("Clear color history"));
0154 
0155     connect(m_clearColorHistoryButton, SIGNAL(clicked(bool)), m_resourceManager, SLOT(slotClearHistory()));
0156     //Otherwise the colors won't disappear until the cursor moves away from the button:
0157     connect(m_clearColorHistoryButton, SIGNAL(released()), this, SLOT(slotUpdate()));
0158 
0159     // add some stuff below the pop-up palette that will make it easier to use for tablet people
0160     QGridLayout* gLayout = new QGridLayout(this);
0161     gLayout->setSizeConstraint(QLayout::SetFixedSize);
0162     gLayout->setSpacing(0);
0163     gLayout->setContentsMargins(QMargins());
0164     m_mainArea = new QSpacerItem(m_popupPaletteSize, m_popupPaletteSize);
0165     gLayout->addItem(m_mainArea, 0, 0); // this should push the box to the bottom
0166     gLayout->setColumnMinimumWidth(1, WIDGET_MARGIN);
0167     gLayout->addWidget(m_brushHud, 0, 2);
0168     gLayout->setRowMinimumHeight(1, WIDGET_MARGIN);
0169     gLayout->addWidget(m_bottomBarWidget, 2, 0);
0170 
0171     QHBoxLayout* hLayout = new QHBoxLayout(m_bottomBarWidget);
0172     
0173     mirrorMode = new KisHighlightedToolButton(this);
0174     mirrorMode->setFixedSize(35, 35);
0175 
0176     mirrorMode->setToolTip(i18n("Mirror Canvas"));
0177     mirrorMode->setDefaultAction(m_actionCollection->action("mirror_canvas_around_cursor"));
0178     connect(mirrorMode, SIGNAL(clicked(bool)), this, SLOT(slotUpdate()));
0179     connect(mirrorMode, SIGNAL(pressed()), this, SLOT(slotSetMirrorPos()));
0180     connect(mirrorMode, SIGNAL(clicked()), this, SLOT(slotRemoveMirrorPos()));
0181 
0182     canvasOnlyButton = new KisHighlightedToolButton(this);
0183     canvasOnlyButton->setFixedSize(35, 35);
0184 
0185     canvasOnlyButton->setToolTip(i18n("Canvas Only"));
0186     canvasOnlyButton->setDefaultAction(m_actionCollection->action("view_show_canvas_only"));
0187 
0188     zoomToOneHundredPercentButton = new QPushButton(this);
0189     zoomToOneHundredPercentButton->setText(i18n("100%"));
0190     zoomToOneHundredPercentButton->setFixedHeight(35);
0191 
0192     zoomToOneHundredPercentButton->setToolTip(i18n("Zoom to 100%"));
0193     connect(zoomToOneHundredPercentButton, SIGNAL(clicked(bool)), this, SLOT(slotZoomToOneHundredPercentClicked()));
0194 
0195     zoomCanvasSlider = new QSlider(Qt::Horizontal, this);
0196     zoomSliderMinValue = 10; // set in %
0197     zoomSliderMaxValue = 200; // set in %
0198 
0199     zoomCanvasSlider->setRange(zoomSliderMinValue, zoomSliderMaxValue);
0200     zoomCanvasSlider->setFixedHeight(35);
0201     zoomCanvasSlider->setValue(m_coordinatesConverter->zoomInPercent());
0202 
0203     zoomCanvasSlider->setSingleStep(1);
0204     zoomCanvasSlider->setPageStep(1);
0205 
0206     connect(zoomCanvasSlider, SIGNAL(valueChanged(int)), this, SLOT(slotZoomSliderChanged(int)));
0207     connect(zoomCanvasSlider, SIGNAL(sliderPressed()), this, SLOT(slotZoomSliderPressed()));
0208     connect(zoomCanvasSlider, SIGNAL(sliderReleased()), this, SLOT(slotZoomSliderReleased()));
0209     
0210     slotUpdateIcons();
0211 
0212     hLayout->setSpacing(2);
0213     hLayout->setContentsMargins(0, 6, 0, 0);
0214     hLayout->addWidget(mirrorMode);
0215     hLayout->addWidget(canvasOnlyButton);
0216     hLayout->addWidget(zoomCanvasSlider);
0217     hLayout->addWidget(zoomToOneHundredPercentButton);
0218     
0219     setVisible(false);
0220     reconfigure();
0221 
0222     opacityChange = new QGraphicsOpacityEffect(this);
0223     setGraphicsEffect(opacityChange);
0224 
0225     /**
0226      * Tablet support code generates a spurious right-click right after opening
0227      * the window, so we should ignore it. Next right-click will be used for
0228      * closing the popup palette
0229      */
0230     installEventFilter(m_clicksEater);
0231 
0232     // Prevent tablet events from being captured by the canvas
0233     setAttribute(Qt::WA_NoMousePropagation, true);
0234 
0235     // Because we can create a popup in canvas widget, synthetic events sometimes arrive here (see mousePressEvent()).
0236     setAttribute(Qt::WA_AcceptTouchEvents, true);
0237     installEventFilter(this);
0238     const QList<QWidget *> childrenWidgets = findChildren<QWidget *>();
0239     for (const auto &child: childrenWidgets) {
0240         child->setAttribute(Qt::WA_AcceptTouchEvents, true);
0241         child->installEventFilter(this);
0242     }
0243 
0244     // Load configuration..
0245     KisConfig cfg(true);
0246     m_brushHudButton->setChecked(cfg.showBrushHud());
0247     m_bottomBarButton->setChecked(cfg.showPaletteBottomBar());
0248 }
0249 
0250 KisPopupPalette::~KisPopupPalette()
0251 {
0252 }
0253 
0254 void KisPopupPalette::slotConfigurationChanged()
0255 {
0256     reconfigure();
0257     layout()->invalidate();
0258 }
0259 
0260 void KisPopupPalette::reconfigure()
0261 {
0262     KisConfig config(true);
0263     m_useDynamicSlotCount = config.readEntry("popuppalette/useDynamicSlotCount", true);
0264     m_maxPresetSlotCount = config.favoritePresets();
0265     if (m_useDynamicSlotCount) {
0266         int presetCount = m_resourceManager->numFavoritePresets();
0267         // if there are no presets because the tag is empty
0268         // show the maximum number allowed (they will be painted as empty slots)
0269         m_presetSlotCount = presetCount == 0
0270             ? m_maxPresetSlotCount
0271             : qMin(m_maxPresetSlotCount, m_resourceManager->numFavoritePresets());
0272     } else {
0273         m_presetSlotCount = m_maxPresetSlotCount;
0274     }
0275     m_popupPaletteSize = config.readEntry("popuppalette/size", 385);
0276     qreal selectorRadius = config.readEntry("popuppalette/selectorSize", 140) / 2;
0277     
0278     m_showColorHistory = config.readEntry("popuppalette/showColorHistory", true);
0279     m_showRotationTrack = config.readEntry("popuppalette/showRotationTrack", true);
0280     
0281     m_colorHistoryInnerRadius = selectorRadius + m_presetRingMargin;
0282     m_colorHistoryOuterRadius = m_colorHistoryInnerRadius;
0283     if (m_showColorHistory) {
0284          m_colorHistoryOuterRadius += 20;
0285          m_clearColorHistoryButton->setVisible(true);
0286     } else {
0287          m_clearColorHistoryButton->setVisible(false);
0288     }
0289 
0290     m_mainArea->changeSize(m_popupPaletteSize, m_popupPaletteSize);
0291 
0292     bool useVisualSelector = config.readEntry<bool>("popuppalette/usevisualcolorselector", false);
0293     if (m_colorSelector) {
0294         // if the selector type changed, delete it
0295         bool haveVisualSelector = qobject_cast<KisVisualColorSelector*>(m_colorSelector) != 0;
0296         if (useVisualSelector != haveVisualSelector) {
0297             delete m_colorSelector;
0298             m_colorSelector = 0;
0299         }
0300     }
0301     if (!m_colorSelector) {
0302         if (useVisualSelector) {
0303             KisVisualColorSelector *selector = new KisVisualColorSelector(this);
0304             selector->setAcceptTabletEvents(true);
0305             m_colorSelector = selector;
0306         }
0307         else {
0308             m_colorSelector  = new PopupColorTriangle(m_displayRenderer, this);
0309             connect(m_colorSelector, SIGNAL(requestCloseContainer()), this, SIGNAL(finished()));
0310         }
0311         m_colorSelector->setDisplayRenderer(m_displayRenderer);
0312         m_colorSelector->setConfig(true,false);
0313         m_colorSelector->setVisible(true);
0314         slotDisplayConfigurationChanged();
0315         connect(m_colorSelector, SIGNAL(sigNewColor(KoColor)),
0316                 m_colorChangeCompressor.data(), SLOT(start()));
0317         connect(KisConfigNotifier::instance(), SIGNAL(configChanged()),
0318                 m_colorSelector, SLOT(configurationChanged()));
0319     }
0320 
0321 
0322     const int auxButtonSize = 35;
0323     m_colorSelector->move(m_popupPaletteSize/2 - selectorRadius, m_popupPaletteSize/2 - selectorRadius);
0324     m_colorSelector->resize(m_popupPaletteSize - 2*m_colorSelector->x(), m_popupPaletteSize - 2*m_colorSelector->y());
0325 
0326     // ellipse - to make sure the widget doesn't eat events meant for recent colors or brushes
0327     //         - needs to be +2 pixels on every side for anti-aliasing to look nice on high dpi displays
0328     // rectangle - to make sure the area doesn't extend outside of the widget
0329     QRegion maskedEllipse(-2, -2, m_colorSelector->width() + 4, m_colorSelector->height() + 4, QRegion::Ellipse );
0330     QRegion maskedRectangle(0, 0, m_colorSelector->width(), m_colorSelector->height(), QRegion::Rectangle);
0331     QRegion maskedRegion = maskedEllipse.intersected(maskedRectangle);
0332 
0333     m_colorSelector->setMask(maskedRegion);
0334 
0335     m_brushHud->setFixedHeight(int(m_popupPaletteSize));
0336     
0337     // arranges the buttons around the popup palette
0338     // buttons are spread out from the center of the set arc length
0339     
0340     // the margin in degrees between buttons
0341     qreal margin = 10.0; 
0342     // visual center
0343     qreal center = m_popupPaletteSize/2 - auxButtonSize/2;
0344     qreal length = m_popupPaletteSize/2 + auxButtonSize/2 + 5;
0345     {
0346         int buttonCount = 2;
0347         int arcLength = 90;
0348         // note the int buttonCount/2 is on purpose
0349         qreal start = arcLength/2 - (buttonCount/2) * margin;
0350         if (buttonCount % 2 == 0) start += margin / 2;
0351         int place = 0;
0352         m_brushHudButton->setGeometry(
0353             center + qCos(qDegreesToRadians(start + place*margin))*length,
0354             center + qSin(qDegreesToRadians(start + place*margin))*length,
0355             auxButtonSize, auxButtonSize
0356         );
0357         place++;
0358         m_bottomBarButton->setGeometry (
0359             center + qCos(qDegreesToRadians(start + place*margin))*length,
0360             center + qSin(qDegreesToRadians(start + place*margin))*length,
0361             auxButtonSize, auxButtonSize
0362         );
0363     }
0364     {
0365         int buttonCount = m_showColorHistory ? 2 : 1 ;
0366         int arcLength = 90;
0367         int shiftArc = 90;
0368         // note the int buttonCount/2 is on purpose
0369         qreal start = shiftArc + arcLength / 2 - (buttonCount/2) * margin;
0370         if (buttonCount % 2 == 0) start += margin / 2;
0371         int place = 0;
0372         if (m_showColorHistory) {
0373             m_clearColorHistoryButton->setGeometry(
0374                 center + qCos(qDegreesToRadians(start + place * margin)) * length,
0375                 center + qSin(qDegreesToRadians(start + place * margin)) * length,
0376                 auxButtonSize, auxButtonSize);
0377             place++;
0378         }
0379         m_tagsButton->setGeometry(
0380             center + qCos(qDegreesToRadians(start + place*margin))*length,
0381             center + qSin(qDegreesToRadians(start + place*margin))*length,
0382             auxButtonSize, auxButtonSize
0383         );
0384     }
0385     calculatePresetLayout();
0386 }
0387 
0388 void KisPopupPalette::slotDisplayConfigurationChanged()
0389 {
0390     // Visual Color Selector picks up color space from input
0391     KoColor col = m_viewManager->canvasResourceProvider()->fgColor();
0392     const KoColorSpace *paintingCS = m_displayRenderer->getPaintingColorSpace();
0393     //hack to get around cmyk for now.
0394     if (paintingCS->colorChannelCount()>3) {
0395         paintingCS = KoColorSpaceRegistry::instance()->rgb8();
0396     }
0397     m_colorSelector->slotSetColorSpace(paintingCS);
0398     m_colorSelector->slotSetColor(col);
0399 }
0400 
0401 void KisPopupPalette::slotExternalFgColorChanged(const KoColor &color)
0402 {
0403     KisSignalsBlocker b(m_colorSelector);
0404     m_colorSelector->slotSetColor(color);
0405     update();
0406 }
0407 
0408 void KisPopupPalette::slotEmitColorChanged()
0409 {
0410     if (isVisible()) {
0411         update();
0412         emit sigChangefGColor(m_colorSelector->getCurrentColor());
0413     }
0414 }
0415 
0416 void KisPopupPalette::slotUpdate() {
0417     int presetCount = m_resourceManager->numFavoritePresets();
0418     if (m_useDynamicSlotCount && presetCount != m_presetSlotCount) {
0419          m_presetSlotCount = presetCount == 0
0420             ? m_maxPresetSlotCount
0421             : qMin(m_maxPresetSlotCount, presetCount);
0422         calculatePresetLayout();
0423     }
0424     m_canvasRotationIndicatorRect = rotationIndicatorRect(m_coordinatesConverter->rotationAngle());
0425     update();
0426 }
0427 
0428 //setting KisPopupPalette properties
0429 int KisPopupPalette::hoveredPreset() const
0430 {
0431     return m_hoveredPreset;
0432 }
0433 
0434 void KisPopupPalette::setHoveredPreset(int x)
0435 {
0436     m_hoveredPreset = x;
0437 }
0438 
0439 int KisPopupPalette::hoveredColor() const
0440 {
0441     return m_hoveredColor;
0442 }
0443 
0444 void KisPopupPalette::setHoveredColor(int x)
0445 {
0446     m_hoveredColor = x;
0447 }
0448 
0449 int KisPopupPalette::selectedColor() const
0450 {
0451     return m_selectedColor;
0452 }
0453 
0454 void KisPopupPalette::setSelectedColor(int x)
0455 {
0456     m_selectedColor = x;
0457 }
0458 
0459 void KisPopupPalette::slotZoomSliderChanged(int zoom) {
0460     emit zoomLevelChanged(zoom);
0461 }
0462 
0463 void KisPopupPalette::slotZoomSliderPressed()
0464 {
0465    m_isZoomingCanvas = true;
0466 }
0467 
0468 void KisPopupPalette::slotZoomSliderReleased()
0469 {
0470     m_isZoomingCanvas = false;
0471 }
0472 
0473 void KisPopupPalette::slotUpdateIcons()
0474 {
0475     this->setPalette(qApp->palette());
0476 
0477     for(int i=0; i<this->children().size(); i++) {
0478         QWidget *w = qobject_cast<QWidget*>(this->children().at(i));
0479         if (w) {
0480             w->setPalette(qApp->palette());
0481         }
0482     }
0483     zoomToOneHundredPercentButton->setIcon(m_actionCollection->action("zoom_to_100pct")->icon());
0484     m_brushHud->updateIcons();
0485     m_tagsButton->setIcon(KisIconUtils::loadIcon("tag"));
0486     m_clearColorHistoryButton->setIcon(KisIconUtils::loadIcon("reload-preset-16"));
0487     m_bottomBarButton->setOnOffIcons(KisIconUtils::loadIcon("arrow-up"), KisIconUtils::loadIcon("arrow-down"));
0488     m_brushHudButton->setOnOffIcons(KisIconUtils::loadIcon("arrow-left"), KisIconUtils::loadIcon("arrow-right"));
0489 }
0490 
0491 void KisPopupPalette::showHudWidget(bool visible)
0492 {
0493     const bool reallyVisible = visible && m_brushHudButton->isChecked();
0494 
0495     if (reallyVisible) {
0496         m_brushHud->updateProperties();
0497     }
0498 
0499     m_brushHud->setVisible(reallyVisible);
0500 
0501     KisConfig cfg(false);
0502     cfg.setShowBrushHud(visible);
0503 }
0504 
0505 void KisPopupPalette::showBottomBarWidget(bool visible)
0506 {
0507     const bool reallyVisible = visible && m_bottomBarButton->isChecked();
0508 
0509     m_bottomBarWidget->setVisible(reallyVisible);
0510 
0511     KisConfig cfg(false);
0512     cfg.setShowPaletteBottomBar(visible);
0513 }
0514 
0515 void KisPopupPalette::setParent(QWidget *parent) {
0516     QWidget::setParent(parent);
0517 }
0518 
0519 
0520 QSize KisPopupPalette::sizeHint() const
0521 {
0522     // Note: the canvas popup widget system "abuses" the sizeHint to determine
0523     // the position to show the widget; this does not reflect the true size.
0524     return QSize(m_popupPaletteSize, m_popupPaletteSize);
0525 }
0526 
0527 void KisPopupPalette::paintEvent(QPaintEvent* e)
0528 {
0529     Q_UNUSED(e);
0530 
0531     QPainter painter(this);
0532 
0533     QPen pen(palette().color(QPalette::Text), BORDER_WIDTH);
0534     painter.setPen(pen);
0535 
0536     painter.setRenderHint(QPainter::Antialiasing);
0537     painter.setRenderHint(QPainter::SmoothPixmapTransform);
0538 
0539     if (m_isOverFgBgColors) {
0540         painter.save();
0541         painter.setPen(QPen(palette().color(QPalette::Highlight), BORDER_WIDTH));
0542     }
0543     
0544     // painting background color indicator
0545     QPainterPath bgColor(drawFgBgColorIndicator(0));
0546     painter.fillPath(bgColor, m_displayRenderer->toQColor(m_resourceManager->bgColor()));
0547     painter.drawPath(bgColor);
0548 
0549     // painting foreground color indicator
0550     QPainterPath fgColor(drawFgBgColorIndicator(1));
0551     painter.fillPath(fgColor, m_displayRenderer->toQColor(m_colorSelector->getCurrentColor()));
0552     painter.drawPath(fgColor);
0553     
0554     if (m_isOverFgBgColors) painter.restore();
0555     
0556 
0557     // create a circle background that everything else will go into
0558     QPainterPath backgroundContainer;
0559 
0560     // draws the circle halfway into the border so that the border never goes past the bounds of the popup
0561     QRectF circleRect(BORDER_WIDTH/2, BORDER_WIDTH/2, m_popupPaletteSize - BORDER_WIDTH, m_popupPaletteSize - BORDER_WIDTH);
0562     backgroundContainer.addEllipse(circleRect);
0563     painter.fillPath(backgroundContainer, palette().brush(QPalette::Background));
0564     painter.drawPath(backgroundContainer);
0565 
0566     if (m_showRotationTrack) {
0567         painter.save();
0568         QPen pen(palette().color(QPalette::Background).lighter(150), 2);
0569         painter.setPen(pen);
0570 
0571         // draw rotation snap lines
0572         if (m_isRotatingCanvasIndicator) {
0573             for (QLineF &line: m_snapLines) {
0574                 painter.drawLine(line);
0575             }
0576         }
0577         // create a path slightly inside the container circle. this will create a 'track' to indicate that we can rotate the canvas
0578         // with the indicator
0579         QPainterPath rotationTrackPath;
0580         QRectF circleRect2(m_rotationTrackSize, m_rotationTrackSize,
0581                         m_popupPaletteSize - m_rotationTrackSize * 2, m_popupPaletteSize - m_rotationTrackSize * 2);
0582 
0583         rotationTrackPath.addEllipse(circleRect2);
0584         painter.drawPath(rotationTrackPath);
0585         
0586         // create a reset canvas rotation indicator to bring the canvas back to 0 degrees
0587         QRectF resetRotationIndicator = m_resetCanvasRotationIndicatorRect;
0588 
0589         pen.setColor(m_isOverResetCanvasRotationIndicator
0590             ? palette().color(QPalette::Highlight)
0591             : palette().color(QPalette::Text));
0592         // cover the first snap line
0593         painter.setBrush(palette().brush(QPalette::Background));
0594         painter.setPen(pen);
0595         painter.drawEllipse(resetRotationIndicator);
0596 
0597         // create the canvas rotation handle
0598         // highlight if either just hovering or currently rotating
0599         pen.setColor(m_isOverCanvasRotationIndicator || m_isRotatingCanvasIndicator
0600             ? palette().color(QPalette::Highlight)
0601             : palette().color(QPalette::Text));
0602         painter.setPen(pen);
0603         
0604         // fill with highlight if snapping
0605         painter.setBrush(m_isRotatingCanvasIndicator && m_snapRotation
0606             ? palette().brush(QPalette::Highlight)
0607             : palette().brush(QPalette::Text));
0608         
0609         painter.drawEllipse(m_canvasRotationIndicatorRect);
0610 
0611         painter.restore();
0612     }
0613     
0614     // the following things needs to be based off the center, so let's translate the painter
0615     painter.translate(m_popupPaletteSize / 2, m_popupPaletteSize / 2);
0616 
0617     // painting favorite brushes
0618     QList<QImage> images(m_resourceManager->favoritePresetImages());
0619     
0620     // painting favorite brushes pixmap/icon
0621     QPainterPath presetPath;
0622     int presetCount = images.size();
0623     bool isTagEmpty = presetCount == 0;
0624     for (int pos = 0; pos < m_presetSlotCount; pos++) {
0625         painter.save();
0626         presetPath = createPathFromPresetIndex(pos);
0627 
0628         if (pos < presetCount) {
0629             painter.setClipPath(presetPath);
0630 
0631             QRect bounds = presetPath.boundingRect().toAlignedRect();
0632             if (!images.at(pos).isNull()) {
0633                 QImage previewHighDPI = images.at(pos).scaled(bounds.size()*devicePixelRatioF() , Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
0634                 previewHighDPI.setDevicePixelRatio(devicePixelRatioF());
0635                 painter.drawImage(bounds.topLeft(), previewHighDPI);
0636             }
0637         } else {
0638             painter.fillPath(presetPath, palette().brush(QPalette::Window));  // brush slot that has no brush in it
0639         }
0640         // needs to be called here so that the clipping is removed
0641         painter.restore();
0642         // if the slot is empty, stroke it slightly darker
0643         QColor color = isTagEmpty || pos >= presetCount
0644             ? palette().color(QPalette::Background).lighter(150)
0645             : palette().color(QPalette::Text);
0646         painter.setPen(QPen(color, 1));
0647         painter.drawPath(presetPath);
0648     }
0649     if (hoveredPreset() > -1) {
0650         presetPath = createPathFromPresetIndex(hoveredPreset());
0651         painter.setPen(QPen(palette().color(QPalette::Highlight), BORDER_WIDTH));
0652         painter.drawPath(presetPath);
0653     }
0654 
0655     if (m_showColorHistory) {
0656         // paint recent colors area.
0657         painter.setPen(Qt::NoPen);
0658         qreal rotationAngle = -360.0 / m_resourceManager->recentColorsTotal();
0659 
0660         // there might be no recent colors at the start, so paint a placeholder
0661         if (m_resourceManager->recentColorsTotal() == 0) {
0662             painter.setBrush(Qt::transparent);
0663 
0664             QPainterPath emptyRecentColorsPath(drawDonutPathFull(0, 0, m_colorHistoryInnerRadius, m_colorHistoryOuterRadius));
0665             painter.setPen(QPen(palette().color(QPalette::Background).lighter(150), 2, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
0666             painter.drawPath(emptyRecentColorsPath);
0667         } else {
0668 
0669             for (int pos = 0; pos < m_resourceManager->recentColorsTotal(); pos++) {
0670                 QPainterPath recentColorsPath(drawDonutPathAngle(m_colorHistoryInnerRadius, m_colorHistoryOuterRadius, m_resourceManager->recentColorsTotal()));
0671 
0672                 //accessing recent color of index pos
0673                 painter.fillPath(recentColorsPath, m_displayRenderer->toQColor( m_resourceManager->recentColorAt(pos) ));
0674                 painter.drawPath(recentColorsPath);
0675                 painter.rotate(rotationAngle);
0676             }
0677         }
0678 
0679         // painting hovered color
0680         if (hoveredColor() > -1) {
0681             painter.setPen(QPen(palette().color(QPalette::Highlight), 2, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
0682 
0683             if (m_resourceManager->recentColorsTotal() == 1) {
0684                 QPainterPath path_ColorDonut(drawDonutPathFull(0, 0, m_colorHistoryInnerRadius, m_colorHistoryOuterRadius));
0685                 painter.drawPath(path_ColorDonut);
0686             } else {
0687                 painter.rotate((m_resourceManager->recentColorsTotal() + hoveredColor()) *rotationAngle);
0688                 QPainterPath path(drawDonutPathAngle(m_colorHistoryInnerRadius, m_colorHistoryOuterRadius, m_resourceManager->recentColorsTotal()));
0689                 painter.drawPath(path);
0690                 painter.rotate(hoveredColor() * -1 * rotationAngle);
0691             }
0692         }
0693 
0694         // painting selected color
0695         if (selectedColor() > -1) {
0696             painter.setPen(QPen(palette().color(QPalette::Highlight).darker(130), 2, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));
0697 
0698             if (m_resourceManager->recentColorsTotal() == 1) {
0699                 QPainterPath path_ColorDonut(drawDonutPathFull(0, 0, m_colorHistoryInnerRadius, m_colorHistoryOuterRadius));
0700                 painter.drawPath(path_ColorDonut);
0701             } else {
0702                 painter.rotate((m_resourceManager->recentColorsTotal() + selectedColor()) *rotationAngle);
0703                 QPainterPath path(drawDonutPathAngle(m_colorHistoryInnerRadius, m_colorHistoryOuterRadius, m_resourceManager->recentColorsTotal()));
0704                 painter.drawPath(path);
0705                 painter.rotate(selectedColor() * -1 * rotationAngle);
0706             }
0707         }
0708     }
0709 
0710 
0711     // if we are actively rotating the canvas or zooming, make the panel slightly transparent to see the canvas better
0712     if(m_isRotatingCanvasIndicator || m_isZoomingCanvas) {
0713         opacityChange->setOpacity(0.4);
0714     } else {
0715         opacityChange->setOpacity(1.0);
0716     }
0717 
0718 }
0719 
0720 void KisPopupPalette::resizeEvent(QResizeEvent* resizeEvent) {
0721     Q_UNUSED(resizeEvent);
0722     calculateRotationSnapAreas();
0723     m_resetCanvasRotationIndicatorRect = rotationIndicatorRect(0);
0724     m_canvasRotationIndicatorRect = rotationIndicatorRect(m_coordinatesConverter->rotationAngle());
0725     // Ensure that the resized geometry fits within the desired rect...
0726     QRect tempGeo = rect(); 
0727     tempGeo.translate(pos());
0728     ensureWithinParent(tempGeo.topLeft(), true);
0729 }
0730 
0731 QPainterPath KisPopupPalette::drawDonutPathFull(int x, int y, int inner_radius, int outer_radius)
0732 {
0733     QPainterPath path;
0734     path.addEllipse(QPointF(x, y), outer_radius, outer_radius);
0735     path.addEllipse(QPointF(x, y), inner_radius, inner_radius);
0736     path.setFillRule(Qt::OddEvenFill);
0737 
0738     return path;
0739 }
0740 
0741 QPainterPath KisPopupPalette::drawDonutPathAngle(int inner_radius, int outer_radius, int limit)
0742 {
0743     QPainterPath path;
0744     path.moveTo(-0.999 * outer_radius * sin(M_PI / limit), 0.999 * outer_radius * cos(M_PI / limit));
0745     path.arcTo(-1 * outer_radius, -1 * outer_radius, 2 * outer_radius, 2 * outer_radius, -90.0 - 180.0 / limit,
0746                360.0 / limit);
0747     path.arcTo(-1 * inner_radius, -1 * inner_radius, 2 * inner_radius, 2 * inner_radius, -90.0 + 180.0 / limit,
0748                - 360.0 / limit);
0749     path.closeSubpath();
0750 
0751     return path;
0752 }
0753 
0754 QPainterPath KisPopupPalette::drawFgBgColorIndicator(int type) const
0755 {
0756     QPointF edgePoint = QPointF(0.14645, 0.14645) * (m_popupPaletteSize);
0757     
0758     // the points are really (-5, 15) and (5, 15) shifted right 1px
0759     // this is so that where the circles meet the circle of the palette, the space occupied is exactly half to either side of the -45deg line
0760     QPainterPath indicator;
0761     switch (type) {
0762         case 0: { // background
0763             indicator.addEllipse(edgePoint + QPointF(-4, 15), 30, 30);
0764             break;
0765         }
0766         case 1: { //foreground
0767             indicator.addEllipse(edgePoint + QPointF(6, -15), 30, 30);
0768             break;
0769         }
0770     }
0771     return indicator;
0772 }
0773 
0774 QRectF KisPopupPalette::rotationIndicatorRect(qreal rotationAngle) const
0775 {
0776     qreal paletteRadius = 0.5 * m_popupPaletteSize;
0777     QPointF rotationDialPosition(drawPointOnAngle(rotationAngle, paletteRadius - 10));
0778     rotationDialPosition += QPointF(paletteRadius, paletteRadius);
0779     
0780     QPointF indicatorDiagonal(7.5, 7.5);
0781     return QRectF(rotationDialPosition - indicatorDiagonal, rotationDialPosition + indicatorDiagonal);
0782 }
0783 
0784 void KisPopupPalette::mouseMoveEvent(QMouseEvent *event)
0785 {
0786     QPointF point = event->localPos();
0787     event->accept();
0788 
0789     if (m_showRotationTrack) {
0790         // check if mouse is over the canvas rotation knob
0791         bool wasOverRotationIndicator = m_isOverCanvasRotationIndicator;
0792         m_isOverCanvasRotationIndicator = m_canvasRotationIndicatorRect.contains(point);
0793         bool wasOverResetRotationIndicator = m_isOverResetCanvasRotationIndicator;
0794         m_isOverResetCanvasRotationIndicator = m_resetCanvasRotationIndicatorRect.contains(point);
0795 
0796         if (
0797             wasOverRotationIndicator != m_isOverCanvasRotationIndicator ||
0798             wasOverResetRotationIndicator != m_isOverResetCanvasRotationIndicator
0799         ) {
0800             update();
0801         }
0802 
0803         if (m_isRotatingCanvasIndicator) {
0804             m_snapRotation = false;
0805             int i = 0;
0806             for (QRect &rect: m_snapRects) {
0807                 QPainterPath circle;
0808                 circle.addEllipse(rect);
0809                 if (circle.contains(point)) {
0810                     m_snapRotation = true;
0811                     m_rotationSnapAngle = i * 15;
0812                     break;
0813                 }
0814                 i++;
0815             }
0816             qreal finalAngle = 0.0;
0817             if (m_snapRotation) {
0818                 finalAngle = m_rotationSnapAngle;
0819                 // to match the numbers displayed when rotating without snapping
0820                 if (finalAngle >= 270) {
0821                     finalAngle = finalAngle - 360;
0822                 }
0823             } else {
0824                 // we are rotating the canvas, so calculate the rotation angle based off the center
0825                 // calculate the angle we are at first
0826                 QPoint widgetCenterPoint = QPoint(m_popupPaletteSize/2, m_popupPaletteSize/2);
0827 
0828                 qreal dX = point.x() - widgetCenterPoint.x();
0829                 qreal dY = point.y() - widgetCenterPoint.y();
0830 
0831 
0832                 finalAngle = qAtan2(dY,dX) * 180 / M_PI; // what we need if we have two points, but don't know the angle
0833                 finalAngle = finalAngle + 90; // add 90 degrees so 0 degree position points up
0834             }
0835             qreal angleDifference = finalAngle - m_coordinatesConverter->rotationAngle(); // the rotation function accepts diffs, so find it out
0836 
0837             KisCanvasController *canvasController =
0838                 dynamic_cast<KisCanvasController*>(m_viewManager->canvasBase()->canvasController());
0839             canvasController->rotateCanvas(angleDifference);
0840             m_canvasRotationIndicatorRect = rotationIndicatorRect(finalAngle);
0841 
0842             update();
0843             emit sigUpdateCanvas();
0844         }
0845     }
0846 
0847     if (m_isRotatingCanvasIndicator == false) {
0848         QPainterPath bgColor(drawFgBgColorIndicator(0));
0849         QPainterPath fgColor(drawFgBgColorIndicator(1));
0850         QPainterPath backgroundContainer;
0851         QRectF circleRect(BORDER_WIDTH / 2, BORDER_WIDTH / 2, m_popupPaletteSize - BORDER_WIDTH, m_popupPaletteSize - BORDER_WIDTH);
0852         backgroundContainer.addEllipse(circleRect);
0853         
0854         QPainterPath fgBgColors = (fgColor + bgColor) - backgroundContainer;
0855         
0856         if (fgBgColors.contains(point)) {
0857             if (!m_isOverFgBgColors) {
0858                 m_isOverFgBgColors = true;
0859                 setToolTip(i18n("Click to swap foreground and background colors.\nRight click to set to black and white."));
0860                 update();
0861             }
0862         } else {
0863             if (m_isOverFgBgColors) {
0864                 m_isOverFgBgColors = false;
0865                 setToolTip(QString());
0866                 update();
0867             }
0868         }
0869 
0870         QPainterPath colorHistoryPath(drawDonutPathFull(m_popupPaletteSize / 2, m_popupPaletteSize / 2, m_colorHistoryInnerRadius, m_colorHistoryOuterRadius));
0871         if (colorHistoryPath.contains(point)) {
0872             if (hoveredPreset() >= 0) {
0873                 setToolTip(QString());
0874                 setHoveredPreset(-1);
0875             }
0876 
0877             int pos = calculateColorIndex(point, m_resourceManager->recentColorsTotal());
0878 
0879             if (pos != hoveredColor()) {
0880                 setHoveredColor(pos);
0881                 update();
0882             }
0883         }
0884         else {
0885             if (hoveredColor() >= 0) {
0886                 setHoveredColor(-1);
0887                 update();
0888             }
0889 
0890             int pos = findPresetSlot(point);
0891 
0892             if (pos != hoveredPreset()) {
0893 
0894                 if (pos >= 0 && pos < m_resourceManager->numFavoritePresets()) {
0895                     setToolTip(m_resourceManager->favoritePresetNamesList().at(pos));
0896                     setHoveredPreset(pos);
0897                 }
0898                 else {
0899                     setToolTip(QString());
0900                     setHoveredPreset(-1);
0901                 }
0902 
0903                 update();
0904             }
0905         }
0906     }
0907 }
0908 
0909 void KisPopupPalette::mousePressEvent(QMouseEvent *event)
0910 {
0911     event->accept();
0912 
0913     if (event->button() == Qt::LeftButton) {
0914         if (m_showRotationTrack) {
0915             if (m_isOverCanvasRotationIndicator) {
0916                 m_isRotatingCanvasIndicator = true;
0917                 update();
0918             }
0919 
0920             if (m_isOverResetCanvasRotationIndicator) {
0921                 qreal angleDifference = -m_coordinatesConverter->rotationAngle(); // the rotation function accepts diffs
0922                 KisCanvasController *canvasController =
0923                         dynamic_cast<KisCanvasController*>(m_viewManager->canvasBase()->canvasController());
0924                 canvasController->rotateCanvas(angleDifference);
0925                 m_canvasRotationIndicatorRect = rotationIndicatorRect(0);
0926 
0927                 emit sigUpdateCanvas();
0928             }
0929         }
0930     }
0931 }
0932 
0933 bool KisPopupPalette::eventFilter(QObject *, QEvent *event)
0934 {
0935     switch (event->type()) {
0936     case QEvent::TouchBegin:
0937         m_touchBeginReceived = true;
0938         break;
0939     case QEvent::MouseButtonPress:
0940     case QEvent::MouseMove:
0941         // HACK(sh_zam): Let's say the tap gesture is used by the canvas to launch the popup. Following that, a
0942         // synthesized mousePress is sent and this arrives in our event filter here. But, this event was meant for the
0943         // canvas (even though it blocks it), so we only act on the event if we got a TouchBegin on it first.
0944         if (static_cast<QMouseEvent *>(event)->source() == Qt::MouseEventSynthesizedBySystem && !m_touchBeginReceived) {
0945             event->accept();
0946             return true;
0947         }
0948         break;
0949     case QEvent::MouseButtonRelease:
0950         if (static_cast<QMouseEvent *>(event)->source() == Qt::MouseEventSynthesizedBySystem && !m_touchBeginReceived) {
0951             event->accept();
0952             return true;
0953         }
0954         // fallthrough
0955     case QEvent::Show:
0956     case QEvent::FocusOut:
0957         m_touchBeginReceived = false;
0958         break;
0959     default:
0960         break;
0961     }
0962     return false;
0963 }
0964 
0965 void KisPopupPalette::slotShowTagsPopup()
0966 {
0967     KisTagModel model (ResourceType::PaintOpPresets);
0968     QVector<QString> tags;
0969     for (int i = 0; i < model.rowCount(); ++i) {
0970         QModelIndex idx = model.index(i, 0);
0971         tags << model.data(idx, Qt::DisplayRole).toString();
0972     }
0973 
0974     //std::sort(tags.begin(), tags.end());
0975 
0976     if (!tags.isEmpty()) {
0977         QMenu menu;
0978         Q_FOREACH (const QString& tag, tags) {
0979             menu.addAction(tag);
0980         }
0981 
0982         QAction *action = menu.exec(QCursor::pos());
0983         if (action) {
0984 
0985             for (int i = 0; i < model.rowCount(); ++i) {
0986                 QModelIndex idx = model.index(i, 0);
0987                 if (model.data(idx, Qt::DisplayRole).toString() == KLocalizedString::removeAcceleratorMarker(action->text())) {
0988                     m_resourceManager->setCurrentTag(model.tagForIndex(idx));
0989                     reconfigure();
0990                     break;
0991                 }
0992             }
0993         }
0994     } else {
0995         QWhatsThis::showText(QCursor::pos(),
0996                              i18n("There are no tags available to show in this popup. To add presets, you need to tag them and then select the tag here."));
0997     }
0998 
0999 }
1000 
1001 void KisPopupPalette::slotZoomToOneHundredPercentClicked() {
1002     QAction *action = m_actionCollection->action("zoom_to_100pct");
1003 
1004     if (action) {
1005         action->trigger();
1006     }
1007 
1008     // also move the zoom slider to 100% position so they are in sync
1009     zoomCanvasSlider->setValue(100);
1010 }
1011 
1012 void KisPopupPalette::slotSetMirrorPos() {
1013     m_actionCollection->action("mirror_canvas_around_cursor")->setProperty("customPosition", QVariant(m_mirrorPos));
1014 }
1015 void KisPopupPalette::slotRemoveMirrorPos() {
1016     m_actionCollection->action("mirror_canvas_around_cursor")->setProperty("customPosition", QVariant());
1017 }
1018 
1019 void KisPopupPalette::popup(const QPoint &position) {
1020     setVisible(true);
1021     ensureWithinParent(position, false);
1022     m_mirrorPos = QCursor::pos();
1023 }
1024 
1025 void KisPopupPalette::dismiss()
1026 {
1027     setVisible(false);
1028 }
1029 
1030 bool KisPopupPalette::onScreen()
1031 {
1032     return isVisible();
1033 }
1034 
1035 void KisPopupPalette::ensureWithinParent(const QPoint& position, bool useUpperLeft) {
1036     if (isVisible() && parentWidget())  {
1037         const qreal widgetMargin = -20.0;
1038         const QRect fitRect = kisGrowRect(parentWidget()->rect(), widgetMargin);
1039         const QPoint paletteCenterOffset(sizeHint().width() / 2, sizeHint().height() / 2);
1040 
1041         QRect paletteRect = rect();
1042 
1043         if (!useUpperLeft) {
1044             paletteRect.moveTo(position - paletteCenterOffset);
1045         } else {
1046             paletteRect.moveTopLeft(position);
1047         }
1048 
1049         paletteRect = kisEnsureInRect(paletteRect, fitRect);
1050         move(paletteRect.topLeft());
1051     }
1052 }
1053 
1054 void KisPopupPalette::showEvent(QShowEvent *event)
1055 {
1056     m_clicksEater->reset();
1057 
1058     // don't set the zoom slider if we are outside of the zoom slider bounds. It will change the zoom level to within
1059     // the bounds and cause the canvas to jump between the slider's min and max
1060     if (m_coordinatesConverter->zoomInPercent() > zoomSliderMinValue &&
1061             m_coordinatesConverter->zoomInPercent() < zoomSliderMaxValue){
1062         KisSignalsBlocker b(zoomCanvasSlider);
1063         zoomCanvasSlider->setValue(m_coordinatesConverter->zoomInPercent()); // sync the zoom slider
1064     }
1065 
1066     m_brushHud->setVisible(m_brushHudButton->isChecked());
1067     m_bottomBarWidget->setVisible(m_bottomBarButton->isChecked());
1068 
1069     QWidget::showEvent(event);
1070 }
1071 
1072 void KisPopupPalette::tabletEvent(QTabletEvent *event)
1073 {
1074     if (event->button() == Qt::RightButton && event->type() == QEvent::TabletPress) {
1075         m_tabletRightClickPressed = true;
1076     }
1077 
1078     event->ignore();
1079 }
1080 
1081 void KisPopupPalette::mouseReleaseEvent(QMouseEvent *event)
1082 {
1083     QPointF point = event->localPos();
1084     event->accept();
1085 
1086     if (m_isRotatingCanvasIndicator) {
1087         update();
1088     }
1089 
1090     m_isRotatingCanvasIndicator = false;
1091 
1092     if (event->button() == Qt::LeftButton) {
1093         if (m_isOverFgBgColors) {
1094             m_viewManager->slotToggleFgBg();
1095         }
1096         
1097         //in favorite brushes area
1098         if (hoveredPreset() > -1) {
1099             //setSelectedBrush(hoveredBrush());
1100             emit sigChangeActivePaintop(hoveredPreset());
1101         }
1102 
1103         if (m_showColorHistory) {
1104             QPainterPath pathColor(drawDonutPathFull(m_popupPaletteSize / 2, m_popupPaletteSize / 2, m_colorHistoryInnerRadius, m_colorHistoryOuterRadius));
1105             if (pathColor.contains(point)) {
1106                 int pos = calculateColorIndex(point, m_resourceManager->recentColorsTotal());
1107 
1108                 if (pos >= 0 && pos < m_resourceManager->recentColorsTotal()) {
1109                     emit sigUpdateRecentColor(pos);
1110                 }
1111             }
1112         }
1113     } else if (event->button() == Qt::RightButton) {
1114         emit finished();
1115     }
1116 }
1117 
1118 int KisPopupPalette::calculateColorIndex(QPointF position, int numColors) const
1119 {
1120     if (numColors < 1) {
1121         return -1;
1122     }
1123     // relative to palette center
1124     QPointF relPosition = position - QPointF(0.5 * m_popupPaletteSize, 0.5 * m_popupPaletteSize);
1125 
1126     qreal angle = qAtan2(relPosition.x(), relPosition.y()) + M_PI/numColors;
1127     if (angle < 0) {
1128         angle += 2 * M_PI;
1129     }
1130 
1131     int index = floor(angle * numColors / (2 * M_PI));
1132 
1133     return qBound(0, index, numColors - 1);
1134 }
1135 
1136 bool KisPopupPalette::isPointInPixmap(QPointF &point, int pos)
1137 {
1138     if (createPathFromPresetIndex(pos).contains(point + QPointF(-m_popupPaletteSize / 2, -m_popupPaletteSize / 2))) {
1139         return true;
1140     }
1141     return false;
1142 }
1143 
1144 QPointF KisPopupPalette::drawPointOnAngle(qreal angle, qreal radius) const
1145 {
1146     QPointF p(
1147         // -90 so it starts at the top since this is mainly used by calculatePresetLayout
1148         radius * qCos(qDegreesToRadians(angle - 90)),
1149         radius * qSin(qDegreesToRadians(angle - 90))
1150     );
1151     return p;
1152 }
1153 
1154 void KisPopupPalette::calculatePresetLayout()
1155 {
1156     if (m_presetSlotCount == 0) {
1157         m_cachedPresetLayout = {};
1158         return;
1159     }
1160     // how many degrees each slice will get
1161     // if the slot count is 1, we must divide by 2 to get 180 for the algorithm to work
1162     qreal angleSlice = 360.0 / qMax(m_presetSlotCount, 2);
1163     qreal outerRadius = m_popupPaletteSize/2 - m_presetRingMargin - (m_showRotationTrack ? m_rotationTrackSize + 1 /* half of stroke */ : BORDER_WIDTH);
1164     qreal innerRadius = m_colorHistoryOuterRadius +
1165     (m_showColorHistory
1166         ? 1 /* half of stroke */ + m_presetRingMargin
1167         : 0 /* preset margin is already included in either color history radius when it's not showing */
1168     );
1169     
1170     qreal ringWidth = outerRadius - innerRadius;
1171     qreal halfRingWidth = 0.5 * ringWidth;
1172     qreal ringMidRadius = innerRadius + halfRingWidth;
1173 
1174     // reset the cached layout
1175     m_cachedPresetLayout = {};
1176     CachedPresetLayout& c = m_cachedPresetLayout;
1177 
1178     // note: adding the margin the way it's done
1179     // (calculating the radiuses without taking it into account then subtracting it after)
1180     // is not particularly accurate, but it looks fine since it's so small
1181     int margin = 2;
1182 
1183     // assume one row and get the max radius until the circles would touch
1184     qreal oneRowAngleSlice = angleSlice / 2;
1185     qreal oneRowMaxRadius = ringMidRadius * qSin(qDegreesToRadians(oneRowAngleSlice));
1186 
1187     // if the circles are bigger than the ring we're limited by the
1188     // ring's width instead and only one row would fit
1189     
1190     // oneRowMaxRadius * 0.2 is to make sure that we still do one row if
1191     // there isn't that much of a difference and two would just look weirder
1192     if (oneRowMaxRadius - margin > halfRingWidth - oneRowMaxRadius * 0.2) {
1193         c.ringCount = 1;
1194         c.firstRowRadius = qMin(halfRingWidth, oneRowMaxRadius - margin);
1195         c.firstRowPos = ringMidRadius;
1196         return;
1197     }
1198     
1199     // otherwise 2 or 3 rows always fit
1200     qreal tempRadius = halfRingWidth;
1201     {
1202         // for two rows, the first row is tangent to the inner radius
1203         // and the second row to the outer one
1204         qreal twoRowInnerCount = ceil(qMax(m_presetSlotCount, 2) / 2.0);
1205         qreal twoRowAngleSlice = 360.0 / twoRowInnerCount;
1206 
1207         // we can start at half the ring width and shrink the radius until nothing is overlapping
1208         while (tempRadius >= 0) {
1209             tempRadius -= 0.2;
1210             QPointF r1p1(drawPointOnAngle(twoRowAngleSlice / 2, innerRadius + tempRadius));
1211             QPointF r1p2(drawPointOnAngle(twoRowAngleSlice / 2 * 3, innerRadius + tempRadius));
1212             QPointF r2p(drawPointOnAngle(twoRowAngleSlice, outerRadius - tempRadius));
1213             qreal row1SiblingDistance = kisDistance(r1p1, r1p2);
1214             qreal row1To2Distance = kisDistance(r1p1, r2p);
1215             if (row1To2Distance >= (tempRadius + margin) * 2) {
1216                 // the previous radius is the one that's guaranteed not to be overlapping
1217                 if (row1SiblingDistance < (tempRadius + margin) * 2) {
1218                     // the inner row still overlaps, attempt 3 rows instead
1219                     break;
1220                 }
1221                 c.ringCount = 2;
1222                 c.secondRowRadius = tempRadius;
1223                 c.secondRowPos = outerRadius - tempRadius;
1224                 c.firstRowRadius = tempRadius;
1225                 c.firstRowPos = innerRadius + tempRadius;
1226                 return;
1227             }
1228         }
1229     }
1230 
1231     // for three rows, we initially arrange them like so:
1232     // the first row tangent to the inner radius
1233     // the second row in the middle
1234     // the third row tangent to the outer radius
1235 
1236     qreal threeRowInnerCount = ceil(qMax(m_presetSlotCount, 2) / 3.0);
1237     qreal threeRowAngleSlice = 360.0 / threeRowInnerCount;
1238 
1239     // then we decrease the radius until no row is overlapping eachother or itself
1240     while (tempRadius >= 0) {
1241         QPointF r1p1(drawPointOnAngle(threeRowAngleSlice / 2, innerRadius + tempRadius));
1242         QPointF r1p2(drawPointOnAngle(threeRowAngleSlice / 2 * 3, innerRadius + tempRadius));
1243         QPointF r2p1(drawPointOnAngle(threeRowAngleSlice, ringMidRadius));
1244         QPointF r2p2(drawPointOnAngle(threeRowAngleSlice * 2, ringMidRadius));
1245         QPointF r3p(drawPointOnAngle(threeRowAngleSlice / 2, outerRadius - tempRadius));
1246 
1247         qreal row1SiblingDistance = kisDistance(r1p1, r1p2);
1248         qreal row1to2Distance = kisDistance(r1p1, r2p1);
1249         qreal row2to3Distance = kisDistance(r2p1, r3p);
1250         qreal row1to3Distance = kisDistance(r1p1, r3p);
1251 
1252         if (
1253             row1to2Distance >= tempRadius * 2 &&
1254             row2to3Distance >= tempRadius * 2 &&
1255             row1to3Distance >= tempRadius * 2 &&
1256             row1SiblingDistance >= tempRadius * 2
1257         ) {
1258 
1259             qreal row2SiblingDistance = kisDistance(r2p1, r2p2);
1260 
1261             qreal firstRowRadius = tempRadius;
1262             qreal thirdRowRadius = tempRadius;
1263             qreal secondRowRadius = tempRadius;
1264 
1265             bool firstRowTouching = row1SiblingDistance - firstRowRadius * 2 < 1;
1266             if (firstRowTouching) {
1267                 // attempt to expand the second row
1268                 // and expand + move the third row inwards
1269                 QPointF tempR3p = r3p;
1270                 qreal tempSecondThirdRowRadius = secondRowRadius;
1271                 qreal tempRow2to3Distance = row2to3Distance;
1272                 qreal tempRow1to3Distance = row1to3Distance;
1273                 while (
1274                     tempSecondThirdRowRadius * 2 < tempRow2to3Distance &&
1275                     tempSecondThirdRowRadius * 2 < row2SiblingDistance &&
1276                     tempSecondThirdRowRadius * 2 < tempRow1to3Distance &&
1277                     tempSecondThirdRowRadius + firstRowRadius < row1to2Distance
1278                 ) {
1279                     // the previous temp variables are within limits
1280                     r3p = tempR3p;
1281                     row2to3Distance = tempRow2to3Distance;
1282                     row1to3Distance = tempRow1to3Distance;
1283                     secondRowRadius = tempSecondThirdRowRadius;
1284 
1285                     tempSecondThirdRowRadius += 1;
1286 
1287                     tempR3p = drawPointOnAngle(threeRowAngleSlice / 2, outerRadius - tempSecondThirdRowRadius);
1288 
1289                     tempRow2to3Distance = kisDistance(r2p1, tempR3p);
1290                     tempRow1to3Distance = kisDistance(r1p1, tempR3p);
1291                 }
1292                 thirdRowRadius = secondRowRadius;
1293             }
1294 
1295             {
1296                 // the third row can sometimes be expanded + moved a bit more
1297                 qreal tempThirdRowRadius = thirdRowRadius;
1298                 QPointF tempR3p = r3p;
1299                 qreal tempRow2to3Distance = row2to3Distance;
1300                 qreal tempRow1to3Distance = row1to3Distance;
1301                 while (
1302                     tempThirdRowRadius < halfRingWidth &&
1303                     secondRowRadius + tempThirdRowRadius < tempRow2to3Distance &&
1304                     firstRowRadius + tempThirdRowRadius < tempRow1to3Distance
1305                 ) {
1306                     r3p = tempR3p;
1307                     row2to3Distance = tempRow2to3Distance;
1308                     row1to3Distance = tempRow1to3Distance;
1309                     thirdRowRadius = tempThirdRowRadius;
1310 
1311                     tempThirdRowRadius += 1;
1312 
1313                     tempR3p = drawPointOnAngle(threeRowAngleSlice / 2, outerRadius - tempThirdRowRadius);
1314                     tempRow2to3Distance = kisDistance(r2p1, tempR3p);
1315                     tempRow1to3Distance = kisDistance(r1p1, tempR3p);
1316                 }
1317             }
1318             // the third row is no longer moved
1319             qreal thirdRowPos = outerRadius - thirdRowRadius;
1320 
1321             // many times, e.g. when the second row is touching
1322             // the first row can be moved outwards and expanded
1323             // sometimes it will even detach from the inner radius if the ringwidth is large enough
1324             // and there's a lot of presets
1325             qreal firstRowPos = innerRadius + tempRadius;
1326             {
1327                 qreal tempFirstRowPos = firstRowPos;
1328                 qreal tempFirstRowRadius = firstRowRadius;
1329                 qreal tempRow1SiblingDistance = row1SiblingDistance;
1330                 qreal tempRow1to3Distance = row1to3Distance;
1331                 qreal tempRow1to2Distance = row1to2Distance;
1332                 QPointF tempR1p1 = r1p1;
1333                 QPointF tempR1p2 = r1p2;
1334 
1335                 while (
1336                     tempFirstRowPos < ringMidRadius &&
1337                     tempFirstRowRadius + secondRowRadius < tempRow1to2Distance &&
1338                     tempFirstRowRadius + thirdRowRadius < tempRow1to3Distance
1339                 ) {
1340                     firstRowPos = tempFirstRowPos;
1341                     firstRowRadius = tempFirstRowRadius;
1342                     row1to2Distance = tempRow1to2Distance;
1343                     r1p1 = tempR1p1;
1344                     // these are unused after so it's not neccesary to update them
1345                     // row1to3Distance = tempRow1to3Distance;
1346                     // row1SiblingDistance = tempRow1SiblingDistance;
1347                     // r1p2 = tempR1p2;
1348 
1349                     tempFirstRowPos += 1;
1350 
1351                     tempR1p1 = drawPointOnAngle(threeRowAngleSlice / 2, tempFirstRowPos);
1352                     tempR1p2 = drawPointOnAngle(threeRowAngleSlice / 2 * 3, tempFirstRowPos);
1353                     tempRow1SiblingDistance = kisDistance(tempR1p1, tempR1p2);
1354                     // expand it to the max size
1355                     tempFirstRowRadius = tempRow1SiblingDistance / 2;
1356                     tempRow1to2Distance = kisDistance(tempR1p2, r2p1);
1357                     tempRow1to3Distance = kisDistance(tempR1p1, r3p);
1358                 }
1359             }
1360 
1361             // finally it's rare, but sometimes possible to also move + expand the second row
1362             qreal secondRowPos = ringMidRadius;
1363             bool row2touching1 = row1to2Distance - (firstRowRadius + secondRowRadius) < 1;
1364             bool row2touching3 = row2to3Distance - (thirdRowRadius + secondRowRadius) < 1;
1365             if (!row2touching1 && !row2touching3) {
1366                 // move the second row in until it's touching the first row
1367                 qreal knownAngleRatio = qSin(qDegreesToRadians(threeRowAngleSlice / 2)) /
1368                                         (firstRowRadius + secondRowRadius);
1369                 qreal angleRow1Row2Center = qAsin(knownAngleRatio * firstRowPos);
1370                 qreal angleCenterRow2Row1 = 180 - threeRowAngleSlice / 2 - qRadiansToDegrees(angleRow1Row2Center);
1371                 secondRowPos = qSin(qDegreesToRadians(angleCenterRow2Row1)) / knownAngleRatio;
1372             }
1373             if (!row2touching3) {
1374                 QPointF tempR2p1 = r2p1;
1375                 qreal tempRadius = secondRowRadius;
1376                 qreal tempRow1to2Distance = row1to2Distance;
1377                 qreal tempRow2to3Distance = row2to3Distance;
1378                 qreal tempSecondRowPos = secondRowPos;
1379                 while (
1380                     tempSecondRowPos < thirdRowPos &&
1381                     tempRadius + thirdRowRadius < tempRow2to3Distance &&
1382                     // this is an artificial limit, it could get bigger but looks weird
1383                     tempRadius < thirdRowRadius
1384                 ) {
1385                     secondRowRadius = tempRadius;
1386                     secondRowPos = tempSecondRowPos;
1387                     // these are unused after so it's not neccesary to update them
1388                     // r2p1 = tempR2p1;
1389                     // row1to2Distance = tempRow1to2Distance;
1390                     // row2to3Distance = tempRow2to3Distance;
1391 
1392                     tempSecondRowPos += 1;
1393 
1394                     tempR2p1 = drawPointOnAngle(threeRowAngleSlice, secondRowPos + 1);
1395                     tempRow1to2Distance = kisDistance(tempR2p1, r1p1);
1396                     tempRow2to3Distance = kisDistance(tempR2p1, r3p);
1397                     tempRadius = tempRow1to2Distance - firstRowRadius;
1398                 }
1399             }
1400             c = {
1401                 3, //ringCount
1402                 firstRowRadius - margin,
1403                 secondRowRadius - margin,
1404                 thirdRowRadius - margin,
1405                 firstRowPos,
1406                 secondRowPos,
1407                 thirdRowPos
1408             };
1409             return;
1410         }
1411         tempRadius -= 0.2;
1412     }
1413 }
1414 
1415 QPainterPath KisPopupPalette::createPathFromPresetIndex(int index) const
1416 {
1417     // how many degrees each slice will get
1418     // if the slot count is 1, we must divide by 2 to get 180 for the algorithm to work
1419     qreal angleSlice = 360.0 / qMax(m_presetSlotCount, 2);
1420     // the starting angle of the slice we need to draw. the negative sign makes us go clockwise.
1421     // adding 90 degrees makes us start at the top. otherwise we would start at the right
1422     qreal startingAngle = -(index * angleSlice) + 90;
1423     qreal length = m_cachedPresetLayout.firstRowPos;
1424     qreal radius = m_cachedPresetLayout.firstRowRadius;
1425     switch (m_cachedPresetLayout.ringCount) {
1426     case 1: break;
1427     case 2: {
1428         angleSlice = 180.0/((m_presetSlotCount+1) / 2);
1429         startingAngle = -(index * angleSlice) + 90;
1430 
1431         if (index % 2) {
1432             length = m_cachedPresetLayout.secondRowPos;
1433             radius = m_cachedPresetLayout.secondRowRadius;
1434         }
1435         break;
1436     }
1437     case 3: {
1438         int triplet = index / 3;
1439         angleSlice = 180.0 / ((m_presetSlotCount + 2) / 3);
1440         switch (index % 3) {
1441         case 0:
1442             startingAngle = -(triplet * 2 * angleSlice) + 90;
1443             length = m_cachedPresetLayout.firstRowPos;
1444             radius = m_cachedPresetLayout.firstRowRadius;
1445             break;
1446         case 1:
1447             startingAngle = -(triplet * 2 * angleSlice) + 90;
1448             length = m_cachedPresetLayout.thirdRowPos;
1449             radius = m_cachedPresetLayout.thirdRowRadius;
1450             break;
1451         case 2:
1452             startingAngle = -((triplet * 2 + 1) * angleSlice) + 90;
1453             length = m_cachedPresetLayout.secondRowPos;
1454             radius = m_cachedPresetLayout.secondRowRadius;
1455             break;
1456         default:
1457             KIS_ASSERT(false);
1458         }
1459         break;
1460     }
1461     default:
1462         KIS_ASSERT_RECOVER_NOOP(false);
1463     }
1464     QPainterPath path;
1465     qreal pathX = length * qCos(qDegreesToRadians(startingAngle)) - radius;
1466     qreal pathY = -(length) * qSin(qDegreesToRadians(startingAngle)) - radius;
1467     qreal pathDiameter = 2 * radius; // distance is used to calculate the X/Y in addition to the preset circle size
1468     path.addEllipse(pathX, pathY, pathDiameter, pathDiameter);
1469     return path;
1470 }
1471 
1472 void KisPopupPalette::calculateRotationSnapAreas() {
1473     int i = 0;
1474     for (QRect &rect: m_snapRects) {
1475         QPointF point(drawPointOnAngle(i * 15, m_popupPaletteSize / 2 - BORDER_WIDTH - m_snapRadius/2));
1476         point += QPointF(m_popupPaletteSize / 2 - m_snapRadius, m_popupPaletteSize / 2 - m_snapRadius);
1477         rect = QRect(point.x(), point.y(), m_snapRadius*2, m_snapRadius*2);
1478         i++;
1479     }
1480     i = 0;
1481     for (QLineF &line: m_snapLines) {
1482         qreal penWidth = BORDER_WIDTH / 2;
1483         QPointF point1 = drawPointOnAngle(i * 15, m_popupPaletteSize / 2 - m_rotationTrackSize + penWidth);
1484         point1 += QPointF(m_popupPaletteSize / 2, m_popupPaletteSize / 2);
1485         QPointF point2 = drawPointOnAngle(i * 15, m_popupPaletteSize / 2 - BORDER_WIDTH - penWidth);
1486         point2 += QPointF(m_popupPaletteSize / 2, m_popupPaletteSize / 2);
1487         line = QLineF(point1, point2);
1488         i++;
1489     }
1490 }
1491 
1492 int KisPopupPalette::findPresetSlot(QPointF position) const
1493 {
1494     QPointF adujustedPoint = position - QPointF(m_popupPaletteSize/2, m_popupPaletteSize/2);
1495     for (int i = 0; i < m_presetSlotCount; i++) {
1496         if (createPathFromPresetIndex(i).contains(adujustedPoint)) {
1497             return i;
1498         }
1499     }
1500     return -1;
1501 }