File indexing completed on 2024-05-05 09:12:59

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