Skip to content

Commit a8131e4

Browse files
committed
Replace QAction menu with custom QToolButton tray menu
- Replace native QAction items with QToolButton+QWidgetAction for full control over icon/text spacing, padding and dark-theme styling - Add inline Color/Effects/Clear icon buttons for the first LED instance (replaces submenu) with colored 4px indicator strips per button - Add recoloredIcon() helper that renders SVG icons at any size and recolors per-pixel for dark/light theme support - Auto-detect Windows dark theme and apply matching QSS + white icons - Fix autorun toggle text not updating after QAction→QToolButton change - Track last selected color so the color dialog opens with it preselected
1 parent f2e8fe7 commit a8131e4

2 files changed

Lines changed: 249 additions & 38 deletions

File tree

src/hyperiond/systray.cpp

Lines changed: 240 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,31 @@
1717

1818
#include <QDesktopServices>
1919
#include <QSettings>
20+
#include <QWidgetAction>
21+
#include <QToolButton>
22+
#include <QHBoxLayout>
23+
#include <QVBoxLayout>
24+
#include <QFrame>
25+
#include <QPainter>
26+
27+
namespace {
28+
QIcon recoloredIcon(const QString &svgPath, const QColor &color, int size = 18)
29+
{
30+
QIcon svgIcon(svgPath);
31+
QImage img = svgIcon.pixmap(QSize(size, size)).toImage().convertToFormat(QImage::Format_ARGB32);
32+
for (int y = 0; y < img.height(); y++)
33+
{
34+
QRgb *line = reinterpret_cast<QRgb*>(img.scanLine(y));
35+
for (int x = 0; x < img.width(); x++)
36+
{
37+
int a = qAlpha(line[x]);
38+
if (a > 0)
39+
line[x] = qRgba(color.red(), color.green(), color.blue(), a);
40+
}
41+
}
42+
return QIcon(QPixmap::fromImage(img));
43+
}
44+
}
2045

2146
SysTray::SysTray(HyperionDaemon* hyperiond)
2247
: QSystemTrayIcon(QIcon(":/hyperion-32px.png"), hyperiond)
@@ -31,6 +56,10 @@ SysTray::SysTray(HyperionDaemon* hyperiond)
3156
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
3257
#endif
3358

59+
// Detect dark theme
60+
QPalette pal = QGuiApplication::palette();
61+
_darkTheme = pal.color(QPalette::Window).lightness() < 128;
62+
3463
setupConnections();
3564
createBaseTrayMenu();
3665

