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

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