File indexing completed on 2024-05-12 16:39:55

0001 /* This file is part of the KDE project
0002    Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
0003    Copyright (C) 2011 Jarosław Staniek <staniek@kde.org>
0004 
0005    This library is free software; you can redistribute it and/or
0006    modify it under the terms of the GNU Library General Public
0007    License as published by the Free Software Foundation; either
0008    version 2 of the License, or (at your option) any later version.
0009 
0010    This library is distributed in the hope that it will be useful,
0011    but WITHOUT ANY WARRANTY; without even the implied warranty of
0012    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0013    Library General Public License for more details.
0014 
0015    You should have received a copy of the GNU Library General Public License
0016    along with this library; see the file COPYING.LIB.  If not, write to
0017    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0018  * Boston, MA 02110-1301, USA.
0019 */
0020 
0021 #include "KexiCommandLinkButton.h"
0022 #include <QStylePainter>
0023 #include <QStyleOption>
0024 #include <QTextLayout>
0025 #include <QColor>
0026 #include <QFont>
0027 #include <qmath.h>
0028 
0029 /*!
0030     \class KexiCommandLinkButton
0031     \brief The KexiCommandLinkButton widget provides a Vista style command link button.
0032 
0033     The command link is a new control that was introduced by Windows Vista. It's
0034     intended use is similar to that of a radio button in that it is used to choose
0035     between a set of mutually exclusive options. Command link buttons should not
0036     be used by themselves but rather as an alternative to radio buttons in
0037     Wizards and dialogs and makes pressing the "next" button redundant.
0038     The appearance is generally similar to that of a flat pushbutton, but
0039     it allows for a descriptive text in addition to the normal button text.
0040     By default it will also carry an arrow icon, indicating that pressing the
0041     control will open another window or page.
0042 
0043     \sa QPushButton QRadioButton
0044 */
0045 
0046 /*!
0047     \property KexiCommandLinkButton::description
0048     \brief A descriptive label to complement the button text
0049 
0050     Setting this property will set a descriptive text on the
0051     button, complementing the text label. This will usually
0052     be displayed in a smaller font than the primary text.
0053 */
0054 
0055 /*!
0056     \property KexiCommandLinkButton::flat
0057     \brief This property determines whether the button is displayed as a flat
0058     panel or with a border.
0059 
0060     By default, this property is set to false.
0061 
0062     \sa QPushButton::flat
0063 */
0064 
0065 class KexiCommandLinkButtonPrivate
0066 {
0067 public:
0068     explicit KexiCommandLinkButtonPrivate(KexiCommandLinkButton *qq)
0069      : isArrowVisible(false), q(qq) {}
0070 
0071     void init();
0072     qreal titleSize() const;
0073     bool usingVistaStyle() const;
0074 
0075     QFont titleFont() const;
0076     QFont descriptionFont() const;
0077 
0078     QRect titleRect() const;
0079     QRect descriptionRect() const;
0080 
0081     int textOffset() const;
0082     int descriptionOffset() const;
0083     int descriptionHeight(int width) const;
0084     QColor mergedColors(const QColor &a, const QColor &b, int value) const;
0085 
0086     int topMargin() const { return 10; }
0087     int leftMargin() const { return 7; }
0088     int rightMargin() const { return 4; }
0089     int bottomMargin() const { return 10; }
0090 
0091     QString description;
0092     QColor currentColor;
0093     bool isArrowVisible;
0094     KexiCommandLinkButton *q;
0095 };
0096 
0097 // Mix colors a and b with a ratio in the range [0-255]
0098 QColor KexiCommandLinkButtonPrivate::mergedColors(const QColor &a, const QColor &b, int value = 50) const
0099 {
0100     Q_ASSERT(value >= 0);
0101     Q_ASSERT(value <= 255);
0102     QColor tmp = a;
0103     tmp.setRed((tmp.red() * value) / 255 + (b.red() * (255 - value)) / 255);
0104     tmp.setGreen((tmp.green() * value) / 255 + (b.green() * (255 - value)) / 255);
0105     tmp.setBlue((tmp.blue() * value) / 255 + (b.blue() * (255 - value)) / 255);
0106     return tmp;
0107 }
0108 
0109 QFont KexiCommandLinkButtonPrivate::titleFont() const
0110 {
0111     QFont font = q->font();
0112     if (usingVistaStyle()) {
0113         font.setPointSizeF(font.pointSizeF() + 1);
0114     } else {
0115         font.setBold(true);
0116     }
0117 
0118     // Note the font will be resolved against
0119     // QPainters font, so we need to restore the mask
0120     int resolve_mask = font.resolve();
0121     QFont modifiedFont = q->font().resolve(font);
0122     modifiedFont.resolve(resolve_mask);
0123     return modifiedFont;
0124 }
0125 
0126 QFont KexiCommandLinkButtonPrivate::descriptionFont() const
0127 {
0128     return q->font();
0129 }
0130 
0131 QRect KexiCommandLinkButtonPrivate::titleRect() const
0132 {
0133     QRect r = q->rect().adjusted(textOffset(), topMargin(), -rightMargin(), 0);
0134     QFontMetrics fm(titleFont());
0135     r.setHeight(fm.height());
0136     if (description.isEmpty())
0137     {
0138         r.setTop(r.top() + qMax(0, (q->icon().actualSize(q->iconSize()).height()
0139                  - fm.height()) / 2));
0140     }
0141 
0142     return r;
0143 }
0144 
0145 QRect KexiCommandLinkButtonPrivate::descriptionRect() const
0146 {
0147     return q->rect().adjusted(textOffset(), descriptionOffset(),
0148                               -rightMargin(), -bottomMargin());
0149 }
0150 
0151 int KexiCommandLinkButtonPrivate::textOffset() const
0152 {
0153     return q->icon().actualSize(q->iconSize()).width() + leftMargin() + 6;
0154 }
0155 
0156 int KexiCommandLinkButtonPrivate::descriptionOffset() const
0157 {
0158     QFontMetrics fm(descriptionFont());
0159     return topMargin() + fm.height();
0160 }
0161 
0162 bool KexiCommandLinkButtonPrivate::usingVistaStyle() const
0163 {
0164     //### This is a hack to detect if we are indeed running Vista style themed and not in classic
0165     // When we add api to query for this, we should change this implementation to use it.
0166     return q->style()->inherits("QWindowsVistaStyle")
0167         && !q->style()->pixelMetric(QStyle::PM_ButtonShiftHorizontal);
0168 }
0169 
0170 void KexiCommandLinkButtonPrivate::init()
0171 {
0172     q->setAttribute(Qt::WA_Hover);
0173 
0174     QSizePolicy policy(QSizePolicy::Preferred, QSizePolicy::Preferred, QSizePolicy::PushButton);
0175     policy.setHeightForWidth(true);
0176     q->setSizePolicy(policy);
0177 
0178     q->setIconSize(QSize(20, 20));
0179     QStyleOptionButton opt;
0180     q->initStyleOption(&opt);
0181     q->setIcon(q->style()->standardIcon(QStyle::SP_CommandLink, &opt));
0182 }
0183 
0184 // Calculates the height of the description text based on widget width
0185 int KexiCommandLinkButtonPrivate::descriptionHeight(int widgetWidth) const
0186 {
0187     // Calc width of actual paragraph
0188     int lineWidth = widgetWidth - textOffset() - rightMargin();
0189 
0190     qreal descriptionheight = 0;
0191     if (!description.isEmpty()) {
0192         QTextLayout layout(description);
0193         layout.setFont(descriptionFont());
0194         layout.beginLayout();
0195         while (true) {
0196             QTextLine line = layout.createLine();
0197             if (!line.isValid())
0198                 break;
0199             line.setLineWidth(lineWidth);
0200             line.setPosition(QPointF(0, descriptionheight));
0201             descriptionheight += line.height();
0202         }
0203         layout.endLayout();
0204     }
0205     return qCeil(descriptionheight);
0206 }
0207 
0208 /*!
0209     \reimp
0210  */
0211 QSize KexiCommandLinkButton::minimumSizeHint() const
0212 {
0213     QSize size = sizeHint();
0214     int minimumHeight = qMax(d->descriptionOffset() + d->bottomMargin(),
0215                              icon().actualSize(iconSize()).height() + d->topMargin());
0216     size.setHeight(minimumHeight);
0217     return size;
0218 }
0219 
0220 /*!
0221     Constructs a command link with no text and a \a parent.
0222 */
0223 
0224 KexiCommandLinkButton::KexiCommandLinkButton(QWidget *parent)
0225     : KexiPushButton(parent), d(new KexiCommandLinkButtonPrivate(this))
0226 {
0227     d->init();
0228 }
0229 
0230 /*!
0231     Constructs a command link with the parent \a parent and the text \a
0232     text.
0233 */
0234 
0235 KexiCommandLinkButton::KexiCommandLinkButton(const QString &text, QWidget *parent)
0236     : KexiPushButton(parent), d(new KexiCommandLinkButtonPrivate(this))
0237 {
0238     setText(text);
0239     d->init();
0240 }
0241 
0242 /*!
0243     Constructs a command link with a \a text, a \a description, and a \a parent.
0244 */
0245 KexiCommandLinkButton::KexiCommandLinkButton(const QString &text, const QString &description, QWidget *parent)
0246     : KexiPushButton(parent), d(new KexiCommandLinkButtonPrivate(this))
0247 {
0248     setText(text);
0249     setDescription(description);
0250     d->init();
0251 }
0252 
0253 KexiCommandLinkButton::~KexiCommandLinkButton()
0254 {
0255     delete d;
0256 }
0257 
0258 /*! \reimp */
0259 bool KexiCommandLinkButton::event(QEvent *e)
0260 {
0261     return QPushButton::event(e);
0262 }
0263 
0264 /*! \reimp */
0265 QSize KexiCommandLinkButton::sizeHint() const
0266 {
0267 //  Standard size hints from UI specs
0268 //  Without note: 135, 41
0269 //  With note: 135, 60
0270 
0271     QSize size = QPushButton::sizeHint();
0272     QFontMetrics fm(d->titleFont());
0273     int textWidth = qMax(fm.width(text()), 135);
0274     int buttonWidth = textWidth + d->textOffset() + d->rightMargin();
0275     int heightWithoutDescription = d->descriptionOffset() + d->bottomMargin();
0276 
0277     size.setWidth(qMax(size.width(), buttonWidth));
0278     size.setHeight(qMax(d->description.isEmpty() ? 41 : 60,
0279                         heightWithoutDescription + d->descriptionHeight(buttonWidth)));
0280     return size;
0281 }
0282 
0283 /*! \reimp */
0284 int KexiCommandLinkButton::heightForWidth(int width) const
0285 {
0286     int heightWithoutDescription = d->descriptionOffset() + d->bottomMargin();
0287     // find the width available for the description area
0288     return qMax(heightWithoutDescription + d->descriptionHeight(width),
0289                 icon().actualSize(iconSize()).height() + d->topMargin() +
0290                 d->bottomMargin());
0291 }
0292 
0293 /*! \reimp */
0294 void KexiCommandLinkButton::paintEvent(QPaintEvent *)
0295 {
0296     QStylePainter p(this);
0297     p.save();
0298 
0299     QStyleOptionButton option;
0300     initStyleOption(&option);
0301 
0302     //Enable command link appearance on Vista
0303     option.features |= QStyleOptionButton::CommandLinkButton;
0304     option.text.clear();
0305     option.icon = QIcon(); //we draw this ourselves
0306     QSize pixmapSize = icon().actualSize(iconSize());
0307 
0308     int vOffset = isDown() ? style()->pixelMetric(QStyle::PM_ButtonShiftVertical) : 0;
0309     int hOffset = isDown() ? style()->pixelMetric(QStyle::PM_ButtonShiftHorizontal) : 0;
0310 
0311     //Draw icon
0312     p.drawControl(QStyle::CE_PushButton, option);
0313     if (!icon().isNull())
0314         p.drawPixmap(d->leftMargin() + hOffset, d->topMargin() + vOffset,
0315         icon().pixmap(pixmapSize, isEnabled() ? QIcon::Normal : QIcon::Disabled,
0316                                   isChecked() ? QIcon::On : QIcon::Off));
0317 
0318     //Draw title
0319     QColor textColor = palette().buttonText().color();
0320     if (isEnabled() && d->usingVistaStyle()) {
0321         textColor = QColor(21, 28, 85);
0322         if (underMouse() && !isDown())
0323             textColor = QColor(7, 64, 229);
0324         //A simple text color transition
0325         d->currentColor = d->mergedColors(textColor, d->currentColor, 60);
0326         option.palette.setColor(QPalette::ButtonText, d->currentColor);
0327     }
0328     int arrowWidth = d->isArrowVisible ? 12 : 0;
0329     int textflags = Qt::TextShowMnemonic | Qt::AlignTop;
0330     if (!style()->styleHint(QStyle::SH_UnderlineShortcut, &option, this))
0331         textflags |= Qt::TextHideMnemonic;
0332 
0333     p.setFont(d->titleFont());
0334     p.drawItemText(d->titleRect().adjusted(0, 0, -arrowWidth, 0).translated(hOffset, vOffset),
0335                     textflags, option.palette, isEnabled(), text(), QPalette::ButtonText);
0336 
0337     //Draw description
0338     textflags = Qt::TextWordWrap | Qt::ElideRight | Qt::AlignTop;
0339     p.setFont(d->descriptionFont());
0340     p.drawItemText(d->descriptionRect().adjusted(0, 0, -arrowWidth, 0).translated(hOffset, vOffset), textflags,
0341                     option.palette, isEnabled(), description(), QPalette::ButtonText);
0342 
0343     //Optional arrow
0344     if (d->isArrowVisible) {
0345         const int margin = style()->pixelMetric(QStyle::PM_ButtonMargin, &option, this);
0346         option.rect.setX(option.rect.width() - margin * 2 - 10);
0347         style()->drawPrimitive(QStyle::PE_IndicatorArrowRight, &option, &p, this);
0348     }
0349     p.restore();
0350 }
0351 
0352 void KexiCommandLinkButton::setDescription(const QString &description)
0353 {
0354     d->description = description;
0355     updateGeometry();
0356     update();
0357 }
0358 
0359 QString KexiCommandLinkButton::description() const
0360 {
0361     return d->description;
0362 }
0363 
0364 bool KexiCommandLinkButton::isArrowVisible() const
0365 {
0366     return d->isArrowVisible;
0367 }
0368 
0369 void KexiCommandLinkButton::setArrowVisible(bool visible)
0370 {
0371     if (d->isArrowVisible == visible) {
0372         return;
0373     }
0374     d->isArrowVisible = visible;
0375     update();
0376 }