File indexing completed on 2024-04-21 04:58:38

0001 /*
0002     SPDX-FileCopyrightText: 2007-2008 Urs Wolfer <uwolfer@kde.org>
0003     Parts of this file have been take from okular:
0004     SPDX-FileCopyrightText: 2004-2005 Enrico Ros <eros.kde@email.it>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later
0007 */
0008 
0009 #include "floatingtoolbar.h"
0010 #include "krdc_debug.h"
0011 
0012 #include <QApplication>
0013 #include <QBitmap>
0014 #include <QMouseEvent>
0015 #include <QPainter>
0016 #include <QStyle>
0017 #include <QTimer>
0018 
0019 static const int actionIconSize = 22;
0020 static const int toolBarRBMargin = 2;
0021 static const double toolBarOpacity = 0.8;
0022 static const int visiblePixelWhenAutoHidden = 6;
0023 static const int autoHideTimeout = 500;
0024 static const int initialAutoHideTimeout = 2000;
0025 
0026 /**
0027  * Denotes the various states of the animation.
0028  */
0029 enum AnimState { Hiding, Showing, Still };
0030 
0031 class FloatingToolBarPrivate
0032 {
0033 public:
0034     FloatingToolBarPrivate(FloatingToolBar *qq)
0035         : q(qq)
0036         , anchorSide(FloatingToolBar::Left)
0037         , offsetPlaceHolder(new QWidget(qq))
0038         , animState(Still)
0039         , toDelete(false)
0040         , visible(false)
0041         , sticky(false)
0042         , opacity(toolBarOpacity)
0043         // set queuedShow to true so we show the toolbar if we get a resize event on the anchorWidget
0044         , queuedShow(true)
0045     {
0046     }
0047 
0048     // rebuild contents and reposition then widget
0049     void buildToolBar();
0050     void reposition();
0051     // compute the visible and hidden positions along current side
0052     QPoint getInnerPoint() const;
0053     QPoint getOuterPoint() const;
0054 
0055     FloatingToolBar *q;
0056 
0057     QWidget *anchorWidget;
0058     FloatingToolBar::Side anchorSide;
0059     QWidget *offsetPlaceHolder;
0060 
0061     QTimer *animTimer;
0062     QTimer *autoHideTimer;
0063     QPoint currentPosition;
0064     QPoint endPosition;
0065     AnimState animState;
0066     bool toDelete;
0067     bool visible;
0068     bool sticky;
0069     qreal opacity;
0070     bool queuedShow;
0071 
0072     QPixmap backgroundPixmap;
0073 };
0074 
0075 FloatingToolBar::FloatingToolBar(QWidget *parent, QWidget *anchorWidget)
0076     : QToolBar(parent)
0077     , d(new FloatingToolBarPrivate(this))
0078 {
0079     ;
0080     addWidget(d->offsetPlaceHolder);
0081 
0082     setMouseTracking(true);
0083     setIconSize(QSize(actionIconSize, actionIconSize));
0084     d->anchorWidget = anchorWidget;
0085 
0086     d->animTimer = new QTimer(this);
0087     connect(d->animTimer, SIGNAL(timeout()), this, SLOT(animate()));
0088 
0089     d->autoHideTimer = new QTimer(this);
0090     connect(d->autoHideTimer, SIGNAL(timeout()), this, SLOT(hide()));
0091 
0092     // apply a filter to get notified when anchor changes geometry
0093     d->anchorWidget->installEventFilter(this);
0094 }
0095 
0096 FloatingToolBar::~FloatingToolBar()
0097 {
0098     delete d;
0099 }
0100 
0101 void FloatingToolBar::addAction(QAction *action)
0102 {
0103     QToolBar::addAction(action);
0104 
0105     // rebuild toolbar shape and contents only if the toolbar is already visible,
0106     // otherwise it will be done in showAndAnimate()
0107     if (isVisible())
0108         d->reposition();
0109 }
0110 
0111 void FloatingToolBar::setSide(Side side)
0112 {
0113     d->anchorSide = side;
0114 
0115     if (isVisible())
0116         d->reposition();
0117 }
0118 
0119 void FloatingToolBar::setSticky(bool sticky)
0120 {
0121     d->sticky = sticky;
0122 
0123     if (sticky)
0124         d->autoHideTimer->stop();
0125 }
0126 
0127 void FloatingToolBar::showAndAnimate()
0128 {
0129     if (d->animState == Showing)
0130         return;
0131 
0132     d->animState = Showing;
0133 
0134     show();
0135 
0136     // force update for case when toolbar has not been built yet
0137     d->reposition();
0138 
0139     // start scrolling in
0140     d->animTimer->start(20);
0141 
0142     // This permits to show the toolbar for a while when going full screen.
0143     if (!d->sticky)
0144         d->autoHideTimer->start(initialAutoHideTimeout);
0145 }
0146 
0147 void FloatingToolBar::hideAndDestroy()
0148 {
0149     if (d->animState == Hiding)
0150         return;
0151 
0152     // set parameters for sliding out
0153     d->animState = Hiding;
0154     d->toDelete = true;
0155     d->endPosition = d->getOuterPoint();
0156 
0157     // start scrolling out
0158     d->animTimer->start(20);
0159 }
0160 
0161 void FloatingToolBar::hide()
0162 {
0163     if (underMouse())
0164         return;
0165 
0166     if (d->visible) {
0167         QPoint diff;
0168         switch (d->anchorSide) {
0169         case Left:
0170             diff = QPoint(visiblePixelWhenAutoHidden, 0);
0171             break;
0172         case Right:
0173             diff = QPoint(-visiblePixelWhenAutoHidden, 0);
0174             break;
0175         case Top:
0176             diff = QPoint(0, visiblePixelWhenAutoHidden);
0177             break;
0178         case Bottom:
0179             diff = QPoint(0, -visiblePixelWhenAutoHidden);
0180             break;
0181         }
0182         d->animState = Hiding;
0183         d->endPosition = d->getOuterPoint() + diff;
0184 
0185         // start scrolling out
0186         d->animTimer->start(20);
0187     }
0188 }
0189 
0190 bool FloatingToolBar::eventFilter(QObject *obj, QEvent *e)
0191 {
0192     if (obj == d->anchorWidget && e->type() == QEvent::Resize) {
0193         if (d->queuedShow) { // if the toolbar is not visible yet, try to show it if the anchor widget is in fullscreen already
0194             d->queuedShow = false;
0195             showAndAnimate();
0196             return true;
0197         }
0198 
0199         // if anchorWidget changed geometry reposition toolbar
0200         d->animTimer->stop();
0201         if ((d->animState == Hiding || !d->visible) && d->toDelete)
0202             deleteLater();
0203         else
0204             d->reposition();
0205     }
0206 
0207     return QToolBar::eventFilter(obj, e);
0208 }
0209 
0210 void FloatingToolBar::paintEvent(QPaintEvent *e)
0211 {
0212     QToolBar::paintEvent(e);
0213 
0214     // paint the internal pixmap over the widget
0215     QPainter p(this);
0216     p.setOpacity(d->opacity);
0217     p.drawImage(e->rect().topLeft(), d->backgroundPixmap.toImage(), e->rect());
0218 }
0219 
0220 void FloatingToolBar::mousePressEvent(QMouseEvent *e)
0221 {
0222     if (e->button() == Qt::LeftButton)
0223         setCursor(Qt::SizeAllCursor);
0224 
0225     QToolBar::mousePressEvent(e);
0226 }
0227 
0228 void FloatingToolBar::mouseMoveEvent(QMouseEvent *e)
0229 {
0230     // show the toolbar again when it is auto-hidden
0231     if (!d->visible) {
0232         showAndAnimate();
0233         return;
0234     }
0235 
0236     if ((QApplication::mouseButtons() & Qt::LeftButton) != Qt::LeftButton)
0237         return;
0238 
0239     // compute the nearest side to attach the widget to
0240     const QPoint parentPos = mapToParent(e->pos());
0241     const float nX = (float)parentPos.x() / (float)d->anchorWidget->width();
0242     const float nY = (float)parentPos.y() / (float)d->anchorWidget->height();
0243     if (nX > 0.3 && nX < 0.7 && nY > 0.3 && nY < 0.7)
0244         return;
0245     bool LT = nX < (1.0 - nY);
0246     bool LB = nX < (nY);
0247     Side side = LT ? (LB ? Left : Top) : (LB ? Bottom : Right);
0248 
0249     // check if side changed
0250     if (side == d->anchorSide)
0251         return;
0252 
0253     d->anchorSide = side;
0254     d->reposition();
0255     Q_EMIT orientationChanged((int)side);
0256 
0257     QToolBar::mouseMoveEvent(e);
0258 }
0259 
0260 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0261 void FloatingToolBar::enterEvent(QEnterEvent *e)
0262 #else
0263 void FloatingToolBar::enterEvent(QEvent *e)
0264 #endif
0265 {
0266     // Stop the autohide timer while the mouse is inside
0267     d->autoHideTimer->stop();
0268 
0269     if (!d->visible)
0270         showAndAnimate();
0271     QToolBar::enterEvent(e);
0272 }
0273 
0274 void FloatingToolBar::leaveEvent(QEvent *e)
0275 {
0276     if (!d->sticky)
0277         d->autoHideTimer->start(autoHideTimeout);
0278     QToolBar::leaveEvent(e);
0279 }
0280 
0281 void FloatingToolBar::mouseReleaseEvent(QMouseEvent *e)
0282 {
0283     if (e->button() == Qt::LeftButton)
0284         setCursor(Qt::ArrowCursor);
0285 
0286     QToolBar::mouseReleaseEvent(e);
0287 }
0288 
0289 void FloatingToolBar::wheelEvent(QWheelEvent *e)
0290 {
0291     e->accept();
0292 
0293     const qreal diff = e->angleDelta().y() / 100.0 / 15.0;
0294     //    qCDebug(KRDC) << diff;
0295     if (((d->opacity <= 1) && (diff > 0)) || ((d->opacity >= 0) && (diff < 0)))
0296         d->opacity += diff;
0297 
0298     update();
0299 
0300     QToolBar::wheelEvent(e);
0301 }
0302 
0303 void FloatingToolBarPrivate::buildToolBar()
0304 {
0305     const bool prevUpdates = q->updatesEnabled();
0306     q->setUpdatesEnabled(false);
0307 
0308     // 1. init numbers we are going to use
0309     const bool topLeft = anchorSide == FloatingToolBar::Left || anchorSide == FloatingToolBar::Top;
0310     const bool vertical = anchorSide == FloatingToolBar::Left || anchorSide == FloatingToolBar::Right;
0311 
0312     if (vertical) {
0313         offsetPlaceHolder->setFixedSize(1, 7);
0314         q->setOrientation(Qt::Vertical);
0315     } else {
0316         offsetPlaceHolder->setFixedSize(7, 1);
0317         q->setOrientation(Qt::Horizontal);
0318     }
0319 
0320     // 2. compute widget size
0321     const int myWidth = q->sizeHint().width() - 1;
0322     const int myHeight = q->sizeHint().height() - 1;
0323 
0324     // 3. resize pixmap, mask and widget
0325     QBitmap mask(myWidth + 1, myHeight + 1);
0326     backgroundPixmap = QPixmap(myWidth + 1, myHeight + 1);
0327     backgroundPixmap.fill(Qt::transparent);
0328 
0329     q->resize(myWidth + 1, myHeight + 1);
0330 
0331     // 4. create and set transparency mask
0332     QPainter maskPainter(&mask);
0333     mask.fill(Qt::white);
0334     maskPainter.setBrush(Qt::black);
0335     if (vertical)
0336         maskPainter.drawRoundedRect(topLeft ? -10 : 0, 0, myWidth + 10, myHeight, 2000 / (myWidth + 10), 2000 / myHeight, Qt::RelativeSize);
0337     else
0338         maskPainter.drawRoundedRect(0, topLeft ? -10 : 0, myWidth, myHeight + 10, 2000 / myWidth, 2000 / (myHeight + 10), Qt::RelativeSize);
0339     maskPainter.end();
0340     q->setMask(mask);
0341 
0342     // 5. draw background
0343     QPainter bufferPainter(&backgroundPixmap);
0344     bufferPainter.translate(0.5, 0.5);
0345     QPalette pal = q->palette();
0346     // 5.1. draw horizontal/vertical gradient
0347     QLinearGradient grad;
0348     switch (anchorSide) {
0349     case FloatingToolBar::Left:
0350         grad = QLinearGradient(0, 1, myWidth + 1, 1);
0351         break;
0352     case FloatingToolBar::Right:
0353         grad = QLinearGradient(myWidth + 1, 1, 0, 1);
0354         break;
0355     case FloatingToolBar::Top:
0356         grad = QLinearGradient(1, 0, 1, myHeight + 1);
0357         break;
0358     case FloatingToolBar::Bottom:
0359         grad = QLinearGradient(1, myHeight + 1, 0, 1);
0360         break;
0361     }
0362     grad.setColorAt(0, pal.color(QPalette::Active, QPalette::Button));
0363     grad.setColorAt(1, pal.color(QPalette::Active, QPalette::Light));
0364     bufferPainter.setBrush(QBrush(grad));
0365     // 5.2. draw rounded border
0366     bufferPainter.setPen(pal.color(QPalette::Active, QPalette::Dark).lighter(40));
0367     bufferPainter.setRenderHints(QPainter::Antialiasing);
0368     if (vertical)
0369         bufferPainter.drawRoundedRect(topLeft ? -10 : 0, 0, myWidth + 10, myHeight, 2000 / (myWidth + 10), 2000 / myHeight, Qt::RelativeSize);
0370     else
0371         bufferPainter.drawRoundedRect(0, topLeft ? -10 : 0, myWidth, myHeight + 10, 2000 / myWidth, 2000 / (myHeight + 10), Qt::RelativeSize);
0372     // 5.3. draw handle
0373     bufferPainter.translate(-0.5, -0.5);
0374     bufferPainter.setPen(pal.color(QPalette::Active, QPalette::Mid));
0375     if (vertical) {
0376         int dx = anchorSide == FloatingToolBar::Left ? 2 : 4;
0377         bufferPainter.drawLine(dx, 6, dx + myWidth - 8, 6);
0378         bufferPainter.drawLine(dx, 9, dx + myWidth - 8, 9);
0379         bufferPainter.setPen(pal.color(QPalette::Active, QPalette::Light));
0380         bufferPainter.drawLine(dx + 1, 7, dx + myWidth - 7, 7);
0381         bufferPainter.drawLine(dx + 1, 10, dx + myWidth - 7, 10);
0382     } else {
0383         int dy = anchorSide == FloatingToolBar::Top ? 2 : 4;
0384         bufferPainter.drawLine(6, dy, 6, dy + myHeight - 8);
0385         bufferPainter.drawLine(9, dy, 9, dy + myHeight - 8);
0386         bufferPainter.setPen(pal.color(QPalette::Active, QPalette::Light));
0387         bufferPainter.drawLine(7, dy + 1, 7, dy + myHeight - 7);
0388         bufferPainter.drawLine(10, dy + 1, 10, dy + myHeight - 7);
0389     }
0390 
0391     q->setUpdatesEnabled(prevUpdates);
0392 }
0393 
0394 void FloatingToolBarPrivate::reposition()
0395 {
0396     // note: hiding widget here will gives better gfx, but ends drag operation
0397     // rebuild widget and move it to its final place
0398     buildToolBar();
0399     if (!visible) {
0400         currentPosition = getOuterPoint();
0401         endPosition = getInnerPoint();
0402     } else {
0403         currentPosition = getInnerPoint();
0404         endPosition = getOuterPoint();
0405     }
0406     q->move(currentPosition);
0407 }
0408 
0409 QPoint FloatingToolBarPrivate::getInnerPoint() const
0410 {
0411     // returns the final position of the widget
0412     if (anchorSide == FloatingToolBar::Left)
0413         return QPoint(0, (anchorWidget->height() - q->height()) / 2);
0414     if (anchorSide == FloatingToolBar::Top)
0415         return QPoint((anchorWidget->width() - q->width()) / 2, 0);
0416     if (anchorSide == FloatingToolBar::Right)
0417         return QPoint(anchorWidget->width() - q->width() + toolBarRBMargin, (anchorWidget->height() - q->height()) / 2);
0418     return QPoint((anchorWidget->width() - q->width()) / 2, anchorWidget->height() - q->height() + toolBarRBMargin);
0419 }
0420 
0421 QPoint FloatingToolBarPrivate::getOuterPoint() const
0422 {
0423     // returns the point from which the transition starts
0424     if (anchorSide == FloatingToolBar::Left)
0425         return QPoint(-q->width(), (anchorWidget->height() - q->height()) / 2);
0426     if (anchorSide == FloatingToolBar::Top)
0427         return QPoint((anchorWidget->width() - q->width()) / 2, -q->height());
0428     if (anchorSide == FloatingToolBar::Right)
0429         return QPoint(anchorWidget->width() + toolBarRBMargin, (anchorWidget->height() - q->height()) / 2);
0430     return QPoint((anchorWidget->width() - q->width()) / 2, anchorWidget->height() + toolBarRBMargin);
0431 }
0432 
0433 void FloatingToolBar::animate()
0434 {
0435     if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this)) {
0436         // move currentPosition towards endPosition
0437         int dX = d->endPosition.x() - d->currentPosition.x();
0438         int dY = d->endPosition.y() - d->currentPosition.y();
0439         dX = dX / 6 + qMax(-1, qMin(1, dX));
0440         dY = dY / 6 + qMax(-1, qMin(1, dY));
0441         d->currentPosition.setX(d->currentPosition.x() + dX);
0442         d->currentPosition.setY(d->currentPosition.y() + dY);
0443     } else {
0444         d->currentPosition = d->endPosition;
0445     }
0446 
0447     move(d->currentPosition);
0448 
0449     // handle arrival to the end
0450     if (d->currentPosition == d->endPosition) {
0451         d->animTimer->stop();
0452         switch (d->animState) {
0453         case Hiding:
0454             d->visible = false;
0455             d->animState = Still;
0456             if (d->toDelete)
0457                 deleteLater();
0458             break;
0459         case Showing:
0460             d->visible = true;
0461             d->animState = Still;
0462             break;
0463         default:
0464             qCDebug(KRDC) << "Illegal state";
0465         }
0466     }
0467 }