Skip to content

Fix flickering popups/title bar (Wayland window-activation feedback loop) - #29

Open
dc0sk wants to merge 8 commits into
rigexpert:masterfrom
dc0sk:fix/1sec-timer-flicker
Open

dc0sk wants to merge 8 commits into
rigexpert:masterfrom
dc0sk:fix/1sec-timer-flicker

Conversation

@dc0sk

@dc0sk dc0sk commented Aug 31, 2026

Copy link
Copy Markdown

Summary

Fixes a real usability bug: popups in the main window flicker rapidly (several times a second), and the main window's title bar flickers along with them, starting specifically after alt-tabbing away from and back to the app. Reproduced and fixed live on Linux (COSMIC desktop / cosmic-comp Wayland compositor) with a real analyzer (Stick 500) connected.

Root cause

MainWindow::event() emits a focus(bool) signal on every single QEvent::WindowActivate/WindowDeactivate, which drives Measurements/Markers to show/hide two always-on-top Qt::Tool popups ("Hint"/"BriefHint" graph hints and the markers popup) via real QWidget::show()/hide(). On some Wayland compositors, mapping/unmapping those Qt::Tool surfaces itself triggers a spurious WindowActivate/WindowDeactivate back on the parent window — a self-sustaining feedback loop entirely internal to the app.

What changed

  • m_1secTimer was started with a 100ms interval despite its name and its handler being written for a once-per-second cadence — a separate, smaller flicker source, fixed first.
  • The real fix: MainWindow now debounces WindowActivate/WindowDeactivate symmetrically — neither is acted on immediately, only once the window's activation state has held steady for 300ms. And more importantly, PopUp/MarkersPopUp are now converted to plain embedded child widgets instead of separate Qt::Tool top-level windows, so there is no window for a compositor to ever see, no window-manager interaction that can perturb activation state, and nothing left in any window/task list either. All existing screen-coordinate positioning logic is untouched; only the final geometry application translates through mapFromGlobal() when embedded.
  • Removed a dead #include "screeninfo.h" / unused signal declaration in ble_analyzer.hscreeninfo.h doesn't exist anywhere in this repository at any commit reachable from master, which breaks the build on any platform.
  • Fixed a missing ; in two places (popup.cpp, onefqwidget.cpp) where a label's default stylesheet concatenates the text color straight into the next CSS property (color : whitemargin-top: 6px), silently dropping the color.
  • Fixed Measurements::m_focus/Markers::m_focus never being initialized (only ever assigned inside on_focus()) — the new debounced focus() emission can legitimately not fire at all during a normal startup, so this was being read as undefined behavior.
  • Added a reentrancy guard around on_selectDeviceDialog(): it runs a nested event loop via dlg.exec(), during which a pending QTimer::singleShot call to the same slot could in principle stack a second dialog on top of the first.

Test plan

  • Built and ran on Linux (Qt 6.11) with a RigExpert Stick 500 connected
  • Confirmed the flicker (popups + title bar) is gone, including the specific alt-tab-away-and-back trigger that reproduced it
  • Confirmed hovering a data point on the graph still shows the hint popup at the correct position, not clipped
  • Confirmed no leftover popup/window entries in the window manager's task list
  • Ran a continuous sweep with the connected analyzer to confirm normal measurement flow is unaffected

dc0sk added 8 commits August 31, 2026 18:12
m_1secTimer->start(100) started the timer with a 100ms interval despite
its name and its handler, on_1secTimerTick(), being written for a
once-per-second cadence: it toggles the graph hint popups' visibility
based on cursor position every tick. At 10Hz instead of 1Hz this causes
visibly flickering popups during normal use.
analyzer/ble_analyzer.h includes "screeninfo.h" and declares a
setScreenInfo(ScreenInfo&) signal; screeninfo.h does not exist anywhere
in this repository at any commit reachable from master, and
setScreenInfo is never defined or called anywhere either. This breaks
any build on any platform.
…dback loop