@@ -64,6 +93,15 @@ void SysTray::createBaseTrayMenu()
6493
{
6594
_trayMenu = new QMenu();
6695

96+
// Dark theme styling
97+
if (_darkTheme)
98+
{
99+
_trayMenu->setStyleSheet(
100+
"QMenu { background-color: #2b2b2b; color: #e0e0e0; border: 1px solid #555; }"
101+
"QMenu::separator { height: 1px; background: #555; margin: 4px 8px; }"
102+
);
103+
}
104+
67105
// Create actions
68106
_settingsAction = createAction(tr("&Settings"), ":/settings.svg", [this]() {
69107
settings();
@@ -121,9 +159,24 @@ void SysTray::setupConnections()
121159

122160
QAction *SysTray::createAction(const QString &text, const QString &iconPath, const std::function<void()> &method)
123161
{
124-
auto* action = new QAction(text, this);
125-
action->setIcon(QIcon(iconPath));
126-
connect(action, &QAction::triggered, this, method);
162+
QColor ic = _darkTheme ? Qt::white : Qt::black;
163+
QIcon icon = recoloredIcon(iconPath, ic, 18);
164+
165+
auto* btn = new QToolButton();
166+
btn->setIcon(icon);
167+
btn->setText(QStringLiteral(" ") + text);
168+
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
169+
btn->setIconSize(QSize(18, 18));
170+
btn->setAutoRaise(true);
171+
btn->setCursor(Qt::PointingHandCursor);
172+
btn->setStyleSheet(
173+
"QToolButton { border: none; text-align: left; padding: 4px 16px 4px 10px; }"
174+
"QToolButton:hover { background-color: rgba(128,128,128,64); }"
175+
);
176+
QObject::connect(btn, &QToolButton::clicked, this, method);
177+
178+
auto* action = new QWidgetAction(this);
179+
action->setDefaultWidget(btn);
127180
return action;
128181
}
129182

@@ -194,35 +247,26 @@ void SysTray::handleInstanceStarted(quint8 instance)
194247
return;
195248
}
196249

197-
// Create a new menu for this instance
250+
// Get instance name
198251
QString instanceName;
199252
if (auto mgr = _instanceManagerWeak.toStrongRef())
200253
{
201254
instanceName = mgr->getInstanceName(instance);
202255
}
203-
auto* instanceMenu = new QMenu(instanceName);
204256

205-
// Create actions for the instance menu
206-
QAction *colorAction = createAction(tr("&Color"), ":/color.svg", [this, instance]() {
207-
showColorDialog(instance);
208-
});
209-
instanceMenu->addAction(colorAction);
257+
// First instance: horizontal icon buttons directly in main menu
258+
if (!_firstInstanceAction)
259+
{
260+
_firstInstanceNumber = instance;
210261

211262
#if defined(ENABLE_EFFECTENGINE)
212-
// Get the list of effects
213-
const QList<EffectDefinition> effectsDefinitions = EffectFileHandler::getInstance()->getEffects();
214-
215-
if(!effectsDefinitions.empty())
216-
{
217-
// Effects submenu
218-
auto* effectsMenu = new QMenu(tr("Effects"), instanceMenu);
219-
effectsMenu->setObjectName("effectsMenu");
220-
instanceMenu->addMenu(effectsMenu);
263+
_firstEffectsMenu = new QMenu(tr("Effects"));
264+
_firstEffectsMenu->setObjectName("effectsMenu");
221265

222-
// Add effects when the menu is first created
266+
const QList<EffectDefinition> effectsDefinitions = EffectFileHandler::getInstance()->getEffects();
223267
for (const auto& effect : effectsDefinitions)
224268
{
225-
QAction const * effectAction = effectsMenu->addAction(effect.name);
269+
QAction* effectAction = _firstEffectsMenu->addAction(effect.name);
226270
connect(effectAction, &QAction::triggered, [this, instance, effectName = effect.name]() {
227271
setEffect(instance, effectName);
228272
});
@@ -231,23 +275,163 @@ void SysTray::handleInstanceStarted(quint8 instance)
231275
{
232276
connect(eff.get(), &EffectFileHandler::effectListChanged, this, &SysTray::onEffectListChanged);
233277
}
234-
}
235278
#endif
236279

237-
QAction *clearAction = createAction(tr("&Clear"), ":/clear.svg", [instance, this]() {
238-
clearSource(instance);
239-
});
240-
instanceMenu->addAction(clearAction);
280+
QColor ic = _darkTheme ? Qt::white : Qt::black;
281+
282+
auto* btnWidget = new QWidget();
283+
auto* hLayout = new QHBoxLayout(btnWidget);
284+
hLayout->setContentsMargins(8, 6, 8, 0);
285+
hLayout->setSpacing(4);
286+
btnWidget->setFixedHeight(37);
287+
288+
auto makeBtnGroup = [&](const QIcon& icon, const QString& tip, bool checkable = false) -> std::tuple<QToolButton*, QFrame*> {
289+
auto* container = new QWidget();
290+
container->setFixedWidth(26);
291+
auto* col = new QVBoxLayout(container);
292+
col->setContentsMargins(0, 0, 0, 0);
293+
col->setSpacing(1);
294+
295+
auto* btn = new QToolButton();
296+
btn->setIcon(icon);
297+
btn->setToolTip(tip);
298+
if (checkable) btn->setCheckable(true);
299+
btn->setIconSize(QSize(26, 26));
300+
btn->setAutoRaise(true);
301+
btn->setStyleSheet("QToolButton:hover { background-color: #3d6db5; }");
302+
col->addWidget(btn, 0, Qt::AlignHCenter);
303+
304+
auto* ind = new QFrame();
305+
ind->setFixedSize(26, 4);
306+
ind->setStyleSheet("background-color: transparent; border-radius: 2px;");
307+
col->addWidget(ind, 0, Qt::AlignHCenter);
308+
309+
hLayout->addWidget(container, 0, Qt::AlignVCenter);
310+
return {btn, ind};
311+
};
312+
313+
auto [colorBtn, colorInd] = makeBtnGroup(recoloredIcon(":/color.svg", ic, 26), tr("Color"));
314+
315+
#if defined(ENABLE_EFFECTENGINE)
316+
auto [effectsBtn, effectsInd] = makeBtnGroup(recoloredIcon(":/effects.svg", ic, 26), tr("Effects"), true);
317+
effectsBtn->setPopupMode(QToolButton::InstantPopup);
318+
effectsBtn->setMenu(_firstEffectsMenu);
319+
#else
320+
QToolButton* effectsBtn = nullptr;
321+
QFrame* effectsInd = nullptr;
322+
#endif
323+
324+
auto [clearBtn, clearInd] = makeBtnGroup(recoloredIcon(":/clear.svg", ic, 26), tr("Clear"));
325+
326+
// Color clicked
327+
connect(colorBtn, &QToolButton::clicked, [this, colorBtn, effectsBtn, effectsInd, instance, colorInd]() {
328+
QColor selectedColor = QColorDialog::getColor(_lastColor, nullptr, tr("Select Color"));
329+
if (selectedColor.isValid())
330+
{
331+
_lastColor = selectedColor;
332+
setColor(instance, selectedColor);
333+
colorInd->setStyleSheet(QString(
334+
"background-color: %1; border-radius: 2px;"
335+
).arg(selectedColor.name()));
336+
if (effectsBtn) {
337+
effectsBtn->setChecked(false);
338+
effectsInd->setStyleSheet(
339+
"background-color: transparent; border-radius: 2px;"
340+
);
341+
}
342+
}
343+
});
344+
345+
// Effects triggered
346+
#if defined(ENABLE_EFFECTENGINE)
347+
connect(_firstEffectsMenu, &QMenu::triggered, [effectsBtn, colorInd, effectsInd](QAction*) {
348+
effectsBtn->setChecked(true);
349+
colorInd->setStyleSheet(
350+
"background-color: transparent; border-radius: 2px;"
351+
);
352+
effectsInd->setStyleSheet(
353+
"background-color: #32cd32; border-radius: 2px;"
354+
);
355+
});
356+
#endif
357+
358+
// Clear clicked
359+
connect(clearBtn, &QToolButton::clicked, [this, colorInd, effectsBtn, effectsInd, instance]() {
360+
clearSource(instance);
361+
colorInd->setStyleSheet(
362+
"background-color: transparent; border-radius: 2px;"
363+
);
364+
if (effectsBtn) {
365+
effectsBtn->setChecked(false);
366+
effectsInd->setStyleSheet(
367+
"background-color: transparent; border-radius: 2px;"
368+
);
369+
}
370+
});
371+
372+
_firstInstanceAction = new QWidgetAction(this);
373+
_firstInstanceAction->setDefaultWidget(btnWidget);
374+
_trayMenu->insertAction(_settingsAction, _firstInstanceAction);
375+
}
376+
else
377+
{
378+
// Subsequent instances get submenus
379+
auto* instanceMenu = new QMenu(instanceName);
241380

242-
_trayMenu->insertMenu(_suspendAction, instanceMenu);
381+
QAction *colorAction = createAction(tr("&Color"), ":/color.svg", [this, instance]() {
382+
showColorDialog(instance);
383+
});
384+
instanceMenu->addAction(colorAction);
243385

244-
// Store the menu for later reference
245-
_instanceMenus[instance] = instanceMenu;
386+
#if defined(ENABLE_EFFECTENGINE)
387+
const QList<EffectDefinition> effectsDefinitions = EffectFileHandler::getInstance()->getEffects();
388+
if (!effectsDefinitions.empty())
389+
{
390+
auto* effectsMenu = new QMenu(tr("Effects"), instanceMenu);
391+
effectsMenu->setObjectName("effectsMenu");
392+
instanceMenu->addMenu(effectsMenu);
393+
394+
for (const auto& effect : effectsDefinitions)
395+
{
396+
QAction* effectAction = effectsMenu->addAction(effect.name);
397+
connect(effectAction, &QAction::triggered, [this, instance, effectName = effect.name]() {
398+
setEffect(instance, effectName);
399+
});
400+
}
401+
if (auto eff = EffectFileHandler::getInstance())
402+
{
403+
connect(eff.get(), &EffectFileHandler::effectListChanged, this, &SysTray::onEffectListChanged);
404+
}
405+
}
406+
#endif
407+
408+
QAction *clearAction = createAction(tr("&Clear"), ":/clear.svg", [instance, this]() {
409+
clearSource(instance);
410+
});
411+
instanceMenu->addAction(clearAction);
412+
413+
_trayMenu->insertMenu(_suspendAction, instanceMenu);
414+
_instanceMenus[instance] = instanceMenu;
415+
}
246416
}
247417

248418
void SysTray::handleInstanceStopped(quint8 instance)
249419
{
250-
// Check if the instance exists
420+
// Check if the first instance (inline buttons) is stopping
421+
if (_firstInstanceAction && instance == _firstInstanceNumber)
422+
{
423+
_trayMenu->removeAction(_firstInstanceAction);
424+
delete _firstInstanceAction;
425+
_firstInstanceAction = nullptr;
426+
427+
#if defined(ENABLE_EFFECTENGINE)
428+
delete _firstEffectsMenu;
429+
_firstEffectsMenu = nullptr;
430+
#endif
431+
return;
432+
}
433+
434+
// Check if the instance menu exists
251435
if (!_instanceMenus.contains(instance))
252436
return;
253437

@@ -280,9 +464,24 @@ void SysTray::onEffectListChanged()
280464
// Get the updated list of effects
281465
const QList<EffectDefinition> effectsDefinitions = EffectFileHandler::getInstance()->getEffects();
282466

467+
// Update inline effects menu for the first instance
468+
if (_firstEffectsMenu)
469+
{
470+
_firstEffectsMenu->clear();
471+
quint8 instance = _firstInstanceNumber;
472+
for (const auto& effect : effectsDefinitions)
473+
{
474+
QAction* effectAction = _firstEffectsMenu->addAction(effect.name);
475+
connect(effectAction, &QAction::triggered, [this, instance, effectName = effect.name]() {
476+
setEffect(instance, effectName);
477+
});
478+
}
479+
}
480+
481+
// Update submenu effects for other instances
283482
for (auto it = _instanceMenus.begin(); it != _instanceMenus.end(); ++it)
284483
{
285-
QMenu const * instanceMenu = it.value(); // Access the value (QMenu*) from the map
484+
QMenu* instanceMenu = it.value();
286485
quint8 instanceNumber = it.key();
287486

288487
QMenu* effectsMenu = instanceMenu->findChild<QMenu*>("effectsMenu");
@@ -295,7 +494,7 @@ void SysTray::onEffectListChanged()
295494
// Re-add the updated list of effects
296495
for (const auto& effect : effectsDefinitions)
297496
{
298-
QAction const * effectAction = effectsMenu->addAction(effect.name);
497+
QAction* effectAction = effectsMenu->addAction(effect.name);
299498
connect(effectAction, &QAction::triggered, [this, instance = instanceNumber, effectName = effect.name]() {
300499
setEffect(instance, effectName);
301500
});
@@ -342,14 +541,17 @@ void SysTray::settings() const
342541
bool SysTray::getCurrentAutorunState()
343542
{
344543
const QSettings reg("HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", QSettings::NativeFormat);
345-
if (reg.value("Hyperion", 0).toString() == QApplication::applicationFilePath().replace('/', '\\'))
544+
bool enabled = (reg.value("Hyperion", 0).toString() == QApplication::applicationFilePath().replace('/', '\\'));
545+
auto* wa = qobject_cast<QWidgetAction*>(_autorunAction);
546+
if (wa)
346547
{
347-
_autorunAction->setText(tr("&Disable autostart"));
348-
return true;
548+
QToolButton* btn = qobject_cast<QToolButton*>(wa->defaultWidget());
549+
if (btn)
550+
{
551+
btn->setText(QStringLiteral(" ") + (enabled ? tr("&Disable autostart") : tr("&Enable autostart")));
552+
}
349553
}
350-
351-
_autorunAction->setText(tr("&Enable autostart"));
352-
return false;
554+
return enabled;
353555
}
354556

355557
void SysTray::setAutorunState()

src/hyperiond/systray.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <QSystemTrayIcon>
99
#include <QMenu>
1010
#include <QWidget>
11+
#include <QWidgetAction>
1112
#include <QColorDialog>
1213
#include <QCloseEvent>
1314
#include <QSharedPointer>
@@ -88,6 +89,14 @@ private slots:
8889
QAction* _restartAction;
8990
QAction* _quitAction;
9091

92+
bool _darkTheme = false;
93+
94+
// First instance inline buttons (horizontal row instead of submenu)
95+
quint8 _firstInstanceNumber = 0;
96+
QWidgetAction* _firstInstanceAction = nullptr;
97+
QMenu* _firstEffectsMenu = nullptr;
98+
QColor _lastColor = Qt::white;
99+
91100
#ifdef _WIN32
92101
QAction* _autorunAction;
93102
#endif

0 commit comments

Comments
 (0)