Use Qt's High-DPI scaling on Windows (#4868)

This commit is contained in:
nerix 2024-05-12 13:59:14 +02:00 committed by GitHub
parent 8202cd0d99
commit febcf464fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 459 additions and 320 deletions

View file

@ -3,8 +3,10 @@
## Unversioned
- Major: Release plugins alpha. (#5288)
- Major: Improve high-DPI support on Windows. (#4868)
- Minor: Add option to customise Moderation buttons with images. (#5369)
- Bugfix: If a network request errors with 200 OK, Qt's error code is now reported instead of the HTTP status. (#5378)
- Dev: Use Qt's high DPI scaling. (#4868)
- Dev: Add doxygen build target. (#5377)
- Dev: Make printing of strings in tests easier. (#5379)
- Dev: Refactor and document `Scrollbar`. (#5334)

View file

@ -1,11 +1,11 @@
* {
font-size: <font-size>px;
font-size: 14px;
font-family: "Segoe UI";
}
QCheckBox::indicator {
width: <checkbox-size>px;
height: <checkbox-size>px;
width: 14px;
height: 14px;
}
chatterino--ComboBox {

View file

@ -86,10 +86,6 @@ namespace {
QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
#endif
#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true);
#endif
QApplication::setStyle(QStyleFactory::create("Fusion"));
#ifndef Q_OS_MAC

View file

@ -26,11 +26,6 @@ using namespace chatterino;
int main(int argc, char **argv)
{
// TODO: This is a temporary fix (see #4552).
#if defined(Q_OS_WINDOWS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
qputenv("QT_ENABLE_HIGHDPI_SCALING", "0");
#endif
QApplication a(argc, argv);
QCoreApplication::setApplicationName("chatterino");

View file

@ -155,8 +155,8 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container,
{
if (flags.has(MessageElementFlag::EmoteImages))
{
auto image =
this->emote_->images.getImageOrLoaded(container.getScale());
auto image = this->emote_->images.getImageOrLoaded(
container.getImageScale());
if (image->isEmpty())
{
return;
@ -210,7 +210,7 @@ void LayeredEmoteElement::addToContainer(MessageLayoutContainer &container,
{
if (flags.has(MessageElementFlag::EmoteImages))
{
auto images = this->getLoadedImages(container.getScale());
auto images = this->getLoadedImages(container.getImageScale());
if (images.empty())
{
return;
@ -364,7 +364,7 @@ void BadgeElement::addToContainer(MessageLayoutContainer &container,
if (flags.hasAny(this->getFlags()))
{
auto image =
this->emote_->images.getImageOrLoaded(container.getScale());
this->emote_->images.getImageOrLoaded(container.getImageScale());
if (image->isEmpty())
{
return;
@ -798,7 +798,7 @@ void ScalingImageElement::addToContainer(MessageLayoutContainer &container,
if (flags.hasAny(this->getFlags()))
{
const auto &image =
this->images_.getImageOrLoaded(container.getScale());
this->images_.getImageOrLoaded(container.getImageScale());
if (image->isEmpty())
{
return;

View file

@ -74,7 +74,8 @@ int MessageLayout::getWidth() const
// Layout
// return true if redraw is required
bool MessageLayout::layout(int width, float scale, MessageElementFlags flags,
bool MessageLayout::layout(int width, float scale, float imageScale,
MessageElementFlags flags,
bool shouldInvalidateBuffer)
{
// BenchmarkGuard benchmark("MessageLayout::layout()");
@ -106,6 +107,8 @@ bool MessageLayout::layout(int width, float scale, MessageElementFlags flags,
// check if dpi changed
layoutRequired |= this->scale_ != scale;
this->scale_ = scale;
layoutRequired |= this->imageScale_ != imageScale;
this->imageScale_ = imageScale;
if (!layoutRequired)
{
@ -148,7 +151,8 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
bool hideSimilar = getSettings()->hideSimilar;
bool hideReplies = !flags.has(MessageElementFlag::RepliedMessage);
this->container_.beginLayout(width, this->scale_, messageFlags);
this->container_.beginLayout(width, this->scale_, this->imageScale_,
messageFlags);
for (const auto &element : this->message_->elements)
{
@ -288,16 +292,11 @@ QPixmap *MessageLayout::ensureBuffer(QPainter &painter, int width)
}
// Create new buffer
#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX)
this->buffer_ = std::make_unique<QPixmap>(
int(width * painter.device()->devicePixelRatioF()),
int(this->container_.getHeight() *
painter.device()->devicePixelRatioF()));
this->buffer_->setDevicePixelRatio(painter.device()->devicePixelRatioF());
#else
this->buffer_ = std::make_unique<QPixmap>(
width, std::max(16, this->container_.getHeight()));
#endif
this->bufferValid_ = false;
DebugCount::increase("message drawing buffers");

View file

@ -56,8 +56,8 @@ public:
MessageLayoutFlags flags;
bool layout(int width, float scale_, MessageElementFlags flags,
bool shouldInvalidateBuffer);
bool layout(int width, float scale_, float imageScale,
MessageElementFlags flags, bool shouldInvalidateBuffer);
// Painting
MessagePaintResult paint(const MessagePaintContext &ctx);
@ -128,6 +128,7 @@ private:
int currentLayoutWidth_ = -1;
int layoutState_ = -1;
float scale_ = -1;
float imageScale_ = -1.F;
MessageElementFlags currentWordFlags_;
#ifdef FOURTF

View file

@ -30,7 +30,7 @@ constexpr const QMargins MARGIN{8, 4, 8, 4};
namespace chatterino {
void MessageLayoutContainer::beginLayout(int width, float scale,
MessageFlags flags)
float imageScale, MessageFlags flags)
{
this->elements_.clear();
this->lines_.clear();
@ -45,6 +45,7 @@ void MessageLayoutContainer::beginLayout(int width, float scale,
this->width_ = width;
this->height_ = 0;
this->scale_ = scale;
this->imageScale_ = imageScale;
this->flags_ = flags;
auto mediumFontMetrics =
getIApp()->getFonts()->getFontMetrics(FontStyle::ChatMedium, scale);
@ -526,6 +527,11 @@ float MessageLayoutContainer::getScale() const
return this->scale_;
}
float MessageLayoutContainer::getImageScale() const
{
return this->imageScale_;
}
bool MessageLayoutContainer::isCollapsed() const
{
return this->isCollapsed_;

View file

@ -32,7 +32,8 @@ struct MessageLayoutContainer {
* This will reset all line calculations, and will be considered incomplete
* until the accompanying end function has been called
*/
void beginLayout(int width_, float scale_, MessageFlags flags_);
void beginLayout(int width, float scale, float imageScale,
MessageFlags flags);
/**
* Finish the layout process of this message
@ -146,6 +147,11 @@ struct MessageLayoutContainer {
*/
float getScale() const;
/**
* Returns the image scale
*/
float getImageScale() const;
/**
* Returns true if this message is collapsed
*/
@ -270,6 +276,10 @@ private:
// variables
float scale_ = 1.F;
/**
* Scale factor for images
*/
float imageScale_ = 1.F;
int width_ = 0;
MessageFlags flags_{};
/**

View file

@ -270,20 +270,22 @@ void AttachedWindow::updateWindowRect(void *_attachedPtr)
}
float scale = 1.f;
float ourScale = 1.F;
if (auto dpi = getWindowDpi(attached))
{
scale = *dpi / 96.f;
ourScale = scale / this->devicePixelRatio();
for (auto w : this->ui_.split->findChildren<BaseWidget *>())
{
w->setOverrideScale(scale);
w->setOverrideScale(ourScale);
}
this->ui_.split->setOverrideScale(scale);
this->ui_.split->setOverrideScale(ourScale);
}
if (this->height_ != -1)
{
this->ui_.split->setFixedWidth(int(this->width_ * scale));
this->ui_.split->setFixedWidth(int(this->width_ * ourScale));
// offset
int o = this->fullscreen_ ? 0 : 8;

View file

@ -120,19 +120,6 @@ void BaseWidget::setScaleIndependantHeight(int value)
QSize(this->scaleIndependantSize_.width(), value));
}
float BaseWidget::qtFontScale() const
{
if (auto *window = dynamic_cast<BaseWindow *>(this->window()))
{
// ensure no div by 0
return this->scale() / std::max<float>(0.01f, window->nativeScale_);
}
else
{
return this->scale();
}
}
void BaseWidget::childEvent(QChildEvent *event)
{
if (event->added())

View file

@ -34,8 +34,6 @@ public:
void setScaleIndependantWidth(int value);
void setScaleIndependantHeight(int value);
float qtFontScale() const;
protected:
void childEvent(QChildEvent *) override;
void showEvent(QShowEvent *) override;

View file

@ -29,12 +29,163 @@
# pragma comment(lib, "Dwmapi.lib")
# include <QHBoxLayout>
# define WM_DPICHANGED 0x02E0
# include <QOperatingSystemVersion>
#endif
#include "widgets/helper/TitlebarButton.hpp"
namespace {
#ifdef USEWINSDK
// From kHiddenTaskbarSize in Firefox
constexpr UINT HIDDEN_TASKBAR_SIZE = 2;
bool isWindows11OrGreater()
{
static const bool result = [] {
// This calls RtlGetVersion under the hood so we don't have to.
// The micro version corresponds to dwBuildNumber.
auto version = QOperatingSystemVersion::current();
return (version.majorVersion() > 10) ||
(version.microVersion() >= 22000);
}();
return result;
}
/// Finds the taskbar HWND on a specific monitor (or any)
HWND findTaskbarWindow(LPRECT rcMon = nullptr)
{
HWND taskbar = nullptr;
RECT taskbarRect;
// return value of IntersectRect, unused
RECT intersectionRect;
while ((taskbar = FindWindowEx(nullptr, taskbar, L"Shell_TrayWnd",
nullptr)) != nullptr)
{
if (!rcMon)
{
// no monitor was specified, return the first encountered window
break;
}
if (GetWindowRect(taskbar, &taskbarRect) != 0 &&
IntersectRect(&intersectionRect, &taskbarRect, rcMon) != 0)
{
// taskbar intersects with the monitor - this is the one
break;
}
}
return taskbar;
}
/// Gets the edge of the taskbar if it's automatically hidden
std::optional<UINT> hiddenTaskbarEdge(LPRECT rcMon = nullptr)
{
HWND taskbar = findTaskbarWindow(rcMon);
if (!taskbar)
{
return std::nullopt;
}
APPBARDATA state = {sizeof(state), taskbar};
APPBARDATA pos = {sizeof(pos), taskbar};
auto appBarState =
static_cast<LRESULT>(SHAppBarMessage(ABM_GETSTATE, &state));
if ((appBarState & ABS_AUTOHIDE) == 0)
{
return std::nullopt;
}
if (SHAppBarMessage(ABM_GETTASKBARPOS, &pos) == 0)
{
qCDebug(chatterinoApp) << "Failed to get taskbar pos";
return ABE_BOTTOM;
}
return pos.uEdge;
}
/// @brief Gets the window borders for @a hwnd
///
/// Each side of the returned RECT has the correct sign, so they can be added
/// to a window rect.
/// Shrinking by 1px would return {left: 1, top: 1, right: -1, left: -1}.
RECT windowBordersFor(HWND hwnd, bool isMaximized)
{
RECT margins{0, 0, 0, 0};
auto addBorders = isMaximized || isWindows11OrGreater();
if (addBorders)
{
auto dpi = GetDpiForWindow(hwnd);
auto systemMetric = [&](auto index) {
if (dpi != 0)
{
return GetSystemMetricsForDpi(index, dpi);
}
return GetSystemMetrics(index);
};
auto paddedBorder = systemMetric(SM_CXPADDEDBORDER);
auto borderWidth = systemMetric(SM_CXSIZEFRAME) + paddedBorder;
auto borderHeight = systemMetric(SM_CYSIZEFRAME) + paddedBorder;
margins.left += borderWidth;
margins.right -= borderWidth;
if (isMaximized)
{
margins.top += borderHeight;
}
margins.bottom -= borderHeight;
}
if (isMaximized)
{
auto *hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi;
mi.cbSize = sizeof(mi);
auto *monitor = [&]() -> LPRECT {
if (GetMonitorInfo(hMonitor, &mi))
{
return &mi.rcMonitor;
}
return nullptr;
}();
auto edge = hiddenTaskbarEdge(monitor);
if (edge)
{
switch (*edge)
{
case ABE_LEFT:
margins.left += HIDDEN_TASKBAR_SIZE;
break;
case ABE_RIGHT:
margins.right -= HIDDEN_TASKBAR_SIZE;
break;
case ABE_TOP:
margins.top += HIDDEN_TASKBAR_SIZE;
break;
case ABE_BOTTOM:
margins.bottom -= HIDDEN_TASKBAR_SIZE;
break;
default:
break;
}
}
}
return margins;
}
#endif
} // namespace
namespace chatterino {
BaseWindow::BaseWindow(FlagsEnum<Flags> _flags, QWidget *parent)
@ -117,23 +268,17 @@ float BaseWindow::scale() const
return std::max<float>(0.01f, this->overrideScale().value_or(this->scale_));
}
float BaseWindow::qtFontScale() const
{
return this->scale() / std::max<float>(0.01F, this->nativeScale_);
}
void BaseWindow::init()
{
#ifdef USEWINSDK
if (this->hasCustomWindowFrame())
{
// CUSTOM WINDOW FRAME
QVBoxLayout *layout = new QVBoxLayout();
auto *layout = new QVBoxLayout(this);
this->ui_.windowLayout = layout;
layout->setContentsMargins(1, 1, 1, 1);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
this->setLayout(layout);
{
if (!this->frameless_)
{
QHBoxLayout *buttonLayout = this->ui_.titlebarBox =
@ -148,64 +293,55 @@ void BaseWindow::init()
title->setText(text);
});
QSizePolicy policy(QSizePolicy::Ignored,
QSizePolicy::Preferred);
QSizePolicy policy(QSizePolicy::Ignored, QSizePolicy::Preferred);
policy.setHorizontalStretch(1);
title->setSizePolicy(policy);
buttonLayout->addWidget(title);
this->ui_.titleLabel = title;
// buttons
TitleBarButton *_minButton = new TitleBarButton;
_minButton->setButtonStyle(TitleBarButtonStyle::Minimize);
TitleBarButton *_maxButton = new TitleBarButton;
_maxButton->setButtonStyle(TitleBarButtonStyle::Maximize);
TitleBarButton *_exitButton = new TitleBarButton;
_exitButton->setButtonStyle(TitleBarButtonStyle::Close);
auto *minButton = new TitleBarButton;
minButton->setButtonStyle(TitleBarButtonStyle::Minimize);
auto *maxButton = new TitleBarButton;
maxButton->setButtonStyle(TitleBarButtonStyle::Maximize);
auto *exitButton = new TitleBarButton;
exitButton->setButtonStyle(TitleBarButtonStyle::Close);
QObject::connect(_minButton, &TitleBarButton::leftClicked, this,
QObject::connect(minButton, &TitleBarButton::leftClicked, this,
[this] {
this->setWindowState(Qt::WindowMinimized |
this->windowState());
});
QObject::connect(_maxButton, &TitleBarButton::leftClicked, this,
[this, _maxButton] {
this->setWindowState(
_maxButton->getButtonStyle() !=
QObject::connect(
maxButton, &TitleBarButton::leftClicked, this,
[this, maxButton] {
this->setWindowState(maxButton->getButtonStyle() !=
TitleBarButtonStyle::Maximize
? Qt::WindowActive
: Qt::WindowMaximized);
});
QObject::connect(_exitButton, &TitleBarButton::leftClicked,
this, [this] {
QObject::connect(exitButton, &TitleBarButton::leftClicked, this,
[this] {
this->close();
});
this->ui_.titlebarButtons = new TitleBarButtons(
this, _minButton, _maxButton, _exitButton);
this->ui_.titlebarButtons =
new TitleBarButtons(this, minButton, maxButton, exitButton);
this->ui_.buttons.push_back(_minButton);
this->ui_.buttons.push_back(_maxButton);
this->ui_.buttons.push_back(_exitButton);
this->ui_.buttons.push_back(minButton);
this->ui_.buttons.push_back(maxButton);
this->ui_.buttons.push_back(exitButton);
// buttonLayout->addStretch(1);
buttonLayout->addWidget(_minButton);
buttonLayout->addWidget(_maxButton);
buttonLayout->addWidget(_exitButton);
buttonLayout->addWidget(minButton);
buttonLayout->addWidget(maxButton);
buttonLayout->addWidget(exitButton);
buttonLayout->setSpacing(0);
}
}
this->ui_.layoutBase = new BaseWidget(this);
this->ui_.layoutBase->setContentsMargins(1, 0, 1, 1);
layout->addWidget(this->ui_.layoutBase);
}
// DPI
// auto dpi = getWindowDpi(this->safeHWND());
// if (dpi) {
// this->scale = dpi.value() / 96.f;
// }
#endif
// TopMost flag overrides setting
@ -571,29 +707,8 @@ void BaseWindow::resizeEvent(QResizeEvent *)
}
#ifdef USEWINSDK
if (this->hasCustomWindowFrame() && !this->isResizeFixing_)
{
this->isResizeFixing_ = true;
QTimer::singleShot(50, this, [this] {
auto hwnd = this->safeHWND();
if (!hwnd)
{
this->isResizeFixing_ = false;
return;
}
RECT rect;
::GetWindowRect(*hwnd, &rect);
::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left + 1,
rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER);
::SetWindowPos(*hwnd, nullptr, 0, 0, rect.right - rect.left,
rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER);
QTimer::singleShot(10, this, [this] {
this->isResizeFixing_ = false;
});
});
}
this->calcButtonsSizes();
this->updateRealSize();
#endif
}
@ -655,10 +770,6 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
switch (msg->message)
{
case WM_DPICHANGED:
returnValue = this->handleDPICHANGED(msg);
break;
case WM_SHOWWINDOW:
returnValue = this->handleSHOWWINDOW(msg);
break;
@ -697,12 +808,15 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
{
*result = 0;
returnValue = true;
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
RECT winrect;
GetWindowRect(msg->hwnd, &winrect);
QPoint globalPos(x, y);
POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)};
ScreenToClient(msg->hwnd, &p);
QPoint globalPos(p.x, p.y);
globalPos /= this->devicePixelRatio();
globalPos = this->mapToGlobal(globalPos);
// TODO(nerix): use TrackMouseEvent here
this->ui_.titlebarButtons->hover(msg->wParam, globalPos);
this->lastEventWasNcMouseMove_ = true;
}
@ -748,12 +862,14 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
*result = 0;
auto ht = msg->wParam;
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
RECT winrect;
GetWindowRect(msg->hwnd, &winrect);
QPoint globalPos(x, y);
POINT p{GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)};
ScreenToClient(msg->hwnd, &p);
QPoint globalPos(p.x, p.y);
globalPos /= this->devicePixelRatio();
globalPos = this->mapToGlobal(globalPos);
if (msg->message == WM_NCLBUTTONDOWN)
{
this->ui_.titlebarButtons->mousePress(ht, globalPos);
@ -784,7 +900,7 @@ void BaseWindow::scaleChangedEvent(float scale)
#endif
this->setFont(
getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->qtFontScale()));
getIApp()->getFonts()->getFont(FontStyle::UiTabs, this->scale()));
}
void BaseWindow::paintEvent(QPaintEvent *)
@ -802,10 +918,9 @@ void BaseWindow::paintEvent(QPaintEvent *)
void BaseWindow::updateScale()
{
auto scale =
this->nativeScale_ * (this->flags_.has(DisableCustomScaling)
auto scale = this->flags_.has(DisableCustomScaling)
? 1
: getSettings()->getClampedUiScale());
: getSettings()->getClampedUiScale();
this->setScale(scale);
@ -815,6 +930,22 @@ void BaseWindow::updateScale()
}
}
#ifdef USEWINSDK
void BaseWindow::updateRealSize()
{
auto hwnd = this->safeHWND();
if (!hwnd)
{
return;
}
RECT real;
::GetWindowRect(*hwnd, &real);
this->realBounds_ = QRect(real.left, real.top, real.right - real.left,
real.bottom - real.top);
}
#endif
void BaseWindow::calcButtonsSizes()
{
if (!this->shown_)
@ -846,31 +977,25 @@ void BaseWindow::drawCustomWindowFrame(QPainter &painter)
{
QColor bg = this->overrideBackgroundColor_.value_or(
this->theme->window.background);
painter.fillRect(QRect(1, 2, this->width() - 2, this->height() - 3),
bg);
if (this->isMaximized_)
{
painter.fillRect(this->rect(), bg);
}
else
{
// Draw a border that's exactly 1px wide
//
// There is a bug where the border can get <dpr>px wide while dragging.
// this "fixes" itself when deselecting the window.
auto dpr = this->devicePixelRatio();
if (dpr != 1)
{
painter.setTransform(QTransform::fromScale(1 / dpr, 1 / dpr));
}
painter.fillRect(1, 1, this->realBounds_.width() - 2,
this->realBounds_.height() - 2, bg);
}
}
#endif
}
bool BaseWindow::handleDPICHANGED(MSG *msg)
{
#ifdef USEWINSDK
int dpi = HIWORD(msg->wParam);
float _scale = dpi / 96.f;
auto *prcNewWindow = reinterpret_cast<RECT *>(msg->lParam);
SetWindowPos(msg->hwnd, nullptr, prcNewWindow->left, prcNewWindow->top,
prcNewWindow->right - prcNewWindow->left,
prcNewWindow->bottom - prcNewWindow->top,
SWP_NOZORDER | SWP_NOACTIVATE);
this->nativeScale_ = _scale;
this->updateScale();
return true;
#else
return false;
#endif
}
@ -883,16 +1008,6 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg)
return true;
}
if (auto dpi = getWindowDpi(msg->hwnd))
{
float currentScale = (float)dpi.value() / 96.F;
if (currentScale != this->nativeScale_)
{
this->nativeScale_ = currentScale;
this->updateScale();
}
}
if (!this->shown_)
{
this->shown_ = true;
@ -906,14 +1021,12 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg)
if (!this->initalBounds_.isNull())
{
::SetWindowPos(msg->hwnd, nullptr, this->initalBounds_.x(),
this->initalBounds_.y(), this->initalBounds_.width(),
this->initalBounds_.height(),
SWP_NOZORDER | SWP_NOACTIVATE);
this->setGeometry(this->initalBounds_);
this->currentBounds_ = this->initalBounds_;
}
this->calcButtonsSizes();
this->updateRealSize();
}
return true;
@ -929,23 +1042,54 @@ bool BaseWindow::handleNCCALCSIZE(MSG *msg, long *result)
#endif
{
#ifdef USEWINSDK
if (this->hasCustomWindowFrame())
if (!this->hasCustomWindowFrame())
{
if (msg->wParam == TRUE)
{
// remove 1 extra pixel on top of custom frame
auto *ncp = reinterpret_cast<NCCALCSIZE_PARAMS *>(msg->lParam);
if (ncp)
{
ncp->lppos->flags |= SWP_NOREDRAW;
ncp->rgrc[0].top -= 1;
}
return false;
}
if (msg->wParam != TRUE)
{
*result = 0;
return true;
}
return false;
auto *params = reinterpret_cast<NCCALCSIZE_PARAMS *>(msg->lParam);
auto *r = &params->rgrc[0];
WINDOWPLACEMENT wp;
wp.length = sizeof(WINDOWPLACEMENT);
this->isMaximized_ = GetWindowPlacement(msg->hwnd, &wp) != 0 &&
(wp.showCmd == SW_SHOWMAXIMIZED);
auto borders = windowBordersFor(msg->hwnd, this->isMaximized_);
r->left += borders.left;
r->top += borders.top;
r->right += borders.right;
r->bottom += borders.bottom;
if (borders.left != 0 || borders.top != 0 || borders.right != 0 ||
borders.bottom != 0)
{
// We added borders -> we changed the rect, so we can't return
// WVR_VALIDRECTS
*result = 0;
return true;
}
// This is an attempt at telling Windows to not redraw (or at least to do a
// better job at redrawing) the window. There is a long list of tricks
// people tried to prevent this at
// https://stackoverflow.com/q/53000291/16300717
//
// We set the source and destination rectangles to a 1x1 rectangle at the
// top left. Windows is instructed by WVR_VALIDRECTS to copy and preserve
// some parts of the window image.
QPoint fixed = {r->left, r->top};
params->rgrc[1] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1};
params->rgrc[2] = {fixed.x(), fixed.y(), fixed.x() + 1, fixed.y() + 1};
*result = WVR_VALIDRECTS;
return true;
#else
return false;
#endif
@ -962,28 +1106,11 @@ bool BaseWindow::handleSIZE(MSG *msg)
}
else if (this->hasCustomWindowFrame())
{
if (msg->wParam == SIZE_MAXIMIZED)
{
auto offset =
int(getWindowDpi(msg->hwnd).value_or(96) * 8 / 96);
this->ui_.windowLayout->setContentsMargins(offset, offset,
offset, offset);
}
else
{
this->ui_.windowLayout->setContentsMargins(0, 1, 0, 0);
}
this->isNotMinimizedOrMaximized_ = msg->wParam == SIZE_RESTORED;
if (this->isNotMinimizedOrMaximized_)
{
RECT rect;
::GetWindowRect(msg->hwnd, &rect);
this->currentBounds_ =
QRect(QPoint(rect.left, rect.top),
QPoint(rect.right - 1, rect.bottom - 1));
this->currentBounds_ = this->geometry();
}
this->useNextBounds_.stop();
@ -993,6 +1120,12 @@ bool BaseWindow::handleSIZE(MSG *msg)
// the minimize button, so we have to emulate it.
this->ui_.titlebarButtons->leave();
}
RECT real;
::GetWindowRect(msg->hwnd, &real);
this->realBounds_ =
QRect(real.left, real.top, real.right - real.left,
real.bottom - real.top);
}
}
return false;
@ -1006,11 +1139,7 @@ bool BaseWindow::handleMOVE(MSG *msg)
#ifdef USEWINSDK
if (this->isNotMinimizedOrMaximized_)
{
RECT rect;
::GetWindowRect(msg->hwnd, &rect);
this->nextBounds_ = QRect(QPoint(rect.left, rect.top),
QPoint(rect.right - 1, rect.bottom - 1));
this->nextBounds_ = this->geometry();
this->useNextBounds_.start(10);
}
#endif
@ -1024,31 +1153,37 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
#endif
{
#ifdef USEWINSDK
const LONG border_width = 8; // in pixels
RECT winrect;
GetWindowRect(msg->hwnd, &winrect);
const LONG borderWidth = 8; // in device independent pixels
long x = GET_X_LPARAM(msg->lParam);
long y = GET_Y_LPARAM(msg->lParam);
auto rect = this->rect();
QPoint point(x - winrect.left, y - winrect.top);
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();
if (this->hasCustomWindowFrame())
{
*result = 0;
bool resizeWidth = minimumWidth() != maximumWidth();
bool resizeHeight = minimumHeight() != maximumHeight();
bool resizeWidth =
minimumWidth() != maximumWidth() && !this->isMaximized();
bool resizeHeight =
minimumHeight() != maximumHeight() && !this->isMaximized();
if (resizeWidth)
{
// left border
if (x < winrect.left + border_width)
if (x < rect.left() + borderWidth)
{
*result = HTLEFT;
}
// right border
if (x >= winrect.right - border_width)
if (x >= rect.right() - borderWidth)
{
*result = HTRIGHT;
}
@ -1056,12 +1191,12 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
if (resizeHeight)
{
// bottom border
if (y >= winrect.bottom - border_width)
if (y >= rect.bottom() - borderWidth)
{
*result = HTBOTTOM;
}
// top border
if (y < winrect.top + border_width)
if (y < rect.top() + borderWidth)
{
*result = HTTOP;
}
@ -1069,26 +1204,26 @@ bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
if (resizeWidth && resizeHeight)
{
// bottom left corner
if (x >= winrect.left && x < winrect.left + border_width &&
y < winrect.bottom && y >= winrect.bottom - border_width)
if (x >= rect.left() && x < rect.left() + borderWidth &&
y < rect.bottom() && y >= rect.bottom() - borderWidth)
{
*result = HTBOTTOMLEFT;
}
// bottom right corner
if (x < winrect.right && x >= winrect.right - border_width &&
y < winrect.bottom && y >= winrect.bottom - border_width)
if (x < rect.right() && x >= rect.right() - borderWidth &&
y < rect.bottom() && y >= rect.bottom() - borderWidth)
{
*result = HTBOTTOMRIGHT;
}
// top left corner
if (x >= winrect.left && x < winrect.left + border_width &&
y >= winrect.top && y < winrect.top + border_width)
if (x >= rect.left() && x < rect.left() + borderWidth &&
y >= rect.top() && y < rect.top() + borderWidth)
{
*result = HTTOPLEFT;
}
// top right corner
if (x < winrect.right && x >= winrect.right - border_width &&
y >= winrect.top && y < winrect.top + border_width)
if (x < rect.right() && x >= rect.right() - borderWidth &&
y >= rect.top() && y < rect.top() + borderWidth)
{
*result = HTTOPRIGHT;
}

View file

@ -75,7 +75,6 @@ public:
bool applyLastBoundsCheck();
float scale() const override;
float qtFontScale() const;
/// @returns true if the window is the top-most window.
/// Either #setTopMost was called or the `TopMost` flag is set which overrides this
@ -132,7 +131,6 @@ private:
void drawCustomWindowFrame(QPainter &painter);
void onFocusLost();
bool handleDPICHANGED(MSG *msg);
bool handleSHOWWINDOW(MSG *msg);
bool handleSIZE(MSG *msg);
bool handleMOVE(MSG *msg);
@ -149,8 +147,6 @@ private:
bool frameless_;
bool shown_ = false;
FlagsEnum<Flags> flags_;
float nativeScale_ = 1;
bool isResizeFixing_ = false;
bool isTopMost_ = false;
struct {
@ -168,6 +164,7 @@ private:
widgets::BoundsChecking lastBoundsCheckMode_ = widgets::BoundsChecking::Off;
#ifdef USEWINSDK
void updateRealSize();
/// @brief Returns the HWND of this window if it has one
///
/// A QWidget only has an HWND if it has been created. Before that,
@ -193,6 +190,10 @@ private:
QTimer useNextBounds_;
bool isNotMinimizedOrMaximized_{};
bool lastEventWasNcMouseMove_ = false;
/// The real bounds of the window as returned by
/// GetWindowRect. Used for drawing.
QRect realBounds_;
bool isMaximized_ = false;
#endif
pajlada::Signals::SignalHolder connections_;

View file

@ -88,23 +88,10 @@ void Label::paintEvent(QPaintEvent *)
{
QPainter painter(this);
qreal deviceDpi =
#ifdef Q_OS_WIN
this->devicePixelRatioF();
#else
1.0;
#endif
QFontMetrics metrics = getIApp()->getFonts()->getFontMetrics(
this->getFontStyle(),
this->scale() * 96.f /
std::max<float>(
0.01F, static_cast<float>(this->logicalDpiX() * deviceDpi)));
painter.setFont(getIApp()->getFonts()->getFont(
this->getFontStyle(),
this->scale() * 96.f /
std::max<float>(
0.02F, static_cast<float>(this->logicalDpiX() * deviceDpi))));
this->getFontStyle(), this->scale());
painter.setFont(
getIApp()->getFonts()->getFont(this->getFontStyle(), this->scale()));
int offset = this->getOffset();

View file

@ -86,6 +86,7 @@ bool TooltipEntryWidget::refreshPixmap()
this->attemptRefresh_ = true;
return false;
}
pixmap->setDevicePixelRatio(this->devicePixelRatio());
if (this->customImgWidth_ > 0 || this->customImgHeight_ > 0)
{

View file

@ -47,7 +47,10 @@ SettingsDialog::SettingsDialog(QWidget *parent)
this->resize(915, 600);
this->themeChangedEvent();
this->scaleChangedEvent(this->scale());
QFile styleFile(":/qss/settings.qss");
styleFile.open(QFile::ReadOnly);
QString stylesheet = QString::fromUtf8(styleFile.readAll());
this->setStyleSheet(stylesheet);
this->initUi();
this->addTabs();
@ -396,25 +399,19 @@ void SettingsDialog::refresh()
void SettingsDialog::scaleChangedEvent(float newDpi)
{
QFile file(":/qss/settings.qss");
file.open(QFile::ReadOnly);
QString styleSheet = QLatin1String(file.readAll());
styleSheet.replace("<font-size>", QString::number(int(14 * newDpi)));
styleSheet.replace("<checkbox-size>", QString::number(int(14 * newDpi)));
assert(newDpi == 1.F &&
"Scaling is disabled for the settings dialog - its scale should "
"always be 1");
for (SettingsDialogTab *tab : this->tabs_)
{
tab->setFixedHeight(int(30 * newDpi));
tab->setFixedHeight(30);
}
this->setStyleSheet(styleSheet);
if (this->ui_.tabContainerContainer)
{
this->ui_.tabContainerContainer->setFixedWidth(int(150 * newDpi));
this->ui_.tabContainerContainer->setFixedWidth(150);
}
this->dpi_ = newDpi;
}
void SettingsDialog::themeChangedEvent()

View file

@ -8,26 +8,44 @@
#include <QPainter>
#include <QScreen>
namespace chatterino {
namespace {
// returns a new resized image or the old one if the size didn't change
auto resizePixmap(const QPixmap &current, const QPixmap resized,
const QSize &size) -> QPixmap
QSizeF deviceIndependentSize(const QPixmap &pixmap)
{
#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0)
return QSizeF(pixmap.width(), pixmap.height()) / pixmap.devicePixelRatio();
#else
return pixmap.deviceIndependentSize();
#endif
}
/**
* 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 (deviceIndependentSize(target) == size)
{
if (resized.size() == size)
{
return resized;
}
else
{
return current.scaled(size, Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
return;
}
QPixmap resized = source;
resized.setDevicePixelRatio(dpr);
target = resized.scaled(size * dpr, Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
} // namespace
namespace chatterino {
Button::Button(BaseWidget *parent)
: BaseWidget(parent)
{
@ -47,6 +65,12 @@ void Button::setMouseEffectColor(std::optional<QColor> color)
void Button::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();
@ -158,8 +182,8 @@ void Button::paintButton(QPainter &painter)
QRect rect = this->rect();
this->resizedPixmap_ =
resizePixmap(this->pixmap_, this->resizedPixmap_, rect.size());
resizePixmap(this->resizedPixmap_, this->pixmap_, rect.size(),
this->devicePixelRatio());
int margin = this->height() < 22 * this->scale() ? 3 : 6;

View file

@ -615,7 +615,7 @@ void ChannelView::scaleChangedEvent(float scale)
if (this->goToBottom_)
{
auto factor = this->qtFontScale();
auto factor = this->scale();
#ifdef Q_OS_MACOS
factor = scale * 80.F /
std::max<float>(
@ -703,8 +703,10 @@ void ChannelView::layoutVisibleMessages(
{
const auto &message = messages[i];
redrawRequired |= message->layout(layoutWidth, this->scale(), flags,
this->bufferInvalidationQueued_);
redrawRequired |= message->layout(
layoutWidth, this->scale(),
this->scale() * static_cast<float>(this->devicePixelRatio()),
flags, this->bufferInvalidationQueued_);
y += message->getHeight();
}
@ -738,7 +740,10 @@ void ChannelView::updateScrollbar(
{
auto *message = messages[i].get();
message->layout(layoutWidth, this->scale(), flags, false);
message->layout(
layoutWidth, this->scale(),
this->scale() * static_cast<float>(this->devicePixelRatio()), flags,
false);
h -= message->getHeight();
@ -1720,9 +1725,11 @@ void ChannelView::wheelEvent(QWheelEvent *event)
}
else
{
snapshot[i - 1]->layout(this->getLayoutWidth(),
this->scale(), this->getFlags(),
false);
snapshot[i - 1]->layout(
this->getLayoutWidth(), this->scale(),
this->scale() *
static_cast<float>(this->devicePixelRatio()),
this->getFlags(), false);
scrollFactor = 1;
currentScrollLeft = snapshot[i - 1]->getHeight();
}
@ -1755,9 +1762,11 @@ void ChannelView::wheelEvent(QWheelEvent *event)
}
else
{
snapshot[i + 1]->layout(this->getLayoutWidth(),
this->scale(), this->getFlags(),
false);
snapshot[i + 1]->layout(
this->getLayoutWidth(), this->scale(),
this->scale() *
static_cast<float>(this->devicePixelRatio()),
this->getFlags(), false);
scrollFactor = 1;
currentScrollLeft = snapshot[i + 1]->getHeight();

View file

@ -27,15 +27,6 @@
namespace chatterino {
namespace {
qreal deviceDpi(QWidget *widget)
{
#ifdef Q_OS_WIN
return widget->devicePixelRatioF();
#else
return 1.0;
#endif
}
// Translates the given rectangle by an amount in the direction to appear like the tab is selected.
// For example, if location is Top, the rectangle will be translated in the negative Y direction,
// or "up" on the screen, by amount.
@ -196,8 +187,8 @@ int NotebookTab::normalTabWidth()
float scale = this->scale();
int width;
auto metrics = getIApp()->getFonts()->getFontMetrics(
FontStyle::UiTabs, float(qreal(this->scale()) * deviceDpi(this)));
QFontMetrics metrics =
getIApp()->getFonts()->getFontMetrics(FontStyle::UiTabs, scale);
if (this->hasXButton())
{
@ -439,11 +430,9 @@ void NotebookTab::paintEvent(QPaintEvent *)
QPainter painter(this);
float scale = this->scale();
auto div = std::max<float>(0.01f, this->logicalDpiX() * deviceDpi(this));
painter.setFont(
getIApp()->getFonts()->getFont(FontStyle::UiTabs, scale * 96.f / div));
painter.setFont(app->getFonts()->getFont(FontStyle::UiTabs, scale));
QFontMetrics metrics =
app->getFonts()->getFontMetrics(FontStyle::UiTabs, scale * 96.f / div);
app->getFonts()->getFontMetrics(FontStyle::UiTabs, scale);
int height = int(scale * NOTEBOOK_TAB_HEIGHT);

View file

@ -63,7 +63,7 @@ public:
builder.append(
std::make_unique<TextElement>(text, MessageElementFlag::Text));
this->layout = std::make_unique<MessageLayout>(builder.release());
this->layout->layout(WIDTH, 1, MessageElementFlag::Text, false);
this->layout->layout(WIDTH, 1, 1, MessageElementFlag::Text, false);
}
MockApplication mockApplication;