Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,7 @@ set(GUI_SOURCES
src/gui/MainWindow.cpp
src/gui/MainWindowHelpers.cpp
src/gui/WindowGeometryRestore.cpp
src/gui/WindowShowState.cpp
src/gui/MainWindow_Controllers.cpp
src/gui/MainWindow_DspApplets.cpp
src/gui/MainWindow_Menus.cpp
Expand Down
15 changes: 9 additions & 6 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
#include "models/XvtrPolicy.h"
#include "core/BandStackSettings.h"
#include "gui/BandStackPanel.h"
#include "gui/WindowShowState.h"
#include "models/TunerModel.h"
#include "models/TransmitModel.h"
#include "models/EqualizerModel.h"
Expand Down Expand Up @@ -9378,13 +9379,15 @@ void MainWindow::toggleAetherialStrip()
m_aetherialStrip->setMicInputReady(ready);
m_aetherialStrip->setTxActive(ready && tx.isTransmitting());
}
if (m_aetherialStrip->isVisible()) {
// windowIsShowing() rather than isVisible(): a minimized strip still
// reports isVisible(), so the bare check sent it down the hide() branch and
// the next press called show(), which restores it straight back to
// minimized. The strip could then only be recovered from the taskbar/Dock,
// never from its own button.
if (windowIsShowing(m_aetherialStrip))
m_aetherialStrip->hide();
} else {
m_aetherialStrip->show();
m_aetherialStrip->raise();
m_aetherialStrip->activateWindow();
}
else
showAndRaiseWindow(m_aetherialStrip);
}

void MainWindow::toggleMinimalModeFromAction()
Expand Down
25 changes: 25 additions & 0 deletions src/gui/WindowShowState.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#include "gui/WindowShowState.h"

#include <QWidget>

namespace AetherSDR {

bool windowIsShowing(const QWidget* w)
{
return w && w->isVisible() && !w->isMinimized();
}

void showAndRaiseWindow(QWidget* w)
{
if (!w)
return;
Comment on lines +14 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking convention nit: AGENTS.md requires braces on all control flow. Please brace this guard (and the toggle branches in the new test).

Suggested change
if (!w)
return;
if (!w) {
return;
}

// showNormal() only for a minimized window — see the header note on #3918.
if (w->isMinimized())
w->showNormal();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (non-blocking) — showNormal() drops a prior maximize. It clears both the Minimized and Maximized bits, so minimize a maximized strip and reopen → it returns un-maximized. Clear only the minimized bit to preserve the pre-minimize size:

Suggested change
w->showNormal();
if (w->isMinimized())
w->setWindowState(w->windowState() & ~Qt::WindowMinimized);
else
w->show();

Add a test row (maximize → minimize → reopen → still maximized); case 7 doesn't cover the minimized-from-maximized path.

else
w->show();
w->raise();
w->activateWindow();
}

} // namespace AetherSDR
24 changes: 24 additions & 0 deletions src/gui/WindowShowState.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

class QWidget;

