Skip to content

Fix floating applets losing float state and position across restarts (#959)#985

Merged
ten9876 merged 7 commits into
aethersdr:mainfrom
chibondking:fix/conditional-applet-float-restore
Apr 8, 2026
Merged

Fix floating applets losing float state and position across restarts (#959)#985
ten9876 merged 7 commits into
aethersdr:mainfrom
chibondking:fix/conditional-applet-float-restore

Conversation

@chibondking

Copy link
Copy Markdown
Collaborator

Fix floating applets losing float state and position across restarts (#959)

Four distinct bugs in the floating applet save/restore cycle, all fixed.

Bug 1 — Float state lost on toggle off/on (#959)

When a floating applet was toggled off and back on it reappeared docked.
Root cause: toggle button handlers and setTunerVisible/setAmpVisible/
setAgVisible re-enable paths did not check IsFloating from AppSettings
when no floating window was present. The bare QWidget::setVisible()
connections for TUN/AMP/AG were replaced with float-aware lambdas. All
toggle handlers now call floatApplet() via QTimer::singleShot(0) when
IsFloating is True, matching the startup restore logic.

setTunerVisible/setAmpVisible/setAgVisible also apply the same check on
the visible=true path (device reconnect). On the visible=false path they
now call hideAndSave() instead of dockApplet(), preserving IsFloating so
the window returns floating on reconnect.

Bug 2 — Window drift on every hide/show cycle

saveGeometry() was saving geometry().x()/y() (client-area origin) and
restoring with move() (frame origin). The decoration height accumulated
on every cycle, drifting the window down one title-bar height per show.
Fix: save pos() for x/y and geometry().size() for w/h.

Bug 3 — WM centering overwriting saved position on restore

On show(), the WM re-centers the window and fires moveEvent, starting
the 400ms debounce timer. restoreGeometry() at 200ms moved the window
correctly, but the WM's ConfigureNotify for that move arrived after the
guard cleared at 500ms, restarting the debounce and saving the centered
position. Three-part fix:

  • showAndRestore() owns the entire guard lifetime: sets
    m_restoringGeometry=true before show(), clears at 650ms (200ms delay
    • 300ms WM settle + 150ms safety). Previously cleared at 500ms.
  • restoreGeometry() no longer manages the guard — it was racing with
    showAndRestore's lifetime and clearing it early.
  • moveEvent and resizeEvent check the guard before starting the debounce
    timer; dockApplet() uses hideAndSave() to stop the debounce timer
    before hiding so a pending callback cannot fire on a hidden window.

Bug 4 — Float state and positions lost on app restart (Windows)

Three separate causes:

a) Shutdown race (all platforms)
On Windows, the OS sends WM_CLOSE to all owned tool windows when the
main window closes. Whichever floating windows received their close event
before AppletPanel became invisible triggered the "user docked it" branch
of closeEvent, writing IsFloating=False. Timing was non-deterministic so
different applets failed on different exits.
Fix: FloatingAppletWindow connects to QCoreApplication::aboutToQuit and
sets m_appShuttingDown=true. closeEvent checks this flag and takes the
"save and accept" branch regardless of parent visibility, so dockRequested
is never emitted during shutdown.

b) P/CW applet ID contains '/' — invalid XML element name
AppSettings::save() writes keys as XML element names. The key
"FloatingApplet_P/CW_IsFloating" is not valid XML. The validKey regex
incorrectly allowed '/' so the malformed element was written to the temp
file. The post-write XML validator caught it and aborted the entire save —
meaning ALL settings changes in any session where P/CW was floating were
silently discarded (positions, IsFloating, frequency, etc.).
Fix: validKey regex tightened to ^[A-Za-z_][A-Za-z0-9_]$ (no '/' or '.').
A settingsPrefix() helper in FloatingAppletWindow and a floatKey() helper
in AppletPanel sanitize '/' to '' before forming any settings key, so
"P/CW" maps to "FloatingApplet_P_CW
" keys throughout.

c) Geometry not saved on shutdown
saveGeometry() was only called from the debounce timer and dockApplet().
On app exit the windows are destroyed without an explicit dock, so the
final position was never written.
Fix: saveGeometry() is now called explicitly in hideAndSave() (before
hide()), in both branches of closeEvent, and in hideEvent as a
belt-and-suspenders backup for Linux-style parent-cascade hides. This
ensures geometry is captured from a valid window state on every platform
regardless of which shutdown path the WM takes.

NOTE: S-Meter applet is still not fixed.. Lower priority to fix the above mentioned key underlying bugs: this was very difficult to chase down on Windows.

Fixes #959

chibondking and others added 7 commits April 7, 2026 14:36
When a floating applet was toggled off and back on, it reappeared docked
instead of floating. Restarting the app restored the correct float state
because the startup path reads IsFloating from AppSettings — but the
toggle and device-reconnect paths did not.

## Root cause

Two separate gaps in AppletPanel:

1. Toggle button handlers — the makeEntry lambda and the bare
   `connect(btn, toggled, wrapper, &QWidget::setVisible)` connections
   for TUN/AMP/AG only checked m_floatingWindows (which is empty after
   a dock) and never read IsFloating from AppSettings on re-enable.

2. setTunerVisible / setAmpVisible / setAgVisible re-enable paths —
   called when a conditional device (TGXL, amp, Antenna Genius)
   reconnects. Same omission: applet was shown docked regardless of
   persisted float state.

## Fix

All toggle button handlers now check FloatingApplet_<ID>_IsFloating
from AppSettings when checked=true and no floating window exists,
and call floatApplet() via QTimer::singleShot(0) if the flag is set.
This mirrors the startup restore logic exactly.

The bare QWidget::setVisible slot connections for TUN, AMP, and AG
are replaced with full float-aware lambdas matching the makeEntry
pattern, so all 10 applets now have consistent toggle behaviour.

setTunerVisible / setAmpVisible / setAgVisible also apply the same
check on the visible=true path, covering the device-reconnect scenario.

Fixes aethersdr#959

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ck cycles (aethersdr#959)

Three related bugs in the floating applet save/restore cycle are fixed together.

When a floating applet was toggled off and back on, it reappeared docked
instead of floating. Restarting the app correctly restored float state
because the startup path reads IsFloating from AppSettings, but the toggle
and device-reconnect paths did not.

Root cause: two gaps in AppletPanel:

1. Toggle button handlers — the makeEntry lambda and the bare
   `connect(btn, toggled, wrapper, &QWidget::setVisible)` connections
   for TUN/AMP/AG only checked m_floatingWindows (which is empty after
   a dock) and never read IsFloating from AppSettings on re-enable.

2. setTunerVisible / setAmpVisible / setAgVisible re-enable paths —
   called when a conditional device (TGXL, amp, Antenna Genius)
   reconnects. Same omission: applet was shown docked regardless of
   persisted float state.

Fix: all toggle button handlers now check FloatingApplet_<ID>_IsFloating
from AppSettings when checked=true and no floating window exists, and call
floatApplet() via QTimer::singleShot(0) if set. This mirrors the startup
restore logic exactly. The bare QWidget::setVisible connections for TUN,
AMP, and AG are replaced with full float-aware lambdas matching the
makeEntry pattern, so all 10 applets have consistent toggle behaviour.

setTunerVisible / setAmpVisible / setAgVisible also apply the same check
on the visible=true path, covering the device-reconnect scenario. On the
visible=false path they now call hideAndSave() instead of dockApplet(),
preserving IsFloating so the window returns floating on reconnect.

saveGeometry() was saving geometry().x()/y() (client-area origin) and
restoring with move() (which sets the frame origin). The decoration height
was added to Y on every cycle, causing the window to drift downward one
title-bar height per hide/show.

Fix: save pos() for x/y (frame position, what move() sets) and
geometry().size() for w/h (client size, what resize() sets).

On show(), the WM re-centers the window and fires moveEvent. restoreGeometry()
runs at 200ms and moves the window correctly. But the 400ms debounce timer
(started by the WM's moveEvent) was not suppressed, and the WM sends a final
ConfigureNotify for the move() call after the guard was cleared at 500ms —
restarting the debounce timer. On slow WMs or under load, this caused the
centered position to be saved over the correct one.

Three-part fix:

1. showAndRestore() now owns the entire guard lifetime: sets
   m_restoringGeometry = true before show(), clears it at 650ms
   (200ms restore delay + 300ms WM settle + 150ms safety margin).
   Previously the guard was cleared at 500ms, leaving a window where
   a late ConfigureNotify could corrupt the save.

2. restoreGeometry() no longer manages the guard at all — it was
   previously racing with showAndRestore's lifetime and clearing it
   early. moveEvent and resizeEvent check the guard before starting
   the debounce timer, so all WM-generated events during the full
   show+restore+settle window are suppressed.

3. dockApplet() now calls hideAndSave() instead of saveGeometry()+hide().
   This stops the debounce timer before hiding, preventing a pending
   timer callback from calling saveGeometry() on an already-hidden
   window where pos() is not valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ck cycles (aethersdr#959)

Three related bugs in the floating applet save/restore cycle are fixed together.

When a floating applet was toggled off and back on, it reappeared docked
instead of floating. Restarting the app correctly restored float state
because the startup path reads IsFloating from AppSettings, but the toggle
and device-reconnect paths did not.

Root cause: two gaps in AppletPanel:

1. Toggle button handlers — the makeEntry lambda and the bare
   `connect(btn, toggled, wrapper, &QWidget::setVisible)` connections
   for TUN/AMP/AG only checked m_floatingWindows (which is empty after
   a dock) and never read IsFloating from AppSettings on re-enable.

2. setTunerVisible / setAmpVisible / setAgVisible re-enable paths —
   called when a conditional device (TGXL, amp, Antenna Genius)
   reconnects. Same omission: applet was shown docked regardless of
   persisted float state.

Fix: all toggle button handlers now check FloatingApplet_<ID>_IsFloating
from AppSettings when checked=true and no floating window exists, and call
floatApplet() via QTimer::singleShot(0) if set. This mirrors the startup
restore logic exactly. The bare QWidget::setVisible connections for TUN,
AMP, and AG are replaced with full float-aware lambdas matching the
makeEntry pattern, so all 10 applets have consistent toggle behaviour.

setTunerVisible / setAmpVisible / setAgVisible also apply the same check
on the visible=true path, covering the device-reconnect scenario. On the
visible=false path they now call hideAndSave() instead of dockApplet(),
preserving IsFloating so the window returns floating on reconnect.

saveGeometry() was saving geometry().x()/y() (client-area origin) and
restoring with move() (which sets the frame origin). The decoration height
was added to Y on every cycle, causing the window to drift downward one
title-bar height per hide/show.

Fix: save pos() for x/y (frame position, what move() sets) and
geometry().size() for w/h (client size, what resize() sets).

On show(), the WM re-centers the window and fires moveEvent. restoreGeometry()
runs at 200ms and moves the window correctly. But the 400ms debounce timer
(started by the WM's moveEvent) was not suppressed, and the WM sends a final
ConfigureNotify for the move() call after the guard was cleared at 500ms —
restarting the debounce timer. On slow WMs or under load, this caused the
centered position to be saved over the correct one.

Three-part fix:

1. showAndRestore() now owns the entire guard lifetime: sets
   m_restoringGeometry = true before show(), clears it at 650ms
   (200ms restore delay + 300ms WM settle + 150ms safety margin).
   Previously the guard was cleared at 500ms, leaving a window where
   a late ConfigureNotify could corrupt the save.

2. restoreGeometry() no longer manages the guard at all — it was
   previously racing with showAndRestore's lifetime and clearing it
   early. moveEvent and resizeEvent check the guard before starting
   the debounce timer, so all WM-generated events during the full
   show+restore+settle window are suppressed.

3. dockApplet() now calls hideAndSave() instead of saveGeometry()+hide().
   This stops the debounce timer before hiding, preventing a pending
   timer callback from calling saveGeometry() on an already-hidden
   window where pos() is not valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ck cycles (aethersdr#959)

Three related bugs in the floating applet save/restore cycle are fixed together.

When a floating applet was toggled off and back on, it reappeared docked
instead of floating. Restarting the app correctly restored float state
because the startup path reads IsFloating from AppSettings, but the toggle
and device-reconnect paths did not.

Root cause: two gaps in AppletPanel:

1. Toggle button handlers — the makeEntry lambda and the bare
   `connect(btn, toggled, wrapper, &QWidget::setVisible)` connections
   for TUN/AMP/AG only checked m_floatingWindows (which is empty after
   a dock) and never read IsFloating from AppSettings on re-enable.

2. setTunerVisible / setAmpVisible / setAgVisible re-enable paths —
   called when a conditional device (TGXL, amp, Antenna Genius)
   reconnects. Same omission: applet was shown docked regardless of
   persisted float state.

Fix: all toggle button handlers now check FloatingApplet_<ID>_IsFloating
from AppSettings when checked=true and no floating window exists, and call
floatApplet() via QTimer::singleShot(0) if set. This mirrors the startup
restore logic exactly. The bare QWidget::setVisible connections for TUN,
AMP, and AG are replaced with full float-aware lambdas matching the
makeEntry pattern, so all 10 applets have consistent toggle behaviour.

setTunerVisible / setAmpVisible / setAgVisible also apply the same check
on the visible=true path, covering the device-reconnect scenario. On the
visible=false path they now call hideAndSave() instead of dockApplet(),
preserving IsFloating so the window returns floating on reconnect.

saveGeometry() was saving geometry().x()/y() (client-area origin) and
restoring with move() (which sets the frame origin). The decoration height
was added to Y on every cycle, causing the window to drift downward one
title-bar height per hide/show.

Fix: save pos() for x/y (frame position, what move() sets) and
geometry().size() for w/h (client size, what resize() sets).

On show(), the WM re-centers the window and fires moveEvent. restoreGeometry()
runs at 200ms and moves the window correctly. But the 400ms debounce timer
(started by the WM's moveEvent) was not suppressed, and the WM sends a final
ConfigureNotify for the move() call after the guard was cleared at 500ms —
restarting the debounce timer. On slow WMs or under load, this caused the
centered position to be saved over the correct one.

Three-part fix:

1. showAndRestore() now owns the entire guard lifetime: sets
   m_restoringGeometry = true before show(), clears it at 650ms
   (200ms restore delay + 300ms WM settle + 150ms safety margin).
   Previously the guard was cleared at 500ms, leaving a window where
   a late ConfigureNotify could corrupt the save.

2. restoreGeometry() no longer manages the guard at all — it was
   previously racing with showAndRestore's lifetime and clearing it
   early. moveEvent and resizeEvent check the guard before starting
   the debounce timer, so all WM-generated events during the full
   show+restore+settle window are suppressed.

3. dockApplet() now calls hideAndSave() instead of saveGeometry()+hide().
   This stops the debounce timer before hiding, preventing a pending
   timer callback from calling saveGeometry() on an already-hidden
   window where pos() is not valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chibondking chibondking requested a review from ten9876 as a code owner April 8, 2026 02:51
@ten9876 ten9876 merged commit 5466642 into aethersdr:main Apr 8, 2026
14 checks passed
chibondking added a commit to chibondking/AetherSDR that referenced this pull request Apr 10, 2026
The S-Meter section can now be detached into a floating window and
re-docked exactly like all other applets — using the same
FloatingAppletWindow infrastructure, AppSettings geometry persistence,
and PR aethersdr#916 / aethersdr#985 bug fixes (drift, WM centering, shutdown race).

## What changed

### src/gui/AppletPanel.h
- Added `QWidget* m_sMeterContent` — the floatable content container
  (SMeterWidget + TX/RX select row + Peak Hold row), analogous to
  the applet widget held at layout index 1 in every other wrapper.
- Added private `floatSMeter()` and `dockSMeter()` methods.

### src/gui/AppletPanel.cpp

**S-Meter construction:**
- `AppletTitleBar("S-Meter", "VU", this)` — passes the panel pointer
  so the right-click context menu ("⬈ Pop out" / "↩ Dock") is active.
- Introduced `m_sMeterContent` QWidget with its own QVBoxLayout as
  layout index 1 of `m_sMeterSection` (after the title bar), so the
  extractor logic in `floatSMeter()` / `dockSMeter()` is identical to
  every other applet.

**ANLG toggle button:**
- Updated to the float-aware pattern used by TUN / AMP / AG:
  raises/hides the floating window when VU is floating, re-floats
  via `QTimer::singleShot(0)` when IsFloating is persisted but no
  window is open yet (toggle-off → toggle-on cycle fix from aethersdr#985).

**`isAppletFloating()`:**
- Added early return for `"VU"`: checks `m_floatingWindows.contains("VU")`
  directly (VU is not in `m_appletOrder`).

**`floatApplet()` / `dockApplet()`:**
- Added `if (id == "VU")` dispatch at the top of each, delegating to
  `floatSMeter()` / `dockSMeter()`.

**`floatSMeter()` / `dockSMeter()`:**
- Mirror `floatApplet()` / `dockApplet()` exactly: reparent content
  out of/back into `m_sMeterSection`, create/destroy
  `FloatingAppletWindow`, persist `FloatingApplet_VU_IsFloating`,
  save geometry on dock.

**Startup restore:**
- After the existing `m_appletOrder` restore loop, added a
  `QTimer::singleShot(0)` restore for `"VU"` so the S-Meter reopens
  floating if it was floating when the app last closed.

Closes aethersdr#993

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
aethersdr-agent Bot pushed a commit that referenced this pull request Apr 10, 2026
…959) (#985)

* Fix floating applets losing float state on toggle off/on (#959)

When a floating applet was toggled off and back on, it reappeared docked
instead of floating. Restarting the app restored the correct float state
because the startup path reads IsFloating from AppSettings — but the
toggle and device-reconnect paths did not.

## Root cause

Two separate gaps in AppletPanel:

1. Toggle button handlers — the makeEntry lambda and the bare
   `connect(btn, toggled, wrapper, &QWidget::setVisible)` connections
   for TUN/AMP/AG only checked m_floatingWindows (which is empty after
   a dock) and never read IsFloating from AppSettings on re-enable.

2. setTunerVisible / setAmpVisible / setAgVisible re-enable paths —
   called when a conditional device (TGXL, amp, Antenna Genius)
   reconnects. Same omission: applet was shown docked regardless of
   persisted float state.

## Fix

All toggle button handlers now check FloatingApplet_<ID>_IsFloating
from AppSettings when checked=true and no floating window exists,
and call floatApplet() via QTimer::singleShot(0) if the flag is set.
This mirrors the startup restore logic exactly.

The bare QWidget::setVisible slot connections for TUN, AMP, and AG
are replaced with full float-aware lambdas matching the makeEntry
pattern, so all 10 applets now have consistent toggle behaviour.

setTunerVisible / setAmpVisible / setAgVisible also apply the same
check on the visible=true path, covering the device-reconnect scenario.

Fixes #959

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Potential fix on applet positioning when restoring from panel

* Fix applet restore 'bounce' affecting window position on restore

* Fix applet restore 'bounce' affecting window position on restore

* Fix floating applets losing float state and position across dock/undock cycles (#959)

Three related bugs in the floating applet save/restore cycle are fixed together.

When a floating applet was toggled off and back on, it reappeared docked
instead of floating. Restarting the app correctly restored float state
because the startup path reads IsFloating from AppSettings, but the toggle
and device-reconnect paths did not.

Root cause: two gaps in AppletPanel:

1. Toggle button handlers — the makeEntry lambda and the bare
   `connect(btn, toggled, wrapper, &QWidget::setVisible)` connections
   for TUN/AMP/AG only checked m_floatingWindows (which is empty after
   a dock) and never read IsFloating from AppSettings on re-enable.

2. setTunerVisible / setAmpVisible / setAgVisible re-enable paths —
   called when a conditional device (TGXL, amp, Antenna Genius)
   reconnects. Same omission: applet was shown docked regardless of
   persisted float state.

Fix: all toggle button handlers now check FloatingApplet_<ID>_IsFloating
from AppSettings when checked=true and no floating window exists, and call
floatApplet() via QTimer::singleShot(0) if set. This mirrors the startup
restore logic exactly. The bare QWidget::setVisible connections for TUN,
AMP, and AG are replaced with full float-aware lambdas matching the
makeEntry pattern, so all 10 applets have consistent toggle behaviour.

setTunerVisible / setAmpVisible / setAgVisible also apply the same check
on the visible=true path, covering the device-reconnect scenario. On the
visible=false path they now call hideAndSave() instead of dockApplet(),
preserving IsFloating so the window returns floating on reconnect.

saveGeometry() was saving geometry().x()/y() (client-area origin) and
restoring with move() (which sets the frame origin). The decoration height
was added to Y on every cycle, causing the window to drift downward one
title-bar height per hide/show.

Fix: save pos() for x/y (frame position, what move() sets) and
geometry().size() for w/h (client size, what resize() sets).

On show(), the WM re-centers the window and fires moveEvent. restoreGeometry()
runs at 200ms and moves the window correctly. But the 400ms debounce timer
(started by the WM's moveEvent) was not suppressed, and the WM sends a final
ConfigureNotify for the move() call after the guard was cleared at 500ms —
restarting the debounce timer. On slow WMs or under load, this caused the
centered position to be saved over the correct one.

Three-part fix:

1. showAndRestore() now owns the entire guard lifetime: sets
   m_restoringGeometry = true before show(), clears it at 650ms
   (200ms restore delay + 300ms WM settle + 150ms safety margin).
   Previously the guard was cleared at 500ms, leaving a window where
   a late ConfigureNotify could corrupt the save.

2. restoreGeometry() no longer manages the guard at all — it was
   previously racing with showAndRestore's lifetime and clearing it
   early. moveEvent and resizeEvent check the guard before starting
   the debounce timer, so all WM-generated events during the full
   show+restore+settle window are suppressed.

3. dockApplet() now calls hideAndSave() instead of saveGeometry()+hide().
   This stops the debounce timer before hiding, preventing a pending
   timer callback from calling saveGeometry() on an already-hidden
   window where pos() is not valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix floating applets losing float state and position across dock/undock cycles (#959)

Three related bugs in the floating applet save/restore cycle are fixed together.

When a floating applet was toggled off and back on, it reappeared docked
instead of floating. Restarting the app correctly restored float state
because the startup path reads IsFloating from AppSettings, but the toggle
and device-reconnect paths did not.

Root cause: two gaps in AppletPanel:

1. Toggle button handlers — the makeEntry lambda and the bare
   `connect(btn, toggled, wrapper, &QWidget::setVisible)` connections
   for TUN/AMP/AG only checked m_floatingWindows (which is empty after
   a dock) and never read IsFloating from AppSettings on re-enable.

2. setTunerVisible / setAmpVisible / setAgVisible re-enable paths —
   called when a conditional device (TGXL, amp, Antenna Genius)
   reconnects. Same omission: applet was shown docked regardless of
   persisted float state.

Fix: all toggle button handlers now check FloatingApplet_<ID>_IsFloating
from AppSettings when checked=true and no floating window exists, and call
floatApplet() via QTimer::singleShot(0) if set. This mirrors the startup
restore logic exactly. The bare QWidget::setVisible connections for TUN,
AMP, and AG are replaced with full float-aware lambdas matching the
makeEntry pattern, so all 10 applets have consistent toggle behaviour.

setTunerVisible / setAmpVisible / setAgVisible also apply the same check
on the visible=true path, covering the device-reconnect scenario. On the
visible=false path they now call hideAndSave() instead of dockApplet(),
preserving IsFloating so the window returns floating on reconnect.

saveGeometry() was saving geometry().x()/y() (client-area origin) and
restoring with move() (which sets the frame origin). The decoration height
was added to Y on every cycle, causing the window to drift downward one
title-bar height per hide/show.

Fix: save pos() for x/y (frame position, what move() sets) and
geometry().size() for w/h (client size, what resize() sets).

On show(), the WM re-centers the window and fires moveEvent. restoreGeometry()
runs at 200ms and moves the window correctly. But the 400ms debounce timer
(started by the WM's moveEvent) was not suppressed, and the WM sends a final
ConfigureNotify for the move() call after the guard was cleared at 500ms —
restarting the debounce timer. On slow WMs or under load, this caused the
centered position to be saved over the correct one.

Three-part fix:

1. showAndRestore() now owns the entire guard lifetime: sets
   m_restoringGeometry = true before show(), clears it at 650ms
   (200ms restore delay + 300ms WM settle + 150ms safety margin).
   Previously the guard was cleared at 500ms, leaving a window where
   a late ConfigureNotify could corrupt the save.

2. restoreGeometry() no longer manages the guard at all — it was
   previously racing with showAndRestore's lifetime and clearing it
   early. moveEvent and resizeEvent check the guard before starting
   the debounce timer, so all WM-generated events during the full
   show+restore+settle window are suppressed.

3. dockApplet() now calls hideAndSave() instead of saveGeometry()+hide().
   This stops the debounce timer before hiding, preventing a pending
   timer callback from calling saveGeometry() on an already-hidden
   window where pos() is not valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix floating applets losing float state and position across dock/undock cycles (#959)

Three related bugs in the floating applet save/restore cycle are fixed together.

When a floating applet was toggled off and back on, it reappeared docked
instead of floating. Restarting the app correctly restored float state
because the startup path reads IsFloating from AppSettings, but the toggle
and device-reconnect paths did not.

Root cause: two gaps in AppletPanel:

1. Toggle button handlers — the makeEntry lambda and the bare
   `connect(btn, toggled, wrapper, &QWidget::setVisible)` connections
   for TUN/AMP/AG only checked m_floatingWindows (which is empty after
   a dock) and never read IsFloating from AppSettings on re-enable.

2. setTunerVisible / setAmpVisible / setAgVisible re-enable paths —
   called when a conditional device (TGXL, amp, Antenna Genius)
   reconnects. Same omission: applet was shown docked regardless of
   persisted float state.

Fix: all toggle button handlers now check FloatingApplet_<ID>_IsFloating
from AppSettings when checked=true and no floating window exists, and call
floatApplet() via QTimer::singleShot(0) if set. This mirrors the startup
restore logic exactly. The bare QWidget::setVisible connections for TUN,
AMP, and AG are replaced with full float-aware lambdas matching the
makeEntry pattern, so all 10 applets have consistent toggle behaviour.

setTunerVisible / setAmpVisible / setAgVisible also apply the same check
on the visible=true path, covering the device-reconnect scenario. On the
visible=false path they now call hideAndSave() instead of dockApplet(),
preserving IsFloating so the window returns floating on reconnect.

saveGeometry() was saving geometry().x()/y() (client-area origin) and
restoring with move() (which sets the frame origin). The decoration height
was added to Y on every cycle, causing the window to drift downward one
title-bar height per hide/show.

Fix: save pos() for x/y (frame position, what move() sets) and
geometry().size() for w/h (client size, what resize() sets).

On show(), the WM re-centers the window and fires moveEvent. restoreGeometry()
runs at 200ms and moves the window correctly. But the 400ms debounce timer
(started by the WM's moveEvent) was not suppressed, and the WM sends a final
ConfigureNotify for the move() call after the guard was cleared at 500ms —
restarting the debounce timer. On slow WMs or under load, this caused the
centered position to be saved over the correct one.

Three-part fix:

1. showAndRestore() now owns the entire guard lifetime: sets
   m_restoringGeometry = true before show(), clears it at 650ms
   (200ms restore delay + 300ms WM settle + 150ms safety margin).
   Previously the guard was cleared at 500ms, leaving a window where
   a late ConfigureNotify could corrupt the save.

2. restoreGeometry() no longer manages the guard at all — it was
   previously racing with showAndRestore's lifetime and clearing it
   early. moveEvent and resizeEvent check the guard before starting
   the debounce timer, so all WM-generated events during the full
   show+restore+settle window are suppressed.

3. dockApplet() now calls hideAndSave() instead of saveGeometry()+hide().
   This stops the debounce timer before hiding, preventing a pending
   timer callback from calling saveGeometry() on an already-hidden
   window where pos() is not valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
chibondking added a commit to chibondking/AetherSDR that referenced this pull request Apr 11, 2026
The S-Meter section can now be detached into a floating window and
re-docked exactly like all other applets — using the same
FloatingAppletWindow infrastructure, AppSettings geometry persistence,
and PR aethersdr#916 / aethersdr#985 bug fixes (drift, WM centering, shutdown race).

## What changed

### src/gui/AppletPanel.h
- Added `QWidget* m_sMeterContent` — the floatable content container
  (SMeterWidget + TX/RX select row + Peak Hold row), analogous to
  the applet widget held at layout index 1 in every other wrapper.
- Added private `floatSMeter()` and `dockSMeter()` methods.

### src/gui/AppletPanel.cpp

**S-Meter construction:**
- `AppletTitleBar("S-Meter", "VU", this)` — passes the panel pointer
  so the right-click context menu ("⬈ Pop out" / "↩ Dock") is active.
- Introduced `m_sMeterContent` QWidget with its own QVBoxLayout as
  layout index 1 of `m_sMeterSection` (after the title bar), so the
  extractor logic in `floatSMeter()` / `dockSMeter()` is identical to
  every other applet.

**ANLG toggle button:**
- Updated to the float-aware pattern used by TUN / AMP / AG:
  raises/hides the floating window when VU is floating, re-floats
  via `QTimer::singleShot(0)` when IsFloating is persisted but no
  window is open yet (toggle-off → toggle-on cycle fix from aethersdr#985).

**`isAppletFloating()`:**
- Added early return for `"VU"`: checks `m_floatingWindows.contains("VU")`
  directly (VU is not in `m_appletOrder`).

**`floatApplet()` / `dockApplet()`:**
- Added `if (id == "VU")` dispatch at the top of each, delegating to
  `floatSMeter()` / `dockSMeter()`.

**`floatSMeter()` / `dockSMeter()`:**
- Mirror `floatApplet()` / `dockApplet()` exactly: reparent content
  out of/back into `m_sMeterSection`, create/destroy
  `FloatingAppletWindow`, persist `FloatingApplet_VU_IsFloating`,
  save geometry on dock.

**Startup restore:**
- After the existing `m_appletOrder` restore loop, added a
  `QTimer::singleShot(0)` restore for `"VU"` so the S-Meter reopens
  floating if it was floating when the app last closed.

**dockSMeter() — use m_sMeterContent directly (agent review fix)**
The original used a fragile layout-walk to recover m_sMeterContent from
FloatingAppletWindow's internal layout. If any itemAt() returned null,
appletWidget would be null while the code still called win->deleteLater(),
destroying m_sMeterContent and all its children (m_sMeter, m_txSelect,
m_rxSelect, peak-hold widgets) — crash on next signal-level update.

Fix: use the named member directly. Reparent m_sMeterContent into
m_sMeterSection before deleteLater() so Qt's parent-chain destruction
cannot reach it.

**.gitignore — drop Visual Studio artifacts (agent review fix)**
/.vs and /out/install/x64-Debug are Windows VS IDE artifacts; they belong
in ~/.gitignore_global, not the project-wide .gitignore.

Closes aethersdr#993

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chibondking added a commit to chibondking/AetherSDR that referenced this pull request Apr 11, 2026
The S-Meter section can now be detached into a floating window and
re-docked exactly like all other applets — using the same
FloatingAppletWindow infrastructure, AppSettings geometry persistence,
and PR aethersdr#916 / aethersdr#985 bug fixes (drift, WM centering, shutdown race).

## What changed

### src/gui/AppletPanel.h
- Added `QWidget* m_sMeterContent` — the floatable content container
  (SMeterWidget + TX/RX select row + Peak Hold row), analogous to
  the applet widget held at layout index 1 in every other wrapper.
- Added private `floatSMeter()` and `dockSMeter()` methods.

### src/gui/AppletPanel.cpp

**S-Meter construction:**
- `AppletTitleBar("S-Meter", "VU", this)` — passes the panel pointer
  so the right-click context menu ("⬈ Pop out" / "↩ Dock") is active.
- Introduced `m_sMeterContent` QWidget with its own QVBoxLayout as
  layout index 1 of `m_sMeterSection` (after the title bar), so the
  extractor logic in `floatSMeter()` / `dockSMeter()` is identical to
  every other applet.

**ANLG toggle button:**
- Updated to the float-aware pattern used by TUN / AMP / AG:
  raises/hides the floating window when VU is floating, re-floats
  via `QTimer::singleShot(0)` when IsFloating is persisted but no
  window is open yet (toggle-off → toggle-on cycle fix from aethersdr#985).

**`isAppletFloating()`:**
- Added early return for `"VU"`: checks `m_floatingWindows.contains("VU")`
  directly (VU is not in `m_appletOrder`).

**`floatApplet()` / `dockApplet()`:**
- Added `if (id == "VU")` dispatch at the top of each, delegating to
  `floatSMeter()` / `dockSMeter()`.

**`floatSMeter()` / `dockSMeter()`:**
- Mirror `floatApplet()` / `dockApplet()` exactly: reparent content
  out of/back into `m_sMeterSection`, create/destroy
  `FloatingAppletWindow`, persist `FloatingApplet_VU_IsFloating`,
  save geometry on dock.

**Startup restore:**
- After the existing `m_appletOrder` restore loop, added a
  `QTimer::singleShot(0)` restore for `"VU"` so the S-Meter reopens
  floating if it was floating when the app last closed.

**dockSMeter() — use m_sMeterContent directly (agent review fix)**
The original used a fragile layout-walk to recover m_sMeterContent from
FloatingAppletWindow's internal layout. If any itemAt() returned null,
appletWidget would be null while the code still called win->deleteLater(),
destroying m_sMeterContent and all its children (m_sMeter, m_txSelect,
m_rxSelect, peak-hold widgets) — crash on next signal-level update.

Fix: use the named member directly. Reparent m_sMeterContent into
m_sMeterSection before deleteLater() so Qt's parent-chain destruction
cannot reach it.

**.gitignore — drop Visual Studio artifacts (agent review fix)**
/.vs and /out/install/x64-Debug are Windows VS IDE artifacts; they belong
in ~/.gitignore_global, not the project-wide .gitignore.

Closes aethersdr#993

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ten9876 pushed a commit that referenced this pull request Apr 11, 2026
* Updated for vscode and visual studio ignore patterns for local windows development.

* Add pop-out/pop-in support for the S-Meter (VU) applet (#993)

The S-Meter section can now be detached into a floating window and
re-docked exactly like all other applets — using the same
FloatingAppletWindow infrastructure, AppSettings geometry persistence,
and PR #916 / #985 bug fixes (drift, WM centering, shutdown race).

## What changed

### src/gui/AppletPanel.h
- Added `QWidget* m_sMeterContent` — the floatable content container
  (SMeterWidget + TX/RX select row + Peak Hold row), analogous to
  the applet widget held at layout index 1 in every other wrapper.
- Added private `floatSMeter()` and `dockSMeter()` methods.

### src/gui/AppletPanel.cpp

**S-Meter construction:**
- `AppletTitleBar("S-Meter", "VU", this)` — passes the panel pointer
  so the right-click context menu ("⬈ Pop out" / "↩ Dock") is active.
- Introduced `m_sMeterContent` QWidget with its own QVBoxLayout as
  layout index 1 of `m_sMeterSection` (after the title bar), so the
  extractor logic in `floatSMeter()` / `dockSMeter()` is identical to
  every other applet.

**ANLG toggle button:**
- Updated to the float-aware pattern used by TUN / AMP / AG:
  raises/hides the floating window when VU is floating, re-floats
  via `QTimer::singleShot(0)` when IsFloating is persisted but no
  window is open yet (toggle-off → toggle-on cycle fix from #985).

**`isAppletFloating()`:**
- Added early return for `"VU"`: checks `m_floatingWindows.contains("VU")`
  directly (VU is not in `m_appletOrder`).

**`floatApplet()` / `dockApplet()`:**
- Added `if (id == "VU")` dispatch at the top of each, delegating to
  `floatSMeter()` / `dockSMeter()`.

**`floatSMeter()` / `dockSMeter()`:**
- Mirror `floatApplet()` / `dockApplet()` exactly: reparent content
  out of/back into `m_sMeterSection`, create/destroy
  `FloatingAppletWindow`, persist `FloatingApplet_VU_IsFloating`,
  save geometry on dock.

**Startup restore:**
- After the existing `m_appletOrder` restore loop, added a
  `QTimer::singleShot(0)` restore for `"VU"` so the S-Meter reopens
  floating if it was floating when the app last closed.

Closes #993

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add pop-out/pop-in support for the S-Meter (VU) applet (#993)

The S-Meter section can now be detached into a floating window and
re-docked exactly like all other applets — using the same
FloatingAppletWindow infrastructure, AppSettings geometry persistence,
and PR #916 / #985 bug fixes (drift, WM centering, shutdown race).

## What changed

### src/gui/AppletPanel.h
- Added `QWidget* m_sMeterContent` — the floatable content container
  (SMeterWidget + TX/RX select row + Peak Hold row), analogous to
  the applet widget held at layout index 1 in every other wrapper.
- Added private `floatSMeter()` and `dockSMeter()` methods.

### src/gui/AppletPanel.cpp

**S-Meter construction:**
- `AppletTitleBar("S-Meter", "VU", this)` — passes the panel pointer
  so the right-click context menu ("⬈ Pop out" / "↩ Dock") is active.
- Introduced `m_sMeterContent` QWidget with its own QVBoxLayout as
  layout index 1 of `m_sMeterSection` (after the title bar), so the
  extractor logic in `floatSMeter()` / `dockSMeter()` is identical to
  every other applet.

**ANLG toggle button:**
- Updated to the float-aware pattern used by TUN / AMP / AG:
  raises/hides the floating window when VU is floating, re-floats
  via `QTimer::singleShot(0)` when IsFloating is persisted but no
  window is open yet (toggle-off → toggle-on cycle fix from #985).

**`isAppletFloating()`:**
- Added early return for `"VU"`: checks `m_floatingWindows.contains("VU")`
  directly (VU is not in `m_appletOrder`).

**`floatApplet()` / `dockApplet()`:**
- Added `if (id == "VU")` dispatch at the top of each, delegating to
  `floatSMeter()` / `dockSMeter()`.

**`floatSMeter()` / `dockSMeter()`:**
- Mirror `floatApplet()` / `dockApplet()` exactly: reparent content
  out of/back into `m_sMeterSection`, create/destroy
  `FloatingAppletWindow`, persist `FloatingApplet_VU_IsFloating`,
  save geometry on dock.

**Startup restore:**
- After the existing `m_appletOrder` restore loop, added a
  `QTimer::singleShot(0)` restore for `"VU"` so the S-Meter reopens
  floating if it was floating when the app last closed.

**dockSMeter() — use m_sMeterContent directly (agent review fix)**
The original used a fragile layout-walk to recover m_sMeterContent from
FloatingAppletWindow's internal layout. If any itemAt() returned null,
appletWidget would be null while the code still called win->deleteLater(),
destroying m_sMeterContent and all its children (m_sMeter, m_txSelect,
m_rxSelect, peak-hold widgets) — crash on next signal-level update.

Fix: use the named member directly. Reparent m_sMeterContent into
m_sMeterSection before deleteLater() so Qt's parent-chain destruction
cannot reach it.

**.gitignore — drop Visual Studio artifacts (agent review fix)**
/.vs and /out/install/x64-Debug are Windows VS IDE artifacts; they belong
in ~/.gitignore_global, not the project-wide .gitignore.

Closes #993

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add pop-out/pop-in support for the S-Meter (VU) applet (#993)

The S-Meter section can now be detached into a floating window and
re-docked exactly like all other applets — using the same
FloatingAppletWindow infrastructure, AppSettings geometry persistence,
and PR #916 / #985 bug fixes (drift, WM centering, shutdown race).

## What changed

### src/gui/AppletPanel.h
- Added `QWidget* m_sMeterContent` — the floatable content container
  (SMeterWidget + TX/RX select row + Peak Hold row), analogous to
  the applet widget held at layout index 1 in every other wrapper.
- Added private `floatSMeter()` and `dockSMeter()` methods.

### src/gui/AppletPanel.cpp

**S-Meter construction:**
- `AppletTitleBar("S-Meter", "VU", this)` — passes the panel pointer
  so the right-click context menu ("⬈ Pop out" / "↩ Dock") is active.
- Introduced `m_sMeterContent` QWidget with its own QVBoxLayout as
  layout index 1 of `m_sMeterSection` (after the title bar), so the
  extractor logic in `floatSMeter()` / `dockSMeter()` is identical to
  every other applet.

**ANLG toggle button:**
- Updated to the float-aware pattern used by TUN / AMP / AG:
  raises/hides the floating window when VU is floating, re-floats
  via `QTimer::singleShot(0)` when IsFloating is persisted but no
  window is open yet (toggle-off → toggle-on cycle fix from #985).

**`isAppletFloating()`:**
- Added early return for `"VU"`: checks `m_floatingWindows.contains("VU")`
  directly (VU is not in `m_appletOrder`).

**`floatApplet()` / `dockApplet()`:**
- Added `if (id == "VU")` dispatch at the top of each, delegating to
  `floatSMeter()` / `dockSMeter()`.

**`floatSMeter()` / `dockSMeter()`:**
- Mirror `floatApplet()` / `dockApplet()` exactly: reparent content
  out of/back into `m_sMeterSection`, create/destroy
  `FloatingAppletWindow`, persist `FloatingApplet_VU_IsFloating`,
  save geometry on dock.

**Startup restore:**
- After the existing `m_appletOrder` restore loop, added a
  `QTimer::singleShot(0)` restore for `"VU"` so the S-Meter reopens
  floating if it was floating when the app last closed.

**dockSMeter() — use m_sMeterContent directly (agent review fix)**
The original used a fragile layout-walk to recover m_sMeterContent from
FloatingAppletWindow's internal layout. If any itemAt() returned null,
appletWidget would be null while the code still called win->deleteLater(),
destroying m_sMeterContent and all its children (m_sMeter, m_txSelect,
m_rxSelect, peak-hold widgets) — crash on next signal-level update.

Fix: use the named member directly. Reparent m_sMeterContent into
m_sMeterSection before deleteLater() so Qt's parent-chain destruction
cannot reach it.

**.gitignore — drop Visual Studio artifacts (agent review fix)**
/.vs and /out/install/x64-Debug are Windows VS IDE artifacts; they belong
in ~/.gitignore_global, not the project-wide .gitignore.

Closes #993

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
NF0T pushed a commit that referenced this pull request Jun 5, 2026
…ettings key (#3358) (#3409)

## Problem

\`ButtonBar/Layout\`, \`ButtonBar/DrawerOpen\`, and
\`ButtonBar/Favorites\` all contain a \`/\`. \`AppSettings::save()\`
silently drops any key not matching \`^[A-Za-z_][A-Za-z0-9_]*$\` — the
validator was tightened in #985 to prevent invalid XML element names.
The customisable button bar shipped in #3150 *after* that tightening, so
its layout, drawer state, and favourites have **never round-tripped to
disk for any user**.

## Fix

Rename the three keys to \`ButtonBarLayout\`, \`ButtonBarDrawerOpen\`,
\`ButtonBarFavorites\`. No migration needed — nothing was ever written
to the old slash-containing keys, so there is no on-disk state to
convert.

## Testing

Verified locally on macOS (Apple Silicon, dev build off \`main\`):
- Before fix: \`ButtonBar/Layout\` and \`ButtonBar/DrawerOpen\` absent
from \`AetherSDR.settings\` after every save (silently discarded)
- After fix: \`ButtonBarLayout\` and \`ButtonBarDrawerOpen\` present in
\`AetherSDR.settings\` with correct JSON values
- Button bar order and drawer state survive app restart

Fixes #3358
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…ettings key (aethersdr#3358) (aethersdr#3409)

## Problem

\`ButtonBar/Layout\`, \`ButtonBar/DrawerOpen\`, and
\`ButtonBar/Favorites\` all contain a \`/\`. \`AppSettings::save()\`
silently drops any key not matching \`^[A-Za-z_][A-Za-z0-9_]*$\` — the
validator was tightened in aethersdr#985 to prevent invalid XML element names.
The customisable button bar shipped in aethersdr#3150 *after* that tightening, so
its layout, drawer state, and favourites have **never round-tripped to
disk for any user**.

## Fix

Rename the three keys to \`ButtonBarLayout\`, \`ButtonBarDrawerOpen\`,
\`ButtonBarFavorites\`. No migration needed — nothing was ever written
to the old slash-containing keys, so there is no on-disk state to
convert.

## Testing

Verified locally on macOS (Apple Silicon, dev build off \`main\`):
- Before fix: \`ButtonBar/Layout\` and \`ButtonBar/DrawerOpen\` absent
from \`AetherSDR.settings\` after every save (silently discarded)
- After fix: \`ButtonBarLayout\` and \`ButtonBarDrawerOpen\` present in
\`AetherSDR.settings\` with correct JSON values
- Button bar order and drawer state survive app restart

Fixes aethersdr#3358
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.

Poppable applets - can't pop top applet and can't restore applets popped out if disabled and reenabled

2 participants