Add transparent overlay window (#4746)

This commit is contained in:
nerix
2024-10-06 12:54:24 +02:00
committed by GitHub
parent 9ba7ef324d
commit afa8067a20
40 changed files with 1464 additions and 190 deletions
+16
View File
@@ -1633,4 +1633,20 @@ void SplitNotebook::select(QWidget *page, bool focusPage)
this->Notebook::select(page, focusPage);
}
void SplitNotebook::forEachSplit(const std::function<void(Split *)> &cb)
{
for (const auto &item : this->items())
{
auto *page = dynamic_cast<SplitContainer *>(item.page);
if (!page)
{
continue;
}
for (auto *split : page->getSplits())
{
cb(split);
}
}
}
} // namespace chatterino
+3
View File
@@ -18,6 +18,7 @@ class UpdateDialog;
class NotebookButton;
class NotebookTab;
class SplitContainer;
class Split;
enum NotebookTabLocation { Top = 0, Left = 1, Right = 2, Bottom = 3 };
@@ -229,6 +230,8 @@ public:
void addNotebookActionsToMenu(QMenu *menu) override;
void forEachSplit(const std::function<void(Split *)> &cb);
/**
* Toggles between the "Show all tabs" and "Hide all tabs" tab visibility states
*/
+530
View File
@@ -0,0 +1,530 @@
#include "widgets/OverlayWindow.hpp"
#include "Application.hpp"
#include "common/FlagsEnum.hpp"
#include "common/Literals.hpp"
#include "controllers/hotkeys/HotkeyController.hpp"
#include "singletons/Settings.hpp"
#include "singletons/WindowManager.hpp"
#include "widgets/BaseWidget.hpp"
#include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/InvisibleSizeGrip.hpp"
#include "widgets/Scrollbar.hpp"
#include "widgets/splits/Split.hpp"
#include <QBoxLayout>
#include <QCursor>
#include <QGraphicsEffect>
#include <QGridLayout>
#include <QKeySequence>
#include <QSizeGrip>
#ifdef Q_OS_WIN
# include <Windows.h>
# include <windowsx.h>
// This definition can be used to test the move interaction for other platforms
// on Windows by commenting it out. In a final build, Windows must always use
// this, as it's much smoother.
# define OVERLAY_NATIVE_MOVE
#endif
namespace {
using namespace chatterino;
using namespace literals;
/// Progress the user has made in exploring the overlay
enum class Knowledge : std::int32_t { // NOLINT(performance-enum-size)
None = 0,
// User opened the overlay at least once
Activation = 1 << 0,
};
bool hasKnowledge(Knowledge knowledge)
{
FlagsEnum<Knowledge> current(static_cast<Knowledge>(
getSettings()->overlayKnowledgeLevel.getValue()));
return current.has(knowledge);
}
void acquireKnowledge(Knowledge knowledge)
{
FlagsEnum<Knowledge> current(static_cast<Knowledge>(
getSettings()->overlayKnowledgeLevel.getValue()));
current.set(knowledge);
getSettings()->overlayKnowledgeLevel =
static_cast<std::underlying_type_t<Knowledge>>(current.value());
}
/// Returns [seq?, toggleAllOverlays]
std::pair<QKeySequence, bool> toggleIntertiaShortcut()
{
auto seq = getApp()->getHotkeys()->getDisplaySequence(
HotkeyCategory::Split, u"toggleOverlayInertia"_s, {{u"this"_s}});
if (!seq.isEmpty())
{
return {seq, false};
}
seq = getApp()->getHotkeys()->getDisplaySequence(
HotkeyCategory::Split, u"toggleOverlayInertia"_s, {{u"thisOrAll"_s}});
if (!seq.isEmpty())
{
return {seq, false};
}
return {
getApp()->getHotkeys()->getDisplaySequence(HotkeyCategory::Split,
u"toggleOverlayInertia"_s),
true,
};
}
} // namespace
namespace chatterino {
using namespace std::chrono_literals;
OverlayWindow::OverlayWindow(IndirectChannel channel)
: QWidget(nullptr,
Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint)
#ifdef Q_OS_WIN
, sizeAllCursor_(::LoadCursor(nullptr, IDC_SIZEALL))
#endif
, channel_(std::move(channel))
, channelView_(nullptr)
, interaction_(this)
{
this->setAttribute(Qt::WA_DeleteOnClose);
this->setWindowTitle(u"Chatterino - Overlay"_s);
// QGridLayout is (ab)used to stack widgets and position them
auto *grid = new QGridLayout(this);
grid->addWidget(&this->channelView_, 0, 0);
this->interaction_.attach(grid);
#ifndef OVERLAY_NATIVE_MOVE
grid->addWidget(new InvisibleSizeGrip(this), 0, 0,
Qt::AlignBottom | Qt::AlignRight);
#endif
// the interaction overlay currently captures all events
this->interaction_.installEventFilter(this);
this->shortInteraction_.setInterval(750ms);
QObject::connect(&this->shortInteraction_, &QTimer::timeout, [this] {
this->endInteraction();
});
this->channelView_.installEventFilter(this);
this->channelView_.setChannel(this->channel_.get());
this->channelView_.setIsOverlay(true); // use overlay colors
this->channelView_.setAttribute(Qt::WA_TranslucentBackground);
this->holder_.managedConnect(this->channel_.getChannelChanged(), [this]() {
this->channelView_.setChannel(this->channel_.get());
});
this->channelView_.scrollbar()->setShowThumb(false);
this->setAutoFillBackground(false);
this->resize(300, 500);
this->move(QCursor::pos() - this->rect().center());
this->setContentsMargins(0, 0, 0, 0);
this->setAttribute(Qt::WA_TranslucentBackground);
auto *settings = getSettings();
settings->enableOverlayShadow.connect(
[this](bool value) {
if (value)
{
this->dropShadow_ = new QGraphicsDropShadowEffect;
this->channelView_.setGraphicsEffect(this->dropShadow_);
}
else
{
this->channelView_.setGraphicsEffect(nullptr);
this->dropShadow_ = nullptr; // deleted by setGraphicsEffect
}
this->applyTheme();
},
this->holder_);
settings->overlayBackgroundOpacity.connect(
[this] {
this->channelView_.updateColorTheme();
this->update();
},
this->holder_, false);
auto applyIt = [this](auto /*unused*/) {
this->applyTheme();
};
settings->overlayShadowOffsetX.connect(applyIt, this->holder_, false);
settings->overlayShadowOffsetY.connect(applyIt, this->holder_, false);
settings->overlayShadowOpacity.connect(applyIt, this->holder_, false);
settings->overlayShadowRadius.connect(applyIt, this->holder_, false);
settings->overlayShadowColor.connect(applyIt, this->holder_, false);
this->addShortcuts();
this->triggerFirstActivation();
}
OverlayWindow::~OverlayWindow()
{
#ifdef Q_OS_WIN
::DestroyCursor(this->sizeAllCursor_);
#endif
}
void OverlayWindow::applyTheme()
{
auto *settings = getSettings();
if (this->dropShadow_)
{
QColor shadowColor(settings->overlayShadowColor.getValue());
shadowColor.setAlpha(
std::clamp(settings->overlayShadowOpacity.getValue(), 0, 255));
this->dropShadow_->setColor(shadowColor);
this->dropShadow_->setOffset(settings->overlayShadowOffsetX,
settings->overlayShadowOffsetY);
this->dropShadow_->setBlurRadius(settings->overlayShadowRadius);
}
this->update();
}
bool OverlayWindow::eventFilter(QObject * /*object*/, QEvent *event)
{
#ifndef OVERLAY_NATIVE_MOVE
switch (event->type())
{
case QEvent::MouseButtonPress: {
auto *evt = dynamic_cast<QMouseEvent *>(event);
this->moving_ = true;
this->moveOrigin_ = evt->globalPos();
return true;
}
break;
case QEvent::MouseButtonRelease: {
if (this->moving_)
{
this->moving_ = false;
return true;
}
return false;
}
break;
case QEvent::MouseMove: {
auto *evt = dynamic_cast<QMouseEvent *>(event);
if (this->moving_)
{
auto newPos = evt->globalPos() - this->moveOrigin_;
this->move(newPos + this->pos());
this->moveOrigin_ = evt->globalPos();
return true;
}
if (this->interaction_.isInteracting())
{
this->setOverrideCursor(Qt::SizeAllCursor);
return true;
}
return false;
}
break;
default:
return false;
}
#else
(void)event;
return false;
#endif
}
void OverlayWindow::setOverrideCursor(const QCursor &cursor)
{
this->channelView_.setCursor(cursor);
this->setCursor(cursor);
}
bool OverlayWindow::isInert() const
{
return this->inert_;
}
void OverlayWindow::toggleInertia()
{
this->setInert(!this->inert_);
}
void OverlayWindow::enterEvent(EnterEvent * /*event*/)
{
#ifndef OVERLAY_NATIVE_MOVE
this->startInteraction();
#endif
}
void OverlayWindow::leaveEvent(QEvent * /*event*/)
{
#ifndef OVERLAY_NATIVE_MOVE
this->endInteraction();
#endif
}
#ifdef Q_OS_WIN
bool OverlayWindow::nativeEvent(const QByteArray &eventType, void *message,
NativeResult *result)
{
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
MSG *msg = reinterpret_cast<MSG *>(message);
bool returnValue = false;
switch (msg->message)
{
# ifdef OVERLAY_NATIVE_MOVE
case WM_NCHITTEST:
this->handleNCHITTEST(msg, result);
returnValue = true;
break;
case WM_MOUSEMOVE:
case WM_NCMOUSEMOVE:
this->startShortInteraction();
break;
case WM_ENTERSIZEMOVE:
this->startInteraction();
break;
case WM_EXITSIZEMOVE:
// wait a few seconds before hiding
this->startShortInteraction();
break;
case WM_SETCURSOR: {
// When the window can be moved, the size-all cursor should be
// shown. Qt doesn't provide an interface to do this, so this
// manually sets the cursor.
if (LOWORD(msg->lParam) == HTCAPTION)
{
::SetCursor(this->sizeAllCursor_);
*result = TRUE;
returnValue = true;
}
}
break;
# endif
default:
return QWidget::nativeEvent(eventType, message, result);
}
QWidget::nativeEvent(eventType, message, result);
return returnValue;
}
void OverlayWindow::handleNCHITTEST(MSG *msg, NativeResult *result)
{
// This implementation is similar to the one of BaseWindow, but has the
// following differences:
// - The window can always be resized (or: it can't be maximized)
// - The close button is advertised as HTCLIENT instead of HTCLOSE
// - There isn't any other client area (the entire window can be moved)
const LONG borderWidth = 8; // in device independent pixels
auto rect = this->rect();
POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)};
ScreenToClient(msg->hwnd, &p);
QPoint point(p.x, p.y);
point /= this->devicePixelRatio();
auto x = point.x();
auto y = point.y();
*result = 0;
// left border
if (x < rect.left() + borderWidth)
{
*result = HTLEFT;
}
// right border
if (x >= rect.right() - borderWidth)
{
*result = HTRIGHT;
}
// bottom border
if (y >= rect.bottom() - borderWidth)
{
*result = HTBOTTOM;
}
// top border
if (y < rect.top() + borderWidth)
{
*result = HTTOP;
}
// bottom left corner
if (x >= rect.left() && x < rect.left() + borderWidth &&
y < rect.bottom() && y >= rect.bottom() - borderWidth)
{
*result = HTBOTTOMLEFT;
}
// bottom right corner
if (x < rect.right() && x >= rect.right() - borderWidth &&
y < rect.bottom() && y >= rect.bottom() - borderWidth)
{
*result = HTBOTTOMRIGHT;
}
// top left corner
if (x >= rect.left() && x < rect.left() + borderWidth && y >= rect.top() &&
y < rect.top() + borderWidth)
{
*result = HTTOPLEFT;
}
// top right corner
if (x < rect.right() && x >= rect.right() - borderWidth &&
y >= rect.top() && y < rect.top() + borderWidth)
{
*result = HTTOPRIGHT;
}
if (*result == 0)
{
auto *closeButton = this->interaction_.closeButton();
if (closeButton->isVisible() && closeButton->geometry().contains(point))
{
*result = HTCLIENT;
}
else
{
*result = HTCAPTION;
}
}
}
#endif
void OverlayWindow::triggerFirstActivation()
{
if (hasKnowledge(Knowledge::Activation))
{
return;
}
acquireKnowledge(Knowledge::Activation);
auto welcomeText =
u"Hey! It looks like this is the first time you're using the overlay. "_s
"You can move the overlay by dragging it with your mouse. "
#ifdef OVERLAY_NATIVE_MOVE
"To resize the window, drag on any edge."
#else
"To resize the window, drag on the bottom right corner."
#endif
"<br><br>"
"By default, the overlay is interactive. ";
auto [actualShortcut, allOverlays] = toggleIntertiaShortcut();
if (actualShortcut.isEmpty())
{
welcomeText +=
u"To toggle the click-through mode, "
"add a hotkey for \"Toggle overlay click-through\" in the split "
"category to press while any Chatterino window is focused."_s;
}
else
{
welcomeText +=
u"To toggle the click-through mode, press %1 (customizable "_s
"in the settings) while any Chatterino window is focused.".arg(
actualShortcut.toString());
}
welcomeText += u"<br><br>"_s
"This is still an early version and some features are "
"missing. Please provide feedback <a "
"href=\"https://github.com/Chatterino/chatterino2/"
"discussions\">on GitHub</a>.";
auto *box =
new QMessageBox(QMessageBox::Information, u"Chatterino - Overlay"_s,
welcomeText, QMessageBox::Ok, this);
box->open();
}
void OverlayWindow::addShortcuts()
{
auto [seq, allOverlays] = toggleIntertiaShortcut();
if (seq.isEmpty())
{
return;
}
auto *shortcut = new QShortcut(seq, this);
if (allOverlays)
{
QObject::connect(shortcut, &QShortcut::activated, this, [] {
getApp()->getWindows()->toggleAllOverlayInertia();
});
}
else
{
QObject::connect(shortcut, &QShortcut::activated, this,
&OverlayWindow::toggleInertia);
}
}
void OverlayWindow::startInteraction()
{
if (this->inert_)
{
return;
}
this->interaction_.startInteraction();
this->shortInteraction_.stop();
}
void OverlayWindow::startShortInteraction()
{
if (this->inert_)
{
return;
}
this->interaction_.startInteraction();
this->shortInteraction_.start();
}
void OverlayWindow::endInteraction()
{
this->interaction_.endInteraction();
}
void OverlayWindow::setInert(bool inert)
{
if (this->inert_ == inert)
{
return;
}
this->inert_ = inert;
this->setWindowFlag(Qt::WindowTransparentForInput, inert);
if (this->isHidden())
{
this->show();
}
this->endInteraction();
if (inert)
{
if (this->channelView_.scrollbar()->isVisible())
{
this->channelView_.scrollbar()->scrollToBottom();
}
this->interaction_.hide();
}
else
{
this->interaction_.show();
}
}
} // namespace chatterino
+87
View File
@@ -0,0 +1,87 @@
#pragma once
#include "common/Channel.hpp"
#include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/OverlayInteraction.hpp"
#include <pajlada/signals/scoped-connection.hpp>
#include <pajlada/signals/signalholder.hpp>
#include <QTimer>
#include <QWidget>
#ifdef Q_OS_WIN
# include <QtGui/qwindowdefs_win.h>
#endif
class QGraphicsDropShadowEffect;
namespace chatterino {
class OverlayWindow : public QWidget
{
Q_OBJECT
public:
OverlayWindow(IndirectChannel channel);
~OverlayWindow() override;
OverlayWindow(const OverlayWindow &) = delete;
OverlayWindow(OverlayWindow &&) = delete;
OverlayWindow &operator=(const OverlayWindow &) = delete;
OverlayWindow &operator=(OverlayWindow &&) = delete;
void setOverrideCursor(const QCursor &cursor);
bool isInert() const;
void setInert(bool inert);
void toggleInertia();
protected:
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
using NativeResult = qintptr;
using EnterEvent = QEnterEvent;
#else
using NativeResult = long;
using EnterEvent = QEvent;
#endif
bool eventFilter(QObject *object, QEvent *event) override;
void enterEvent(EnterEvent *event) override;
void leaveEvent(QEvent *event) override;
#ifdef Q_OS_WIN
bool nativeEvent(const QByteArray &eventType, void *message,
NativeResult *result) override;
#endif
private:
void triggerFirstActivation();
void addShortcuts();
void startInteraction();
void startShortInteraction();
void endInteraction();
void applyTheme();
#ifdef Q_OS_WIN
void handleNCHITTEST(MSG *msg, NativeResult *result);
HCURSOR sizeAllCursor_;
#endif
IndirectChannel channel_;
pajlada::Signals::SignalHolder holder_;
ChannelView channelView_;
QGraphicsDropShadowEffect *dropShadow_;
bool inert_ = false;
bool moving_ = false;
QPoint moveOrigin_;
OverlayInteraction interaction_;
QTimer shortInteraction_;
};
} // namespace chatterino
+25 -11
View File
@@ -285,18 +285,21 @@ void Scrollbar::paintEvent(QPaintEvent * /*event*/)
bool enableElevatedMessageHighlights =
getSettings()->enableElevatedMessageHighlight;
this->thumbRect_.setX(xOffset);
if (this->showThumb_)
{
this->thumbRect_.setX(xOffset);
// mouse over thumb
if (this->mouseDownLocation_ == MouseLocation::InsideThumb)
{
painter.fillRect(this->thumbRect_,
this->theme->scrollbars.thumbSelected);
}
// mouse not over thumb
else
{
painter.fillRect(this->thumbRect_, this->theme->scrollbars.thumb);
// mouse over thumb
if (this->mouseDownLocation_ == MouseLocation::InsideThumb)
{
painter.fillRect(this->thumbRect_,
this->theme->scrollbars.thumbSelected);
}
// mouse not over thumb
else
{
painter.fillRect(this->thumbRect_, this->theme->scrollbars.thumb);
}
}
// draw highlights
@@ -449,6 +452,17 @@ void Scrollbar::updateScroll()
this->update();
}
void Scrollbar::setShowThumb(bool showThumb)
{
if (this->showThumb_ == showThumb)
{
return;
}
this->showThumb_ = showThumb;
this->update();
}
Scrollbar::MouseLocation Scrollbar::locationOfMouseEvent(
QMouseEvent *event) const
{
+3
View File
@@ -127,6 +127,8 @@ public:
/// unaffected by simultaneous shifts of minimum and maximum.
qreal getRelativeCurrentValue() const;
void setShowThumb(bool showthumb);
// offset the desired value without breaking smooth scolling
void offset(qreal value);
pajlada::Signals::NoArgSignal &getCurrentValueChanged();
@@ -169,6 +171,7 @@ private:
boost::circular_buffer<ScrollbarHighlight> highlights_;
bool atBottom_{false};
bool showThumb_ = true;
MouseLocation mouseOverLocation_ = MouseLocation::Outside;
MouseLocation mouseDownLocation_ = MouseLocation::Outside;
+58 -16
View File
@@ -362,7 +362,8 @@ ChannelView::ChannelView(InternalCtor /*tag*/, QWidget *parent, Split *split,
this->queueUpdate();
});
this->messageColors_.applyTheme(getTheme());
this->messageColors_.applyTheme(getTheme(), this->isOverlay_,
getSettings()->overlayBackgroundOpacity);
this->messagePreferences_.connectSettings(getSettings(),
this->signalHolder_);
}
@@ -450,6 +451,11 @@ void ChannelView::initializeSignals()
});
}
Scrollbar *ChannelView::scrollbar()
{
return this->scrollBar_;
}
bool ChannelView::pausable() const
{
return pausable_;
@@ -574,7 +580,19 @@ void ChannelView::themeChangedEvent()
this->setupHighlightAnimationColors();
this->queueLayout();
this->messageColors_.applyTheme(getTheme());
this->messageColors_.applyTheme(getTheme(), this->isOverlay_,
getSettings()->overlayBackgroundOpacity);
}
void ChannelView::updateColorTheme()
{
this->themeChangedEvent();
}
void ChannelView::setIsOverlay(bool isOverlay)
{
this->isOverlay_ = isOverlay;
this->themeChangedEvent();
}
void ChannelView::setupHighlightAnimationColors()
@@ -680,9 +698,15 @@ void ChannelView::layoutVisibleMessages(
const auto &message = messages[i];
redrawRequired |= message->layout(
layoutWidth, this->scale(),
this->scale() * static_cast<float>(this->devicePixelRatio()),
flags, this->bufferInvalidationQueued_);
{
.messageColors = this->messageColors_,
.flags = flags,
.width = layoutWidth,
.scale = this->scale(),
.imageScale = this->scale() *
static_cast<float>(this->devicePixelRatio()),
},
this->bufferInvalidationQueued_);
y += message->getHeight();
}
@@ -717,8 +741,14 @@ void ChannelView::updateScrollbar(
auto *message = messages[i].get();
message->layout(
layoutWidth, this->scale(),
this->scale() * static_cast<float>(this->devicePixelRatio()), flags,
{
.messageColors = this->messageColors_,
.flags = flags,
.width = layoutWidth,
.scale = this->scale(),
.imageScale = this->scale() *
static_cast<float>(this->devicePixelRatio()),
},
false);
h -= message->getHeight();
@@ -1486,7 +1516,7 @@ void ChannelView::paintEvent(QPaintEvent *event)
QPainter painter(this);
painter.fillRect(rect(), this->theme->splits.background);
painter.fillRect(rect(), this->messageColors_.channelBackground);
// draw messages
this->drawMessages(painter, event->rect());
@@ -1705,10 +1735,16 @@ void ChannelView::wheelEvent(QWheelEvent *event)
else
{
snapshot[i - 1]->layout(
this->getLayoutWidth(), this->scale(),
this->scale() *
static_cast<float>(this->devicePixelRatio()),
this->getFlags(), false);
{
.messageColors = this->messageColors_,
.flags = this->getFlags(),
.width = this->getLayoutWidth(),
.scale = this->scale(),
.imageScale =
this->scale() *
static_cast<float>(this->devicePixelRatio()),
},
false);
scrollFactor = 1;
currentScrollLeft = snapshot[i - 1]->getHeight();
}
@@ -1742,10 +1778,16 @@ void ChannelView::wheelEvent(QWheelEvent *event)
else
{
snapshot[i + 1]->layout(
this->getLayoutWidth(), this->scale(),
this->scale() *
static_cast<float>(this->devicePixelRatio()),
this->getFlags(), false);
{
.messageColors = this->messageColors_,
.flags = this->getFlags(),
.width = this->getLayoutWidth(),
.scale = this->scale(),
.imageScale =
this->scale() *
static_cast<float>(this->devicePixelRatio()),
},
false);
scrollFactor = 1;
currentScrollLeft = snapshot[i + 1]->getHeight();
+12
View File
@@ -202,6 +202,16 @@ public:
*/
bool mayContainMessage(const MessagePtr &message);
void updateColorTheme();
/// @brief Adjusts the colors this view uses
///
/// If @a isOverlay is true, the overlay colors (as specified in the theme)
/// will be used. Otherwise, regular message-colors will be used.
void setIsOverlay(bool isOverlay);
Scrollbar *scrollbar();
pajlada::Signals::Signal<QMouseEvent *> mouseDown;
pajlada::Signals::NoArgSignal selectionChanged;
pajlada::Signals::Signal<HighlightState> tabHighlightRequested;
@@ -377,6 +387,8 @@ private:
bool onlyUpdateEmotes_ = false;
bool isOverlay_ = false;
// Mouse event variables
bool isLeftMouseDown_ = false;
bool isRightMouseDown_ = false;
+11 -5
View File
@@ -97,8 +97,8 @@ void MessageView::paintEvent(QPaintEvent * /*event*/)
void MessageView::themeChangedEvent()
{
this->messageColors_.applyTheme(getTheme());
this->messageColors_.regular = getTheme()->splits.input.background;
this->messageColors_.applyTheme(getTheme(), false, 255);
this->messageColors_.regularBg = getTheme()->splits.input.background;
if (this->messageLayout_)
{
this->messageLayout_->invalidateBuffer();
@@ -120,9 +120,15 @@ void MessageView::layoutMessage()
}
bool updateRequired = this->messageLayout_->layout(
this->width_, this->scale(),
this->scale() * static_cast<float>(this->devicePixelRatio()),
MESSAGE_FLAGS, false);
{
.messageColors = this->messageColors_,
.flags = MESSAGE_FLAGS,
.width = this->width_,
.scale = this->scale(),
.imageScale =
this->scale() * static_cast<float>(this->devicePixelRatio()),
},
false);
if (updateRequired)
{
+123
View File
@@ -0,0 +1,123 @@
#include "widgets/helper/OverlayInteraction.hpp"
#include "common/Literals.hpp"
#include "widgets/OverlayWindow.hpp"
#include <QGridLayout>
namespace chatterino {
using namespace literals;
OverlayInteraction::OverlayInteraction(OverlayWindow *parent)
: QWidget(nullptr)
, interactAnimation_(this, "interactionProgress"_ba)
, window_(parent)
{
this->interactAnimation_.setStartValue(0.0);
this->interactAnimation_.setEndValue(1.0);
this->closeButton_.setButtonStyle(TitleBarButtonStyle::Close);
this->closeButton_.setScaleIndependantSize(46, 30);
this->closeButton_.hide();
this->closeButton_.setCursor(Qt::PointingHandCursor);
}
void OverlayInteraction::attach(QGridLayout *layout)
{
layout->addWidget(this, 0, 0);
layout->addWidget(&this->closeButton_, 0, 0, Qt::AlignTop | Qt::AlignRight);
layout->setContentsMargins(0, 0, 0, 0);
QObject::connect(&this->closeButton_, &TitleBarButton::leftClicked,
[this]() {
this->window_->close();
});
}
QWidget *OverlayInteraction::closeButton()
{
return &this->closeButton_;
}
void OverlayInteraction::startInteraction()
{
if (this->interacting_)
{
return;
}
this->interacting_ = true;
if (this->interactAnimation_.state() != QPropertyAnimation::Stopped)
{
this->interactAnimation_.stop();
}
this->interactAnimation_.setDirection(QPropertyAnimation::Forward);
this->interactAnimation_.setDuration(100);
this->interactAnimation_.start();
this->window_->setOverrideCursor(Qt::SizeAllCursor);
this->closeButton_.show();
}
void OverlayInteraction::endInteraction()
{
if (!this->interacting_)
{
return;
}
this->interacting_ = false;
if (this->interactAnimation_.state() != QPropertyAnimation::Stopped)
{
this->interactAnimation_.stop();
}
this->interactAnimation_.setDirection(QPropertyAnimation::Backward);
this->interactAnimation_.setDuration(200);
this->interactAnimation_.start();
this->window_->setOverrideCursor(Qt::ArrowCursor);
this->closeButton_.hide();
}
bool OverlayInteraction::isInteracting() const
{
return this->interacting_;
}
void OverlayInteraction::paintEvent(QPaintEvent * /*event*/)
{
QPainter painter(this);
QColor highlightColor(
255, 255, 255, std::max(int(255.0 * this->interactionProgress()), 50));
painter.setPen({highlightColor, 2});
// outline
auto bounds = this->rect();
painter.drawRect(bounds);
if (this->interactionProgress() <= 0.0)
{
return;
}
highlightColor.setAlpha(highlightColor.alpha() / 4);
painter.setBrush(highlightColor);
painter.setPen(Qt::transparent);
// close button
auto buttonSize = this->closeButton_.size();
painter.drawRect(
QRect{bounds.topRight() - QPoint{buttonSize.width(), 0}, buttonSize});
}
double OverlayInteraction::interactionProgress() const
{
return this->interactionProgress_;
}
void OverlayInteraction::setInteractionProgress(double progress)
{
this->interactionProgress_ = progress;
this->update();
}
} // namespace chatterino
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "widgets/helper/TitlebarButton.hpp"
#include <QPropertyAnimation>
#include <QWidget>
class QGridLayout;
namespace chatterino {
class OverlayWindow;
class OverlayInteraction : public QWidget
{
Q_OBJECT
public:
OverlayInteraction(OverlayWindow *parent);
void attach(QGridLayout *layout);
QWidget *closeButton();
void startInteraction();
void endInteraction();
bool isInteracting() const;
protected:
void paintEvent(QPaintEvent *event) override;
private:
Q_PROPERTY(double interactionProgress READ interactionProgress WRITE
setInteractionProgress)
TitleBarButton closeButton_;
double interactionProgress() const;
void setInteractionProgress(double progress);
bool interacting_ = false;
double interactionProgress_ = 0.0;
QPropertyAnimation interactAnimation_;
OverlayWindow *window_;
};
} // namespace chatterino
+34
View File
@@ -969,6 +969,40 @@ void GeneralPage::initLayout(GeneralPageView &layout)
layout.addCheckbox("Use custom FrankerFaceZ VIP badges",
s.useCustomFfzVipBadges);
layout.addSubtitle("Overlay");
layout.addIntInput(
"Background opacity (0-255)", s.overlayBackgroundOpacity, 0, 255, 1,
"Controls the opacity of the (possibly alternating) background behind "
"messages. The color is set through the current theme. 255 corresponds "
"to a fully opaque background.");
layout.addCheckbox("Enable Shadow", s.enableOverlayShadow, false,
"Enables a drop shadow on the overlay. This will use "
"more processing power.");
layout.addIntInput("Shadow opacity (0-255)", s.overlayShadowOpacity, 0, 255,
1,
"Controls the opacity of the added drop shadow. 255 "
"corresponds to a fully opaque shadow.");
layout.addColorButton("Shadow color",
QColor(getSettings()->overlayShadowColor.getValue()),
getSettings()->overlayShadowColor);
layout
.addIntInput("Shadow radius", s.overlayShadowRadius, 0, 40, 1,
"Controls how far the shadow is spread (the blur "
"radius) in device-independent pixels.")
->setSuffix("dp");
layout
.addIntInput("Shadow offset x", s.overlayShadowOffsetX, -20, 20, 1,
"Controls how far the shadow is offset on the x axis in "
"device-independent pixels. A negative value offsets to "
"the left and a positive to the right.")
->setSuffix("dp");
layout
.addIntInput("Shadow offset y", s.overlayShadowOffsetY, -20, 20, 1,
"Controls how far the shadow is offset on the y axis in "
"device-independent pixels. A negative value offsets to "
"the top and a positive to the bottom.")
->setSuffix("dp");
layout.addSubtitle("Miscellaneous");
if (supportsIncognitoLinks())
+56
View File
@@ -33,6 +33,7 @@
#include "widgets/helper/ResizingTextEdit.hpp"
#include "widgets/helper/SearchPopup.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/OverlayWindow.hpp"
#include "widgets/Scrollbar.hpp"
#include "widgets/splits/DraggedSplit.hpp"
#include "widgets/splits/SplitContainer.hpp"
@@ -765,6 +766,47 @@ void Split::addShortcuts()
}
return "";
}},
{"popupOverlay",
[this](const auto &) -> QString {
this->showOverlayWindow();
return {};
}},
{"toggleOverlayInertia",
[this](const auto &args) -> QString {
if (args.empty())
{
return "No arguments provided to toggleOverlayInertia "
"(expected one)";
}
const auto &arg = args.front();
if (arg == "this")
{
if (this->overlayWindow_)
{
this->overlayWindow_->toggleInertia();
}
return {};
}
if (arg == "thisOrAll")
{
if (this->overlayWindow_)
{
this->overlayWindow_->toggleInertia();
}
else
{
getApp()->getWindows()->toggleAllOverlayInertia();
}
return {};
}
if (arg == "all")
{
getApp()->getWindows()->toggleAllOverlayInertia();
return {};
}
return {};
}},
};
this->shortcuts_ = getApp()->getHotkeys()->shortcutsForCategory(
@@ -1104,6 +1146,20 @@ void Split::popup()
window.show();
}
OverlayWindow *Split::overlayWindow()
{
return this->overlayWindow_.data();
}
void Split::showOverlayWindow()
{
if (!this->overlayWindow_)
{
this->overlayWindow_ = new OverlayWindow(this->getIndirectChannel());
}
this->overlayWindow_->show();
}
void Split::clear()
{
this->view_->clearMessages();
+6
View File
@@ -21,6 +21,7 @@ class SplitInput;
class SplitContainer;
class SplitOverlay;
class SelectChannelDialog;
class OverlayWindow;
// Each ChatWidget consists of three sub-elements that handle their own part of
// the chat widget: ChatWidgetHeader
@@ -80,6 +81,8 @@ public:
// This is called on window focus lost
void unpause();
OverlayWindow *overlayWindow();
static pajlada::Signals::Signal<Qt::KeyboardModifiers>
modifierStatusChanged;
static Qt::KeyboardModifiers modifierStatus;
@@ -158,6 +161,8 @@ private:
SplitInput *const input_;
SplitOverlay *const overlay_;
QPointer<OverlayWindow> overlayWindow_;
QPointer<SelectChannelDialog> selectChannelDialog_;
pajlada::Signals::Connection channelIDChangedConnection_;
@@ -179,6 +184,7 @@ public slots:
void explainMoving();
void explainSplitting();
void popup();
void showOverlayWindow();
void clear();
void openInBrowser();
void openModViewInBrowser();
+3
View File
@@ -390,6 +390,9 @@ std::unique_ptr<QMenu> SplitHeader::createMainMenu()
menu->addAction(
"Popup", this->split_, &Split::popup,
h->getDisplaySequence(HotkeyCategory::Window, "popup", {{"split"}}));
menu->addAction(
"Popup overlay", this->split_, &Split::showOverlayWindow,
h->getDisplaySequence(HotkeyCategory::Split, "popupOverlay"));
menu->addAction(
"Search", this->split_,
[this] {