1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qtoolbox.h"
5
6#include <qapplication.h>
7#include <qeventloop.h>
8#include <qlayout.h>
9#include <qlist.h>
10#include <qpainter.h>
11#include <qscrollarea.h>
12#include <qstyle.h>
13#include <qstyleoption.h>
14#if QT_CONFIG(tooltip)
15#include <qtooltip.h>
16#endif
17#include <qabstractbutton.h>
18
19#include "qframe_p.h"
20
21QT_BEGIN_NAMESPACE
22
23using namespace Qt::StringLiterals;
24
25class QToolBoxButton : public QAbstractButton
26{
27 Q_OBJECT
28public:
29 QToolBoxButton(QWidget *parent)
30 : QAbstractButton(parent), selected(false), indexInPage(-1)
31 {
32 setBackgroundRole(QPalette::Window);
33 setSizePolicy(hor: QSizePolicy::Preferred, ver: QSizePolicy::Minimum);
34 setFocusPolicy(Qt::NoFocus);
35 }
36
37 inline void setSelected(bool b) { selected = b; update(); }
38 inline void setIndex(int newIndex) { indexInPage = newIndex; }
39
40 QSize sizeHint() const override;
41 QSize minimumSizeHint() const override;
42
43protected:
44 void initStyleOption(QStyleOptionToolBox *opt) const;
45 void paintEvent(QPaintEvent *) override;
46
47private:
48 bool selected;
49 int indexInPage;
50};
51
52
53class QToolBoxPrivate : public QFramePrivate
54{
55 Q_DECLARE_PUBLIC(QToolBox)
56public:
57 struct Page
58 {
59 QToolBoxButton *button;
60 QScrollArea *sv;
61 QWidget *widget;
62
63 inline void setText(const QString &text) { button->setText(text); }
64 inline void setIcon(const QIcon &is) { button->setIcon(is); }
65#if QT_CONFIG(tooltip)
66 inline void setToolTip(const QString &tip) { button->setToolTip(tip); }
67 inline QString toolTip() const { return button->toolTip(); }
68#endif
69 inline QString text() const { return button->text(); }
70 inline QIcon icon() const { return button->icon(); }
71
72 inline bool operator==(const Page& other) const
73 {
74 return widget == other.widget;
75 }
76 };
77 typedef std::vector<std::unique_ptr<Page>> PageList;
78
79 inline QToolBoxPrivate()
80 : currentPage(nullptr)
81 {
82 }
83 void _q_buttonClicked();
84 void _q_widgetDestroyed(QObject*);
85
86 const Page *page(const QObject *widget) const;
87 const Page *page(int index) const;
88 Page *page(int index);
89
90 void updateTabs();
91 void relayout();
92
93 PageList pageList;
94 QVBoxLayout *layout;
95 Page *currentPage;
96};
97
98const QToolBoxPrivate::Page *QToolBoxPrivate::page(const QObject *widget) const
99{
100 if (!widget)
101 return nullptr;
102
103 for (const auto &page : pageList) {
104 if (page->widget == widget)
105 return page.get();
106 }
107 return nullptr;
108}
109
110QToolBoxPrivate::Page *QToolBoxPrivate::page(int index)
111{
112 if (index >= 0 && index < static_cast<int>(pageList.size()))
113 return pageList[index].get();
114 return nullptr;
115}
116
117const QToolBoxPrivate::Page *QToolBoxPrivate::page(int index) const
118{
119 if (index >= 0 && index < static_cast<int>(pageList.size()))
120 return pageList[index].get();
121 return nullptr;
122}
123
124void QToolBoxPrivate::updateTabs()
125{
126 QToolBoxButton *lastButton = currentPage ? currentPage->button : nullptr;
127 bool after = false;
128 int index = 0;
129 for (const auto &page : pageList) {
130 QToolBoxButton *tB = page->button;
131 // update indexes, since the updates are delayed, the indexes will be correct
132 // when we actually paint.
133 tB->setIndex(index);
134 QWidget *tW = page->widget;
135 if (after) {
136 QPalette p = tB->palette();
137 p.setColor(acr: tB->backgroundRole(), acolor: tW->palette().color(cr: tW->backgroundRole()));
138 tB->setPalette(p);
139 tB->update();
140 } else if (tB->backgroundRole() != QPalette::Window) {
141 tB->setBackgroundRole(QPalette::Window);
142 tB->update();
143 }
144 after = tB == lastButton;
145 ++index;
146 }
147}
148
149QSize QToolBoxButton::sizeHint() const
150{
151 QSize iconSize(8, 8);
152 if (!icon().isNull()) {
153 int icone = style()->pixelMetric(metric: QStyle::PM_SmallIconSize, option: nullptr, widget: parentWidget() /* QToolBox */);
154 iconSize += QSize(icone + 2, icone);
155 }
156 QSize textSize = fontMetrics().size(flags: Qt::TextShowMnemonic, str: text()) + QSize(0, 8);
157
158 return QSize(iconSize.width() + textSize.width(), qMax(a: iconSize.height(), b: textSize.height()));
159}
160
161QSize QToolBoxButton::minimumSizeHint() const
162{
163 if (icon().isNull())
164 return QSize();
165 int icone = style()->pixelMetric(metric: QStyle::PM_SmallIconSize, option: nullptr, widget: parentWidget() /* QToolBox */);
166 return QSize(icone + 8, icone + 8);
167}
168
169void QToolBoxButton::initStyleOption(QStyleOptionToolBox *option) const
170{
171 if (!option)
172 return;
173 option->initFrom(w: this);
174 if (selected)
175 option->state |= QStyle::State_Selected;
176 if (isDown())
177 option->state |= QStyle::State_Sunken;
178 option->text = text();
179 option->icon = icon();
180
181 QToolBox *toolBox = static_cast<QToolBox *>(parentWidget()); // I know I'm in a tool box.
182 const int widgetCount = toolBox->count();
183 const int currIndex = toolBox->currentIndex();
184 if (widgetCount == 1) {
185 option->position = QStyleOptionToolBox::OnlyOneTab;
186 } else if (indexInPage == 0) {
187 option->position = QStyleOptionToolBox::Beginning;
188 } else if (indexInPage == widgetCount - 1) {
189 option->position = QStyleOptionToolBox::End;
190 } else {
191 option->position = QStyleOptionToolBox::Middle;
192 }
193 if (currIndex == indexInPage - 1) {
194 option->selectedPosition = QStyleOptionToolBox::PreviousIsSelected;
195 } else if (currIndex == indexInPage + 1) {
196 option->selectedPosition = QStyleOptionToolBox::NextIsSelected;
197 } else {
198 option->selectedPosition = QStyleOptionToolBox::NotAdjacent;
199 }
200}
201
202void QToolBoxButton::paintEvent(QPaintEvent *)
203{
204 QPainter paint(this);
205 QPainter *p = &paint;
206 QStyleOptionToolBox opt;
207 initStyleOption(option: &opt);
208 style()->drawControl(element: QStyle::CE_ToolBoxTab, opt: &opt, p, w: parentWidget());
209}
210
211/*!
212 \class QToolBox
213
214 \brief The QToolBox class provides a column of tabbed widget items.
215
216
217 \ingroup basicwidgets
218 \inmodule QtWidgets
219
220 A toolbox is a widget that displays a column of tabs one above the
221 other, with the current item displayed below the current tab.
222 Every tab has an index position within the column of tabs. A tab's
223 item is a QWidget.
224
225 Each item has an itemText(), an optional itemIcon(), an optional
226 itemToolTip(), and a widget(). The item's attributes can be
227 changed with setItemText(), setItemIcon(), and
228 setItemToolTip(). Each item can be enabled or disabled
229 individually with setItemEnabled().
230
231 Items are added using addItem(), or inserted at particular
232 positions using insertItem(). The total number of items is given
233 by count(). Items can be deleted with delete, or removed from the
234 toolbox with removeItem(). Combining removeItem() and insertItem()
235 allows you to move items to different positions.
236
237 The index of the current item widget is returned by currentIndex(),
238 and set with setCurrentIndex(). The index of a particular item can
239 be found using indexOf(), and the item at a given index is returned
240 by item().
241
242 The currentChanged() signal is emitted when the current item is
243 changed.
244
245 \sa QTabWidget
246*/
247
248/*!
249 \fn void QToolBox::currentChanged(int index)
250
251 This signal is emitted when the current item is changed. The new
252 current item's index is passed in \a index, or -1 if there is no
253 current item.
254*/
255
256
257/*!
258 Constructs a new toolbox with the given \a parent and the flags, \a f.
259*/
260QToolBox::QToolBox(QWidget *parent, Qt::WindowFlags f)
261 : QFrame(*new QToolBoxPrivate, parent, f)
262{
263 Q_D(QToolBox);
264 d->layout = new QVBoxLayout(this);
265 d->layout->setContentsMargins(QMargins());
266 setBackgroundRole(QPalette::Button);
267}
268
269/*!
270 Destroys the toolbox.
271*/
272
273QToolBox::~QToolBox()
274{
275}
276
277/*!
278 \fn int QToolBox::addItem(QWidget *w, const QString &text)
279 \overload
280
281 Adds the widget \a w in a new tab at bottom of the toolbox. The
282 new tab's text is set to \a text. Returns the new tab's index.
283*/
284
285/*!
286 \fn int QToolBox::addItem(QWidget *widget, const QIcon &iconSet,const QString &text)
287 Adds the \a widget in a new tab at bottom of the toolbox. The
288 new tab's text is set to \a text, and the \a iconSet is
289 displayed to the left of the \a text. Returns the new tab's index.
290*/
291
292/*!
293 \fn int QToolBox::insertItem(int index, QWidget *widget, const QString &text)
294 \overload
295
296 Inserts the \a widget at position \a index, or at the bottom
297 of the toolbox if \a index is out of range. The new item's text is
298 set to \a text. Returns the new item's index.
299*/
300
301/*!
302 Inserts the \a widget at position \a index, or at the bottom
303 of the toolbox if \a index is out of range. The new item's text
304 is set to \a text, and the \a icon is displayed to the left of
305 the \a text. Returns the new item's index.
306*/
307
308int QToolBox::insertItem(int index, QWidget *widget, const QIcon &icon, const QString &text)
309{
310 if (!widget)
311 return -1;
312
313 Q_D(QToolBox);
314 connect(sender: widget, SIGNAL(destroyed(QObject*)), receiver: this, SLOT(_q_widgetDestroyed(QObject*)));
315
316 auto newPage = std::make_unique<QToolBoxPrivate::Page>();
317 auto &c = *newPage;
318 c.widget = widget;
319 c.button = new QToolBoxButton(this);
320 c.button->setObjectName("qt_toolbox_toolboxbutton"_L1);
321 connect(sender: c.button, SIGNAL(clicked()), receiver: this, SLOT(_q_buttonClicked()));
322
323 c.sv = new QScrollArea(this);
324 c.sv->setWidget(widget);
325 c.sv->setWidgetResizable(true);
326 c.sv->hide();
327 c.sv->setFrameStyle(QFrame::NoFrame);
328
329 c.setText(text);
330 c.setIcon(icon);
331
332 if (index < 0 || index >= static_cast<int>(d->pageList.size())) {
333 index = static_cast<int>(d->pageList.size());
334 d->pageList.push_back(x: std::move(newPage));
335 d->layout->addWidget(c.button);
336 d->layout->addWidget(c.sv);
337 if (index == 0)
338 setCurrentIndex(index);
339 } else {
340 d->pageList.insert(position: d->pageList.cbegin() + index, x: std::move(newPage));
341 d->relayout();
342 if (d->currentPage) {
343 QWidget *current = d->currentPage->widget;
344 int oldindex = indexOf(widget: current);
345 if (index <= oldindex) {
346 d->currentPage = nullptr; // trigger change
347 setCurrentIndex(oldindex);
348 }
349 }
350 }
351
352 c.button->show();
353
354 d->updateTabs();
355 itemInserted(index);
356 return index;
357}
358
359void QToolBoxPrivate::_q_buttonClicked()
360{
361 Q_Q(QToolBox);
362 QToolBoxButton *tb = qobject_cast<QToolBoxButton*>(object: q->sender());
363 QWidget* item = nullptr;
364 for (const auto &page : pageList) {
365 if (page->button == tb) {
366 item = page->widget;
367 break;
368 }
369 }
370
371 q->setCurrentIndex(q->indexOf(widget: item));
372}
373
374/*!
375 \property QToolBox::count
376 \brief The number of items contained in the toolbox.
377
378 By default, this property has a value of 0.
379*/
380
381int QToolBox::count() const
382{
383 Q_D(const QToolBox);
384 return static_cast<int>(d->pageList.size());
385}
386
387void QToolBox::setCurrentIndex(int index)
388{
389 Q_D(QToolBox);
390 QToolBoxPrivate::Page *c = d->page(index);
391 if (!c || d->currentPage == c)
392 return;
393
394 c->button->setSelected(true);
395 if (d->currentPage) {
396 d->currentPage->sv->hide();
397 d->currentPage->button->setSelected(false);
398 }
399 d->currentPage = c;
400 d->currentPage->sv->show();
401 d->updateTabs();
402 emit currentChanged(index);
403}
404
405void QToolBoxPrivate::relayout()
406{
407 Q_Q(QToolBox);
408 delete layout;
409 layout = new QVBoxLayout(q);
410 layout->setContentsMargins(QMargins());
411 for (const auto &page : pageList) {
412 layout->addWidget(page->button);
413 layout->addWidget(page->sv);
414 }
415}
416
417auto pageEquals = [](const QToolBoxPrivate::Page *page) {
418 return [page](const std::unique_ptr<QToolBoxPrivate::Page> &ptr) {
419 return ptr.get() == page;
420 };
421};
422
423void QToolBoxPrivate::_q_widgetDestroyed(QObject *object)
424{
425 Q_Q(QToolBox);
426
427 const QToolBoxPrivate::Page * const c = page(widget: object);
428 if (!c)
429 return;
430
431 layout->removeWidget(w: c->sv);
432 layout->removeWidget(w: c->button);
433 c->sv->deleteLater(); // page might still be a child of sv
434 delete c->button;
435
436 bool removeCurrent = c == currentPage;
437 pageList.erase(first: std::remove_if(first: pageList.begin(), last: pageList.end(), pred: pageEquals(c)), last: pageList.end());
438
439 if (pageList.empty()) {
440 currentPage = nullptr;
441 emit q->currentChanged(index: -1);
442 } else if (removeCurrent) {
443 currentPage = nullptr;
444 q->setCurrentIndex(0);
445 }
446}
447
448/*!
449 Removes the item at position \a index from the toolbox. Note that
450 the widget is \e not deleted.
451*/
452
453void QToolBox::removeItem(int index)
454{
455 Q_D(QToolBox);
456 if (QWidget *w = widget(index)) {
457 disconnect(sender: w, SIGNAL(destroyed(QObject*)), receiver: this, SLOT(_q_widgetDestroyed(QObject*)));
458 w->setParent(this);
459 // destroy internal data
460 d->_q_widgetDestroyed(object: w);
461 itemRemoved(index);
462 }
463}
464
465
466/*!
467 \property QToolBox::currentIndex
468 \brief the index of the current item
469
470 By default, for an empty toolbox, this property has a value of -1.
471
472 \sa indexOf(), widget()
473*/
474
475
476int QToolBox::currentIndex() const
477{
478 Q_D(const QToolBox);
479 return d->currentPage ? indexOf(widget: d->currentPage->widget) : -1;
480}
481
482/*!
483 Returns a pointer to the current widget, or \nullptr if there is
484 no such item.
485
486 \sa currentIndex(), setCurrentWidget()
487*/
488
489QWidget * QToolBox::currentWidget() const
490{
491 Q_D(const QToolBox);
492 return d->currentPage ? d->currentPage->widget : nullptr;
493}
494
495/*!
496 Makes\a widget the current widget. The \a widget must be an item in this tool box.
497
498 \sa addItem(), setCurrentIndex(), currentWidget()
499 */
500void QToolBox::setCurrentWidget(QWidget *widget)
501{
502 int i = indexOf(widget);
503 if (Q_UNLIKELY(i < 0))
504 qWarning(msg: "QToolBox::setCurrentWidget: widget not contained in tool box");
505 else
506 setCurrentIndex(i);
507}
508
509/*!
510 Returns the widget at position \a index, or \nullptr if there is
511 no such item.
512*/
513
514QWidget *QToolBox::widget(int index) const
515{
516 Q_D(const QToolBox);
517 if (index < 0 || index >= static_cast<int>(d->pageList.size()))
518 return nullptr;
519 return d->pageList[index]->widget;
520}
521
522/*!
523 Returns the index of \a widget, or -1 if the item does not
524 exist.
525*/
526
527int QToolBox::indexOf(const QWidget *widget) const
528{
529 Q_D(const QToolBox);
530 const QToolBoxPrivate::Page *c = (widget ? d->page(widget) : nullptr);
531 if (!c)
532 return -1;
533 const auto it = std::find_if(first: d->pageList.cbegin(), last: d->pageList.cend(), pred: pageEquals(c));
534 if (it == d->pageList.cend())
535 return -1;
536 return static_cast<int>(it - d->pageList.cbegin());
537}
538
539/*!
540 If \a enabled is true then the item at position \a index is enabled; otherwise
541 the item at position \a index is disabled.
542*/
543
544void QToolBox::setItemEnabled(int index, bool enabled)
545{
546 Q_D(QToolBox);
547 QToolBoxPrivate::Page *c = d->page(index);
548 if (!c)
549 return;
550
551 c->button->setEnabled(enabled);
552 if (!enabled && c == d->currentPage) {
553 int curIndexUp = index;
554 int curIndexDown = curIndexUp;
555 const int count = static_cast<int>(d->pageList.size());
556 while (curIndexUp > 0 || curIndexDown < count-1) {
557 if (curIndexDown < count-1) {
558 if (d->page(index: ++curIndexDown)->button->isEnabled()) {
559 index = curIndexDown;
560 break;
561 }
562 }
563 if (curIndexUp > 0) {
564 if (d->page(index: --curIndexUp)->button->isEnabled()) {
565 index = curIndexUp;
566 break;
567 }
568 }
569 }
570 setCurrentIndex(index);
571 }
572}
573
574
575/*!
576 Sets the text of the item at position \a index to \a text.
577
578 If the provided text contains an ampersand character ('&'), a
579 mnemonic is automatically created for it. The character that
580 follows the '&' will be used as the shortcut key. Any previous
581 mnemonic will be overwritten, or cleared if no mnemonic is defined
582 by the text. See the \l {QShortcut#mnemonic}{QShortcut}
583 documentation for details (to display an actual ampersand, use
584 '&&').
585*/
586
587void QToolBox::setItemText(int index, const QString &text)
588{
589 Q_D(QToolBox);
590 QToolBoxPrivate::Page *c = d->page(index);
591 if (c)
592 c->setText(text);
593}
594
595/*!
596 Sets the icon of the item at position \a index to \a icon.
597*/
598
599void QToolBox::setItemIcon(int index, const QIcon &icon)
600{
601 Q_D(QToolBox);
602 QToolBoxPrivate::Page *c = d->page(index);
603 if (c)
604 c->setIcon(icon);
605}
606
607#if QT_CONFIG(tooltip)
608/*!
609 Sets the tooltip of the item at position \a index to \a toolTip.
610*/
611
612void QToolBox::setItemToolTip(int index, const QString &toolTip)
613{
614 Q_D(QToolBox);
615 QToolBoxPrivate::Page *c = d->page(index);
616 if (c)
617 c->setToolTip(toolTip);
618}
619#endif // QT_CONFIG(tooltip)
620
621/*!
622 Returns \c true if the item at position \a index is enabled; otherwise returns \c false.
623*/
624
625bool QToolBox::isItemEnabled(int index) const
626{
627 Q_D(const QToolBox);
628 const QToolBoxPrivate::Page *c = d->page(index);
629 return c && c->button->isEnabled();
630}
631
632/*!
633 Returns the text of the item at position \a index, or an empty string if
634 \a index is out of range.
635*/
636
637QString QToolBox::itemText(int index) const
638{
639 Q_D(const QToolBox);
640 const QToolBoxPrivate::Page *c = d->page(index);
641 return (c ? c->text() : QString());
642}
643
644/*!
645 Returns the icon of the item at position \a index, or a null
646 icon if \a index is out of range.
647*/
648
649QIcon QToolBox::itemIcon(int index) const
650{
651 Q_D(const QToolBox);
652 const QToolBoxPrivate::Page *c = d->page(index);
653 return (c ? c->icon() : QIcon());
654}
655
656#if QT_CONFIG(tooltip)
657/*!
658 Returns the tooltip of the item at position \a index, or an
659 empty string if \a index is out of range.
660*/
661
662QString QToolBox::itemToolTip(int index) const
663{
664 Q_D(const QToolBox);
665 const QToolBoxPrivate::Page *c = d->page(index);
666 return (c ? c->toolTip() : QString());
667}
668#endif // QT_CONFIG(tooltip)
669
670/*! \reimp */
671void QToolBox::showEvent(QShowEvent *e)
672{
673 QWidget::showEvent(event: e);
674}
675
676/*! \reimp */
677void QToolBox::changeEvent(QEvent *ev)
678{
679 Q_D(QToolBox);
680 if (ev->type() == QEvent::StyleChange)
681 d->updateTabs();
682 QFrame::changeEvent(ev);
683}
684
685/*!
686 This virtual handler is called after a new item was added or
687 inserted at position \a index.
688
689 \sa itemRemoved()
690 */
691void QToolBox::itemInserted(int index)
692{
693 Q_UNUSED(index);
694}
695
696/*!
697 This virtual handler is called after an item was removed from
698 position \a index.
699
700 \sa itemInserted()
701 */
702void QToolBox::itemRemoved(int index)
703{
704 Q_UNUSED(index);
705}
706
707/*! \reimp */
708bool QToolBox::event(QEvent *e)
709{
710 return QFrame::event(e);
711}
712
713QT_END_NAMESPACE
714
715#include "moc_qtoolbox.cpp"
716#include "qtoolbox.moc"
717

source code of qtbase/src/widgets/widgets/qtoolbox.cpp