lib_ui/ui/platform/ui_platform_window.cpp

525 lines
14 KiB
C++

// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/platform/ui_platform_window.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/platform/ui_platform_utility.h"
#include "ui/widgets/rp_window.h"
#include "ui/widgets/shadow.h"
#include "ui/painter.h"
#include "styles/style_widgets.h"
#include "styles/style_layers.h"
#include "styles/palette.h"
#include <QtCore/QCoreApplication>
#include <QtGui/QWindow>
#include <QtGui/QtEvents>
namespace Ui {
namespace Platform {
namespace {
[[nodiscard]] const style::Shadow &Shadow() {
return st::callShadow;
}
[[nodiscard]] int Radius() {
return st::callRadius;
}
} // namespace
BasicWindowHelper::BasicWindowHelper(not_null<RpWidget*> window)
: _window(window) {
_window->setWindowFlag(Qt::Window);
}
void BasicWindowHelper::initInWindow(not_null<RpWindow*> window) {
}
not_null<RpWidget*> BasicWindowHelper::body() {
return _window;
}
QMargins BasicWindowHelper::frameMargins() {
return nativeFrameMargins();
}
int BasicWindowHelper::additionalContentPadding() const {
return 0;
}
rpl::producer<int> BasicWindowHelper::additionalContentPaddingValue() const {
return rpl::single(0);
}
auto BasicWindowHelper::hitTestRequests() const
-> rpl::producer<not_null<HitTestRequest*>> {
return rpl::never<not_null<HitTestRequest*>>();
}
rpl::producer<HitTestResult> BasicWindowHelper::systemButtonOver() const {
return rpl::never<HitTestResult>();
}
rpl::producer<HitTestResult> BasicWindowHelper::systemButtonDown() const {
return rpl::never<HitTestResult>();
}
void BasicWindowHelper::setTitle(const QString &title) {
_window->setWindowTitle(title);
}
void BasicWindowHelper::setTitleStyle(const style::WindowTitle &st) {
}
void BasicWindowHelper::setNativeFrame(bool enabled) {
}
void BasicWindowHelper::setMinimumSize(QSize size) {
_window->setMinimumSize(size);
}
void BasicWindowHelper::setFixedSize(QSize size) {
_window->setFixedSize(size);
}
void BasicWindowHelper::setStaysOnTop(bool enabled) {
_window->setWindowFlag(Qt::WindowStaysOnTopHint, enabled);
}
void BasicWindowHelper::setGeometry(QRect rect) {
_window->setGeometry(rect);
}
void BasicWindowHelper::showFullScreen() {
_window->showFullScreen();
}
void BasicWindowHelper::showNormal() {
_window->showNormal();
}
void BasicWindowHelper::close() {
_window->close();
}
void BasicWindowHelper::setBodyTitleArea(
Fn<WindowTitleHitTestFlags(QPoint)> testMethod) {
Expects(!_bodyTitleAreaTestMethod || testMethod);
if (!testMethod) {
return;
}
if (!_bodyTitleAreaTestMethod) {
setupBodyTitleAreaEvents();
}
_bodyTitleAreaTestMethod = std::move(testMethod);
}
QMargins BasicWindowHelper::nativeFrameMargins() const {
const auto inner = window()->geometry();
const auto outer = window()->frameGeometry();
return QMargins(
inner.x() - outer.x(),
inner.y() - outer.y(),
outer.x() + outer.width() - inner.x() - inner.width(),
outer.y() + outer.height() - inner.y() - inner.height());
}
void BasicWindowHelper::setupBodyTitleAreaEvents() {
// This is not done on macOS, because startSystemMove
// doesn't work from event handler there.
body()->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
const auto hitTest = [&] {
return bodyTitleAreaHit(
static_cast<QMouseEvent*>(e.get())->pos());
};
if (e->type() == QEvent::MouseButtonDblClick) {
_mousePressed = false;
const auto hit = hitTest();
if (hit & WindowTitleHitTestFlag::Maximize) {
const auto state = _window->windowState();
if (state & Qt::WindowMaximized) {
_window->setWindowState(state & ~Qt::WindowMaximized);
} else {
_window->setWindowState(state | Qt::WindowMaximized);
}
} else if (hit & WindowTitleHitTestFlag::FullScreen) {
if (_window->isFullScreen()) {
showNormal();
} else {
showFullScreen();
}
}
} else if (e->type() == QEvent::MouseButtonRelease) {
_mousePressed = false;
} else if (e->type() == QEvent::MouseButtonPress
&& (static_cast<QMouseEvent*>(e.get())->button()
== Qt::LeftButton)) {
_mousePressed = true;
} else if (e->type() == QEvent::MouseMove) {
if (_mousePressed
#ifndef Q_OS_WIN // We handle fullscreen startSystemMove() only on Windows.
&& !_window->isFullScreen()
#endif // !Q_OS_WIN
&& (hitTest() & WindowTitleHitTestFlag::Move)) {
#ifdef Q_OS_WIN
if (_window->isFullScreen()) {
// On Windows we just jump out of fullscreen
// like we do automatically for dragging a window
// by title bar in a maximized state.
showNormal();
}
#endif // Q_OS_WIN
_mousePressed = false;
_window->windowHandle()->startSystemMove();
}
}
}, body()->lifetime());
}
DefaultWindowHelper::DefaultWindowHelper(not_null<RpWidget*> window)
: BasicWindowHelper(window)
, _title(Ui::CreateChild<DefaultTitleWidget>(window.get()))
, _body(Ui::CreateChild<RpWidget>(window.get()))
, _roundRect(Radius(), st::windowBg) {
init();
}
void DefaultWindowHelper::init() {
if (WindowExtentsSupported()) {
window()->setAttribute(Qt::WA_TranslucentBackground);
}
window()->createWinId();
_title->show();
rpl::combine(
window()->widthValue(),
_windowState.value(),
_title->shownValue(),
TitleControlsLayoutValue()
) | rpl::start_with_next([=](
int width,
Qt::WindowStates windowState,
bool shown,
TitleControls::Layout controlsLayout) {
const auto area = resizeArea();
_title->setGeometry(
area.left(),
area.top(),
width - area.left() - area.right(),
_title->controlsGeometry().height()
? _title->st()->height
: 0);
}, _title->lifetime());
rpl::combine(
window()->sizeValue(),
_windowState.value(),
_title->heightValue(),
_title->shownValue(),
TitleControlsLayoutValue()
) | rpl::start_with_next([=](
QSize size,
Qt::WindowStates windowState,
int titleHeight,
bool titleShown,
TitleControls::Layout controlsLayout) {
const auto area = resizeArea();
const auto sizeWithoutMargins = size
.shrunkBy({ 0, titleShown ? titleHeight : 0, 0, 0 })
.shrunkBy(area);
const auto topLeft = QPoint(
area.left(),
area.top() + (titleShown ? titleHeight : 0));
_body->setGeometry(QRect(topLeft, sizeWithoutMargins));
updateRoundingOverlay();
}, _body->lifetime());
window()->paintRequest(
) | rpl::filter([=] {
return !hasShadow() && !resizeArea().isNull();
}) | rpl::start_with_next([=] {
Painter p(window());
paintBorders(p);
}, window()->lifetime());
rpl::combine(
window()->shownValue(),
_title->shownValue(),
_windowState.value()
) | rpl::start_with_next([=](
bool shown,
bool titleShown,
Qt::WindowStates windowState) {
window()->windowHandle()->setFlag(Qt::FramelessWindowHint, titleShown);
updateWindowExtents();
}, window()->lifetime());
window()->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
const auto mouseEvent = static_cast<QMouseEvent*>(e.get());
const auto currentPoint = mouseEvent->windowPos().toPoint();
const auto edges = edgesFromPos(currentPoint);
if (mouseEvent->button() == Qt::LeftButton && edges) {
window()->windowHandle()->startSystemResize(edges);
}
} else if (e->type() == QEvent::WindowStateChange) {
_windowState = window()->windowState();
}
}, window()->lifetime());
QCoreApplication::instance()->installEventFilter(this);
}
void DefaultWindowHelper::updateRoundingOverlay() {
if (!hasShadow() || resizeArea().isNull()) {
_roundingOverlay.destroy();
return;
} else if (_roundingOverlay) {
return;
}
_roundingOverlay.create(window());
_roundingOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
_roundingOverlay->show();
window()->sizeValue(
) | rpl::start_with_next([=](QSize size) {
_roundingOverlay->setGeometry(QRect(QPoint(), size));
}, _roundingOverlay->lifetime());
_roundingOverlay->paintRequest(
) | rpl::filter([=](QRect clip) {
const auto rect = window()->rect().marginsRemoved(resizeArea());
const auto radius = Radius();
const auto radiusWithFix = radius - 1;
const auto radiusSize = QSize(radius, radius);
return clip.intersects(QRect(
rect.topLeft(),
radiusSize
)) || clip.intersects(QRect(
rect.topRight() - QPoint(radiusWithFix, 0),
radiusSize
)) || clip.intersects(QRect(
rect.bottomLeft() - QPoint(0, radiusWithFix),
radiusSize
)) || clip.intersects(QRect(
rect.bottomRight() - QPoint(radiusWithFix, radiusWithFix),
radiusSize
)) || !rect.contains(clip);
}) | rpl::start_with_next([=] {
Painter p(_roundingOverlay);
const auto rect = window()->rect().marginsRemoved(resizeArea());
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
_roundRect.paint(p, rect, RectPart::AllCorners);
p.setCompositionMode(QPainter::CompositionMode_DestinationOver);
Shadow::paint(p, rect, window()->width(), Shadow());
}, _roundingOverlay->lifetime());
}
not_null<RpWidget*> DefaultWindowHelper::body() {
return _body;
}
QMargins DefaultWindowHelper::frameMargins() {
return _title->isHidden()
? BasicWindowHelper::nativeFrameMargins()
: QMargins{ 0, _title->height(), 0, 0 };
}
bool DefaultWindowHelper::hasShadow() const {
const auto center = window()->geometry().center();
return WindowExtentsSupported() && TranslucentWindowsSupported(center);
}
QMargins DefaultWindowHelper::resizeArea() const {
if (window()->isMaximized()
|| window()->isFullScreen()
|| _title->isHidden()
|| (!hasShadow() && !_title->controlsGeometry().height())) {
return QMargins();
}
return Shadow().extend;
}
Qt::Edges DefaultWindowHelper::edgesFromPos(const QPoint &pos) const {
const auto area = resizeArea();
const auto ignoreHorizontal = (window()->minimumWidth()
== window()->maximumWidth());
const auto ignoreVertical = (window()->minimumHeight()
== window()->maximumHeight());
if (area.isNull()) {
return Qt::Edges();
} else if (!ignoreHorizontal && pos.x() <= area.left()) {
if (!ignoreVertical && pos.y() <= area.top()) {
return Qt::LeftEdge | Qt::TopEdge;
} else if (!ignoreVertical
&& pos.y() >= (window()->height() - area.bottom())) {
return Qt::LeftEdge | Qt::BottomEdge;
}
return Qt::LeftEdge;
} else if (!ignoreHorizontal
&& pos.x() >= (window()->width() - area.right())) {
if (!ignoreVertical && pos.y() <= area.top()) {
return Qt::RightEdge | Qt::TopEdge;
} else if (!ignoreVertical
&& pos.y() >= (window()->height() - area.bottom())) {
return Qt::RightEdge | Qt::BottomEdge;
}
return Qt::RightEdge;
} else if (!ignoreVertical && pos.y() <= area.top()) {
return Qt::TopEdge;
} else if (!ignoreVertical
&& pos.y() >= (window()->height() - area.bottom())) {
return Qt::BottomEdge;
}
return Qt::Edges();
}
bool DefaultWindowHelper::eventFilter(QObject *obj, QEvent *e) {
// doesn't work with RpWidget::events() for some reason
if (e->type() == QEvent::MouseMove
&& obj->isWidgetType()
&& window()->isAncestorOf(static_cast<QWidget*>(obj))) {
const auto mouseEvent = static_cast<QMouseEvent*>(e);
const auto currentPoint = mouseEvent->windowPos().toPoint();
const auto edges = edgesFromPos(currentPoint);
if (mouseEvent->buttons() == Qt::NoButton) {
updateCursor(edges);
}
}
return QObject::eventFilter(obj, e);
}
void DefaultWindowHelper::setTitle(const QString &title) {
_title->setText(title);
window()->setWindowTitle(title);
}
void DefaultWindowHelper::setTitleStyle(const style::WindowTitle &st) {
const auto area = resizeArea();
_title->setStyle(st);
_title->setGeometry(
area.left(),
area.top(),
window()->width() - area.left() - area.right(),
_title->st()->height);
}
void DefaultWindowHelper::setNativeFrame(bool enabled) {
_title->setVisible(!enabled);
}
void DefaultWindowHelper::setMinimumSize(QSize size) {
const auto sizeWithMargins = size.grownBy(bodyPadding());
window()->setMinimumSize(sizeWithMargins);
}
void DefaultWindowHelper::setFixedSize(QSize size) {
const auto sizeWithMargins = size.grownBy(bodyPadding());
window()->setFixedSize(sizeWithMargins);
_title->setResizeEnabled(false);
}
void DefaultWindowHelper::setGeometry(QRect rect) {
window()->setGeometry(rect.marginsAdded(bodyPadding()));
}
void DefaultWindowHelper::paintBorders(QPainter &p) {
const auto titleBackground = window()->isActiveWindow()
? _title->st()->bgActive
: _title->st()->bg;
const auto defaultTitleBackground = window()->isActiveWindow()
? st::defaultWindowTitle.bgActive
: st::defaultWindowTitle.bg;
const auto borderColor = QBrush(titleBackground).isOpaque()
? titleBackground
: defaultTitleBackground;
const auto area = resizeArea();
p.fillRect(
0,
area.top(),
area.left(),
window()->height() - area.top() - area.bottom(),
borderColor);
p.fillRect(
window()->width() - area.right(),
area.top(),
area.right(),
window()->height() - area.top() - area.bottom(),
borderColor);
p.fillRect(
0,
0,
window()->width(),
area.top(),
borderColor);
p.fillRect(
0,
window()->height() - area.bottom(),
window()->width(),
area.bottom(),
borderColor);
}
void DefaultWindowHelper::updateWindowExtents() {
if (hasShadow() && !_title->isHidden()) {
SetWindowExtents(window(), resizeArea());
_extentsSet = true;
} else if (_extentsSet) {
UnsetWindowExtents(window());
_extentsSet = false;
}
}
int DefaultWindowHelper::titleHeight() const {
return _title->isHidden() ? 0 : _title->height();
}
QMargins DefaultWindowHelper::bodyPadding() const {
return resizeArea() + QMargins{ 0, titleHeight(), 0, 0 };
}
void DefaultWindowHelper::updateCursor(Qt::Edges edges) {
if (((edges & Qt::LeftEdge) && (edges & Qt::TopEdge))
|| ((edges & Qt::RightEdge) && (edges & Qt::BottomEdge))) {
window()->setCursor(QCursor(Qt::SizeFDiagCursor));
} else if (((edges & Qt::LeftEdge) && (edges & Qt::BottomEdge))
|| ((edges & Qt::RightEdge) && (edges & Qt::TopEdge))) {
window()->setCursor(QCursor(Qt::SizeBDiagCursor));
} else if ((edges & Qt::LeftEdge) || (edges & Qt::RightEdge)) {
window()->setCursor(QCursor(Qt::SizeHorCursor));
} else if ((edges & Qt::TopEdge) || (edges & Qt::BottomEdge)) {
window()->setCursor(QCursor(Qt::SizeVerCursor));
} else {
window()->setCursor(QCursor(Qt::ArrowCursor));
}
}
} // namespace Platform
} // namespace Ui