From ec92e7000af1d09b8349039b984796de4cf54efe Mon Sep 17 00:00:00 2001
From: Elliu <elliu@hashi.re>
Date: Fri, 1 Sep 2023 21:30:32 +0200
Subject: [PATCH] UI: refactor TimingParams to make it a member of TimingScene

---
 TOFIX.md                                      |   3 +
 src/UI/DocumentViews/AudioVisualizer.cc       |  10 +-
 .../AudioVisualizer/TimingAxis.cc             |  27 ++---
 .../AudioVisualizer/TimingAxis.hh             |   2 +-
 .../AudioVisualizer/TimingCursor.cc           |  17 ++-
 .../AudioVisualizer/TimingCursor.hh           |   3 +
 .../AudioVisualizer/TimingLine.cc             | 108 +++++++++---------
 .../AudioVisualizer/TimingLine.hh             |   1 +
 .../AudioVisualizer/TimingParams.cc           |  67 ++++++++++-
 .../AudioVisualizer/TimingParams.hh           |  78 +++++++++++++
 .../AudioVisualizer/TimingScene.cc            |  43 ++++---
 .../AudioVisualizer/TimingScene.hh            |   9 +-
 .../AudioVisualizer/TimingSeparator.cc        |   8 +-
 .../AudioVisualizer/TimingSyl.cc              |   6 +-
 .../AudioVisualizer/TimingUtils.cc            |  95 ---------------
 .../AudioVisualizer/TimingUtils.hh            |  76 ------------
 16 files changed, 277 insertions(+), 276 deletions(-)
 create mode 100644 TOFIX.md
 delete mode 100644 src/UI/DocumentViews/AudioVisualizer/TimingUtils.cc
 delete mode 100644 src/UI/DocumentViews/AudioVisualizer/TimingUtils.hh

