1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qcommandlinkbutton.h"
5#include "qstylepainter.h"
6#include "qstyleoption.h"
7#include "qtextdocument.h"
8#include "qtextlayout.h"
9#include "qcolor.h"
10#include "qfont.h"
11#include <qmath.h>
12
13#include "private/qpushbutton_p.h"
14
15QT_BEGIN_NAMESPACE
16
17/*!
18 \class QCommandLinkButton
19 \since 4.4
20 \brief The QCommandLinkButton widget provides a Vista style command link button.
21
22 \ingroup basicwidgets
23 \inmodule QtWidgets
24
25 The command link is a new control that was introduced by Windows Vista. Its
26 intended use is similar to that of a radio button in that it is used to choose
27 between a set of mutually exclusive options. Command link buttons should not
28 be used by themselves but rather as an alternative to radio buttons in
29 Wizards and dialogs and makes pressing the "next" button redundant.
30 The appearance is generally similar to that of a flat pushbutton, but
31 it allows for a descriptive text in addition to the normal button text.
32 By default it will also carry an arrow icon, indicating that pressing the
33 control will open another window or page.
34
35 \sa QPushButton, QRadioButton
36*/
37
38/*!
39 \property QCommandLinkButton::description
40 \brief A descriptive label to complement the button text
41
42 Setting this property will set a descriptive text on the
43 button, complementing the text label. This will usually
44 be displayed in a smaller font than the primary text.
45*/
46
47/*!
48 \property QCommandLinkButton::flat
49 \brief This property determines whether the button is displayed as a flat
50 panel or with a border.
51
52 By default, this property is set to false.
53
54 \sa QPushButton::flat
55*/
56
57class QCommandLinkButtonPrivate : public QPushButtonPrivate
58{
59 Q_DECLARE_PUBLIC(QCommandLinkButton)
60
61public:
62 QCommandLinkButtonPrivate()
63 : QPushButtonPrivate(){}
64
65 void init();
66 qreal titleSize() const;
67 bool usingVistaStyle() const;
68
69 QFont titleFont() const;
70 QFont descriptionFont() const;
71
72 QRect titleRect() const;
73 QRect descriptionRect() const;
74
75 int textOffset() const;
76 int descriptionOffset() const;
77 int descriptionHeight(int width) const;
78 QColor mergedColors(const QColor &a, const QColor &b, int value) const;
79
80 int topMargin() const { return 10; }
81 int leftMargin() const { return 7; }
82 int rightMargin() const { return 4; }
83 int bottomMargin() const { return 10; }
84
85 QString description;
86 QColor currentColor;
87};
88
89// Mix colors a and b with a ratio in the range [0-255]
90QColor QCommandLinkButtonPrivate::mergedColors(const QColor &a, const QColor &b, int value = 50) const
91{
92 Q_ASSERT(value >= 0);
93 Q_ASSERT(value <= 255);
94 QColor tmp = a;
95 tmp.setRed((tmp.red() * value) / 255 + (b.red() * (255 - value)) / 255);
96 tmp.setGreen((tmp.green() * value) / 255 + (b.green() * (255 - value)) / 255);
97 tmp.setBlue((tmp.blue() * value) / 255 + (b.blue() * (255 - value)) / 255);
98 return tmp;
99}
100
101QFont QCommandLinkButtonPrivate::titleFont() const
102{
103 Q_Q(const QCommandLinkButton);
104 QFont font = q->font();
105 if (usingVistaStyle()) {
106 font.setPointSizeF(12.0);
107 } else {
108 font.setBold(true);
109 font.setPointSizeF(9.0);
110 }
111
112 // Note the font will be resolved against
113 // QPainters font, so we need to restore the mask
114 int resolve_mask = font.resolve_mask;
115 QFont modifiedFont = q->font().resolve(font);
116 modifiedFont.detach();
117 modifiedFont.resolve_mask = resolve_mask;
118 return modifiedFont;
119}
120
121QFont QCommandLinkButtonPrivate::descriptionFont() const
122{
123 Q_Q(const QCommandLinkButton);
124 QFont font = q->font();
125 font.setPointSizeF(9.0);
126
127 // Note the font will be resolved against
128 // QPainters font, so we need to restore the mask
129 int resolve_mask = font.resolve_mask;
130 QFont modifiedFont = q->font().resolve(font);
131 modifiedFont.detach();
132 modifiedFont.resolve_mask = resolve_mask;
133 return modifiedFont;
134}
135
136QRect QCommandLinkButtonPrivate::titleRect() const
137{
138 Q_Q(const QCommandLinkButton);
139 QRect r = q->rect().adjusted(xp1: textOffset(), yp1: topMargin(), xp2: -rightMargin(), yp2: 0);
140 if (description.isEmpty())
141 {
142 QFontMetrics fm(titleFont());
143 r.setTop(r.top() + qMax(a: 0, b: (q->icon().actualSize(size: q->iconSize()).height()
144 - fm.height()) / 2));
145 }
146
147 return r;
148}
149
150QRect QCommandLinkButtonPrivate::descriptionRect() const
151{
152 Q_Q(const QCommandLinkButton);
153 return q->rect().adjusted(xp1: textOffset(), yp1: descriptionOffset(),
154 xp2: -rightMargin(), yp2: -bottomMargin());
155}
156
157int QCommandLinkButtonPrivate::textOffset() const
158{
159 Q_Q(const QCommandLinkButton);
160 return q->icon().actualSize(size: q->iconSize()).width() + leftMargin() + 6;
161}
162
163int QCommandLinkButtonPrivate::descriptionOffset() const
164{
165 QFontMetrics fm(titleFont());
166 return topMargin() + fm.height();
167}
168
169bool QCommandLinkButtonPrivate::usingVistaStyle() const
170{
171 Q_Q(const QCommandLinkButton);
172 //### This is a hack to detect if we are indeed running Vista style themed and not in classic
173 // When we add api to query for this, we should change this implementation to use it.
174 return q->property(name: "_qt_usingVistaStyle").toBool()
175 && q->style()->pixelMetric(metric: QStyle::PM_ButtonShiftHorizontal, option: nullptr) == 0;
176}
177
178void QCommandLinkButtonPrivate::init()
179{
180 Q_Q(QCommandLinkButton);
181 QPushButtonPrivate::init();
182 q->setAttribute(Qt::WA_Hover);
183 q->setAttribute(Qt::WA_MacShowFocusRect, on: false);
184
185 QSizePolicy policy(QSizePolicy::Preferred, QSizePolicy::Preferred, QSizePolicy::PushButton);
186 policy.setHeightForWidth(true);
187 q->setSizePolicy(policy);
188
189 q->setIconSize(QSize(20, 20));
190 QStyleOptionButton opt;
191 q->initStyleOption(option: &opt);
192 q->setIcon(q->style()->standardIcon(standardIcon: QStyle::SP_CommandLink, option: &opt));
193}
194
195// Calculates the height of the description text based on widget width
196int QCommandLinkButtonPrivate::descriptionHeight(int widgetWidth) const
197{
198 // Calc width of actual paragraph
199 int lineWidth = widgetWidth - textOffset() - rightMargin();
200
201 qreal descriptionheight = 0;
202 if (!description.isEmpty()) {
203 QTextLayout layout(description);
204 layout.setFont(descriptionFont());
205 layout.beginLayout();
206 while (true) {
207 QTextLine line = layout.createLine();
208 if (!line.isValid())
209 break;
210 line.setLineWidth(lineWidth);
211 line.setPosition(QPointF(0, descriptionheight));
212 descriptionheight += line.height();
213 }
214 layout.endLayout();
215 }
216 return qCeil(v: descriptionheight);
217}
218
219/*!
220 \reimp
221 */
222QSize QCommandLinkButton::minimumSizeHint() const
223{
224 Q_D(const QCommandLinkButton);
225 QSize size = sizeHint();
226 int minimumHeight = qMax(a: d->descriptionOffset() + d->bottomMargin(),
227 b: icon().actualSize(size: iconSize()).height() + d->topMargin());
228 size.setHeight(minimumHeight);
229 return size;
230}
231
232void QCommandLinkButton::initStyleOption(QStyleOptionButton *option) const
233{
234 QPushButton::initStyleOption(option);
235 option->features |= QStyleOptionButton::CommandLinkButton;
236}
237
238/*!
239 Constructs a command link with no text and a \a parent.
240*/
241
242QCommandLinkButton::QCommandLinkButton(QWidget *parent)
243: QPushButton(*new QCommandLinkButtonPrivate, parent)
244{
245 Q_D(QCommandLinkButton);
246 d->init();
247}
248
249/*!
250 Constructs a command link with the parent \a parent and the text \a
251 text.
252*/
253
254QCommandLinkButton::QCommandLinkButton(const QString &text, QWidget *parent)
255 : QCommandLinkButton(parent)
256{
257 setText(text);
258}
259
260/*!
261 Constructs a command link with a \a text, a \a description, and a \a parent.
262*/
263QCommandLinkButton::QCommandLinkButton(const QString &text, const QString &description, QWidget *parent)
264 : QCommandLinkButton(text, parent)
265{
266 setDescription(description);
267}
268
269/*!
270 Destructor.
271*/
272QCommandLinkButton::~QCommandLinkButton()
273{
274}
275
276/*! \reimp */
277bool QCommandLinkButton::event(QEvent *e)
278{
279 return QPushButton::event(e);
280}
281
282/*! \reimp */
283QSize QCommandLinkButton::sizeHint() const
284{
285// Standard size hints from UI specs
286// Without note: 135, 41
287// With note: 135, 60
288 Q_D(const QCommandLinkButton);
289
290 QSize size = QPushButton::sizeHint();
291 QFontMetrics fm(d->titleFont());
292 int textWidth = qMax(a: fm.horizontalAdvance(text()), b: 135);
293 int buttonWidth = textWidth + d->textOffset() + d->rightMargin();
294 int heightWithoutDescription = d->descriptionOffset() + d->bottomMargin();
295
296 size.setWidth(qMax(a: size.width(), b: buttonWidth));
297 size.setHeight(qMax(a: d->description.isEmpty() ? 41 : 60,
298 b: heightWithoutDescription + d->descriptionHeight(widgetWidth: buttonWidth)));
299 return size;
300}
301
302/*! \reimp */
303int QCommandLinkButton::heightForWidth(int width) const
304{
305 Q_D(const QCommandLinkButton);
306 int heightWithoutDescription = d->descriptionOffset() + d->bottomMargin();
307 // find the width available for the description area
308 return qMax(a: heightWithoutDescription + d->descriptionHeight(widgetWidth: width),
309 b: icon().actualSize(size: iconSize()).height() + d->topMargin() +
310 d->bottomMargin());
311}
312
313/*! \reimp */
314void QCommandLinkButton::paintEvent(QPaintEvent *)
315{
316 Q_D(QCommandLinkButton);
317 QStylePainter p(this);
318 p.save();
319
320 QStyleOptionButton option;
321 initStyleOption(option: &option);
322
323 option.text = QString();
324 option.icon = QIcon(); //we draw this ourselves
325 QSize pixmapSize = icon().actualSize(size: iconSize());
326
327 const int vOffset = isDown()
328 ? style()->pixelMetric(metric: QStyle::PM_ButtonShiftVertical, option: &option) : 0;
329 const int hOffset = isDown()
330 ? style()->pixelMetric(metric: QStyle::PM_ButtonShiftHorizontal, option: &option) : 0;
331
332 //Draw icon
333 p.drawControl(ce: QStyle::CE_PushButton, opt: option);
334 if (!icon().isNull())
335 p.drawPixmap(x: d->leftMargin() + hOffset, y: d->topMargin() + vOffset,
336 pm: icon().pixmap(size: pixmapSize, mode: isEnabled() ? QIcon::Normal : QIcon::Disabled,
337 state: isChecked() ? QIcon::On : QIcon::Off));
338
339 //Draw title
340 QColor textColor = palette().buttonText().color();
341 if (isEnabled() && d->usingVistaStyle()) {
342 textColor = option.palette.buttonText().color();
343 if (underMouse() && !isDown())
344 textColor = option.palette.brightText().color();
345 //A simple text color transition
346 d->currentColor = d->mergedColors(a: textColor, b: d->currentColor, value: 60);
347 option.palette.setColor(acr: QPalette::ButtonText, acolor: d->currentColor);
348 }
349
350 int textflags = Qt::TextShowMnemonic;
351 if (!style()->styleHint(stylehint: QStyle::SH_UnderlineShortcut, opt: &option, widget: this))
352 textflags |= Qt::TextHideMnemonic;
353
354 p.setFont(d->titleFont());
355 p.drawItemText(r: d->titleRect().translated(dx: hOffset, dy: vOffset),
356 flags: textflags, pal: option.palette, enabled: isEnabled(), text: text(), textRole: QPalette::ButtonText);
357
358 //Draw description
359 textflags |= Qt::TextWordWrap | Qt::ElideRight;
360 p.setFont(d->descriptionFont());
361 p.drawItemText(r: d->descriptionRect().translated(dx: hOffset, dy: vOffset), flags: textflags,
362 pal: option.palette, enabled: isEnabled(), text: description(), textRole: QPalette::ButtonText);
363 p.restore();
364}
365
366void QCommandLinkButton::setDescription(const QString &description)
367{
368 Q_D(QCommandLinkButton);
369 d->description = description;
370 updateGeometry();
371 update();
372}
373
374QString QCommandLinkButton::description() const
375{
376 Q_D(const QCommandLinkButton);
377 return d->description;
378}
379
380QT_END_NAMESPACE
381
382#include "moc_qcommandlinkbutton.cpp"
383

source code of qtbase/src/widgets/widgets/qcommandlinkbutton.cpp