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 | |
15 | QT_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 | |
57 | class QCommandLinkButtonPrivate : public QPushButtonPrivate |
58 | { |
59 | Q_DECLARE_PUBLIC(QCommandLinkButton) |
60 | |
61 | public: |
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] |
90 | QColor 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 | |
101 | QFont 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 | |
121 | QFont 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 | |
136 | QRect 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 | |
150 | QRect 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 | |
157 | int QCommandLinkButtonPrivate::textOffset() const |
158 | { |
159 | Q_Q(const QCommandLinkButton); |
160 | return q->icon().actualSize(size: q->iconSize()).width() + leftMargin() + 6; |
161 | } |
162 | |
163 | int QCommandLinkButtonPrivate::descriptionOffset() const |
164 | { |
165 | QFontMetrics fm(titleFont()); |
166 | return topMargin() + fm.height(); |
167 | } |
168 | |
169 | bool 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 | |
178 | void 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 |
196 | int 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 | */ |
222 | QSize 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 | |
232 | void 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 | |
242 | QCommandLinkButton::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 | |
254 | QCommandLinkButton::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 | */ |
263 | QCommandLinkButton::QCommandLinkButton(const QString &text, const QString &description, QWidget *parent) |
264 | : QCommandLinkButton(text, parent) |
265 | { |
266 | setDescription(description); |
267 | } |
268 | |
269 | /*! |
270 | Destructor. |
271 | */ |
272 | QCommandLinkButton::~QCommandLinkButton() |
273 | { |
274 | } |
275 | |
276 | /*! \reimp */ |
277 | bool QCommandLinkButton::event(QEvent *e) |
278 | { |
279 | return QPushButton::event(e); |
280 | } |
281 | |
282 | /*! \reimp */ |
283 | QSize 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 */ |
303 | int 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 */ |
314 | void 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 | |
366 | void QCommandLinkButton::setDescription(const QString &description) |
367 | { |
368 | Q_D(QCommandLinkButton); |
369 | d->description = description; |
370 | updateGeometry(); |
371 | update(); |
372 | } |
373 | |
374 | QString QCommandLinkButton::description() const |
375 | { |
376 | Q_D(const QCommandLinkButton); |
377 | return d->description; |
378 | } |
379 | |
380 | QT_END_NAMESPACE |
381 | |
382 | #include "moc_qcommandlinkbutton.cpp" |
383 | |