Skip to content
Draft
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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ set(SOURCES
src/ui/notificationwidget.cpp
src/ui/k4styles.cpp
src/ui/k4popupbase.cpp
src/ui/adjustoverlay.cpp
src/ui/kpa1500panel.cpp
src/ui/kpa1500window.cpp
src/ui/sidecontroloverlay.cpp
Expand Down Expand Up @@ -318,6 +319,7 @@ set(HEADERS
src/ui/notificationwidget.h
src/ui/k4styles.h
src/ui/k4popupbase.h
src/ui/adjustoverlay.h
src/ui/inwindowpopup.h
src/ui/inwindowdialog.h
src/ui/kpa1500panel.h
Expand Down
465 changes: 408 additions & 57 deletions src/mainwindow.cpp

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,8 @@ private slots:
// Spectrum/Waterfall displays (QRhiWidget - Metal/DirectX/Vulkan)
PanadapterRhiWidget *m_panadapterA; // VFO A (Main RX)
PanadapterRhiWidget *m_panadapterB; // VFO B (Sub RX) - for future use
QWidget *m_panAFrame = nullptr; // thin bordered wrapper around panadapter A
QWidget *m_panBFrame = nullptr; // thin bordered wrapper around panadapter B
QWidget *m_spectrumContainer;

// Span control buttons (overlay on panadapter A)
Expand Down
186 changes: 186 additions & 0 deletions src/ui/adjustoverlay.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#include "adjustoverlay.h"
#include "k4styles.h"

#include <QGuiApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QPainter>
#include <QPushButton>
#include <QScreen>
#include <QSlider>
#include <QStyle>
#include <QTimer>
#include <QVBoxLayout>

AdjustOverlay::AdjustOverlay(QWidget *parent) : QWidget(parent, Qt::Popup) {
setAttribute(Qt::WA_TranslucentBackground);
setFixedWidth(240);

auto *outer = new QVBoxLayout(this);
// Leave room for the indicator bar on the left.
outer->setContentsMargins(IndicatorBarWidth + 12, 10, 12, 12);
outer->setSpacing(8);

m_titleLabel = new QLabel(this);
m_titleLabel->setStyleSheet(
QString("color: %1; font-size: 12px; font-weight: bold;").arg(K4Styles::Colors::TextWhite));
outer->addWidget(m_titleLabel);

m_valueLabel = new QLabel(this);
m_valueLabel->setAlignment(Qt::AlignCenter);
m_valueLabel->setStyleSheet(
QString("color: %1; font-size: 22px; font-weight: bold;").arg(K4Styles::Colors::TextWhite));
outer->addWidget(m_valueLabel);

auto *row = new QHBoxLayout();
row->setSpacing(10);

const QString stepBtnStyle =
QString("QPushButton { color: %1; background: %2; border: 1px solid %3; border-radius: 6px; "
"font-size: 22px; font-weight: bold; } QPushButton:pressed { background: %3; }")
.arg(K4Styles::Colors::TextWhite, K4Styles::Colors::DarkBackground, K4Styles::Colors::BorderNormal);

m_minusBtn = new QPushButton(QStringLiteral("−"), this); // minus sign
m_minusBtn->setFixedSize(44, 44);
m_minusBtn->setStyleSheet(stepBtnStyle);
row->addWidget(m_minusBtn);

m_slider = new QSlider(Qt::Horizontal, this);
m_slider->setMinimumHeight(40);
// A raw QSlider treats a touch that lands on the groove as a page-step and
// does not track the drag. Map x->value directly instead (the popup is not
// inside a scroll area, so there is no scroll-vs-adjust ambiguity).
m_slider->installEventFilter(this);
row->addWidget(m_slider, 1);

m_plusBtn = new QPushButton(QStringLiteral("+"), this);
m_plusBtn->setFixedSize(44, 44);
m_plusBtn->setStyleSheet(stepBtnStyle);
row->addWidget(m_plusBtn);

outer->addLayout(row);

connect(m_minusBtn, &QPushButton::clicked, this, [this]() {
m_slider->setValue(m_slider->value() - m_slider->singleStep());
pokeActivity();
});
connect(m_plusBtn, &QPushButton::clicked, this, [this]() {
m_slider->setValue(m_slider->value() + m_slider->singleStep());
pokeActivity();
});
connect(m_slider, &QSlider::valueChanged, this, [this](int value) {
m_valueLabel->setText(formatValue(value));
pokeActivity();
});

m_inactivityTimer = new QTimer(this);
m_inactivityTimer->setSingleShot(true);
m_inactivityTimer->setInterval(InactivityMs);
connect(m_inactivityTimer, &QTimer::timeout, this, &QWidget::hide);
}

void AdjustOverlay::configure(const QString &title, DualControlButton::Context context) {
m_context = context;
m_titleLabel->setText(title);
m_slider->setStyleSheet(
K4Styles::sliderHorizontal(K4Styles::Colors::DarkBackground, barColor().name()));
update();
}

void AdjustOverlay::setValueText(const QString &text) {
m_valueLabel->setText(text);
}

void AdjustOverlay::setValueFormatter(std::function<QString(int)> formatter) {
m_formatter = std::move(formatter);
if (m_slider)
m_valueLabel->setText(formatValue(m_slider->value()));
}

QString AdjustOverlay::formatValue(int value) const {
return m_formatter ? m_formatter(value) : QString::number(value);
}

void AdjustOverlay::pokeActivity() {
if (m_inactivityTimer)
m_inactivityTimer->start();
}

void AdjustOverlay::showOver(QWidget *anchor) {
adjustSize();
QPoint pos;
if (anchor) {
// Sit just to the right of the tile, vertically centred on it.
const QPoint tl = anchor->mapToGlobal(QPoint(anchor->width(), 0));
pos = QPoint(tl.x() + 8, tl.y() + anchor->height() / 2 - height() / 2);
} else {
pos = QCursor::pos();
}
// Keep on screen.
if (QScreen *screen = QGuiApplication::screenAt(pos) ? QGuiApplication::screenAt(pos)
: QGuiApplication::primaryScreen()) {
const QRect avail = screen->availableGeometry();
int x = qBound(avail.left() + 4, pos.x(), avail.right() - width() - 4);
int y = qBound(avail.top() + 4, pos.y(), avail.bottom() - height() - 4);
pos = QPoint(x, y);
}
move(pos);
show();
raise();
pokeActivity();
}

QColor AdjustOverlay::barColor() const {
switch (m_context) {
case DualControlButton::MainRx:
return QColor(K4Styles::Colors::VfoACyan);
case DualControlButton::SubRx:
return QColor(K4Styles::Colors::VfoBGreen);
case DualControlButton::Global:
default:
return QColor(K4Styles::Colors::AccentAmber);
}
}

bool AdjustOverlay::eventFilter(QObject *watched, QEvent *event) {
if (watched == m_slider) {
if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove) {
auto *me = static_cast<QMouseEvent *>(event);
if (me->buttons() & Qt::LeftButton || event->type() == QEvent::MouseButtonPress) {
setSliderFromX(me->pos().x());
pokeActivity();
return true; // consume: we position absolutely, not by page-step
}
}
}
return QWidget::eventFilter(watched, event);
}

void AdjustOverlay::setSliderFromX(int xPosition) {
if (!m_slider)
return;
const int handleWidth = qMax(12, m_slider->height() / 2);
const int span = qMax(1, m_slider->width() - handleWidth);
const int position = qBound(0, xPosition - handleWidth / 2, span);
m_slider->setValue(QStyle::sliderValueFromPosition(m_slider->minimum(), m_slider->maximum(), position, span,
m_slider->invertedAppearance()));
}

void AdjustOverlay::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const QRect r = rect();

painter.setPen(Qt::NoPen);
painter.setBrush(QColor(K4Styles::Colors::DarkBackground));
painter.drawRoundedRect(r, CornerRadius, CornerRadius);

QRect barRect(0, 0, IndicatorBarWidth, r.height());
painter.setBrush(barColor());
painter.drawRoundedRect(barRect, CornerRadius / 2, CornerRadius / 2);

painter.setPen(QPen(QColor(K4Styles::Colors::BorderNormal), 1));
painter.setBrush(Qt::NoBrush);
painter.drawRoundedRect(r.adjusted(0, 0, -1, -1), CornerRadius, CornerRadius);
}
74 changes: 74 additions & 0 deletions src/ui/adjustoverlay.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#ifndef ADJUSTOVERLAY_H
#define ADJUSTOVERLAY_H

