gqtbuttonswidget
Loading...
Searching...
No Matches
gQtButtonsWidget.cc
Go to the documentation of this file.
1#include "gQtButtonsWidget.h"
2#include <QBoxLayout>
3#include <QFile>
4#include <QListView>
5#include <QEvent>
6#include <QShowEvent>
7#include <QCoreApplication>
8#include <QFontMetricsF>
9#include <QRegularExpression>
10
11QString gemc::translateSvgLabels(QString svg) {
12 // These captions are rendered from the existing SVGs; Qt extracts the complete source phrases.
13 static const char* captions[] = {
14 QT_TRANSLATE_NOOP("GGuiIcons", "Display"), QT_TRANSLATE_NOOP("GGuiIcons", "Setup"),
15 QT_TRANSLATE_NOOP("GGuiIcons", "Volumes"), QT_TRANSLATE_NOOP("GGuiIcons", "G4Dialog"),
16 QT_TRANSLATE_NOOP("GGuiIcons", "Generator"), QT_TRANSLATE_NOOP("GGuiIcons", "Analyzer"),
17 QT_TRANSLATE_NOOP("GGuiIcons", "Wire"), QT_TRANSLATE_NOOP("GGuiIcons", "Surface"),
18 QT_TRANSLATE_NOOP("GGuiIcons", "Cloud"), QT_TRANSLATE_NOOP("GGuiIcons", "Twinkle"),
19 QT_TRANSLATE_NOOP("GGuiIcons", "Hidden\nLines"), QT_TRANSLATE_NOOP("GGuiIcons", "Anti\nAliasing"),
20 QT_TRANSLATE_NOOP("GGuiIcons", "Auxiliary\nEdges"), QT_TRANSLATE_NOOP("GGuiIcons", "Field\nLines")
21 };
22 const QRegularExpression textTag("(<text\\b[^>]*>)([^<]*)(</text>)");
23 const QRegularExpression fontSize("font-size=\"([0-9.]+)\"");
24 const QRegularExpression fontFamily("font-family=\"([^\"]+)\"");
25 QList<QRegularExpressionMatch> matches;
26 QStringList lines;
27 auto iterator = textTag.globalMatch(svg);
28 while (iterator.hasNext()) {
29 const auto match = iterator.next();
30 const auto text = match.captured(2).trimmed();
31 if (text == ">_" || text.isEmpty()) continue; // Command glyph, not a caption.
32 matches.append(match);
33 lines.append(text);
34 }
35 const auto source = lines.join('\n');
36 for (const auto caption : captions) {
37 if (source != QString::fromUtf8(caption)) continue;
38 const auto translated = QCoreApplication::translate("GGuiIcons", caption).split('\n');
39 if (translated.size() != matches.size()) return svg;
40 for (int index = matches.size() - 1; index >= 0; --index) {
41 const auto& match = matches[index];
42 auto opening = match.captured(1);
43 const auto size = fontSize.match(opening);
44 if (size.hasMatch()) {
45 const qreal pixels = size.captured(1).toDouble();
46 const auto family = fontFamily.match(opening);
47 QFont font(family.hasMatch() ? family.captured(1) : QString());
48 // Reserve space inside the frame and account conservatively for fallback glyphs.
49 font.setPointSizeF(pixels);
50 const qreal width = QFontMetricsF(font).horizontalAdvance(translated[index]);
51 if (width > 32) {
52 opening.replace(size.capturedStart(1), size.capturedLength(1),
53 QString::number(pixels * 32 / width, 'f', 2));
54 }
55 }
56 svg.replace(match.capturedStart(), match.capturedLength(),
57 opening + translated[index].toHtmlEscaped() + match.captured(3));
58 }
59 break;
60 }
61 return svg;
62}
63
64/* --- ButtonInfo Implementation ---
65 *
66 * This implementation provides:
67 * - construction of a QListWidgetItem configured as selectable/enabled
68 * - icon lookup using the "<base>_<state>.svg" filename convention
69 */
70
71ButtonInfo::ButtonInfo(const std::string& icon)
72 : buttonName(icon) {
73 thisButton = new QListWidgetItem();
74
75 // The list item must be enabled/selectable to behave as a clickable icon entry.
76 thisButton->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
77}
78
79QIcon ButtonInfo::iconForState(int state, const QSize& iconSize, const QPalette& palette) const {
80 // Build the expected resource/file name for the requested state.
81 const std::string filename = buttonName + "_" + std::to_string(state) + ".svg";
82
83 QFile file(QString::fromStdString(filename));
84 if (!file.open(QIODevice::ReadOnly)) {
85 return QIcon();
86 }
87
88 QString svg = gemc::translateSvgLabels(QString::fromUtf8(file.readAll()));
89 const QColor foreground = state == 2
90 ? palette.color(QPalette::HighlightedText)
91 : palette.color(QPalette::WindowText);
92 const QColor selectedBackground = palette.color(QPalette::Highlight);
93
94 svg.replace("width=\"48\" height=\"48\"",
95 QString("width=\"%1\" height=\"%2\"").arg(iconSize.width()).arg(iconSize.height()));
96 svg.replace("currentColor", foreground.name(QColor::HexRgb));
97 svg.replace("#aaddff", selectedBackground.name(QColor::HexRgb), Qt::CaseInsensitive);
98
99 QPixmap pixmap;
100 if (!pixmap.loadFromData(svg.toUtf8(), "SVG")) {
101 return QIcon();
102 }
103 return QIcon(pixmap);
104}
105
106/* --- GQTButtonsWidget Implementation ---
107 *
108 * This widget:
109 * - builds one ButtonInfo per base icon name
110 * - displays them in a QListWidget configured for icon mode
111 * - updates icons on press to reflect "normal" vs "pressed" state
112 */
113
115 const std::vector<std::string>& bicons,
116 bool vertical, QWidget* parent)
117 : QWidget(parent) {
118 constexpr int distanceToMargin = 12;
119
120 // Convert icon base names into internal ButtonInfo entries.
121 for (const auto& b : bicons) {
122 buttons.push_back(new ButtonInfo(b));
123 }
124
125 // Create and configure the QListWidget backend (icon mode, fixed icon size).
126 buttonsWidget = new QListWidget(this);
127 buttonsWidget->setViewMode(QListView::IconMode);
128 buttonsWidget->setIconSize(QSize(static_cast<int>(h), static_cast<int>(v)));
129
130 // SVG colors are resolved from the Qt palette in ButtonInfo::iconForState().
131 buttonsWidget->setStyleSheet(
132 "background-color: transparent; "
133 "QListWidget::item:selected { background: transparent; border: none; }");
134
135 // Insert each QListWidgetItem into the list widget.
136 for (auto& b : buttons) {
137 b->thisButton->setIcon(b->iconForState(1, buttonsWidget->iconSize(), buttonsWidget->palette()));
138 buttonsWidget->addItem(b->thisButton);
139 }
140
141 // When an item is pressed, update all icons so only the pressed one shows state 2.
142 connect(buttonsWidget, &QListWidget::itemPressed,
143 this, &GQTButtonsWidget::buttonWasPressed);
144
145 // Choose layout based on the requested orientation.
146 QBoxLayout* layout = vertical
147 ? static_cast<QBoxLayout*>(new QVBoxLayout(this))
148 : static_cast<QBoxLayout*>(new QHBoxLayout(this));
149 layout->setContentsMargins(0, 0, 0, 0);
150 layout->addWidget(buttonsWidget);
151 setLayout(layout);
152
153 // Size the widget so the icon strip fits exactly (plus a margin factor).
154 double hsize = (h + distanceToMargin) * (buttons.size());
155 double vsize = v + distanceToMargin;
156 if (vertical) {
157 hsize = h + distanceToMargin;
158 vsize = (v + distanceToMargin) * (buttons.size());
159 }
160 buttonsWidget->setFixedSize(static_cast<int>(hsize), static_cast<int>(vsize));
161
162 // Final style: remove focus rectangles and keep a transparent background.
163 buttonsWidget->setFocusPolicy(Qt::NoFocus);
164 buttonsWidget->setStyleSheet(
165 "QListWidget { background-color: transparent; }"
166 "QListWidget::item { background: transparent; border: none; }"
167 "QListWidget::item:selected { background: transparent; border: none; outline: none; }"
168 );
169
170 refresh_icons();
171}
172
174 // Select the requested row and switch its icon to the "pressed" state.
175 buttonsWidget->setCurrentRow(i);
176 refresh_icons();
177}
178
179void GQTButtonsWidget::buttonWasPressed(QListWidgetItem* item) {
180 buttonsWidget->setCurrentItem(item);
181 refresh_icons();
182}
183
185 // Restore every button icon to the "normal" state (state 1).
186 buttonsWidget->setCurrentRow(-1);
187 refresh_icons();
188}
189
190void GQTButtonsWidget::refresh_icons() {
191 if (!buttonsWidget) { return; }
192
193 const int currentRow = buttonsWidget->currentRow();
194 const QPalette palette = buttonsWidget->palette();
195 const QSize iconSize = buttonsWidget->iconSize();
196
197 for (int row = 0; row < buttonsWidget->count(); row++) {
198 const int state = row == currentRow ? 2 : 1;
199 buttonsWidget->item(row)->setIcon(buttons[row]->iconForState(state, iconSize, palette));
200 }
201}
202
204 QWidget::changeEvent(event);
205
206 if (event->type() == QEvent::PaletteChange ||
207 event->type() == QEvent::ApplicationPaletteChange ||
208 event->type() == QEvent::StyleChange) {
209 refresh_icons();
210 }
211}
212
214 QWidget::showEvent(event);
215 refresh_icons();
216}
217
218/* --- GQTToggleButtonWidget Implementation ---
219 *
220 * This widget:
221 * - creates one checkable QPushButton per title
222 * - arranges them vertically/horizontally
223 * - tracks the last clicked button index and emits a signal on changes
224 */
225
226GQTToggleButtonWidget::GQTToggleButtonWidget(int buttonWidth, int buttonHeight, int borderRadius,
227 const std::vector<std::string>& titles,
228 bool vertical, QWidget* parent)
229 : QWidget(parent),
230 buttonPressedIndex(-1) {
231 QBoxLayout* layout = vertical
232 ? static_cast<QBoxLayout*>(new QVBoxLayout(this))
233 : static_cast<QBoxLayout*>(new QHBoxLayout(this));
234
235 // Convert std::string titles to QString for QPushButton labels.
236 QStringList buttonStrings;
237 for (const auto& title : titles) {
238 buttonStrings.append(QString::fromStdString(title));
239 }
240
241 // Create a toggleable button for each title.
242 for (int i = 0; i < buttonStrings.size(); ++i) {
243 QPushButton* button = new QPushButton(buttonStrings[i], this);
244 button->setCheckable(true);
245 button->setFixedSize(buttonWidth, buttonHeight);
246
247 // SVG handles all visual states (frame, highlight, text color); no CSS decoration needed.
248 button->setStyleSheet(
249 "QPushButton { border: none; background-color: transparent; padding: 0; margin: 0; }"
250 "QPushButton:checked { background-color: transparent; }"
251 );
252
253 layout->addWidget(button);
254 buttons.push_back(button);
255
256 // Use a lambda to bind each button click to its index.
257 connect(button, &QPushButton::clicked, this, [this, i]() {
258 this->setButtonPressed(i);
259 });
260 connect(button, &QPushButton::toggled, this, [this]() {
261 refresh_svg_icons();
262 });
263 }
264 setLayout(layout);
265}
266
268 // A missing or invalid index is treated as "not checked".
269 if (buttonPressedIndex >= 0 && buttonPressedIndex < buttons.size())
270 return buttons[buttonPressedIndex]->isChecked();
271 return false;
272}
273
275 // Toggle changes checked->unchecked or unchecked->checked.
276 if (index >= 0 && index < buttons.size())
277 buttons[index]->toggle();
278}
279
281 // Query individual button state; invalid index returns false.
282 if (index >= 0 && index < buttons.size())
283 return buttons[index]->isChecked();
284 return false;
285}
286
287void GQTToggleButtonWidget::setButtonPressed(int index) {
288 // Track the last pressed index and notify listeners.
289 buttonPressedIndex = index;
290 emit buttonPressedIndexChanged(buttonPressedIndex);
291}
292
294 // Uncheck all buttons; does not change the stored pressed index.
295 for (auto& b : buttons) {
296 b->setChecked(false);
297 }
298}
299
300void GQTToggleButtonWidget::setSvgButtonIcon(int index, const QString& svgResourcePath, const QSize& iconSize) {
301 if (index < 0 || index >= static_cast<int>(buttons.size())) { return; }
302
303 // Grow the svgIcons table to cover this index.
304 while (svgIcons.size() <= index) { svgIcons.append(SvgIcon{}); }
305
306 const QSize sz = iconSize.isValid() ? iconSize
307 : QSize(buttons[index]->width() - 6, buttons[index]->height() - 6);
308 svgIcons[index] = SvgIcon{ svgResourcePath, sz };
309 refresh_svg_icons();
310}
311
312void GQTToggleButtonWidget::refresh_svg_icons() {
313 const QColor windowText = palette().color(QPalette::WindowText);
314 const QColor hlText = palette().color(QPalette::HighlightedText);
315 const QColor hlBg = palette().color(QPalette::Highlight);
316
317 for (int i = 0; i < svgIcons.size(); ++i) {
318 const SvgIcon& entry = svgIcons[i];
319 if (entry.path.isEmpty() || i >= static_cast<int>(buttons.size())) { continue; }
320
321 QFile f(entry.path);
322 if (!f.open(QIODevice::ReadOnly)) { continue; }
323
324 const bool checked = buttons[i]->isChecked();
325 const QColor fg = checked ? hlText : windowText;
326 const QString bg = checked ? hlBg.name(QColor::HexRgb) : "none";
327
328 QString svg = gemc::translateSvgLabels(QString::fromUtf8(f.readAll()));
329 svg.replace("width=\"48\" height=\"48\"",
330 QString("width=\"%1\" height=\"%2\"").arg(entry.size.width()).arg(entry.size.height()));
331 svg.replace("currentColor", fg.name(QColor::HexRgb));
332 svg.replace("#aaddff", bg, Qt::CaseInsensitive);
333
334 QPixmap pix;
335 if (pix.loadFromData(svg.toUtf8(), "SVG")) {
336 buttons[i]->setIcon(QIcon(pix));
337 buttons[i]->setIconSize(entry.size);
338 }
339 }
340}
341
343 QWidget::changeEvent(event);
344 if (event->type() == QEvent::PaletteChange ||
345 event->type() == QEvent::ApplicationPaletteChange ||
346 event->type() == QEvent::StyleChange) {
347 refresh_svg_icons();
348 }
349}
350
352 QWidget::showEvent(event);
353 refresh_svg_icons();
354}
void changeEvent(QEvent *event) override
void reset_buttons()
Reset all buttons to the "normal" icon state.
void showEvent(QShowEvent *event) override
QListWidget * buttonsWidget
Underlying QListWidget used to render the icons.
void press_button(int i)
Programmatically press a button.
GQTButtonsWidget(double h, double v, const std::vector< std::string > &bicons, bool vertical=true, QWidget *parent=nullptr)
Constructs a GQTButtonsWidget.
void toggleButton(int index)
Toggle the checked state of a button.
void setSvgButtonIcon(int index, const QString &svgResourcePath, const QSize &iconSize=QSize())
Assign an SVG resource to a button so it re-renders on every palette/theme change.
void showEvent(QShowEvent *event) override
GQTToggleButtonWidget(int buttonWidth, int buttonHeight, int borderRadius, const std::vector< std::string > &titles, bool vertical=true, QWidget *parent=nullptr)
Constructs a GQTToggleButtonWidget.
void changeEvent(QEvent *event) override
void reset_buttons()
Uncheck all buttons.
void buttonPressedIndexChanged(int index)
Emitted whenever the last pressed button index changes.
bool buttonStatus(int index) const
Returns the checked state for a specific button.
bool lastButtonState() const
Returns the checked state of the last pressed button.
event
QString translateSvgLabels(QString svg)
Translate SVG toolbar captions and fit their font size without changing stored artwork.
Logical description of one icon-button entry used by GQTButtonsWidget.
QIcon iconForState(int state, const QSize &iconSize, const QPalette &palette) const
Returns the icon for a given interaction state.
QListWidgetItem * thisButton
The UI item representing this logical button.
ButtonInfo(const std::string &icon)
Constructs a ButtonInfo for a given base icon name.