refactor: buttons and friends (#6102)

This commit is contained in:
nerix
2025-05-25 11:17:06 +02:00
committed by GitHub
parent deed1061b5
commit 84c0b39fde
49 changed files with 1083 additions and 691 deletions
+392
View File
@@ -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
+211
View File
@@ -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
+36
View File
@@ -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
+40
View File
@@ -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
+71
View File
@@ -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
+15
View File
@@ -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
+56
View File
@@ -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
+55
View File
@@ -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
+215
View File
@@ -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
+40
View File
@@ -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
+98
View File
@@ -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
+46
View File
@@ -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
+41
View File
@@ -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
+32
View File
@@ -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
+80
View File
@@ -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
+70
View File
@@ -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
+143
View File
@@ -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
+51
View File
@@ -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
+116
View File
@@ -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
+70
View File
@@ -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