#include <QWidget>
#include <functional>
#include "dualcontrolbutton.h"

class QSlider;
class QLabel;
class QPushButton;
class QTimer;

/**
* @brief Touch adjustment popup for a single left-column control tile.
*
* Mirrors the macOS SideControlOverlay look (dark rounded panel with a
* context-coloured indicator bar) but is driven by a large slider plus fine
* -/+ buttons instead of the mouse wheel, so a DualControlButton value can be
* set by touch on iPad/iPhone. Opened by a long-press on the tile (the touch
* equivalent of the macOS right-click/wheel interaction).
*
* A Qt::Popup window: tapping anywhere outside dismisses it. It also closes
* itself after a short period of inactivity.
*/
class AdjustOverlay : public QWidget {
Q_OBJECT

public:
explicit AdjustOverlay(QWidget *parent = nullptr);

/// The slider the owner configures (range/value) and connects to.
QSlider *slider() const { return m_slider; }

/// Set the title text and indicator-bar colour for this control.
void configure(const QString &title, DualControlButton::Context context);

/// Update the large value readout shown above the slider.
void setValueText(const QString &text);

/// Set how the slider value is rendered in the readout (e.g. kHz). Pass an
/// empty function to fall back to the raw integer.
void setValueFormatter(std::function<QString(int)> formatter);

/// Position over @p anchor (global coords) and show. Resets inactivity.
void showOver(QWidget *anchor);

/// Restart the inactivity auto-close timer (call on any interaction).
void pokeActivity();

protected:
void paintEvent(QPaintEvent *event) override;
bool eventFilter(QObject *watched, QEvent *event) override;

private:
QColor barColor() const;
void setSliderFromX(int xPosition);

DualControlButton::Context m_context = DualControlButton::Global;
QString formatValue(int value) const;

std::function<QString(int)> m_formatter;
QLabel *m_titleLabel = nullptr;
QLabel *m_valueLabel = nullptr;
QSlider *m_slider = nullptr;
QPushButton *m_minusBtn = nullptr;
QPushButton *m_plusBtn = nullptr;
QTimer *m_inactivityTimer = nullptr;

static constexpr int IndicatorBarWidth = 5;
static constexpr int CornerRadius = 8;
static constexpr int InactivityMs = 3500;
};