namespace AetherSDR {

// Window show-state helpers for press-to-open / press-again-to-close buttons.
//
// QWidget::isVisible() stays TRUE for a minimized window, and QWidget::show()
// on a minimized window restores it to its saved state — which is minimized.
// A toggle written as `if (w->isVisible()) w->hide(); else w->show();` is
// therefore unrecoverable once the window is minimized: the first press hides
// it, and the second press "shows" it straight back into the taskbar/Dock.
// Both behaviours are pinned by window_show_state_test.

// True when w is actually on screen for the user — visible AND not minimized.
[[nodiscard]] bool windowIsShowing(const QWidget* w);

// Bring w to the front, un-minimizing it first if needed. showNormal() is
// guarded on isMinimized() because calling it unconditionally would clear a
// Maximized or FullScreen window (#3918).
void showAndRaiseWindow(QWidget* w);

} // namespace AetherSDR
14 changes: 14 additions & 0 deletions tests/tests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,20 @@ add_test(NAME window_geometry_restore_test COMMAND window_geometry_restore_test)
set_tests_properties(window_geometry_restore_test PROPERTIES
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")

# Aetherial Audio Channel Strip toggle: a minimized window still reports
# isVisible() and show() does not un-minimize it, so the old toggle could not
# reopen the strip once minimized. Drives a real QWidget offscreen so both Qt
# behaviours are pinned rather than assumed.
add_executable(window_show_state_test
tests/window_show_state_test.cpp
src/gui/WindowShowState.cpp
)
target_include_directories(window_show_state_test PRIVATE src)
target_link_libraries(window_show_state_test PRIVATE Qt6::Widgets)
add_test(NAME window_show_state_test COMMAND window_show_state_test)
set_tests_properties(window_show_state_test PROPERTIES
ENVIRONMENT "QT_QPA_PLATFORM=offscreen")

# Workspace canvas (RFC #4887) phase 1 — normalized geometry. Pure logic, no
# widgets: the edge-rounding rule that keeps tiled items seam-free, and the
# resolution independence the whole RFC rests on, are pinned on every platform.
Expand Down
116 changes: 116 additions & 0 deletions tests/window_show_state_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Regression test for the Aetherial Audio Channel Strip toggle, which could
// not reopen the window once it had been minimized.
//
// Two Qt behaviours combine into the bug, and both are pinned here against a
// REAL QWidget rather than asserted from memory:
//
// 1. QWidget::isVisible() stays TRUE while a window is minimized. A toggle
// written `if (w->isVisible()) w->hide(); else w->show();` therefore
// treats a minimized window as "showing" and hides it.
// 2. QWidget::show() on a minimized window restores its SAVED state, which
// is still minimized — so the follow-up press does not recover it either.
//
// windowIsShowing() fixes (1) and showAndRaiseWindow() fixes (2), while the
// isMinimized() guard inside it keeps a maximized window maximized (#3918).
//
// Runs on the offscreen platform, where both behaviours reproduce.

#include "gui/WindowShowState.h"

#include <QApplication>
#include <QWidget>

#include <cstdio>

using AetherSDR::showAndRaiseWindow;
using AetherSDR::windowIsShowing;

namespace {

int g_failures = 0;

void report(const char* name, bool ok)
{
std::printf("%s %s\n", ok ? "[ OK ]" : "[FAIL]", name);
if (!ok) {
++g_failures;
}
}

// The toggle exactly as MainWindow::toggleAetherialStrip() runs it.
void toggle(QWidget* w)
{
if (windowIsShowing(w))
w->hide();
else
showAndRaiseWindow(w);
}

} // namespace

int main(int argc, char** argv)
{
QApplication app(argc, argv);

// --- Case 1: null is never "showing", and raising it must not crash.
report("windowIsShowing(nullptr) is false", !windowIsShowing(nullptr));
showAndRaiseWindow(nullptr);
report("showAndRaiseWindow(nullptr) is a no-op", true);

QWidget w;
w.resize(320, 240);

// --- Case 2: a hidden window is not showing; the toggle opens it.
report("hidden window is not showing", !windowIsShowing(&w));
toggle(&w);
app.processEvents();
report("toggle opens a hidden window", windowIsShowing(&w));

// --- Case 3: the toggle closes an ordinary open window.
toggle(&w);
app.processEvents();
report("toggle hides a showing window", !w.isVisible());

// --- Case 4: THE BUG. Qt reports a minimized window as visible, so the
// old bare isVisible() check would take the hide() branch here.
w.show();
app.processEvents();
w.showMinimized();
app.processEvents();
report("Qt: minimized window still reports isVisible()", w.isVisible());
report("Qt: minimized window reports isMinimized()", w.isMinimized());
report("windowIsShowing() treats minimized as not showing",
!windowIsShowing(&w));

// --- Case 5: why the second press never recovered it either — show() on a
// minimized window leaves it minimized.
w.show();
app.processEvents();
report("Qt: show() does not un-minimize", w.isMinimized());

// --- Case 6: the toggle restores a minimized window in ONE press.
toggle(&w);
app.processEvents();
report("toggle restores a minimized window", windowIsShowing(&w));
report("restored window is no longer minimized", !w.isMinimized());

// --- Case 7: the isMinimized() guard — raising a maximized window must
// not drop it out of maximized state (#3918).
w.showMaximized();
app.processEvents();
if (w.isMaximized()) {
showAndRaiseWindow(&w);
app.processEvents();
report("showAndRaiseWindow keeps a maximized window maximized",
w.isMaximized());
} else {
// Some platforms decline to maximize; the guard is still pinned by
// case 6, so skip rather than fail on a platform quirk.
std::printf("[SKIP] maximized state unavailable on this platform\n");
}

if (g_failures == 0) {
std::printf("All window show-state tests passed.\n");
}
return g_failures == 0 ? 0 : 1;
}