QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskTabBar.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskTabBar.h"
7#include "QskAspect.h"
8#include "QskScrollBox.h"
9#include "QskLinearBox.h"
10#include "QskTabButton.h"
11#include "QskTextOptions.h"
12#include "QskAnimationHint.h"
13#include "QskQuick.h"
14
15#include <qquickwindow.h>
16
17QSK_SUBCONTROL( QskTabBar, Panel )
18
19static inline Qt::Orientation qskOrientation( Qt::Edge edge )
20{
21 if ( ( edge == Qt::TopEdge ) || ( edge == Qt::BottomEdge ) )
22 return Qt::Horizontal;
23 else
24 return Qt::Vertical;
25}
26
27static inline void qskTransposeSizePolicy( QskControl* control )
28{
29 control->setSizePolicy( control->sizePolicy().transposed() );
30}
31
32namespace
33{
34 class ButtonBox final : public QskLinearBox
35 {
36 using Inherited = QskLinearBox;
37
38 public:
39 ButtonBox( Qt::Orientation orientation, QQuickItem* parent )
40 : QskLinearBox( orientation, parent )
41 {
42 setObjectName( QStringLiteral( "QskTabBarLayoutBox" ) );
43 setExtraSpacingAt( Qt::RightEdge | Qt::BottomEdge );
44 }
45
46 void restack( int currentIndex )
47 {
48 /*
49 We can't simply reorder the buttons as the focus tab chain depends on it.
50 Again we have some QML only API ( QQuickKeyNavigationAttached ),
51 that is useless for C++, but nothing like QWidget::setTabOrder.
52 Maybe it makes sense to implement our own navigation in
53 QskControl::keyPressEvent.
54 */
55
56 for ( int i = 0; i < elementCount(); i++ )
57 {
58 if ( auto button = itemAtIndex( i ) )
59 button->setZ( i == currentIndex ? 0.001 : 0.0 );
60 }
61 }
62 };
63
64 class ScrollBox final : public QskScrollBox
65 {
66 using Inherited = QskScrollBox;
67
68 public:
69 ScrollBox( QQuickItem* parent )
70 : QskScrollBox( parent )
71 {
72 setPolishOnResize( true );
73 setWheelEnabled( false );
74 enableAutoTranslation( true );
75
76 setFocusPolicy( Qt::NoFocus );
77
78 connect( this, &ScrollBox::scrollPosChanged,
80 }
81
82 QRectF focusIndicatorClipRect() const override
83 {
84 auto r = clipRect();
85
86 if ( window() )
87 {
88 if ( auto focusItem = window()->activeFocusItem() )
89 {
90 const auto itemRect = mapRectFromItem(
91 focusItem, focusItem->boundingRect() );
92
93 if ( r.intersects( itemRect ) )
94 return QRectF();
95 }
96 }
97
98 return r;
99 }
100
101 QskAnimationHint flickHint() const override
102 {
103 if ( auto tabBar = qobject_cast< const QskTabBar* >( parentItem() ) )
104 {
105 return tabBar->effectiveAnimation( QskAspect::Metric,
106 QskTabBar::Panel, QskAspect::NoState );
107 }
108
109 // should be a skin hint TODO ...
110 return QskAnimationHint( 200, QEasingCurve::OutCubic );
111 }
112
113 QRectF viewContentsRect() const override
114 {
115 return layoutRect();
116 }
117
118 void setOrientation( Qt::Orientation orientation )
119 {
120 setFlickableOrientations( orientation );
121
122 if ( orientation == Qt::Horizontal )
123 setSizePolicy( QskSizePolicy::Ignored, QskSizePolicy::MinimumExpanding );
124 else
125 setSizePolicy( QskSizePolicy::MinimumExpanding, QskSizePolicy::Ignored );
126 }
127
128 void ensureItemVisible( const QQuickItem* item )
129 {
130 if ( qskIsAncestorOf( this, item ) )
131 {
132 const auto pos = mapFromItem( item, QPointF() );
133 ensureVisible( QRectF( pos.x(), pos.y(), item->width(), item->height() ) );
134 }
135 }
136
137 QRectF clipRect() const override
138 {
139 auto r = Inherited::clipRect();
140
141 if ( auto control = qskControlCast( parentItem() ) )
142 r += control->paddingHint( QskTabBar::Panel );
143
144 /*
145 Often the current tab button grows beyond the bounding rectangle
146 of the tab bar, so that it looks like being on top of the tab page
147 border. So we only want to clip in scroll direction.
148 Note: std::numeric_limits< int >::max() does not work - guess
149 some overflow somewhere ...
150 */
151 constexpr qreal expanded = 0.90 * std::numeric_limits< int >::max();
152
153 if ( flickableOrientations() & Qt::Horizontal )
154 {
155 r.setTop( r.top() - 0.5 * expanded );
156 r.setHeight( expanded );
157 }
158 else
159 {
160 r.setLeft( r.left() - 0.5 * expanded );
161 r.setWidth( expanded );
162 }
163
164 return r;
165 }
166
167 protected:
168
169 bool event( QEvent* event ) override
170 {
171 if ( event->type() == QEvent::LayoutRequest )
172 {
173 resetImplicitSize();
174 polish();
175 }
176
177 return Inherited::event( event );
178 }
179
180 void updateLayout() override
181 {
182 auto box = buttonBox();
183
184 auto boxSize = viewContentsRect().size();
185 boxSize = qskConstrainedItemSize( box, boxSize );
186
187 if ( box )
188 box->setSize( boxSize );
189
190 enableAutoTranslation( false );
191
192 setScrollableSize( boxSize );
193 setScrollPos( scrollPos() );
194
195 enableAutoTranslation( true );
196
197 translateButtonBox();
198
199 setClip( width() < boxSize.width() || height() < boxSize.height() );
200 }
201
202 QSizeF layoutSizeHint( Qt::SizeHint which, const QSizeF& constraint ) const override
203 {
204 auto hint = buttonBox()->sizeConstraint( which, constraint );
205
206 if ( which == Qt::MinimumSize )
207 {
208 if ( sizePolicy().horizontalPolicy() & QskSizePolicy::ShrinkFlag )
209 hint.setWidth( -1 );
210
211 if ( sizePolicy().verticalPolicy() & QskSizePolicy::ShrinkFlag )
212 hint.setHeight( -1 );
213 }
214
215 return hint;
216 }
217
218 private:
219
220 inline QskLinearBox* buttonBox() const
221 {
222 return qobject_cast< QskLinearBox* >( childItems().constFirst() );
223 }
224
225 void enableAutoTranslation( bool on )
226 {
227 if ( on )
228 {
229 connect( this, &QskScrollBox::scrollPosChanged,
230 this, &ScrollBox::translateButtonBox );
231 }
232 else
233 {
234 disconnect( this, &QskScrollBox::scrollPosChanged,
235 this, &ScrollBox::translateButtonBox );
236 }
237 }
238
239 void translateButtonBox()
240 {
241 if ( auto box = buttonBox() )
242 {
243 const QPointF pos = viewContentsRect().topLeft() - scrollPos();
244 box->setPosition( pos );
245 }
246 }
247 };
248}
249
250class QskTabBar::PrivateData
251{
252 public:
253 void connectButton( QskTabButton* button, QskTabBar* tabBar, bool on )
254 {
255 if ( on )
256 {
257 connect( button, &QskTabButton::toggled,
258 tabBar, &QskTabBar::adjustCurrentIndex, Qt::UniqueConnection );
259 }
260 else
261 {
262 disconnect( button, &QskTabButton::toggled,
263 tabBar, &QskTabBar::adjustCurrentIndex );
264 }
265 }
266
267 ScrollBox* scrollBox = nullptr;
268 ButtonBox* buttonBox = nullptr;
269 int currentIndex = -1;
270
271 QskTextOptions textOptions;
272};
273
274QskTabBar::QskTabBar( QQuickItem* parent )
275 : Inherited( parent )
276 , m_data( new PrivateData() )
277{
278 setAutoLayoutChildren( true );
279
280 const auto orientation = qskOrientation( edge() );
281
282 if ( orientation == Qt::Horizontal )
283 initSizePolicy( QskSizePolicy::Preferred, QskSizePolicy::Fixed );
284 else
285 initSizePolicy( QskSizePolicy::Fixed, QskSizePolicy::Preferred );
286
287 m_data->scrollBox = new ScrollBox( this );
288 m_data->scrollBox->setOrientation( orientation );
289
290 m_data->buttonBox = new ButtonBox( orientation, m_data->scrollBox );
291 m_data->buttonBox->setSpacing( spacingHint( QskTabBar::Panel ) );
292 m_data->buttonBox->setSizePolicy( QskSizePolicy::Maximum, QskSizePolicy::Maximum );
293
294 connect( this, &QskTabBar::currentIndexChanged,
295 m_data->buttonBox, &ButtonBox::restack, Qt::QueuedConnection );
296}
297
298QskTabBar::QskTabBar( Qt::Edge edge, QQuickItem* parent )
299 : QskTabBar( parent )
300{
301 setEdge( edge );
302}
303
304QskTabBar::~QskTabBar()
305{
306}
307
308void QskTabBar::setEdge( Qt::Edge edge )
309{
310 const auto oldEdge = this->edge();
311
312 setFlagHint( Panel | QskAspect::Style, edge );
313
314 if ( edge == oldEdge )
315 return;
316
317 const auto orientation = qskOrientation( edge );
318
319 if ( orientation != m_data->buttonBox->orientation() )
320 {
321 qskTransposeSizePolicy( this );
322
323 m_data->buttonBox->setOrientation( orientation );
324 qskTransposeSizePolicy( m_data->buttonBox );
325
326 m_data->scrollBox->setOrientation( orientation );
327 }
328
330
331 for ( int i = 0; i < count(); i++ )
332 buttonAt( i )->update();
333
334 Q_EMIT edgeChanged( edge );
335}
336
337void QskTabBar::resetEdge()
338{
339 if ( resetSkinHint( Panel | QskAspect::Style ) )
340 Q_EMIT edgeChanged( edge() );
341}
342
343Qt::Edge QskTabBar::edge() const
344{
345 /*
346 We add a meaningless QskAspect::Vertical bit to avoid that effectivePlacement
347 gets called finally ending up in an endless recursion ...
348 */
349 const auto aspect = Panel | QskAspect::Style | QskAspect::Vertical;
350 return flagHint< Qt::Edge > ( aspect, Qt::TopEdge );
351}
352
353Qt::Orientation QskTabBar::orientation() const
354{
355 return qskOrientation( edge() );
356}
357
358void QskTabBar::setAutoScrollFocusedButton( bool on )
359{
360 if ( m_data->scrollBox->autoScrollFocusItem() != on )
361 {
362 m_data->scrollBox->setAutoScrollFocusedItem( on );
363 Q_EMIT autoScrollFocusedButtonChanged( on );
364 }
365}
366
367bool QskTabBar::autoScrollFocusButton() const
368{
369 return m_data->scrollBox->autoScrollFocusItem();
370}
371
372void QskTabBar::setAutoFitTabs( bool on )
373{
374 const auto orientation = qskOrientation( edge() );
375 int policy = m_data->buttonBox->sizePolicy( orientation );
376
377 if ( ( policy & QskSizePolicy::GrowFlag ) != on )
378 {
379 if ( on )
380 policy |= QskSizePolicy::GrowFlag;
381 else
382 policy &= ~QskSizePolicy::GrowFlag;
383
384 // we need operators for QskSizePolicy::Policy: TODO ...
385 m_data->buttonBox->setSizePolicy(
386 orientation, static_cast< QskSizePolicy::Policy >( policy ) );
387
388 polish();
389
390 Q_EMIT autoFitTabsChanged( on );
391 }
392}
393
394bool QskTabBar::autoFitTabs() const
395{
396 const auto policy = m_data->buttonBox->sizePolicy( orientation() );
397 return ( policy & QskSizePolicy::GrowFlag );
398}
399
400void QskTabBar::setTextOptions( const QskTextOptions& options )
401{
402 if ( options != m_data->textOptions )
403 {
404 // we should consider text options being something for
405 // QskControl - maybe added to the propagation system ???
406
407 m_data->textOptions = options;
408 Q_EMIT textOptionsChanged( options );
409
410 for ( int i = 0; i < count(); i++ )
411 buttonAt( i )->setTextOptions( options );
412 }
413}
414
415QskTextOptions QskTabBar::textOptions() const
416{
417 return m_data->textOptions;
418}
419
420int QskTabBar::addTab( const QString& text )
421{
422 return insertTab( -1, text );
423}
424
425int QskTabBar::insertTab( int index, const QString& text )
426{
427 return insertTab( index, new QskTabButton( text ) );
428}
429
430int QskTabBar::addTab( QskTabButton* button )
431{
432 return insertTab( -1, button );
433}
434
435int QskTabBar::insertTab( int index, QskTabButton* button )
436{
437 auto buttonBox = m_data->buttonBox;
438
439 if ( index < 0 || index >= buttonBox->elementCount() )
440 index = buttonBox->elementCount();
441
442 if ( isComponentComplete() )
443 {
444 if ( count() == 0 )
445 {
446 m_data->currentIndex = 0;
447 button->setChecked( true );
448 }
449 }
450
451 buttonBox->insertItem( index, button );
452 buttonBox->restack( m_data->currentIndex );
453
454 if ( button->textOptions() != m_data->textOptions )
455 button->setTextOptions( m_data->textOptions );
456
457 m_data->connectButton( button, this, true );
458
459 connect( button, &QskAbstractButton::clicked,
460 this, &QskTabBar::handleButtonClick );
461
462 Q_EMIT countChanged( count() );
463
464 return index;
465}
466
467void QskTabBar::removeTab( int index )
468{
469 auto item = m_data->buttonBox->itemAtIndex( index );
470 if ( item == nullptr )
471 return;
472
473 delete item;
474
475 if ( index > m_data->currentIndex )
476 {
477 Q_EMIT countChanged( count() );
478 }
479 else if ( index < m_data->currentIndex )
480 {
481 m_data->currentIndex--;
482
483 Q_EMIT countChanged( count() );
484 Q_EMIT currentIndexChanged( m_data->currentIndex );
485 }
486 else
487 {
488 QskTabButton* nextButton = nullptr;
489 int nextIndex = -1;
490
491 for ( int i = m_data->currentIndex; i >= 0; i-- )
492 {
493 auto button = buttonAt( i );
494 if ( button && button->isEnabled() )
495 {
496 nextButton = button;
497 nextIndex = i;
498
499 break;
500 }
501 }
502
503 if ( nextButton == nullptr )
504 {
505 for ( int i = m_data->currentIndex + 1; i < count(); i++ )
506 {
507 auto button = buttonAt( i );
508 if ( button && button->isEnabled() )
509 {
510 nextButton = button;
511 nextIndex = i;
512
513 break;
514 }
515 }
516 }
517
518 if ( nextButton )
519 {
520 m_data->connectButton( nextButton, this, false );
521 nextButton->setChecked( true );
522 m_data->connectButton( nextButton, this, true );
523 }
524
525 m_data->currentIndex = nextIndex;
526
527 Q_EMIT countChanged( count() );
528 Q_EMIT currentIndexChanged( nextIndex );
529 }
530}
531
532void QskTabBar::clear( bool autoDelete )
533{
534 if ( count() == 0 )
535 return;
536
537 const int idx = currentIndex();
538 m_data->buttonBox->clear( autoDelete );
539
540 Q_EMIT countChanged( count() );
541
542 if ( idx >= 0 )
543 Q_EMIT currentIndexChanged( -1 );
544}
545
546bool QskTabBar::isTabEnabled( int index ) const
547{
548 const auto button = buttonAt( index );
549 return button ? button->isEnabled() : false;
550}
551
552void QskTabBar::setTabEnabled( int index, bool enabled )
553{
554 if ( auto button = buttonAt( index ) )
555 {
556 // what happens, when it is the current button ???
557 button->setEnabled( enabled );
558 }
559}
560
561void QskTabBar::setCurrentIndex( int index )
562{
563 if ( index != m_data->currentIndex )
564 {
565 if ( isComponentComplete() )
566 {
567 auto button = buttonAt( index );
568 if ( button && button->isEnabled() && !button->isChecked() )
569 button->setChecked( true );
570 }
571 else
572 {
573 m_data->currentIndex = index;
574 Q_EMIT currentIndexChanged( m_data->currentIndex );
575 }
576 }
577}
578
579int QskTabBar::currentIndex() const
580{
581 return m_data->currentIndex;
582}
583
584int QskTabBar::count() const
585{
586 return m_data->buttonBox->elementCount();
587}
588
589QskTabButton* QskTabBar::buttonAt( int position )
590{
591 return qobject_cast< QskTabButton* >(
592 m_data->buttonBox->itemAtIndex( position ) );
593}
594
595const QskTabButton* QskTabBar::buttonAt( int position ) const
596{
597 auto that = const_cast< QskTabBar* >( this );
598 return that->buttonAt( position );
599}
600
601QskTabButton* QskTabBar::currentButton()
602{
603 return buttonAt( currentIndex() );
604}
605
606const QskTabButton* QskTabBar::currentButton() const
607{
608 return buttonAt( currentIndex() );
609}
610
611QString QskTabBar::currentButtonText() const
612{
613 return buttonTextAt( currentIndex() );
614}
615
616QString QskTabBar::buttonTextAt( int index ) const
617{
618 if ( const auto button = buttonAt( index ) )
619 return button->text();
620
621 return QString();
622}
623
624int QskTabBar::indexOf( QskTabButton* button ) const
625{
626 return m_data->buttonBox->indexOf( button );
627}
628
629void QskTabBar::ensureButtonVisible( const QskTabButton* button )
630{
631 m_data->scrollBox->ensureItemVisible( button );
632}
633
634void QskTabBar::componentComplete()
635{
637
638 if ( m_data->currentIndex < 0 && count() >= 0 )
639 m_data->currentIndex = 0;
640
641 if ( auto button = buttonAt( m_data->currentIndex ) )
642 {
643 if ( button->isEnabled() && !button->isChecked() )
644 button->setChecked( true );
645 }
646}
647
648void QskTabBar::adjustCurrentIndex()
649{
650 int index = -1;
651
652 for ( int i = 0; i < count(); i++ )
653 {
654 if ( auto button = buttonAt( i ) )
655 {
656 if ( button->isChecked() )
657 {
658 index = i;
659 break;
660 }
661 }
662 }
663
664 if ( index != m_data->currentIndex )
665 {
666 m_data->currentIndex = index;
667 Q_EMIT currentIndexChanged( index );
668 }
669}
670
671void QskTabBar::handleButtonClick()
672{
673 if ( auto button = qobject_cast< const QskTabButton* >( sender() ) )
674 {
675 const auto index = indexOf( button );
676
677 if ( index >= 0 )
678 Q_EMIT buttonClicked( index );
679 }
680}
681
682QskAspect::Subcontrol QskTabBar::substitutedSubcontrol(
683 QskAspect::Subcontrol subControl ) const
684{
685 if ( subControl == QskBox::Panel )
686 return QskTabBar::Panel;
687
688 return Inherited::substitutedSubcontrol( subControl );
689}
690
692{
693 switch ( edge() )
694 {
695 case Qt::LeftEdge:
696 return QskAspect::Left;
697
698 case Qt::RightEdge:
699 return QskAspect::Right;
700
701 case Qt::TopEdge:
702 return QskAspect::Top;
703
704 case Qt::BottomEdge:
705 return QskAspect::Bottom;
706 }
707
709}
710
711#include "moc_QskTabBar.cpp"
Variation
Some sort of variation.
Definition QskAspect.h:82
@ NoVariation
Definition QskAspect.h:83
Subcontrol
For use within the rendering or lay-outing of a specific QskSkinnable.
Definition QskAspect.h:104
Base class of all controls.
Definition QskControl.h:23
void focusIndicatorRectChanged()
void setSizePolicy(QskSizePolicy)
QskSizePolicy sizePolicy
Definition QskControl.h:43
void resetImplicitSize()
Definition QskItem.cpp:721
void componentComplete() override
Definition QskItem.cpp:272
Layout stringing items in rows and columns.
QMarginsF paddingHint(QskAspect, QskSkinHintStatus *=nullptr) const
Retrieves a padding hint.
bool resetSkinHint(QskAspect)
Remove a hint from the local hint table.
bool setFlagHint(QskAspect, int flag)
Sets a flag hint.
QskAspect::Variation effectiveVariation() const override