#include "./buttonoverlay.h" #include "./iconbutton.h" #include #include #include #include #include #include #include #include #include #include #include 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(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(m_infoButtonOrAction)) { le->removeAction(infoAction); m_infoButtonOrAction = nullptr; } return; } if (auto *infoButton = static_cast(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(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(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(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(m_infoButtonOrAction); QPixmap infoPixmap; QString infoText; QList actions; if (auto *const le = lineEditForWidget()) { if ((clearButtonEnabled = le->isClearButtonEnabled())) { setClearButtonEnabled(false); } if ((iconAction = static_cast(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(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(m_infoButtonOrAction)) { const auto pos = QCursor::pos(); if (!pos.isNull()) { QToolTip::showText(pos, infoAction->toolTip(), m_widget); } } return; } if (auto *const infoButton = static_cast(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