Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 docs/user/docs/windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ There is no built-in DAX audio driver on Windows. The DAX applet says so: "No bu
- **Menus** are behind the **☰** button at the left of the title bar. Alt+letter mnemonics (Alt+F, Alt+S, …) still open each menu. F11 toggles full screen. See [Menu Reference](./menu-reference.md).
- **Radio Setup** is **Settings → Radio Setup...**.
- **Window frame:** a Qt-drawn frame with the system shadow and resize borders, with AetherSDR's own minimize, maximize and close buttons.
- **Snap:** drag the title bar to a screen edge to snap the window to that half or corner, the same as any other window; Windows then offers Snap Assist to fill the rest of the screen. **Win+Left**, **Win+Right** and **Win+Up** snap it from the keyboard. Dragging works on the title bar's empty background — over the **☰** button, the radio tabs, the audio buttons and volume sliders, and the caption buttons a drag does what that control does instead. The logo and the labels that have their own hover text still move the window, but do not start a snap; drag from the empty background to snap. Hovering over the maximize button does not show the Snap Layouts flyout ([#6224](https://github.com/aethersdr/AetherSDR/issues/6224)).
- **GPU choice on hybrid laptops:** the discrete GPU is used by default. The integrated GPU is listed in **Display → SYSTEM → GPU:** but grayed out ("disabled (#1921)"), because it crashes when a panadapter is popped out or docked. See [GPU Rendering](./gpu-rendering.md).
- **Noise reduction:** **BNR** (NVIDIA Maxine, on an NVIDIA RTX/GeForce GPU) is available on Windows; its runtime comes as a single self-contained download. **MNR** is macOS only and is grayed out. See [BNR GPU Noise Removal](./bnr-gpu-noise-removal.md) and [DSP Noise Mitigation](./dsp-noise-mitigation.md).
- **Copy Assist** can use a **Vulkan** GPU (NVIDIA, AMD or Intel). See [Copy Assist](./copy-assist.md).
Expand Down
22 changes: 20 additions & 2 deletions src/gui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3876,6 +3876,7 @@ static LRESULT frameHitCode(WindowChrome::FrameHit hit)
case WindowChrome::FrameHit::TopRight: return HTTOPRIGHT;
case WindowChrome::FrameHit::BottomLeft: return HTBOTTOMLEFT;
case WindowChrome::FrameHit::BottomRight: return HTBOTTOMRIGHT;
case WindowChrome::FrameHit::Caption: return HTCAPTION;
case WindowChrome::FrameHit::Client: break;
}
return HTCLIENT;
Expand All @@ -3887,7 +3888,8 @@ bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr
// Under the expanded client area Qt 6.12 answers WM_NCHITTEST by turning
// the live mouse-button state into synthetic presses, which doubles real
// clicks and closes menus as they open (#6272). Answer it here instead:
// the resize band at the edges, client area everywhere else.
// the resize band at the edges, the title bar's own background as the
// caption so Windows offers Snap (#6825), client area everywhere else.
if (msg && result && msg->message == WM_NCHITTEST
&& windowFlags().testFlag(Qt::ExpandedClientAreaHint)) {
RECT window{};
Expand All @@ -3903,7 +3905,23 @@ bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr
const QPoint point(GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam));
const QRect rect(window.left, window.top,
window.right - window.left, window.bottom - window.top);
*result = frameHitCode(WindowChrome::expandedFrameHit(point, rect, borderX, borderY));
// The client area's top-left in screen pixels. GetWindowRect includes
// the invisible DWM resize border and Qt's geometry does not, so the
// caption gate anchors here instead — see logicalScreenPoint().
POINT clientOrigin{0, 0};
ClientToScreen(msg->hwnd, &clientOrigin);
// The bar's height is in logical pixels and WM_NCHITTEST is physical.
// Classify the message's point even when the shell queries it away
// from the cursor. Interactive controls stay client, as do tooltipped
// handles because caption mouse moves bypass Qt's tooltip timer.
const bool barShown = m_titleBar && m_titleBar->isVisible();
const int captionHeight = WindowChrome::captionHeightForHitTest(
point, window.top, QPoint(clientOrigin.x, clientOrigin.y),
mapToGlobal(QPoint(0, 0)), barShown ? m_titleBar->height() : 0,
devicePixelRatioF(),
[this](const QPoint& logical) { return m_titleBar->isCaptionMoveAreaAt(logical); });
*result = frameHitCode(
WindowChrome::expandedFrameHit(point, rect, borderX, borderY, captionHeight));
return true;
}
// The whole window is client area, inset only while maximized so nothing
Expand Down
20 changes: 20 additions & 0 deletions src/gui/TitleBar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#include "core/NetworkDiagnostics.h"
#include "core/AppSettings.h"

