File indexing completed on 2024-06-16 04:16:15

0001 /*
0002  *  SPDX-FileCopyrightText: 2009 Cyrille Berger <cberger@cberger.net>
0003  *  SPDX-FileCopyrightText: 2021 Deif Lou <ginoba@gmail.com>
0004  *
0005  *  SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 
0008 #include "overviewdocker_dock.h"
0009 #include "overviewwidget.h"
0010 
0011 #include <QLabel>
0012 #include <QVBoxLayout>
0013 #include <QHBoxLayout>
0014 #include <QToolButton>
0015 #include <QStatusBar>
0016 #include <QApplication>
0017 
0018 #include <KisAngleSelector.h>
0019 #include <klocalizedstring.h>
0020 #include "kis_canvas2.h"
0021 #include <KisViewManager.h>
0022 #include <kactioncollection.h>
0023 #include <kis_action.h>
0024 #include <kis_zoom_manager.h>
0025 #include "kis_image.h"
0026 #include "kis_paint_device.h"
0027 #include "kis_signal_compressor.h"
0028 #include "kis_canvas_controller.h"
0029 #include "kis_icon_utils.h"
0030 #include "kis_signals_blocker.h"
0031 #include <KoZoomWidget.h>
0032 #include <kis_icon_utils.h>
0033 
0034 #include <kconfiggroup.h>
0035 #include <ksharedconfig.h>
0036 
0037 OverviewDockerDock::OverviewDockerDock()
0038     : QDockWidget(i18n("Overview"))
0039     , m_lastOverviewMousePos(0.0, 0.0)
0040 {
0041     m_page = new QWidget(this);
0042 
0043     m_overviewWidget = new OverviewWidget(m_page);
0044     m_overviewWidget->setMinimumHeight(50);
0045     m_overviewWidget->setBackgroundRole(QPalette::Base);
0046     // paints background role before paint()
0047     m_overviewWidget->setAutoFillBackground(true);
0048     m_overviewWidget->setAttribute(Qt::WA_AcceptTouchEvents, true);
0049     m_overviewWidget->installEventFilter(this);
0050     connect(m_overviewWidget, SIGNAL(signalDraggingStarted()), SLOT(on_overviewWidget_signalDraggingStarted()));
0051     connect(m_overviewWidget, SIGNAL(signalDraggingFinished()), SLOT(on_overviewWidget_signalDraggingFinished()));
0052 
0053     m_controlsContainer = new QWidget(m_page);
0054 
0055     m_controlsLayout = new QVBoxLayout;
0056     m_controlsLayout->setContentsMargins(2, 2, 2, 2);
0057     m_controlsLayout->setSpacing(2);
0058     m_controlsContainer->setLayout(m_controlsLayout);
0059 
0060     setWidget(m_page);
0061 
0062     m_showControlsTimer.setSingleShot(true);
0063 
0064     m_showControlsAnimation.setEasingCurve(QEasingCurve(QEasingCurve::InOutCubic));
0065     connect(&m_showControlsAnimation, &QVariantAnimation::valueChanged, this, &OverviewDockerDock::layoutMainWidgets);
0066 
0067     KConfigGroup config = KSharedConfig::openConfig()->group("OverviewDocker");
0068     m_pinControls = config.readEntry("pinControls", true);
0069     m_areControlsHidden = !m_pinControls;
0070 
0071     setEnabled(false);
0072 }
0073 
0074 OverviewDockerDock::~OverviewDockerDock()
0075 {
0076     KConfigGroup config = KSharedConfig::openConfig()->group("OverviewDocker");
0077     config.writeEntry("pinControls", m_pinControls);
0078 }
0079 
0080 void OverviewDockerDock::setCanvas(KoCanvasBase * canvas)
0081 {
0082     if(m_canvas == canvas)
0083         return;
0084 
0085     setEnabled(canvas != nullptr);
0086 
0087     if (m_canvas) {
0088         m_canvas->disconnectCanvasObserver(this);
0089         m_canvas->image()->disconnect(this);
0090     }
0091 
0092     if (m_zoomSlider) {
0093         m_controlsSecondRowLayout->removeWidget(m_zoomSlider);
0094         delete m_zoomSlider;
0095         m_zoomSlider = nullptr;
0096     }
0097 
0098     if (m_rotateAngleSelector) {
0099         m_controlsSecondRowLayout->removeWidget(m_rotateAngleSelector);
0100         delete m_rotateAngleSelector;
0101         m_rotateAngleSelector = nullptr;
0102     }
0103 
0104     if (m_mirrorCanvas) {
0105         m_controlsSecondRowLayout->removeWidget(m_mirrorCanvas);
0106         delete m_mirrorCanvas;
0107         m_mirrorCanvas = nullptr;
0108     }
0109 
0110     if (m_pinControlsButton) {
0111         m_controlsSecondRowLayout->removeWidget(m_pinControlsButton);
0112         delete m_pinControlsButton;
0113         m_pinControlsButton = nullptr;
0114     }
0115 
0116     // Delete the stretch
0117     while (m_controlsSecondRowLayout && m_controlsSecondRowLayout->count() && m_controlsSecondRowLayout->itemAt(0)->spacerItem()) {
0118         delete m_controlsSecondRowLayout->takeAt(0);
0119     }
0120 
0121     m_controlsLayout->removeItem(m_controlsSecondRowLayout);
0122 
0123     delete m_controlsSecondRowLayout;
0124 
0125     m_canvas = dynamic_cast<KisCanvas2*>(canvas);
0126 
0127     m_overviewWidget->setCanvas(m_canvas);
0128     if (m_canvas && m_canvas->viewManager() && m_canvas->viewManager()->zoomController() && m_canvas->viewManager()->zoomController()->zoomAction()) {
0129         m_zoomSlider = m_canvas->viewManager()->zoomController()->zoomAction()->createWidget(m_canvas->imageView()->KisView::statusBar());
0130         static_cast<KoZoomWidget*>(m_zoomSlider)->setZoomInputFlat(false);
0131         m_controlsLayout->addWidget(m_zoomSlider);
0132 
0133         m_rotateAngleSelector = new KisAngleSelector();
0134         m_rotateAngleSelector->setRange(-360.00, 360.0);
0135         m_rotateAngleSelector->setAngle(m_canvas->rotationAngle());
0136         m_rotateAngleSelector->setIncreasingDirection(KisAngleGauge::IncreasingDirection_Clockwise);
0137         m_rotateAngleSelector->setFlipOptionsMode(KisAngleSelector::FlipOptionsMode_ContextMenu);
0138         connect(m_rotateAngleSelector, SIGNAL(angleChanged(qreal)), this, SLOT(rotateCanvasView(qreal)), Qt::UniqueConnection);
0139         connect(m_canvas->canvasController()->proxyObject, SIGNAL(canvasOffsetXChanged(int)), this, SLOT(updateSlider()));
0140 
0141         m_mirrorCanvas = new QToolButton();
0142         QList<QAction *> actions = m_canvas->viewManager()->actionCollection()->actions();
0143         Q_FOREACH(QAction* action, actions) {
0144             if (action->objectName()=="mirror_canvas") {
0145                 m_mirrorCanvas->setDefaultAction(action);
0146             }
0147         }
0148         m_mirrorCanvas->setIcon(KisIconUtils::loadIcon("mirror-view-16"));
0149         m_mirrorCanvas->setAutoRaise(true);
0150         connect(m_mirrorCanvas, SIGNAL(toggled(bool)), this, SLOT(mirrorUpdateIcon()));
0151 
0152         m_pinControlsButton = new QToolButton;
0153         m_pinControlsButton->setCheckable(true);
0154         m_pinControlsButton->setChecked(m_pinControls);
0155         m_pinControlsButton->setToolTip(
0156             i18nc("Make the controls in the overview docker auto-hide or always visible", "Pin navigation controls")
0157         );
0158         m_pinControlsButton->setIcon(KisIconUtils::loadIcon("krita_tool_reference_images"));
0159         m_pinControlsButton->setAutoRaise(true);
0160         connect(m_pinControlsButton, SIGNAL(toggled(bool)), SLOT(setPinControls(bool)));
0161 
0162         m_controlsSecondRowLayout = new QHBoxLayout();
0163 
0164         m_controlsSecondRowLayout->addWidget(m_rotateAngleSelector);
0165         m_controlsSecondRowLayout->addStretch();
0166         m_controlsSecondRowLayout->addWidget(m_mirrorCanvas);
0167         m_controlsSecondRowLayout->addStretch();
0168         m_controlsSecondRowLayout->addWidget(m_pinControlsButton);
0169         m_controlsLayout->addLayout(m_controlsSecondRowLayout);
0170 
0171         m_zoomSlider->setVisible(true);
0172         m_rotateAngleSelector->setVisible(true);
0173 
0174         // Show/hide the controls
0175         if (m_pinControls) {
0176             showControls(0);
0177         } else {
0178             if (m_cursorIsHover) {
0179                 showControls(0);
0180             } else {
0181                 hideControls(0);
0182             }
0183         }
0184     }
0185 }
0186 
0187 void OverviewDockerDock::unsetCanvas()
0188 {
0189     setEnabled(false);
0190     m_canvas = nullptr;
0191     m_overviewWidget->setCanvas(0);
0192 }
0193 
0194 void OverviewDockerDock::mirrorUpdateIcon()
0195 {
0196     if(!m_mirrorCanvas) return;
0197     m_mirrorCanvas->setIcon(KisIconUtils::loadIcon("mirror-view-16"));
0198 }
0199 
0200 void OverviewDockerDock::rotateCanvasView(qreal rotation)
0201 {
0202     if (!m_canvas) return;
0203     KisCanvasController *canvasController =
0204             dynamic_cast<KisCanvasController*>(m_canvas->viewManager()->canvasBase()->canvasController());
0205     if (canvasController) {
0206         canvasController->rotateCanvas(rotation-m_canvas->rotationAngle());
0207     }
0208 }
0209 
0210 void OverviewDockerDock::updateSlider()
0211 {
0212     if (!m_canvas) return;
0213     KisSignalsBlocker l(m_rotateAngleSelector);
0214 
0215     m_rotateAngleSelector->setAngle(m_canvas->rotationAngle());
0216 }
0217 
0218 void OverviewDockerDock::setPinControls(bool pin)
0219 {
0220     m_pinControls = pin;
0221 }
0222 
0223 void OverviewDockerDock::resizeEvent(QResizeEvent*)
0224 {
0225     layoutMainWidgets();
0226 }
0227 
0228 void OverviewDockerDock::leaveEvent(QEvent*)
0229 {
0230     m_cursorIsHover = false;
0231     if (isEnabled() && !m_pinControls) {
0232         hideControls(0);
0233         m_cumulatedMouseDistanceSquared = 0.0;
0234     }
0235 }
0236 
0237 void OverviewDockerDock::enterEvent(QEvent*)
0238 {
0239     m_cursorIsHover = true;
0240     if (isEnabled() && !m_pinControls) {
0241         showControls(showControlsTimerDuration);
0242     }
0243 }
0244 
0245 bool OverviewDockerDock::event(QEvent *e)
0246 {
0247     if (e->type() == QEvent::StyleChange || e->type() == QEvent::FontChange) {
0248         resizeEvent(nullptr);
0249     }
0250     return QDockWidget::event(e);
0251 }
0252 
0253 bool OverviewDockerDock::eventFilter(QObject *o, QEvent *e)
0254 {
0255     if (!isEnabled()) {
0256         return false;
0257     }
0258 
0259     if (o == m_overviewWidget) {
0260         // Filter out the mouse events if we are touching the overview widget
0261         // and the event was not synthesized in this function  from the touch events
0262         if (e->type() == QEvent::MouseButtonPress) {
0263             if (m_isTouching) {
0264                 return static_cast<QMouseEvent*>(e)->source() != Qt::MouseEventSynthesizedByApplication;
0265             }
0266 
0267         } else if (e->type() == QEvent::MouseButtonRelease) {
0268             if (m_isTouching) {
0269                 return static_cast<QMouseEvent*>(e)->source() != Qt::MouseEventSynthesizedByApplication;
0270             }
0271 
0272         } else if (e->type() == QEvent::MouseMove) {
0273             if (m_isTouching) {
0274                 return static_cast<QMouseEvent*>(e)->source() != Qt::MouseEventSynthesizedByApplication;
0275             }
0276             if (!m_overviewWidget->isDragging() && m_areControlsHidden && !m_pinControls) {
0277                 QMouseEvent *me = static_cast<QMouseEvent*>(e);
0278                 constexpr double showControlsAreaRadiusSquared = showControlsAreaRadius * showControlsAreaRadius;
0279                 const QPointF d = me->localPos() - m_lastOverviewMousePos;
0280                 const double distanceSquared = d.x() * d.x() + d.y() * d.y();
0281                 if (distanceSquared > m_cumulatedMouseDistanceSquared) {
0282                     if (distanceSquared >= showControlsAreaRadiusSquared) {
0283                         showControls(showControlsTimerDuration);
0284                         m_lastOverviewMousePos = me->localPos();
0285                         m_cumulatedMouseDistanceSquared = 0.0;
0286                     } else {
0287                         m_cumulatedMouseDistanceSquared = distanceSquared;
0288                     }
0289                 }
0290             }
0291 
0292         } else if (e->type() == QEvent::TouchBegin) {
0293             if (!m_isTouching) {
0294                 QTouchEvent *te = static_cast<QTouchEvent*>(e);
0295                 m_isTouching = true;
0296                 // Store the first touch point. We will only track this one
0297                 m_touchPointId = te->touchPoints().first().id();
0298                 m_lastTouchPos = te->touchPoints().first().pos();
0299             }
0300             // Accept the event so that other touch events keep coming
0301             e->accept();
0302             return true;
0303 
0304         } else if (e->type() == QEvent::TouchUpdate) {
0305             if (!m_isTouching) {
0306                 return true;
0307             }
0308             QTouchEvent *te = static_cast<QTouchEvent*>(e);
0309             // Get the touch point position
0310             QPointF currentPosition;
0311             for (const QTouchEvent::TouchPoint &touchPoint : te->touchPoints()) {
0312                 if (touchPoint.id() == m_touchPointId) {
0313                     // If the touch point wasn't moved, this event wasn't
0314                     // generated from our touch point
0315                     if (touchPoint.state() == Qt::TouchPointStationary) {
0316                         return true;
0317                     }
0318                     currentPosition = touchPoint.pos();
0319                     break;
0320                 }
0321             }
0322             if (!m_isDraggingWithTouch) {
0323                 // Compute distance
0324                 const QPointF delta = currentPosition - m_lastTouchPos;
0325                 const qreal distanceSquared = delta.x() * delta.x() + delta.y() * delta.y();
0326                 if (distanceSquared >= touchDragDistanceSquared) {
0327                     m_isDraggingWithTouch = true;
0328                     // synthesize mouse press event
0329                     QMouseEvent *se = new QMouseEvent(QEvent::MouseButtonPress,
0330                                                       m_lastTouchPos, QPointF(), QPointF(),
0331                                                       Qt::LeftButton, Qt::LeftButton, Qt::NoModifier,
0332                                                       Qt::MouseEventSynthesizedByApplication);
0333                     qApp->sendEvent(m_overviewWidget, se);
0334                 }
0335             }
0336             if (m_isDraggingWithTouch) {
0337                 // Synthesize mouse move event
0338                 QMouseEvent *se = new QMouseEvent(QEvent::MouseMove,
0339                                                   currentPosition, QPointF(), QPointF(),
0340                                                   Qt::LeftButton, Qt::LeftButton, Qt::NoModifier,
0341                                                   Qt::MouseEventSynthesizedByApplication);
0342                 qApp->sendEvent(m_overviewWidget, se);
0343                 // Update
0344                 m_lastTouchPos = currentPosition;
0345             }
0346             return true;
0347 
0348         } else if (e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) {
0349             if (!m_isTouching) {
0350                 return true;
0351             }
0352             QTouchEvent *te = static_cast<QTouchEvent*>(e);
0353             if (e->type() == QEvent::TouchEnd) {
0354                 // If the touch point is not in the released state
0355                 // then this event wasn't generated from our touch point
0356                 for (const QTouchEvent::TouchPoint &touchPoint : te->touchPoints()) {
0357                     if (touchPoint.id() == m_touchPointId) {
0358                         if (touchPoint.state() != Qt::TouchPointReleased) {
0359                             return true;
0360                         }
0361                         break;
0362                     }
0363                 }
0364             }
0365             // If we are dragging then synthesize mouse release event.
0366             // Show/hide the controls otherwise
0367             if (m_isDraggingWithTouch) {
0368                 QMouseEvent *se = new QMouseEvent(QEvent::MouseButtonRelease,
0369                                                   m_lastTouchPos, QPointF(), QPointF(),
0370                                                   Qt::LeftButton, Qt::LeftButton, Qt::NoModifier,
0371                                                   Qt::MouseEventSynthesizedByApplication);
0372                 qApp->sendEvent(m_overviewWidget, se);
0373             } else if (e->type() == QEvent::TouchEnd) {
0374                 m_pinControls = m_areControlsHidden;
0375                 KisSignalsBlocker blocker(m_pinControlsButton);
0376                 m_pinControlsButton->setChecked(m_pinControls);
0377                 if (m_areControlsHidden) {
0378                     showControls(0);
0379                 } else {
0380                     hideControls(0);
0381                 }
0382             }
0383             // Reset
0384             m_isTouching = false;
0385             m_isDraggingWithTouch = false;
0386             return true;
0387         }
0388     }
0389     return false;
0390 }
0391 
0392 void OverviewDockerDock::layoutMainWidgets()
0393 {
0394     m_page->setMinimumHeight(m_overviewWidget->minimumHeight() +
0395                              m_controlsContainer->minimumSizeHint().height());
0396 
0397     if (m_showControlsAnimation.state() == QVariantAnimation::Running) {
0398         const qreal pageHeight = static_cast<qreal>(m_page->height());
0399         const qreal controlsContainerHeight = static_cast<qreal>(m_controlsContainer->sizeHint().height());
0400         const qreal animationProgress = m_showControlsAnimation.currentValue().toReal();
0401         const int widgetLimitPosition = static_cast<int>(std::round(pageHeight - animationProgress * controlsContainerHeight));
0402         m_overviewWidget->setGeometry(0, 0, m_page->width(), widgetLimitPosition);
0403         m_controlsContainer->setGeometry(0, widgetLimitPosition, m_page->width(), static_cast<int>(controlsContainerHeight));
0404     } else {
0405         const int controlsContainerHeight = m_controlsContainer->sizeHint().height();
0406         if (m_pinControls || !m_areControlsHidden) {
0407             const int widgetLimitPosition = m_page->height() - controlsContainerHeight;
0408             m_overviewWidget->setGeometry(0, 0, m_page->width(), widgetLimitPosition);
0409             m_controlsContainer->setGeometry(0, widgetLimitPosition, m_page->width(), controlsContainerHeight);
0410         } else {
0411             m_overviewWidget->setGeometry(0, 0, m_page->width(), m_page->height());
0412             m_controlsContainer->setGeometry(0, m_page->height(), m_page->width(), controlsContainerHeight);
0413         }
0414     }
0415 }
0416 
0417 void OverviewDockerDock::showControls(int delay) const
0418 {
0419     auto animFunction =
0420         [this]() -> void
0421         {
0422             int animationDuration;
0423             qreal animationStartValue;
0424 
0425             if (m_areControlsHidden) {
0426                 if (m_showControlsAnimation.state() == QVariantAnimation::Running) {
0427                     m_showControlsAnimation.stop();
0428                     animationDuration =
0429                         static_cast<int>(std::round((1.0 - m_showControlsAnimation.currentValue().toReal()) * showControlsAnimationDuration));
0430                     animationStartValue = m_showControlsAnimation.currentValue().toReal();
0431                 } else {
0432                     animationDuration = showControlsAnimationDuration;
0433                     animationStartValue = 0.0;
0434                 }
0435             } else {
0436                 animationDuration = 1;
0437                 animationStartValue = 1.0;
0438             }
0439 
0440             m_areControlsHidden = false;
0441             m_showControlsAnimation.setStartValue(animationStartValue);
0442             m_showControlsAnimation.setEndValue(1.0);
0443             m_showControlsAnimation.setDuration(animationDuration);
0444             m_showControlsAnimation.start();
0445         };
0446 
0447     delay = qMax(delay, 0);
0448 
0449     m_showControlsTimer.disconnect();
0450     connect(&m_showControlsTimer, &QTimer::timeout, animFunction);
0451     m_showControlsTimer.start(delay);
0452 }
0453 
0454 void OverviewDockerDock::hideControls(int delay) const
0455 {
0456     auto animFunction =
0457         [this]() -> void
0458         {
0459             int animationDuration;
0460             qreal animationStartValue;
0461 
0462             if (!m_areControlsHidden) {
0463                 if (m_showControlsAnimation.state() == QVariantAnimation::Running) {
0464                     m_showControlsAnimation.stop();
0465                     animationDuration =
0466                         static_cast<int>(std::round(m_showControlsAnimation.currentValue().toReal() * showControlsAnimationDuration));
0467                     animationStartValue = m_showControlsAnimation.currentValue().toReal();
0468                 } else {
0469                     animationDuration = showControlsAnimationDuration;
0470                     animationStartValue = 1.0;
0471                 }
0472             } else {
0473                 animationDuration = 1;
0474                 animationStartValue = 0.0;
0475             }
0476 
0477             m_areControlsHidden = true;
0478             m_showControlsAnimation.setStartValue(animationStartValue);
0479             m_showControlsAnimation.setEndValue(0.0);
0480             m_showControlsAnimation.setDuration(animationDuration);
0481             m_showControlsAnimation.start();
0482         };
0483 
0484     delay = qMax(delay, 0);
0485 
0486     m_showControlsTimer.disconnect();
0487     connect(&m_showControlsTimer, &QTimer::timeout, animFunction);
0488     m_showControlsTimer.start(delay);
0489 }
0490 
0491 void OverviewDockerDock::on_overviewWidget_signalDraggingStarted()
0492 {
0493     if (!m_pinControls && m_areControlsHidden && m_showControlsTimer.isActive()) {
0494         m_showControlsTimer.stop();
0495     }
0496 }
0497 
0498 void OverviewDockerDock::on_overviewWidget_signalDraggingFinished()
0499 {
0500     if (!m_pinControls && m_areControlsHidden && !m_isTouching) {
0501         showControls(showControlsTimerDuration);
0502     }
0503 }