* Remove all existing actions when falling back to using a custom layout to prevent duplicating those buttons * Enable clear button of ClearComboBox by default for consistency with other widgets
422 lines
13 KiB
C++
422 lines
13 KiB
C++
#include "./buttonoverlay.h"
|
|
#include "./iconbutton.h"
|
|
|
|
#include <QAction>
|
|
#include <QComboBox>
|
|
#include <QCursor>
|
|
#include <QHBoxLayout>
|
|
#include <QIcon>
|
|
#include <QLineEdit>
|
|
#include <QStyle>
|
|
#include <QStyleOption>
|
|
#include <QToolTip>
|
|
#include <QWidget>
|
|
|
|
#include <functional>
|
|
|
|
namespace QtUtilities {
|
|
|
|
/*!
|
|
* \class ButtonOverlay
|
|
* \brief The ButtonOverlay class is used to display buttons on top of other widgets.
|
|
*
|
|
* 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 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(nullptr)
|
|
, m_buttonLayout(nullptr)
|
|
, m_clearButton(nullptr)
|
|
, m_infoButtonOrAction(nullptr)
|
|
{
|
|
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();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Destroys the button overlay.
|
|
*/
|
|
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 (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 (!clearButtonEnabled && enabled) {
|
|
// enable clear button
|
|
m_clearButton = new IconButton;
|
|
m_clearButton->setHidden(isCleared());
|
|
m_clearButton->setPixmap(QIcon::fromTheme(QStringLiteral("edit-clear")).pixmap(IconButton::defaultPixmapSize));
|
|
m_clearButton->setGeometry(QRect(QPoint(), IconButton::defaultPixmapSize));
|
|
m_clearButton->setToolTip(QObject::tr("Clear"));
|
|
QObject::connect(m_clearButton, &IconButton::clicked, std::bind(&ButtonOverlay::handleClearButtonClicked, this));
|
|
m_buttonLayout->addWidget(m_clearButton);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Shows an info button with the specified \a pixmap and \a infoText.
|
|
*
|
|
* If there is already an info button enabled, it gets replaced with the new
|
|
* button.
|
|
*
|
|
* \sa ButtonOverlay::disableInfoButton()
|
|
*/
|
|
void ButtonOverlay::enableInfoButton(const QPixmap &pixmap, const QString &infoText)
|
|
{
|
|
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(QRect(QPoint(), IconButton::defaultPixmapSize));
|
|
if (m_clearButton) {
|
|
m_buttonLayout->insertWidget(m_buttonLayout->count() - 2, infoButton);
|
|
} else {
|
|
m_buttonLayout->addWidget(infoButton);
|
|
}
|
|
}
|
|
infoButton->setPixmap(pixmap);
|
|
infoButton->setToolTip(infoText);
|
|
}
|
|
|
|
/*!
|
|
* \brief Hides an info button if one is shown.
|
|
* \sa ButtonOverlay::enableInfoButton()
|
|
*/
|
|
void ButtonOverlay::disableInfoButton()
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \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);
|
|
}
|
|
|
|
/*!
|
|
* \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);
|
|
}
|
|
|
|
/*!
|
|
* \brief Removes the specified custom \a button; does nothing if \a button has not been added.
|
|
*
|
|
* The ownership of widget remains the same as when it was added.
|
|
*/
|
|
void ButtonOverlay::removeCustomButton(QWidget *button)
|
|
{
|
|
if (isUsingCustomLayout()) {
|
|
m_buttonLayout->removeWidget(button);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Adds a custom \a action.
|
|
*/
|
|
void ButtonOverlay::addCustomAction(QAction *action)
|
|
{
|
|
if (auto *const le = lineEditForWidget()) {
|
|
le->addAction(action, QLineEdit::TrailingPosition);
|
|
} else {
|
|
addCustomButton(IconButton::fromAction(action, reinterpret_cast<std::uintptr_t>(this)));
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Inserts a custom \a action at the specified \a index.
|
|
*/
|
|
void ButtonOverlay::insertCustomAction(int index, QAction *action)
|
|
{
|
|
if (auto *const le = lineEditForWidget()) {
|
|
const auto actions = le->actions();
|
|
le->insertAction(index < actions.size() ? actions[index] : nullptr, action);
|
|
} else {
|
|
insertCustomButton(index, IconButton::fromAction(action, reinterpret_cast<std::uintptr_t>(this)));
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Removes the specified custom \a action; does nothing if \a action has not been added.
|
|
*/
|
|
void ButtonOverlay::removeCustomAction(QAction *action)
|
|
{
|
|
if (auto *const le = lineEditForWidget()) {
|
|
le->removeAction(action);
|
|
} else {
|
|
removeCustomButton(IconButton::fromAction(action, reinterpret_cast<std::uintptr_t>(this)));
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Updates the visibility of the clear button.
|
|
*
|
|
* This function is meant to be called when subclassing.
|
|
*/
|
|
void ButtonOverlay::updateClearButtonVisibility(bool visible)
|
|
{
|
|
if (m_clearButton) {
|
|
m_clearButton->setVisible(visible);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Clears the related widget.
|
|
*
|
|
* 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
|
|
auto clearButtonEnabled = false;
|
|
auto *iconAction = static_cast<QAction *>(m_infoButtonOrAction);
|
|
QPixmap infoPixmap;
|
|
QString infoText;
|
|
QList<QAction *> actions;
|
|
if (auto *const le = lineEditForWidget()) {
|
|
if ((clearButtonEnabled = le->isClearButtonEnabled())) {
|
|
setClearButtonEnabled(false);
|
|
}
|
|
if ((iconAction = static_cast<QAction *>(m_infoButtonOrAction))) {
|
|
const auto icon = iconAction->icon();
|
|
const auto sizes = icon.availableSizes();
|
|
infoPixmap = icon.pixmap(sizes.empty() ? IconButton::defaultPixmapSize : sizes.front());
|
|
infoText = iconAction->toolTip();
|
|
disableInfoButton();
|
|
}
|
|
actions = le->actions();
|
|
for (auto *const action : actions) {
|
|
le->removeAction(action);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
for (auto *const action : actions) {
|
|
addCustomAction(action);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \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.
|
|
*
|
|
* This method is meant to be implemented when subclassing.
|
|
*/
|
|
bool ButtonOverlay::isCleared() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/*!
|
|
* \brief Shows the info text using a tool tip.
|
|
*
|
|
* This method is called when the info button is clicked.
|
|
*
|
|
* \remarks
|
|
* This function avoids using QCursor::pos() because it is problematic to use under Wayland. For the action case it seems not
|
|
* possible to avoid it because the position of QLineEditIconButton used by QLineEdit is not exposed.
|
|
*/
|
|
void ButtonOverlay::showInfo()
|
|
{
|
|
if (auto const *const le = lineEditForWidget()) {
|
|
if (auto *const infoAction = static_cast<QAction *>(m_infoButtonOrAction)) {
|
|
const auto pos = QCursor::pos();
|
|
if (!pos.isNull()) {
|
|
QToolTip::showText(pos, infoAction->toolTip(), m_widget);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (auto *const infoButton = static_cast<IconButton *>(m_infoButtonOrAction)) {
|
|
QToolTip::showText(infoButton->mapToGlobal(infoButton->rect().center()), 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)
|
|
{
|
|
const auto margins = m_widget->contentsMargins();
|
|
const auto buttonWidth = m_widget->width() - editFieldRect.width();
|
|
buttonLayout()->setContentsMargins(margins.left() + frameWidth + padding, margins.top() + frameWidth,
|
|
margins.right() + frameWidth + padding + buttonWidth, margins.bottom() + frameWidth);
|
|
}
|
|
|
|
} // namespace QtUtilities
|