diff --git a/TOFIX.md b/TOFIX.md
new file mode 100644
index 00000000..91e3e79c
--- /dev/null
+++ b/TOFIX.md
@@ -0,0 +1,3 @@
+[ ] Save file: do not error on save = name
+[ ] Load Ass: do not fail on Default style not defined
+[ ] Save file: add .vivy extension by default
diff --git a/src/UI/DocumentViews/AudioVisualizer.cc b/src/UI/DocumentViews/AudioVisualizer.cc
index 311045a6..cad48f10 100644
--- a/src/UI/DocumentViews/AudioVisualizer.cc
+++ b/src/UI/DocumentViews/AudioVisualizer.cc
@@ -71,19 +71,19 @@ AudioVisualizer::printSpectrum(QImage pixmap, AudioContext::StreamPtr stream) no
 {
     TimingScene *timingScene = new TimingScene(pixmap, stream, rootVivyDocumentView, this);
     TimingView *timingView   = new TimingView(timingScene, pixmap, stream, this);
-    TimingParams *params     = new TimingParams(this);
 
     // The only that we want to take all the space is the timing scene in itself
     QGridLayout *layout = new QGridLayout;
     layout->addWidget(timingView, 1, 0);
     layout->setColumnStretch(0, 10);
-    layout->addWidget(params, 1, 1);
+    layout->addWidget(timingScene->getParams(), 1, 1);
     layout->setColumnStretch(1, 0);
     setLayout(layout);
 
-    connect(params->getZoomSlider(), &QSlider::valueChanged, timingScene->getAxis(),
-            &TimingAxis::refreshTicks);
-    connect(params->getRebuildSceneButton(), &QPushButton::released, timingScene,
+    connect(timingScene->getParams()->getZoomSlider(), &QSlider::valueChanged, timingScene->getParams(), &TimingParams::setAudioWidthScale);
+    // FIXME: remove for now, may freeze PC
+    //connect(timingScene->getParams(), &TimingParams::paramsChanged, timingScene, &TimingScene::rebuildScene);
+    connect(timingScene->getParams()->getRebuildSceneButton(), &QPushButton::released, timingScene,
             &TimingScene::rebuildScene);
     connect(&rootVivyDocumentView, &VivyDocumentView::assSubDocumentChanged, timingScene,
             &TimingScene::rebuildScene);
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingAxis.cc b/src/UI/DocumentViews/AudioVisualizer/TimingAxis.cc
index 986b1c6d..5dd4d5fd 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingAxis.cc
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingAxis.cc
@@ -1,6 +1,6 @@
 #include "TimingAxis.hh"
 
-#include "TimingUtils.hh"
+#include "TimingParams.hh"
 
 using namespace Vivy;
 
@@ -9,27 +9,22 @@ TimingAxis::TimingAxis() noexcept
 {
     pen = QPen(axisColour);
     pen.setWidth(penWidth);
-    refreshTicks();
 }
 
 QRectF
 TimingAxis::boundingRect() const
 {
-    return QRectF(0, penWidth, TimingUtils::audioWidth() + 2 * penWidth,
-                  -TimingUtils::axisHeight() - penWidth);
+    return QRectF(0, 10+penWidth, getTimingParam()->audioWidth() + 2 * penWidth,
+                  -2*getTimingParam()->axisHeight() - penWidth);
 }
 
-/*
- * Refresh the minor/major ticks value
- * For intended results, this function should only be called when scene() returns a valid QGraphicsScene (so not in the constructors)
- */
 void
-TimingAxis::refreshTicks()
+TimingAxis::onParamsChanged()
 {
     int nbAvailableTicks = availableTicks.size();
     int minorTicksIndex  = 0;
-    int length           = TimingUtils::audioLength();
-    int width            = TimingUtils::audioWidth();
+    int length           = getTimingParam()->audioLength();
+    int width            = getTimingParam()->audioWidth();
     if (width != 0 && length != 0) {
         for (minorTicksIndex = 0; minorTicksIndex < nbAvailableTicks; minorTicksIndex++) {
             if (width * availableTicks[minorTicksIndex] / length >= minBetweenMinor) {
@@ -46,13 +41,13 @@ TimingAxis::refreshTicks()
         minorTicks = 0;
         majorTicks = availableTicks[minorTicksIndex];
     }
-    TimingUtils::setTicks(minorTicks, majorTicks);
+    getTimingParam()->setTicks(minorTicks, majorTicks);
 }
 
 void
 TimingAxis::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
 {
-    int audioLength = TimingUtils::audioLength();
+    int audioLength = getTimingParam()->audioLength();
     if (audioLength == 0)
         return;
 
@@ -70,13 +65,13 @@ TimingAxis::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWi
      * should look whether this precision loss is negligible or not
      */
     for (int i = 0; i < audioLength; i += majorTicks) {
-        int pos = TimingUtils::posFromMs(i);
-        painter->drawText(QPoint(pos, yText), TimingUtils::printMajorTicks(i));
+        int pos = getTimingParam()->posFromMs(i);
+        painter->drawText(QPoint(pos, yText), getTimingParam()->printMajorTicks(i));
         painter->drawLine(pos, 0, pos, -majorTicksHeight);
     }
     if (minorTicks > 0) {
         for (int i = 0; i < audioLength; i += minorTicks) {
-            int pos = TimingUtils::posFromMs(i);
+            int pos = getTimingParam()->posFromMs(i);
             if (Q_LIKELY(fmod(i, majorTicks) != 0))
                 painter->drawLine(pos, 0, pos, -minorTicksHeight);
         }
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingAxis.hh b/src/UI/DocumentViews/AudioVisualizer/TimingAxis.hh
index d2cfd923..266c0431 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingAxis.hh
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingAxis.hh
@@ -42,7 +42,7 @@ private:
 
 protected:
 public slots:
-    void refreshTicks();
+    void onParamsChanged();
 };
 
 }
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingCursor.cc b/src/UI/DocumentViews/AudioVisualizer/TimingCursor.cc
index 6896039c..9927333e 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingCursor.cc
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingCursor.cc
@@ -1,6 +1,6 @@
 #include "TimingCursor.hh"
 
-#include "TimingUtils.hh"
+#include "TimingParams.hh"
 
 using namespace Vivy;
 
@@ -8,25 +8,23 @@ TimingCursor::TimingCursor()
     : QGraphicsItem()
 {
     setZValue(Z_CURSOR_BAR);
-    textRect = QRectF(0, 10, maxWidth,
-                      TimingUtils::audioHeight() - 20); // TODO : remove 10/20 magic numbers
 }
 
 QRectF
 TimingCursor::boundingRect() const
 {
-    return QRectF(-maxWidth, 0, maxWidth * 2, TimingUtils::audioHeight());
+    return QRectF(-maxWidth, 0, maxWidth * 2, getTimingParam()->audioHeight());
 }
 
 void
 TimingCursor::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
 {
-    painter->drawLine(0, 0, 0, TimingUtils::audioHeight());
+    painter->drawLine(0, 0, 0, getTimingParam()->audioHeight());
 
     QRectF textRectangle = textRect;
     QPointF sceneLeft    = mapFromScene(QPointF(0, 0));
     QPointF sceneRight   = mapFromScene(QPointF(scene()->sceneRect().right(), 0));
-    TimingUtils::adjustFlip(&textRectangle, sceneLeft.rx(), sceneRight.rx());
+    getTimingParam()->adjustFlip(&textRectangle, sceneLeft.rx(), sceneRight.rx());
 
     //painter->drawRect(textRectangle);
     painter->drawText(textRectangle, Qt::AlignHCenter | Qt::AlignTop, cursorTime);
@@ -37,3 +35,10 @@ TimingCursor::setTime(QString str) noexcept
 {
     cursorTime = str;
 }
+
+void
+TimingCursor::onParamsChanged()
+{
+    textRect = QRectF(0, 10, maxWidth,
+                      getTimingParam()->audioHeight() - 20); // TODO : remove 10/20 magic numbers
+}
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingCursor.hh b/src/UI/DocumentViews/AudioVisualizer/TimingCursor.hh
index c7c72ff3..130f2b99 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingCursor.hh
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingCursor.hh
@@ -24,6 +24,9 @@ private:
 
 public:
     void setTime(QString str) noexcept;
+
+public slots:
+    void onParamsChanged();
 };
 
 }
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingLine.cc b/src/UI/DocumentViews/AudioVisualizer/TimingLine.cc
index a51875c8..c64a40e1 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingLine.cc
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingLine.cc
@@ -1,69 +1,30 @@
 #include "TimingLine.hh"
 
-#include "TimingUtils.hh"
+#include "TimingParams.hh"
 
 using namespace Vivy;
 
-#define CONNECT_SEP(sep)                                                                           \
-    connect(sep, &TimingSeparator::positionChanged, this, &TimingLine::timingSeparatorHasChanged); \
-    connect(sep, &TimingSeparator::enterPress, this, &TimingLine::sepEnterPress);                  \
-    connect(sep, &TimingSeparator::exitPress, this, &TimingLine::sepExitPress);
-
 TimingLine::TimingLine(Ass::Line& _line, int index, QGraphicsItem *parent)
     : QGraphicsObject(parent)
     , line(_line)
     , lineIndex(index)
 {
-    setPos(TimingUtils::posFromMs(int(line.getStart()) * 10), TimingUtils::axisHeight());
-    int currentTime = 0;
-    int endSyl      = 0;
-    int i;
-
-    setZValue(Z_LINE_BACKGROUND);
-
-    TimingSeparator *timingSeparatorStart =
-        new TimingSeparator(currentTime, 0, TimingSeparator::SeparatorStyle::Start, this);
-    seps.append(timingSeparatorStart);
-    CONNECT_SEP(timingSeparatorStart);
-
-    QVector<Ass::Syl> syls = line.getContent();
-    for (i = 0; i < syls.size(); ++i) {
-        endSyl = currentTime + 10 * int(syls.at(i).getDuration());
-
-        TimingSyl *timingSyl = new TimingSyl(syls.at(i), currentTime, this);
-        timingSyls.append(timingSyl);
-
-        if (i != 0) {
-            TimingSeparator *timingSeparator =
-                new TimingSeparator(currentTime, i, TimingSeparator::SeparatorStyle::Middle, this);
-            seps.append(timingSeparator);
-            CONNECT_SEP(timingSeparator);
-        }
-
-        currentTime = endSyl;
-    }
-
-    TimingSeparator *timingSeparatorEnd =
-        new TimingSeparator(currentTime, i, TimingSeparator::SeparatorStyle::End, this);
-    seps.append(timingSeparatorEnd);
-    CONNECT_SEP(timingSeparatorEnd);
 }
-#undef CONNECT_SEP
 
 QRectF
 TimingLine::boundingRect() const
 {
     return QRectF(tempOffset, 0,
-                  TimingUtils::posFromMs(int(10 * (line.getEnd() - line.getStart()))),
-                  TimingUtils::audioHeight());
+                  getTimingParam()->posFromMs(int(10 * (line.getEnd() - line.getStart()))),
+                  getTimingParam()->audioHeight());
 }
 
 void
 TimingLine::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
 {
     painter->fillRect(QRectF(tempOffset, 0,
-                             TimingUtils::posFromMs(int(10 * (line.getEnd() - line.getStart()))),
-                             TimingUtils::audioHeight()),
+                             getTimingParam()->posFromMs(int(10 * (line.getEnd() - line.getStart()))),
+                             getTimingParam()->audioHeight()),
                       QColor(0, 255, 255, 50));
 }
 
@@ -95,7 +56,7 @@ TimingLine::sepExitPress(int sepIndex)
                 seps[i]->silentlyMoveBy(-tempOffset, 0);
             }
 