MainWindow::event() reacted to every single WindowActivate/WindowDeactivate
by showing/hiding two always-on-top Qt::Tool popups (the graph 'Hint' and
'BriefHint' hints, and the markers popup). On some Wayland compositors
(observed on COSMIC/cosmic-comp), unmapping/remapping those Qt::Tool
surfaces itself triggers a spurious WindowActivate/WindowDeactivate on the
parent window - which re-fires the same show/hide logic, creating a
self-sustaining feedback loop that reads as rapidly flickering popups and
a flickering main window title bar.

Two changes, together:
- MainWindow now debounces WindowActivate/WindowDeactivate symmetrically:
  neither is acted on immediately: only once the window's activation
  state has held steady for 300ms is focus() actually emitted, and only
  if it differs from the last emitted state.
- PopUp::focusShow()/focusHide() and MarkersPopUp::focusShow()/focusHide()
  no longer map/unmap the underlying window at all; they move it
  on/off-screen instead. This keeps the Wayland surface continuously
  mapped, so showing/hiding these popups can no longer perturb the
  parent window's activation state, breaking the feedback loop at its
  root rather than just slowing it down.

Also adds Qt::WindowDoesNotAcceptFocus to both classes' window flags as
defense in depth.

Separately, mainwindow.cpp already had m_1secTimer - a timer whose
handler toggles the same hint popups based on cursor position - started
with a 100ms interval despite its name and intent; that is a distinct,
already-fixed bug (see previous commit) that made the same popups flash
on every mouse-cursor check instead of once a second.
label.setStyleSheet("QLabel { color : " + m_textColor + "margin-top: ...")
concatenates the text color directly onto the next CSS property with no
separating semicolon, producing an invalid declaration like
'color : whitemargin-top: 6px' - the color is silently dropped by Qt's
stylesheet parser. PopUp::setTextColor() already builds the equivalent
string correctly (with a ';' after the color); this makes the default
built in init() match it, so any PopUp that never calls setTextColor()
still gets a valid, visible label color instead of relying on it being
overwritten later.
PopUp and MarkersPopUp were always separate Qt::Tool top-level windows.
The previous fix (park off-screen instead of unmapping) stopped them
causing flicker, but left them permanently registered as real windows
with the window manager/task switcher, since they are never truly
unmapped once shown.

Add an 'embedded' constructor parameter: when true, the widget is a
plain child of its parent instead of a separate top-level window, so
there is no window for a window manager/compositor to ever see - no
flicker risk, and nothing to show up in any window/task list either.
All existing position-tracking code (setName/setPosition/
MainWindowPos/mouseMoveEvent) keeps computing m_x/m_y as global screen
coordinates exactly as before; only the final geometry application
(a new applyGeometry() helper) translates through
parentWidget()->mapFromGlobal() when embedded, so none of that math
needed to change.

m_graphHint, m_graphBriefHint (Measurements) and m_markersHint
(Markers) are now constructed embedded, parented to MainWindow.
Both were declared but never initialized, and only ever assigned inside
on_focus(bool). Previously the first WindowActivate at startup emitted
focus(true) essentially immediately, setting it before anything read it.
The new debounced focus() emission (see the previous commit on this
branch fixing the WindowActivate/Deactivate feedback loop) can now
legitimately not emit at all during startup if the window starts
active, leaving m_focus read before it is ever written - undefined
behavior. Default-initialize both to true, matching the normal case of
an app that starts with the main window focused.
Same bug as db3c4ae (popup.cpp): the color declaration runs straight
into the next CSS property with no separating semicolon
('color : #RRGGBBmargin-top: 6px'), silently dropping the color. The
class's own setTextColor() builds the equivalent string correctly.
on_selectDeviceDialog() runs a nested event loop via dlg.exec(). A
pending QTimer::singleShot call to the same slot (e.g. from
on_refreshConnection()) would still fire while that nested loop is
running, stacking a second real top-level SelectDeviceDialog on top of
the first. No live path currently re-enters it, but the pattern is
fragile against future changes and costs nothing to guard directly.
archlinux-github pushed a commit to archlinux/aur that referenced this pull request Aug 31, 2026
Upstream has moved from 1.2.6 (Qt5) to 2.0.3 (Qt6, Bluetooth support,
UI redesign) with no tags or releases for Linux, so the head commit is
pinned as of packaging (bc7b74d). Reworks the package around that:

