Use QLineEdit's functions to implement ButtonOverlay functions if possible

This commit is contained in:
Martchus 2020-06-09 22:03:26 +02:00
parent 7db2fd02fc
commit 5974690006
11 changed files with 279 additions and 91 deletions

View File

@ -9,8 +9,8 @@ set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
set(META_APP_DESCRIPTION
"Common Qt related C++ classes and routines used by my applications such as dialogs, widgets and models")
set(META_VERSION_MAJOR 6)
set(META_VERSION_MINOR 0)
set(META_VERSION_PATCH 7)
set(META_VERSION_MINOR 1)
set(META_VERSION_PATCH 0)
set(META_APP_VERSION ${META_VERSION_MAJOR}.${META_VERSION_MINOR}.${META_VERSION_PATCH})
project(${META_PROJECT_NAME})

View File

@ -1,8 +1,11 @@
#include "./buttonoverlay.h"
#include "./iconbutton.h"
#include <QAction>
#include <QComboBox>
#include <QCursor>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QStyle>
#include <QStyleOption>
#include <QToolTip>
@ -14,31 +17,60 @@ namespace QtUtilities {
/*!
* \class ButtonOverlay
* \brief The ButtonOverlay class is used to display buttons on top of other
* widgets.
* \brief The ButtonOverlay class is used to display buttons on top of other widgets.
*
* The class creates a new layout manager and sets it to the widget which is
* specified
* when constructing an instance. Thus this widget must not already have a
* layout manager.
* This class had been created before QLineEdit's functions setClearButtonEnabled() and
* addAction() have been available. (These functions have been available only since Qt 5.2.)
*
* The class is used to implement widget customization like ClearLineEidt and
* ClearComboBox.
* The downside of the "custom approach" compared to QLineEdit's own functions is that the
* buttons are shown over the text as the text margins are not updated accordingly. Hence
* the ButtonOverlay class has been updated to use QLineEdit's functions internally when the
* specified widget is QLineEdit-based and its QLineEdit has been passed to the constructor.
* However, when using any functions which can not be implemented using QLineEdit's own
* functions, the ButtonOverlay has to fallback to its "custom approach". All functions which
* cause this have a remark in their documentation.
*
* When QLineEdit's functions can not be used, the ButtonOverlay class creates a new layout
* manager and sets it to the widget specified when constructing an instance. Thus this widget
* must not already have a layout manager.
*
* The class is used to implement widget customization like ClearLineEidt and ClearComboBox
* and most of the times it makes sense to use these widgets instead of using ButtonOverlay
* directly.
*/
/*!
* \brief Constructs a button overlay for the specified \a widget.
* \param widget Specifies the widget to display the buttons on.
* \remarks This function enforces the "custom approach" mentioned in the class documentation
* and should therefore be avoided.
*/
ButtonOverlay::ButtonOverlay(QWidget *widget)
: m_widget(widget)
, m_buttonWidget(new QWidget(widget))
, m_buttonLayout(new QHBoxLayout(m_buttonWidget))
, m_buttonWidget(nullptr)
, m_buttonLayout(nullptr)
, m_clearButton(nullptr)
, m_infoButton(nullptr)
, m_infoButtonOrAction(nullptr)
{
buttonLayout()->setAlignment(Qt::AlignCenter | Qt::AlignRight);
widget->setLayout(m_buttonLayout);
fallbackToUsingCustomLayout();
}
/*!
* \brief Constructs a button overlay for the specified \a widget.
* \param widget Specifies the widget to display the buttons on.
* \param lineEdit Specifies the line edit used by \a widget to use the QLineEdit's functions
* for adding actions instead of a custom layout.
*/
ButtonOverlay::ButtonOverlay(QWidget *widget, QLineEdit *lineEdit)
: m_widget(widget)
, m_buttonWidget(lineEdit)
, m_buttonLayout(nullptr)
, m_clearButton(nullptr)
, m_infoButtonOrAction(nullptr)
{
if (!m_buttonWidget) {
fallbackToUsingCustomLayout();
}
}
/*!
@ -48,17 +80,60 @@ ButtonOverlay::~ButtonOverlay()
{
}
/*!
* \brief Returns whether the "custom approach" mentioned in the class documentation is used.
*/
bool ButtonOverlay::isUsingCustomLayout() const
{
return m_buttonLayout != nullptr;
}
/*!
* \brief Returns the layout manager holding the buttons.
* \remarks This function enforces the "custom approach" mentioned in the class documentation
* and should therefore be avoided.
*/
QHBoxLayout *ButtonOverlay::buttonLayout()
{
fallbackToUsingCustomLayout();
return m_buttonLayout;
}
/*!
* \brief Returns whether the clear button is enabled.
*/
bool ButtonOverlay::isClearButtonEnabled() const
{
if (isUsingCustomLayout()) {
return m_clearButton != nullptr;
}
return lineEditForWidget()->isClearButtonEnabled();
}
/*!
* \brief Returns whether the info button is enabled.
*/
bool ButtonOverlay::isInfoButtonEnabled() const
{
return m_infoButtonOrAction != nullptr;
}
/*!
* \brief Sets whether the clear button is enabled.
*/
void ButtonOverlay::setClearButtonEnabled(bool enabled)
{
if (isClearButtonEnabled() && !enabled) {
if (auto *const le = lineEditForWidget()) {
le->setClearButtonEnabled(enabled);
return;
}
const auto clearButtonEnabled = isClearButtonEnabled();
if (clearButtonEnabled && !enabled) {
// disable clear button
m_buttonLayout->removeWidget(m_clearButton);
delete m_clearButton;
m_clearButton = nullptr;
} else if (!isClearButtonEnabled() && enabled) {
} else if (!clearButtonEnabled && enabled) {
// enable clear button
m_clearButton = new IconButton;
m_clearButton->setHidden(isCleared());
@ -80,18 +155,27 @@ void ButtonOverlay::setClearButtonEnabled(bool enabled)
*/
void ButtonOverlay::enableInfoButton(const QPixmap &pixmap, const QString &infoText)
{
if (!m_infoButton) {
m_infoButton = new IconButton;
m_infoButton->setGeometry(0, 0, 16, 16);
QObject::connect(m_infoButton, &IconButton::clicked, std::bind(&ButtonOverlay::showInfo, this));
if (auto *const le = lineEditForWidget()) {
disableInfoButton();
auto *const action = le->addAction(QIcon(pixmap), QLineEdit::TrailingPosition);
action->setToolTip(infoText);
QObject::connect(action, &QAction::triggered, std::bind(&ButtonOverlay::showInfo, this));
m_infoButtonOrAction = action;
return;
}
auto *infoButton = static_cast<IconButton *>(m_infoButtonOrAction);
if (!infoButton) {
m_infoButtonOrAction = infoButton = new IconButton;
infoButton->setGeometry(0, 0, 16, 16);
QObject::connect(infoButton, &IconButton::clicked, std::bind(&ButtonOverlay::showInfo, this));
if (m_clearButton) {
m_buttonLayout->insertWidget(m_buttonLayout->count() - 2, m_infoButton);
m_buttonLayout->insertWidget(m_buttonLayout->count() - 2, infoButton);
} else {
m_buttonLayout->addWidget(m_infoButton);
m_buttonLayout->addWidget(infoButton);
}
}
m_infoButton->setPixmap(pixmap);
m_infoButton->setToolTip(infoText);
infoButton->setPixmap(pixmap);
infoButton->setToolTip(infoText);
}
/*!
@ -100,10 +184,17 @@ void ButtonOverlay::enableInfoButton(const QPixmap &pixmap, const QString &infoT
*/
void ButtonOverlay::disableInfoButton()
{
if (m_infoButton) {
m_buttonLayout->removeWidget(m_infoButton);
delete m_infoButton;
m_infoButton = nullptr;
if (auto *const le = lineEditForWidget()) {
if (auto *const infoAction = static_cast<QAction *>(m_infoButtonOrAction)) {
le->removeAction(infoAction);
m_infoButtonOrAction = nullptr;
}
return;
}
if (auto *infoButton = static_cast<IconButton *>(m_infoButtonOrAction)) {
m_buttonLayout->removeWidget(infoButton);
delete infoButton;
m_infoButtonOrAction = nullptr;
}
}
@ -111,9 +202,13 @@ void ButtonOverlay::disableInfoButton()
* \brief Adds a custom \a button.
*
* The button overlay takes ownership over the specified \a button.
*
* \remarks This function enforces the "custom approach" mentioned in the class documentation
* and should therefore be avoided.
*/
void ButtonOverlay::addCustomButton(QWidget *button)
{
fallbackToUsingCustomLayout();
m_buttonLayout->addWidget(button);
}
@ -121,9 +216,13 @@ void ButtonOverlay::addCustomButton(QWidget *button)
* \brief Inserts a custom \a button at the specified \a index.
*
* The button overlay takes ownership over the specified \a button.
*
* \remarks This function enforces the "custom approach" mentioned in the class documentation
* and should therefore be avoided.
*/
void ButtonOverlay::insertCustomButton(int index, QWidget *button)
{
fallbackToUsingCustomLayout();
m_buttonLayout->insertWidget(index, button);
}
@ -131,16 +230,20 @@ void ButtonOverlay::insertCustomButton(int index, QWidget *button)
* \brief Removes the specified custom \a button.
*
* The ownership of widget remains the same as when it was added.
*
* \remarks This function enforces the "custom approach" mentioned in the class documentation
* and should therefore be avoided.
*/
void ButtonOverlay::removeCustomButton(QWidget *button)
{
fallbackToUsingCustomLayout();
m_buttonLayout->removeWidget(button);
}
/*!
* \brief Updates the visibility of the clear button.
*
* This method is meant to be called when subclassing.
* This function is meant to be called when subclassing.
*/
void ButtonOverlay::updateClearButtonVisibility(bool visible)
{
@ -152,12 +255,75 @@ void ButtonOverlay::updateClearButtonVisibility(bool visible)
/*!
* \brief Clears the related widget.
*
* This method is meant to be implemented when subclassing.
* This function is meant to be implemented when subclassing to support the clear button.
*/
void ButtonOverlay::handleClearButtonClicked()
{
}
/*!
* \brief Applies additional handling when the button layout has been created.
*
* This function is meant to be implemented when subclassing when additional handling is
* required.
*/
void ButtonOverlay::handleCustomLayoutCreated()
{
}
/*!
* \brief Switches to the "custom approach".
* \remarks This function is internally used when any legacy function is called
* or when the QLineEdit for the specified widget can not be determined.
*/
void ButtonOverlay::fallbackToUsingCustomLayout()
{
// skip if custom layout is already used
if (isUsingCustomLayout()) {
return;
}
// disable QLineEdit's clear button and actions; save configuration
const auto clearButtonEnabled = isClearButtonEnabled();
if (clearButtonEnabled) {
setClearButtonEnabled(false);
}
auto *const iconAction = static_cast<QAction *>(m_infoButtonOrAction);
QPixmap infoPixmap;
QString infoText;
if (iconAction) {
const auto icon = iconAction->icon();
const auto sizes = icon.availableSizes();
infoPixmap = icon.pixmap(sizes.empty() ? QSize(16, 16) : sizes.front());
infoText = iconAction->toolTip();
disableInfoButton();
}
// initialize custom layout
m_buttonLayout = new QHBoxLayout(m_buttonWidget);
m_buttonWidget = new QWidget(m_widget);
m_buttonLayout->setAlignment(Qt::AlignCenter | Qt::AlignRight);
m_widget->setLayout(m_buttonLayout);
handleCustomLayoutCreated();
// restore old configuration
if (clearButtonEnabled) {
setClearButtonEnabled(true);
}
if (iconAction) {
enableInfoButton(infoPixmap, infoText);
}
}
/*!
* \brief Returns the QLineEdit used to implement the button overlay.
* \remarks This is always nullptr in case the "custom approach" is used.
*/
QLineEdit *ButtonOverlay::lineEditForWidget() const
{
return isUsingCustomLayout() ? nullptr : static_cast<QLineEdit *>(m_buttonWidget);
}
/*!
* \brief Returns whether the related widget is cleared.
*
@ -172,17 +338,27 @@ bool ButtonOverlay::isCleared() const
* \brief Shows the info text using a tool tip.
*
* This method is called when the info button is clicked.
*
* \todo Don't use QCursor::pos() here because it will not work under Wayland.
*/
void ButtonOverlay::showInfo()
{
if (m_infoButton) {
QToolTip::showText(QCursor::pos(), m_infoButton->toolTip(), m_infoButton);
if (!isUsingCustomLayout()) {
if (auto *const infoAction = static_cast<QAction *>(m_infoButtonOrAction)) {
QToolTip::showText(QCursor::pos(), infoAction->toolTip(), m_widget);
}
return;
}
if (auto *const infoButton = static_cast<IconButton *>(m_infoButtonOrAction)) {
QToolTip::showText(QCursor::pos(), infoButton->toolTip(), infoButton);
}
}
/*!
* \brief Sets the contents margins of the button layout so the overlay buttons will only be shown over the \a editFieldRect and
* not interfere with e.g. spin box buttons.
* \remarks This function enforces the "custom approach" mentioned in the class documentation
* and should therefore be avoided. Of course it makes sense to call it within handleCustomLayoutCreated().
*/
void ButtonOverlay::setContentsMarginsFromEditFieldRectAndFrameWidth(const QRect &editFieldRect, int frameWidth, int padding)
{

View File

@ -11,6 +11,7 @@ QT_FORWARD_DECLARE_CLASS(QString)
QT_FORWARD_DECLARE_CLASS(QPixmap)
QT_FORWARD_DECLARE_CLASS(QMargins)
QT_FORWARD_DECLARE_CLASS(QRect)
QT_FORWARD_DECLARE_CLASS(QLineEdit)
namespace QtUtilities {
@ -29,8 +30,10 @@ class QT_UTILITIES_EXPORT ButtonOverlay {
public:
explicit ButtonOverlay(QWidget *widget);
explicit ButtonOverlay(QWidget *widget, QLineEdit *lineEdit);
virtual ~ButtonOverlay();
bool isUsingCustomLayout() const;
QHBoxLayout *buttonLayout();
bool isClearButtonEnabled() const;
void setClearButtonEnabled(bool enabled);
@ -45,8 +48,11 @@ public:
protected:
void updateClearButtonVisibility(bool visible);
virtual void handleClearButtonClicked();
virtual void handleCustomLayoutCreated();
private:
void fallbackToUsingCustomLayout();
QLineEdit *lineEditForWidget() const;
void showInfo();
void setContentsMarginsFromEditFieldRectAndFrameWidth(const QRect &editFieldRect, int frameWidth, int padding = 0);
@ -54,32 +60,9 @@ private:
QWidget *m_buttonWidget;
QHBoxLayout *m_buttonLayout;
IconButton *m_clearButton;
IconButton *m_infoButton;
void *m_infoButtonOrAction;
};
/*!
* \brief Returns the layout manager holding the buttons.
*/
inline QHBoxLayout *ButtonOverlay::buttonLayout()
{
return m_buttonLayout;
}
/*!
* \brief Returns whether the clear button is enabled.
*/
inline bool ButtonOverlay::isClearButtonEnabled() const
{
return m_clearButton != nullptr;
}
/*!
* \brief Returns whether the info button is enabled.
*/
inline bool ButtonOverlay::isInfoButtonEnabled() const
{
return m_infoButton != nullptr;
}
} // namespace QtUtilities
#endif // WIDGETS_BUTTONOVERLAY_H

View File

@ -11,20 +11,22 @@ namespace QtUtilities {
* \brief A QComboBox with an embedded button for clearing its contents.
*/
/// \cond
static inline auto *getComboBoxLineEdit(QComboBox *comboBox)
{
comboBox->setEditable(true);
return comboBox->lineEdit();
}
/// \endcond
/*!
* \brief Constructs a clear combo box.
* \remarks The combo box is initialized to be editable and which must not be changed.
*/
ClearComboBox::ClearComboBox(QWidget *parent)
: QComboBox(parent)
, ButtonOverlay(this)
, ButtonOverlay(this, getComboBoxLineEdit(this))
{
const QStyle *const s = style();
QStyleOptionComboBox opt;
opt.initFrom(this);
setContentsMarginsFromEditFieldRectAndFrameWidth(
s->subControlRect(QStyle::CC_ComboBox, &opt, QStyle::SC_ComboBoxEditField, this), s->pixelMetric(QStyle::PM_ComboBoxFrameWidth, &opt, this));
setClearButtonEnabled(isEditable());
connect(this, &ClearComboBox::currentTextChanged, this, &ClearComboBox::handleTextChanged);
}
/*!
@ -47,6 +49,16 @@ void ClearComboBox::handleClearButtonClicked()
clearEditText();
}
void ClearComboBox::handleCustomLayoutCreated()
{
const QStyle *const s = style();
QStyleOptionComboBox opt;
opt.initFrom(this);
setContentsMarginsFromEditFieldRectAndFrameWidth(
s->subControlRect(QStyle::CC_ComboBox, &opt, QStyle::SC_ComboBoxEditField, this), s->pixelMetric(QStyle::PM_ComboBoxFrameWidth, &opt, this));
connect(this, &ClearComboBox::currentTextChanged, this, &ClearComboBox::handleTextChanged);
}
bool ClearComboBox::isCleared() const
{
return currentText().isEmpty();

View File

@ -19,6 +19,7 @@ public:
private Q_SLOTS:
void handleTextChanged(const QString &text);
void handleClearButtonClicked() override;
void handleCustomLayoutCreated() override;
};
} // namespace QtUtilities

View File

@ -15,15 +15,9 @@ namespace QtUtilities {
*/
ClearLineEdit::ClearLineEdit(QWidget *parent)
: QLineEdit(parent)
, ButtonOverlay(this)
, ButtonOverlay(this, this)
{
const QStyle *const s = style();
QStyleOptionFrame opt;
opt.initFrom(this);
setContentsMarginsFromEditFieldRectAndFrameWidth(s->subElementRect(QStyle::SE_LineEditContents, &opt, this),
s->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, m_widget), s->pixelMetric(QStyle::PM_LayoutVerticalSpacing, &opt, m_widget));
ButtonOverlay::setClearButtonEnabled(true);
connect(this, &ClearLineEdit::textChanged, this, &ClearLineEdit::handleTextChanged);
}
/*!
@ -46,6 +40,16 @@ void ClearLineEdit::handleClearButtonClicked()
clear();
}
void ClearLineEdit::handleCustomLayoutCreated()
{
const QStyle *const s = style();
QStyleOptionFrame opt;
opt.initFrom(this);
setContentsMarginsFromEditFieldRectAndFrameWidth(s->subElementRect(QStyle::SE_LineEditContents, &opt, this),
s->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, m_widget), s->pixelMetric(QStyle::PM_LayoutVerticalSpacing, &opt, m_widget));
connect(this, &ClearLineEdit::textChanged, this, &ClearLineEdit::handleTextChanged);
}
bool ClearLineEdit::isCleared() const
{
return text().isEmpty();

View File

@ -23,6 +23,7 @@ public:
private Q_SLOTS:
void handleTextChanged(const QString &text);
void handleClearButtonClicked() override;
void handleCustomLayoutCreated() override;
};
} // namespace QtUtilities

View File

@ -21,18 +21,8 @@ ClearPlainTextEdit::ClearPlainTextEdit(QWidget *parent)
: QPlainTextEdit(parent)
, ButtonOverlay(viewport())
{
// set alignment to show buttons in the bottom right corner
ButtonOverlay::buttonLayout()->setAlignment(Qt::AlignBottom | Qt::AlignRight);
const QStyle *const s = style();
QStyleOptionFrame opt;
opt.initFrom(this);
setContentsMarginsFromEditFieldRectAndFrameWidth(s->subElementRect(QStyle::SE_FrameContents, &opt, this),
s->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, m_widget), s->pixelMetric(QStyle::PM_LayoutVerticalSpacing, &opt, m_widget));
handleCustomLayoutCreated();
ButtonOverlay::setClearButtonEnabled(true);
connect(this, &QPlainTextEdit::textChanged, this, &ClearPlainTextEdit::handleTextChanged);
// ensure button layout is realigned when scrolling
connect(verticalScrollBar(), &QScrollBar::actionTriggered, this, &ClearPlainTextEdit::handleScroll);
connect(this, &QPlainTextEdit::cursorPositionChanged, this, &ClearPlainTextEdit::handleScroll);
}
/*!
@ -58,6 +48,21 @@ void ClearPlainTextEdit::handleClearButtonClicked()
cursor.removeSelectedText();
}
void ClearPlainTextEdit::handleCustomLayoutCreated()
{
// set alignment to show buttons in the bottom right corner
ButtonOverlay::buttonLayout()->setAlignment(Qt::AlignBottom | Qt::AlignRight);
const QStyle *const s = style();
QStyleOptionFrame opt;
opt.initFrom(this);
setContentsMarginsFromEditFieldRectAndFrameWidth(s->subElementRect(QStyle::SE_FrameContents, &opt, this),
s->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt, m_widget), s->pixelMetric(QStyle::PM_LayoutVerticalSpacing, &opt, m_widget));
connect(this, &QPlainTextEdit::textChanged, this, &ClearPlainTextEdit::handleTextChanged);
// ensure button layout is realigned when scrolling
connect(verticalScrollBar(), &QScrollBar::actionTriggered, this, &ClearPlainTextEdit::handleScroll);
connect(this, &QPlainTextEdit::cursorPositionChanged, this, &ClearPlainTextEdit::handleScroll);
}
void ClearPlainTextEdit::handleScroll()
{
buttonLayout()->update();

View File

@ -19,6 +19,7 @@ public:
private Q_SLOTS:
void handleTextChanged();
void handleClearButtonClicked() override;
void handleCustomLayoutCreated() override;
void handleScroll();
};

View File

@ -18,16 +18,10 @@ namespace QtUtilities {
*/
ClearSpinBox::ClearSpinBox(QWidget *parent)
: QSpinBox(parent)
, ButtonOverlay(this)
, ButtonOverlay(this, lineEdit())
, m_minimumHidden(false)
{
const QStyle *const s = style();
QStyleOptionSpinBox opt;
opt.initFrom(this);
setContentsMarginsFromEditFieldRectAndFrameWidth(
s->subControlRect(QStyle::CC_SpinBox, &opt, QStyle::SC_SpinBoxEditField, this), s->pixelMetric(QStyle::PM_SpinBoxFrameWidth, &opt, this));
setClearButtonEnabled(true);
connect(this, static_cast<void (ClearSpinBox::*)(int)>(&ClearSpinBox::valueChanged), this, &ClearSpinBox::handleValueChanged);
ButtonOverlay::setClearButtonEnabled(true);
}
/*!
@ -50,6 +44,16 @@ void ClearSpinBox::handleClearButtonClicked()
setValue(minimum());
}
void ClearSpinBox::handleCustomLayoutCreated()
{
const QStyle *const s = style();
QStyleOptionSpinBox opt;
opt.initFrom(this);
setContentsMarginsFromEditFieldRectAndFrameWidth(
s->subControlRect(QStyle::CC_SpinBox, &opt, QStyle::SC_SpinBoxEditField, this), s->pixelMetric(QStyle::PM_SpinBoxFrameWidth, &opt, this));
connect(this, static_cast<void (ClearSpinBox::*)(int)>(&ClearSpinBox::valueChanged), this, &ClearSpinBox::handleValueChanged);
}
bool ClearSpinBox::isCleared() const
{
return value() == minimum();

View File

@ -34,6 +34,7 @@ protected:
private Q_SLOTS:
void handleValueChanged(int value);
void handleClearButtonClicked() override;
void handleCustomLayoutCreated() override;
private:
bool m_minimumHidden;