-            line.setStart(quint64(TimingUtils::msFromPos(mapToScene(0, 0).x()) / 10));
+            line.setStart(quint64(getTimingParam()->msFromPos(mapToScene(0, 0).x()) / 10));
         }
     }
 
@@ -116,16 +77,16 @@ TimingLine::requestMove(int sepIndex, qreal x)
 
         mini = sceneRect.left();
         maxi = sepIndex < syls.size() - 1 ? timingSyls[sepIndex + 1]->pos().x()
-                                          : TimingUtils::posFromMs(int(line.getDuration()) * 10);
+                                          : getTimingParam()->posFromMs(int(line.getDuration()) * 10);
 
         given      = qBound(mini, given, maxi);
         tempOffset = given;
 
-        quint64 dur1 = quint64(TimingUtils::msFromPos(int(seps[1]->pos().x() - given)) / 10);
+        quint64 dur1 = quint64(getTimingParam()->msFromPos(int(seps[1]->pos().x() - given)) / 10);
         syls[0].setDuration(dur1);
         timingSyls[0]->setLen(dur1);
         timingSyls[0]->setPos(given, 0);
-        line.setStart(quint64(TimingUtils::msFromPos(mapToScene(seps[0]->pos().x(), 0).x()) / 10));
+        line.setStart(quint64(getTimingParam()->msFromPos(mapToScene(seps[0]->pos().x(), 0).x()) / 10));
     }
 
     else if (sepIndex >= syls.size()) {
@@ -136,21 +97,21 @@ TimingLine::requestMove(int sepIndex, qreal x)
         given = qBound(mini, given, maxi);
 
         quint64 dur2 =
-            quint64(TimingUtils::msFromPos(int(given - timingSyls[sepIndex - 1]->pos().x())) / 10);
+            quint64(getTimingParam()->msFromPos(int(given - timingSyls[sepIndex - 1]->pos().x())) / 10);
         syls[sepIndex - 1].setDuration(dur2);
         timingSyls[sepIndex - 1]->setLen(dur2);
-        line.setEnd(line.getStart() + quint64(TimingUtils::msFromPos(int(given)) / 10));
+        line.setEnd(line.getStart() + quint64(getTimingParam()->msFromPos(int(given)) / 10));
     }
 
     else {
         mini  = timingSyls[sepIndex - 1]->pos().x();
         maxi  = sepIndex < syls.size() - 1 ? timingSyls[sepIndex + 1]->pos().x()
-                                           : TimingUtils::posFromMs(int(line.getDuration() * 10));
+                                           : getTimingParam()->posFromMs(int(line.getDuration() * 10));
         given = qBound(mini, given, maxi);
 
         quint64 sumDur = syls[sepIndex].getDuration() + syls[sepIndex - 1].getDuration();
         quint64 dur1   = quint64(
-              TimingUtils::msFromPos(int(given) - int(timingSyls[sepIndex - 1]->pos().x())) / 10);
+              getTimingParam()->msFromPos(int(given) - int(timingSyls[sepIndex - 1]->pos().x())) / 10);
         dur1         = qMin(dur1, sumDur);
         quint64 dur2 = sumDur - dur1;
 
