File indexing completed on 2024-05-19 04:29:06

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