1/*
2 SPDX-FileCopyrightText: 2001, 2002, 2003 Joseph Wenninger <jowenn@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "kmultitabbar.h"
8#include "kmultitabbar_p.h"
9#include "moc_kmultitabbar.cpp"
10#include "moc_kmultitabbar_p.cpp"
11
12#include <QMouseEvent>
13#include <QPainter>
14#include <QStyle>
15#include <QStyleOptionButton>
16
17#include <math.h>
18
19/*
20 A bit of an overview about what's going on here:
21 There are two sorts of things that can be in the tab bar: tabs and regular
22 buttons. The former are managed by KMultiTabBarInternal, the later
23 by the KMultiTabBar itself.
24*/
25
26class KMultiTabBarTabPrivate
27{
28};
29class KMultiTabBarButtonPrivate
30{
31};
32
33class KMultiTabBarPrivate
34{
35public:
36 class KMultiTabBarInternal *m_internal;
37 QBoxLayout *m_l;
38 QFrame *m_btnTabSep;
39 QList<KMultiTabBarButton *> m_buttons;
40 KMultiTabBar::KMultiTabBarPosition m_position;
41};
42
43KMultiTabBarInternal::KMultiTabBarInternal(QWidget *parent, KMultiTabBar::KMultiTabBarPosition pos)
44 : QFrame(parent)
45{
46 m_position = pos;
47 if (pos == KMultiTabBar::Left || pos == KMultiTabBar::Right) {
48 mainLayout = new QVBoxLayout(this);
49 } else {
50 mainLayout = new QHBoxLayout(this);
51 }
52 mainLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
53 mainLayout->setSpacing(0);
54 mainLayout->addStretch();
55 setFrameStyle(NoFrame);
56 setBackgroundRole(QPalette::Window);
57}
58
59KMultiTabBarInternal::~KMultiTabBarInternal()
60{
61 qDeleteAll(c: m_tabs);
62 m_tabs.clear();
63}
64
65void KMultiTabBarInternal::setStyle(enum KMultiTabBar::KMultiTabBarStyle style)
66{
67 m_style = style;
68 for (int i = 0; i < m_tabs.count(); i++) {
69 m_tabs.at(i)->setStyle(m_style);
70 }
71
72 updateGeometry();
73}
74
75void KMultiTabBarInternal::contentsMousePressEvent(QMouseEvent *ev)
76{
77 ev->ignore();
78}
79
80void KMultiTabBarInternal::mousePressEvent(QMouseEvent *ev)
81{
82 ev->ignore();
83}
84
85KMultiTabBarTab *KMultiTabBarInternal::tab(int id) const
86{
87 QListIterator<KMultiTabBarTab *> it(m_tabs);
88 while (it.hasNext()) {
89 KMultiTabBarTab *tab = it.next();
90 if (tab->id() == id) {
91 return tab;
92 }
93 }
94 return nullptr;
95}
96
97int KMultiTabBarInternal::appendTab(const QIcon &icon, int id, const QString &text)
98{
99 KMultiTabBarTab *tab;
100 m_tabs.append(t: tab = new KMultiTabBarTab(icon, text, id, this, m_position, m_style));
101
102 // Insert before the stretch..
103 mainLayout->insertWidget(index: m_tabs.size() - 1, widget: tab);
104 tab->show();
105 return 0;
106}
107
108int KMultiTabBarInternal::appendTab(const QPixmap &pic, int id, const QString &text)
109{
110 // reuse icon variant
111 return appendTab(icon: QIcon(pic), id, text);
112}
113
114void KMultiTabBarInternal::removeTab(int id)
115{
116 for (int pos = 0; pos < m_tabs.count(); pos++) {
117 if (m_tabs.at(i: pos)->id() == id) {
118 // remove & delete the tab
119 delete m_tabs.takeAt(i: pos);
120 break;
121 }
122 }
123}
124
125void KMultiTabBarInternal::setPosition(enum KMultiTabBar::KMultiTabBarPosition pos)
126{
127 m_position = pos;
128 for (int i = 0; i < m_tabs.count(); i++) {
129 m_tabs.at(i)->setPosition(m_position);
130 }
131 updateGeometry();
132}
133
134// KMultiTabBarButton
135///////////////////////////////////////////////////////////////////////////////
136
137KMultiTabBarButton::KMultiTabBarButton(const QIcon &icon, const QString &text, int id, QWidget *parent)
138 : QPushButton(icon, text, parent)
139 , m_id(id)
140 , d(nullptr)
141{
142 connect(sender: this, signal: &QPushButton::clicked, context: this, slot: &KMultiTabBarButton::slotClicked);
143
144 // we can't see the focus, so don't take focus. #45557
145 // If keyboard navigation is wanted, then only the bar should take focus,
146 // and arrows could change the focused button; but generally, tabbars don't take focus anyway.
147 setFocusPolicy(Qt::NoFocus);
148 setAttribute(Qt::WA_LayoutUsesWidgetRect);
149 Q_UNUSED(d);
150}
151
152KMultiTabBarButton::~KMultiTabBarButton()
153{
154}
155
156void KMultiTabBarButton::setText(const QString &text)
157{
158 QPushButton::setText(text);
159}
160
161void KMultiTabBarButton::slotClicked()
162{
163 updateGeometry();
164 Q_EMIT clicked(t1: m_id);
165}
166
167int KMultiTabBarButton::id() const
168{
169 return m_id;
170}
171
172void KMultiTabBarButton::hideEvent(QHideEvent *he)
173{
174 QPushButton::hideEvent(event: he);
175 KMultiTabBar *tb = dynamic_cast<KMultiTabBar *>(parentWidget());
176 if (tb) {
177 tb->updateSeparator();
178 }
179}
180
181void KMultiTabBarButton::showEvent(QShowEvent *he)
182{
183 QPushButton::showEvent(event: he);
184 KMultiTabBar *tb = dynamic_cast<KMultiTabBar *>(parentWidget());
185 if (tb) {
186 tb->updateSeparator();
187 }
188}
189
190void KMultiTabBarButton::paintEvent(QPaintEvent *)
191{
192 QStyleOptionButton opt;
193 opt.initFrom(w: this);
194 opt.icon = icon();
195 opt.iconSize = iconSize();
196 // removes the QStyleOptionButton::HasMenu ButtonFeature
197 opt.features = QStyleOptionButton::Flat;
198 QPainter painter(this);
199 style()->drawControl(element: QStyle::CE_PushButton, opt: &opt, p: &painter, w: this);
200}
201
202// KMultiTabBarTab
203///////////////////////////////////////////////////////////////////////////////
204
205KMultiTabBarTab::KMultiTabBarTab(const QIcon &icon,
206 const QString &text,
207 int id,
208 QWidget *parent,
209 KMultiTabBar::KMultiTabBarPosition pos,
210 KMultiTabBar::KMultiTabBarStyle style)
211 : KMultiTabBarButton(icon, text, id, parent)
212 , m_style(style)
213 , d(nullptr)
214{
215 m_position = pos;
216 setToolTip(text);
217 setCheckable(true);
218 // shrink down to icon only, but prefer to show text if it's there
219 setSizePolicy(hor: QSizePolicy::Preferred, ver: QSizePolicy::Preferred);
220}
221
222KMultiTabBarTab::~KMultiTabBarTab()
223{
224}
225
226void KMultiTabBarTab::setPosition(KMultiTabBar::KMultiTabBarPosition pos)
227{
228 m_position = pos;
229 updateGeometry();
230}
231
232void KMultiTabBarTab::setStyle(KMultiTabBar::KMultiTabBarStyle style)
233{
234 m_style = style;
235 updateGeometry();
236}
237
238void KMultiTabBarTab::initStyleOption(QStyleOptionToolButton *opt) const
239{
240 opt->initFrom(w: this);
241
242 // Setup icon..
243 if (!icon().isNull()) {
244 opt->iconSize = iconSize();
245 opt->icon = icon();
246 }
247
248 // Should we draw text?
249 if (shouldDrawText()) {
250 opt->text = text();
251 }
252
253 opt->state |= QStyle::State_AutoRaise;
254 if (!isChecked() && !isDown()) {
255 opt->state |= QStyle::State_Raised;
256 }
257 if (isDown()) {
258 opt->state |= QStyle::State_Sunken;
259 }
260 if (isChecked()) {
261 opt->state |= QStyle::State_On;
262 }
263
264 opt->font = font();
265 opt->toolButtonStyle = shouldDrawText() ? Qt::ToolButtonTextBesideIcon : Qt::ToolButtonIconOnly;
266 opt->subControls = QStyle::SC_ToolButton;
267}
268
269QSize KMultiTabBarTab::sizeHint() const
270{
271 return computeSizeHint(withText: shouldDrawText());
272}
273
274QSize KMultiTabBarTab::minimumSizeHint() const
275{
276 return computeSizeHint(withText: false);
277}
278
279void KMultiTabBarTab::computeMargins(int *hMargin, int *vMargin) const
280{
281 // Unfortunately, QStyle does not give us enough information to figure out
282 // where to place things, so we try to reverse-engineer it
283 QStyleOptionToolButton opt;
284 initStyleOption(opt: &opt);
285
286 QSize trialSize = opt.iconSize;
287 QSize expandSize = style()->sizeFromContents(ct: QStyle::CT_ToolButton, opt: &opt, contentsSize: trialSize, w: this);
288
289 *hMargin = (expandSize.width() - trialSize.width()) / 2;
290 *vMargin = (expandSize.height() - trialSize.height()) / 2;
291}
292
293QSize KMultiTabBarTab::computeSizeHint(bool withText) const
294{
295 // Compute as horizontal first, then flip around if need be.
296 QStyleOptionToolButton opt;
297 initStyleOption(opt: &opt);
298
299 int hMargin;
300 int vMargin;
301 computeMargins(hMargin: &hMargin, vMargin: &vMargin);
302
303 // Compute interior size, starting from pixmap..
304 QSize size = opt.iconSize;
305
306 // Always include text height in computation, to avoid resizing the minor direction
307 // when expanding text..
308 QSize textSize = fontMetrics().size(flags: 0, str: text());
309 size.setHeight(qMax(a: size.height(), b: textSize.height()));
310
311 // Pick margins for major/minor direction, depending on orientation
312 int majorMargin = isVertical() ? vMargin : hMargin;
313 int minorMargin = isVertical() ? hMargin : vMargin;
314
315 size.setWidth(size.width() + 2 * majorMargin);
316 size.setHeight(size.height() + 2 * minorMargin);
317
318 if (withText && !text().isEmpty())
319 // Add enough room for the text, and an extra major margin.
320 {
321 size.setWidth(size.width() + textSize.width() + majorMargin);
322 }
323
324 // flip time?
325 if (isVertical()) {
326 return QSize(size.height(), size.width());
327 } else {
328 return size;
329 }
330}
331
332void KMultiTabBarTab::setState(bool newState)
333{
334 setChecked(newState);
335 updateGeometry();
336}
337
338bool KMultiTabBarTab::shouldDrawText() const
339{
340 return (m_style == KMultiTabBar::KDEV3ICON) || isChecked();
341}
342
343bool KMultiTabBarTab::isVertical() const
344{
345 return m_position == KMultiTabBar::Right || m_position == KMultiTabBar::Left;
346}
347
348void KMultiTabBarTab::paintEvent(QPaintEvent *)
349{
350 QPainter painter(this);
351
352 QStyleOptionToolButton opt;
353 initStyleOption(opt: &opt);
354
355 // Paint bevel..
356 if (underMouse() || isChecked()) {
357 opt.text.clear();
358 opt.icon = QIcon();
359 style()->drawComplexControl(cc: QStyle::CC_ToolButton, opt: &opt, p: &painter, widget: this);
360 }
361
362 int hMargin;
363 int vMargin;
364 computeMargins(hMargin: &hMargin, vMargin: &vMargin);
365
366 // We first figure out how much room we have for the text, based on
367 // icon size and margin, try to fit in by eliding, and perhaps
368 // give up on drawing the text entirely if we're too short on room
369 int textRoom = 0;
370 int iconRoom = 0;
371
372 QString t;
373 if (shouldDrawText()) {
374 if (isVertical()) {
375 iconRoom = opt.iconSize.height() + 2 * vMargin;
376 textRoom = height() - iconRoom - vMargin;
377 } else {
378 iconRoom = opt.iconSize.width() + 2 * hMargin;
379 textRoom = width() - iconRoom - hMargin;
380 }
381
382 t = painter.fontMetrics().elidedText(text: text(), mode: Qt::ElideRight, width: textRoom);
383
384 // See whether anything is left. Qt will return either
385 // ... or the ellipsis unicode character, 0x2026
386 if (t == QLatin1String("...") || t == QChar(0x2026)) {
387 t.clear();
388 }
389 }
390
391 const QIcon::Mode iconMode = (opt.state & QStyle::State_MouseOver) ? QIcon::Active : QIcon::Normal;
392 const QPixmap iconPixmap = icon().pixmap(size: opt.iconSize, devicePixelRatio: devicePixelRatioF(), mode: iconMode, state: QIcon::On);
393
394 // Label time.... Simple case: no text, so just plop down the icon right in the center
395 // We only do this when the button never draws the text, to avoid jumps in icon position
396 // when resizing
397 if (!shouldDrawText()) {
398 style()->drawItemPixmap(painter: &painter, rect: rect(), alignment: Qt::AlignCenter | Qt::AlignVCenter, pixmap: iconPixmap);
399 return;
400 }
401
402 // Now where the icon/text goes depends on text direction and tab position
403 QRect iconArea;
404 QRect labelArea;
405
406 bool bottomIcon = false;
407 bool rtl = layoutDirection() == Qt::RightToLeft;
408 if (isVertical()) {
409 if (m_position == KMultiTabBar::Left && !rtl) {
410 bottomIcon = true;
411 }
412 if (m_position == KMultiTabBar::Right && rtl) {
413 bottomIcon = true;
414 }
415 }
416 // alignFlags = Qt::AlignLeading | Qt::AlignVCenter;
417
418 const int iconXShift = (isChecked() || isDown()) ? style()->pixelMetric(metric: QStyle::PM_ButtonShiftHorizontal, option: &opt, widget: this) : 0;
419 const int iconYShift = (isChecked() || isDown()) ? style()->pixelMetric(metric: QStyle::PM_ButtonShiftVertical, option: &opt, widget: this) : 0;
420 if (isVertical()) {
421 if (bottomIcon) {
422 labelArea = QRect(0, vMargin, width(), textRoom);
423 iconArea = QRect(0, vMargin + textRoom, width(), iconRoom);
424 iconArea.translate(dx: iconYShift, dy: -iconXShift);
425 } else {
426 labelArea = QRect(0, iconRoom, width(), textRoom);
427 iconArea = QRect(0, 0, width(), iconRoom);
428 iconArea.translate(dx: -iconYShift, dy: iconXShift);
429 }
430 } else {
431 // Pretty simple --- depends only on RTL/LTR
432 if (rtl) {
433 labelArea = QRect(hMargin, 0, textRoom, height());
434 iconArea = QRect(hMargin + textRoom, 0, iconRoom, height());
435 } else {
436 labelArea = QRect(iconRoom, 0, textRoom, height());
437 iconArea = QRect(0, 0, iconRoom, height());
438 }
439 iconArea.translate(dx: iconXShift, dy: iconYShift);
440 }
441
442 style()->drawItemPixmap(painter: &painter, rect: iconArea, alignment: Qt::AlignCenter | Qt::AlignVCenter, pixmap: iconPixmap);
443
444 if (t.isEmpty()) {
445 return;
446 }
447
448 QRect labelPaintArea = labelArea;
449
450 if (isVertical()) {
451 // If we're vertical, we paint to a simple 0,0 origin rect,
452 // and get the transformations to get us in the right place
453 labelPaintArea = QRect(0, 0, labelArea.height(), labelArea.width());
454
455 QTransform tr;
456
457 if (bottomIcon) {
458 tr.translate(dx: labelArea.x(), dy: labelPaintArea.width() + labelArea.y());
459 tr.rotate(a: -90);
460 } else {
461 tr.translate(dx: labelPaintArea.height() + labelArea.x(), dy: labelArea.y());
462 tr.rotate(a: 90);
463 }
464 painter.setTransform(transform: tr);
465 }
466
467 opt.text = t;
468 opt.icon = QIcon();
469 opt.rect = labelPaintArea;
470 style()->drawControl(element: QStyle::CE_ToolButtonLabel, opt: &opt, p: &painter, w: this);
471}
472
473// KMultiTabBar
474///////////////////////////////////////////////////////////////////////////////
475
476KMultiTabBar::KMultiTabBar(QWidget *parent)
477 : KMultiTabBar(Left, parent)
478{
479}
480
481KMultiTabBar::KMultiTabBar(KMultiTabBarPosition pos, QWidget *parent)
482 : QWidget(parent)
483 , d(new KMultiTabBarPrivate)
484{
485 if (pos == Left || pos == Right) {
486 d->m_l = new QVBoxLayout(this);
487 setSizePolicy(hor: QSizePolicy::Fixed, ver: QSizePolicy::Expanding /*, true*/);
488 } else {
489 d->m_l = new QHBoxLayout(this);
490 setSizePolicy(hor: QSizePolicy::Expanding, ver: QSizePolicy::Fixed /*, true*/);
491 }
492 d->m_l->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
493 d->m_l->setSpacing(0);
494
495 d->m_internal = new KMultiTabBarInternal(this, pos);
496 setPosition(pos);
497 setStyle(VSNET);
498 d->m_l->insertWidget(index: 0, widget: d->m_internal);
499 d->m_l->insertWidget(index: 0, widget: d->m_btnTabSep = new QFrame(this));
500 d->m_btnTabSep->setFixedHeight(4);
501 d->m_btnTabSep->setFrameStyle(QFrame::Panel | QFrame::Sunken);
502 d->m_btnTabSep->setLineWidth(2);
503 d->m_btnTabSep->hide();
504
505 updateGeometry();
506}
507
508KMultiTabBar::~KMultiTabBar()
509{
510 qDeleteAll(c: d->m_buttons);
511 d->m_buttons.clear();
512}
513
514int KMultiTabBar::appendButton(const QIcon &icon, int id, QMenu *popup, const QString &)
515{
516 KMultiTabBarButton *btn = new KMultiTabBarButton(icon, QString(), id, this);
517 // a button with a QMenu can have another size. Make sure the button has always the same size.
518 btn->setFixedWidth(btn->height());
519 btn->setMenu(popup);
520 d->m_buttons.append(t: btn);
521 d->m_l->insertWidget(index: 0, widget: btn);
522 btn->show();
523 d->m_btnTabSep->show();
524 return 0;
525}
526
527void KMultiTabBar::updateSeparator()
528{
529 bool hideSep = true;
530 QListIterator<KMultiTabBarButton *> it(d->m_buttons);
531 while (it.hasNext()) {
532 if (it.next()->isVisibleTo(this)) {
533 hideSep = false;
534 break;
535 }
536 }
537 if (hideSep) {
538 d->m_btnTabSep->hide();
539 } else {
540 d->m_btnTabSep->show();
541 }
542}
543
544int KMultiTabBar::appendTab(const QIcon &icon, int id, const QString &text)
545{
546 d->m_internal->appendTab(icon, id, text);
547 return 0;
548}
549
550KMultiTabBarButton *KMultiTabBar::button(int id) const
551{
552 QListIterator<KMultiTabBarButton *> it(d->m_buttons);
553 while (it.hasNext()) {
554 KMultiTabBarButton *button = it.next();
555 if (button->id() == id) {
556 return button;
557 }
558 }
559
560 return nullptr;
561}
562
563KMultiTabBarTab *KMultiTabBar::tab(int id) const
564{
565 return d->m_internal->tab(id);
566}
567
568void KMultiTabBar::removeButton(int id)
569{
570 for (int pos = 0; pos < d->m_buttons.count(); pos++) {
571 if (d->m_buttons.at(i: pos)->id() == id) {
572 d->m_buttons.takeAt(i: pos)->deleteLater();
573 break;
574 }
575 }
576
577 if (d->m_buttons.isEmpty()) {
578 d->m_btnTabSep->hide();
579 }
580}
581
582void KMultiTabBar::removeTab(int id)
583{
584 d->m_internal->removeTab(id);
585}
586
587void KMultiTabBar::setTab(int id, bool state)
588{
589 KMultiTabBarTab *ttab = tab(id);
590 if (ttab) {
591 ttab->setState(state);
592 }
593}
594
595bool KMultiTabBar::isTabRaised(int id) const
596{
597 KMultiTabBarTab *ttab = tab(id);
598 if (ttab) {
599 return ttab->isChecked();
600 }
601
602 return false;
603}
604
605void KMultiTabBar::setStyle(KMultiTabBarStyle style)
606{
607 d->m_internal->setStyle(style);
608}
609
610KMultiTabBar::KMultiTabBarStyle KMultiTabBar::tabStyle() const
611{
612 return d->m_internal->m_style;
613}
614
615void KMultiTabBar::setPosition(KMultiTabBarPosition pos)
616{
617 d->m_position = pos;
618 d->m_internal->setPosition(pos);
619}
620
621KMultiTabBar::KMultiTabBarPosition KMultiTabBar::position() const
622{
623 return d->m_position;
624}
625
626void KMultiTabBar::fontChange(const QFont & /* oldFont */)
627{
628 updateGeometry();
629}
630
631void KMultiTabBar::paintEvent(class QPaintEvent *)
632{
633 // By default plain QWidget don't call the QStyle and are transparent
634 // using drawPrimitive(QStyle::PE_Widget) allow QStyle implementation
635 // to theme this class.
636 QPainter painter(this);
637 QStyleOption opt;
638 opt.initFrom(w: this);
639 style()->drawPrimitive(pe: QStyle::PE_Widget, opt: &opt, p: &painter, w: this);
640}
641

source code of kwidgetsaddons/src/kmultitabbar.cpp