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