File indexing completed on 2024-05-19 04:36:39

0001 /* This file is part of the TikZKit project.
0002  *
0003  * Copyright (C) 2016 Dominik Haumann <dhaumann@kde.org>
0004  *
0005  * This library is free software; you can redistribute it and/or modify
0006  * it under the terms of the GNU Library General Public License as published
0007  * by the Free Software Foundation, either version 2 of the License, or
0008  * (at your option) any later version.
0009  *
0010  * This library is distributed in the hope that it will be useful,
0011  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0012  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013  * GNU Library General Public License for more details.
0014  *
0015  * You should have received a copy of the GNU Library General Public License
0016  * along with this library; see the file COPYING.LIB.  If not, see
0017  * <http://www.gnu.org/licenses/>.
0018  */
0019 #include "ZoomController.h"
0020 
0021 #include <QComboBox>
0022 #include <QLineEdit>
0023 #include <QPinchGesture>
0024 #include <QTimeLine>
0025 #include <QValidator>
0026 #include <QWheelEvent>
0027 
0028 #include <cmath>
0029 
0030 namespace {
0031     QString zoomToString(qreal zoom)
0032     {
0033         const qreal z = qRound(zoom * 10000.0) / 100.0;
0034         return QString(QLatin1String("%1 %")).arg(z);
0035     }
0036 }
0037 
0038 namespace tikz {
0039 namespace ui {
0040 
0041 ZoomController::ZoomController(QObject *parent)
0042     : QObject(parent)
0043     // NOTE: add pre-defined zoom entries here:
0044     , m_zoomFactors({
0045         5.0,
0046         4.0,
0047         3.0,
0048         2.0,
0049         1.5,
0050         1.0,
0051         0.75,
0052         0.5,
0053         0.33,
0054         0.25,
0055         0.10})
0056     , m_inputRegExp(QLatin1String("^\\s*((\\d+)(\\.\\d+)?)\\s*%?\\s*$"))
0057 {
0058     // zoom animation should take 100ms
0059     m_animationTimeLine = new QTimeLine(100, this);
0060     connect(m_animationTimeLine, &QTimeLine::valueChanged, this, &ZoomController::setZoomAnimated);
0061     connect(m_animationTimeLine, &QTimeLine::finished, this, &ZoomController::syncComboBox);
0062 }
0063 
0064 ZoomController::~ZoomController()
0065 {
0066 }
0067 
0068 void ZoomController::setZoom(qreal z)
0069 {
0070     if (z == m_currentZoom) {
0071         return;
0072     }
0073 
0074     m_currentZoom = z;
0075 
0076     syncComboBox();
0077 
0078     Q_EMIT zoomChanged(m_currentZoom);
0079 }
0080 
0081 qreal ZoomController::currentZoom() const
0082 {
0083     return m_currentZoom;
0084 }
0085 
0086 bool ZoomController::canZoomIn() const
0087 {
0088     return m_currentZoom < m_zoomFactors.first();
0089 }
0090 
0091 bool ZoomController::canZoomOut() const
0092 {
0093     return m_currentZoom > m_zoomFactors.back();
0094 }
0095 
0096 void ZoomController::processWheelEvent(int delta)
0097 {
0098     if (delta < 0) {
0099         zoomOut(true);
0100     } else if (delta >= 0) {
0101         zoomIn(true);
0102     }
0103 }
0104 
0105 void ZoomController::zoomIn(bool animated)
0106 {
0107     if (m_animationTimeLine->state() == QTimeLine::Running) {
0108         return;
0109     }
0110 
0111     for (int i = m_zoomFactors.count() - 1; i >= 0; --i) {
0112         if (m_zoomFactors[i] > m_currentZoom) {
0113             if (animated) {
0114                 startZoomAnimation(m_currentZoom, m_zoomFactors[i]);
0115             } else {
0116                 setZoom(m_zoomFactors[i]);
0117             }
0118             break;
0119         }
0120     }
0121 }
0122 
0123 void ZoomController::zoomOut(bool animated)
0124 {
0125     if (m_animationTimeLine->state() == QTimeLine::Running) {
0126         return;
0127     }
0128 
0129     for (qreal z : m_zoomFactors) {
0130         if (z < m_currentZoom) {
0131             if (animated) {
0132                 startZoomAnimation(m_currentZoom, z);
0133             } else {
0134                 setZoom(z);
0135             }
0136             break;
0137         }
0138     }
0139 }
0140 
0141 void ZoomController::startZoomAnimation(qreal startZoom, qreal endZoom)
0142 {
0143     if (m_animationTimeLine->state() == QTimeLine::Running) {
0144         return;
0145     }
0146 
0147     m_startZoom = startZoom;
0148     m_endZoom = endZoom;
0149     m_animationTimeLine->start();
0150 }
0151 
0152 void ZoomController::setZoomAnimated(qreal value)
0153 {
0154     setZoom((1.0 - value) * m_startZoom + value * m_endZoom);
0155 }
0156 
0157 void ZoomController::resetZoom()
0158 {
0159     setZoom(1);
0160 }
0161 
0162 void ZoomController::setZoomFactors(const QVector<qreal>& factors)
0163 {
0164     m_zoomFactors = factors;
0165 }
0166 
0167 void ZoomController::attachToComboBox(QComboBox *comboBox)
0168 {
0169     if (m_comboBox == comboBox) {
0170         return;
0171     }
0172 
0173     if (m_comboBox) {
0174         m_comboBox->removeEventFilter(this);
0175         m_comboBox->disconnect(this);
0176         if (m_comboBox->lineEdit()) {
0177             m_comboBox->lineEdit()->disconnect(this);
0178         }
0179         m_comboBox->setValidator(nullptr);
0180     }
0181 
0182     m_comboBox = comboBox;
0183 
0184     if (m_comboBox) {
0185         m_comboBox->clear();
0186         for (qreal z : m_zoomFactors) {
0187             m_comboBox->addItem(zoomToString(z), z);
0188         }
0189         syncComboBox();
0190         connect(m_comboBox, SIGNAL(activated(int)),
0191                 this, SLOT(comboBoxActivated(int)));
0192 
0193         m_comboBox->setEditable(true);
0194         m_comboBox->setInsertPolicy(QComboBox::NoInsert);
0195         connect(m_comboBox->lineEdit(), SIGNAL(editingFinished()),
0196                 this, SLOT(comboBoxEdited()));
0197 
0198         if (!m_inputValidator) {
0199             m_inputValidator = new QRegExpValidator(m_inputRegExp, this);
0200         }
0201         m_comboBox->setValidator(m_inputValidator);
0202         m_comboBox->installEventFilter(this);
0203     }
0204 }
0205 
0206 void ZoomController::comboBoxActivated(int index)
0207 {
0208     startZoomAnimation(m_currentZoom, m_comboBox->itemData(index).toReal());
0209 }
0210 
0211 void ZoomController::comboBoxEdited()
0212 {
0213     if (m_animationTimeLine->state() == QTimeLine::Running) {
0214         return;
0215     }
0216 
0217     const int index = m_inputRegExp.indexIn(m_comboBox->currentText());
0218     Q_ASSERT(index >= 0); // validator ensures this
0219 
0220     if (index >= 0) {
0221         const qreal z = qBound(m_zoomFactors.back(),
0222                                qreal(m_inputRegExp.cap(1).toDouble() / 100.0),
0223                                m_zoomFactors.first());
0224 
0225         setZoom(z);
0226     }
0227 }
0228 
0229 void ZoomController::syncComboBox()
0230 {
0231     if (!m_comboBox || m_animationTimeLine->state() == QTimeLine::Running) {
0232         return;
0233     }
0234 
0235     // Set current index to -1 for custom zooms
0236     const int index = m_comboBox->findData(m_currentZoom);
0237     m_comboBox->setCurrentIndex(index);
0238     m_comboBox->setEditText(zoomToString(m_currentZoom));
0239 }
0240 
0241 bool ZoomController::eventFilter(QObject *watched, QEvent *event)
0242 {
0243     // filter correct watched object
0244     if (!m_comboBox || m_comboBox != watched) {
0245         return QObject::eventFilter(watched, event);
0246     }
0247 
0248     // we only want wheel events
0249     if (event->type() != QEvent::Wheel) {
0250         return QObject::eventFilter(watched, event);
0251     }
0252 
0253     // if currentIndex() is != -1, then scrolling works as expected
0254     if (m_comboBox->currentIndex() != -1) {
0255         return QObject::eventFilter(watched, event);
0256     }
0257 
0258     // we want vertical wheel events
0259     auto evWheel = static_cast<QWheelEvent*>(event);
0260     if (!evWheel || evWheel->angleDelta().y() > 0) {
0261         return QObject::eventFilter(watched, event);
0262     }
0263 
0264     // zoom in/out
0265     if (evWheel->angleDelta().y() < 0) {
0266         zoomOut(true);
0267     } else if (evWheel->angleDelta().y() > 0) {
0268         zoomIn(true);
0269     }
0270 
0271     return true;
0272 }
0273 
0274 }
0275 }