@@ -168,3 +129,46 @@ TimingLine::requestMove(int sepIndex, qreal x)
 
     return given;
 }
+
+#define CONNECT_SEP(sep)                                                                           \
+    connect(sep, &TimingSeparator::positionChanged, this, &TimingLine::timingSeparatorHasChanged); \
+    connect(sep, &TimingSeparator::enterPress, this, &TimingLine::sepEnterPress);                  \
+    connect(sep, &TimingSeparator::exitPress, this, &TimingLine::sepExitPress);
+void
+TimingLine::onParamsChanged()
+{
+    setPos(getTimingParam()->posFromMs(int(line.getStart()) * 10), getTimingParam()->axisHeight());
+    int currentTime = 0;
+    int endSyl      = 0;
+    int i;
+
+    setZValue(Z_LINE_BACKGROUND);
+
+    TimingSeparator *timingSeparatorStart =
+        new TimingSeparator(currentTime, 0, TimingSeparator::SeparatorStyle::Start, this);
+    seps.append(timingSeparatorStart);
+    CONNECT_SEP(timingSeparatorStart);
+
+    QVector<Ass::Syl> syls = line.getContent();
+    for (i = 0; i < syls.size(); ++i) {
+        endSyl = currentTime + 10 * int(syls.at(i).getDuration());
+
+        TimingSyl *timingSyl = new TimingSyl(syls.at(i), currentTime, this);
+        timingSyls.append(timingSyl);
+
+        if (i != 0) {
+            TimingSeparator *timingSeparator =
+                new TimingSeparator(currentTime, i, TimingSeparator::SeparatorStyle::Middle, this);
+            seps.append(timingSeparator);
+            CONNECT_SEP(timingSeparator);
+        }
+
+        currentTime = endSyl;
+    }
+
+    TimingSeparator *timingSeparatorEnd =
+        new TimingSeparator(currentTime, i, TimingSeparator::SeparatorStyle::End, this);
+    seps.append(timingSeparatorEnd);
+    CONNECT_SEP(timingSeparatorEnd);
+}
+#undef CONNECT_SEP
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingLine.hh b/src/UI/DocumentViews/AudioVisualizer/TimingLine.hh
index 261d37a7..a9249281 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingLine.hh
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingLine.hh
@@ -34,6 +34,7 @@ public slots:
     void timingSeparatorHasChanged(int, qreal);
     void sepEnterPress(int);
     void sepExitPress(int);
+    void onParamsChanged();
 };
 
 }
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingParams.cc b/src/UI/DocumentViews/AudioVisualizer/TimingParams.cc
index 83baf547..b18c1c00 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingParams.cc
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingParams.cc
@@ -11,11 +11,13 @@ TimingParams::TimingParams(QWidget *parent) noexcept
 
     zoomSlider = std::make_unique<QSlider>(new QSlider(Qt::Vertical));
     if (QSlider *slider = zoomSlider.get()) {
-        slider->setMinimum(0);
+        slider->setMinimum(1);
         slider->setMaximum(100);
         slider->setSingleStep(0);
         slider->setPageStep(0);
         slider->setValue(0);
+        // FIXME: set to false for now as it may be intensive while rebuilding the scene isn't optimized
+        slider->setTracking(false);
         hlayout->addWidget(slider);
     }
     sensibilitySlider = std::make_unique<QSlider>(new QSlider(Qt::Vertical));
@@ -25,6 +27,8 @@ TimingParams::TimingParams(QWidget *parent) noexcept
         slider->setSingleStep(0);
         slider->setPageStep(0);
         slider->setValue(0);
+        // FIXME: set to false for now as it may be intensive while rebuilding the scene isn't optimized
+        slider->setTracking(false);
         hlayout->addWidget(slider);
     }
 
@@ -53,3 +57,64 @@ TimingParams::getRebuildSceneButton() const noexcept
 {
     return rebuildSceneButton.get();
 }
+
+void
+TimingParams::adjustFlip(QRectF *rect, qreal left, qreal right) noexcept
+{
+    if (Q_UNLIKELY(right - rect->right() < 0)) {
+        qreal w = rect->width();
+        rect->adjust(-w, 0, -w, 0);
+    }
+}
+
+void
+TimingParams::adjustTranslate(QRectF *rect, qreal left, qreal right) noexcept
+{
+    qreal d;
+    qreal hw = rect->width() / 2;
+    if (Q_UNLIKELY((d = hw + left) > 0)) {
+        rect->adjust(d, 0, d, 0);
+    }
+    if (Q_UNLIKELY((d = right - hw) < 0)) {
+        rect->adjust(d, 0, d, 0);
+    }
+}
+
+QString
+TimingParams::printMajorTicks(int t) noexcept
+{
+    // TODO : adapt to different ticks (not everytime with "." first)
+    QString ret(
+        QStringLiteral("%1").arg(t % 60000 / 1000, t >= 60000 ? 2 : 1, 10, QLatin1Char('0')) +
+        QString(".") + QString::number(t % 1000 / m_majorTicks));
+    if (t >= 60000)
+        ret.prepend(QString::number(t / 60000) + QString(":"));
+    if (t >= 360000)
+        ret.prepend(QString::number(t / 360000) + QString(":"));
+    return ret;
+}
+
+QString
+TimingParams::printCursor(int t) noexcept
+{
+    int absT = abs(t);
+    // TODO : adapt to different ticks (not everytime with "." first)
+    QString ret(
+        QString::fromLatin1(t < 0 ? "-" : "" ) +
+        QStringLiteral("%1").arg(absT % 60000 / 1000, absT >= 60000 ? 2 : 1, 10, QLatin1Char('0')) +
+        QString(".") + QString::number(absT % 1000 / m_minorTicks));
+    if (absT >= 60000)
+        ret.prepend(QString::number(absT / 60000) + QString(":"));
+    if (absT >= 360000)
+        ret.prepend(QString::number(absT / 360000) + QString(":"));
+    return ret;
+}
+
+void
+TimingParams::updateRatios() noexcept
+{
+    m_wl = m_audioLength != 0 ? qreal(m_audioWidth) / qreal(m_audioLength) : 0;
+    m_lw = m_audioWidth != 0 ? qreal(m_audioLength) / qreal(m_audioWidth) : 0;
+
+    emit paramsChanged();
+}
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingParams.hh b/src/UI/DocumentViews/AudioVisualizer/TimingParams.hh
index 048caa6b..b2103f30 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingParams.hh
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingParams.hh
@@ -7,6 +7,18 @@
 
 #include "PreCompiledHeaders.hh"
 #include "Lib/Utils.hh"
+#include "TimingScene.hh"
+
+#define getTimingScene() static_cast<TimingScene *>(scene())
+#define getTimingParam() getTimingScene()->getParams()
+
+#define Z_SPECTER             -1000
+#define Z_AXIS                10
+#define Z_LINE_BACKGROUND     -100
+#define Z_SEPARATOR_START_END 1000
+#define Z_SEPARATOR_MIDDLE    100
+#define Z_LINE_SYL_TEXT       50
+#define Z_CURSOR_BAR          500
 
 namespace Vivy
 {
@@ -25,6 +37,72 @@ public:
     QSlider *getZoomSlider() const noexcept;
     QSlider *getSensibilitySlider() const noexcept;
     QPushButton *getRebuildSceneButton() const noexcept;
+
+private:
+    // Managed by TimingAxis
+    int m_majorTicks{1}, m_minorTicks{1};
+    // Managed by TimingScene
+    int m_axisHeight{1}, m_audioWidth{1}, m_audioLength{1}, m_audioHeight{1};
+    // Self-managed
+    qreal m_wl{0}, m_lw{0};
+    qreal m_audioWidthScale{1};
+
+public:
+    void adjustTranslate(QRectF *, qreal, qreal) noexcept;
+    void adjustFlip(QRectF *, qreal, qreal) noexcept;
+
+    QString printMajorTicks(int) noexcept;
+    QString printCursor(int) noexcept;
+
+    inline void setTicks(int t, int T) noexcept
+    {
+        m_minorTicks = t;
+        m_majorTicks = T;
+    };
+    inline void setAxisHeight(int x) { m_axisHeight = x; };
+    inline void setAudioHeight(int x) { m_audioHeight = x; };
+    inline void setAudioWidth(int x)
+    {
+        m_audioWidth = int(x * m_audioWidthScale);
+        updateRatios();
+    };
+    inline void setAudioLength(int x)
+    {
+        m_audioLength = x;
+        updateRatios();
+    };
+
+    inline int axisHeight() { return m_axisHeight; };
+    inline int audioHeight() { return m_audioHeight; };
+    inline int audioWidth() { return m_audioWidth; };
+    inline int audioLength() { return m_audioLength; };
+
+    inline void setAudioWidthScale(int s)
+    {
+        m_audioWidthScale = 1 + 0.01 * s;
+        setAudioWidth(m_audioWidth);
+        updateRatios();
+    };
+
+    template <typename T> T posFromMs(T t) noexcept {
+        return T(t * m_wl);
+    }
+    template <typename T> T posFromMs(T t, int audioWidth, int audioLength) noexcept {
+        return audioLength == 0 ? 0 : T(qint64(t) * qint64(audioWidth) / audioLength);
+    }
+
+    template <typename T> T msFromPos(T x) noexcept {
+        return T(x * m_lw);
+    }
+    template <typename T> T msFromPos(T x, int audioLength, int audioWidth) noexcept {
+        return audioWidth == 0 ? 0 : int(qint64(x) * qint64(audioLength) / audioWidth);
+    }
+
+private:
+    void updateRatios() noexcept;
+
+signals:
+    void paramsChanged();
 };
 
 }
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingScene.cc b/src/UI/DocumentViews/AudioVisualizer/TimingScene.cc
index 39760c37..4fae53b0 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingScene.cc
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingScene.cc
@@ -1,6 +1,6 @@
 #include "TimingScene.hh"
 