#include <QAbstractButton>
#include <QAbstractSlider>
#include <QFrame>
#include <QHBoxLayout>
#include <QIcon>
Expand Down Expand Up @@ -841,6 +843,24 @@ bool TitleBar::isSystemMoveAreaAt(const QPoint& globalPos) const
return false;
}

bool TitleBar::isCaptionMoveAreaAt(const QPoint& globalPos) const
{
if (!isSystemMoveAreaAt(globalPos)) {
return false;
}
// Interactive children keep client input inside a draggable container.
// Tooltipped handles also stay client because caption mouse moves bypass
// Qt's tooltip timer.
for (QWidget* widget = childAt(mapFromGlobal(globalPos)); widget && widget != this;
widget = widget->parentWidget()) {
if (qobject_cast<QAbstractButton*>(widget) || qobject_cast<QAbstractSlider*>(widget)
|| !widget->toolTip().isEmpty()) {
return false;
}
}
return true;
}

bool TitleBar::startWindowMove(QMouseEvent* ev)
{
if (!ev || ev->button() != Qt::LeftButton)
Expand Down
4 changes: 4 additions & 0 deletions src/gui/TitleBar.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ class TitleBar : public QWidget {

bool isSystemMoveAreaAt(const QPoint& globalPos) const;

// Windows caption subset: interactive controls and tooltipped handles
// stay client so their input and Qt tooltip delivery remain available.
bool isCaptionMoveAreaAt(const QPoint& globalPos) const;

// ── Radio tabs ──────────────────────────────────────────────────────────
// The strip of per-radio tabs plus the "+" discovered-radios popover.
// MainWindow feeds these from discovery + the active connection.
Expand Down
96 changes: 81 additions & 15 deletions src/gui/WindowChrome.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,27 +90,93 @@ inline bool claimsWholeWindowAsClient(Qt::WindowFlags flags, const QOperatingSys
// live mouse-button state into synthetic presses, doubling real clicks (#6272).
// Physical pixels; `borderX` / `borderY` are the resize bands on the left/right
// and top/bottom edges (0 while maximized).
enum class FrameHit { Client, Left, Right, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight };
//
// `Caption` is the title bar's own draggable background: the answer Windows
// needs before it will offer drag-to-edge Snap (#6825). Only the caller knows
// which points are the bar's empty background rather than its tabs, menus and
// buttons, so this reports Caption for the whole `captionHeight` strip and
// MainWindow::nativeEvent narrows it with TitleBar::isCaptionMoveAreaAt() —
// answering HTCAPTION over an interactive widget is what made menus close as
// they opened (#6272). A `captionHeight` of 0 disables it, which is how every
// non-main window keeps the old behaviour.
enum class FrameHit { Client, Caption, Left, Right, Top, Bottom, TopLeft, TopRight, BottomLeft, BottomRight };

inline FrameHit expandedFrameHit(const QPoint& point, const QRect& window, int borderX, int borderY)
// WM_NCHITTEST carries the screen point being asked about in physical pixels,
// while Qt's widget geometry — and so TitleBar::isSystemMoveAreaAt() — is in
// logical ones.
//
// Anchor on the client origin: GetWindowRect includes Windows 11's
// invisible DWM resize border, while Qt's widget geometry describes the
// client area. Pairing the two client origins preserves control alignment.
//
// `physical` is the WM_NCHITTEST point; `clientOriginPhysical` is the client
// area's top-left in screen pixels (GetClientRect + ClientToScreen);
// `widgetOriginLogical` is that same corner as Qt places it
// (QWidget::mapToGlobal({0, 0})). `dpr` is devicePixelRatioF().
inline QPoint logicalScreenPoint(const QPoint& physical, const QPoint& clientOriginPhysical,
const QPoint& widgetOriginLogical, qreal dpr)
{
if (borderX <= 0 || borderY <= 0 || !window.contains(point)) {
return FrameHit::Client;
if (!(dpr > 0)) {
return physical;
}
const bool left = point.x() < window.left() + borderX;
const bool right = point.x() > window.right() - borderX;
const bool top = point.y() < window.top() + borderY;
const bool bottom = point.y() > window.bottom() - borderY;
if (left) {
return top ? FrameHit::TopLeft : bottom ? FrameHit::BottomLeft : FrameHit::Left;
return QPoint(widgetOriginLogical.x() + qRound((physical.x() - clientOriginPhysical.x()) / dpr),
widgetOriginLogical.y() + qRound((physical.y() - clientOriginPhysical.y()) / dpr));
}

// Ask the move-area gate about the message's point in logical coordinates.
// Measure from the client top, including the maximized frame inset. A missing
// bar or rejected point gives no caption.
//
// `windowTop` is GetWindowRect's top; `clientOriginPhysical` the client's
// top-left in screen pixels; `widgetOriginLogical` the same corner per Qt;
// `barHeightLogical` the title bar's height; `dpr` devicePixelRatioF().
template <typename MoveAreaTest>
inline int captionHeightForHitTest(const QPoint& physical, int windowTop,
const QPoint& clientOriginPhysical,
const QPoint& widgetOriginLogical,
int barHeightLogical, qreal dpr,
MoveAreaTest&& isMoveArea)
{
if (barHeightLogical <= 0) {
return 0;
}
if (!isMoveArea(logicalScreenPoint(physical, clientOriginPhysical,
widgetOriginLogical, dpr))) {
return 0;
}
return clientOriginPhysical.y() - windowTop + int(barHeightLogical * dpr);
}

inline FrameHit expandedFrameHit(const QPoint& point, const QRect& window, int borderX, int borderY,
int captionHeight = 0)
{
if (!window.contains(point)) {
return FrameHit::Client;
}
if (right) {
return top ? FrameHit::TopRight : bottom ? FrameHit::BottomRight : FrameHit::Right;
// Maximized and fullscreen have no resize band, but the caption still has
// to answer or the window cannot be dragged back off the top of the screen.
if (borderX > 0 && borderY > 0) {
const bool left = point.x() < window.left() + borderX;
const bool right = point.x() > window.right() - borderX;
const bool top = point.y() < window.top() + borderY;
const bool bottom = point.y() > window.bottom() - borderY;
if (left) {
return top ? FrameHit::TopLeft : bottom ? FrameHit::BottomLeft : FrameHit::Left;
}
if (right) {
return top ? FrameHit::TopRight : bottom ? FrameHit::BottomRight : FrameHit::Right;
}
if (top) {
return FrameHit::Top;
}
if (bottom) {
return FrameHit::Bottom;
}
}
if (top) {
return FrameHit::Top;
if (captionHeight > 0 && point.y() < window.top() + captionHeight) {
return FrameHit::Caption;
}
return bottom ? FrameHit::Bottom : FrameHit::Client;
return FrameHit::Client;
}

// Windows: the native window rect that gives `client`, keeping the frame the
Expand Down
Loading
Loading