diff --git a/examples/gallery/dialog/DialogPage.cpp b/examples/gallery/dialog/DialogPage.cpp index 0fc586d8..e96a10eb 100644 --- a/examples/gallery/dialog/DialogPage.cpp +++ b/examples/gallery/dialog/DialogPage.cpp @@ -104,6 +104,9 @@ namespace auto directorySelectionButton = new Button( "Directory selection", this ); connect( directorySelectionButton, &Button::clicked, this, &ButtonBox::execDirectorySelection ); + auto colorSelectionButton = new Button( "Color selection", this ); + connect( colorSelectionButton, &Button::clicked, this, &ButtonBox::execColorSelection ); + setExtraSpacingAt( Qt::BottomEdge ); } @@ -181,6 +184,15 @@ namespace // not implemented for now (class is not public) #else ( void ) qskDialog->selectDirectory( "select directory", QDir::currentPath() ); +#endif + } + + void execColorSelection() + { +#ifndef QSK_USE_EXEC + // not implemented for now (class is not public) +#else + ( void ) qskDialog->selectColor( "select color" ); #endif } }; diff --git a/playground/systemdialogs/main.cpp b/playground/systemdialogs/main.cpp index a7a1f7d9..a7281b5c 100644 --- a/playground/systemdialogs/main.cpp +++ b/playground/systemdialogs/main.cpp @@ -111,6 +111,12 @@ namespace { switch( dialogType ) { + case ColorDialog: + { + auto color = qskDialog->selectColor( "select color" ); + qDebug() << "selected color" << QColor( color ); + break; + } case FileDialog: { auto file = qskDialog->selectFile( "select file", QDir::currentPath() ); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7fde8b5e..124fc8c7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -452,7 +452,14 @@ list(APPEND HEADERS dialogs/QskSelectionWindow.h ) +list(APPEND PRIVATE_HEADERS + dialogs/QskColorPicker.h + dialogs/QskColorPickerSkinlet.h +) + list(APPEND SOURCES + dialogs/QskColorPicker.cpp + dialogs/QskColorPickerSkinlet.cpp dialogs/QskDialogButton.cpp dialogs/QskDialogButtonBox.cpp dialogs/QskDialog.cpp diff --git a/src/controls/QskSkin.cpp b/src/controls/QskSkin.cpp index 1b4a73e7..0ada463b 100644 --- a/src/controls/QskSkin.cpp +++ b/src/controls/QskSkin.cpp @@ -109,6 +109,9 @@ #include "QskStatusIndicator.h" #include "QskStatusIndicatorSkinlet.h" +#include "QskColorPicker.h" +#include "QskColorPickerSkinlet.h" + #include "QskInternalMacros.h" #include @@ -219,6 +222,7 @@ QskSkin::QskSkin( QObject* parent ) declareSkinlet< QskProgressBar, QskProgressBarSkinlet >(); declareSkinlet< QskProgressRing, QskProgressRingSkinlet >(); declareSkinlet< QskRadioBox, QskRadioBoxSkinlet >(); + declareSkinlet< QskColorPicker, QskColorPickerSkinlet >(); const QFont font = QGuiApplication::font(); setupFontTable( font.family(), font.italic() ); diff --git a/src/dialogs/QskColorPicker.cpp b/src/dialogs/QskColorPicker.cpp new file mode 100644 index 00000000..8f21ae0b --- /dev/null +++ b/src/dialogs/QskColorPicker.cpp @@ -0,0 +1,126 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskColorPicker.h" +#include "QskColorPickerSkinlet.h" +#include "QskEvent.h" +#include "QskFunctions.h" + +QSK_SUBCONTROL( QskColorPicker, Panel ) +QSK_SUBCONTROL( QskColorPicker, ColorPane ) +QSK_SUBCONTROL( QskColorPicker, Selector ) + +class QskColorPicker::PrivateData +{ + public: + qreal value = 255; + bool isPressed = false; + int colorChangedEventType; +}; + +QskColorPicker::QskColorPicker( QQuickItem* parent ) + : Inherited( parent ) + , m_data( new PrivateData ) +{ + m_data->colorChangedEventType = QEvent::registerEventType(); + + setAcceptedMouseButtons( Qt::LeftButton ); + setBoundaries( 0, 255 ); +} + +QskColorPicker::~QskColorPicker() = default; + +QColor QskColorPicker::selectedColor() const +{ + auto* skinlet = static_cast< const QskColorPickerSkinlet* >( effectiveSkinlet() ); + Q_ASSERT( skinlet ); + return skinlet->selectedColor(); +} + +qreal QskColorPicker::value() const +{ + return m_data->value; +} + +qreal QskColorPicker::valueAsRatio() const +{ + return valueAsRatio( m_data->value ); +} + +int QskColorPicker::colorChangedEventType() const +{ + return m_data->colorChangedEventType; +} + +void QskColorPicker::setValue( qreal v ) +{ + if( !qskFuzzyCompare( m_data->value, v ) ) + { + m_data->value = v; + update(); + Q_EMIT valueChanged( m_data->value ); + } +} + +void QskColorPicker::setValueAsRatio( qreal ratio ) +{ + ratio = qBound( 0.0, ratio, 1.0 ); + setValue( minimum() + ratio * boundaryLength() ); +} + +bool QskColorPicker::event( QEvent* event ) +{ + if( event->type() == colorChangedEventType() ) + { + event->setAccepted( true ); + Q_EMIT selectedColorChanged(); + return true; + } + + return Inherited::event( event ); +} + +void QskColorPicker::mousePressEvent( QMouseEvent* event ) +{ + m_data->isPressed = true; + updatePosition( event ); +} + +void QskColorPicker::mouseMoveEvent( QMouseEvent* event ) +{ + if( !m_data->isPressed ) + { + return; + } + + updatePosition( event ); +} + +void QskColorPicker::mouseReleaseEvent( QMouseEvent* ) +{ + m_data->isPressed = false; +} + +void QskColorPicker::updatePosition( QMouseEvent* event ) +{ + auto p = qskMousePosition( event ); + const auto rect = subControlRect( ColorPane ); + + p.rx() = qBound( rect.x(), p.x(), rect.right() ); + p.ry() = qBound( rect.y(), p.y(), rect.bottom() ); + + const auto oldX = positionHint( Selector | QskAspect::Horizontal ); + const auto oldY = positionHint( Selector | QskAspect::Vertical ); + + if( !qskFuzzyCompare( p.x(), oldX ) || !qskFuzzyCompare( p.y(), oldY ) ) + { + setPositionHint( Selector | QskAspect::Horizontal, p.x() ); + setPositionHint( Selector | QskAspect::Vertical, p.y() ); + + update(); + } +} + +#include "moc_QskColorPicker.cpp" diff --git a/src/dialogs/QskColorPicker.h b/src/dialogs/QskColorPicker.h new file mode 100644 index 00000000..fa28e12c --- /dev/null +++ b/src/dialogs/QskColorPicker.h @@ -0,0 +1,54 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#ifndef QSK_COLOR_PICKER_H +#define QSK_COLOR_PICKER_H + +#include "QskBoundedControl.h" + +#include + +class QskColorPicker : public QskBoundedControl +{ + Q_OBJECT + + using Inherited = QskBoundedControl; + + public: + QSK_SUBCONTROLS( Panel, ColorPane, Selector ) + + QskColorPicker( QQuickItem* parentItem = nullptr ); + ~QskColorPicker() override; + + QColor selectedColor() const; + + qreal value() const; // value as in hue / saturation / value + qreal valueAsRatio() const; // [0.0, 1.0] + using QskBoundedControl::valueAsRatio; + + int colorChangedEventType() const; + + public Q_SLOTS: + void setValue( qreal ); + void setValueAsRatio( qreal ); + + Q_SIGNALS: + void valueChanged( qreal ); + void selectedColorChanged() const; + + protected: + bool event( QEvent* ) override; + void mousePressEvent( QMouseEvent* ) override; + void mouseMoveEvent( QMouseEvent* ) override; + void mouseReleaseEvent( QMouseEvent* ) override; + + private: + void updatePosition( QMouseEvent* ); + + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; + +#endif diff --git a/src/dialogs/QskColorPickerSkinlet.cpp b/src/dialogs/QskColorPickerSkinlet.cpp new file mode 100644 index 00000000..d6ea1b49 --- /dev/null +++ b/src/dialogs/QskColorPickerSkinlet.cpp @@ -0,0 +1,181 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#include "QskColorPickerSkinlet.h" +#include "QskColorPicker.h" + +#include "QskSGNode.h" +#include "QskPaintedNode.h" + +#include + +using Q = QskColorPicker; + +namespace +{ + class ColorPaneNode : public QskPaintedNode + { + public: + ColorPaneNode() + { + } + + void paint( QPainter* p, const QSize& size, const void* nodeData ) override + { + if( m_image.size() != size ) + { + m_image = QImage( size.width(), size.height(), QImage::Format_RGB32 ); + } + + QColor color; + float h, s; + const float v = *reinterpret_cast< const int* >( nodeData ) / 255.0; + + for( int x = 0; x < m_image.width(); x++ ) + { + h = ( float ) x / m_image.width(); + + for( int y = 0; y < m_image.height(); y++ ) + { + s = 1.0 - ( float ) y / m_image.height(); + color.setHsvF( h, s, v ); + m_image.setPixel( x, y, color.rgb() ); + } + } + + p->drawImage( QPointF( 0, 0 ), m_image ); + } + + void updateNode( QQuickWindow* window, const QRectF& rect, int value ) + { + update( window, rect, QSizeF(), &value ); + } + + QColor selectedColor( const QPointF& p ) const + { + return m_image.pixelColor( p.toPoint() ); + } + + protected: + QskHashValue hash( const void* nodeData ) const override + { + const auto* value = reinterpret_cast< const int* >( nodeData ); + return *value; + } + + private: + QImage m_image; + }; +} + +class QskColorPickerSkinlet::PrivateData +{ + public: + QColor selectedColor; + ColorPaneNode* colorPaneNode = nullptr; +}; + +QskColorPickerSkinlet::QskColorPickerSkinlet( QskSkin* skin ) + : Inherited( skin ) + , m_data( new PrivateData ) +{ + setNodeRoles( { PanelRole, ColorPaneRole, SelectorRole } ); +} + +QskColorPickerSkinlet::~QskColorPickerSkinlet() = default; + +QSGNode* QskColorPickerSkinlet::updateSubNode( + const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const +{ + const auto q = static_cast< const Q* >( skinnable ); + + switch ( nodeRole ) + { + case PanelRole: + { + return updateBoxNode( skinnable, node, Q::Panel ); + } + case ColorPaneRole: + { + return updateColorPaneNode( q, node ); + } + case SelectorRole: + { + auto* n = updateBoxNode( skinnable, node, Q::Selector ); + updateSelectedColor( q ); + return n; + } + } + + return Inherited::updateSubNode( skinnable, nodeRole, node ); +} + +QRectF QskColorPickerSkinlet::subControlRect( + const QskSkinnable* skinnable, const QRectF& contentsRect, + QskAspect::Subcontrol subControl ) const +{ + const auto q = static_cast< const Q* >( skinnable ); + + if( subControl == Q::Panel || subControl == Q::ColorPane ) + { + return contentsRect; + } + + if( subControl == Q::Selector ) + { + const auto size = q->strutSizeHint( Q::Selector ); + const auto x = q->positionHint( Q::Selector | QskAspect::Horizontal ); + const auto y = q->positionHint( Q::Selector | QskAspect::Vertical ); + + QRectF r( { x - size.width() / 2.0, y - size.height() / 2.0 }, size ); + return r; + } + + return Inherited::subControlRect( skinnable, contentsRect, subControl ); +} + +QColor QskColorPickerSkinlet::selectedColor() const +{ + return m_data->selectedColor; +} + +QSGNode* QskColorPickerSkinlet::updateColorPaneNode( + const QskColorPicker* q, QSGNode* node ) const +{ + m_data->colorPaneNode = QskSGNode::ensureNode< ColorPaneNode >( node ); + + const auto rect = q->subControlRect( Q::ColorPane ); + m_data->colorPaneNode->updateNode( q->window(), rect, q->value() ); + + updateSelectedColor( q ); + + return m_data->colorPaneNode; +} + +QPointF QskColorPickerSkinlet::selectorPos( const Q* q ) const +{ + const auto x = q->positionHint( Q::Selector | QskAspect::Horizontal ); + const auto y = q->positionHint( Q::Selector | QskAspect::Vertical ); + + QPointF p( x, y ); + + const auto rect = q->subControlRect( Q::ColorPane ); + + p.rx() = qBound( rect.x(), p.x(), rect.right() ); + p.ry() = qBound( rect.y(), p.y(), rect.bottom() ); + + return p; +} + +void QskColorPickerSkinlet::updateSelectedColor( const Q* q ) const +{ + const auto color = m_data->colorPaneNode->selectedColor( selectorPos( q ) ); + m_data->selectedColor = color; + + auto* e = new QEvent( static_cast< QEvent::Type >( q->colorChangedEventType() ) ); + QCoreApplication::postEvent( const_cast< Q* >( q ), e ); +} + +#include "moc_QskColorPickerSkinlet.cpp" diff --git a/src/dialogs/QskColorPickerSkinlet.h b/src/dialogs/QskColorPickerSkinlet.h new file mode 100644 index 00000000..2bb7d844 --- /dev/null +++ b/src/dialogs/QskColorPickerSkinlet.h @@ -0,0 +1,52 @@ +/****************************************************************************** + * QSkinny - Copyright (C) The authors + * SPDX-License-Identifier: BSD-3-Clause + *****************************************************************************/ + +#ifndef QSK_COLOR_PICKER_SKINLET_H +#define QSK_COLOR_PICKER_SKINLET_H + +#include "QskSkinlet.h" + +class QskColorPicker; + +class QskColorPickerSkinlet : public QskSkinlet +{ + Q_GADGET + + using Inherited = QskSkinlet; + + public: + enum NodeRole : quint8 + { + PanelRole, + ColorPaneRole, + SelectorRole, + + RoleCount + }; + + Q_INVOKABLE QskColorPickerSkinlet( QskSkin* = nullptr ); + ~QskColorPickerSkinlet() override; + + QRectF subControlRect( const QskSkinnable*, + const QRectF&, QskAspect::Subcontrol ) const override; + + QColor selectedColor() const; + + protected: + QSGNode* updateSubNode( const QskSkinnable*, + quint8 nodeRole, QSGNode* ) const override; + + QSGNode* updateColorPaneNode( const QskColorPicker*, QSGNode* ) const; + + private: + QRectF cursorRect( const QskSkinnable*, const QRectF&, int index ) const; + QPointF selectorPos( const QskColorPicker* ) const; + void updateSelectedColor( const QskColorPicker* ) const; + + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; + +#endif diff --git a/src/dialogs/QskDialog.cpp b/src/dialogs/QskDialog.cpp index ee81f407..661caf79 100644 --- a/src/dialogs/QskDialog.cpp +++ b/src/dialogs/QskDialog.cpp @@ -14,11 +14,16 @@ #include "QskSelectionSubWindow.h" #include "QskSelectionWindow.h" +#include "QskBoxBorderColors.h" +#include "QskBoxBorderMetrics.h" +#include "QskBoxShapeMetrics.h" +#include "QskColorPicker.h" #include "QskEvent.h" #include "QskFunctions.h" #include "QskListView.h" #include "QskPushButton.h" #include "QskScrollArea.h" +#include "QskSlider.h" #include "QskFocusIndicator.h" @@ -458,6 +463,72 @@ namespace FileSystemView* m_fileView; }; + + template< typename W > + class ColorSelectionWindow : public WindowOrSubWindow< W > + { + using Inherited = WindowOrSubWindow< W >; + + public: + + ColorSelectionWindow( QObject* parent, const QString& title, + QskDialog::Actions actions, QskDialog::Action defaultAction ) + : WindowOrSubWindow< W >( parent, title, actions, defaultAction ) + { + auto* outerBox = new QskLinearBox( Qt::Vertical ); + outerBox->setMargins( 20 ); + outerBox->setSpacing( 20 ); +#if 1 + outerBox->setFixedSize( 700, 500 ); +#endif + auto* upperBox = new QskLinearBox( Qt::Horizontal, outerBox ); + upperBox->setSpacing( 12 ); + + m_picker = new QskColorPicker( upperBox ); + m_picker->setStrutSizeHint( QskColorPicker::Selector, { 18, 18 } ); + m_picker->setBoxShapeHint( QskColorPicker::Selector, { 100, Qt::RelativeSize } ); + m_picker->setBoxBorderMetricsHint( QskColorPicker::Selector, 2 ); + m_picker->setBoxBorderColorsHint( QskColorPicker::Selector, Qt::black ); + m_picker->setGradientHint( QskColorPicker::Selector, Qt::transparent ); + + auto* outputBox = new QskBox( upperBox ); + outputBox->setPanel( true ); + + QObject::connect( m_picker, &QskColorPicker::selectedColorChanged, + this, [this, outputBox]() + { + const auto c = m_picker->selectedColor(); + outputBox->setGradientHint( QskBox::Panel, c ); + } ); + + upperBox->setStretchFactor( m_picker, 9 ); + upperBox->setStretchFactor( outputBox, 1 ); + + + auto* valueSlider = new QskSlider( outerBox ); + valueSlider->setBoundaries( 0, 1 ); + valueSlider->setValue( m_picker->value() ); + + QskGradient g( Qt::black, Qt::white ); + g.setLinearDirection( Qt::Horizontal ); + valueSlider->setGradientHint( QskSlider::Groove, g ); + valueSlider->setGradientHint( QskSlider::Fill, Qt::transparent ); + valueSlider->setGradientHint( QskSlider::Handle, Qt::black ); + + QObject::connect( valueSlider, &QskSlider::valueChanged, + m_picker, &QskColorPicker::setValueAsRatio ); + + Inherited::setContentItem( outerBox ); + } + + QColor selectedColor() const + { + return m_picker->selectedColor(); + } + + private: + QskColorPicker* m_picker; + }; } static QQuickWindow* qskSomeQuickWindow() @@ -625,6 +696,17 @@ static QString qskSelectPath( FileSelectionWindow< W >& window ) return selectedFile; } +template< typename W > +static QColor qskSelectColor( ColorSelectionWindow< W >& window ) +{ + QColor selectedColor = window.selectedColor(); + + if( window.exec() == QskDialog::Accepted ) + selectedColor = window.selectedColor(); + + return selectedColor; +} + class QskDialog::PrivateData { public: @@ -801,6 +883,34 @@ QString QskDialog::selectDirectory( return qskSelectPath< QskDialogWindow >( window ); } +QColor QskDialog::selectColor( const QString& title ) const +{ +#if 1 + // should be parameters + const auto actions = QskDialog::Ok | QskDialog::Cancel; + const auto defaultAction = QskDialog::Ok; +#endif + + if ( m_data->policy == EmbeddedBox ) + { + auto quickWindow = qobject_cast< QQuickWindow* >( m_data->transientParent ); + + if ( quickWindow == nullptr ) + quickWindow = qskSomeQuickWindow(); + + if ( quickWindow ) + { + ColorSelectionWindow< QskDialogSubWindow > window( quickWindow, title, + actions, defaultAction ); + return qskSelectColor< QskDialogSubWindow >( window ); + } + } + + ColorSelectionWindow< QskDialogWindow > window( m_data->transientParent, title, + actions, defaultAction ); + return qskSelectColor< QskDialogWindow >( window ); +} + QskDialog::ActionRole QskDialog::actionRole( Action action ) { using Q = QPlatformDialogHelper; diff --git a/src/dialogs/QskDialog.h b/src/dialogs/QskDialog.h index 41d5f3e6..72bbf9b2 100644 --- a/src/dialogs/QskDialog.h +++ b/src/dialogs/QskDialog.h @@ -8,6 +8,7 @@ #include "QskGlobal.h" +#include #include #include @@ -136,6 +137,8 @@ class QSK_EXPORT QskDialog : public QObject Q_INVOKABLE QString selectDirectory( const QString& title, const QString& directory ) const; + Q_INVOKABLE QColor selectColor( const QString& title ) const; + static ActionRole actionRole( Action action ); Q_SIGNALS: