diff --git a/examples/gallery/button/ButtonPage.cpp b/examples/gallery/button/ButtonPage.cpp index 540d4f8d..0f47ef0e 100644 --- a/examples/gallery/button/ButtonPage.cpp +++ b/examples/gallery/button/ButtonPage.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -96,6 +97,9 @@ namespace auto button3 = new QskCheckBox( "Error", this ); button3->setSkinStateFlag( QskCheckBox::Error ); + new QskRadioBox( { "One", "Two", "Three" }, this ); + auto radios = new QskRadioBox( { "One", "Two", "Three" }, this ); + radios->setLayoutMirroring(true); } }; } diff --git a/skins/material3/QskMaterial3Skin.cpp b/skins/material3/QskMaterial3Skin.cpp index 6265185f..816b8a39 100644 --- a/skins/material3/QskMaterial3Skin.cpp +++ b/skins/material3/QskMaterial3Skin.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -139,6 +140,7 @@ namespace void setupPageIndicator(); void setupPopup(); void setupProgressBar(); + void setupRadioBox(); void setupPushButton(); void setupScrollView(); void setupSegmentedBar(); @@ -199,6 +201,7 @@ void Editor::setup() setupPopup(); setupProgressBar(); setupPushButton(); + setupRadioBox(); setupScrollView(); setupSegmentedBar(); setupSeparator(); @@ -463,6 +466,40 @@ void Editor::setupProgressBar() setGradient( Q::Bar | Q::Disabled, m_pal.onSurface38 ); } +void Editor::setupRadioBox() +{ + using Q = QskRadioBox; + using A = QskAspect; + + setStrutSize( Q::Text, {100, 20 }); + setStrutSize( Q::Radio, {20, 20 }); + setStrutSize( Q::Symbol, {10, 10 }); + setStrutSize( Q::Ripple | Q::Focused, { 40, 40 }); + + setSpacing(Q::Panel, 10); + + setColor( Q::Text, m_pal.onBackground ); + + setBoxShape(Q::Radio, 20); + setBoxBorderMetrics( Q::Radio, 2_dp ); + setBoxBorderColors( Q::Radio, m_pal.onBackground ); + setBoxBorderColors( Q::Radio | Q::Selected, m_pal.primary ); + + setBoxShape(Q::Ripple, 40); + + setColor( Q::Symbol, m_pal.primary ); + setColor( Q::Ripple | Q::Focused, + stateLayerColor( m_pal.onSurface, m_pal.focusOpacity ) ); + setColor( Q::Ripple | Q::Selected | Q::Focused, + // stateLayerColor( m_pal.primary, m_pal.focusOpacity ) ); + Qt::red); + + setMargin( Q::Text, QskMargins(10, 0,0,0)); + + setAlignment( Q::Text, Qt::AlignBottom ); + setAnimation(Q::Ripple | A::Metric, 1000); +} + void Editor::setupFocusIndicator() { using Q = QskFocusIndicator; diff --git a/src/controls/QskRadioBox.cpp b/src/controls/QskRadioBox.cpp new file mode 100644 index 00000000..b99541af --- /dev/null +++ b/src/controls/QskRadioBox.cpp @@ -0,0 +1,201 @@ +#include "QskRadioBox.h" +#include "QskEvent.h" +#include + +QSK_SUBCONTROL( QskRadioBox, Panel ) +QSK_SUBCONTROL( QskRadioBox, Radio ) +QSK_SUBCONTROL( QskRadioBox, Symbol ) +QSK_SUBCONTROL( QskRadioBox, Text ) +QSK_SUBCONTROL( QskRadioBox, Ripple ) + +QSK_STATE( QskRadioBox, Selected, QskAspect::FirstUserState ) +QSK_STATE( QskRadioBox, Pressed, QskAspect::FirstUserState << 1) +QSK_STATE( QskRadioBox, Focused, QskAspect::FirstUserState << 2) + +class QskRadioBox::PrivateData { +public: + QStringList items; + int selectedIndex = -1; + int pressedIndex = -1; + int focusedIndex = -1; +}; + +QskRadioBox::QskRadioBox( QQuickItem* parent ) : + Inherited( parent ), + m_data( new PrivateData{} ) +{ + setFocusPolicy( Qt::NoFocus ); + setAcceptedMouseButtons( Qt::LeftButton ); + + connect(this, &QskRadioBox::itemsChanged, this, + [this]( const QStringList& items ) { + if( items.count() > 0 ) { + setFocusPolicy( Qt::StrongFocus ); + } else { + setFocusPolicy( Qt::NoFocus ); + } + }); +} + +QskRadioBox::QskRadioBox( const QStringList& list, QQuickItem* parent ) : + QskRadioBox( parent ) +{ + setItems( list ); +} + +QskRadioBox::QskRadioBox( const QStringList& items, + int selectedIndex, + QQuickItem* parent ) : + QskRadioBox( items, parent ) +{ + if( selectedIndex >= 0 && selectedIndex < items.count() ) + { + m_data->selectedIndex = selectedIndex; + } +} + +int QskRadioBox::selectedIndex() const { + return m_data->selectedIndex; +} + +const QStringList& QskRadioBox::items() const { + return m_data->items; +} + +int QskRadioBox::focusedIndex() const { + return m_data->focusedIndex; +} + +void QskRadioBox::setSelectedIndex( int index ) { + if( index == m_data->selectedIndex + || index >= m_data->items.count() ) { + return; + } + + if( index < 0 ) { + m_data->selectedIndex = -1; + } else { + m_data->selectedIndex = index; + } + + selectedIndexChanged( m_data->selectedIndex ); +} + +void QskRadioBox::setItems( const QStringList& items ){ + if( m_data->items == items ) { + return; + } + + m_data->items = items; + itemsChanged( items ); + setSelectedIndex( m_data->selectedIndex ); +} + +void QskRadioBox::keyPressEvent( QKeyEvent* event ) +{ + switch ( event->key() ) + { + case Qt::Key_Up: + case Qt::Key_Left: + m_data->selectedIndex = qMax(m_data->selectedIndex - 1, 0); + m_data->focusedIndex = m_data->selectedIndex; + setSkinStateFlag( QskRadioBox::Selected ); + event->setAccepted( true ); + update(); + return; + case Qt::Key_Down: + case Qt::Key_Right: + m_data->selectedIndex = qMin(m_data->selectedIndex + 1, items().size() - 1); + m_data->focusedIndex = m_data->selectedIndex; + setSkinStateFlag( QskRadioBox::Selected ); + event->setAccepted( true ); + update(); + return; + case Qt::Key_Select: + case Qt::Key_Return: + case Qt::Key_Space: + m_data->selectedIndex = m_data->focusedIndex; + setSkinStateFlag( QskRadioBox::Selected ); + setSkinStateFlag( QskRadioBox::Pressed ); + event->setAccepted( true ); + update(); + return; + } + + auto nextTabIndex = m_data->focusedIndex; + nextTabIndex += qskFocusChainIncrement( event ); + if( nextTabIndex >= items().size() + || nextTabIndex < 0 ) { + Inherited::keyPressEvent( event ); + } else { + m_data->focusedIndex = nextTabIndex; + setSkinStateFlag( QskRadioBox::Focused ); + event->setAccepted( true ); + update(); + } +} + +void QskRadioBox::keyReleaseEvent( QKeyEvent* e ) +{ + setSkinStateFlag( QskRadioBox::Pressed, false ); + e->setAccepted( true ); + update(); +} + +void QskRadioBox::mousePressEvent( QMouseEvent* e ) +{ + auto indexAtPosition = indexAt(e->localPos()); + + m_data->pressedIndex = indexAtPosition; + m_data->selectedIndex = -1; + + m_data->focusedIndex = indexAtPosition; + + setSkinStateFlag( QskRadioBox::Pressed ); + + e->setAccepted( true ); + update(); +} + +void QskRadioBox::mouseReleaseEvent( QMouseEvent* e ) +{ + setSkinStateFlag( QskRadioBox::Pressed, false ); + + auto index = indexAt( e->localPos() ); + if( index == m_data->pressedIndex ) { + setSelectedIndex( index ); + } + + e->setAccepted( true ); + update(); +} + +void QskRadioBox::focusInEvent( QFocusEvent* e ) { + if( e->reason() == Qt::TabFocusReason ) { + m_data->focusedIndex = 0; + } else if( e->reason() == Qt::BacktabFocusReason ) { + m_data->focusedIndex = items().size() - 1; + } + + setSkinStateFlag( Focused ); + Inherited::focusInEvent( e ); +} + +void QskRadioBox::focusOutEvent( QFocusEvent* e ) { + m_data->focusedIndex = -1; + setSkinStateFlag( Focused, false ); + update(); + Inherited::focusOutEvent( e ); +} + +int QskRadioBox::indexAt( const QPointF& target ) const { + auto itemHeight = contentsRect().height() / items().size(); + auto index = target.y() / itemHeight; + + if( index < 0 || index >= items().size() ) + return -1; + + return index; +} + +#include "moc_QskRadioBox.cpp" diff --git a/src/controls/QskRadioBox.h b/src/controls/QskRadioBox.h new file mode 100644 index 00000000..4e5ef368 --- /dev/null +++ b/src/controls/QskRadioBox.h @@ -0,0 +1,61 @@ +#ifndef QSK_RADIO_BOX_H +#define QSK_RADIO_BOX_H + +#include "QskControl.h" + +#include + +class QSK_EXPORT QskRadioBox : public QskControl +{ + Q_OBJECT + + Q_PROPERTY( int selectedIndex + READ selectedIndex + WRITE setSelectedIndex + NOTIFY selectedIndexChanged FINAL ) + + Q_PROPERTY( QStringList items + READ items + WRITE setItems + NOTIFY itemsChanged FINAL ) + + using Inherited = QskControl; + + public: + QSK_SUBCONTROLS( Panel, Radio, Symbol, Text, Ripple ) + QSK_STATES( Selected, Pressed, Focused ) + + QskRadioBox( QQuickItem* parent = nullptr ); + QskRadioBox( const QStringList&, QQuickItem* parent = nullptr ); + QskRadioBox( const QStringList&, int, QQuickItem* parent = nullptr ); + + int selectedIndex() const; + const QStringList& items() const; + int focusedIndex() const; + + public Q_SLOTS: + void setSelectedIndex( int ); + void setItems( const QStringList& ); + + Q_SIGNALS: + void selectedIndexChanged( int ); + void itemsChanged( const QStringList& ); + + protected: + void keyPressEvent( QKeyEvent* ) override; + void keyReleaseEvent( QKeyEvent* ) override; + + void mousePressEvent( QMouseEvent* ) override; + void mouseReleaseEvent( QMouseEvent* ) override; + + void focusInEvent( QFocusEvent* ) override; + void focusOutEvent( QFocusEvent* ) override; + + int indexAt( const QPointF& ) const; + + private: + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; + +#endif diff --git a/src/controls/QskRadioBoxSkinlet.cpp b/src/controls/QskRadioBoxSkinlet.cpp new file mode 100644 index 00000000..d0bff61b --- /dev/null +++ b/src/controls/QskRadioBoxSkinlet.cpp @@ -0,0 +1,227 @@ +#include "QskRadioBoxSkinlet.h" + +#include "QskAspect.h" +#include "QskRadioBox.h" + +#include "QskStandardSymbol.h" +#include "QskColorFilter.h" +#include "QskGraphic.h" +#include "QskFunctions.h" +#include "QskSkin.h" +#include + +namespace { + using Q = QskRadioBox; +}; + +QskRadioBoxSkinlet::QskRadioBoxSkinlet( QskSkin* ) +{ + setNodeRoles( { PanelRole, RadioRole, SymbolRole, TextRole, RippleRole } ); +} + +QskRadioBoxSkinlet::~QskRadioBoxSkinlet() +{ +} + +QRectF QskRadioBoxSkinlet::subControlRect( const QskSkinnable* skinnable, + const QRectF& contentsRect, QskAspect::Subcontrol subcontrol) const +{ + auto radio = static_cast( skinnable ); + + if( subcontrol == Q::Ripple ) { + auto result = contentsRect; + auto lh = lineHeight( radio ); + auto spacing = radio->spacingHint(Q::Panel); + result.setSize( radio->strutSizeHint( subcontrol ) ); + result.moveTop( (lh + spacing) * radio->focusedIndex() + - (result.size().height() - lh ) / 2); + result.moveLeft(( radio->strutSizeHint( Q::Radio ).width() + - result.width()) /2); + return result; + } + + return contentsRect; +} + +QSizeF QskRadioBoxSkinlet::sizeHint( const QskSkinnable* skinnable, + Qt::SizeHint, const QSizeF& ) const +{ + auto radio = static_cast( skinnable ); + + const auto font = skinnable->effectiveFont( Q::Text ); + const auto textMargins = skinnable->marginHint( Q::Text ); + const auto buttonMargins = skinnable->marginHint( Q::Radio ); + const auto symbolMargins = skinnable->marginHint( Q::Symbol ); + + qreal maxTextWidth = 0; + for(auto& item : radio->items() ) { + maxTextWidth = std::max( maxTextWidth, qskHorizontalAdvance( font, item ) ); + } + + auto radioWidth = radio->strutSizeHint(Q::Radio).width(); + auto symbolWidth = radio->strutSizeHint(Q::Symbol).width(); + + maxTextWidth += textMargins.left() + textMargins.right(); + radioWidth += buttonMargins.left() + buttonMargins.right(); + symbolWidth += symbolMargins.left() + symbolMargins.right(); + + auto spacing = radio->spacingHint(Q::Panel); + return QSizeF( maxTextWidth + qMax(radioWidth, symbolWidth), + ( lineHeight( radio ) + spacing ) * radio->items().size() + - spacing ); +} + +QSGNode* QskRadioBoxSkinlet::updateSubNode( const QskSkinnable* skinnable, + quint8 nodeRole, QSGNode* node) const +{ + auto radioButtons = static_cast( skinnable ); + + switch( nodeRole ) + { + case PanelRole: + return updateBoxNode( skinnable, node, Q::Panel ); + + case RadioRole: + return updateSeriesNode( radioButtons, Q::Radio, node ); + + case SymbolRole: + return updateSeriesNode( radioButtons, Q::Symbol, node ); + + case TextRole: + return updateSeriesNode( radioButtons, Q::Text, node ); + + case RippleRole: + return updateBoxNode( radioButtons, node, Q::Ripple ); + }; + + return Inherited::updateSubNode( skinnable, nodeRole, node ); +} + +qreal QskRadioBoxSkinlet::lineHeight(const QskRadioBox* target) const { + auto strutHight = qMax( target->strutSizeHint( Q::Radio ).height(), + target->strutSizeHint( Q::Text ).height() ); + const auto textMargins = target->marginHint( Q::Text ); + auto fontHeight = target->effectiveFontHeight( Q::Text ); + fontHeight += textMargins.top() + textMargins.bottom(); + + return qMax( strutHight, fontHeight ); +} + + +int QskRadioBoxSkinlet::sampleCount( const QskSkinnable* skinnable, + QskAspect::Subcontrol ) const { + const auto radio = static_cast< const QskRadioBox* >( skinnable ); + return radio->items().count(); +} + +QRectF QskRadioBoxSkinlet::radioRect( const QskRadioBox* radio, + const QskAspect::Subcontrol target, + const QRectF& rect, int index ) const { + auto result = rect; + result.setSize( radio->strutSizeHint( target ) ); + + auto spacing = radio->spacingHint(Q::Panel); + result.moveTop( ( lineHeight( radio ) + spacing ) * index + + (lineHeight(radio) - result.size().height()) / 2); + + if( radio->layoutMirroring() ) { + result.moveRight( rect.width() ); + } else { + result.moveLeft((radio->strutSizeHint( Q::Radio ).width() + - result.width()) / 2); + } + + return result; +} + +QRectF QskRadioBoxSkinlet::textRect( const QskRadioBox* radio, + const QRectF& rect, int index ) const { + QRectF result = rect; + auto spacing = radio->spacingHint(Q::Panel); + auto lh = lineHeight( radio ); + const auto textMargins = radio->marginHint( Q::Text ); + + result.setSize( { radio->strutSizeHint( Q::Text ).width(), lh } ); + + + result.moveTop( index * ( lh + spacing ) + + lh - radio->effectiveFontHeight(Q::Text) + + textMargins.top()); + + if(!radio->layoutMirroring()) { + auto symbolWidth = radioRect( radio, Q::Symbol, rect, index ).width(); + auto radioWidth = radioRect( radio, Q::Radio, rect, index ).width(); + result.moveLeft( qMax(symbolWidth, radioWidth) + textMargins.left()); + } + + return result; +} + +QRectF QskRadioBoxSkinlet::sampleRect( const QskSkinnable* skinnable, + const QRectF& rect, QskAspect::Subcontrol subcontrol, + int index ) const { + const auto radio = static_cast< const QskRadioBox* >( skinnable ); + + if( subcontrol == Q::Text ) { + return textRect( radio, rect, index ); + } + + return radioRect( radio, subcontrol, rect, index); +} + +QskAspect::States QskRadioBoxSkinlet::sampleStates( const QskSkinnable* skinnable, + QskAspect::Subcontrol subControl, int index ) const { + auto radioButtons = static_cast( skinnable ); + auto states = Inherited::sampleStates( skinnable, subControl, index ); + + if( radioButtons->selectedIndex() == index ) { + return states | Q::Selected; + } + + return states; +} + +QSGNode* QskRadioBoxSkinlet::updateSampleNode( const QskSkinnable* skinnable, + QskAspect::Subcontrol subcontrol, int index, QSGNode* node ) const { + auto radioButtons = static_cast( skinnable ); + + auto rect = sampleRect( skinnable, radioButtons->contentsRect(), + subcontrol, index ); + + if( subcontrol == Q::Text ) { + return QskSkinlet::updateTextNode( radioButtons, + node, + rect, + Qt::AlignLeft, + radioButtons->items()[index], + subcontrol); + } else if (subcontrol == Q::Radio) { + return QskSkinlet::updateBoxNode(radioButtons, + node, + rect, + subcontrol); + } else if( subcontrol == Q::Symbol ) { + auto symbol = QskStandardSymbol::NoSymbol; + auto color = radioButtons->color( subcontrol ).rgb(); + + if( radioButtons->selectedIndex() == index ) { + symbol = QskStandardSymbol::Bullet; + color = radioButtons->color( subcontrol | Q::Selected ).rgb(); + } + + auto graphic = radioButtons->effectiveSkin()->symbol( symbol ); + + /* + Our default skins do not have the concept of colorRoles + implemented. Until then we do the recoloring manually here + */ + QskColorFilter filter; + filter.addColorSubstitution( Qt::black, color ); + + QskGraphic::fromGraphic( graphic, filter ); + + return updateGraphicNode( radioButtons, node, graphic, filter, rect ); + } + + return node; +} diff --git a/src/controls/QskRadioBoxSkinlet.h b/src/controls/QskRadioBoxSkinlet.h new file mode 100644 index 00000000..08fdb8bc --- /dev/null +++ b/src/controls/QskRadioBoxSkinlet.h @@ -0,0 +1,57 @@ +#ifndef QSK_RADIO_BOX_SKINLET_H +#define QSK_RADIO_BOX_SKINLET_H + +#include "QskSkinlet.h" + +class QskRadioBox; + +class QSK_EXPORT QskRadioBoxSkinlet : public QskSkinlet +{ + Q_GADGET + + using Inherited = QskSkinlet; + + public: + enum NodeRole + { + PanelRole, + RadioRole, + SymbolRole, + TextRole, + RippleRole, + + RoleCount + }; + + Q_INVOKABLE QskRadioBoxSkinlet( QskSkin* = nullptr ); + ~QskRadioBoxSkinlet() override; + + QRectF subControlRect( const QskSkinnable*, + const QRectF&, QskAspect::Subcontrol ) const override; + + QSizeF sizeHint( const QskSkinnable*, + Qt::SizeHint, const QSizeF& ) const override; + + int sampleCount( const QskSkinnable*, QskAspect::Subcontrol ) const override; + + QRectF sampleRect( const QskSkinnable*, + const QRectF&, QskAspect::Subcontrol, int index ) const override; + + QRectF textRect( const QskRadioBox*, const QRectF&, int ) const; + QRectF radioRect( const QskRadioBox*, const QskAspect::Subcontrol target, const QRectF&, int ) const; + + QskAspect::States sampleStates( const QskSkinnable*, + QskAspect::Subcontrol, int index ) const override; + + protected: + QSGNode* updateSubNode( const QskSkinnable*, + quint8 nodeRole, QSGNode* ) const override; + + QSGNode* updateSampleNode( const QskSkinnable*, + QskAspect::Subcontrol, int index, QSGNode* ) const override; + + private: + qreal lineHeight( const QskRadioBox* target ) const; +}; + +#endif diff --git a/src/controls/QskSkin.cpp b/src/controls/QskSkin.cpp index 914b8c41..47eb8f0d 100644 --- a/src/controls/QskSkin.cpp +++ b/src/controls/QskSkin.cpp @@ -56,6 +56,9 @@ QSK_QT_PRIVATE_END #include "QskProgressBar.h" #include "QskProgressBarSkinlet.h" +#include "QskRadioBox.h" +#include "QskRadioBoxSkinlet.h" + #include "QskPushButton.h" #include "QskPushButtonSkinlet.h" @@ -178,6 +181,7 @@ QskSkin::QskSkin( QObject* parent ) declareSkinlet< QskTextLabel, QskTextLabelSkinlet >(); declareSkinlet< QskTextInput, QskTextInputSkinlet >(); declareSkinlet< QskProgressBar, QskProgressBarSkinlet >(); + declareSkinlet< QskRadioBox, QskRadioBoxSkinlet >(); const QFont font = QGuiApplication::font(); setupFonts( font.family(), font.weight(), font.italic() ); diff --git a/src/graphic/QskStandardSymbol.cpp b/src/graphic/QskStandardSymbol.cpp index 08176cfb..0299dee1 100644 --- a/src/graphic/QskStandardSymbol.cpp +++ b/src/graphic/QskStandardSymbol.cpp @@ -206,6 +206,12 @@ static void qskCrossMarkGraphic( QPainter* painter ) painter->drawLine( 0.0, 1.0, 1.0, 0.0 ); } +static void qskBulletGraphic( QPainter* painter ) +{ + painter->setPen( QPen( Qt::black, 1.0 ) ); + painter->drawEllipse( QRectF( 0.0, 0.0, 1.0, 1.0 ) ); +} + QskGraphic QskStandardSymbol::graphic( Type symbolType ) { static QskGraphic graphics[ SymbolTypeCount ]; @@ -263,6 +269,11 @@ QskGraphic QskStandardSymbol::graphic( Type symbolType ) case QskStandardSymbol::SegmentedBarCheckMark: { qskCheckMarkGraphic( &painter ); + break; + } + case QskStandardSymbol::Bullet: + { + qskBulletGraphic( &painter ); break; } case QskStandardSymbol::NoSymbol: diff --git a/src/graphic/QskStandardSymbol.h b/src/graphic/QskStandardSymbol.h index 78f5a11d..196e340e 100644 --- a/src/graphic/QskStandardSymbol.h +++ b/src/graphic/QskStandardSymbol.h @@ -34,6 +34,8 @@ namespace QskStandardSymbol ComboBoxSymbolPopupClosed, ComboBoxSymbolPopupOpen, + Bullet, + SymbolTypeCount }; diff --git a/src/src.pro b/src/src.pro index 7ba0bd8b..eb51ddab 100644 --- a/src/src.pro +++ b/src/src.pro @@ -205,6 +205,8 @@ HEADERS += \ controls/QskQuick.h \ controls/QskQuickItem.h \ controls/QskQuickItemPrivate.h \ + controls/QskRadioBox.h \ + controls/QskRadioBoxSkinlet.h \ controls/QskScrollArea.h \ controls/QskScrollBox.h \ controls/QskScrollView.h \ @@ -297,6 +299,8 @@ SOURCES += \ controls/QskScrollArea.cpp \ controls/QskScrollBox.cpp \ controls/QskScrollView.cpp \ + controls/QskRadioBox.cpp \ + controls/QskRadioBoxSkinlet.cpp \ controls/QskScrollViewSkinlet.cpp \ controls/QskSegmentedBar.cpp \ controls/QskSegmentedBarSkinlet.cpp \