Support Windows 11 snap layouts in RpWindow title controls.

This commit is contained in:
John Preston 2022-01-18 12:59:37 +03:00
parent e8f4f02131
commit d7e47aa8a6
8 changed files with 288 additions and 70 deletions

View file

@ -26,7 +26,9 @@ AbstractButton::AbstractButton(QWidget *parent) : RpWidget(parent) {
}
void AbstractButton::leaveEventHook(QEvent *e) {
if (_state & StateFlag::Down) return;
if (_state & StateFlag::Down) {
return;
}
setOver(false, StateChangeSource::ByHover);
return TWidget::leaveEventHook(e);
@ -48,12 +50,13 @@ void AbstractButton::checkIfOver(QPoint localPos) {
void AbstractButton::mousePressEvent(QMouseEvent *e) {
checkIfOver(e->pos());
if (_acceptBoth || (e->buttons() & Qt::LeftButton)) {
if ((_state & StateFlag::Over) && !(_state & StateFlag::Down)) {
auto was = _state;
_state |= StateFlag::Down;
onStateChanged(was, StateChangeSource::ByPress);
if (_state & StateFlag::Over) {
const auto set = setDown(
true,
StateChangeSource::ByPress,
e->modifiers(),
e->button());
if (set) {
e->accept();
}
}
@ -68,21 +71,13 @@ void AbstractButton::mouseMoveEvent(QMouseEvent *e) {
}
void AbstractButton::mouseReleaseEvent(QMouseEvent *e) {
if (_state & StateFlag::Down) {
const auto was = _state;
_state &= ~State(StateFlag::Down);
const auto weak = MakeWeak(this);
onStateChanged(was, StateChangeSource::ByPress);
if (!weak) {
return;
}
if (was & StateFlag::Over) {
clicked(e->modifiers(), e->button());
} else {
setOver(false, StateChangeSource::ByHover);
}
const auto set = setDown(
false,
StateChangeSource::ByPress,
e->modifiers(),
e->button());
if (set) {
e->accept();
}
}
@ -123,6 +118,36 @@ void AbstractButton::setOver(bool over, StateChangeSource source) {
updateCursor();
}
bool AbstractButton::setDown(
bool down,
StateChangeSource source,
Qt::KeyboardModifiers modifiers,
Qt::MouseButton button) {
if (down
&& !(_state & StateFlag::Down)
&& (_acceptBoth || button == Qt::LeftButton)) {
auto was = _state;
_state |= StateFlag::Down;
onStateChanged(was, source);
return true;
} else if (!down && (_state & StateFlag::Down)) {
const auto was = _state;
_state &= ~State(StateFlag::Down);
const auto weak = MakeWeak(this);
onStateChanged(was, source);
if (weak) {
if (was & StateFlag::Over) {
clicked(modifiers, button);
} else {
setOver(false, source);
}
}
return true;
}
return false;
}
void AbstractButton::updateCursor() {
auto pointerCursor = _enablePointerCursor && (_state & StateFlag::Over);
setCursor(pointerCursor ? style::cur_pointer : style::cur_default);

View file

@ -80,6 +80,11 @@ protected:
ByHover = 0x02,
};
void setOver(bool over, StateChangeSource source = StateChangeSource::ByUser);
bool setDown(
bool down,
StateChangeSource source,
Qt::KeyboardModifiers modifiers,
Qt::MouseButton button);
virtual void onStateChanged(State was, StateChangeSource source) {
}

View file

@ -35,6 +35,22 @@ void RemoveDuplicates(std::vector<T> &v) {
} // namespace
class TitleControls::Button final : public IconButton {
public:
using IconButton::IconButton;
void setOver(bool over) {
IconButton::setOver(over, StateChangeSource::ByPress);
}
void setDown(bool down) {
IconButton::setDown(
down,
StateChangeSource::ByPress,
{},
Qt::LeftButton);
}
};
TitleControls::TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
@ -172,7 +188,47 @@ void TitleControls::raise() {
_close->raise();
}
Ui::IconButton *TitleControls::controlWidget(Control control) const {
HitTestResult TitleControls::hitTest(QPoint point) const {
const auto test = [&](const object_ptr<Button> &button) {
return button && button->geometry().contains(point);
};
if (test(_minimize)) {
return HitTestResult::Minimize;
} else if (test(_maximizeRestore)) {
return HitTestResult::MaximizeRestore;
} else if (test(_close)) {
return HitTestResult::Close;
}
return HitTestResult::None;
}
void TitleControls::buttonOver(HitTestResult testResult) {
const auto update = [&](
const object_ptr<Button> &button,
HitTestResult buttonTestResult) {
if (const auto raw = button.data()) {
raw->setOver(testResult == buttonTestResult);
}
};
update(_minimize, HitTestResult::Minimize);
update(_maximizeRestore, HitTestResult::MaximizeRestore);
update(_close, HitTestResult::Close);
}
void TitleControls::buttonDown(HitTestResult testResult, bool down) {
const auto update = [&](
const object_ptr<Button> &button,
HitTestResult buttonTestResult) {
if (const auto raw = button.data()) {
raw->setDown(testResult == buttonTestResult && down);
}
};
update(_minimize, HitTestResult::Minimize);
update(_maximizeRestore, HitTestResult::MaximizeRestore);
update(_close, HitTestResult::Close);
}
TitleControls::Button *TitleControls::controlWidget(Control control) const {
switch (control) {
case Control::Minimize: return _minimize;
case Control::Maximize: return _maximizeRestore;

View file

@ -23,6 +23,23 @@ class PlainShadow;
namespace Platform {
enum class HitTestResult {
None = 0,
Client,
Minimize,
MaximizeRestore,
Close,
Caption,
Top,
TopRight,
Right,
BottomRight,
Bottom,
BottomLeft,
Left,
TopLeft,
};
class TitleControls final {
public:
TitleControls(
@ -36,6 +53,11 @@ public:
void setResizeEnabled(bool enabled);
void raise();
[[nodiscard]] HitTestResult hitTest(QPoint point) const;
void buttonOver(HitTestResult testResult);
void buttonDown(HitTestResult testResult, bool down);
enum class Control {
Unknown,
Minimize,
@ -49,9 +71,11 @@ public:
};
private:
class Button;
[[nodiscard]] not_null<RpWidget*> parent() const;
[[nodiscard]] not_null<QWidget*> window() const;
[[nodiscard]] Ui::IconButton *controlWidget(Control control) const;
[[nodiscard]] Button *controlWidget(Control control) const;
void init(Fn<void(bool maximized)> maximize);
void subscribeToStateChanges();
@ -64,9 +88,9 @@ private:
not_null<const style::WindowTitle*> _st;
object_ptr<Ui::IconButton> _minimize;
object_ptr<Ui::IconButton> _maximizeRestore;
object_ptr<Ui::IconButton> _close;
object_ptr<Button> _minimize;
object_ptr<Button> _maximizeRestore;
object_ptr<Button> _close;
bool _maximizedState = false;
bool _activeState = false;

View file

@ -113,8 +113,9 @@ void TitleWidget::resizeEvent(QResizeEvent *e) {
}
HitTestResult TitleWidget::hitTest(QPoint point) const {
if (_controls.geometry().contains(point)) {
return HitTestResult::SysButton;
const auto titleResult = _controls.hitTest(point);
if (titleResult != HitTestResult::None) {
return titleResult;
} else if (rect().contains(point)) {
return HitTestResult::Caption;
}
@ -125,6 +126,14 @@ bool TitleWidget::additionalPaddingRequired() const {
return _paddingHelper && !isHidden();
}
void TitleWidget::sysButtonOver(HitTestResult testResult) {
_controls.buttonOver(testResult);
}
void TitleWidget::sysButtonDown(HitTestResult testResult, bool down) {
_controls.buttonDown(testResult, down);
}
void TitleWidget::refreshAdditionalPaddings() {
if (!additionalPaddingRequired()) {
return;

View file

@ -26,21 +26,6 @@ class PlainShadow;
namespace Platform {
enum class HitTestResult {
None = 0,
Client,
SysButton,
Caption,
Top,
TopRight,
Right,
BottomRight,
Bottom,
BottomLeft,
Left,
TopLeft,
};
class TitleWidget : public RpWidget {
public:
explicit TitleWidget(not_null<RpWidget*> parent);
@ -58,6 +43,9 @@ public:
HWND handle,
const WINDOWPLACEMENT &placement);
void sysButtonOver(HitTestResult testResult);
void sysButtonDown(HitTestResult testResult, bool down);
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;

View file

@ -321,6 +321,10 @@ bool WindowHelper::handleNativeEvent(
WPARAM wParam,
LPARAM lParam,
LRESULT *result) {
if (handleSysButtonEvent(msg, wParam, lParam, result)) {
return true;
}
switch (msg) {
case WM_ACTIVATE: {
@ -392,6 +396,14 @@ bool WindowHelper::handleNativeEvent(
if (result) *result = 0;
} return true;
case WM_NCRBUTTONUP: {
if (_title->isHidden()) {
return false;
}
SendMessage(_handle, WM_SYSCOMMAND, SC_MOUSEMENU, lParam);
if (result) *result = 0;
} return true;
case WM_NCACTIVATE: {
if (_title->isHidden()) {
return false;
@ -483,33 +495,33 @@ bool WindowHelper::handleNativeEvent(
const auto mapped = QPoint(
p.x - r.left + _marginsDelta.left(),
p.y - r.top + _marginsDelta.top());
if (!window()->rect().contains(mapped)) {
*result = HTTRANSPARENT;
} else if (_title->isHidden()
|| !_title->geometry().contains(mapped)) {
*result = HTCLIENT;
} else switch (_title->hitTest(_title->pos() + mapped)) {
case HitTestResult::Client:
case HitTestResult::SysButton: *result = HTCLIENT; break;
case HitTestResult::Caption: *result = HTCAPTION; break;
case HitTestResult::Top: *result = HTTOP; break;
case HitTestResult::TopRight: *result = HTTOPRIGHT; break;
case HitTestResult::Right: *result = HTRIGHT; break;
case HitTestResult::BottomRight: *result = HTBOTTOMRIGHT; break;
case HitTestResult::Bottom: *result = HTBOTTOM; break;
case HitTestResult::BottomLeft: *result = HTBOTTOMLEFT; break;
case HitTestResult::Left: *result = HTLEFT; break;
case HitTestResult::TopLeft: *result = HTTOPLEFT; break;
case HitTestResult::None:
default: *result = HTTRANSPARENT; break;
};
} return true;
case WM_NCRBUTTONUP: {
if (_title->isHidden()) {
return false;
*result = [&] {
if (!window()->rect().contains(mapped)) {
return HTTRANSPARENT;
} else if (_title->isHidden()
|| !_title->geometry().contains(mapped)) {
return HTCLIENT;
} else switch (const auto test = _title->hitTest(mapped)) {
case HitTestResult::Client: return HTCLIENT;
case HitTestResult::Minimize:
case HitTestResult::MaximizeRestore:
case HitTestResult::Close: return sysButtonHitTest(test);
case HitTestResult::Caption: return HTCAPTION;
case HitTestResult::Top: return HTTOP;
case HitTestResult::TopRight: return HTTOPRIGHT;
case HitTestResult::Right: return HTRIGHT;
case HitTestResult::BottomRight: return HTBOTTOMRIGHT;
case HitTestResult::Bottom: return HTBOTTOM;
case HitTestResult::BottomLeft: return HTBOTTOMLEFT;
case HitTestResult::Left: return HTLEFT;
case HitTestResult::TopLeft: return HTTOPLEFT;
case HitTestResult::None:
default: return HTTRANSPARENT;
};
}();
if (complexSysButtonProcessing()) {
_title->sysButtonOver(sysButtonHitTest(*result));
}
SendMessage(_handle, WM_SYSCOMMAND, SC_MOUSEMENU, lParam);
} return true;
case WM_SYSCOMMAND: {
@ -559,6 +571,96 @@ bool WindowHelper::fixedSize() const {
return window()->minimumSize() == window()->maximumSize();
}
bool WindowHelper::handleSysButtonEvent(
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result) {
if (!complexSysButtonProcessing()) {
return false;
}
const auto testResult = LOWORD(wParam);
const auto sysButtons = { HTMINBUTTON, HTMAXBUTTON, HTCLOSE };
const auto overSysButton = ranges::contains(sysButtons, testResult);
switch (msg) {
case WM_NCLBUTTONDBLCLK:
case WM_NCMBUTTONDBLCLK:
case WM_NCRBUTTONDBLCLK:
case WM_NCXBUTTONDBLCLK: {
if (!overSysButton || fixedSize()) {
return false;
}
// Ignore double clicks on system buttons.
if (result) *result = 0;
} return true;
case WM_NCLBUTTONDOWN:
case WM_NCLBUTTONUP:
_title->sysButtonDown(
sysButtonHitTest(testResult),
(msg == WM_NCLBUTTONDOWN));
if (overSysButton) {
if (result) *result = 0;
}
return overSysButton;
case WM_NCMBUTTONDOWN:
case WM_NCMBUTTONUP:
case WM_NCRBUTTONDOWN:
case WM_NCRBUTTONUP:
case WM_NCXBUTTONDOWN:
case WM_NCXBUTTONUP:
if (!overSysButton) {
return false;
}
if (result) *result = 0;
return true;
case WM_NCMOUSEHOVER:
case WM_NCMOUSEMOVE:
_title->sysButtonOver(sysButtonHitTest(testResult));
if (overSysButton) {
if (result) *result = 0;
}
return overSysButton;
case WM_NCMOUSELEAVE:
_title->sysButtonOver(HitTestResult::None);
return false;
}
return false;
}
int WindowHelper::sysButtonHitTest(HitTestResult result) const {
if (!complexSysButtonProcessing()) {
return HTCLIENT;
}
switch (result) {
case HitTestResult::Minimize: return HTMINBUTTON;
case HitTestResult::MaximizeRestore: return HTMAXBUTTON;
case HitTestResult::Close: return HTCLOSE;
}
return HTTRANSPARENT;
}
HitTestResult WindowHelper::sysButtonHitTest(int result) const {
if (!complexSysButtonProcessing()) {
return HitTestResult::Client;
}
switch (result) {
case HTMINBUTTON: return HitTestResult::Minimize;
case HTMAXBUTTON: return HitTestResult::MaximizeRestore;
case HTCLOSE: return HitTestResult::Close;
}
return HitTestResult::None;
}
bool WindowHelper::complexSysButtonProcessing() const {
if (_title->isHidden()) {
return false;
} else if (!::Platform::IsWindows11OrGreater()) {
return false;
}
return true;
}
int WindowHelper::titleHeight() const {
return _title->isHidden() ? 0 : _title->height();
}

View file

@ -15,6 +15,7 @@ namespace Ui {
namespace Platform {
class TitleWidget;
enum class HitTestResult;
class WindowHelper final : public BasicWindowHelper {
public:
@ -49,7 +50,15 @@ private:
WPARAM wParam,
LPARAM lParam,
LRESULT *result);
[[nodiscard]] bool handleSysButtonEvent(
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result);
[[nodiscard]] bool fixedSize() const;
[[nodiscard]] bool complexSysButtonProcessing() const;
[[nodiscard]] int sysButtonHitTest(HitTestResult result) const;
[[nodiscard]] HitTestResult sysButtonHitTest(int result) const;
[[nodiscard]] int titleHeight() const;
static not_null<NativeFilter*> GetNativeFilter();