From ba9ec8e783381476f941d74fee1a68929d7ebf03 Mon Sep 17 00:00:00 2001 From: "Vogel, Rick" Date: Fri, 17 Feb 2023 12:01:56 +0100 Subject: [PATCH] initial commit of QskSpinBox --- examples/gallery/gallery.pro | 6 + examples/gallery/main.cpp | 4 + examples/gallery/spinbox/SpinBoxPage.cpp | 50 ++++ examples/gallery/spinbox/SpinBoxPage.h | 17 ++ skins/material3/QskMaterial3Skin.cpp | 73 +++++ src/controls/QskSkin.cpp | 4 + src/controls/QskSpinBox.cpp | 353 +++++++++++++++++++++++ src/controls/QskSpinBox.h | 48 +++ src/controls/QskSpinBoxSkinlet.cpp | 221 ++++++++++++++ src/controls/QskSpinBoxSkinlet.h | 28 ++ src/src.pro | 4 + 11 files changed, 808 insertions(+) create mode 100644 examples/gallery/spinbox/SpinBoxPage.cpp create mode 100644 examples/gallery/spinbox/SpinBoxPage.h create mode 100644 src/controls/QskSpinBox.cpp create mode 100644 src/controls/QskSpinBox.h create mode 100644 src/controls/QskSpinBoxSkinlet.cpp create mode 100644 src/controls/QskSpinBoxSkinlet.h diff --git a/examples/gallery/gallery.pro b/examples/gallery/gallery.pro index cae45a37..0d5c9042 100644 --- a/examples/gallery/gallery.pro +++ b/examples/gallery/gallery.pro @@ -42,6 +42,12 @@ HEADERS += \ SOURCES += \ dialog/DialogPage.cpp \ +HEADERS += \ + spinbox/SpinBoxPage.h + +SOURCES += \ + spinbox/SpinBoxPage.cpp + HEADERS += \ Page.h diff --git a/examples/gallery/main.cpp b/examples/gallery/main.cpp index 0156bd06..9bc35a0a 100644 --- a/examples/gallery/main.cpp +++ b/examples/gallery/main.cpp @@ -10,6 +10,7 @@ #include "textinput/TextInputPage.h" #include "selector/SelectorPage.h" #include "dialog/DialogPage.h" +#include "spinbox/SpinBoxPage.h" #include #include @@ -199,6 +200,9 @@ namespace tabView->addTab( "Text\nInputs", new TextInputPage() ); tabView->addTab( "Selectors", new SelectorPage() ); tabView->addTab( "Dialogs", new DialogPage() ); + tabView->addTab( "SpinBoxes", new SpinBoxPage() ); + + tabView->setCurrentIndex(tabView->count() - 1); connect( header, &Header::enabledToggled, tabView, &TabView::setTabsEnabled ); diff --git a/examples/gallery/spinbox/SpinBoxPage.cpp b/examples/gallery/spinbox/SpinBoxPage.cpp new file mode 100644 index 00000000..ae559c6c --- /dev/null +++ b/examples/gallery/spinbox/SpinBoxPage.cpp @@ -0,0 +1,50 @@ +/****************************************************************************** + * Copyright (C) 2023 Edelhirsch Software GmbH + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "SpinBoxPage.h" +#include +#include +#include +#include + +SpinBoxPage::SpinBoxPage( QQuickItem* parent ) : Page( Qt::Horizontal, parent ) +{ + setMargins( 10 ); + setSpacing( 20 ); + + populate(); +} + +void SpinBoxPage::populate() +{ + const QMap layouts = + { + { Qt::AlignLeft, QStringLiteral("Qt::AlignLeft") }, + { Qt::AlignHCenter, QStringLiteral("Qt::AlignHCenter") }, + { Qt::AlignRight, QStringLiteral("Qt::AlignRight") }, + { Qt::AlignTop, QStringLiteral("Qt::AlignTop") }, + { Qt::AlignVCenter, QStringLiteral("Qt::AlignVCenter") }, + { Qt::AlignBottom, QStringLiteral("Qt::AlignBottom") }, + { Qt::AlignLeft | Qt::AlignVCenter, QStringLiteral("Qt::AlignLeft | Qt::AlignVCenter") }, + { Qt::AlignRight | Qt::AlignVCenter, QStringLiteral("Qt::AlignRight | Qt::AlignVCenter") }, + { Qt::AlignTop | Qt::AlignHCenter, QStringLiteral("Qt::AlignTop | Qt::AlignHCenter") }, + { Qt::AlignBottom | Qt::AlignHCenter, QStringLiteral("Qt::AlignBottom | Qt::AlignHCenter") } + }; + + auto* const grid = new QskGridBox(this); + constexpr int cols = 5; + for(const auto& layout : layouts.keys()) + { + const auto x = grid->elementCount() % cols; + const auto y = grid->elementCount() / cols; + auto* const column = new QskLinearBox(Qt::Vertical, grid); + auto* const label = new QskTextLabel(layouts.value(layout), column); // TODO put label either on top or on the bottom + auto* const spinbox = new QskSpinBox( column ); + spinbox->setAlignmentHint(QskSpinBox::Layout, layout); + grid->addItem(column, y, x); + column->setStretchFactor(label, 1); + column->setStretchFactor(spinbox, 99); + } +} diff --git a/examples/gallery/spinbox/SpinBoxPage.h b/examples/gallery/spinbox/SpinBoxPage.h new file mode 100644 index 00000000..2597e341 --- /dev/null +++ b/examples/gallery/spinbox/SpinBoxPage.h @@ -0,0 +1,17 @@ +/****************************************************************************** + * Copyright (C) 2023 Edelhirsch Software GmbH + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include "Page.h" + +class SpinBoxPage : public Page +{ + public: + SpinBoxPage( QQuickItem* = nullptr ); + + private: + void populate(); +}; diff --git a/skins/material3/QskMaterial3Skin.cpp b/skins/material3/QskMaterial3Skin.cpp index 6ff1dffa..199d2cdf 100644 --- a/skins/material3/QskMaterial3Skin.cpp +++ b/skins/material3/QskMaterial3Skin.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -144,6 +145,7 @@ namespace void setupSeparator(); void setupSubWindow(); void setupSlider(); + void setupSpinBox(); void setupSwitchButton(); void setupTabButton(); void setupTabBar(); @@ -201,6 +203,7 @@ void Editor::setup() setupSegmentedBar(); setupSeparator(); setupSlider(); + setupSpinBox(); setupSubWindow(); setupSwitchButton(); setupTabButton(); @@ -672,6 +675,76 @@ void Editor::setupSlider() setAnimation( Q::Handle | A::Metric | A::Position | Q::Pressed, 0 ); } +void Editor::setupSpinBox() +{ + using Q = QskSpinBox; + + setSpacing(QskSpinBox::Layout, 4_dp); + + setStrutSize(QskSpinBox::TextPanel | QskAspect::Size, {80_dp,40_dp}); + setStrutSize(QskSpinBox::Inc | QskAspect::Size, {40_dp,40_dp}); + setStrutSize(QskSpinBox::Dec | QskAspect::Size, {40_dp,40_dp}); + + setAlignment(QskSpinBox::Layout, Qt::AlignHCenter); + setAlignment(Q::Text, Qt::AlignCenter); + + for(const auto& state : {QskSpinBox::Dec, QskSpinBox::Inc, QskSpinBox::TextPanel}) + { + setBoxShape(state, 4_dp); + setBoxBorderColors(state, QColor("#79747E")); + setBoxBorderMetrics(state, 1_dp); + } + + for(const auto& state : {QskSpinBox::Dec, QskSpinBox::Inc}) + { + setGradient( state, m_pal.primary ); + setGradient( state | Q::Disabled, m_pal.onSurface12 ); + + const auto focusColor = flattenedColor( m_pal.onPrimary, m_pal.primary, 0.12 ); + setGradient( state | Q::Focused, focusColor ); + setGradient( state | Q::Pressed, focusColor ); + + const auto hoverColor = flattenedColor( m_pal.onPrimary, m_pal.primary, 0.08 ); + setGradient( state | Q::Hovered, hoverColor ); + setShadowMetrics( state | Q::Hovered, m_pal.elevationLight1 ); + setShadowColor( state | Q::Hovered, m_pal.shadow ); + } + + for(const auto& state : {QskSpinBox::DecText, QskSpinBox::IncText}) + { + setColor( state, m_pal.onPrimary ); + setColor( state | Q::Disabled, m_pal.onSurface38 ); + setAlignment(state, Qt::AlignCenter); + setFontRole( state, QskMaterial3Skin::M3LabelLarge ); + } + + setColor( Q::Text, m_pal.onBackground ); + setColor( Q::Text | Q::Disabled, m_pal.onSurface38 ); + + setPadding( Q::TextPanel, 5_dp ); + setBoxShape( Q::TextPanel, 4_dp, 4_dp, 0, 0 ); + setBoxBorderMetrics( Q::TextPanel, 0, 0, 0, 1_dp ); + setBoxBorderColors( Q::TextPanel, m_pal.onSurface ); + + setBoxBorderMetrics( Q::TextPanel | Q::Focused, 0, 0, 0, 2_dp ); + setBoxBorderColors( Q::TextPanel | Q::Focused, m_pal.primary ); + +// setBoxBorderMetrics( Q::TextPanel | Q::Editing, 0, 0, 0, 2_dp ); +// setBoxBorderColors( Q::TextPanel | Q::Editing, m_pal.primary ); + + setBoxBorderMetrics( Q::TextPanel | Q::Hovered, 0, 0, 0, 1_dp ); + setBoxBorderColors( Q::TextPanel | Q::Hovered, m_pal.onSurface ); + + setGradient( Q::TextPanel, m_pal.surfaceVariant ); + + const auto c1 = QskRgb::toTransparentF( m_pal.onSurface, 0.04 ); + setGradient( Q::TextPanel | Q::Disabled, c1 ); + setBoxBorderMetrics( Q::TextPanel | Q::Disabled, 0, 0, 0, 1_dp ); + + setColor( Q::TextPanel | Q::Disabled, m_pal.onSurface38 ); + setBoxBorderColors( Q::TextPanel | Q::Disabled, m_pal.onSurface38 ); +} + void Editor::setupSwitchButton() { using A = QskAspect; diff --git a/src/controls/QskSkin.cpp b/src/controls/QskSkin.cpp index b97308e3..ee56230f 100644 --- a/src/controls/QskSkin.cpp +++ b/src/controls/QskSkin.cpp @@ -68,6 +68,9 @@ QSK_QT_PRIVATE_END #include "QskSlider.h" #include "QskSliderSkinlet.h" +#include "QskSpinBox.h" +#include "QskSpinBoxSkinlet.h" + #include "QskSubWindow.h" #include "QskSubWindowSkinlet.h" @@ -161,6 +164,7 @@ QskSkin::QskSkin( QObject* parent ) declareSkinlet< QskSegmentedBar, QskSegmentedBarSkinlet >(); declareSkinlet< QskSeparator, QskSeparatorSkinlet >(); declareSkinlet< QskSlider, QskSliderSkinlet >(); + declareSkinlet< QskSpinBox, QskSpinBoxSkinlet >(); declareSkinlet< QskStatusIndicator, QskStatusIndicatorSkinlet >(); declareSkinlet< QskSubWindow, QskSubWindowSkinlet >(); declareSkinlet< QskSubWindowArea, QskSubWindowAreaSkinlet >(); diff --git a/src/controls/QskSpinBox.cpp b/src/controls/QskSpinBox.cpp new file mode 100644 index 00000000..b7d19198 --- /dev/null +++ b/src/controls/QskSpinBox.cpp @@ -0,0 +1,353 @@ +/****************************************************************************** + * Copyright (C) 2023 Edelhirsch Software GmbH + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "QskSpinBox.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +QSK_SUBCONTROL(QskSpinBox, Inc) +QSK_SUBCONTROL(QskSpinBox, Dec) +QSK_SUBCONTROL(QskSpinBox, IncText) +QSK_SUBCONTROL(QskSpinBox, DecText) +QSK_SUBCONTROL(QskSpinBox, Text) +QSK_SUBCONTROL(QskSpinBox, TextPanel) +QSK_SUBCONTROL(QskSpinBox, Layout) + +QSK_SYSTEM_STATE(QskSpinBox, Pressed, ( QskAspect::QskAspect::FirstSystemState << 0)) + +class QskSpinBox::PrivateData +{ +public: + + enum FocusIndeces : int { Dec = 0, Text = 1, Inc = 2, None = 3 }; + + explicit PrivateData(QskSpinBox* const parent) : q(parent) + { + } + + void setValue(qreal value) + { + value = qBound(q->minimum(), value, q->maximum()); + if(!qFuzzyCompare(m_value, value)) + { + m_value = value; + Q_EMIT q->valueChanged(m_value); + q->polish(); + q->update(); + } + } + + qreal value() const { return m_value; } + + FocusIndeces defaultFocusIndex() const + { + const auto layout = q->alignmentHint(QskSpinBox::Layout); + + if(layout == Qt::AlignLeft) return Text; + if(layout == Qt::AlignRight) return Dec; + if(layout == Qt::AlignHCenter) return Dec; + if(layout == Qt::AlignTop) return Text; + if(layout == Qt::AlignBottom) return Dec; + if(layout == Qt::AlignVCenter) return Inc; + if(layout == (Qt::AlignLeft | Qt::AlignVCenter)) return Text; + if(layout == (Qt::AlignRight | Qt::AlignVCenter)) return Inc; + + return None; + } + + FocusIndeces nextFocusIndex() const + { + const auto layout = q->alignmentHint(QskSpinBox::Layout); + + // [0 ][1 ][2 ][3 ] + // [Dec][Text][Inc][None] + using LUT = std::array; + if(layout == Qt::AlignLeft) return LUT{Inc,Dec,None,Text}[m_focusIndex]; + if(layout == Qt::AlignRight) return LUT{Inc,None,Text,Dec}[m_focusIndex]; + if(layout == Qt::AlignHCenter) return LUT{Text,Inc,None,Dec}[m_focusIndex]; + if(layout == Qt::AlignTop) return LUT{Inc,Dec,None,Text}[m_focusIndex]; + if(layout == Qt::AlignBottom) return LUT{Inc,None,Text,Dec}[m_focusIndex]; + if(layout == Qt::AlignVCenter) return LUT{None,Dec,Text,Inc}[m_focusIndex]; + if(layout == (Qt::AlignLeft | Qt::AlignVCenter)) return LUT{None,Inc,Dec,Text}[m_focusIndex]; + if(layout == (Qt::AlignRight | Qt::AlignVCenter)) return LUT{Text,None,Dec,Inc}[m_focusIndex]; + + return None; + } + + FocusIndeces previousFocusIndex() const + { + const auto layout = q->alignmentHint(QskSpinBox::Layout); + + // [0 ][1 ][2 ][3 ] + // [Dec][Text][Inc][None] + using LUT = std::array; + if(layout == Qt::AlignLeft) return LUT{None,Dec,Text,Inc}[m_focusIndex]; + if(layout == Qt::AlignRight) return LUT{None,Inc,Dec,Text}[m_focusIndex]; + if(layout == Qt::AlignHCenter) return LUT{None,Dec,Text,Inc}[m_focusIndex]; + if(layout == Qt::AlignTop) return LUT{Text,None,Dec,Inc}[m_focusIndex]; + if(layout == Qt::AlignBottom) return LUT{None,Inc,Dec,Text}[m_focusIndex]; + if(layout == Qt::AlignVCenter) return LUT{Text,Inc,None,Dec}[m_focusIndex]; + if(layout == (Qt::AlignLeft | Qt::AlignVCenter)) return LUT{Inc,None,Text,Dec}[m_focusIndex]; + if(layout == (Qt::AlignRight | Qt::AlignVCenter)) return LUT{Inc,Dec,None,Text}[m_focusIndex]; + + return None; + } + + FocusIndeces focusIndex() const + { + return m_focusIndex; + } + + void focusNext() + { + const auto index = nextFocusIndex(); + setFocus(index); + } + + void focusPrevious() + { + const auto index = previousFocusIndex(); + setFocus(index); + } + + void focusDefault() + { + setFocus(defaultFocusIndex()); + } + + void setFocus(const FocusIndeces index) + { + Q_ASSERT(index == Dec || index == Text || index == Inc || index == None); + if(index == Dec || index == Text || index == Inc || index == None) + { + m_focusIndex = index; + Q_EMIT q->focusIndexChanged(m_focusIndex); // TODO register enum + } + } + + QRectF focusIndicatorRect() const + { + switch(m_focusIndex) + { + case PrivateData::FocusIndeces::Dec: + return q->subControlRect(QskSpinBox::Dec); + case PrivateData::FocusIndeces::Text: + return q->subControlRect(QskSpinBox::TextPanel); + case PrivateData::FocusIndeces::Inc: + return q->subControlRect(QskSpinBox::Inc); + default: return {}; + } + } + + void saveMousePosition(const QPointF& pos) + { + q->setSkinHint(QskSpinBox::Layout | QskAspect::Metric | QskAspect::Position, pos ); + } + +private: + qreal m_value{0.0f}; + QskSpinBox* const q; + FocusIndeces m_focusIndex = FocusIndeces::None; +}; + +using S = QskSpinBox; + +QskSpinBox::QskSpinBox(QQuickItem* const parent) + : Inherited(parent) + , m_data(std::make_unique(this)) +{ + setBoundaries(0.0,1.0); + setAcceptHoverEvents(true); + setAcceptedMouseButtons(Qt::LeftButton); + setFocusPolicy( Qt::StrongFocus ); + + connect( this, &S::focusIndexChanged, this, &S::focusIndicatorRectChanged ); + connect( this, &S::boundariesChanged, this, [this](const QskIntervalF& interval){ if(!interval.contains(value())) m_data->setValue(minimum()); }); + connect( this, &S::valueChanged, this, [this](){ polish(); }); +} + +QskSpinBox::~QskSpinBox() = default; + +void QskSpinBox::hoverEnterEvent(QHoverEvent* event) +{ + m_data->saveMousePosition( qskHoverPosition( event ) ); +} + +void QskSpinBox::hoverLeaveEvent(QHoverEvent* event) +{ + m_data->saveMousePosition( {} ); +} + +void QskSpinBox::hoverMoveEvent(QHoverEvent *event) +{ + m_data->saveMousePosition( qskHoverPosition( event ) ); +} + +void QskSpinBox::mouseReleaseEvent(QMouseEvent *event) +{ + m_data->saveMousePosition( qskMousePosition( event ) ); + + const auto focus = ( focusPolicy() & Qt::ClickFocus ) == Qt::ClickFocus && !QGuiApplication::styleHints()->setFocusOnTouchRelease(); + + if(subControlRect(QskSpinBox::Inc).contains( event->pos() )) + { + increment(+stepSize()); + + if( focus ) + { + m_data->setFocus(PrivateData::Inc); + } + + return; + } + + if(subControlRect(QskSpinBox::Dec).contains( event->pos() )) + { + increment(-stepSize()); + + if( focus ) + { + m_data->setFocus(PrivateData::Dec); + } + + return; + } + + if(subControlRect(QskSpinBox::TextPanel).contains( event->pos() )) + { + if( focus ) + { + m_data->setFocus(PrivateData::Text); + } + + return; + } + + event->ignore(); +} + +void QskSpinBox::mousePressEvent(QMouseEvent *event) +{ + m_data->saveMousePosition( -1 * qskMousePosition( event ) ); + + const auto focus = ( focusPolicy() & Qt::ClickFocus ) == Qt::ClickFocus && !QGuiApplication::styleHints()->setFocusOnTouchRelease(); + + if(subControlRect(QskSpinBox::Inc).contains( event->pos() )) + { + if( focus ) + { + m_data->setFocus(PrivateData::Inc); + } + return; + } + + if(subControlRect(QskSpinBox::Dec).contains( event->pos() )) + { + if( focus ) + { + m_data->setFocus(PrivateData::Dec); + } + return; + } + + event->ignore(); +} + +void QskSpinBox::keyPressEvent(QKeyEvent *event) +{ + switch( event->key() ) + { + case Qt::Key_Plus: + case Qt::Key_Up: + case Qt::Key_Right: + // TODO increment + break; + case Qt::Key_Minus: + case Qt::Key_Down: + case Qt::Key_Left: + // TODO decrement + break; + case Qt::Key_Select: + case Qt::Key_Space: + // TODO click currently focused -/+ + break; + default: + { + const int steps = qskFocusChainIncrement( event ); + if(steps != 0) + { + for(int i = 0; i < steps; ++i) + { + m_data->focusNext(); + } + for(int i = steps; i < 0; ++i) + { + m_data->focusPrevious(); + } + } + } + } + Inherited::keyPressEvent( event ); +} + +void QskSpinBox::keyReleaseEvent( QKeyEvent* event ) +{ + if( event->key() == Qt::Key_Select || event->key() == Qt::Key_Space ) + { + return; + } + + Inherited::keyReleaseEvent( event ); +} + +void QskSpinBox::focusInEvent(QFocusEvent *event) +{ + switch( event->reason() ) + { + case Qt::TabFocusReason: + m_data->focusNext(); + break; + + case Qt::BacktabFocusReason: + m_data->focusPrevious(); + break; + + default: + if(m_data->focusIndex() == PrivateData::None) + { + m_data->focusDefault(); + return; + } + } + Inherited::focusInEvent( event ); +} + +QRectF QskSpinBox::focusIndicatorRect() const +{ + auto rect = m_data->focusIndicatorRect(); + return rect; +} + +void QskSpinBox::increment(qreal offset) +{ + m_data->setValue(m_data->value() + offset); +} + +qreal QskSpinBox::value() const +{ + return m_data->value(); +} diff --git a/src/controls/QskSpinBox.h b/src/controls/QskSpinBox.h new file mode 100644 index 00000000..9222b6e2 --- /dev/null +++ b/src/controls/QskSpinBox.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * Copyright (C) 2023 Edelhirsch Software GmbH + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include +#include +#include + +class QSK_EXPORT QskSpinBox : public QskBoundedInput +{ + Q_OBJECT + using Inherited = QskBoundedInput; +public: + Q_PROPERTY(qreal value READ value NOTIFY valueChanged) + QSK_SUBCONTROLS(Inc, Dec, IncText, DecText, TextPanel, Text, Layout) + QSK_STATES( Pressed ) + + explicit QskSpinBox( QQuickItem* parent = nullptr ); + ~QskSpinBox() override; + + void increment( qreal offset ) override; + qreal value() const; + +Q_SIGNALS: + void valueChanged(qreal value); + +private: + Q_SIGNAL void focusIndexChanged(int index); + + void hoverEnterEvent( QHoverEvent* event) override; + void hoverLeaveEvent( QHoverEvent* event) override; + void hoverMoveEvent( QHoverEvent* event) override; + + void mouseReleaseEvent(QMouseEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + + void keyPressEvent( QKeyEvent* event ) override; + void keyReleaseEvent( QKeyEvent* event ) override; + + void focusInEvent(QFocusEvent* event) override; + QRectF focusIndicatorRect() const override; + + class PrivateData; + std::unique_ptr m_data; +}; diff --git a/src/controls/QskSpinBoxSkinlet.cpp b/src/controls/QskSpinBoxSkinlet.cpp new file mode 100644 index 00000000..1d280943 --- /dev/null +++ b/src/controls/QskSpinBoxSkinlet.cpp @@ -0,0 +1,221 @@ +/****************************************************************************** + * Copyright (C) 2023 Edelhirsch Software GmbH + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#include "QskSpinBoxSkinlet.h" +#include "QskSpinBox.h" +#include + +const auto INC_TEXT = QStringLiteral("+"); +const auto DEC_TEXT = QStringLiteral("-"); + +enum SampleIndeces { Dec, Txt, Inc, Count }; + +QskSpinBoxSkinlet::QskSpinBoxSkinlet(QskSkin *) +{ + setNodeRoles({IncPanel, IncText, DecPanel, DecText, TextPanel, TextText}); +} + +int QskSpinBoxSkinlet::sampleCount(const QskSkinnable *, QskAspect::Subcontrol) const +{ + return Count; +} + +QRectF QskSpinBoxSkinlet::sampleRect(const QskSkinnable* const skinnable, const QRectF& rect, QskAspect::Subcontrol subControl, int index) const +{ + if(index == Dec || index == Inc || index == Txt) + { + return subControlRect(skinnable, rect, subControl); + } + + return Inherited::sampleRect( skinnable, rect, subControl, index ); +} + +QskAspect::States QskSpinBoxSkinlet::sampleStates(const QskSkinnable* const skinnable, QskAspect::Subcontrol subControl, int index) const +{ + auto states = Inherited::sampleStates( skinnable, subControl, index ); + + if ( subControl == QskSpinBox::Dec || subControl == QskSpinBox::Inc || subControl == QskSpinBox::TextPanel) + { + const auto* const spinbox = static_cast(skinnable); + const auto cursorPos = spinbox->effectiveSkinHint(QskSpinBox::Layout | QskAspect::Metric | QskAspect::Position).toPointF(); + const QPointF cursorPosAbs{qAbs(cursorPos.x()), qAbs(cursorPos.y())}; + const auto contain = !cursorPosAbs.isNull() && spinbox->subControlRect(subControl).contains(cursorPosAbs); + const auto pressed = contain && (cursorPos.x() < 0 || cursorPos.y() < 0); + const auto hovered = contain && !pressed; + states.setFlag(QskControl::Hovered, hovered); + states.setFlag(QskSpinBox::Pressed, pressed); + } + + return states; +} + +QSizeF QskSpinBoxSkinlet::sizeHint(const QskSkinnable* const skinnable, Qt::SizeHint sizeHint, const QSizeF& size) const +{ + using S = QskSpinBox; + const auto* const spinbox = static_cast(skinnable); + const auto layout = spinbox->alignmentHint(QskSpinBox::Layout); + const auto spacing = spinbox->spacingHint(QskSpinBox::Layout); + + const auto strutInc = spinbox->strutSizeHint(S::Inc); + const auto strutDec = spinbox->strutSizeHint(S::Dec); + const auto strutTxt = spinbox->strutSizeHint(S::TextPanel); + + if(sizeHint == Qt::MinimumSize || sizeHint == Qt::MaximumSize || Qt::PreferredSize) + { + if(layout == Qt::AlignTop || layout == Qt::AlignBottom || layout == Qt::AlignVCenter) + { + const auto w = qMax(strutDec.width(), qMax( strutTxt.width() , strutInc.width())); + const auto h = strutDec.height() + strutTxt.height() + strutInc.height(); + return {w,h + 2.0 * spacing}; + } + if(layout == Qt::AlignLeft || layout == Qt::AlignRight || layout == Qt::AlignHCenter) + { + const auto w = strutDec.width() + strutTxt.width() + strutInc.width(); + const auto h = qMax(strutDec.height(), qMax( strutTxt.height() , strutInc.height())); + return {w + 2.0 * spacing,h}; + } + if(layout == (Qt::AlignLeft | Qt::AlignVCenter) || layout == (Qt::AlignRight | Qt::AlignVCenter)) + { + const auto w = strutTxt.width() + qMax(strutInc.width(), strutDec.width()); + const auto h = qMax(2.0 * qMax(strutInc.height(), strutDec.height()), strutTxt.height()); + return {w + spacing ,h + spacing}; + } + if(layout == (Qt::AlignTop | Qt::AlignHCenter) || layout == (Qt::AlignTop | Qt::AlignHCenter)) + { + const auto w = qMax(strutTxt.width() , strutInc.width() + strutDec.width()); + const auto h = strutTxt.height() + qMax(strutInc.height() , strutDec.height()); + return {w + spacing, h + spacing}; + } + } + return Inherited::sizeHint(skinnable, sizeHint, size); + +} + +QRectF QskSpinBoxSkinlet::subControlRect(const QskSkinnable* const skinnable, const QRectF& rect, QskAspect::Subcontrol subControl) const +{ + if(subControl == QskSpinBox::DecText) return subControlRect(skinnable, rect, QskSpinBox::Dec); + if(subControl == QskSpinBox::IncText) return subControlRect(skinnable, rect, QskSpinBox::Inc); + if(subControl == QskSpinBox::Text) return subControlRect(skinnable, rect, QskSpinBox::TextPanel); + + const auto* const spinbox = static_cast(skinnable); + const auto layout = spinbox->alignmentHint(QskSpinBox::Layout); + const auto spacing = spinbox->spacingHint(QskSpinBox::Layout); + + using S = QskSpinBox; + + QRectF rects[Count] = + { + { {}, spinbox->strutSizeHint(S::Dec)}, + { {}, spinbox->strutSizeHint(S::TextPanel)}, + { {}, spinbox->strutSizeHint(S::Inc)}, + }; + + const auto center = rect.center(); + + // TODO center everything + + if(layout == Qt::AlignLeft) + { + rects[Txt].moveTopLeft({0.0 /************/, center.y() - rects[Txt].height() * 0.5}); + rects[Dec].moveTopLeft({rects[Txt].right() + spacing, center.y() - rects[Dec].height() * 0.5}); + rects[Inc].moveTopLeft({rects[Dec].right() + spacing, center.y() - rects[Inc].height() * 0.5}); + } + else if(layout == Qt::AlignRight) + { + rects[Dec].moveTopLeft({0.0 /************/, center.y() - rects[Dec].height() * 0.5}); + rects[Inc].moveTopLeft({rects[Dec].right() + spacing, center.y() - rects[Inc].height() * 0.5}); + rects[Txt].moveTopLeft({rects[Inc].right() + spacing, center.y() - rects[Txt].height() * 0.5}); + } + else if(layout == Qt::AlignTop) + { + rects[Txt].moveTopLeft({center.x() - rects[Txt].width() * 0.5, 0.0 }); + rects[Inc].moveTopLeft({center.x() - rects[Inc].width() * 0.5, rects[Txt].bottom() + spacing}); + rects[Dec].moveTopLeft({center.x() - rects[Dec].width() * 0.5, rects[Inc].bottom() + spacing}); + } + else if(layout == Qt::AlignBottom) + { + rects[Inc].moveTopLeft({center.x() - rects[Inc].width() * 0.5, 0.0}); + rects[Dec].moveTopLeft({center.x() - rects[Dec].width() * 0.5, rects[Inc].bottom() + spacing}); + rects[Txt].moveTopLeft({center.x() - rects[Txt].width() * 0.5, rects[Dec].bottom() + spacing}); + } + else if(layout == Qt::AlignHCenter) + { + rects[Dec].moveTopLeft({0.0 /************/, center.y() - rects[Dec].height() * 0.5}); + rects[Txt].moveTopLeft({rects[Dec].right() + spacing, center.y() - rects[Txt].height() * 0.5}); + rects[Inc].moveTopLeft({rects[Txt].right() + spacing, center.y() - rects[Inc].height() * 0.5}); + } + else if(layout == Qt::AlignVCenter) + { + rects[Inc].moveTopLeft({center.x() - rects[Inc].width() * 0.5, 0.0}); + rects[Txt].moveTopLeft({center.x() - rects[Txt].width() * 0.5, rects[Inc].bottom() + spacing}); + rects[Dec].moveTopLeft({center.x() - rects[Dec].width() * 0.5, rects[Txt].bottom() + spacing}); + } + else if(layout == (Qt::AlignLeft | Qt::AlignVCenter)) + { + rects[Txt].moveTopLeft({0.0 /************/, center.y() - rects[Txt].height() * 0.5 }); + rects[Inc].moveTopLeft({rects[Txt].right() + spacing, center.y() - spacing * 0.5 - rects[Inc].height()}); + rects[Dec].moveTopLeft({rects[Txt].right() + spacing, center.y() + spacing * 0.5}); + } + else if(layout == (Qt::AlignRight | Qt::AlignVCenter)) + { + const auto dx = qMax(rects[Inc].width(), rects[Dec].width()); + rects[Inc].moveTopLeft({dx - rects[Inc].width(), center.y() - spacing * 0.5 - rects[Inc].height()}); + rects[Dec].moveTopLeft({dx - rects[Dec].width(), center.y() + spacing * 0.5}); + rects[Txt].moveTopLeft({dx + spacing, center.y() - rects[Txt].height() * 0.5 }); + } + else if(layout == (Qt::AlignTop | Qt::AlignHCenter)) + { + rects[Txt].moveTopLeft({center.x() - rects[Txt].width() * 0.5, 0.0 }); + rects[Dec].moveTopLeft({rects[Txt].center().x() - spacing * 0.5 - rects[Dec].width(), rects[Txt].bottom() + spacing }); + rects[Inc].moveTopLeft({rects[Txt].center().x() + spacing * 0.5, rects[Txt].bottom() + spacing }); + } + else if(layout == (Qt::AlignBottom | Qt::AlignHCenter)) + { + const auto dx = qMax(rects[Inc].width(), rects[Dec].width()); + const auto dy = qMax(rects[Inc].height(), rects[Dec].height()); + rects[Txt].moveTopLeft({center.x() - rects[Txt].width() * 0.5, center.y() - rects[Txt].height() * 0.5}); + rects[Dec].moveTopLeft({center.x() - spacing * 0.5 - rects[Dec].width() , rects[Txt].top() - spacing - rects[Dec].height() }); + rects[Inc].moveTopLeft({center.x() + spacing * 0.5, rects[Txt].top() - spacing - rects[Inc].height() }); + } + + if(subControl == S::Dec) + { + return rects[Dec]; + } + if(subControl == S::TextPanel) + { + return rects[Txt]; + } + if(subControl == S::Inc) + { + return rects[Inc]; + } + + return Inherited::subControlRect(skinnable, rect, subControl); +} + +QSGNode* QskSpinBoxSkinlet::updateSubNode(const QskSkinnable* const skinnable, const quint8 nodeRole, QSGNode* const node) const +{ + if(nodeRole == IncPanel) { return updateSeriesNode( skinnable, QskSpinBox::Inc, node); } + if(nodeRole == DecPanel) { return updateSeriesNode( skinnable, QskSpinBox::Dec, node ); } + if(nodeRole == IncText) { return updateTextNode( skinnable, node, INC_TEXT, QskSpinBox::IncText); } + if(nodeRole == DecText) { return updateTextNode( skinnable, node, DEC_TEXT, QskSpinBox::DecText ); } + if(nodeRole == TextPanel) { return updateSeriesNode( skinnable, QskSpinBox::TextPanel, node ); } + if(nodeRole == TextText) { return updateTextNode( skinnable, node, QString::number(static_cast(skinnable)->value()), QskSpinBox::Text ); } + return Inherited::updateSubNode(skinnable, nodeRole, node); +} + +QSGNode *QskSpinBoxSkinlet::updateSampleNode(const QskSkinnable* const skinnable, QskAspect::Subcontrol subControl, const int index, QSGNode* const node) const +{ + const auto* const spinbox = static_cast(skinnable); + + if ( subControl == QskSpinBox::Dec || subControl == QskSpinBox::Inc || subControl == QskSpinBox::TextPanel ) + { + const auto rect = sampleRect(spinbox, spinbox->contentsRect(), subControl, index); + return updateBoxNode( skinnable, node, rect, subControl ); + } + + return Inherited::updateSampleNode( skinnable, subControl, index, node ); +} diff --git a/src/controls/QskSpinBoxSkinlet.h b/src/controls/QskSpinBoxSkinlet.h new file mode 100644 index 00000000..ab6cbf65 --- /dev/null +++ b/src/controls/QskSpinBoxSkinlet.h @@ -0,0 +1,28 @@ +/****************************************************************************** + * Copyright (C) 2023 Edelhirsch Software GmbH + * This file may be used under the terms of the 3-clause BSD License + *****************************************************************************/ + +#pragma once + +#include + +class QSK_EXPORT QskSpinBoxSkinlet : public QskSkinlet +{ + Q_GADGET + using Inherited = QskSkinlet; +public: + enum NodeRole + { + IncPanel, IncText, DecPanel, DecText, TextPanel, TextText, RoleCount + }; + Q_INVOKABLE QskSpinBoxSkinlet( QskSkin* = nullptr ); +protected: + 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* skinnable, QskAspect::Subcontrol subControl, int index ) const override; + QSizeF sizeHint( const QskSkinnable*, Qt::SizeHint, const QSizeF& ) const override; + QRectF subControlRect( const QskSkinnable*, const QRectF&, QskAspect::Subcontrol ) const override; + QSGNode* updateSubNode( const QskSkinnable*, quint8 nodeRole, QSGNode* ) const override; + QSGNode* updateSampleNode( const QskSkinnable* skinnable, QskAspect::Subcontrol subControl, int index, QSGNode* node ) const override; +}; diff --git a/src/src.pro b/src/src.pro index eed2d401..9250a6b3 100644 --- a/src/src.pro +++ b/src/src.pro @@ -227,6 +227,8 @@ HEADERS += \ controls/QskSliderSkinlet.h \ controls/QskStatusIndicator.h \ controls/QskStatusIndicatorSkinlet.h \ + controls/QskSpinBox.h \ + controls/QskSpinBoxSkinlet.h \ controls/QskSubWindowArea.h \ controls/QskSubWindowAreaSkinlet.h \ controls/QskSubWindow.h \ @@ -309,6 +311,8 @@ SOURCES += \ controls/QskSkinnable.cpp \ controls/QskSlider.cpp \ controls/QskSliderSkinlet.cpp \ + controls/QskSpinBox.cpp \ + controls/QskSpinBoxSkinlet.cpp \ controls/QskStatusIndicator.cpp \ controls/QskStatusIndicatorSkinlet.cpp \ controls/QskSubWindowArea.cpp \