File indexing completed on 2024-05-12 04:44:29

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // Own headers
0005 // First the interface, which forces the header to be self-contained.
0006 #include "abstractdiagram.h"
0007 // Second, the private implementation.
0008 #include "abstractdiagram_p.h" // IWYU pragma: associated
0009 
0010 #include "helper.h"
0011 #include <qcolor.h>
0012 #include <qglobal.h>
0013 #include <qimage.h>
0014 #include <qnamespace.h>
0015 #include <qpalette.h>
0016 #include <qsize.h>
0017 #include <qstyle.h>
0018 #include <qstyleoption.h>
0019 #include <qwidget.h>
0020 class QHideEvent;
0021 class QShowEvent;
0022 
0023 namespace PerceptualColor
0024 {
0025 /** @brief The constructor.
0026  * @param parent The widget’s parent widget. This parameter will be passed
0027  * to the base class’s constructor. */
0028 AbstractDiagram::AbstractDiagram(QWidget *parent)
0029     : QWidget(parent)
0030     , d_pointer(new AbstractDiagramPrivate())
0031 {
0032 }
0033 
0034 /** @brief Destructor */
0035 AbstractDiagram::~AbstractDiagram() noexcept
0036 {
0037 }
0038 
0039 /** @brief The color for painting focus indicators
0040  * @returns The color for painting focus indicators. This color is based on
0041  * the current widget style at the moment this function is called. The value
0042  * might therefore be different on the next function call, if the widget style
0043  * has been switched by the user in the meantime.
0044  * @note As there is no build-in support in Qt to get this information, we
0045  * have to do some best guess, which might go wrong on some styles. */
0046 QColor AbstractDiagram::focusIndicatorColor() const
0047 {
0048     return palette().color(QPalette::ColorGroup::Active, QPalette::ColorRole::Highlight);
0049 }
0050 
0051 /** @brief The rounded size of the widget measured in
0052  * <em>physical pixels</em>.
0053  *
0054  * @returns The rounded size of this widget,
0055  * measured in <em>physical pixels</em>, based on
0056  * <tt>QPaintDevice::devicePixelRatioF()</tt>. This is the recommended
0057  * image size for calling <tt>QPainter::drawImage()</tt> during a paint event.
0058  * Both, width and height are guaranteed to be ≥ 0.
0059  *
0060  * Example: You want to prepare a <tt>QImage</tt> of the hole widget to be
0061  * used in <tt>QWidget::paintEvent()</tt>. To make sure a crisp rendering,
0062  * you have to
0063  *
0064  * - Prepare an image with the size that this function returns.
0065  * - Set <tt>QImage::setDevicePixelRatio()</tt> of the image to the same
0066  *   value as <tt>QPaintDevice::devicePixelRatioF()</tt> of the widget.
0067  * - Actually paint the image on the widget at position <tt>(0, 0)</tt>
0068  *   <em>without</em> anti-aliasing.
0069  *
0070  * @note If <tt>QPaintDevice::devicePixelRatioF()</tt> is not an integer,
0071  * the result of this function is rounded down. Qt’s widget geometry code
0072  * has no documentation about how this is handled. However, Qt seems to
0073  * round up starting with 0.5, at least on Linux/X11. But there are a few
0074  * themes (for example the “Kvantum style engine” with the style
0075  * “MildGradientKvantum”) that seem to round down: This becomes visible, as
0076  * the corresponding last physical pixels are not automatically redrawn before
0077  * executing the <tt>paintEvent()</tt> code. To avoid relying on undocumented
0078  * behaviour and to avoid known problems with some styles, this function
0079  * is conservative and always rounds down. */
0080 QSize AbstractDiagram::physicalPixelSize() const
0081 {
0082     // Assert that static_cast<int> always rounds down.
0083     static_assert(static_cast<int>(1.9) == 1);
0084     static_assert(static_cast<int>(1.5) == 1);
0085     static_assert(static_cast<int>(1.0) == 1);
0086     // Multiply the size with the (floating point) scale factor
0087     // and than round down (by using static_cast<int>).
0088     const int width = static_cast<int>(size().width() * devicePixelRatioF());
0089     const int height = static_cast<int>(size().height() * devicePixelRatioF());
0090     return QSize(qMax(width, 0), qMax(height, 0));
0091 }
0092 
0093 /** @brief The maximum possible size of a square within the widget, measured
0094  * in <em>physical pixels</em>.
0095  *
0096  * This is the shorter value of width and height of the widget.
0097  *
0098  * @returns The maximum possible size of a square within the widget, measured
0099  * in <em>physical pixels</em>. Both, width and height are guaranteed
0100  * to be ≥ 0.
0101  *
0102  * @sa @ref maximumWidgetSquareSize */
0103 int AbstractDiagram::maximumPhysicalSquareSize() const
0104 {
0105     return qMin(physicalPixelSize().width(), physicalPixelSize().height());
0106 }
0107 
0108 /** @brief The maximum possible size of a square within the widget, measured
0109  * in <em>device-independent pixels</em>.
0110  *
0111  * This is the conversion of @ref maximumPhysicalSquareSize to the unit
0112  * <em>device-independent pixels</em>. It might be <em>smaller</em> than
0113  * the shortest value of <tt>QWidget::width()</tt> and
0114  * <tt>QWidget::height()</tt> because @ref maximumPhysicalSquareSize
0115  * might have rounded down.
0116  *
0117  * @returns The maximum possible size of a square within the widget, measured
0118  * in <em>device-independent pixels</em>. */
0119 qreal AbstractDiagram::maximumWidgetSquareSize() const
0120 {
0121     return (maximumPhysicalSquareSize() / devicePixelRatioF());
0122 }
0123 
0124 /** @brief Background for semi-transparent colors.
0125  *
0126  * When showing a semi-transparent color, there has to be a background
0127  * on which it is shown. This function provides a suitable background
0128  * for showcasing a color.
0129  *
0130  * Example code (to use within a class that inherits from
0131  * @ref PerceptualColor::AbstractDiagram):
0132  * @snippet testabstractdiagram.cpp useTransparencyBackground
0133  *
0134  * @returns An image of a mosaic of neutral gray rectangles of different
0135  * lightness. You can use this as tiles to paint a background.
0136  *
0137  * @note The image is considering QWidget::devicePixelRatioF() to deliver
0138  * crisp (correctly scaled) images also for high-DPI devices.
0139  * The painting does not use floating point drawing, but rounds
0140  * to full integers. Therefore, the result is always a sharp image.
0141  * This function takes care that each square has the same physical pixel
0142  * size, without scaling errors or anti-aliasing errors.
0143  *
0144  * @internal
0145  * @sa @ref transparencyBackground(qreal devicePixelRatioF)
0146  * @endinternal */
0147 QImage AbstractDiagram::transparencyBackground() const
0148 {
0149     return PerceptualColor::transparencyBackground(devicePixelRatioF());
0150 }
0151 
0152 /** @brief The outline thickness of a handle.
0153  *
0154  * @returns The outline thickness of a (either circular or linear) handle.
0155  * Measured in <em>device-independent pixels</em>. */
0156 int AbstractDiagram::handleOutlineThickness() const
0157 {
0158     /** @note The return value is constant. For a given object instance, this
0159      * function returns the same value every time it is called. This constant
0160      * value may be different for different instances of the object. */
0161     return 2;
0162 }
0163 
0164 /** @brief The radius of a circular handle.
0165  * @returns The radius of a circular handle, measured in
0166  * <em>device-independent pixels</em>. */
0167 qreal AbstractDiagram::handleRadius() const
0168 {
0169     /** @note The return value is constant. For a given object instance, this
0170      * function returns the same value every time it is called. This constant
0171      * value may be different for different instances of the object. */
0172     return handleOutlineThickness() * 2.5;
0173 }
0174 
0175 /** @brief The thickness of a color gradient.
0176  *
0177  * This is the thickness of a one-dimensional gradient, for example in
0178  * a slider or a color wheel.
0179  *
0180  * @returns The thickness of a slider or a color wheel, measured in
0181  * <em>device-independent pixels</em>.
0182  *
0183  * @sa @ref gradientMinimumLength() */
0184 int AbstractDiagram::gradientThickness() const
0185 {
0186     ensurePolished();
0187     int result = 0;
0188     QStyleOptionSlider styleOption;
0189     styleOption.initFrom(this); // Sets also QStyle::State_MouseOver
0190     styleOption.orientation = Qt::Horizontal;
0191     result = qMax(result, style()->pixelMetric(QStyle::PM_SliderThickness, &styleOption, this));
0192     styleOption.orientation = Qt::Vertical;
0193     result = qMax(result, style()->pixelMetric(QStyle::PM_SliderThickness, &styleOption, this));
0194     result = qMax(result, qRound(handleRadius()));
0195     // No supplementary space for ticks is added.
0196     return result;
0197 }
0198 
0199 /** @brief The minimum length of a color gradient.
0200  *
0201  * This is the minimum length of a one-dimensional gradient, for example in
0202  * a slider or a color wheel. This is also the minimum width and minimum
0203  * height of two-dimensional gradients.
0204  *
0205  * @returns The length of a gradient, measured in
0206  * <em>device-independent pixels</em>.
0207  *
0208  * @sa @ref gradientThickness() */
0209 int AbstractDiagram::gradientMinimumLength() const
0210 {
0211     ensurePolished();
0212     QStyleOptionSlider option;
0213     option.initFrom(this);
0214     return qMax(
0215         // Parameter: style-based value:
0216         qMax(
0217             // Similar to QSlider sizeHint():
0218             84,
0219             // Similar to QSlider::minimumSizeHint():
0220             style()->pixelMetric(QStyle::PM_SliderLength, &option, this)),
0221         // Parameter:
0222         gradientThickness());
0223 }
0224 
0225 /** @brief The empty space around diagrams reserved for the focus indicator.
0226  *
0227  * Measured in <em>device-independent pixels</em>.
0228  *
0229  * @returns The empty space around diagrams reserved for the focus
0230  * indicator. */
0231 int AbstractDiagram::spaceForFocusIndicator() const
0232 {
0233     // 1 × handleOutlineThickness() for the focus indicator itself.
0234     // 2 × handleOutlineThickness() for the space between the focus indicator
0235     // and the diagram.
0236     return 3 * handleOutlineThickness();
0237 }
0238 
0239 /** @brief An appropriate color for a handle, depending on the background
0240  * lightness.
0241  * @param lightness The background lightness. Valid range: <tt>[0, 100]</tt>.
0242  * @returns An appropriate color for a handle. This color will provide
0243  * contrast to the background. */
0244 QColor AbstractDiagram::handleColorFromBackgroundLightness(qreal lightness) const
0245 {
0246     if (lightness >= 50) {
0247         return Qt::black;
0248     }
0249     return Qt::white;
0250 }
0251 
0252 /** @brief If this widget is actually visible.
0253  *
0254  * Unlike <tt>QWidget::isVisible</tt>, minimized windows are <em>not</em>
0255  * considered visible.
0256  *
0257  * Changes can be observed with
0258  * @ref AbstractDiagram::actualVisibilityToggledEvent.
0259  *
0260  * @returns If this widget is actually visible.
0261  *
0262  * @internal
0263  *
0264  * This information is based on the last @ref AbstractDiagram::showEvent
0265  * or @ref AbstractDiagram::hideEvent that was received. */
0266 bool AbstractDiagram::isActuallyVisible() const
0267 {
0268     return d_pointer->m_isActuallyVisible;
0269 }
0270 
0271 /** @brief Event occurring after @ref isActuallyVisible has been toggled.
0272  *
0273  * This function is called if and only if @ref isActuallyVisible has
0274  * actually been toggled. */
0275 void AbstractDiagram::actualVisibilityToggledEvent()
0276 {
0277 }
0278 
0279 /** @brief React on a show event.
0280  *
0281  * Reimplemented from base class.
0282  *
0283  * @param event The show event.
0284  *
0285  * @internal
0286  *
0287  * @sa @ref AbstractDiagram::isActuallyVisible */
0288 void AbstractDiagram::showEvent(QShowEvent *event)
0289 {
0290     QWidget::showEvent(event);
0291     if (d_pointer->m_isActuallyVisible == false) {
0292         d_pointer->m_isActuallyVisible = true;
0293         actualVisibilityToggledEvent();
0294     }
0295 }
0296 
0297 /** @brief React on a hide event.
0298  *
0299  * Reimplemented from base class.
0300  *
0301  * @param event The hide event.
0302  *
0303  * @internal
0304  *
0305  * @sa @ref AbstractDiagram::isActuallyVisible */
0306 void AbstractDiagram::hideEvent(QHideEvent *event)
0307 {
0308     QWidget::hideEvent(event);
0309     if (d_pointer->m_isActuallyVisible == true) {
0310         d_pointer->m_isActuallyVisible = false;
0311         actualVisibilityToggledEvent();
0312     }
0313 }
0314 
0315 /** @brief An alternative to QWidget::update(). It’s a workaround
0316  * that avoids trouble with overload resolution.
0317  *
0318  * Connecting a signal to the slot <tt>
0319  * <a href="https://doc.qt.io/qt-6/qwidget.html#update">QWidget::update()</a>
0320  * </tt> is surprisingly difficult, at least if you want to use the functor
0321  * syntax (which provides compile-time checks) for the connection. A simple
0322  * connection fails to compile because it fails to do a correct  overload
0323  * resolution, as there is more than one slot called <tt>update</tt>. Now, <tt>
0324  * <a href="https://doc.qt.io/qt-6/qtglobal.html#qOverload">qOverload&lt;&gt;()
0325  * </a></tt> can be used to choose the correct overload, but in this special
0326  * case, <tt><a href="https://doc.qt.io/qt-6/qtglobal.html#qOverload">
0327  * qOverload&lt;&gt;()</a></tt> generates compiler warnings.
0328  *
0329  * Instead of connecting to <tt>
0330  * <a href="https://doc.qt.io/qt-6/qwidget.html#update">QWidget::update()
0331  * </a></tt> directly, simply connect to this slot instead. It calls
0332  * the actual <tt><a href="https://doc.qt.io/qt-6/qwidget.html#update">
0333  * QWidget::update()</a></tt>, but avoids the annoyance with the overload
0334  * resolution */
0335 void AbstractDiagram::callUpdate()
0336 {
0337     update();
0338 }
0339 
0340 } // namespace PerceptualColor