3131#include < QLabel>
3232#include < QKeyEvent>
3333#include < QTimer>
34+ #include " utils/framepacer.h"
3435#include < QElapsedTimer>
3536#include < QFocusEvent>
3637#include < QMouseEvent>
@@ -121,8 +122,33 @@ class LiveLoopScopeWidget : public QWidget
121122 : QWidget(parent)
122123 {
123124 setAttribute (Qt::WA_TransparentForMouseEvents, true );
125+ // Opaque: every pixel of the rectangle is painted here, over what
126+ // the editor would have drawn (setBase), so Qt never repaints the
127+ // editor beneath the scope — a repaint that walked the whole widget
128+ // tree on every frame of every live loop.
129+ setAttribute (Qt::WA_OpaquePaintEvent, true );
124130 setFocusPolicy (Qt::NoFocus);
125131 }
132+ // A style change (the app's stylesheet applied, a theme switch) has
133+ // Qt re-polish every widget, and the stylesheet style clears the
134+ // opaque-paint attribute as it goes: it is asserted again after each.
135+ bool event (QEvent* e) override
136+ {
137+ const bool r = QWidget::event (e);
138+ if (e->type () == QEvent::StyleChange || e->type () == QEvent::Polish)
139+ setAttribute (Qt::WA_OpaquePaintEvent, true );
140+ return r;
141+ }
142+ // What the editor draws under this rectangle: its paper, or the caret
143+ // line's colour while the header is the current line. The editor says,
144+ // as it positions the scope.
145+ void setBase (const QColor& base)
146+ {
147+ if (m_base == base)
148+ return ;
149+ m_base = base;
150+ update ();
151+ }
126152
127153 ~LiveLoopScopeWidget () override
128154 {
@@ -241,6 +267,7 @@ class LiveLoopScopeWidget : public QWidget
241267 void paintEvent (QPaintEvent*) override
242268 {
243269 QPainter p (this );
270+ p.fillRect (rect (), m_base.isValid () ? m_base : palette ().base ().color ());
244271 p.setRenderHint (QPainter::Antialiasing, true );
245272 const qreal w = width ();
246273 const qreal h = height ();
@@ -443,6 +470,7 @@ class LiveLoopScopeWidget : public QWidget
443470 }
444471 }
445472
473+ QColor m_base; // the editor's colour beneath (setBase)
446474 QColor m_wave;
447475 QColor m_quiet;
448476 QColor m_panel;
@@ -706,7 +734,7 @@ SonicPiScintilla::SonicPiScintilla(SonicPiLexer* lexer, SonicPiTheme* theme, QSt
706734 // changed: SCN_UPDATEUI covers edits and both scroll axes in one hook, and
707735 // the pill reports its own moves/resizes (zoom, viewport reshapes).
708736 connect (this , &QsciScintillaBase::SCN_UPDATEUI , this ,
709- [this ](int ) { updateEditorToolbarOcclusion (); });
737+ [this ](int ) { updateEditorToolbarOcclusion (); positionLiveLoopScopes (); });
710738 connect (m_editorToolbar, &EditorToolbar::geometryChanged, this ,
711739 &SonicPiScintilla::updateEditorToolbarOcclusion);
712740 // Keep the highlights and counter live while the buffer is edited — but
@@ -1209,6 +1237,7 @@ void SonicPiScintilla::setLiveLoopScope(const QString& name, int runLine, int sc
12091237 w = new LiveLoopScopeWidget (viewport ());
12101238 w->setObjectName (QStringLiteral (" liveLoopScope:" ) + name);
12111239 w->setScroll (m_loopScopeScroll);
1240+ w->raise (); // above the editor's other children, once
12121241 m_loopScopes[name] = w;
12131242 }
12141243 w->setReader (reader);
@@ -1217,16 +1246,20 @@ void SonicPiScintilla::setLiveLoopScope(const QString& name, int runLine, int sc
12171246 m_loopScopeSlots[name] = scopeNum;
12181247 applyLoopScopeColours ();
12191248
1220- if (!m_loopScopeTimer)
1221- {
1222- m_loopScopeTimer = new QTimer (this );
1223- connect (m_loopScopeTimer, &QTimer::timeout, this , [this ]() {
1224- // Hidden tab / minimized window: skip the per-scope Scintilla
1225- // position queries and stream reads. The ring overwrites itself,
1226- // so nothing backs up while polling is skipped.
1249+ // The scopes tick off the one frame pacer every animated thing shares,
1250+ // held while any scope lives. The stream is lossless at any poll rate,
1251+ // so the rate is the display's; a tick reads the streams only, and the
1252+ // scopes are placed when something moves a line (SCN_UPDATEUI), not on
1253+ // every tick.
1254+ if (!m_loopScopesPaced)
1255+ {
1256+ m_loopScopesPaced = true ;
1257+ SonicPi::FramePacer::instance ()->retain ();
1258+ m_loopScopeTick = connect (SonicPi::FramePacer::instance (), &SonicPi::FramePacer::tick, this , [this ]() {
1259+ // Hidden tab / minimized window: skip the stream reads. The ring
1260+ // overwrites itself, so nothing backs up while polling is skipped.
12271261 if (!isVisible ())
12281262 return ;
1229- positionLiveLoopScopes ();
12301263 QStringList gone;
12311264 for (auto it = m_loopScopes.constBegin (); it != m_loopScopes.constEnd (); ++it)
12321265 if (it.value ()->poll ())
@@ -1235,10 +1268,6 @@ void SonicPiScintilla::setLiveLoopScope(const QString& name, int runLine, int sc
12351268 endLiveLoopScope (name);
12361269 });
12371270 }
1238- // 16ms is purely the display frame rate: the stream is lossless at any
1239- // poll rate, so this trades smoothness against repaint cost only.
1240- if (!m_loopScopeTimer->isActive ())
1241- m_loopScopeTimer->start (16 );
12421271 positionLiveLoopScopes ();
12431272}
12441273
@@ -1265,8 +1294,8 @@ void SonicPiScintilla::endLiveLoopScope(const QString& name)
12651294 w->deleteLater ();
12661295 m_loopScopeLines.remove (name);
12671296 m_loopScopeSlots.remove (name);
1268- if (m_loopScopes.isEmpty () && m_loopScopeTimer )
1269- m_loopScopeTimer-> stop ();
1297+ if (m_loopScopes.isEmpty ())
1298+ unpaceLiveLoopScopes ();
12701299}
12711300
12721301void SonicPiScintilla::clearLiveLoopScopes ()
@@ -1276,8 +1305,23 @@ void SonicPiScintilla::clearLiveLoopScopes()
12761305 m_loopScopes.clear ();
12771306 m_loopScopeLines.clear ();
12781307 m_loopScopeSlots.clear ();
1279- if (m_loopScopeTimer)
1280- m_loopScopeTimer->stop ();
1308+ unpaceLiveLoopScopes ();
1309+ }
1310+
1311+ // The last scope gone: the pacer is let go, so it stops when nothing else is
1312+ // animating.
1313+ void SonicPiScintilla::unpaceLiveLoopScopes ()
1314+ {
1315+ if (!m_loopScopesPaced)
1316+ return ;
1317+ m_loopScopesPaced = false ;
1318+ disconnect (m_loopScopeTick);
1319+ SonicPi::FramePacer::instance ()->release ();
1320+ }
1321+
1322+ SonicPiScintilla::~SonicPiScintilla ()
1323+ {
1324+ unpaceLiveLoopScopes ();
12811325}
12821326
12831327// Whether `line` is the header of the live_loop called `name`: live_loop, then
@@ -1314,6 +1358,27 @@ void SonicPiScintilla::positionLiveLoopScopes()
13141358 int w = ScaleWidthForDPI (230 );
13151359 int viewW = viewport ()->width ();
13161360 int viewH = viewport ()->height ();
1361+ // What the editor draws beneath a scope, for it to paint over (it is
1362+ // opaque): the paper, or on the current line the caret line's colour,
1363+ // blended over the paper as Scintilla blends it.
1364+ const QColor paperColour = paper ();
1365+ const int caretLine = (int )SendScintilla (SCI_LINEFROMPOSITION , SendScintilla (SCI_GETCURRENTPOS ));
1366+ QColor caretLineColour = paperColour;
1367+ if (SendScintilla (SCI_GETCARETLINEVISIBLE ))
1368+ {
1369+ const long bgr = SendScintilla (SCI_GETCARETLINEBACK );
1370+ const QColor back (bgr & 0xff , (bgr >> 8 ) & 0xff , (bgr >> 16 ) & 0xff );
1371+ const long alpha = SendScintilla (SCI_GETCARETLINEBACKALPHA ); // SC_ALPHA_NOALPHA: opaque
1372+ if (alpha >= 256 )
1373+ caretLineColour = back;
1374+ else
1375+ {
1376+ const qreal t = alpha / 255.0 ;
1377+ caretLineColour = QColor::fromRgbF (paperColour.redF () + (back.redF () - paperColour.redF ()) * t,
1378+ paperColour.greenF () + (back.greenF () - paperColour.greenF ()) * t,
1379+ paperColour.blueF () + (back.blueF () - paperColour.blueF ()) * t);
1380+ }
1381+ }
13171382 for (auto it = m_loopScopes.begin (); it != m_loopScopes.end (); ++it)
13181383 {
13191384 int cur = runLineToCurrent (m_loopScopeLines.value (it.key (), -1 ));
@@ -1344,9 +1409,9 @@ void SonicPiScintilla::positionLiveLoopScopes()
13441409 int x = (int )SendScintilla (SCI_POINTXFROMPOSITION , (unsigned long )0 , endPos)
13451410 + ScaleWidthForDPI (12 );
13461411 x = qBound (0 , x, qMax (0 , viewW - w - ScaleWidthForDPI (6 )));
1412+ it.value ()->setBase (cur == caretLine ? caretLineColour : paperColour);
13471413 it.value ()->setGeometry (x, y, w, h);
13481414 it.value ()->show ();
1349- it.value ()->raise ();
13501415 }
13511416}
13521417
0 commit comments