refactor: buttons and friends (#6102)
This commit is contained in:
@@ -0,0 +1,392 @@
|
||||
#include "widgets/buttons/Button.hpp"
|
||||
|
||||
#include "singletons/Theme.hpp"
|
||||
#include "util/FunctionEventFilter.hpp"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDebug>
|
||||
#include <QPainter>
|
||||
#include <QScreen>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
Button::Button(BaseWidget *parent)
|
||||
: BaseWidget(parent)
|
||||
{
|
||||
connect(&effectTimer_, &QTimer::timeout, this,
|
||||
&Button::onMouseEffectTimeout);
|
||||
|
||||
this->effectTimer_.setInterval(20);
|
||||
this->effectTimer_.start();
|
||||
|
||||
this->setMouseTracking(true);
|
||||
}
|
||||
|
||||
bool Button::enabled() const noexcept
|
||||
{
|
||||
return this->enabled_;
|
||||
}
|
||||
|
||||
void Button::setEnabled(bool enabled)
|
||||
{
|
||||
this->enabled_ = enabled;
|
||||
|
||||
this->update();
|
||||
}
|
||||
|
||||
bool Button::mouseOver() const noexcept
|
||||
{
|
||||
return this->mouseOver_;
|
||||
}
|
||||
|
||||
bool Button::mouseDown() const noexcept
|
||||
{
|
||||
return this->mouseDown_;
|
||||
}
|
||||
|
||||
bool Button::menuVisible() const noexcept
|
||||
{
|
||||
return this->menuVisible_;
|
||||
}
|
||||
|
||||
QColor Button::borderColor() const noexcept
|
||||
{
|
||||
return this->borderColor_;
|
||||
}
|
||||
|
||||
void Button::setBorderColor(const QColor &color)
|
||||
{
|
||||
this->borderColor_ = color;
|
||||
|
||||
this->update();
|
||||
}
|
||||
|
||||
std::optional<QColor> Button::mouseEffectColor() const
|
||||
{
|
||||
return this->mouseEffectColor_;
|
||||
}
|
||||
|
||||
void Button::setMouseEffectColor(std::optional<QColor> color)
|
||||
{
|
||||
this->mouseEffectColor_ = color;
|
||||
}
|
||||
|
||||
QMenu *Button::menu() const
|
||||
{
|
||||
return this->menu_.get();
|
||||
}
|
||||
|
||||
void Button::setMenu(std::unique_ptr<QMenu> menu)
|
||||
{
|
||||
if (this->menu_)
|
||||
{
|
||||
this->menu_.release()->deleteLater();
|
||||
}
|
||||
|
||||
this->menu_ = std::move(menu);
|
||||
|
||||
this->menu_->installEventFilter(
|
||||
new FunctionEventFilter(this, [this](QObject *, QEvent *event) {
|
||||
if (event->type() == QEvent::Hide)
|
||||
{
|
||||
QTimer::singleShot(20, this, [this] {
|
||||
this->menuVisible_ = false;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
|
||||
void Button::paintEvent(QPaintEvent * /*event*/)
|
||||
{
|
||||
QPainter painter(this);
|
||||
this->paintButton(painter);
|
||||
}
|
||||
|
||||
void Button::invalidateContent()
|
||||
{
|
||||
this->pixmapValid_ = false;
|
||||
this->update();
|
||||
}
|
||||
|
||||
bool Button::contentCacheEnabled() const noexcept
|
||||
{
|
||||
return this->cachePixmap_;
|
||||
}
|
||||
|
||||
void Button::setContentCacheEnabled(bool enabled)
|
||||
{
|
||||
if (this->cachePixmap_ == enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
this->cachedPixmap_ = {};
|
||||
}
|
||||
this->cachePixmap_ = enabled;
|
||||
}
|
||||
|
||||
bool Button::opaqueContent() const noexcept
|
||||
{
|
||||
return this->opaqueContent_;
|
||||
}
|
||||
|
||||
void Button::setOpaqueContent(bool opaqueContent)
|
||||
{
|
||||
this->opaqueContent_ = opaqueContent;
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
void Button::enterEvent(QEnterEvent * /*event*/)
|
||||
#else
|
||||
void Button::enterEvent(QEvent * /*event*/)
|
||||
#endif
|
||||
{
|
||||
if (!this->mouseOver_)
|
||||
{
|
||||
this->mouseOver_ = true;
|
||||
this->update();
|
||||
}
|
||||
}
|
||||
|
||||
void Button::leaveEvent(QEvent * /*event*/)
|
||||
{
|
||||
if (this->mouseOver_)
|
||||
{
|
||||
this->mouseOver_ = false;
|
||||
this->update();
|
||||
}
|
||||
}
|
||||
|
||||
void Button::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (!this->enabled_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->button() != Qt::LeftButton)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->clickEffects_.emplace_back(event->pos());
|
||||
|
||||
this->mouseDown_ = true;
|
||||
|
||||
this->leftMousePress();
|
||||
|
||||
if (this->menu_ && !this->menuVisible_)
|
||||
{
|
||||
QTimer::singleShot(80, this, [this] {
|
||||
this->showMenu();
|
||||
});
|
||||
this->mouseDown_ = false;
|
||||
this->mouseOver_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Button::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (!this->enabled_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isInside = this->rect().contains(event->pos());
|
||||
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
this->mouseDown_ = false;
|
||||
|
||||
if (isInside)
|
||||
{
|
||||
leftClicked();
|
||||
}
|
||||
}
|
||||
|
||||
if (isInside)
|
||||
{
|
||||
clicked(event->button());
|
||||
}
|
||||
}
|
||||
|
||||
void Button::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (this->enabled_)
|
||||
{
|
||||
this->mousePos_ = event->pos();
|
||||
|
||||
this->update();
|
||||
}
|
||||
}
|
||||
|
||||
void Button::onMouseEffectTimeout()
|
||||
{
|
||||
bool performUpdate = false;
|
||||
|
||||
if (mouseOver_)
|
||||
{
|
||||
if (this->hoverMultiplier_ != 1)
|
||||
{
|
||||
this->hoverMultiplier_ =
|
||||
std::min(1.0, this->hoverMultiplier_ + 0.5);
|
||||
performUpdate = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this->hoverMultiplier_ != 0)
|
||||
{
|
||||
this->hoverMultiplier_ =
|
||||
std::max(0.0, this->hoverMultiplier_ - 0.3);
|
||||
performUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->clickEffects_.empty())
|
||||
{
|
||||
performUpdate = true;
|
||||
|
||||
for (auto it = this->clickEffects_.begin();
|
||||
it != this->clickEffects_.end();)
|
||||
{
|
||||
it->progress += mouseDown_ ? 0.02 : 0.07;
|
||||
|
||||
if (it->progress >= 1.0)
|
||||
{
|
||||
it = this->clickEffects_.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (performUpdate)
|
||||
{
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
void Button::showMenu()
|
||||
{
|
||||
if (!this->menu_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto menuSizeHint = this->menu_->sizeHint();
|
||||
auto point = this->mapToGlobal(
|
||||
QPoint(this->width() - menuSizeHint.width(), this->height()));
|
||||
|
||||
auto *screen = QApplication::screenAt(point);
|
||||
if (screen == nullptr)
|
||||
{
|
||||
screen = QApplication::primaryScreen();
|
||||
}
|
||||
auto bounds = screen->availableGeometry();
|
||||
|
||||
if (point.y() + menuSizeHint.height() > bounds.bottom())
|
||||
{
|
||||
// Menu doesn't fit going down, flip it to go up instead
|
||||
point.setY(point.y() - menuSizeHint.height() - this->height());
|
||||
}
|
||||
|
||||
this->menu_->popup(point);
|
||||
this->menuVisible_ = true;
|
||||
}
|
||||
|
||||
void Button::paintButton(QPainter &painter)
|
||||
{
|
||||
painter.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
|
||||
if (this->cachePixmap_)
|
||||
{
|
||||
if (this->cachedPixmap_.size() != this->size())
|
||||
{
|
||||
this->cachedPixmap_ = QPixmap(this->size());
|
||||
this->cachedPixmap_.setDevicePixelRatio(this->devicePixelRatio());
|
||||
this->pixmapValid_ = false;
|
||||
}
|
||||
|
||||
if (!this->pixmapValid_)
|
||||
{
|
||||
if (!this->opaqueContent_)
|
||||
{
|
||||
this->cachedPixmap_.fill(Qt::transparent);
|
||||
}
|
||||
|
||||
{
|
||||
QPainter pixmapPainter(&this->cachedPixmap_);
|
||||
this->paintContent(pixmapPainter);
|
||||
}
|
||||
|
||||
this->pixmapValid_ = true;
|
||||
}
|
||||
painter.drawPixmap(this->rect(), this->cachedPixmap_,
|
||||
{{}, this->cachedPixmap_.size()});
|
||||
}
|
||||
else
|
||||
{
|
||||
this->paintContent(painter);
|
||||
}
|
||||
|
||||
this->fancyPaint(painter);
|
||||
|
||||
if (this->borderColor_.isValid())
|
||||
{
|
||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||
painter.setPen(this->borderColor_);
|
||||
painter.drawRect(0, 0, this->width() - 1, this->height() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void Button::fancyPaint(QPainter &painter)
|
||||
{
|
||||
if (!this->enabled_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
QColor c;
|
||||
|
||||
if (this->mouseEffectColor_)
|
||||
{
|
||||
c = *this->mouseEffectColor_;
|
||||
}
|
||||
else
|
||||
{
|
||||
c = this->theme->isLightTheme() ? QColor(0, 0, 0)
|
||||
: QColor(255, 255, 255);
|
||||
}
|
||||
|
||||
if (this->hoverMultiplier_ > 0)
|
||||
{
|
||||
QRadialGradient gradient(QPointF(mousePos_),
|
||||
static_cast<qreal>(this->width()) / 2.0);
|
||||
|
||||
gradient.setColorAt(0, QColor(c.red(), c.green(), c.blue(),
|
||||
int(50 * this->hoverMultiplier_)));
|
||||
gradient.setColorAt(1, QColor(c.red(), c.green(), c.blue(),
|
||||
int(40 * this->hoverMultiplier_)));
|
||||
|
||||
painter.fillRect(this->rect(), gradient);
|
||||
}
|
||||
|
||||
for (auto effect : this->clickEffects_)
|
||||
{
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(QColor(c.red(), c.green(), c.blue(),
|
||||
int((1 - effect.progress) * 95)));
|
||||
painter.drawEllipse(QPointF(effect.position),
|
||||
effect.progress * qreal(width()) * 2,
|
||||
effect.progress * qreal(width()) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,211 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/BaseWidget.hpp"
|
||||
|
||||
#include <QMenu>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPoint>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
/// @brief A generic button with click and hover effects.
|
||||
///
|
||||
/// This button doesn't display anything - subclasses add content to the button.
|
||||
///
|
||||
/// To add content in a derived class, implement #paintContent(). This gets
|
||||
/// called before the click and hover effects are drawn.+
|
||||
///
|
||||
/// Subclasses can enable caching of the content drawn in #paintContent(), as
|
||||
/// the button often repaints due to mouse movement (and the hover effects).
|
||||
/// When updating the button, subclasses can invalidate the cache with
|
||||
/// #invalidateContent(). Alternatively, a subclass opt out of this by calling
|
||||
/// #setContentCacheEnabled().
|
||||
class Button : public BaseWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
struct ClickEffect {
|
||||
double progress = 0.0;
|
||||
QPoint position;
|
||||
|
||||
ClickEffect(QPoint _position)
|
||||
: position(_position)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
Button(BaseWidget *parent = nullptr);
|
||||
|
||||
/// @brief Returns true if the button is enabled
|
||||
///
|
||||
/// An enabled button will emit interaction events and show mouse effects.
|
||||
/// When disabled, only the content and border is shown.
|
||||
///
|
||||
/// By default, a button is enabled.
|
||||
[[nodiscard]] bool enabled() const noexcept;
|
||||
|
||||
/// Setter for #enabled()
|
||||
void setEnabled(bool enabled);
|
||||
|
||||
/// Returns true if the user is hovering over the button
|
||||
[[nodiscard]] bool mouseOver() const noexcept;
|
||||
|
||||
/// Returns true if the left mouse button is held down
|
||||
[[nodiscard]] bool mouseDown() const noexcept;
|
||||
|
||||
/// @brief Returns true if the menu is visible
|
||||
///
|
||||
/// @sa #menu(), #setMenu()
|
||||
[[nodiscard]] bool menuVisible() const noexcept;
|
||||
|
||||
/// @brief Returns the current border color
|
||||
///
|
||||
/// If no color is set, this will return an invalid color.
|
||||
///
|
||||
/// The border is shown with a width of 1 above all other content if the
|
||||
/// border color is valid.
|
||||
///
|
||||
/// By default, no border color is set.
|
||||
[[nodiscard]] QColor borderColor() const noexcept;
|
||||
|
||||
/// Setter for #borderColor()
|
||||
void setBorderColor(const QColor &color);
|
||||
|
||||
/// @brief Returns the current mouse effect color (if set)
|
||||
///
|
||||
/// The mouse effect color is used for hover and click effects.
|
||||
///
|
||||
/// By default, the color is based on the selected theme.
|
||||
[[nodiscard]] std::optional<QColor> mouseEffectColor() const;
|
||||
|
||||
/// Setter for #mouseEffectColor()
|
||||
void setMouseEffectColor(std::optional<QColor> color);
|
||||
|
||||
/// @brief Returns the menu associated with this button.
|
||||
///
|
||||
/// The menu is shown when pressing the left mouse button.
|
||||
///
|
||||
/// The return value is non-owned.
|
||||
/// If no menu is associated, `nullptr` is returned.
|
||||
///
|
||||
/// @sa #menuVisible(), #leftMousePress()
|
||||
[[nodiscard]] QMenu *menu() const;
|
||||
|
||||
/// Setter for #menu()
|
||||
void setMenu(std::unique_ptr<QMenu> menu);
|
||||
|
||||
Q_SIGNALS:
|
||||
/// @brief Emitted after the user left-clicked the button.
|
||||
///
|
||||
/// A click is only emitted if the user released the mouse above the button
|
||||
/// and the button is enabled.
|
||||
void leftClicked();
|
||||
|
||||
/// @brief Emitted after the user clicked the button with any mouse-button
|
||||
///
|
||||
/// A click is only emitted if the user released the mouse above the button
|
||||
/// and the button is enabled.
|
||||
///
|
||||
/// @sa #leftClicked()
|
||||
void clicked(Qt::MouseButton button);
|
||||
|
||||
/// @brief Emitted when the user presses the left mouse button.
|
||||
///
|
||||
/// Avoid using this event where possible and use #leftClicked().
|
||||
void leftMousePress();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent * /*event*/) override;
|
||||
|
||||
/// @brief Paint the contents to be shown below the button
|
||||
///
|
||||
/// First, the content is painted, then hover and click effects and finally
|
||||
/// the border.
|
||||
/// The content is cached by default as it's assumed to be expensive to
|
||||
/// repaint and repaints can occur frequently.
|
||||
///
|
||||
/// @sa #contentCacheEnabled()
|
||||
virtual void paintContent(QPainter &painter) = 0;
|
||||
|
||||
/// @brief Paint hover and click effects.
|
||||
///
|
||||
/// This is provided for custom setups that override #paintEvent().
|
||||
/// When possible, prefer #paintContent().
|
||||
void fancyPaint(QPainter &painter);
|
||||
|
||||
/// @brief Indicate that the cache is invalid and needs to be repainted.
|
||||
///
|
||||
/// This calls update().
|
||||
void invalidateContent();
|
||||
|
||||
/// @brief Returns true if the content drawn in #paintContent() should be
|
||||
/// cached.
|
||||
///
|
||||
/// For complex static content, this makes sense, as the button is often
|
||||
/// repainted when the user is hovering over it.
|
||||
///
|
||||
/// By default, the cache is disabled.
|
||||
[[nodiscard]] bool contentCacheEnabled() const noexcept;
|
||||
|
||||
/// @brief Setter for #contentCacheEnabled()
|
||||
///
|
||||
/// When disabling the content-cache, the cached pixmap is destroyed.
|
||||
void setContentCacheEnabled(bool enabled);
|
||||
|
||||
/// @brief Returns true if the content drawn in #paintContent() is fully
|
||||
/// opaque.
|
||||
///
|
||||
/// If the content is fully opaque, the cached pixmap isn't filled with a
|
||||
/// transparent color, but left in the default state.
|
||||
///
|
||||
/// @sa #setOpaqueContent()
|
||||
[[nodiscard]] bool opaqueContent() const noexcept;
|
||||
|
||||
/// Setter for #opaqueContent()
|
||||
void setOpaqueContent(bool opaqueContent);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
void enterEvent(QEnterEvent * /*event*/) override;
|
||||
#else
|
||||
void enterEvent(QEvent * /*event*/) override;
|
||||
#endif
|
||||
void leaveEvent(QEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
void onMouseEffectTimeout();
|
||||
void showMenu();
|
||||
|
||||
void paintButton(QPainter &painter);
|
||||
|
||||
QColor borderColor_;
|
||||
|
||||
QPoint mousePos_;
|
||||
double hoverMultiplier_ = 0.0;
|
||||
|
||||
std::unique_ptr<QMenu> menu_;
|
||||
|
||||
QTimer effectTimer_;
|
||||
std::vector<ClickEffect> clickEffects_;
|
||||
std::optional<QColor> mouseEffectColor_;
|
||||
|
||||
bool enabled_ = true;
|
||||
bool mouseOver_ = false;
|
||||
bool mouseDown_ = false;
|
||||
bool menuVisible_ = false;
|
||||
|
||||
QPixmap cachedPixmap_;
|
||||
bool pixmapValid_ = false;
|
||||
bool cachePixmap_ = false;
|
||||
bool opaqueContent_ = false;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "widgets/buttons/DimButton.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
DimButton::DimButton(BaseWidget *parent)
|
||||
: Button(parent)
|
||||
{
|
||||
}
|
||||
|
||||
DimButton::Dim DimButton::dim() const noexcept
|
||||
{
|
||||
return this->dim_;
|
||||
}
|
||||
|
||||
void DimButton::setDim(Dim value)
|
||||
{
|
||||
this->dim_ = value;
|
||||
|
||||
this->invalidateContent();
|
||||
}
|
||||
|
||||
qreal DimButton::currentContentOpacity() const noexcept
|
||||
{
|
||||
if (this->dim_ == Dim::None || this->mouseOver())
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
if (this->dim_ == Dim::Some)
|
||||
{
|
||||
return 0.7;
|
||||
}
|
||||
|
||||
return 0.15;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/buttons/Button.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
/// @brief A button with a #dim() setting for controlling the opacity of its
|
||||
/// content.
|
||||
///
|
||||
/// This button doesn't paint anything.
|
||||
///
|
||||
/// @sa #currentContentOpacity()
|
||||
class DimButton : public Button
|
||||
{
|
||||
public:
|
||||
enum class Dim : std::uint8_t {
|
||||
/// Fully opaque (100% opcaity)
|
||||
None,
|
||||
/// Slightly transparent (70% opacity)
|
||||
Some,
|
||||
/// Almost transparent (15% opacity)
|
||||
Lots,
|
||||
};
|
||||
|
||||
DimButton(BaseWidget *parent = nullptr);
|
||||
|
||||
/// Returns the current dim level.
|
||||
[[nodiscard]] Dim dim() const noexcept;
|
||||
|
||||
/// Setter for #dim()
|
||||
void setDim(Dim value);
|
||||
|
||||
/// Returns the current opacity based on the current dim level.
|
||||
[[nodiscard]] qreal currentContentOpacity() const noexcept;
|
||||
|
||||
private:
|
||||
Dim dim_ = Dim::Some;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,71 @@
|
||||
#include "widgets/buttons/InitUpdateButton.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "widgets/buttons/PixmapButton.hpp"
|
||||
#include "widgets/dialogs/UpdateDialog.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void initUpdateButton(PixmapButton &button,
|
||||
pajlada::Signals::SignalHolder &signalHolder)
|
||||
{
|
||||
button.hide();
|
||||
|
||||
// show update prompt when clicking the button
|
||||
QObject::connect(&button, &Button::leftClicked, [&button] {
|
||||
auto *dialog = new UpdateDialog();
|
||||
|
||||
auto globalPoint = button.mapToGlobal(
|
||||
QPoint(int(-100 * button.scale()), button.height()));
|
||||
|
||||
// Make sure that update dialog will not go off left edge of screen
|
||||
if (globalPoint.x() < 0)
|
||||
{
|
||||
globalPoint.setX(0);
|
||||
}
|
||||
|
||||
dialog->moveTo(globalPoint, widgets::BoundsChecking::DesiredPosition);
|
||||
dialog->show();
|
||||
dialog->raise();
|
||||
|
||||
// We can safely ignore the signal connection because the dialog will always
|
||||
// be destroyed before the button is destroyed, since it is destroyed on focus loss
|
||||
//
|
||||
// The button is either attached to a Notebook, or a Window frame
|
||||
std::ignore = dialog->buttonClicked.connect([&button](auto buttonType) {
|
||||
switch (buttonType)
|
||||
{
|
||||
case UpdateDialog::Dismiss: {
|
||||
button.hide();
|
||||
}
|
||||
break;
|
||||
case UpdateDialog::Install: {
|
||||
getApp()->getUpdates().installUpdates();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// handle.reset(dialog);
|
||||
// dialog->closing.connect([&handle] { handle.release(); });
|
||||
});
|
||||
|
||||
// update image when state changes
|
||||
auto updateChange = [&button](auto) {
|
||||
button.setVisible(getApp()->getUpdates().shouldShowUpdateButton());
|
||||
|
||||
const auto *imageUrl = getApp()->getUpdates().isError()
|
||||
? ":/buttons/updateError.png"
|
||||
: ":/buttons/update.png";
|
||||
button.setPixmap(QPixmap(imageUrl));
|
||||
};
|
||||
|
||||
updateChange(getApp()->getUpdates().getStatus());
|
||||
|
||||
signalHolder.managedConnect(getApp()->getUpdates().statusUpdated,
|
||||
[updateChange](auto status) {
|
||||
updateChange(status);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
namespace pajlada::Signals {
|
||||
class SignalHolder;
|
||||
} // namespace pajlada::Signals
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class PixmapButton;
|
||||
class UpdateDialog;
|
||||
|
||||
void initUpdateButton(PixmapButton &button,
|
||||
pajlada::Signals::SignalHolder &signalHolder);
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "widgets/buttons/LabelButton.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
LabelButton::LabelButton(const QString &text, BaseWidget *parent, QSize padding)
|
||||
: Button(parent)
|
||||
, layout_(this)
|
||||
, label_(text)
|
||||
, padding_(padding)
|
||||
{
|
||||
this->layout_.setContentsMargins(0, 0, 0, 0);
|
||||
this->layout_.addWidget(&this->label_);
|
||||
this->label_.setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
this->label_.setAlignment(Qt::AlignCenter);
|
||||
|
||||
this->updatePadding();
|
||||
}
|
||||
|
||||
void LabelButton::setText(const QString &text)
|
||||
{
|
||||
this->label_.setText(text);
|
||||
}
|
||||
|
||||
QSize LabelButton::padding() const noexcept
|
||||
{
|
||||
return this->padding_;
|
||||
}
|
||||
|
||||
void LabelButton::setPadding(QSize padding)
|
||||
{
|
||||
if (this->padding_ == padding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->padding_ = padding;
|
||||
this->updatePadding();
|
||||
}
|
||||
|
||||
void LabelButton::enableRichText()
|
||||
{
|
||||
this->label_.setTextFormat(Qt::RichText);
|
||||
}
|
||||
|
||||
void LabelButton::paintContent(QPainter &painter)
|
||||
{
|
||||
}
|
||||
|
||||
void LabelButton::updatePadding()
|
||||
{
|
||||
auto x = this->padding_.width();
|
||||
auto y = this->padding_.height();
|
||||
this->label_.setContentsMargins(x, y, x, y);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/buttons/Button.hpp"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
/// @brief A button with a label on top.
|
||||
///
|
||||
/// The content can be set with #setText(), #setPixmap(), and #setMovie().
|
||||
class LabelButton : public Button
|
||||
{
|
||||
public:
|
||||
LabelButton(const QString &text = {}, BaseWidget *parent = nullptr,
|
||||
QSize padding = {6, 0});
|
||||
|
||||
/// @brief Returns the current text
|
||||
///
|
||||
/// If no text has been set this will return an empty string.
|
||||
[[nodiscard]] QString text() const;
|
||||
|
||||
/// @brief Sets the label's text to @a text.
|
||||
///
|
||||
/// Any previous content is cleared.
|
||||
/// Supports Qt's HTML subset.
|
||||
void setText(const QString &text);
|
||||
|
||||
/// @brief Returns the padding inside the button.
|
||||
///
|
||||
/// `width` is the padding applied horizontally (left and right).
|
||||
/// `height` is the padding applied vertically (top and bottom).
|
||||
///
|
||||
/// By default, the button has no vertical padding and a horizontal padding of 6.
|
||||
[[nodiscard]] QSize padding() const noexcept;
|
||||
|
||||
/// Setter for #padding()
|
||||
void setPadding(QSize padding);
|
||||
|
||||
/// Sets the label to display rich text (Qt's HTML subset)
|
||||
void enableRichText();
|
||||
|
||||
protected:
|
||||
void paintContent(QPainter &painter) override;
|
||||
|
||||
private:
|
||||
void updatePadding();
|
||||
|
||||
QHBoxLayout layout_;
|
||||
QLabel label_;
|
||||
QSize padding_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,215 @@
|
||||
#include "widgets/buttons/NotebookButton.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "singletons/Theme.hpp"
|
||||
#include "widgets/Notebook.hpp"
|
||||
#include "widgets/splits/DraggedSplit.hpp"
|
||||
#include "widgets/splits/Split.hpp"
|
||||
#include "widgets/splits/SplitContainer.hpp"
|
||||
|
||||
#include <QMimeData>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QRadialGradient>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
NotebookButton::NotebookButton(Notebook *parent)
|
||||
: Button(parent)
|
||||
, parent_(parent)
|
||||
{
|
||||
this->setAcceptDrops(true);
|
||||
}
|
||||
|
||||
void NotebookButton::setIcon(Icon icon)
|
||||
{
|
||||
this->icon_ = icon;
|
||||
|
||||
this->update();
|
||||
}
|
||||
|
||||
NotebookButton::Icon NotebookButton::getIcon() const
|
||||
{
|
||||
return this->icon_;
|
||||
}
|
||||
|
||||
void NotebookButton::themeChangedEvent()
|
||||
{
|
||||
this->setMouseEffectColor(this->theme->tabs.regular.text);
|
||||
}
|
||||
|
||||
void NotebookButton::paintContent(QPainter &painter)
|
||||
{
|
||||
QColor background;
|
||||
QColor foreground;
|
||||
|
||||
if (this->mouseDown() || this->mouseOver())
|
||||
{
|
||||
background = this->theme->tabs.regular.backgrounds.hover;
|
||||
foreground = this->theme->tabs.regular.text;
|
||||
}
|
||||
else
|
||||
{
|
||||
background = this->theme->tabs.regular.backgrounds.regular;
|
||||
foreground = this->theme->tabs.regular.text;
|
||||
}
|
||||
|
||||
painter.setPen(Qt::NoPen);
|
||||
|
||||
float h = height(), w = width();
|
||||
|
||||
switch (icon_)
|
||||
{
|
||||
case Plus: {
|
||||
painter.setPen([&] {
|
||||
QColor tmp = foreground;
|
||||
if (isDraggingSplit())
|
||||
{
|
||||
tmp = this->theme->tabs.selected.line.regular;
|
||||
}
|
||||
else if (!this->mouseOver())
|
||||
{
|
||||
tmp.setAlpha(180);
|
||||
}
|
||||
return tmp;
|
||||
}());
|
||||
QRect rect = this->rect();
|
||||
int s = h * 4 / 9;
|
||||
|
||||
painter.drawLine(rect.left() + rect.width() / 2 - (s / 2),
|
||||
rect.top() + rect.height() / 2,
|
||||
rect.left() + rect.width() / 2 + (s / 2),
|
||||
rect.top() + rect.height() / 2);
|
||||
painter.drawLine(rect.left() + rect.width() / 2,
|
||||
rect.top() + rect.height() / 2 - (s / 2),
|
||||
rect.left() + rect.width() / 2,
|
||||
rect.top() + rect.height() / 2 + (s / 2));
|
||||
}
|
||||
break;
|
||||
|
||||
case User: {
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
auto a = w / 8;
|
||||
QPainterPath path;
|
||||
|
||||
path.arcMoveTo(a, 4 * a, 6 * a, 6 * a, 0);
|
||||
path.arcTo(a, 4 * a, 6 * a, 6 * a, 0, 180);
|
||||
|
||||
QPainterPath remove;
|
||||
remove.addEllipse(2 * a, 1 * a, 4 * a, 4 * a);
|
||||
path = path.subtracted(remove);
|
||||
|
||||
path.addEllipse(2.5 * a, 1.5 * a, 3 * a, 3 * a);
|
||||
|
||||
painter.fillPath(path, foreground);
|
||||
}
|
||||
break;
|
||||
|
||||
case Settings: {
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
auto a = w / 8;
|
||||
QPainterPath path;
|
||||
|
||||
path.arcMoveTo(a, a, 6 * a, 6 * a, 0 - (360 / 32.0));
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
path.arcTo(a, a, 6 * a, 6 * a, i * (360 / 8.0) - (360 / 32.0),
|
||||
(360 / 32.0));
|
||||
path.arcTo(2 * a, 2 * a, 4 * a, 4 * a,
|
||||
i * (360 / 8.0) + (360 / 32.0), (360 / 32.0));
|
||||
}
|
||||
|
||||
QPainterPath remove;
|
||||
remove.addEllipse(3 * a, 3 * a, 2 * a, 2 * a);
|
||||
|
||||
painter.fillPath(path.subtracted(remove), foreground);
|
||||
}
|
||||
break;
|
||||
|
||||
default:;
|
||||
}
|
||||
}
|
||||
|
||||
void NotebookButton::dragEnterEvent(QDragEnterEvent *event)
|
||||
{
|
||||
if (!event->mimeData()->hasFormat("chatterino/split"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
event->acceptProposedAction();
|
||||
|
||||
auto *e =
|
||||
new QMouseEvent(QMouseEvent::MouseButtonPress,
|
||||
QPointF(this->width() / 2, this->height() / 2),
|
||||
QCursor::pos(), Qt::LeftButton, Qt::LeftButton, {});
|
||||
Button::mousePressEvent(e);
|
||||
delete e;
|
||||
}
|
||||
|
||||
void NotebookButton::dragLeaveEvent(QDragLeaveEvent *)
|
||||
{
|
||||
this->update();
|
||||
|
||||
auto *e =
|
||||
new QMouseEvent(QMouseEvent::MouseButtonRelease,
|
||||
QPointF(this->width() / 2, this->height() / 2),
|
||||
QCursor::pos(), Qt::LeftButton, Qt::LeftButton, {});
|
||||
Button::mouseReleaseEvent(e);
|
||||
delete e;
|
||||
}
|
||||
|
||||
void NotebookButton::dropEvent(QDropEvent *event)
|
||||
{
|
||||
auto *draggedSplit = dynamic_cast<Split *>(event->source());
|
||||
if (!draggedSplit)
|
||||
{
|
||||
qCDebug(chatterinoWidget)
|
||||
<< "Dropped something that wasn't a split onto a notebook button";
|
||||
return;
|
||||
}
|
||||
|
||||
auto *notebook = dynamic_cast<Notebook *>(this->parentWidget());
|
||||
if (!notebook)
|
||||
{
|
||||
qCDebug(chatterinoWidget) << "Dropped a split onto a notebook button "
|
||||
"without a parent notebook";
|
||||
return;
|
||||
}
|
||||
|
||||
event->acceptProposedAction();
|
||||
|
||||
auto *page = new SplitContainer(notebook);
|
||||
auto *tab = notebook->addPage(page);
|
||||
page->setTab(tab);
|
||||
|
||||
draggedSplit->setParent(page);
|
||||
page->insertSplit(draggedSplit);
|
||||
}
|
||||
|
||||
void NotebookButton::hideEvent(QHideEvent *)
|
||||
{
|
||||
if (isAppAboutToQuit())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->parent_->refresh();
|
||||
}
|
||||
|
||||
void NotebookButton::showEvent(QShowEvent *)
|
||||
{
|
||||
if (isAppAboutToQuit())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->parent_->refresh();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/buttons/Button.hpp"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Notebook;
|
||||
|
||||
class NotebookButton : public Button
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Icon { None, Plus, User, Settings };
|
||||
|
||||
explicit NotebookButton(Notebook *parent);
|
||||
|
||||
void setIcon(Icon icon);
|
||||
Icon getIcon() const;
|
||||
|
||||
protected:
|
||||
void paintContent(QPainter &painter) override;
|
||||
|
||||
void themeChangedEvent() override;
|
||||
void dragEnterEvent(QDragEnterEvent *) override;
|
||||
void dragLeaveEvent(QDragLeaveEvent *) override;
|
||||
void dropEvent(QDropEvent *) override;
|
||||
|
||||
void hideEvent(QHideEvent *) override;
|
||||
void showEvent(QShowEvent *) override;
|
||||
|
||||
private:
|
||||
Notebook *parent_ = nullptr;
|
||||
QPoint mousePos_;
|
||||
Icon icon_ = None;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "widgets/buttons/PixmapButton.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Resizes a pixmap to a desired size.
|
||||
* Does nothing if the target pixmap is already sized correctly.
|
||||
*
|
||||
* @param target The target pixmap.
|
||||
* @param source The unscaled pixmap.
|
||||
* @param size The desired device independent size.
|
||||
* @param dpr The device pixel ratio of the target area. The size of the target in pixels will be `size * dpr`.
|
||||
*/
|
||||
void resizePixmap(QPixmap &target, const QPixmap &source, const QSize &size,
|
||||
qreal dpr)
|
||||
{
|
||||
if (target.deviceIndependentSize() == size)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QPixmap resized = source;
|
||||
resized.setDevicePixelRatio(dpr);
|
||||
target = resized.scaled(size * dpr, Qt::IgnoreAspectRatio,
|
||||
Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
PixmapButton::PixmapButton(BaseWidget *parent)
|
||||
: DimButton(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QPixmap PixmapButton::pixmap() const
|
||||
{
|
||||
return this->pixmap_;
|
||||
}
|
||||
|
||||
void PixmapButton::setPixmap(const QPixmap &pixmap)
|
||||
{
|
||||
// Avoid updates if the pixmap didn't change
|
||||
if (pixmap.cacheKey() == this->pixmap_.cacheKey())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->pixmap_ = pixmap;
|
||||
this->resizedPixmap_ = {};
|
||||
this->update();
|
||||
}
|
||||
|
||||
bool PixmapButton::marginEnabled() const noexcept
|
||||
{
|
||||
return this->marginEnabled_;
|
||||
}
|
||||
|
||||
void PixmapButton::setMarginEnabled(bool enableMargin)
|
||||
{
|
||||
this->marginEnabled_ = enableMargin;
|
||||
this->update();
|
||||
}
|
||||
|
||||
void PixmapButton::paintContent(QPainter &painter)
|
||||
{
|
||||
if (this->pixmap_.isNull())
|
||||
{
|
||||
return;
|
||||
}
|
||||
painter.setOpacity(this->currentContentOpacity());
|
||||
|
||||
QRect rect = this->rect();
|
||||
|
||||
int shift = 0;
|
||||
if (this->marginEnabled_)
|
||||
{
|
||||
int margin =
|
||||
this->height() < static_cast<int>(22 * this->scale()) ? 3 : 6;
|
||||
|
||||
shift = static_cast<int>(static_cast<float>(margin) * this->scale());
|
||||
}
|
||||
|
||||
rect.moveLeft(shift);
|
||||
rect.setRight(rect.right() - (2 * shift));
|
||||
rect.moveTop(shift);
|
||||
rect.setBottom(rect.bottom() - (2 * shift));
|
||||
|
||||
resizePixmap(this->resizedPixmap_, this->pixmap_, rect.size(),
|
||||
this->devicePixelRatio());
|
||||
|
||||
painter.drawPixmap(rect, this->resizedPixmap_);
|
||||
|
||||
painter.setOpacity(1);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/buttons/DimButton.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
/// @brief A (dimmable) button displaying a pixmap.
|
||||
///
|
||||
/// The pixmap is scaled to fit the button, ignoring any aspect ratio.
|
||||
///
|
||||
/// By default, a margin is added to the button (@see #marginEnabled()).
|
||||
///
|
||||
/// When possible, prefer using a `SvgButton` as it can scale its contents
|
||||
/// to any size without blur and supports scaling and themeing.
|
||||
///
|
||||
/// @sa #setPixmap()
|
||||
class PixmapButton : public DimButton
|
||||
{
|
||||
public:
|
||||
PixmapButton(BaseWidget *parent = nullptr);
|
||||
|
||||
/// Returns the current, non-scaled pixmap
|
||||
[[nodiscard]] QPixmap pixmap() const;
|
||||
|
||||
/// Setter for #pixmap()
|
||||
void setPixmap(const QPixmap &pixmap);
|
||||
|
||||
/// @brief Returns true if this button has margin.
|
||||
///
|
||||
/// The margin is dynamically calculated based on the UI-scale
|
||||
/// and the size of the pixmap.
|
||||
[[nodiscard]] bool marginEnabled() const noexcept;
|
||||
|
||||
/// Setter for #marginEnabled()
|
||||
void setMarginEnabled(bool enableMargin);
|
||||
|
||||
protected:
|
||||
void paintContent(QPainter &painter) override;
|
||||
|
||||
private:
|
||||
QPixmap pixmap_;
|
||||
QPixmap resizedPixmap_;
|
||||
bool marginEnabled_ = true;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "widgets/buttons/SignalLabel.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
SignalLabel::SignalLabel(QWidget *parent, Qt::WindowFlags f)
|
||||
: QLabel(parent, f)
|
||||
{
|
||||
}
|
||||
|
||||
void SignalLabel::mouseDoubleClickEvent(QMouseEvent *ev)
|
||||
{
|
||||
this->mouseDoubleClick(ev);
|
||||
}
|
||||
|
||||
void SignalLabel::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
leftMouseDown();
|
||||
}
|
||||
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void SignalLabel::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
leftMouseUp();
|
||||
}
|
||||
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void SignalLabel::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
this->mouseMove(event);
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFlags>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QWidget>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class SignalLabel : public QLabel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SignalLabel(QWidget *parent = nullptr, Qt::WindowFlags f = {});
|
||||
~SignalLabel() override = default;
|
||||
|
||||
Q_SIGNALS:
|
||||
void mouseDoubleClick(QMouseEvent *ev);
|
||||
|
||||
void leftMouseDown();
|
||||
void leftMouseUp();
|
||||
void mouseMove(QMouseEvent *event);
|
||||
|
||||
protected:
|
||||
void mouseDoubleClickEvent(QMouseEvent *ev) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "widgets/buttons/SvgButton.hpp"
|
||||
|
||||
#include "singletons/Theme.hpp"
|
||||
|
||||
#include <QSvgRenderer>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
SvgButton::SvgButton(Src source, BaseWidget *parent, QSize padding)
|
||||
: Button(parent)
|
||||
, source_(std::move(source))
|
||||
, svg_(new QSvgRenderer(this->currentSvgPath(), this))
|
||||
, padding_(padding)
|
||||
{
|
||||
this->svg_->setAspectRatioMode(Qt::KeepAspectRatio);
|
||||
this->setContentCacheEnabled(true);
|
||||
}
|
||||
|
||||
void SvgButton::setSource(Src source)
|
||||
{
|
||||
this->source_ = std::move(source);
|
||||
this->svg_->load(this->currentSvgPath());
|
||||
this->invalidateContent();
|
||||
}
|
||||
|
||||
void SvgButton::setPadding(QSize padding)
|
||||
{
|
||||
if (this->padding_ == padding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->padding_ = padding;
|
||||
this->invalidateContent();
|
||||
}
|
||||
|
||||
void SvgButton::themeChangedEvent()
|
||||
{
|
||||
Button::themeChangedEvent();
|
||||
|
||||
if (this->source_.dark == this->source_.light)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->svg_->load(this->currentSvgPath());
|
||||
this->invalidateContent();
|
||||
}
|
||||
|
||||
void SvgButton::scaleChangedEvent(float scale)
|
||||
{
|
||||
Button::scaleChangedEvent(scale);
|
||||
|
||||
this->invalidateContent();
|
||||
}
|
||||
|
||||
void SvgButton::resizeEvent(QResizeEvent *e)
|
||||
{
|
||||
Button::resizeEvent(e);
|
||||
|
||||
this->invalidateContent();
|
||||
}
|
||||
|
||||
void SvgButton::paintContent(QPainter &painter)
|
||||
{
|
||||
QSize actualPadding = this->scale() * this->padding_;
|
||||
QPoint topLeft{actualPadding.width(), actualPadding.height()};
|
||||
QSize contentSize = this->size() - 2 * actualPadding;
|
||||
this->svg_->render(&painter, {topLeft, contentSize});
|
||||
}
|
||||
|
||||
QString SvgButton::currentSvgPath() const
|
||||
{
|
||||
if (this->theme->isLightTheme())
|
||||
{
|
||||
return this->source_.light;
|
||||
}
|
||||
return this->source_.dark;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/buttons/Button.hpp"
|
||||
|
||||
#include <QString>
|
||||
|
||||
class QSvgRenderer;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
/// @brief A button displaying SVGs.
|
||||
///
|
||||
/// The source can be specified per theme (dark/light)
|
||||
/// and will get automatically updated when the theme changes.
|
||||
///
|
||||
/// To set the size, use #setInnerHeight(). The width is adjusted according
|
||||
/// to the aspect ratio of the SVG. If the button should scale with the UI,
|
||||
/// turn on auto-scaling (@see #setAutoScale()), which will scale the button
|
||||
/// and padding according to the current UI-scale (linearly).
|
||||
///
|
||||
/// The button has #padding() around the SVG.
|
||||
class SvgButton : public Button
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// Source configuration for the button
|
||||
struct Src {
|
||||
/// Source to load in a dark theme
|
||||
QString dark;
|
||||
/// Source to load in a light theme
|
||||
QString light;
|
||||
};
|
||||
|
||||
[[nodiscard]] SvgButton(Src source, BaseWidget *parent = nullptr,
|
||||
QSize padding = {6, 3});
|
||||
|
||||
/// Returns the current source configuration.
|
||||
[[nodiscard]] Src source() const;
|
||||
|
||||
/// Setter for #source()
|
||||
void setSource(Src source);
|
||||
|
||||
/// @brief Returns the padding inside the button.
|
||||
///
|
||||
/// `width` is the padding applied horizontally (left and right).
|
||||
/// `height` is the padding applied vertically (top and bottom).
|
||||
///
|
||||
/// By default, the button has a vertical padding of 3 and a horizontal padding of 6.
|
||||
[[nodiscard]] QSize padding() const;
|
||||
|
||||
/// Setter for #padding()
|
||||
void setPadding(QSize padding);
|
||||
|
||||
protected:
|
||||
void themeChangedEvent() override;
|
||||
void scaleChangedEvent(float scale) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
void paintContent(QPainter &painter) override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] QString currentSvgPath() const;
|
||||
|
||||
Src source_;
|
||||
QSvgRenderer *svg_;
|
||||
QSize padding_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "widgets/buttons/TitlebarButton.hpp"
|
||||
|
||||
#include "singletons/Theme.hpp"
|
||||
|
||||
#include <QPainterPath>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
TitleBarButton::TitleBarButton(TitleBarButtonStyle style)
|
||||
: DimButton(nullptr)
|
||||
, style_(style)
|
||||
{
|
||||
this->setContentCacheEnabled(true);
|
||||
}
|
||||
|
||||
TitleBarButtonStyle TitleBarButton::getButtonStyle() const
|
||||
{
|
||||
return this->style_;
|
||||
}
|
||||
|
||||
void TitleBarButton::setButtonStyle(TitleBarButtonStyle style)
|
||||
{
|
||||
this->style_ = style;
|
||||
this->invalidateContent();
|
||||
}
|
||||
|
||||
void TitleBarButton::themeChangedEvent()
|
||||
{
|
||||
this->invalidateContent();
|
||||
}
|
||||
|
||||
void TitleBarButton::paintContent(QPainter &painter)
|
||||
{
|
||||
painter.setOpacity(this->currentContentOpacity());
|
||||
|
||||
QColor color = this->theme->window.text;
|
||||
QColor background = this->theme->window.background;
|
||||
|
||||
int xD = this->height() / 3;
|
||||
int centerX = this->width() / 2;
|
||||
|
||||
painter.setRenderHint(QPainter::Antialiasing, false);
|
||||
|
||||
switch (this->style_)
|
||||
{
|
||||
case TitleBarButtonStyle::Minimize: {
|
||||
painter.fillRect(centerX - xD / 2, xD * 3 / 2, xD, 1, color);
|
||||
break;
|
||||
}
|
||||
case TitleBarButtonStyle::Maximize: {
|
||||
painter.setPen(color);
|
||||
painter.drawRect(centerX - xD / 2, xD, xD - 1, xD - 1);
|
||||
break;
|
||||
}
|
||||
case TitleBarButtonStyle::Unmaximize: {
|
||||
int xD2 = xD * 1 / 5;
|
||||
int xD3 = xD * 4 / 5;
|
||||
|
||||
painter.drawRect(centerX - xD / 2 + xD2, xD, xD3, xD3);
|
||||
painter.fillRect(centerX - xD / 2, xD + xD2, xD3, xD3,
|
||||
this->theme->window.background);
|
||||
painter.drawRect(centerX - xD / 2, xD + xD2, xD3, xD3);
|
||||
break;
|
||||
}
|
||||
case TitleBarButtonStyle::Close: {
|
||||
QRect rect(centerX - xD / 2, xD, xD - 1, xD - 1);
|
||||
painter.setPen(QPen(color, 1));
|
||||
|
||||
painter.drawLine(rect.topLeft(), rect.bottomRight());
|
||||
painter.drawLine(rect.topRight(), rect.bottomLeft());
|
||||
break;
|
||||
}
|
||||
case TitleBarButtonStyle::Settings: {
|
||||
painter.setPen(color);
|
||||
color = "#999";
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
painter.save();
|
||||
painter.translate(3, 3);
|
||||
|
||||
auto a = xD / 3;
|
||||
QPainterPath path;
|
||||
|
||||
path.arcMoveTo(a, a, 6 * a, 6 * a, 0 - (360 / 32.0));
|
||||
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
path.arcTo(a, a, 6 * a, 6 * a, i * (360 / 8.0) - (360 / 32.0),
|
||||
(360 / 32.0));
|
||||
path.arcTo(2 * a, 2 * a, 4 * a, 4 * a,
|
||||
i * (360 / 8.0) + (360 / 32.0), (360 / 32.0));
|
||||
}
|
||||
|
||||
painter.strokePath(path, color);
|
||||
painter.fillPath(path, color);
|
||||
|
||||
painter.setBrush(background);
|
||||
painter.drawEllipse(3 * a, 3 * a, 2 * a, 2 * a);
|
||||
painter.restore();
|
||||
break;
|
||||
}
|
||||
default:;
|
||||
}
|
||||
|
||||
painter.setOpacity(1.0);
|
||||
}
|
||||
|
||||
void TitleBarButton::ncEnter()
|
||||
{
|
||||
this->enterEvent(nullptr);
|
||||
this->update();
|
||||
}
|
||||
|
||||
void TitleBarButton::ncLeave()
|
||||
{
|
||||
this->leaveEvent(nullptr);
|
||||
this->update();
|
||||
}
|
||||
|
||||
void TitleBarButton::ncMove(QPoint at)
|
||||
{
|
||||
QMouseEvent evt(QMouseEvent::MouseMove, at, QCursor::pos(), Qt::NoButton,
|
||||
Qt::NoButton, Qt::NoModifier);
|
||||
this->mouseMoveEvent(&evt);
|
||||
}
|
||||
|
||||
void TitleBarButton::ncMousePress(QPoint at)
|
||||
{
|
||||
QMouseEvent evt(QMouseEvent::MouseButtonPress, at, QCursor::pos(),
|
||||
Qt::LeftButton, Qt::NoButton, Qt::NoModifier);
|
||||
this->mousePressEvent(&evt);
|
||||
this->update();
|
||||
}
|
||||
|
||||
void TitleBarButton::ncMouseRelease(QPoint at)
|
||||
{
|
||||
QMouseEvent evt(QMouseEvent::MouseButtonRelease, at, QCursor::pos(),
|
||||
Qt::LeftButton, Qt::NoButton, Qt::NoModifier);
|
||||
this->mouseReleaseEvent(&evt);
|
||||
this->update();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/buttons/DimButton.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
enum class TitleBarButtonStyle : std::uint8_t {
|
||||
None,
|
||||
Minimize,
|
||||
Maximize,
|
||||
Unmaximize,
|
||||
Close,
|
||||
Settings,
|
||||
};
|
||||
|
||||
class TitleBarButton : public DimButton
|
||||
{
|
||||
public:
|
||||
TitleBarButton(TitleBarButtonStyle style = {});
|
||||
|
||||
TitleBarButtonStyle getButtonStyle() const;
|
||||
void setButtonStyle(TitleBarButtonStyle style);
|
||||
|
||||
/// Simulate a `mouseEnter` event.
|
||||
void ncEnter();
|
||||
|
||||
/// Simulate a `mouseLeave` event.
|
||||
void ncLeave();
|
||||
|
||||
/// Simulate a `mouseMove` event.
|
||||
/// @param at a local position relative to this widget
|
||||
void ncMove(QPoint at);
|
||||
|
||||
/// Simulate a `mousePress` event with the left mouse button.
|
||||
/// @param at a local position relative to this widget
|
||||
void ncMousePress(QPoint at);
|
||||
|
||||
/// Simulate a `mouseRelease` event with the left mouse button.
|
||||
/// @param at a local position relative to this widget
|
||||
void ncMouseRelease(QPoint at);
|
||||
|
||||
protected:
|
||||
void themeChangedEvent() override;
|
||||
|
||||
void paintContent(QPainter &painter) override;
|
||||
|
||||
private:
|
||||
TitleBarButtonStyle style_{};
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "widgets/buttons/TitlebarButtons.hpp"
|
||||
|
||||
#ifdef USEWINSDK
|
||||
|
||||
# include "widgets/buttons/TitlebarButton.hpp"
|
||||
|
||||
# include <Windows.h>
|
||||
|
||||
# include <cassert>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
TitleBarButtons::TitleBarButtons(QWidget *window, TitleBarButton *minButton,
|
||||
TitleBarButton *maxButton,
|
||||
TitleBarButton *closeButton)
|
||||
: QObject(window)
|
||||
, window_(window)
|
||||
, minButton_(minButton)
|
||||
, maxButton_(maxButton)
|
||||
, closeButton_(closeButton)
|
||||
{
|
||||
}
|
||||
|
||||
void TitleBarButtons::hover(size_t ht, QPoint at)
|
||||
{
|
||||
TitleBarButton *hovered{};
|
||||
TitleBarButton *other1{};
|
||||
TitleBarButton *other2{};
|
||||
switch (ht)
|
||||
{
|
||||
case HTMAXBUTTON:
|
||||
hovered = this->maxButton_;
|
||||
other1 = this->minButton_;
|
||||
other2 = this->closeButton_;
|
||||
break;
|
||||
case HTMINBUTTON:
|
||||
hovered = this->minButton_;
|
||||
other1 = this->maxButton_;
|
||||
other2 = this->closeButton_;
|
||||
break;
|
||||
case HTCLOSE:
|
||||
hovered = this->closeButton_;
|
||||
other1 = this->minButton_;
|
||||
other2 = this->maxButton_;
|
||||
break;
|
||||
default:
|
||||
assert(false && "TitleBarButtons::hover precondition violated");
|
||||
return;
|
||||
}
|
||||
hovered->ncEnter();
|
||||
hovered->ncMove(hovered->mapFromGlobal(at));
|
||||
other1->ncLeave();
|
||||
other2->ncLeave();
|
||||
}
|
||||
|
||||
void TitleBarButtons::leave()
|
||||
{
|
||||
this->minButton_->ncLeave();
|
||||
this->maxButton_->ncLeave();
|
||||
this->closeButton_->ncLeave();
|
||||
}
|
||||
|
||||
void TitleBarButtons::mousePress(size_t ht, QPoint at)
|
||||
{
|
||||
auto *button = this->buttonForHt(ht);
|
||||
button->ncMousePress(button->mapFromGlobal(at));
|
||||
}
|
||||
|
||||
void TitleBarButtons::mouseRelease(size_t ht, QPoint at)
|
||||
{
|
||||
auto *button = this->buttonForHt(ht);
|
||||
button->ncMouseRelease(button->mapFromGlobal(at));
|
||||
}
|
||||
|
||||
void TitleBarButtons::updateMaxButton()
|
||||
{
|
||||
this->maxButton_->setButtonStyle(
|
||||
this->window_->windowState().testFlag(Qt::WindowMaximized)
|
||||
? TitleBarButtonStyle::Unmaximize
|
||||
: TitleBarButtonStyle::Maximize);
|
||||
}
|
||||
|
||||
void TitleBarButtons::setSmallSize()
|
||||
{
|
||||
this->minButton_->setScaleIndependentSize(30, 30);
|
||||
this->maxButton_->setScaleIndependentSize(30, 30);
|
||||
this->closeButton_->setScaleIndependentSize(30, 30);
|
||||
}
|
||||
|
||||
void TitleBarButtons::setRegularSize()
|
||||
{
|
||||
this->minButton_->setScaleIndependentSize(46, 30);
|
||||
this->maxButton_->setScaleIndependentSize(46, 30);
|
||||
this->closeButton_->setScaleIndependentSize(46, 30);
|
||||
}
|
||||
|
||||
TitleBarButton *TitleBarButtons::buttonForHt(size_t ht) const
|
||||
{
|
||||
switch (ht)
|
||||
{
|
||||
case HTMAXBUTTON:
|
||||
return this->maxButton_;
|
||||
case HTMINBUTTON:
|
||||
return this->minButton_;
|
||||
case HTCLOSE:
|
||||
return this->closeButton_;
|
||||
default:
|
||||
assert(false &&
|
||||
"TitleBarButtons::buttonForHt precondition violated");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
class QPoint;
|
||||
class QWidget;
|
||||
|
||||
#include <QObject>
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
#ifdef USEWINSDK
|
||||
|
||||
class TitleBarButton;
|
||||
class TitleBarButtons : QObject
|
||||
{
|
||||
public:
|
||||
/// The parent of this object is set to `window`.
|
||||
///
|
||||
/// All parameters must have a parent;
|
||||
/// they're not deleted in the destructor.
|
||||
TitleBarButtons(QWidget *window, TitleBarButton *minButton,
|
||||
TitleBarButton *maxButton, TitleBarButton *closeButton);
|
||||
|
||||
/// Hover over the button `ht` at the global position `at`.
|
||||
///
|
||||
/// @pre `ht` must be one of { HTMAXBUTTON, HTMINBUTTON, HTCLOSE }.
|
||||
/// @param ht The hovered button
|
||||
/// @param at The global position of the event
|
||||
void hover(size_t ht, QPoint at);
|
||||
|
||||
/// Leave all buttons - simulate `leaveEvent` for all buttons.
|
||||
void leave();
|
||||
|
||||
/// Press the left mouse over the button `ht` at the global position `at`.
|
||||
///
|
||||
/// @pre `ht` must be one of { HTMAXBUTTON, HTMINBUTTON, HTCLOSE }.
|
||||
/// @param ht The clicked button
|
||||
/// @param at The global position of the event
|
||||
void mousePress(size_t ht, QPoint at);
|
||||
|
||||
/// Release the left mouse button over the button `ht` at the
|
||||
/// global position `at`.
|
||||
///
|
||||
/// @pre `ht` must be one of { HTMAXBUTTON, HTMINBUTTON, HTCLOSE }.
|
||||
/// @param ht The clicked button
|
||||
/// @param at The global position of the event
|
||||
void mouseRelease(size_t ht, QPoint at);
|
||||
|
||||
/// Update the maximize/restore button to show the correct image
|
||||
/// according to the current window state.
|
||||
void updateMaxButton();
|
||||
|
||||
/// Set buttons to be narrow.
|
||||
void setSmallSize();
|
||||
/// Set buttons to be regular size.
|
||||
void setRegularSize();
|
||||
|
||||
private:
|
||||
/// @pre ht must be one of { HTMAXBUTTON, HTMINBUTTON, HTCLOSE }.
|
||||
TitleBarButton *buttonForHt(size_t ht) const;
|
||||
|
||||
QWidget *window_ = nullptr;
|
||||
TitleBarButton *minButton_ = nullptr;
|
||||
TitleBarButton *maxButton_ = nullptr;
|
||||
TitleBarButton *closeButton_ = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace chatterino
|
||||
Reference in New Issue
Block a user