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