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:
QCommandLinkButtonPrivate()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 
topMargin() const116     int topMargin() const { return 10; }
leftMargin() const117     int leftMargin() const { return 7; }
rightMargin() const118     int rightMargin() const { return 4; }
bottomMargin() const119     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]
mergedColors(const QColor & a,const QColor & b,int value=50) const126 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 
titleFont() const137 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 
descriptionFont() const157 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 
titleRect() const172 QRect QCommandLinkButtonPrivate::titleRect() const
173 {
174     Q_Q(const QCommandLinkButton);
175     QRect r = q->rect().adjusted(textOffset(), topMargin(), -rightMargin(), 0);
176     if (description.isEmpty())
177     {
178         QFontMetrics fm(titleFont());
179         r.setTop(r.top() + qMax(0, (q->icon().actualSize(q->iconSize()).height()
180                  - fm.height()) / 2));
181     }
182 
183     return r;
184 }
185 
descriptionRect() const186 QRect QCommandLinkButtonPrivate::descriptionRect() const
187 {
188     Q_Q(const QCommandLinkButton);
189     return q->rect().adjusted(textOffset(), descriptionOffset(),
190                               -rightMargin(), -bottomMargin());
191 }
192 
textOffset() const193 int QCommandLinkButtonPrivate::textOffset() const
194 {
195     Q_Q(const QCommandLinkButton);
196     return q->icon().actualSize(q->iconSize()).width() + leftMargin() + 6;
197 }
198 
descriptionOffset() const199 int QCommandLinkButtonPrivate::descriptionOffset() const
200 {
201     QFontMetrics fm(titleFont());
202     return topMargin() + fm.height();
203 }
204 
usingVistaStyle() const205 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("QWindowsVistaStyle")
211         && q->style()->pixelMetric(QStyle::PM_ButtonShiftHorizontal, nullptr) == 0;
212 }
213 
init()214 void QCommandLinkButtonPrivate::init()
215 {
216     Q_Q(QCommandLinkButton);
217     QPushButtonPrivate::init();
218     q->setAttribute(Qt::WA_Hover);
219 
220     QSizePolicy policy(QSizePolicy::Preferred, QSizePolicy::Preferred, QSizePolicy::PushButton);
221     policy.setHeightForWidth(true);
222     q->setSizePolicy(policy);
223 
224     q->setIconSize(QSize(20, 20));
225     QStyleOptionButton opt;
226     q->initStyleOption(&opt);
227     q->setIcon(q->style()->standardIcon(QStyle::SP_CommandLink, &opt));
228 }
229 
230 // Calculates the height of the description text based on widget width
descriptionHeight(int widgetWidth) const231 int QCommandLinkButtonPrivate::descriptionHeight(int widgetWidth) const
232 {
233     // Calc width of actual paragraph
234     int lineWidth = widgetWidth - textOffset() - rightMargin();
235 
236     qreal descriptionheight = 0;
237     if (!description.isEmpty()) {
238         QTextLayout layout(description);
239         layout.setFont(descriptionFont());
240         layout.beginLayout();
241         while (true) {
242             QTextLine line = layout.createLine();
243             if (!line.isValid())
244                 break;
245             line.setLineWidth(lineWidth);
246             line.setPosition(QPointF(0, descriptionheight));
247             descriptionheight += line.height();
248         }
249         layout.endLayout();
250     }
251     return qCeil(descriptionheight);
252 }
253 
254 /*!
255     \reimp
256  */
minimumSizeHint() const257 QSize QCommandLinkButton::minimumSizeHint() const
258 {
259     Q_D(const QCommandLinkButton);
260     QSize size = sizeHint();
261     int minimumHeight = qMax(d->descriptionOffset() + d->bottomMargin(),
262                              icon().actualSize(iconSize()).height() + d->topMargin());
263     size.setHeight(minimumHeight);
264     return size;
265 }
266 
267 /*!
268     Constructs a command link with no text and a \a parent.
269 */
270 
QCommandLinkButton(QWidget * parent)271 QCommandLinkButton::QCommandLinkButton(QWidget *parent)
272 : QPushButton(*new QCommandLinkButtonPrivate, parent)
273 {
274     Q_D(QCommandLinkButton);
275     d->init();
276 }
277 
278 /*!
279     Constructs a command link with the parent \a parent and the text \a
280     text.
281 */
282 
QCommandLinkButton(const QString & text,QWidget * parent)283 QCommandLinkButton::QCommandLinkButton(const QString &text, QWidget *parent)
284     : QCommandLinkButton(parent)
285 {
286     setText(text);
287 }
288 
289 /*!
290     Constructs a command link with a \a text, a \a description, and a \a parent.
291 */
QCommandLinkButton(const QString & text,const QString & description,QWidget * parent)292 QCommandLinkButton::QCommandLinkButton(const QString &text, const QString &description, QWidget *parent)
293     : QCommandLinkButton(text, parent)
294 {
295     setDescription(description);
296 }
297 
298 /*!
299     Destructor.
300 */
~QCommandLinkButton()301 QCommandLinkButton::~QCommandLinkButton()
302 {
303 }
304 
305 /*! \reimp */
event(QEvent * e)306 bool QCommandLinkButton::event(QEvent *e)
307 {
308     return QPushButton::event(e);
309 }
310 
311 /*! \reimp */
sizeHint() const312 QSize QCommandLinkButton::sizeHint() const
313 {
314 //  Standard size hints from UI specs
315 //  Without note: 135, 41
316 //  With note: 135, 60
317     Q_D(const QCommandLinkButton);
318 
319     QSize size = QPushButton::sizeHint();
320     QFontMetrics fm(d->titleFont());
321     int textWidth = qMax(fm.horizontalAdvance(text()), 135);
322     int buttonWidth = textWidth + d->textOffset() + d->rightMargin();
323     int heightWithoutDescription = d->descriptionOffset() + d->bottomMargin();
324 
325     size.setWidth(qMax(size.width(), buttonWidth));
326     size.setHeight(qMax(d->description.isEmpty() ? 41 : 60,
327                         heightWithoutDescription + d->descriptionHeight(buttonWidth)));
328     return size;
329 }
330 
331 /*! \reimp */
heightForWidth(int width) const332 int QCommandLinkButton::heightForWidth(int width) const
333 {
334     Q_D(const QCommandLinkButton);
335     int heightWithoutDescription = d->descriptionOffset() + d->bottomMargin();
336     // find the width available for the description area
337     return qMax(heightWithoutDescription + d->descriptionHeight(width),
338                 icon().actualSize(iconSize()).height() + d->topMargin() +
339                 d->bottomMargin());
340 }
341 
342 /*! \reimp */
paintEvent(QPaintEvent *)343 void QCommandLinkButton::paintEvent(QPaintEvent *)
344 {
345     Q_D(QCommandLinkButton);
346     QStylePainter p(this);
347     p.save();
348 
349     QStyleOptionButton option;
350     initStyleOption(&option);
351 
352     //Enable command link appearance on Vista
353     option.features |= QStyleOptionButton::CommandLinkButton;
354     option.text = QString();
355     option.icon = QIcon(); //we draw this ourselves
356     QSize pixmapSize = icon().actualSize(iconSize());
357 
358     const int vOffset = isDown()
359         ? style()->pixelMetric(QStyle::PM_ButtonShiftVertical, &option) : 0;
360     const int hOffset = isDown()
361         ? style()->pixelMetric(QStyle::PM_ButtonShiftHorizontal, &option) : 0;
362 
363     //Draw icon
364     p.drawControl(QStyle::CE_PushButton, option);
365     if (!icon().isNull())
366         p.drawPixmap(d->leftMargin() + hOffset, d->topMargin() + vOffset,
367         icon().pixmap(pixmapSize, isEnabled() ? QIcon::Normal : QIcon::Disabled,
368                                   isChecked() ? QIcon::On : QIcon::Off));
369 
370     //Draw title
371     QColor textColor = palette().buttonText().color();
372     if (isEnabled() && d->usingVistaStyle()) {
373         textColor = QColor(21, 28, 85);
374         if (underMouse() && !isDown())
375             textColor = QColor(7, 64, 229);
376         //A simple text color transition
377         d->currentColor = d->mergedColors(textColor, d->currentColor, 60);
378         option.palette.setColor(QPalette::ButtonText, d->currentColor);
379     }
380 
381     int textflags = Qt::TextShowMnemonic;
382     if (!style()->styleHint(QStyle::SH_UnderlineShortcut, &option, this))
383         textflags |= Qt::TextHideMnemonic;
384 
385     p.setFont(d->titleFont());
386     p.drawItemText(d->titleRect().translated(hOffset, vOffset),
387                     textflags, option.palette, isEnabled(), text(), QPalette::ButtonText);
388 
389     //Draw description
390     textflags |= Qt::TextWordWrap | Qt::ElideRight;
391     p.setFont(d->descriptionFont());
392     p.drawItemText(d->descriptionRect().translated(hOffset, vOffset), textflags,
393                     option.palette, isEnabled(), description(), QPalette::ButtonText);
394     p.restore();
395 }
396 
setDescription(const QString & description)397 void QCommandLinkButton::setDescription(const QString &description)
398 {
399     Q_D(QCommandLinkButton);
400     d->description = description;
401     updateGeometry();
402     update();
403 }
404 
description() const405 QString QCommandLinkButton::description() const
406 {
407     Q_D(const QCommandLinkButton);
408     return d->description;
409 }
410 
411 QT_END_NAMESPACE
412 
413 #include "moc_qcommandlinkbutton.cpp"
414