diff --git a/examples/gallery/button/ButtonPage.cpp b/examples/gallery/button/ButtonPage.cpp index eb8d19d9..b62865ce 100644 --- a/examples/gallery/button/ButtonPage.cpp +++ b/examples/gallery/button/ButtonPage.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -118,6 +119,8 @@ namespace : QskLinearBox( Qt::Horizontal, parent ) { setSpacing( 20 ); + setSizePolicy( Qt::Vertical, QskSizePolicy::Fixed ); + setExtraSpacingAt( Qt::LeftEdge | Qt::RightEdge | Qt::BottomEdge ); for ( auto orientation : { Qt::Vertical, Qt::Horizontal } ) @@ -143,7 +146,7 @@ namespace CheckButtonBox( QQuickItem* parent = nullptr ) : QskLinearBox( Qt::Horizontal, 2, parent ) { - setSpacing( 40 ); + setSpacing( 20 ); setExtraSpacingAt( Qt::LeftEdge | Qt::RightEdge | Qt::BottomEdge ); auto button1 = new QskCheckBox( "Options 1", this ); @@ -156,6 +159,22 @@ namespace button3->setSkinStateFlag( QskCheckBox::Error ); } }; + + class RadioButtonBox : public QskLinearBox + { + public: + RadioButtonBox( QQuickItem* parent = nullptr ) + : QskLinearBox( Qt::Horizontal, parent ) + { + setSpacing( 20 ); + setExtraSpacingAt( Qt::LeftEdge | Qt::RightEdge | Qt::BottomEdge ); + + new QskRadioBox( { "One", "Two", "Three" }, this ); + + auto radioBox = new QskRadioBox( { "One", "Two", "Three" }, this ); + radioBox->setLayoutMirroring( true ); + } + }; } ButtonPage::ButtonPage( QQuickItem* parent ) @@ -171,5 +190,8 @@ void ButtonPage::populate() new QskSeparator( Qt::Horizontal, this ); new SwitchButtonBox( this ); new QskSeparator( Qt::Horizontal, this ); - new CheckButtonBox( this ); + + auto hBox = new QskLinearBox( Qt::Horizontal, this ); + new CheckButtonBox( hBox ); + new RadioButtonBox( hBox ); } diff --git a/examples/gallery/dialog/DialogPage.cpp b/examples/gallery/dialog/DialogPage.cpp index 8bd36eb5..46f750d5 100644 --- a/examples/gallery/dialog/DialogPage.cpp +++ b/examples/gallery/dialog/DialogPage.cpp @@ -14,51 +14,39 @@ namespace { class Box : public QskGridBox { - public: + public: Box( QQuickItem* parent ) : QskGridBox( parent ) { auto* messageButton = new QskPushButton( "message", this ); - connect( messageButton, &QskPushButton::clicked, this, []() - { - qskDialog->message( "message", "text", QskStandardSymbol::Ok ); - } ); + connect( messageButton, &QskPushButton::clicked, this, + []() { qskDialog->message( "message", "text", QskStandardSymbol::Ok ); } ); auto* informationButton = new QskPushButton( "information", this ); - connect( informationButton, &QskPushButton::clicked, this, []() - { - qskDialog->information( "information", "text" ); - } ); + connect( informationButton, &QskPushButton::clicked, this, + []() { qskDialog->information( "information", "text" ); } ); auto* warningButton = new QskPushButton( "warning", this ); - connect( warningButton, &QskPushButton::clicked, this, []() - { - qskDialog->warning( "warning", "text" ); - } ); + connect( warningButton, &QskPushButton::clicked, this, + []() { qskDialog->warning( "warning", "text" ); } ); auto* criticalButton = new QskPushButton( "critical", this ); - connect( criticalButton, &QskPushButton::clicked, this, []() - { - qskDialog->critical( "critical", "text" ); - } ); + connect( criticalButton, &QskPushButton::clicked, this, + []() { qskDialog->critical( "critical", "text" ); } ); auto* questionButton = new QskPushButton( "question", this ); - connect( questionButton, &QskPushButton::clicked, this, []() - { - qskDialog->question( "question", "text" ); - } ); + connect( questionButton, &QskPushButton::clicked, this, + []() { qskDialog->question( "question", "text" ); } ); auto* selectButton = new QskPushButton( "select", this ); - connect( selectButton, &QskPushButton::clicked, this, []() - { - qskDialog->select( "select", "text", { "yes", "no", "maybe" } ); - } ); + connect( selectButton, &QskPushButton::clicked, this, + []() { qskDialog->select( "select", "text", { "yes", "no", "maybe" } ); } ); addItem( messageButton, 0, 0 ); addItem( informationButton, 0, 1 ); diff --git a/examples/gallery/selector/SelectorPage.cpp b/examples/gallery/selector/SelectorPage.cpp index d617641e..974a0c5d 100644 --- a/examples/gallery/selector/SelectorPage.cpp +++ b/examples/gallery/selector/SelectorPage.cpp @@ -32,7 +32,7 @@ namespace { auto bar = new QskSegmentedBar( orientation, this ); - for ( const auto text: texts ) + for ( const auto text : texts ) bar->addOption( {}, text ); } diff --git a/skins/material3/QskMaterial3Skin.cpp b/skins/material3/QskMaterial3Skin.cpp index da94592e..519dc20a 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(); @@ -464,6 +467,44 @@ void Editor::setupProgressBar() setGradient( Q::Bar | Q::Disabled, m_pal.onSurface38 ); } +void Editor::setupRadioBox() +{ + using Q = QskRadioBox; + using A = QskAspect; + + setAnimation( Q::Ripple | A::Metric | A::Position, qskDuration ); + + setSpacing( Q::Panel, 10_dp ); + + setStrutSize( Q::Button, { 20_dp, 20_dp } ); + setStrutSize( Q::Symbol, { 10_dp, 10_dp } ); + setStrutSize( Q::Ripple, { 40_dp, 40_dp } ); + + setAlignment( Q::Symbol, Qt::AlignCenter ); + setAlignment( Q::Text, Qt::AlignBottom ); + + setMargin( Q::Text, QskMargins( 10_dp, 0, 10_dp, 0 ) ); + + setBoxShape( Q::Button, 20_dp ); + setBoxShape( Q::Ripple, 40_dp ); + setBoxBorderMetrics( Q::Button, 2_dp ); + setBoxBorderColors( Q::Button, m_pal.onBackground ); + setColor( Q::Text, m_pal.onBackground ); + setColor( Q::Symbol, m_pal.primary ); + setColor( Q::Ripple, stateLayerColor( m_pal.onSurface, m_pal.focusOpacity ) ); + + // Selected + setColor( Q::Ripple | Q::Selected, + stateLayerColor( m_pal.primary, m_pal.focusOpacity ) ); + setBoxBorderColors( Q::Button | Q::Selected, m_pal.primary ); + + // Disabled + setBoxBorderColors( Q::Button | Q::Disabled, m_pal.onSurface38 ); + setBoxBorderColors( Q::Button | Q::Disabled | Q::Selected, m_pal.onSurface38 ); + setColor( Q::Text | Q::Disabled, m_pal.onSurface38 ); + setColor( Q::Symbol | Q::Disabled, m_pal.onSurface38 ); +} + void Editor::setupFocusIndicator() { using Q = QskFocusIndicator; @@ -1336,47 +1377,25 @@ void QskMaterial3Skin::setupFonts() setFont( M3LabelLarge, createFont( "Roboto Medium", 20_dp, 14_dp, 0.1, QFont::Medium ) ); } +void QskMaterial3Skin::setGraphicColor( GraphicRole role, QRgb rgb ) +{ + QskColorFilter colorFilter; + colorFilter.setMask( QskRgb::RGBAMask ); + colorFilter.addColorSubstitution( QskRgb::White, rgb ); + + setGraphicFilter( role, colorFilter ); +} + void QskMaterial3Skin::setupGraphicFilters( const QskMaterial3Theme& palette ) { - QskColorFilter onPrimaryFilter; - onPrimaryFilter.setSubstituteAlphaValue( true ); - onPrimaryFilter.addColorSubstitution( Qt::white, palette.onPrimary ); - setGraphicFilter( GraphicRoleOnPrimary, onPrimaryFilter ); - - QskColorFilter onSecondaryContainerFilter; - onSecondaryContainerFilter.setSubstituteAlphaValue( true ); - onSecondaryContainerFilter.addColorSubstitution( Qt::white, palette.onSecondaryContainer ); - setGraphicFilter( GraphicRoleOnSecondaryContainer, onSecondaryContainerFilter ); - - QskColorFilter onErrorFilter; - onErrorFilter.setSubstituteAlphaValue( true ); - onErrorFilter.addColorSubstitution( Qt::white, palette.onError ); - setGraphicFilter( GraphicRoleOnError, onErrorFilter ); - - QskColorFilter onSurfaceFilter; - onSurfaceFilter.setSubstituteAlphaValue( true ); - onSurfaceFilter.addColorSubstitution( Qt::white, palette.onSurface ); - setGraphicFilter( GraphicRoleOnSurface, onSurfaceFilter ); - - QskColorFilter onSurfaceFilter38; - onSurfaceFilter38.setSubstituteAlphaValue( true ); - onSurfaceFilter38.addColorSubstitution( Qt::white, palette.onSurface38 ); - setGraphicFilter( GraphicRoleOnSurface38, onSurfaceFilter38 ); - - QskColorFilter onSurfaceVariantFilter; - onSurfaceVariantFilter.setSubstituteAlphaValue( true ); - onSurfaceVariantFilter.addColorSubstitution( Qt::white, palette.onSurfaceVariant ); - setGraphicFilter( GraphicRoleOnSurfaceVariant, onSurfaceVariantFilter ); - - QskColorFilter primaryFilter; - primaryFilter.setSubstituteAlphaValue( true ); - primaryFilter.addColorSubstitution( Qt::white, palette.primary ); - setGraphicFilter( GraphicRolePrimary, primaryFilter ); - - QskColorFilter surfaceFilter; - surfaceFilter.setSubstituteAlphaValue( true ); - surfaceFilter.addColorSubstitution( Qt::white, palette.surface ); - setGraphicFilter( GraphicRoleSurface, surfaceFilter ); + setGraphicColor( GraphicRoleOnPrimary, palette.onPrimary ); + setGraphicColor( GraphicRoleOnSecondaryContainer, palette.onSecondaryContainer ); + setGraphicColor( GraphicRoleOnError, palette.onError ); + setGraphicColor( GraphicRoleOnSurface, palette.onSurface ); + setGraphicColor( GraphicRoleOnSurface38, palette.onSurface38 ); + setGraphicColor( GraphicRoleOnSurfaceVariant, palette.onSurfaceVariant ); + setGraphicColor( GraphicRolePrimary, palette.primary ); + setGraphicColor( GraphicRoleSurface, palette.surface ); } #include "moc_QskMaterial3Skin.cpp" diff --git a/skins/material3/QskMaterial3Skin.h b/skins/material3/QskMaterial3Skin.h index b41684a9..221eb538 100644 --- a/skins/material3/QskMaterial3Skin.h +++ b/skins/material3/QskMaterial3Skin.h @@ -157,6 +157,7 @@ class QSK_MATERIAL3_EXPORT QskMaterial3Skin : public QskSkin private: void setupFonts(); void setupGraphicFilters( const QskMaterial3Theme& palette ); + void setGraphicColor( GraphicRole, QRgb ); }; #endif diff --git a/skins/squiek/QskSquiekSkin.cpp b/skins/squiek/QskSquiekSkin.cpp index 9ae9a25f..0a14e8bd 100644 --- a/skins/squiek/QskSquiekSkin.cpp +++ b/skins/squiek/QskSquiekSkin.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -144,6 +145,7 @@ namespace void setupPopup(); void setupProgressBar(); void setupPushButton(); + void setupRadioBox(); void setupScrollView(); void setupSegmentedBar(); void setupSeparator(); @@ -259,6 +261,7 @@ void Editor::setup() setupPopup(); setupProgressBar(); setupPushButton(); + setupRadioBox(); setupScrollView(); setupSegmentedBar(); setupSeparator(); @@ -607,6 +610,39 @@ void Editor::setupPushButton() setAlignment( Q::Graphic, Qt::AlignCenter ); } +void Editor::setupRadioBox() +{ + using Q = QskRadioBox; + + setSpacing(Q::Panel, qskDpiScaled( 10 ) ); + + setStrutSize( Q::Button, { qskDpiScaled( 20 ), qskDpiScaled( 20 ) } ); + setStrutSize( Q::Symbol, { qskDpiScaled( 9 ), qskDpiScaled( 9 ) }) ; + + setBoxShape( Q::Button, qskDpiScaled( 20 ) ); + setBoxShape( Q::Ripple, qskDpiScaled( 40 ) ); + setBoxBorderMetrics( Q::Button, qskDpiScaled( 1 ) ); + + setBoxBorderColors( Q::Button, m_pal.darker125 ); + setBoxBorderColors( Q::Button | Q::Disabled, m_pal.theme ); + + setColor( Q::Text, m_pal.themeForeground ); + setColor( Q::Symbol, m_pal.themeForeground ); + setColor( Q::Panel, m_pal.lighter125 ); + setColor( Q::Panel | Q::Disabled, m_pal.lighter125 ); + + setColor( Q::Button | Q::Disabled, m_pal.lighter110 ); + + setColor( Q::Text | Q::Disabled, m_pal.darker200 ); + + setColor( Q::Symbol | Q::Disabled, m_pal.darker200 ); + + setMargin( Q::Text, QskMargins( qskDpiScaled( 10 ), 0, qskDpiScaled( 10 ), 0 )); + + setAlignment( Q::Symbol, Qt::AlignCenter ); + setAlignment( Q::Text, Qt::AlignBottom ); +} + void Editor::setupDialogButtonBox() { using Q = QskDialogButtonBox; diff --git a/src/common/QskRgbValue.h b/src/common/QskRgbValue.h index 2ff1b55e..17e78333 100644 --- a/src/common/QskRgbValue.h +++ b/src/common/QskRgbValue.h @@ -169,6 +169,7 @@ namespace QskRgb constexpr const QRgb Transparent = 0x00000000; constexpr const QRgb AlphaMask = 0xff000000; constexpr const QRgb ColorMask = 0x00ffffff; + constexpr const QRgb RGBAMask = 0xffffffff; } namespace QskRgb diff --git a/src/controls/QskRadioBox.cpp b/src/controls/QskRadioBox.cpp new file mode 100644 index 00000000..e87835fa --- /dev/null +++ b/src/controls/QskRadioBox.cpp @@ -0,0 +1,272 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#include "QskRadioBox.h" +#include "QskEvent.h" +#include "QskAnimationHint.h" +#include "QskSkinlet.h" + +QSK_SUBCONTROL( QskRadioBox, Panel ) +QSK_SUBCONTROL( QskRadioBox, Button ) +QSK_SUBCONTROL( QskRadioBox, Symbol ) +QSK_SUBCONTROL( QskRadioBox, Text ) +QSK_SUBCONTROL( QskRadioBox, Ripple ) + +QSK_STATE( QskRadioBox, Selected, QskAspect::FirstUserState << 1 ) +QSK_STATE( QskRadioBox, Pressed, QskAspect::FirstUserState << 2 ) +QSK_STATE( QskRadioBox, Focused, QskAspect::FirstUserState << 3 ) + +class QskRadioBox::PrivateData +{ + public: + QStringList items; + + int selectedIndex = -1; + int focusedIndex = -1; + int pressedIndex = -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 ) + { + setFocusPolicy( items.count() > 0 ? Qt::StrongFocus : Qt::NoFocus ); + } + ); + + setFocusedIndex( -1 ); +} + +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; +} + +QskRadioBox::~QskRadioBox() +{ +} + +QRectF QskRadioBox::focusIndicatorRect() const +{ + if( m_data->focusedIndex > -1) + { + auto skinlet = effectiveSkinlet(); + + auto textRect = skinlet->sampleRect( this, + contentsRect(), QskRadioBox::Text, m_data->focusedIndex ); + + auto buttonRect = skinlet->sampleRect( this, + contentsRect(), QskRadioBox::Button, m_data->focusedIndex ); + + if( textRect == QRectF() ) + return buttonRect; + + auto result = QRectF( + qMin( textRect.x(), buttonRect.x() ), + qMin( textRect.y(), buttonRect.y() ), + buttonRect.width() + textRect.width(), + qMax( buttonRect.height(), textRect.height() ) ); + + if( layoutMirroring() ) + { + result.setWidth( + result.width() + marginHint( Text ).right() + + marginHint( Button ).left() ); + } + else + { + result.setWidth( + result.width() + marginHint( Text ).left() + + marginHint( Button ).right() ); + } + + return result; + } + + return QRectF(); +} + +int QskRadioBox::selectedIndex() const +{ + return m_data->selectedIndex; +} + +const QStringList& QskRadioBox::items() const +{ + return m_data->items; +} + +int QskRadioBox::pressedIndex() const +{ + return m_data->pressedIndex; +} + +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 ); + + if( m_data->focusedIndex > items.size() ) + setFocusedIndex( 0 ); +} + +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 ); + setFocusedIndex( m_data->selectedIndex ); + update(); + + return; + } + case Qt::Key_Down: + case Qt::Key_Right: + { + m_data->selectedIndex = qMin( m_data->selectedIndex + 1, + items().size() - 1 ); + setFocusedIndex( m_data->selectedIndex ); + update(); + + return; + } + case Qt::Key_Select: + case Qt::Key_Return: + case Qt::Key_Space: + { + m_data->selectedIndex = m_data->focusedIndex; + update(); + + return; + } + } + + const auto currentTabIndex = m_data->focusedIndex; + const auto nextTabIndex = currentTabIndex + qskFocusChainIncrement( event ); + + if( nextTabIndex >= items().size() || nextTabIndex < 0 ) + { + Inherited::keyPressEvent( event ); + setFocusedIndex( -1 ); + } + else + { + setFocusedIndex( ( float ) nextTabIndex ); + + const auto aspect = Ripple | QskAspect::Metric | QskAspect::Position; + const auto hint = animationHint( aspect | skinStates() ); + + if( hint.isValid() ) + { + startTransition( aspect, hint, + ( float ) currentTabIndex, ( float ) nextTabIndex ); + } + } + + update(); +} + +void QskRadioBox::keyReleaseEvent( QKeyEvent* ) +{ +} + +void QskRadioBox::mousePressEvent( QMouseEvent* e ) +{ + auto indexAtPosition = indexAt( e->localPos() ); + + m_data->pressedIndex = indexAtPosition; + m_data->selectedIndex = -1; + + setFocusedIndex( indexAtPosition ); + update(); +} + +void QskRadioBox::mouseReleaseEvent( QMouseEvent* e ) +{ + const auto index = indexAt( e->localPos() ); + if( index == m_data->pressedIndex ) + setSelectedIndex( index ); + + update(); +} + +void QskRadioBox::focusInEvent( QFocusEvent* e ) +{ + if( e->reason() == Qt::TabFocusReason ) + { + setFocusedIndex( 0 ); + } + else if( e->reason() == Qt::BacktabFocusReason ) + { + setFocusedIndex( items().size() - 1 ); + } + + update(); + Inherited::focusInEvent( e ); +} + +void QskRadioBox::focusOutEvent( QFocusEvent* e ) +{ + setFocusedIndex( -1 ); + update(); + + Inherited::focusOutEvent( e ); +} + +int QskRadioBox::indexAt( const QPointF& target ) const +{ + const auto itemHeight = contentsRect().height() / items().size(); + auto index = target.y() / itemHeight; + + if( index < 0 || index >= items().size() ) + return -1; + + return index; +} + +void QskRadioBox::setFocusedIndex( int index ) +{ + m_data->focusedIndex = index; + setPositionHint( Ripple, index ); + focusIndicatorRectChanged(); +} + +#include "moc_QskRadioBox.cpp" diff --git a/src/controls/QskRadioBox.h b/src/controls/QskRadioBox.h new file mode 100644 index 00000000..c510bb96 --- /dev/null +++ b/src/controls/QskRadioBox.h @@ -0,0 +1,68 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#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, Button, 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 ); + + ~QskRadioBox() override; + + QRectF focusIndicatorRect() const override; + + const QStringList& items() const; + + int selectedIndex() const; + int pressedIndex() 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: + void setFocusedIndex( int index ); + + 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..13cff7e0 --- /dev/null +++ b/src/controls/QskRadioBoxSkinlet.cpp @@ -0,0 +1,333 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#include "QskRadioBoxSkinlet.h" + +#include "QskAspect.h" +#include "QskRadioBox.h" + +#include "QskSkinStateChanger.h" +#include "QskStandardSymbol.h" +#include "QskColorFilter.h" +#include "QskGraphic.h" +#include "QskFunctions.h" +#include "QskSkin.h" + +#include + +namespace +{ + QskAspect::States statesForIndex( const QskRadioBox* radioBox, int index ) + { + using Q = QskRadioBox; + + auto states = radioBox->skinStates(); + + if( radioBox->selectedIndex() == index ) + states |= Q::Selected; + + if( radioBox->pressedIndex() == index ) + states |= Q::Pressed; + + if( radioBox->positionHint( Q::Ripple ) == index ) + states |= Q::Focused; + + return states; + } + + qreal lineHeight( const QskRadioBox* radioBox ) + { + using Q = QskRadioBox; + + auto strutHight = qMax( radioBox->strutSizeHint( Q::Button ).height(), + radioBox->strutSizeHint( Q::Text ).height() ); + + const auto textMargins = radioBox->marginHint( Q::Text ); + + auto fontHeight = radioBox->effectiveFontHeight( Q::Text ); + fontHeight += textMargins.top() + textMargins.bottom(); + + return qMax( strutHight, fontHeight ); + } +} + +QskRadioBoxSkinlet::QskRadioBoxSkinlet( QskSkin* ) +{ + setNodeRoles( { PanelRole, ButtonRole, SymbolRole, TextRole, RippleRole } ); +} + +QskRadioBoxSkinlet::~QskRadioBoxSkinlet() +{ +} + +QRectF QskRadioBoxSkinlet::subControlRect( const QskSkinnable* skinnable, + const QRectF& contentsRect, QskAspect::Subcontrol subcontrol ) const +{ + auto radioBox = static_cast< const QskRadioBox* >( skinnable ); + + if( subcontrol == QskRadioBox::Ripple ) + return rippleRect( radioBox, contentsRect ); + + return contentsRect; +} + +QSizeF QskRadioBoxSkinlet::sizeHint( const QskSkinnable* skinnable, + Qt::SizeHint, const QSizeF& ) const +{ + using Q = QskRadioBox; + + auto radioBox = static_cast< const QskRadioBox* >( skinnable ); + + const auto font = skinnable->effectiveFont( Q::Text ); + const auto textMargins = skinnable->marginHint( Q::Text ); + const auto buttonMargins = skinnable->marginHint( Q::Button ); + const auto symbolMargins = skinnable->marginHint( Q::Symbol ); + + qreal maxTextWidth = 0; + for( const auto& item : radioBox->items() ) + maxTextWidth = std::max( maxTextWidth, qskHorizontalAdvance( font, item ) ); + + auto buttonWidth = radioBox->strutSizeHint( Q::Button ).width(); + auto symbolWidth = radioBox->strutSizeHint( Q::Symbol ).width(); + + maxTextWidth += textMargins.left() + textMargins.right(); + buttonWidth += buttonMargins.left() + buttonMargins.right(); + symbolWidth += symbolMargins.left() + symbolMargins.right(); + + auto spacing = radioBox->spacingHint( Q::Panel ); + return QSizeF( maxTextWidth + qMax( buttonWidth, symbolWidth ), + ( lineHeight( radioBox ) + spacing ) * radioBox->items().size() - spacing ); +} + +QSGNode* QskRadioBoxSkinlet::updateSubNode( const QskSkinnable* skinnable, + quint8 nodeRole, QSGNode* node ) const +{ + using Q = QskRadioBox; + + switch( nodeRole ) + { + case PanelRole: + return updateBoxNode( skinnable, node, Q::Panel ); + + case ButtonRole: + return updateSeriesNode( skinnable, Q::Button, node ); + + case SymbolRole: + return updateSeriesNode( skinnable, Q::Symbol, node ); + + case TextRole: + return updateSeriesNode( skinnable, Q::Text, node ); + + case RippleRole: + { + auto radioBox = static_cast< const QskRadioBox* >( skinnable ); + + QskSkinStateChanger changer( radioBox ); + auto ripplePosition = radioBox->positionHint( Q::Ripple ); + changer.setStates( statesForIndex( radioBox, ripplePosition ) ); + + return updateBoxNode( radioBox, node, Q::Ripple ); + } + }; + + return Inherited::updateSubNode( skinnable, nodeRole, node ); +} + +int QskRadioBoxSkinlet::sampleCount( + const QskSkinnable* skinnable, QskAspect::Subcontrol ) const +{ + const auto radioBox = static_cast< const QskRadioBox* >( skinnable ); + return radioBox->items().count(); +} + +QSizeF QskRadioBoxSkinlet::buttonSymbolSize( const QskRadioBox* radioBox ) const +{ + using Q = QskRadioBox; + + auto buttonStrut = radioBox->strutSizeHint( Q::Button ); + auto symbolStrut = radioBox->strutSizeHint( Q::Symbol ); + + buttonStrut = buttonStrut.grownBy( radioBox->marginHint( Q::Button ) ); + symbolStrut = symbolStrut.grownBy( radioBox->marginHint( Q::Symbol ) ); + + return QSizeF( qMax( buttonStrut.width(), symbolStrut.width() ), + qMax( buttonStrut.height(), symbolStrut.height() ) ); +} + +QRectF QskRadioBoxSkinlet::rippleRect( + const QskRadioBox* radioBox, const QRectF& rect ) const +{ + using Q = QskRadioBox; + + auto ripplePosition = radioBox->positionHint( Q::Ripple ); + + if( ripplePosition < 0 ) + return QRectF(); + + auto rippleSize = radioBox->strutSizeHint( Q::Ripple ); + + auto r = buttonRect( radioBox, Q::Button, rect, ripplePosition ); + + r.moveLeft( r.x() - ( rippleSize.width() - r.width() ) / 2 ); + r.moveTop( r.y() - ( rippleSize.height() - r.height() ) / 2 ); + r.setSize( rippleSize ); + + return r; +} + +QRectF QskRadioBoxSkinlet::buttonRect( const QskRadioBox* radioBox, + const QskAspect::Subcontrol target, const QRectF& rect, double index ) const +{ + using Q = QskRadioBox; + + if( index < 0 ) + return QRectF(); + + auto result = rect; + result.setSize( radioBox->strutSizeHint( target ) ); + + auto spacing = radioBox->spacingHint( Q::Panel ); + result.moveTop( ( lineHeight( radioBox ) + spacing ) * index ); + + auto margins = radioBox->marginHint( target ); + auto withMargins = result.size().grownBy( margins ); + + auto maxSize = buttonSymbolSize( radioBox ); + auto alignment = radioBox->alignmentHint( target ); + + // Vertical positioning + const auto alignHeight = maxSize.height() - withMargins.height(); + if( alignment.testFlag( Qt::AlignVCenter ) ) + result.moveTop( result.top() + alignHeight / 2 ); + else if( alignment.testFlag( Qt::AlignBottom ) ) + result.moveTop( result.top() + alignHeight ); + + result.moveTop( result.top() + margins.top() ); + + // Horizontal positioning + auto alignWidth = 0; + if( alignment.testFlag( Qt::AlignHCenter ) ) + alignWidth = ( maxSize.width() - withMargins.width() ) / 2; + else if ( alignment.testFlag( Qt::AlignRight ) ) + alignWidth = maxSize.width() - withMargins.width(); + + if( radioBox->layoutMirroring() ) + result.moveRight( rect.width() - ( alignWidth + margins.right() ) ); + else + result.moveLeft( margins.left() + alignWidth ); + + return result; +} + +QRectF QskRadioBoxSkinlet::textRect( const QskRadioBox* radioBox, + const QRectF& rect, int index ) const +{ + using Q = QskRadioBox; + + const auto text = radioBox->items()[ index ]; + if( text.isEmpty() ) + return QRectF(); + + QRectF result = rect; + auto spacing = radioBox->spacingHint( Q::Panel ); + auto lh = lineHeight( radioBox ); + const auto textMargins = radioBox->marginHint( Q::Text ); + const auto font = radioBox->effectiveFont( Q::Text ); + + result.moveTop( index * ( lh + spacing ) + + lh - QFontMetricsF( font ).height() + textMargins.top() ); + + result.setHeight( lh ); + result.setWidth( qskHorizontalAdvance( font, text ) ); + + const auto button = buttonRect( radioBox, Q::Button, rect, index ); + const auto buttonsMargins = radioBox->marginHint( Q::Button ); + const auto buttonWidth = button.marginsAdded( buttonsMargins ).width(); + + if( radioBox->layoutMirroring() ) + { + result.moveLeft( rect.width() - textMargins.right() + - result.width() - buttonWidth); + } + else + { + result.moveLeft( buttonWidth + textMargins.left() ); + } + + return result; +} + +QRectF QskRadioBoxSkinlet::sampleRect( const QskSkinnable* skinnable, + const QRectF& rect, QskAspect::Subcontrol subcontrol, int index ) const +{ + using Q = QskRadioBox; + + auto radioBox = static_cast< const QskRadioBox* >( skinnable ); + + if( subcontrol == Q::Text ) + return textRect( radioBox, rect, index ); + + return buttonRect( radioBox, subcontrol, rect, index); +} + +QskAspect::States QskRadioBoxSkinlet::sampleStates( const QskSkinnable* skinnable, + QskAspect::Subcontrol subControl, int index ) const +{ + auto radioBox = static_cast< const QskRadioBox* >( skinnable ); + auto states = Inherited::sampleStates( skinnable, subControl, index ); + + return states | statesForIndex( radioBox, index ); +} + +QSGNode* QskRadioBoxSkinlet::updateSampleNode( const QskSkinnable* skinnable, + QskAspect::Subcontrol subcontrol, int index, QSGNode* node ) const +{ + using Q = QskRadioBox; + + auto radioBox = static_cast< const QskRadioBox* >( skinnable ); + + auto rect = sampleRect( skinnable, radioBox->contentsRect(), subcontrol, index ); + + if( subcontrol == Q::Text ) + { + return QskSkinlet::updateTextNode( radioBox, node, rect, Qt::AlignLeft, + radioBox->items()[ index ], subcontrol ); + } + else if ( subcontrol == Q::Button ) + { + return QskSkinlet::updateBoxNode( radioBox, node, rect, subcontrol ); + } + else if( subcontrol == Q::Symbol ) + { + auto symbol = QskStandardSymbol::NoSymbol; + auto color = radioBox->color( subcontrol ).rgb(); + + if( radioBox->selectedIndex() == index ) + { + symbol = QskStandardSymbol::Bullet; + color = radioBox->color( subcontrol | Q::Selected ).rgb(); + } + + auto graphic = radioBox->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 ); + + auto colorSub = radioBox->color( subcontrol | statesForIndex( radioBox, index ) ); + filter.addColorSubstitution( Qt::black, colorSub.rgb() ); + + QskGraphic::fromGraphic( graphic, filter ); + + return updateGraphicNode( radioBox, node, graphic, filter, rect ); + } + + return node; +} + +#include "moc_QskRadioBoxSkinlet.cpp" diff --git a/src/controls/QskRadioBoxSkinlet.h b/src/controls/QskRadioBoxSkinlet.h new file mode 100644 index 00000000..c1c4f359 --- /dev/null +++ b/src/controls/QskRadioBoxSkinlet.h @@ -0,0 +1,65 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#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, + ButtonRole, + 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; + + 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: + QRectF textRect( const QskRadioBox*, const QRectF&, int ) const; + + QSizeF buttonSymbolSize( const QskRadioBox* ) const; + QRectF buttonRect( const QskRadioBox*, + const QskAspect::Subcontrol, const QRectF&, double ) const; + + QRectF rippleRect( const QskRadioBox*, const QRectF& ) 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/QskColorFilter.cpp b/src/graphic/QskColorFilter.cpp index ab931545..c6f1dc73 100644 --- a/src/graphic/QskColorFilter.cpp +++ b/src/graphic/QskColorFilter.cpp @@ -11,35 +11,31 @@ #include static inline QRgb qskSubstitutedRgb( - const QVector< QPair< QRgb, QRgb > >& substitions, QRgb rgba, bool substituteAlpha ) + const QVector< QPair< QRgb, QRgb > >& substitions, QRgb rgba, QRgb mask ) { // usually we have 2-3 substitutions, so we can simply iterate // and don't need to introduce some sort of sorting or search index - const QRgb rgb = substituteAlpha ? rgba : ( rgba | QskRgb::AlphaMask ); + const QRgb rgb = rgba | ~mask; for ( const auto& s : substitions ) { - if ( rgb == s.first ) - { - const auto ret = substituteAlpha ? s.second - : ( s.second & QskRgb::ColorMask ) - | ( rgba & QskRgb::AlphaMask ); - return ret; - } + if ( rgb == ( s.first | ~mask ) ) + return ( s.second & mask ) | ( rgba & ~mask ); } return rgba; } static inline QColor qskSubstitutedColor( - const QVector< QPair< QRgb, QRgb > >& substitions, const QColor& color, bool substituteAlpha ) + const QVector< QPair< QRgb, QRgb > >& substitions, + const QColor& color, QRgb mask ) { - return QColor::fromRgba( qskSubstitutedRgb( substitions, color.rgba(), substituteAlpha ) ); + return QColor::fromRgba( qskSubstitutedRgb( substitions, color.rgba(), mask ) ); } static inline QBrush qskSubstitutedBrush( - const QVector< QPair< QRgb, QRgb > >& substitions, const QBrush& brush, bool substituteAlpha ) + const QVector< QPair< QRgb, QRgb > >& substitions, const QBrush& brush, QRgb mask ) { QBrush newBrush; @@ -50,7 +46,7 @@ static inline QBrush qskSubstitutedBrush( auto stops = gradient->stops(); for ( auto& stop : stops ) { - const QColor c = qskSubstitutedColor( substitions, stop.second, substituteAlpha ); + const QColor c = qskSubstitutedColor( substitions, stop.second, mask ); if ( c != stop.second ) { stop.second = c; @@ -68,7 +64,7 @@ static inline QBrush qskSubstitutedBrush( } else { - const QColor c = qskSubstitutedColor( substitions, brush.color(), substituteAlpha ); + const QColor c = qskSubstitutedColor( substitions, brush.color(), mask ); if ( c != brush.color() ) { newBrush = brush; @@ -168,7 +164,7 @@ QPen QskColorFilter::substituted( const QPen& pen ) const if ( m_substitutions.isEmpty() || pen.style() == Qt::NoPen ) return pen; - const auto newBrush = qskSubstitutedBrush( m_substitutions, pen.brush(), m_substituteAlphaValue ); + const auto newBrush = qskSubstitutedBrush( m_substitutions, pen.brush(), m_mask ); if ( newBrush.style() == Qt::NoBrush ) return pen; @@ -182,28 +178,18 @@ QBrush QskColorFilter::substituted( const QBrush& brush ) const if ( m_substitutions.isEmpty() || brush.style() == Qt::NoBrush ) return brush; - const auto newBrush = qskSubstitutedBrush( m_substitutions, brush, m_substituteAlphaValue ); + const auto newBrush = qskSubstitutedBrush( m_substitutions, brush, m_mask ); return ( newBrush.style() != Qt::NoBrush ) ? newBrush : brush; } QColor QskColorFilter::substituted( const QColor& color ) const { - return qskSubstitutedColor( m_substitutions, color, m_substituteAlphaValue ); + return qskSubstitutedColor( m_substitutions, color, m_mask ); } QRgb QskColorFilter::substituted( const QRgb& rgb ) const { - return qskSubstitutedRgb( m_substitutions, rgb, m_substituteAlphaValue ); -} - -bool QskColorFilter::substituteAlphaValue() const noexcept -{ - return m_substituteAlphaValue; -} - -void QskColorFilter::setSubstituteAlphaValue( bool on ) -{ - m_substituteAlphaValue = on; + return qskSubstitutedRgb( m_substitutions, rgb, m_mask ); } QskColorFilter QskColorFilter::interpolated( @@ -227,7 +213,7 @@ QDebug operator<<( QDebug debug, const QskColorFilter& filter ) QDebugStateSaver saver( debug ); debug.nospace(); - debug << "Filter" << '('; + debug << "Filter" << '[' << filter.mask() << ']' << '('; for ( const auto& s : filter.substitutions() ) debug << '[' << s.first << "->" << s.second << "]"; debug << ')'; diff --git a/src/graphic/QskColorFilter.h b/src/graphic/QskColorFilter.h index dedac055..f4553fcd 100644 --- a/src/graphic/QskColorFilter.h +++ b/src/graphic/QskColorFilter.h @@ -20,7 +20,7 @@ class QVariant; class QSK_EXPORT QskColorFilter { public: - QskColorFilter() noexcept = default; + QskColorFilter( QRgb mask = 0x00ffffff ) noexcept; void addColorSubstitution( QRgb from, QRgb to ); void addColorSubstitution( Qt::GlobalColor, QRgb ); @@ -37,8 +37,9 @@ class QSK_EXPORT QskColorFilter bool isIdentity() const noexcept; - bool substituteAlphaValue() const noexcept; - void setSubstituteAlphaValue( bool ); + // the bits to be replaced + QRgb mask() const noexcept; + void setMask( QRgb ) noexcept; bool operator==( const QskColorFilter& other ) const noexcept; bool operator!=( const QskColorFilter& other ) const noexcept; @@ -53,10 +54,15 @@ class QSK_EXPORT QskColorFilter const QskColorFilter&, const QskColorFilter&, qreal progress ); private: + QRgb m_mask; QVector< QPair< QRgb, QRgb > > m_substitutions; - bool m_substituteAlphaValue = false; }; +inline QskColorFilter::QskColorFilter( QRgb mask ) noexcept + : m_mask( mask ) +{ +} + inline bool QskColorFilter::isIdentity() const noexcept { return m_substitutions.isEmpty(); @@ -97,6 +103,16 @@ inline void QskColorFilter::addColorSubstitution( addColorSubstitution( QColor( from ).rgb(), QColor( to ).rgb() ); } +inline void QskColorFilter::setMask( QRgb mask ) noexcept +{ + m_mask = mask; +} + +inline QRgb QskColorFilter::mask() const noexcept +{ + return m_mask; +} + Q_DECLARE_METATYPE( QskColorFilter ) #ifndef QT_NO_DEBUG_STREAM 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 \