-#include "TimingUtils.hh"
+#include "TimingParams.hh"
 
 using namespace Vivy;
 
@@ -16,6 +16,10 @@ TimingScene::TimingScene(QImage img_, AudioContext::StreamPtr stream, VivyDocume
     , img(img_)
     , audioStream(stream)
 {
+    params = new TimingParams();
+    // We put a common origin for the items at the bottom of the axis
+    params->setAxisHeight(30); // TODO: remove magic number
+
     rebuildScene();
 }
 
@@ -36,37 +40,38 @@ TimingScene::rebuildScene()
     }
     timingLines.clear();
 
-    // We put a common origin for the items at the bottom of the axis
-    TimingUtils::setAxisHeight(30); // TODO: remove magic number
-
     QPixmap pixmap(QPixmap::fromImage(img));
     backgroundImg = addPixmap(pixmap);
     backgroundImg->setZValue(Z_SPECTER);
-    backgroundImg->setPos(0, TimingUtils::axisHeight());
+    backgroundImg->setPos(0, params->axisHeight());
 
-    TimingUtils::setAudioHeight(pixmap.height());
-    TimingUtils::setAudioWidth(img.width());
-    TimingUtils::setAudioLength(int(audioStream->getLength()));
+    params->setAudioHeight(pixmap.height());
+    params->setAudioWidth(img.width());
+    params->setAudioLength(int(audioStream->getLength()));
 
     ax = new TimingAxis();
     addItem(ax);
+    ax->onParamsChanged();
     ax->setZValue(Z_AXIS);
-    ax->setPos(0, TimingUtils::axisHeight());
-
-    // Freeze the scene boundaries
-    setSceneRect(sceneRect());
+    ax->setPos(0, params->axisHeight());
 
     cursor = new TimingCursor();
     addItem(cursor);