- depends on qt6-base/qt6-serialport/qt6-connectivity/libusb instead
  of qt5-base/qt5-serialport/glibc/gcc-libs
- license now lists both MIT (upstream's own code) and
  GPL-3.0-or-later (the vendored qcustomplot.cpp), previously just MIT
- install layout simplified to a normal /usr/bin/AntScope2 binary
  instead of /usr/share/antscope2/AntScope2 plus a symlink
- fixes real bugs in the old PKGBUILD: LICENSE.txt installed to a
  nonstandard location, a broken .icns icon reference in the desktop
  file (Linux icon themes don't resolve macOS icon files), a udev rule
  numbered after 73-seat-late.rules so its TAG+="uaccess" never
  actually took effect, and a self-conflicting conflicts=('antscope2'
  'antscope2-git') plus a typo'd provide=() that pacman silently
  ignored
- adds the two known upstream Linux defects as patches: a build-
  breaking reference to a nonexistent screeninfo.h, and
  Settings::localDataPath() resolving data files relative to the
  binary's own directory rather than a real install path

Also adds 13 patches for a flickering-popups/title-bar bug (a
WindowActivate/WindowDeactivate feedback loop against certain Wayland
compositors) and its underlying defects, plus five out-of-bounds
reads in device/network parsing and disabled TLS certificate
verification - found and fixed while chasing the flicker bug, and
submitted upstream as:
  rigexpert/AntScope2#29
  rigexpert/AntScope2#30
Not yet merged as of packaging; applied here in the meantime.
archlinux-github pushed a commit to archlinux/aur that referenced this pull request Sep 2, 2026
Long-neglected package that had drifted far from what it actually builds:

- Qt5 deps (qt5-base/qt5-serialport/glibc/gcc-libs) for a project that
  moved to Qt6 with Bluetooth support; switched to
  qt6-base/qt6-serialport/qt6-connectivity/libusb, matching the antscope2
  package's own (tested) dependency list.
- Pinned to a stale commit (19323af) despite being a "-git" package that
  should track the live tip; replaced with a proper pkgver() deriving a
  monotonic version from ANTSCOPE2VER plus commit count/hash, and a VCS
  source with no commit pin.
- Fixed two real bugs that would have broken every build: a missing path
  separator ("${pkgdir}"usr/share/... instead of "${pkgdir}"/usr/share/...)
  and an unterminated string literal in the LICENSE.txt install line.
- Fixed provides=('antcsope2-git') (transposed letters) to
  provides=('antscope2') - a -git package should provide the versioned
  package's name, not a typo of its own.
- Removed a stray conflicts=('antscop2') (another typo, a third
  nonexistent package name) and an unused .INSTALL hook whose symlink
  approach the modernized install layout below no longer needs.
- Modernized the install layout to match the antscope2 package: binary
  installs straight to /usr/bin (no /usr/share/antscope2-git/AntScope2 +
  symlink), the actually-current udev rule (0483:a1de and 0483:a1da, uaccess
  tag, correctly numbered - the old 99-rigexpert.rules used a blanket
  MODE="0666") and desktop file (the old one pointed its icon at a
  Windows-only .icns file that was never installed anywhere on Linux).

Source now points at a maintainer fork branch (linux-packaging) that merges
two sets of fixes - flickering popups/title bar, and out-of-bounds reads
plus a completed firmware-update fix - submitted upstream as
rigexpert/AntScope2#29 and #30 but not yet merged.
This -git package tracks that branch directly (no patch files needed,
unlike the versioned antscope2 package, which pins a commit and applies
patches); once the PRs merge, this should point back at upstream directly.

Verified: a fresh clone of the fork branch, with the one remaining
Linux-packaging-only patch (linux-fixed-system-data-path.patch) applied,
builds cleanly with qmake6/make and produces a working binary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EAiXVpAbBuCAxWNA3mzJY
@dc0sk
dc0sk force-pushed the fix/1sec-timer-flicker branch from 9fb2af8 to 6f8486a Compare September 2, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant