File indexing completed on 2024-04-28 03:59:09

0001 /*
0002     SPDX-FileCopyrightText: 2001, 2002, 2003 Joseph Wenninger <jowenn@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "kmultitabbar.h"
0008 #include "kmultitabbar_p.h"
0009 #include "moc_kmultitabbar.cpp"
0010 #include "moc_kmultitabbar_p.cpp"
0011 
0012 #include <QMouseEvent>
0013 #include <QPainter>
0014 #include <QStyle>
0015 #include <QStyleOptionButton>
0016 
0017 #include <math.h>
0018 
0019 /*
0020  A bit of an overview about what's going on here:
0021  There are two sorts of things that can be in the tab bar: tabs and regular
0022  buttons. The former are managed by KMultiTabBarInternal, the later
0023  by the KMultiTabBar itself.
0024 */
0025 
0026 class KMultiTabBarTabPrivate
0027 {
0028 };
0029 class KMultiTabBarButtonPrivate
0030 {
0031 };
0032 
0033 class KMultiTabBarPrivate
0034 {
0035 public:
0036     class KMultiTabBarInternal *m_internal;
0037     QBoxLayout *m_l;
0038     QFrame *m_btnTabSep;
0039     QList<KMultiTabBarButton *> m_buttons;
0040     KMultiTabBar::KMultiTabBarPosition m_position;
0041 };
0042 
0043 KMultiTabBarInternal::KMultiTabBarInternal(QWidget *parent, KMultiTabBar::KMultiTabBarPosition pos)
0044     : QFrame(parent)
0045 {
0046     m_position = pos;
0047     if (pos == KMultiTabBar::Left || pos == KMultiTabBar::Right) {
0048         mainLayout = new QVBoxLayout(this);
0049     } else {
0050         mainLayout = new QHBoxLayout(this);
0051     }
0052     mainLayout->setContentsMargins(0, 0, 0, 0);
0053     mainLayout->setSpacing(0);
0054     mainLayout->addStretch();
0055     setFrameStyle(NoFrame);
0056     setBackgroundRole(QPalette::Window);
0057 }
0058 
0059 KMultiTabBarInternal::~KMultiTabBarInternal()
0060 {
0061     qDeleteAll(m_tabs);
0062     m_tabs.clear();
0063 }
0064 
0065 void KMultiTabBarInternal::setStyle(enum KMultiTabBar::KMultiTabBarStyle style)
0066 {
0067     m_style = style;
0068     for (int i = 0; i < m_tabs.count(); i++) {
0069         m_tabs.at(i)->setStyle(m_style);
0070     }
0071 
0072     updateGeometry();
0073 }
0074 
0075 void KMultiTabBarInternal::contentsMousePressEvent(QMouseEvent *ev)
0076 {
0077     ev->ignore();
0078 }
0079 
0080 void KMultiTabBarInternal::mousePressEvent(QMouseEvent *ev)
0081 {
0082     ev->ignore();
0083 }
0084 
0085 KMultiTabBarTab *KMultiTabBarInternal::tab(int id) const
0086 {
0087     QListIterator<KMultiTabBarTab *> it(m_tabs);
0088     while (it.hasNext()) {
0089         KMultiTabBarTab *tab = it.next();
0090         if (tab->id() == id) {
0091             return tab;
0092         }
0093     }
0094     return nullptr;
0095 }
0096 
0097 int KMultiTabBarInternal::appendTab(const QIcon &icon, int id, const QString &text)
0098 {
0099     KMultiTabBarTab *tab;
0100     m_tabs.append(tab = new KMultiTabBarTab(icon, text, id, this, m_position, m_style));
0101 
0102     // Insert before the stretch..
0103     mainLayout->insertWidget(m_tabs.size() - 1, tab);
0104     tab->show();
0105     return 0;
0106 }
0107 
0108 int KMultiTabBarInternal::appendTab(const QPixmap &pic, int id, const QString &text)
0109 {
0110     // reuse icon variant
0111     return appendTab(QIcon(pic), id, text);
0112 }
0113 
0114 void KMultiTabBarInternal::removeTab(int id)
0115 {
0116     for (int pos = 0; pos < m_tabs.count(); pos++) {
0117         if (m_tabs.at(pos)->id() == id) {
0118             // remove & delete the tab
0119             delete m_tabs.takeAt(pos);
0120             break;
0121         }
0122     }
0123 }
0124 
0125 void KMultiTabBarInternal::setPosition(enum KMultiTabBar::KMultiTabBarPosition pos)
0126 {
0127     m_position = pos;
0128     for (int i = 0; i < m_tabs.count(); i++) {
0129         m_tabs.at(i)->setPosition(m_position);
0130     }
0131     updateGeometry();
0132 }
0133 
0134 // KMultiTabBarButton
0135 ///////////////////////////////////////////////////////////////////////////////
0136 
0137 KMultiTabBarButton::KMultiTabBarButton(const QIcon &icon, const QString &text, int id, QWidget *parent)
0138     : QPushButton(icon, text, parent)
0139     , m_id(id)
0140     , d(nullptr)
0141 {
0142     connect(this, &QPushButton::clicked, this, &KMultiTabBarButton::slotClicked);
0143 
0144     // we can't see the focus, so don't take focus. #45557
0145     // If keyboard navigation is wanted, then only the bar should take focus,
0146     // and arrows could change the focused button; but generally, tabbars don't take focus anyway.
0147     setFocusPolicy(Qt::NoFocus);
0148     setAttribute(Qt::WA_LayoutUsesWidgetRect);
0149     Q_UNUSED(d);
0150 }
0151 
0152 KMultiTabBarButton::~KMultiTabBarButton()
0153 {
0154 }
0155 
0156 void KMultiTabBarButton::setText(const QString &text)
0157 {
0158     QPushButton::setText(text);
0159 }
0160 
0161 void KMultiTabBarButton::slotClicked()
0162 {
0163     updateGeometry();
0164     Q_EMIT clicked(m_id);
0165 }
0166 
0167 int KMultiTabBarButton::id() const
0168 {
0169     return m_id;
0170 }
0171 
0172 void KMultiTabBarButton::hideEvent(QHideEvent *he)
0173 {
0174     QPushButton::hideEvent(he);
0175     KMultiTabBar *tb = dynamic_cast<KMultiTabBar *>(parentWidget());
0176     if (tb) {
0177         tb->updateSeparator();
0178     }
0179 }
0180 
0181 void KMultiTabBarButton::showEvent(QShowEvent *he)
0182 {
0183     QPushButton::showEvent(he);
0184     KMultiTabBar *tb = dynamic_cast<KMultiTabBar *>(parentWidget());
0185     if (tb) {
0186         tb->updateSeparator();
0187     }
0188 }
0189 
0190 void KMultiTabBarButton::paintEvent(QPaintEvent *)
0191 {
0192     QStyleOptionButton opt;
0193     opt.initFrom(this);
0194     opt.icon = icon();
0195     opt.iconSize = iconSize();
0196     // removes the QStyleOptionButton::HasMenu ButtonFeature
0197     opt.features = QStyleOptionButton::Flat;
0198     QPainter painter(this);
0199     style()->drawControl(QStyle::CE_PushButton, &opt, &painter, this);
0200 }
0201 
0202 // KMultiTabBarTab
0203 ///////////////////////////////////////////////////////////////////////////////
0204 
0205 KMultiTabBarTab::KMultiTabBarTab(const QIcon &icon,
0206                                  const QString &text,
0207                                  int id,
0208                                  QWidget *parent,
0209                                  KMultiTabBar::KMultiTabBarPosition pos,
0210                                  KMultiTabBar::KMultiTabBarStyle style)
0211     : KMultiTabBarButton(icon, text, id, parent)
0212     , m_style(style)
0213     , d(nullptr)
0214 {
0215     m_position = pos;
0216     setToolTip(text);
0217     setCheckable(true);
0218     // shrink down to icon only, but prefer to show text if it's there
0219     setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
0220 }
0221 
0222 KMultiTabBarTab::~KMultiTabBarTab()
0223 {
0224 }
0225 
0226 void KMultiTabBarTab::setPosition(KMultiTabBar::KMultiTabBarPosition pos)
0227 {
0228     m_position = pos;
0229     updateGeometry();
0230 }
0231 
0232 void KMultiTabBarTab::setStyle(KMultiTabBar::KMultiTabBarStyle style)
0233 {
0234     m_style = style;
0235     updateGeometry();
0236 }
0237 
0238 void KMultiTabBarTab::initStyleOption(QStyleOptionToolButton *opt) const
0239 {
0240     opt->initFrom(this);
0241 
0242     // Setup icon..
0243     if (!icon().isNull()) {
0244         opt->iconSize = iconSize();
0245         opt->icon = icon();
0246     }
0247 
0248     // Should we draw text?
0249     if (shouldDrawText()) {
0250         opt->text = text();
0251     }
0252 
0253     opt->state |= QStyle::State_AutoRaise;
0254     if (!isChecked() && !isDown()) {
0255         opt->state |= QStyle::State_Raised;
0256     }
0257     if (isDown()) {
0258         opt->state |= QStyle::State_Sunken;
0259     }
0260     if (isChecked()) {
0261         opt->state |= QStyle::State_On;
0262     }
0263 
0264     opt->font = font();
0265     opt->toolButtonStyle = shouldDrawText() ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly;
0266     opt->subControls = QStyle::SC_ToolButton;
0267 }
0268 
0269 QSize KMultiTabBarTab::sizeHint() const
0270 {
0271     return computeSizeHint(shouldDrawText());
0272 }
0273 
0274 QSize KMultiTabBarTab::minimumSizeHint() const
0275 {
0276     return computeSizeHint(false);
0277 }
0278 
0279 void KMultiTabBarTab::computeMargins(int *hMargin, int *vMargin) const
0280 {
0281     // Unfortunately, QStyle does not give us enough information to figure out
0282     // where to place things, so we try to reverse-engineer it
0283     QStyleOptionToolButton opt;
0284     initStyleOption(&opt);
0285 
0286     QSize trialSize = opt.iconSize;
0287     QSize expandSize = style()->sizeFromContents(QStyle::CT_ToolButton, &opt, trialSize, this);
0288 
0289     *hMargin = (expandSize.width() - trialSize.width()) / 2;
0290     *vMargin = (expandSize.height() - trialSize.height()) / 2;
0291 }
0292 
0293 QSize KMultiTabBarTab::computeSizeHint(bool withText) const
0294 {
0295     // Compute as horizontal first, then flip around if need be.
0296     QStyleOptionToolButton opt;
0297     initStyleOption(&opt);
0298 
0299     int hMargin;
0300     int vMargin;
0301     computeMargins(&hMargin, &vMargin);
0302 
0303     // Compute interior size, starting from pixmap..
0304     QSize size = opt.iconSize;
0305 
0306     // Always include text height in computation, to avoid resizing the minor direction
0307     // when expanding text..
0308     QSize textSize = fontMetrics().size(0, text());
0309     size.setHeight(qMax(size.height(), textSize.height()));
0310 
0311     // Pick margins for major/minor direction, depending on orientation
0312     int majorMargin = isVertical() ? vMargin : hMargin;
0313     int minorMargin = isVertical() ? hMargin : vMargin;
0314 
0315     size.setWidth(size.width() + 2 * majorMargin);
0316     size.setHeight(size.height() + 2 * minorMargin);
0317 
0318     if (withText && !text().isEmpty())
0319     // Add enough room for the text, and an extra major margin.
0320     {
0321         size.setWidth(size.width() + textSize.width() + majorMargin);
0322     }
0323 
0324     // flip time?
0325     if (isVertical()) {
0326         return QSize(size.height(), size.width());
0327     } else {
0328         return size;
0329     }
0330 }
0331 
0332 void KMultiTabBarTab::setState(bool newState)
0333 {
0334     setChecked(newState);
0335     updateGeometry();
0336 }
0337 
0338 bool KMultiTabBarTab::shouldDrawText() const
0339 {
0340     return (m_style == KMultiTabBar::KDEV3ICON) || isChecked();
0341 }
0342 
0343 bool KMultiTabBarTab::isVertical() const
0344 {
0345     return m_position == KMultiTabBar::Right || m_position == KMultiTabBar::Left;
0346 }
0347 
0348 void KMultiTabBarTab::paintEvent(QPaintEvent *)
0349 {
0350     QPainter painter(this);
0351 
0352     QStyleOptionToolButton opt;
0353     initStyleOption(&opt);
0354 
0355     // Paint bevel..
0356     if (underMouse() || isChecked()) {
0357         opt.text.clear();
0358         opt.icon = QIcon();
0359         style()->drawComplexControl(QStyle::CC_ToolButton, &opt, &painter, this);
0360     }
0361 
0362     int hMargin;
0363     int vMargin;
0364     computeMargins(&hMargin, &vMargin);
0365 
0366     // We first figure out how much room we have for the text, based on
0367     // icon size and margin, try to fit in by eliding, and perhaps
0368     // give up on drawing the text entirely if we're too short on room
0369     int textRoom = 0;
0370     int iconRoom = 0;
0371 
0372     QString t;
0373     if (shouldDrawText()) {
0374         if (isVertical()) {
0375             iconRoom = opt.iconSize.height() + 2 * vMargin;
0376             textRoom = height() - iconRoom - vMargin;
0377         } else {
0378             iconRoom = opt.iconSize.width() + 2 * hMargin;
0379             textRoom = width() - iconRoom - hMargin;
0380         }
0381 
0382         t = painter.fontMetrics().elidedText(text(), Qt::ElideRight, textRoom);
0383 
0384         // See whether anything is left. Qt will return either
0385         // ... or the ellipsis unicode character, 0x2026
0386         if (t == QLatin1String("...") || t == QChar(0x2026)) {
0387             t.clear();
0388         }
0389     }
0390 
0391     const QIcon::Mode iconMode = (opt.state & QStyle::State_MouseOver) ? QIcon::Active : QIcon::Normal;
0392     const QPixmap iconPixmap = icon().pixmap(opt.iconSize, devicePixelRatioF(), iconMode, QIcon::On);
0393 
0394     // Label time.... Simple case: no text, so just plop down the icon right in the center
0395     // We only do this when the button never draws the text, to avoid jumps in icon position
0396     // when resizing
0397     if (!shouldDrawText()) {
0398         style()->drawItemPixmap(&painter, rect(), Qt::AlignCenter | Qt::AlignVCenter, iconPixmap);
0399         return;
0400     }
0401 
0402     // Now where the icon/text goes depends on text direction and tab position
0403     QRect iconArea;
0404     QRect labelArea;
0405 
0406     bool bottomIcon = false;
0407     bool rtl = layoutDirection() == Qt::RightToLeft;
0408     if (isVertical()) {
0409         if (m_position == KMultiTabBar::Left && !rtl) {
0410             bottomIcon = true;
0411         }
0412         if (m_position == KMultiTabBar::Right && rtl) {
0413             bottomIcon = true;
0414         }
0415     }
0416     // alignFlags = Qt::AlignLeading | Qt::AlignVCenter;
0417 
0418     const int iconXShift = (isChecked() || isDown()) ? style()->pixelMetric(QStyle::PM_ButtonShiftHorizontal, &opt, this) : 0;
0419     const int iconYShift = (isChecked() || isDown()) ? style()->pixelMetric(QStyle::PM_ButtonShiftVertical, &opt, this) : 0;
0420     if (isVertical()) {
0421         if (bottomIcon) {
0422             labelArea = QRect(0, vMargin, width(), textRoom);
0423             iconArea = QRect(0, vMargin + textRoom, width(), iconRoom);
0424             iconArea.translate(iconYShift, -iconXShift);
0425         } else {
0426             labelArea = QRect(0, iconRoom, width(), textRoom);
0427             iconArea = QRect(0, 0, width(), iconRoom);
0428             iconArea.translate(-iconYShift, iconXShift);
0429         }
0430     } else {
0431         // Pretty simple --- depends only on RTL/LTR
0432         if (rtl) {
0433             labelArea = QRect(hMargin, 0, textRoom, height());
0434             iconArea = QRect(hMargin + textRoom, 0, iconRoom, height());
0435         } else {
0436             labelArea = QRect(iconRoom, 0, textRoom, height());
0437             iconArea = QRect(0, 0, iconRoom, height());
0438         }
0439         iconArea.translate(iconXShift, iconYShift);
0440     }
0441 
0442     style()->drawItemPixmap(&painter, iconArea, Qt::AlignCenter | Qt::AlignVCenter, iconPixmap);
0443 
0444     if (t.isEmpty()) {
0445         return;
0446     }
0447 
0448     QRect labelPaintArea = labelArea;
0449 
0450     if (isVertical()) {
0451         // If we're vertical, we paint to a simple 0,0 origin rect,
0452         // and get the transformations to get us in the right place
0453         labelPaintArea = QRect(0, 0, labelArea.height(), labelArea.width());
0454 
0455         QTransform tr;
0456 
0457         if (bottomIcon) {
0458             tr.translate(labelArea.x(), labelPaintArea.width() + labelArea.y());
0459             tr.rotate(-90);
0460         } else {
0461             tr.translate(labelPaintArea.height() + labelArea.x(), labelArea.y());
0462             tr.rotate(90);
0463         }
0464         painter.setTransform(tr);
0465     }
0466 
0467     opt.text = t;
0468     opt.icon = QIcon();
0469     opt.rect = labelPaintArea;
0470     style()->drawControl(QStyle::CE_ToolButtonLabel, &opt, &painter, this);
0471 }
0472 
0473 // KMultiTabBar
0474 ///////////////////////////////////////////////////////////////////////////////
0475 
0476 KMultiTabBar::KMultiTabBar(QWidget *parent)
0477     : KMultiTabBar(Left, parent)
0478 {
0479 }
0480 
0481 KMultiTabBar::KMultiTabBar(KMultiTabBarPosition pos, QWidget *parent)
0482     : QWidget(parent)
0483     , d(new KMultiTabBarPrivate)
0484 {
0485     if (pos == Left || pos == Right) {
0486         d->m_l = new QVBoxLayout(this);
0487         setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding /*, true*/);
0488     } else {
0489         d->m_l = new QHBoxLayout(this);
0490         setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed /*, true*/);
0491     }
0492     d->m_l->setContentsMargins(0, 0, 0, 0);
0493     d->m_l->setSpacing(0);
0494 
0495     d->m_internal = new KMultiTabBarInternal(this, pos);
0496     setPosition(pos);
0497     setStyle(VSNET);
0498     d->m_l->insertWidget(0, d->m_internal);
0499     d->m_l->insertWidget(0, d->m_btnTabSep = new QFrame(this));
0500     d->m_btnTabSep->setFixedHeight(4);
0501     d->m_btnTabSep->setFrameStyle(QFrame::Panel | QFrame::Sunken);
0502     d->m_btnTabSep->setLineWidth(2);
0503     d->m_btnTabSep->hide();
0504 
0505     updateGeometry();
0506 }
0507 
0508 KMultiTabBar::~KMultiTabBar()
0509 {
0510     qDeleteAll(d->m_buttons);
0511     d->m_buttons.clear();
0512 }
0513 
0514 int KMultiTabBar::appendButton(const QIcon &icon, int id, QMenu *popup, const QString &)
0515 {
0516     KMultiTabBarButton *btn = new KMultiTabBarButton(icon, QString(), id, this);
0517     // a button with a QMenu can have another size. Make sure the button has always the same size.
0518     btn->setFixedWidth(btn->height());
0519     btn->setMenu(popup);
0520     d->m_buttons.append(btn);
0521     d->m_l->insertWidget(0, btn);
0522     btn->show();
0523     d->m_btnTabSep->show();
0524     return 0;
0525 }
0526 
0527 void KMultiTabBar::updateSeparator()
0528 {
0529     bool hideSep = true;
0530     QListIterator<KMultiTabBarButton *> it(d->m_buttons);
0531     while (it.hasNext()) {
0532         if (it.next()->isVisibleTo(this)) {
0533             hideSep = false;
0534             break;
0535         }
0536     }
0537     if (hideSep) {
0538         d->m_btnTabSep->hide();
0539     } else {
0540         d->m_btnTabSep->show();
0541     }
0542 }
0543 
0544 int KMultiTabBar::appendTab(const QIcon &icon, int id, const QString &text)
0545 {
0546     d->m_internal->appendTab(icon, id, text);
0547     return 0;
0548 }
0549 
0550 KMultiTabBarButton *KMultiTabBar::button(int id) const
0551 {
0552     QListIterator<KMultiTabBarButton *> it(d->m_buttons);
0553     while (it.hasNext()) {
0554         KMultiTabBarButton *button = it.next();
0555         if (button->id() == id) {
0556             return button;
0557         }
0558     }
0559 
0560     return nullptr;
0561 }
0562 
0563 KMultiTabBarTab *KMultiTabBar::tab(int id) const
0564 {
0565     return d->m_internal->tab(id);
0566 }
0567 
0568 void KMultiTabBar::removeButton(int id)
0569 {
0570     for (int pos = 0; pos < d->m_buttons.count(); pos++) {
0571         if (d->m_buttons.at(pos)->id() == id) {
0572             d->m_buttons.takeAt(pos)->deleteLater();
0573             break;
0574         }
0575     }
0576 
0577     if (d->m_buttons.isEmpty()) {
0578         d->m_btnTabSep->hide();
0579     }
0580 }
0581 
0582 void KMultiTabBar::removeTab(int id)
0583 {
0584     d->m_internal->removeTab(id);
0585 }
0586 
0587 void KMultiTabBar::setTab(int id, bool state)
0588 {
0589     KMultiTabBarTab *ttab = tab(id);
0590     if (ttab) {
0591         ttab->setState(state);
0592     }
0593 }
0594 
0595 bool KMultiTabBar::isTabRaised(int id) const
0596 {
0597     KMultiTabBarTab *ttab = tab(id);
0598     if (ttab) {
0599         return ttab->isChecked();
0600     }
0601 
0602     return false;
0603 }
0604 
0605 void KMultiTabBar::setStyle(KMultiTabBarStyle style)
0606 {
0607     d->m_internal->setStyle(style);
0608 }
0609 
0610 KMultiTabBar::KMultiTabBarStyle KMultiTabBar::tabStyle() const
0611 {
0612     return d->m_internal->m_style;
0613 }
0614 
0615 void KMultiTabBar::setPosition(KMultiTabBarPosition pos)
0616 {
0617     d->m_position = pos;
0618     d->m_internal->setPosition(pos);
0619 }
0620 
0621 KMultiTabBar::KMultiTabBarPosition KMultiTabBar::position() const
0622 {
0623     return d->m_position;
0624 }
0625 
0626 void KMultiTabBar::fontChange(const QFont & /* oldFont */)
0627 {
0628     updateGeometry();
0629 }
0630 
0631 void KMultiTabBar::paintEvent(class QPaintEvent *)
0632 {
0633     // By default plain QWidget don't call the QStyle and are transparent
0634     // using drawPrimitive(QStyle::PE_Widget) allow QStyle implementation
0635     // to theme this class.
0636     QPainter painter(this);
0637     QStyleOption opt;
0638     opt.initFrom(this);
0639     style()->drawPrimitive(QStyle::PE_Widget, &opt, &painter, this);
0640 }