-    cursor->setPos(0, TimingUtils::axisHeight());
+    cursor->onParamsChanged();
+    cursor->setPos(0, params->axisHeight());
+
+    // Freeze the scene boundaries
+    QRectF sRect = sceneRect();
+    setSceneRect(QRectF(0, sRect.top(), params->audioWidth(), sRect.height()).normalized());
 
     if (auto assDocument = rootVivyDocument.getAssSubDocument()) {
         QVector<Ass::LinePtr> lines = assDocument->getLines();
         for (int i = 0; i < lines.size(); ++i) {
             if (auto line = lines.at(i)) {
                 TimingLine *l = new TimingLine(*line, i);
-                l->setVisible(false);
                 addItem(l);
+                l->onParamsChanged();
+                l->setVisible(false);
                 timingLines.append(l);
                 if (auto assLinesModel = rootVivyDocumentView.getAssLinesModel()) {
                     connect(l, &TimingLine::lineChanged, assLinesModel, &AssLinesModel::updateLine);
@@ -105,7 +110,7 @@ TimingScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) noexcept
 {
     int x = int(mouseEvent->scenePos().rx());
     cursor->setPos(x, 30);
-    cursor->setTime(TimingUtils::printCursor(TimingUtils::msFromPos(x)));
+    cursor->setTime(params->printCursor(params->msFromPos(x)));
     QGraphicsScene::mouseMoveEvent(mouseEvent);
 }
 
@@ -113,7 +118,7 @@ void
 TimingScene::handleMousePressEventLine(QGraphicsSceneMouseEvent *event, Ass::LinePtr p) noexcept
 {
     QPointF pos = event->scenePos();
-    int time    = TimingUtils::msFromPos(int(pos.x()));
+    int time    = params->msFromPos(int(pos.x()));
 
     if (const auto &btn = event->button(); btn == Qt::LeftButton) {
         p->setStart(quint64(time));
@@ -144,6 +149,12 @@ TimingScene::getAxis()
     return ax;
 }
 
+TimingParams *
+TimingScene::getParams()
+{
+    return params;
+}
+
 void
 TimingScene::updateSelectedLines(const QItemSelection &selected, const QItemSelection &deselected)
 {
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingScene.hh b/src/UI/DocumentViews/AudioVisualizer/TimingScene.hh
index 0120029b..7e275b9f 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingScene.hh
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingScene.hh
@@ -14,8 +14,11 @@
 #include "TimingCursor.hh"
 #include "TimingLine.hh"
 
+
 namespace Vivy
 {
+class TimingParams;
+
 class TimingScene final : public QGraphicsScene, public AbstractVivyDocumentNeeder {
     Q_OBJECT
 
@@ -38,7 +41,10 @@ private:
     AudioContext::StreamPtr audioStream;
     Ass::LineWeakPtr currentLine{};
     TimingMode timingMode{ TimingMode::Line };
-    TimingAxis *ax{ nullptr };
+
+    // FIXME: add destructors
+    TimingParams *params;
+    TimingAxis *ax;
 
     QVector<TimingLine *> timingLines;
 
@@ -48,6 +54,7 @@ public:
     void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) noexcept override;
 
     TimingAxis *getAxis();
+    TimingParams *getParams();
 
 private:
     void handleMousePressEventLine(QGraphicsSceneMouseEvent *, Ass::LinePtr) noexcept;
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingSeparator.cc b/src/UI/DocumentViews/AudioVisualizer/TimingSeparator.cc
index 82e48367..2c7d62c6 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingSeparator.cc
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingSeparator.cc
@@ -1,6 +1,6 @@
 #include "TimingSeparator.hh"
 
-#include "TimingUtils.hh"
+#include "TimingParams.hh"
 #include "TimingLine.hh"
 
 using namespace Vivy;
@@ -11,7 +11,7 @@ TimingSeparator::TimingSeparator(int time, int index, SeparatorStyle style_, Tim
     , sepIndex(index)
     , parentTimingLine(parent)
 {
-    setPos(TimingUtils::posFromMs(time), 0);
+    setPos(getTimingParam()->posFromMs(time), 0);
 
     setFlags(QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemSendsGeometryChanges);
     setAcceptHoverEvents(true);
@@ -38,14 +38,14 @@ TimingSeparator::TimingSeparator(int time, int index, SeparatorStyle style_, Tim
 QRectF
 TimingSeparator::boundingRect() const
 {
-    return QRectF(-widthPaw, 0, 2 * widthPaw, TimingUtils::audioHeight());
+    return QRectF(-widthPaw, 0, 2 * widthPaw, getTimingParam()->audioHeight());
 }
 
 void
 TimingSeparator::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
 {
     painter->setPen(pen);
-    int height = TimingUtils::audioHeight();
+    int height = getTimingParam()->audioHeight();
 
     int top    = 1 + pen.width() / 2;
     int bottom = height - 1 - pen.width() / 2;
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingSyl.cc b/src/UI/DocumentViews/AudioVisualizer/TimingSyl.cc
index f38ece88..7202ed4e 100644
--- a/src/UI/DocumentViews/AudioVisualizer/TimingSyl.cc
+++ b/src/UI/DocumentViews/AudioVisualizer/TimingSyl.cc
@@ -1,6 +1,6 @@
 #include "TimingSyl.hh"
 
-#include "TimingUtils.hh"
+#include "TimingParams.hh"
 #include "TimingSeparator.hh"
 
 using namespace Vivy;
@@ -10,13 +10,13 @@ TimingSyl::TimingSyl(Ass::Syl syl_, int startTime, QGraphicsItem *parent)
     , syl(syl_)
 {
     setZValue(Z_LINE_SYL_TEXT);
-    setPos(TimingUtils::posFromMs(startTime), 0);
+    setPos(getTimingParam()->posFromMs(startTime), 0);
 }
 
 QRectF
 TimingSyl::boundingRect() const
 {
-    return QRectF(0, 0, int(syl.getDuration()), TimingUtils::audioHeight());
+    return QRectF(0, 0, int(syl.getDuration()), getTimingParam()->audioHeight());
 }
 
 void
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingUtils.cc b/src/UI/DocumentViews/AudioVisualizer/TimingUtils.cc
deleted file mode 100644
index 7e710002..00000000
--- a/src/UI/DocumentViews/AudioVisualizer/TimingUtils.cc
+++ /dev/null
@@ -1,95 +0,0 @@
-#include "TimingUtils.hh"
-
-using namespace Vivy;
-
-int TimingUtils::m_majorTicks{ 1 };
-int TimingUtils::m_minorTicks{ 1 };
-
-int TimingUtils::m_axisHeight{ 1 };
-int TimingUtils::m_audioHeight{ 1 };
-int TimingUtils::m_audioWidth{ 1 };
-int TimingUtils::m_audioLength{ 1 };
-
-qreal TimingUtils::m_wl{ 0 };
-qreal TimingUtils::m_lw{ 0 };
-
-int
-TimingUtils::posFromMs(int t) noexcept
-{
-    return int(t * m_wl);
-}
-
-int
-TimingUtils::posFromMs(int t, int audioWidth, int audioLength) noexcept
-{
-    return audioLength == 0 ? 0 : int(qint64(t) * qint64(audioWidth) / audioLength);
-}
-
-int
-TimingUtils::msFromPos(int x) noexcept
-{
-    return int(x * m_lw);
-}
-
-int
-TimingUtils::msFromPos(int x, int audioLength, int audioWidth) noexcept
-{
-    return audioWidth == 0 ? 0 : int(qint64(x) * qint64(audioLength) / audioWidth);
-}
-
-void
-TimingUtils::adjustFlip(QRectF *rect, qreal left, qreal right) noexcept
-{
-    if (Q_UNLIKELY(right - rect->right() < 0)) {
-        qreal w = rect->width();
-        rect->adjust(-w, 0, -w, 0);
-    }
-}
-
-void
-TimingUtils::adjustTranslate(QRectF *rect, qreal left, qreal right) noexcept
-{
-    qreal d;
-    qreal hw = rect->width() / 2;
-    if (Q_UNLIKELY((d = hw + left) > 0)) {
-        rect->adjust(d, 0, d, 0);
-    }
-    if (Q_UNLIKELY((d = right - hw) < 0)) {
-        rect->adjust(d, 0, d, 0);
-    }
-}
-
-QString
-TimingUtils::printMajorTicks(int t) noexcept
-{
-    // TODO : adapt to different ticks (not everytime with "." first)
-    QString ret(
-        QStringLiteral("%1").arg(t % 60000 / 1000, t >= 60000 ? 2 : 1, 10, QLatin1Char('0')) +
-        QString(".") + QString::number(t % 1000 / m_majorTicks));
-    if (t >= 60000)
-        ret.prepend(QString::number(t / 60000) + QString(":"));
-    if (t >= 360000)
-        ret.prepend(QString::number(t / 360000) + QString(":"));
-    return ret;
-}
-
-QString
-TimingUtils::printCursor(int t) noexcept
-{
-    // TODO : adapt to different ticks (not everytime with "." first)
-    QString ret(
-        QStringLiteral("%1").arg(t % 60000 / 1000, t >= 60000 ? 2 : 1, 10, QLatin1Char('0')) +
-        QString(".") + QString::number(t % 1000 / m_minorTicks));
-    if (t >= 60000)
-        ret.prepend(QString::number(t / 60000) + QString(":"));
-    if (t >= 360000)
-        ret.prepend(QString::number(t / 360000) + QString(":"));
-    return ret;
-}
-
-void
-TimingUtils::updateRatios() noexcept
-{
-    m_wl = m_audioLength != 0 ? qreal(m_audioWidth) / qreal(m_audioLength) : 0;
-    m_lw = m_audioWidth != 0 ? qreal(m_audioLength) / qreal(m_audioWidth) : 0;
-}
diff --git a/src/UI/DocumentViews/AudioVisualizer/TimingUtils.hh b/src/UI/DocumentViews/AudioVisualizer/TimingUtils.hh
deleted file mode 100644
index 2937527b..00000000
--- a/src/UI/DocumentViews/AudioVisualizer/TimingUtils.hh
+++ /dev/null
@@ -1,76 +0,0 @@
-#pragma once
-
-#ifndef __cplusplus
-#error "This is a C++ header"
-#endif
-
-#include <PreCompiledHeaders.hh>
-
-#define getTimingScene() static_cast<TimingScene *>(scene())
-
-#define Z_SPECTER             -1000
-#define Z_AXIS                10
-#define Z_LINE_BACKGROUND     -100
-#define Z_SEPARATOR_START_END 1000
-#define Z_SEPARATOR_MIDDLE    100
-#define Z_LINE_SYL_TEXT       50
-#define Z_CURSOR_BAR          500
-
-/*
- * FIXME
- * Relying on static may pose issues when multiple audioVisualiser
- * are open (when multiple VivyDocuments are open)
- * Solution : yeet the statics and put an instance of this class
- * in the TimingScene
- */
-namespace Vivy
-{
-class TimingUtils {
-private:
-    // Managed by TimingAxis
-    static int m_majorTicks, m_minorTicks;
-    // Managed by TimingScene
-    static int m_axisHeight, m_audioWidth, m_audioLength, m_audioHeight;
-    // Self-managed
-    static qreal m_wl, m_lw;
-
-public:
-    static int posFromMs(int t, int audioWidth, int audioLength) noexcept;
-    static int posFromMs(int t) noexcept;
-
-    static int msFromPos(int x, int audioLength, int audioWidth) noexcept;
-    static int msFromPos(int x) noexcept;
-
-    static void adjustTranslate(QRectF *, qreal, qreal) noexcept;
-    static void adjustFlip(QRectF *, qreal, qreal) noexcept;
-
-    static QString printMajorTicks(int) noexcept;
-    static QString printCursor(int) noexcept;
-
-    static inline void setTicks(int t, int T) noexcept
-    {
-        m_minorTicks = t;
-        m_majorTicks = T;
-    };
-    static inline void setAxisHeight(int x) { m_axisHeight = x; };
-    static inline void setAudioHeight(int x) { m_audioHeight = x; };
-    static inline void setAudioWidth(int x)
-    {
-        m_audioWidth = x;
-        updateRatios();
-    };
-    static inline void setAudioLength(int x)
-    {
-        m_audioLength = x;
-        updateRatios();
-    };
-
-    static inline int axisHeight() { return m_axisHeight; };
-    static inline int audioHeight() { return m_audioHeight; };
-    static inline int audioWidth() { return m_audioWidth; };
-    static inline int audioLength() { return m_audioLength; };
-
-private:
-    static void updateRatios() noexcept;
-};
-}
-- 
GitLab