#endif // ADJUSTOVERLAY_H
37 changes: 35 additions & 2 deletions src/ui/baloverlay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "k4styles.h"
#include <QVBoxLayout>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QFont>

BalOverlay::BalOverlay(QWidget *parent)
Expand Down Expand Up @@ -113,6 +114,38 @@ void BalOverlay::wheelEvent(QWheelEvent *event) {
}

void BalOverlay::mousePressEvent(QMouseEvent *event) {
// Don't close on click - allow adjustment via wheel
Q_UNUSED(event)
m_dragActive = true;
m_dragMoved = false;
m_dragStartX = event->position().x();
m_dragStartY = event->position().y();
event->accept();
}

void BalOverlay::mouseMoveEvent(QMouseEvent *event) {
if (!m_dragActive)
return;
const qreal x = event->position().x();
const qreal y = event->position().y();
if (!m_dragMoved && (qAbs(y - m_dragStartY) > 4 || qAbs(x - m_dragStartX) > 4))
m_dragMoved = true;
if (m_dragMoved) {
// Top of the overlay is +50 (toward SUB), bottom is -50 (toward MAIN).
const qreal h = qMax(1, height());
const qreal frac = 1.0 - qBound(0.0, y, h) / h;
const int newOffset = qBound(-50, int(qRound((frac - 0.5) * 100.0)), 50);
if (newOffset != m_offset) {
m_offset = newOffset;
updateDisplay();
emit balanceChangeRequested(m_mode, m_offset);
}
}
event->accept();
}

void BalOverlay::mouseReleaseEvent(QMouseEvent *event) {
// A tap (press with no drag) dismisses the overlay; a drag adjusted it.
if (m_dragActive && !m_dragMoved)
hide();
m_dragActive = false;
event->accept();
}
9 changes: 9 additions & 0 deletions src/ui/baloverlay.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class BalOverlay : public SideControlOverlay {
protected:
void wheelEvent(QWheelEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;

private:
void setupUi();
Expand All @@ -46,6 +48,13 @@ class BalOverlay : public SideControlOverlay {

int m_mode = 0; // 0=NOR, 1=BAL
int m_offset = 0; // -50 to +50

// Touch drag-to-adjust: a drag maps the finger's vertical position to the
// balance offset; a tap (no drag) dismisses the overlay.
bool m_dragActive = false;
bool m_dragMoved = false;
qreal m_dragStartX = 0;
qreal m_dragStartY = 0;
};

#endif // BALOVERLAY_H
Loading