Add a Checkbox (#169)

* Add checkbox

* Add checkbox tests

* Add checkbox-groups

* Add checkbox-groups tests
This commit is contained in:
Clemens Manert 2022-04-04 08:53:20 +02:00 committed by GitHub
parent de48deb7c8
commit a05d1e3471
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 704 additions and 0 deletions

View File

@ -7,6 +7,7 @@ SUBDIRS = \
qmlexport \
tools \
support \
tests \
examples \
playground

View File

@ -8,6 +8,7 @@
#include <QskSkinHintTableEditor.h>
#include <QskBox.h>
#include <QskCheckBox.h>
#include <QskDialogButton.h>
#include <QskDialogButtonBox.h>
#include <QskFocusIndicator.h>
@ -112,6 +113,7 @@ namespace
void setupControl();
void setupBox();
void setupCheckBox();
void setupDialogButtonBox();
void setupDialogButton();
void setupFocusIndicator();
@ -142,6 +144,7 @@ void Editor::setup()
setupControl();
setupBox();
setupCheckBox();
setupDialogButtonBox();
setupDialogButton();
setupFocusIndicator();
@ -177,6 +180,24 @@ void Editor::setupControl()
qskShadedColor( m_pal.textColor, 0.6 ) );
}
void Editor::setupCheckBox()
{
using Q = QskCheckBox;
const qreal radius = qskDpiScaled( 18 );
setMargin( QskCheckBox::Tick, QMarginsF( 3, 5, 3, 3 ) );
setStrutSize( Q::Box, radius, radius );
setBoxShape( Q::Box, 2 );
setColor( Q::Box, m_pal.baseColor);
setColor( Q::Box | Q::Checked, m_pal.accentColor );
setGradient( Q::Box | Q::Checked | Q::Disabled, QskRgb::Grey );
setColor( Q::Tick, m_pal.contrastColor );
}
void Editor::setupBox()
{
using Q = QskBox;

View File

@ -8,6 +8,7 @@
#include <QskSkinHintTableEditor.h>
#include <QskBox.h>
#include <QskCheckBox.h>
#include <QskDialogButton.h>
#include <QskDialogButtonBox.h>
#include <QskFocusIndicator.h>
@ -132,6 +133,7 @@ namespace
void setupControl();
void setupBox();
void setupCheckBox();
void setupDialogButton();
void setupDialogButtonBox();
void setupFocusIndicator();
@ -251,6 +253,7 @@ void Editor::setup()
setupControl();
setupBox();
setupCheckBox();
setupDialogButtonBox();
setupDialogButton();
setupFocusIndicator();
@ -292,6 +295,23 @@ void Editor::setupBox()
setPanel( QskBox::Panel, Plain );
}
void Editor::setupCheckBox()
{
using Q = QskCheckBox;
const qreal size = qskDpiScaled( 26 );
setMargin( Q::Tick, QskMargins( qskDpiScaled( 5 ) ) );
setStrutSize( Q::Box, QSizeF( size, size ) );
setBoxShape( Q::Box, qskDpiScaled( 3 ) );
setBoxBorderMetrics( Q::Box, qskDpiScaled( 1 ) );
setBoxBorderColors( Q::Box, m_pal.darker125 );
setColor( Q::Box, m_pal.lighter135 );
setColor( Q::Box | Q::Checked, m_pal.highlighted );
setColor( Q::Tick, m_pal.lighter135 );
}
void Editor::setupPopup()
{
using A = QskAspect;

View File

@ -0,0 +1,185 @@
#include "QskCheckBox.h"
#include "QskAspect.h"
#include <qset.h>
QSK_SUBCONTROL( QskCheckBox, Box )
QSK_SUBCONTROL( QskCheckBox, Tick )
QSK_SYSTEM_STATE( QskCheckBox, PartiallyChecked, QskAspect::LastUserState << 2 )
struct QskCheckBox::PrivateData
{
QSet< QskAbstractButton* > group;
int groupItemsChecked;
Qt::CheckState checkState;
bool checkStateChanging : 1;
bool toggleChanging : 1;
bool triState : 1;
};
QskCheckBox::QskCheckBox( QQuickItem* parent )
: Inherited( parent )
, m_data( new PrivateData { QSet< QskAbstractButton* >(), 0,
Qt::Unchecked, false, false, false } ) {
setAcceptHoverEvents( true );
initSizePolicy( QskSizePolicy::Fixed, QskSizePolicy::Fixed );
connect( this, &QskCheckBox::checkedChanged, this, [ this ]( bool t )
{
setCheckStateInternal( t ? Qt::CheckState::Checked :
Qt::CheckState::Unchecked );
} );
}
QskCheckBox::~QskCheckBox()
{
Q_EMIT removeFromAllGroupsRequested();
}
bool QskCheckBox::isCheckable() const
{
return true;
}
Qt::CheckState QskCheckBox::checkState() const
{
return m_data->checkState;
}
void QskCheckBox::setCheckStateInternal( Qt::CheckState checkState )
{
if( m_data->checkStateChanging )
{
return;
}
setSkinStateFlag( PartiallyChecked,
checkState == Qt::CheckState::PartiallyChecked );
m_data->checkState = checkState;
Q_EMIT checkStateChanged( checkState );
}
void QskCheckBox::setCheckState( Qt::CheckState checkState )
{
if( checkState == m_data->checkState )
return;
m_data->checkStateChanging = true;
if( checkState == Qt::CheckState::PartiallyChecked )
{
setChecked( true );
setTriState( true );
}
else
{
setChecked( checkState == Qt::CheckState::Checked );
}
m_data->checkStateChanging = false;
setCheckStateInternal( checkState );
}
bool QskCheckBox::isTriState() const
{
return m_data->triState;
}
void QskCheckBox::setTriState( bool triState )
{
if( m_data->triState != triState )
{
m_data->triState = triState;
Q_EMIT isTriStateChanged( triState );
}
}
void QskCheckBox::updated()
{
if( m_data->toggleChanging )
{
return;
}
auto& groupItemsChecked = m_data->groupItemsChecked;
if( groupItemsChecked == m_data->group.size() )
{
this->setCheckState( Qt::CheckState::Checked );
}
else if ( groupItemsChecked == 0 )
{
this->setCheckState( Qt::CheckState::Unchecked );
}
else
{
this->setCheckState( Qt::CheckState::PartiallyChecked );
}
}
void QskCheckBox::addToGroup( QskCheckBox* groupItem ) {
if( m_data->group.contains( groupItem ) )
{
return;
}
m_data->group.insert( groupItem );
if( groupItem->checkState() == Qt::CheckState::Checked )
{
m_data->groupItemsChecked += 1;
}
updated();
connect( this, &QskCheckBox::checkStateChanged,
groupItem, [ this, groupItem ] ( Qt::CheckState checkState )
{
if( checkState == Qt::Checked )
{
m_data->toggleChanging = true;
groupItem->setChecked( true );
m_data->groupItemsChecked = m_data->group.size();
m_data->toggleChanging = false;
}
else if ( checkState == Qt::Unchecked )
{
m_data->toggleChanging = true;
groupItem->setChecked( false );
m_data->groupItemsChecked = 0;
m_data->toggleChanging = false;
}
} );
connect( groupItem, &QskAbstractButton::toggled,
this, [ this, groupItem ] ( bool toggled )
{
auto& groupItemsChecked = m_data->groupItemsChecked;
groupItemsChecked += toggled ? 1 : -1;
updated();
} );
connect( groupItem, &QskCheckBox::removeFromAllGroupsRequested,
this, [ this, groupItem ] ( )
{
removeFromGroup( groupItem );
} );
}
void QskCheckBox::removeFromGroup( QskCheckBox* groupItem ) {
if( !m_data->group.remove( groupItem ) )
{
return;
}
if( groupItem->checkState() == Qt::CheckState::Checked )
{
m_data->groupItemsChecked -= 1;
}
updated();
}
#include "moc_QskCheckBox.cpp"

View File

@ -0,0 +1,48 @@
#ifndef QSK_CHECK_BOX_H
#define QSK_CHECK_BOX_H
#include "QskAbstractButton.h"
class QSK_EXPORT QskCheckBox : public QskAbstractButton
{
Q_OBJECT
Q_PROPERTY( Qt::CheckState checkState READ checkState
WRITE setCheckState NOTIFY checkStateChanged FINAL )
Q_PROPERTY( bool isTriState READ isTriState
WRITE setTriState NOTIFY isTriStateChanged FINAL )
using Inherited = QskAbstractButton;
public:
QSK_SUBCONTROLS( Box, Tick )
QSK_STATES( PartiallyChecked )
QskCheckBox( QQuickItem* parent = nullptr );
~QskCheckBox() override;
Qt::CheckState checkState() const;
bool isTriState() const;
bool isCheckable() const override final;
void addToGroup( QskCheckBox* groupItem );
void removeFromGroup( QskCheckBox* groupItem );
public Q_SLOTS:
void setCheckState( Qt::CheckState );
void setTriState( bool triState = true );
Q_SIGNALS:
void checkStateChanged( Qt::CheckState );
void isTriStateChanged( bool );
void removeFromAllGroupsRequested();
private:
void setCheckStateInternal( Qt::CheckState );
void updated();
struct PrivateData;
std::unique_ptr< PrivateData > m_data;
};
#endif // QSK_CHECK_BOX_H

View File

@ -0,0 +1,134 @@
#include "QskCheckBoxSkinlet.h"
#include "QskCheckBox.h"
#include <QSGFlatColorMaterial>
#include <qsgnode.h>
class Tic : public QSGGeometryNode {
QSGFlatColorMaterial material;
QSGGeometry geometry = QSGGeometry(
QSGGeometry::defaultAttributes_Point2D(), 3 );
const QRectF& target;
public:
Tic( const QRectF& rect, const QColor& color ): target( rect ) {
geometry.setDrawingMode( QSGGeometry::DrawLineStrip );
geometry.setLineWidth( 2 );
setGeometry( &geometry );
material.setColor( color );
setMaterial( &material );
markDirty( QSGNode::DirtyGeometry );
}
void setColor( const QColor& color ) {
material.setColor( color );
markDirty( QSGNode::DirtyMaterial );
}
void makeTic() {
const auto& size = target.size();
const auto x = target.x();
const auto y = target.y();
geometry.vertexDataAsPoint2D()[0].set( x, y + size.height() / 2 );
geometry.vertexDataAsPoint2D()[1].set( x + size.width() / 3,
y + size.height() );
geometry.vertexDataAsPoint2D()[2].set( x + size.width(), y );
markDirty( QSGNode::DirtyGeometry );
}
void makePartially() {
const auto& size = target.size();
const auto x = target.x();
const auto y = target.y();
geometry.vertexDataAsPoint2D()[0].set( x, y + size.height() / 2 );
geometry.vertexDataAsPoint2D()[1].set( x, y + size.height() / 2 );
geometry.vertexDataAsPoint2D()[2].set( x + size.width(),
y + size.height() / 2 );
markDirty( QSGNode::DirtyGeometry );
}
void makeEmpty() {
const auto x = target.x();
const auto y = target.y();
geometry.vertexDataAsPoint2D()[0].set( x, y );
geometry.vertexDataAsPoint2D()[1].set( x, y );
geometry.vertexDataAsPoint2D()[2].set( x, y );
markDirty( QSGNode::DirtyGeometry );
}
};
QskCheckBoxSkinlet::QskCheckBoxSkinlet( QskSkin* skin )
: QskSkinlet( skin )
{
setNodeRoles( { BoxRole, TickRole } );
}
QskCheckBoxSkinlet::~QskCheckBoxSkinlet()
{
}
QRectF QskCheckBoxSkinlet::subControlRect(
const QskSkinnable*,
const QRectF& contentsRect,
QskAspect::Subcontrol ) const
{
return contentsRect;
}
QSGNode* QskCheckBoxSkinlet::updateSubNode(
const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
{
switch( nodeRole ) {
case BoxRole:
return updateBoxNode( skinnable, node,
QskCheckBox::Box );
case TickRole:
auto control = dynamic_cast< const QskCheckBox* >( skinnable );
auto rect = control->subControlRect( QskCheckBox::Tick );
rect = rect.marginsRemoved(
skinnable->marginHint( QskCheckBox::Tick ) );
Tic* tic;
if ( static_cast< Tic* >( node ) == nullptr )
{
tic = new Tic( rect, skinnable->color( QskCheckBox::Tick ) );
}
else
{
tic = static_cast< Tic* >( node );
}
switch ( control->checkState() ) {
case Qt::CheckState::Unchecked:
tic->setColor( skinnable->color( QskCheckBox::Tick ) );
tic->makeEmpty();
break;
case Qt::CheckState::PartiallyChecked:
tic->setColor( skinnable->color(
QskCheckBox::Tick | QskCheckBox::PartiallyChecked ) );
tic->makePartially();
break;
case Qt::CheckState::Checked:
tic->setColor( skinnable->color(
QskCheckBox::Tick | QskCheckBox::Checked ) );
tic->makeTic();
break;
}
return tic;
}
return Inherited::updateSubNode( skinnable, nodeRole, node );
}
QSizeF QskCheckBoxSkinlet::sizeHint( const QskSkinnable* skinnable,
Qt::SizeHint, const QSizeF& ) const
{
return skinnable->strutSizeHint( QskCheckBox::Box );
}

View File

@ -0,0 +1,32 @@
#ifndef QSK_CHECK_BOX_SKINLET_H
#define QSK_CHECK_BOX_SKINLET_H
#include "QskSkinlet.h"
class QSK_EXPORT QskCheckBoxSkinlet : public QskSkinlet
{
Q_GADGET
using Inherited = QskSkinlet;
public:
enum NodeRole
{
BoxRole,
TickRole,
};
Q_INVOKABLE QskCheckBoxSkinlet( QskSkin* = nullptr );
~QskCheckBoxSkinlet() override;
QRectF subControlRect( const QskSkinnable*,
const QRectF&, QskAspect::Subcontrol ) const override;
QSizeF sizeHint( const QskSkinnable*,
Qt::SizeHint, const QSizeF& ) const override;
protected:
QSGNode* updateSubNode( const QskSkinnable*,
quint8 nodeRole, QSGNode* ) const override;
};
#endif // QSK_CHECK_BOX_SKINLET_H

View File

@ -29,6 +29,9 @@ QSK_QT_PRIVATE_END
#include "QskBox.h"
#include "QskBoxSkinlet.h"
#include "QskCheckBox.h"
#include "QskCheckBoxSkinlet.h"
#include "QskFocusIndicator.h"
#include "QskFocusIndicatorSkinlet.h"
@ -143,6 +146,7 @@ QskSkin::QskSkin( QObject* parent )
declareSkinlet< QskControl, QskSkinlet >();
declareSkinlet< QskBox, QskBoxSkinlet >();
declareSkinlet< QskCheckBox, QskCheckBoxSkinlet >();
declareSkinlet< QskFocusIndicator, QskFocusIndicatorSkinlet >();
declareSkinlet< QskGraphicLabel, QskGraphicLabelSkinlet >();
declareSkinlet< QskListView, QskListViewSkinlet >();

View File

@ -141,6 +141,8 @@ HEADERS += \
controls/QskBoundedValueInput.h \
controls/QskBox.h \
controls/QskBoxSkinlet.h \
controls/QskCheckBox.h \
controls/QskCheckBoxSkinlet.h \
controls/QskControl.h \
controls/QskControlPrivate.h \
controls/QskDirtyItemFilter.h \
@ -221,6 +223,8 @@ SOURCES += \
controls/QskBoundedValueInput.cpp \
controls/QskBox.cpp \
controls/QskBoxSkinlet.cpp \
controls/QskCheckBox.cpp \
controls/QskCheckBoxSkinlet.cpp \
controls/QskControl.cpp \
controls/QskControlPrivate.cpp \
controls/QskDirtyItemFilter.cpp \

View File

@ -0,0 +1,11 @@
CONFIG += qskexample
CONFIG += console
CONFIG += testcase
QT += testlib
HEADERS += \
main.h
SOURCES += \
main.cpp

209
tests/checkboxes/main.cpp Normal file
View File

@ -0,0 +1,209 @@
#include "main.h"
#include <QskCheckBox.h>
void CheckBoxTests::init() {
root = new QskControl();
}
void CheckBoxTests::cleanup() {
delete root;
}
void CheckBoxTests::checkbox() {
auto t = new QskCheckBox( root );
QVERIFY( t->isCheckable() );
}
void CheckBoxTests::click() {
auto t = new QskCheckBox( root );
QVERIFY( t->isChecked() == false );
t->click();
QVERIFY( t->isChecked() );
}
void CheckBoxTests::toggle() {
auto t = new QskCheckBox( root );
QVERIFY( t->isChecked() == false );
t->toggle();
QVERIFY( t->isChecked() );
t->toggle();
QVERIFY( t->isChecked() == false );
}
void CheckBoxTests::triState() {
auto t = new QskCheckBox( root );
QVERIFY( t->isChecked() == false );
QVERIFY( t->isTriState() == false );
t->setCheckState( Qt::CheckState::PartiallyChecked );
QVERIFY( t->isChecked() == true );
QVERIFY( t->isTriState() == true );
}
void CheckBoxTests::higherGroupUpdatesLower() {
auto t = new QskCheckBox( root );
auto t1 = new QskCheckBox( root );
auto t2 = new QskCheckBox( root );
auto t3 = new QskCheckBox( root );
t->addToGroup( t1 );
t->addToGroup( t2 );
t->addToGroup( t3 );
QVERIFY( t->isChecked() == false );
QVERIFY( t1->isChecked() == false );
QVERIFY( t2->isChecked() == false );
QVERIFY( t3->isChecked() == false );
t->setChecked( true );
QVERIFY( t->isChecked() );
QVERIFY( t1->isChecked() );
QVERIFY( t2->isChecked() );
QVERIFY( t3->isChecked() );
t->setChecked( false );
QVERIFY( t->isChecked() == false );
QVERIFY( t1->isChecked() == false );
QVERIFY( t2->isChecked() == false );
QVERIFY( t3->isChecked() == false );
}
void CheckBoxTests::lowerGroupUpdatesHigher() {
auto t = new QskCheckBox( root );
auto t1 = new QskCheckBox( root );
auto t2 = new QskCheckBox( root );
t->addToGroup( t1 );
t->addToGroup( t2 );
t1->setChecked( true );
QVERIFY( t->isChecked() );
QVERIFY( t->isTriState() );
QVERIFY( t->checkState() == Qt::CheckState::PartiallyChecked );
QVERIFY( t1->isChecked() == true );
QVERIFY( t2->isChecked() == false );
t2->setChecked( true );
QVERIFY( t->isChecked() );
QVERIFY( t->isTriState() );
QVERIFY( t->checkState() == Qt::CheckState::Checked );
QVERIFY( t1->isChecked() == true );
QVERIFY( t2->isChecked() == true );
t1->setChecked( false );
QVERIFY( t->isChecked() );
QVERIFY( t->isTriState() );
QVERIFY( t->checkState() == Qt::CheckState::PartiallyChecked );
QVERIFY( t1->isChecked() == false );
QVERIFY( t2->isChecked() == true );
t2->setChecked( false );
QVERIFY( t->isChecked() == false );
QVERIFY( t->isTriState() );
QVERIFY( t->checkState() == Qt::CheckState::Unchecked );
QVERIFY( t1->isChecked() == false );
QVERIFY( t2->isChecked() == false );
}
void CheckBoxTests::addToGroup() {
auto t = new QskCheckBox( root );
auto t1 = new QskCheckBox( root );
auto t2 = new QskCheckBox( root );
t->addToGroup( t1 );
t->addToGroup( t2 );
t->setChecked( true );
QVERIFY( t->isChecked() );
QVERIFY( t1->isChecked() );
QVERIFY( t2->isChecked() );
auto t3 = new QskCheckBox( root );
t->addToGroup( t3 );
QVERIFY( t->checkState() == Qt::CheckState::PartiallyChecked );
t3->setChecked( true );
QVERIFY( t->checkState() == Qt::CheckState::Checked );
auto t4 = new QskCheckBox( root );
t4->setChecked( true );
t->addToGroup( t4 );
QVERIFY( t->checkState() == Qt::CheckState::Checked );
}
void CheckBoxTests::addPartlyToGroup() {
auto t = new QskCheckBox( root );
auto t1 = new QskCheckBox( root );
auto t1a = new QskCheckBox( root );
auto t1b = new QskCheckBox( root );
t1->addToGroup( t1a );
t1->addToGroup( t1b );
t1a->setChecked( true );
QVERIFY( t1->checkState() == Qt::CheckState::PartiallyChecked );
t->addToGroup( t1 );
QVERIFY( t1->checkState() == Qt::CheckState::PartiallyChecked );
}
void CheckBoxTests::removeFromGroup() {
auto t = new QskCheckBox( root );
auto t1 = new QskCheckBox( root );
auto t2 = new QskCheckBox( root );
t->addToGroup( t1 );
t->addToGroup( t2 );
t2->setChecked( true );
QVERIFY( t->checkState() == Qt::CheckState::PartiallyChecked );
t->removeFromGroup( t2 );
QVERIFY( t->isChecked() == false );
}
void CheckBoxTests::groupMemberGetsDeleted() {
auto t = new QskCheckBox( root );
auto t1 = new QskCheckBox( root );
auto t2 = new QskCheckBox( root );
t->addToGroup( t1 );
t->addToGroup( t2 );
t2->setChecked( true );
QVERIFY( t->checkState() == Qt::CheckState::PartiallyChecked );
delete t2;
QVERIFY( t->isChecked() == false );
}
void CheckBoxTests::addTwiceToSameGroup() {
auto t = new QskCheckBox( root );
auto t1 = new QskCheckBox( root );
auto t2 = new QskCheckBox( root );
t->addToGroup( t1 );
t->addToGroup( t1 );
t->removeFromGroup( t1 );
t->addToGroup( t2 );
t2->setChecked( true );
QVERIFY( t->checkState() == Qt::CheckState::Checked );
}
#include "moc_main.cpp"

30
tests/checkboxes/main.h Normal file
View File

@ -0,0 +1,30 @@
#pragma once
#include <qobject.h>
#include <QtTest/QtTest>
class QskControl;
class CheckBoxTests : public QObject
{
Q_OBJECT
QskControl * root;
private Q_SLOTS:
void init();
void cleanup();
void checkbox();
void click();
void toggle();
void triState();
void higherGroupUpdatesLower();
void lowerGroupUpdatesHigher();
void addToGroup();
void addPartlyToGroup();
void removeFromGroup();
void groupMemberGetsDeleted();
void addTwiceToSameGroup();
};
QTEST_MAIN(CheckBoxTests)

5
tests/tests.pro Normal file
View File

@ -0,0 +1,5 @@
TEMPLATE = subdirs
SUBDIRS += \
checkboxes