#include "widgets/OverlayWindow.hpp" #include "Application.hpp" #include "common/FlagsEnum.hpp" #include "common/Literals.hpp" #include "controllers/hotkeys/HotkeyController.hpp" #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" #include "util/PostToThread.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 #include #include #include #include #include #ifdef Q_OS_WIN # include # include // 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 current(static_cast( getSettings()->overlayKnowledgeLevel.getValue())); return current.has(knowledge); } void acquireKnowledge(Knowledge knowledge) { FlagsEnum current(static_cast( getSettings()->overlayKnowledgeLevel.getValue())); current.set(knowledge); getSettings()->overlayKnowledgeLevel = static_cast>(current.value()); } /// Returns [seq?, toggleAllOverlays] std::pair 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, const QList &filterIDs) : 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_.setFilters(filterIDs); 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()->setHideThumb(true); this->channelView_.scrollbar()->setHideHighlights(true); 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(); getApp()->getEmotes()->getGIFTimer().registerOpenOverlayWindow(); } OverlayWindow::~OverlayWindow() { #ifdef Q_OS_WIN ::DestroyCursor(this->sizeAllCursor_); #endif getApp()->getEmotes()->getGIFTimer().unregisterOpenOverlayWindow(); } 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(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(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(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 case WM_DPICHANGED: { // wait for Qt to process this message, same as in BaseWindow postToThread([] { getApp()->getWindows()->invalidateChannelViewBuffers(); }); } break; 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 "

" "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"

"_s "This is still an early version and some features are " "missing. Please provide feedback on GitHub."; 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