• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mbits-os / quick_dra / 28474731813

30 Jun 2026 08:46PM UTC coverage: 99.67% (-0.3%) from 100.0%
28474731813

Pull #89

github

web-flow
Merge 57ae247d7 into 1c58171ec
Pull Request #89: feat: redesign panel layout

715 of 739 new or added lines in 10 files covered. (96.75%)

1 existing line in 1 file now uncovered.

7552 of 7577 relevant lines covered (99.67%)

1255.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.6
/libs/gui/controls/src/app/controls/PanelButtonGroup.cpp
1
// Copyright (c) 2026 midnightBITS
2
// This code is licensed under MIT license (see LICENSE for details)
3

4
#include <QLabel>
5
#include <QMouseEvent>
6
#include <QPainter>
7
#include <QPainterPath>
8
#include <app/controls/Glyph.hpp>
9
#include <app/controls/Panel.hpp>
10
#include <app/controls/PanelButtonGroup.hpp>
11
#include <app/controls/PanelButtonGroup_p.hpp>
12
#include <app/gui/CurrentColor.hpp>
13
#include <app/utils/LaidOut.hpp>
14
#include <app/utils/utils.hpp>
15
#include <memory>
16
#include <print>
17
#include <utility>
18

19
namespace quick_dra::gui {
20
        namespace {
21
                bool lightModeActive(QPalette const& palette) {
67✔
22
                        return palette.color(QPalette::Window).toHsl().lightness() >
67✔
23
                               palette.color(QPalette::WindowText).toHsl().lightness();
66✔
24
                }
1✔
25

26
                PanelButtonStyle::Palette const& selectPalette() {
67✔
27
                        return lightModeActive(qApp->palette()) ? PanelButtonStyle::lightPalette : PanelButtonStyle::darkPalette;
67✔
28
                }
1✔
29

30
                void clear(QLayoutItem* item);
31
                void clear(QLayout* layout) {
3✔
32
                        while (layout->count() > 0) {
17✔
33
                                auto item = layout->takeAt(layout->count() - 1);
13✔
34
                                clear(item);
13✔
35
                                delete item;
13✔
36
                        }
1✔
37
                }
3✔
38

39
                void clear(QLayoutItem* item) {
73✔
40
                        if (auto widget = item->widget()) {
73✔
41
                                widget->deleteLater();
71✔
42
                        } else if (auto layout = item->layout()) {
3✔
43
                                clear(layout);
3✔
44
                                layout->deleteLater();
3✔
45
                        }
46
                }
73✔
47
        }  // namespace
48

49
        PanelButtonPrivate::~PanelButtonPrivate() {
259✔
50
                if (has_item_ownership) {
259✔
51
                        delete item;
61✔
52
                }
53
        }
259✔
54

55
        void PanelButtonPrivate::paint(QPainter& painter, Positions pos, PanelButtonStyle::Palette const& palette) const {
357✔
56
                using namespace PanelButtonStyle;
57
                static constexpr auto DiameterF = RadiusF * 2;
58

59
                auto const rect = item->geometry().marginsAdded({TrueMargin, TrueMargin, TrueMargin, TrueMargin}).toRectF();
357✔
60
                QPainterPath path{};
357✔
61
                if (pos & PanePosition::Bottom) {
357✔
62
                        path.moveTo(rect.left(), rect.bottom() - RadiusF);
61✔
63
                } else {
1✔
64
                        path.moveTo(rect.left(), rect.bottom());
297✔
65
                }
66

67
                if (pos & PanePosition::Top) {
357✔
68
                        // RIGHT
69
                        path.lineTo(rect.left(), rect.top() + RadiusF);
61✔
70
                        // TL corner
71
                        path.arcTo(rect.left(), rect.top(), DiameterF, DiameterF, 180, -90);
61✔
72
                        // TOP
73
                        path.lineTo(rect.right() - RadiusF, rect.top());
61✔
74
                        // TR corner
75
                        path.arcTo(rect.right() - DiameterF, rect.top(), DiameterF, DiameterF, 90, -90);
61✔
76
                } else {
1✔
77
                        // RIGHT
78
                        path.lineTo(rect.left(), rect.top());
297✔
79
                        // TOP
80
                        path.lineTo(rect.right(), rect.top());
297✔
81
                }
82

83
                if (pos & PanePosition::Bottom) {
357✔
84
                        path.lineTo(rect.right(), rect.bottom() - RadiusF);
61✔
85
                        path.arcTo(rect.right() - DiameterF, rect.bottom() - DiameterF, DiameterF, DiameterF, 0, -90);
61✔
86
                        path.lineTo(rect.left() + RadiusF, rect.bottom());
61✔
87
                        path.arcTo(rect.left(), rect.bottom() - DiameterF, DiameterF, DiameterF, 270, -90);
61✔
88
                } else {
1✔
89
                        path.lineTo(rect.right(), rect.bottom());
297✔
90
                }
91

92
                auto const normal = !isClickable() || (!isActive() && !isHovered());
357✔
93
                auto const pane = !isEnabled() ? palette.disabled
371✔
94
                                  : normal     ? palette.normal
95
                                  : isActive() ? palette.active
14✔
96
                                               : palette.hover;
356✔
97

98
                painter.setPen(palette.frame);
357✔
99
                painter.setBrush(pane);
357✔
100
                painter.drawPath(path);
357✔
101
        }
357✔
102

103
        PanelButton::PanelButton() : d_ptr{new PanelButtonPrivate} {
259✔
104
                Q_D(PanelButton);
259✔
105
                d->q_ptr = this;
259✔
106
        }
259✔
107

108
        PanelButton::~PanelButton() = default;
517✔
109

110
        QWidget* PanelButton::widget() const { return d_func()->item->widget(); }
91✔
111
        QLayout* PanelButton::layout() const { return d_func()->item->layout(); }
17✔
112

113
        void PanelButton::clearItem() {
61✔
114
                Q_D(PanelButton);
61✔
115
                clear(d->item);
61✔
116
        }
61✔
117

118
#define FWD(CLS, NAME)                                                   \
119
        bool CLS::is##NAME() const noexcept { return d_func()->is##NAME(); } \
120
        void CLS::set##NAME(bool value) noexcept { d_func()->set##NAME(value); }
121
        FWD(PanelButton, Clickable)
317✔
122
        FWD(PanelButton, Enabled)
65✔
123
        FWD(PanelButton, Hovered)
261✔
124
        FWD(PanelButton, Active)
63✔
125
#undef GWD
126

127
        void PanelButtonGroupPrivate::UI::setupUI(PanelButtonGroup* parent) {
45✔
128
                LaidOut{parent}.createLayout(layout, "layout", parent, [](auto& layout) {
89✔
129
                        using PanelButtonStyle::Margin;
130

131
                        layout.setContentsMargins(Margin, Margin, Margin, Margin);
45✔
132
                        layout.setSpacing(2 * PanelButtonStyle::TrueMargin - 1);  // only one separating line...
45✔
133
                });
45✔
134
        }
45✔
135

136
        PanelButtonGroupPrivate::PanelButtonGroupPrivate() = default;
45✔
137
        PanelButtonGroupPrivate::~PanelButtonGroupPrivate() = default;
45✔
138

139
        int PanelButtonGroupPrivate::count() const { return static_cast<int>(controls_.size()); }
7✔
140
        PanelButton* PanelButtonGroupPrivate::itemAt(int pos) const { return controls_.at(static_cast<size_t>(pos)).get(); }
5✔
141
        PanelButton* PanelButtonGroupPrivate::addItem(QLayoutItem* item) {
259✔
142
                auto priv = std::make_unique<PanelButton>();
259✔
143
                auto result = priv.get();
259✔
144

145
                auto const d = priv->d_func();
259✔
146
                d->item = item;
259✔
147
                d->q_parent = q_func();
259✔
148
                controls_.emplace_back(std::move(priv));
388✔
149

150
                return result;
259✔
151
        }
259✔
152

153
        void PanelButtonGroupPrivate::paintEvent(QPainter& painter, PanelButtonStyle::Palette const& palette) {
67✔
154
                auto lastIndex = -1;
67✔
155

156
                for (auto index = -1; auto const& control : controls_) {
500✔
157
                        ++index;
401✔
158
                        auto const& d = *control.get()->d_func();
401✔
159
                        if (auto const wdg = d.item->widget(); wdg && wdg->isHidden()) {
401✔
160
                                continue;
45✔
161
                        }
162
                        lastIndex = index;
357✔
163
                }
1✔
164

165
                auto first = true;
67✔
166
                for (auto index = -1; auto const& control : controls_) {
500✔
167
                        ++index;
401✔
168
                        auto const& d = *control.get()->d_func();
401✔
169
                        if (auto const wdg = d.item->widget(); wdg && wdg->isHidden()) {
401✔
170
                                continue;
45✔
171
                        }
172

173
                        Positions pos{};
357✔
174
                        if (first) pos |= PanePosition::Top;
357✔
175
                        if (index == lastIndex) pos |= PanePosition::Bottom;
357✔
176
                        first = false;
357✔
177

178
                        d.paint(painter, pos, palette);
357✔
179
                }
1✔
180
        }
67✔
181

182
        void PanelButtonGroupPrivate::mouseMoveEvent(QPointF const& pos) { trackHover(fromPos(pos.toPoint())); }
59✔
183

184
        void PanelButtonGroupPrivate::mousePressEvent(QPointF const& pos, Qt::MouseButton button) {
15✔
185
                if (button != Qt::MouseButton::LeftButton) {
15✔
186
                        return;
3✔
187
                }
188

189
                trackHover(fromPos(pos.toPoint()));
13✔
190

191
                if (hovered_ && hovered_->isClickable()) {
13✔
192
                        originalActive_ = hovered_;
9✔
193
                } else {
1✔
194
                        originalActive_ = nullptr;
5✔
195
                }
196
                if (originalActive_) {
13✔
197
                        originalActive_->setHovered(false);
9✔
198
                        originalActive_->setActive(true);
9✔
199
                }
200
        }
1✔
201

202
        void PanelButtonGroupPrivate::mouseReleaseEvent(QPointF const& pos, Qt::MouseButton button) {
15✔
203
                if (button != Qt::MouseButton::LeftButton) {
15✔
204
                        return;
3✔
205
                }
206

207
                trackHover(fromPos(pos.toPoint()));
13✔
208

209
                if (!originalActive_) {
13✔
210
                        return;
5✔
211
                }
212

213
                originalActive_->setActive(false);
9✔
214
                if (hovered_) hovered_->setHovered(true);
9✔
215
                if (originalActive_ == hovered_ && originalActive_->isEnabled() && originalActive_->isClickable()) {
9✔
216
                        originalActive_->clicked();
7✔
217
                }
218
                originalActive_ = nullptr;
9✔
219
                if (!q_func()->geometry().contains(pos.toPoint())) {
9✔
UNCOV
220
                        q_func()->setMouseTracking(false);
×
221
                }
222
        }
1✔
223

224
        PanelButton* PanelButtonGroupPrivate::fromPos(QPoint const& pos) const {
83✔
225
                using namespace PanelButtonStyle;
226

227
                for (auto const& control : controls_) {
598✔
228
                        auto const& d = *control.get()->d_func();
509✔
229
                        if (auto const wdg = d.item->widget(); wdg && wdg->isHidden()) {
509✔
230
                                continue;
57✔
231
                        }
232

233
                        auto const rect = d.item->geometry().marginsAdded({TrueMargin, TrueMargin, TrueMargin, TrueMargin});
453✔
234
                        if (rect.contains(pos)) {
453✔
235
                                return control.get();
35✔
236
                        }
237
                }
1✔
238

239
                return nullptr;
49✔
240
        }
1✔
241

242
        void PanelButtonGroupPrivate::trackHover(PanelButton* next) {
97✔
243
                if (next == hovered_) return;
97✔
244
                if (hovered_) hovered_->setHovered(false);
41✔
245
                hovered_ = next;
41✔
246
                if (originalActive_) {
41✔
247
                        originalActive_->setActive(originalActive_ == hovered_);
7✔
248
                } else {
1✔
249
                        if (hovered_) hovered_->setHovered(true);
35✔
250
                }
251

252
                if (hovered_) {
41✔
253
                        q_func()->setCursor(hovered_->d_func()->cursor());
27✔
254
                } else {
1✔
255
                        q_func()->setCursor(Qt::ArrowCursor);
15✔
256
                }
257
        }
1✔
258

259
        PanelButtonGroup::PanelButtonGroup(QWidget* parent) : QWidget{parent}, d_ptr{new PanelButtonGroupPrivate} {
45✔
260
                Q_D(PanelButtonGroup);
45✔
261
                d->q_ptr = this;
45✔
262
                d->ui.setupUI(this);
45✔
263
        }
45✔
264

265
        PanelButtonGroup::~PanelButtonGroup() = default;
71✔
266

267
        int PanelButtonGroup::count() const { return d_func()->count(); }
7✔
268

269
        PanelButton* PanelButtonGroup::itemAt(int pos) const { return d_func()->itemAt(pos); }
5✔
270

271
        PanelButton* PanelButtonGroup::addButton(QString const& label, bool bold) {
37✔
272
                return createWidget<QLabel>([&label, bold](QLabel& control) {
73✔
273
                        control.setText(label);
37✔
274
                        control.setSizePolicy(TakeWidth / HeightForWidth);
37✔
275
                        control.setWordWrap(true);
37✔
276
                        if (bold) {
37✔
277
                                auto font = control.font();
19✔
278
                                font.setBold(true);
19✔
279
                                control.setFont(font);
19✔
280
                        }
19✔
281
                });
109✔
282
        }
1✔
283

284
        PanelButton* PanelButtonGroup::addWidget(QWidget* widget) {
241✔
285
                Q_D(PanelButtonGroup);
241✔
286
                auto const count = d->ui.layout->count();
241✔
287
                d->ui.layout->addWidget(widget);
241✔
288
                auto item = d->ui.layout->itemAt(count);
241✔
289
                return d->addItem(item);
241✔
290
        }
1✔
291

292
        PanelButton* PanelButtonGroup::addLayout(QLayout* layout) {
19✔
293
                Q_D(PanelButtonGroup);
19✔
294
                auto const count = d->ui.layout->count();
19✔
295
                d->ui.layout->addLayout(layout);
19✔
296
                auto item = d->ui.layout->itemAt(count);
19✔
297
                return d->addItem(item);
19✔
298
        }
1✔
299

300
        PanelButton* PanelButtonGroup::createPanel(PanelInfo const& info, QAnyStringView objectName) {
205✔
301
                auto const result = createWidget<Panel>(objectName, [&info](Panel& panel) { panel.setInfo(info); });
409✔
302
                if (info.isClickable) {
205✔
303
                        result->setClickable(*info.isClickable);
205✔
304
                }
305
                if (info.isEnabled) {
205✔
306
                        result->setEnabled(*info.isEnabled);
5✔
307
                }
308
                return result;
205✔
309
        }
1✔
310

311
        PanelButton* PanelButtonGroup::takeLast() {
91✔
312
                Q_D(PanelButtonGroup);
91✔
313
                if (d->controls_.empty()) {
91✔
314
                        return nullptr;
31✔
315
                }
316

317
                auto local = std::move(d->controls_.back());
91✔
318
                d->controls_.pop_back();
61✔
319

320
                auto button_d = local->d_func();
61✔
321

322
                if (local) {
61✔
323
                        auto const index = d->ui.layout->indexOf(button_d->item);
61✔
324
                        d->ui.layout->takeAt(index);
61✔
325
                        button_d->has_item_ownership = true;
61✔
326
                }
327
                return local.release();
61✔
328
        }
61✔
329

330
        void PanelButtonGroup::clearAll() {
31✔
331
                while (auto item = takeLast()) {
91✔
332
                        item->clearItem();
61✔
333
                        delete item;
61✔
334
                }
61✔
335
        }
31✔
336

337
        void PanelButtonGroup::paintEvent(QPaintEvent* event) {
67✔
338
                QWidget::paintEvent(event);
67✔
339

340
                Q_D(PanelButtonGroup);
67✔
341

342
                QPainter painter{this};
67✔
343
                d->paintEvent(painter, selectPalette());
67✔
344
        }
67✔
345

346
        void PanelButtonGroup::enterEvent(QEnterEvent* event) {
31✔
347
                Q_D(PanelButtonGroup);
31✔
348

349
                setMouseTracking(true);
31✔
350

351
                d->mouseMoveEvent(event->position());
31✔
352
        }
31✔
353

354
        void PanelButtonGroup::leaveEvent(QEvent*) {
15✔
355
                Q_D(PanelButtonGroup);
15✔
356

357
                if (!d->trackingActive()) setMouseTracking(false);
15✔
358
                d->trackHover(nullptr);
15✔
359
        }
15✔
360

361
        void PanelButtonGroup::mouseMoveEvent(QMouseEvent* event) {
29✔
362
                Q_D(PanelButtonGroup);
29✔
363

364
                d->mouseMoveEvent(event->position());
29✔
365
        }
29✔
366
        void PanelButtonGroup::mousePressEvent(QMouseEvent* event) {
15✔
367
                Q_D(PanelButtonGroup);
15✔
368

369
                d->mousePressEvent(event->position(), event->button());
15✔
370
        }
15✔
371
        void PanelButtonGroup::mouseReleaseEvent(QMouseEvent* event) {
15✔
372
                Q_D(PanelButtonGroup);
15✔
373

374
                d->mouseReleaseEvent(event->position(), event->button());
15✔
375
        }
15✔
376
}  // namespace quick_dra::gui
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc