init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,616 @@
// 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/linux/ui_utility_linux.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_linux_library.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xcb_utilities.h"
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#include <QtCore/QPoint>
#include <QtGui/QWindow>
#include <QtWidgets/QApplication>
#include <qpa/qplatformwindow.h>
#include <qpa/qplatformwindow_p.h>
extern "C" {
typedef int32_t wl_fixed_t;
struct wl_object;
struct wl_array;
struct wl_proxy;
struct xdg_toplevel;
union wl_argument {
int32_t i; /**< `int` */
uint32_t u; /**< `uint` */
wl_fixed_t f; /**< `fixed` */
const char *s; /**< `string` */
struct wl_object *o; /**< `object` */
uint32_t n; /**< `new_id` */
struct wl_array *a; /**< `array` */
int32_t h; /**< `fd` */
};
}
namespace Ui {
namespace Platform {
namespace {
static const auto kXCBFrameExtentsAtomName = u"_GTK_FRAME_EXTENTS"_q;
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
std::optional<bool> XCBWindowMapped(xcb_window_t window) {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return std::nullopt;
}
const auto cookie = xcb_get_window_attributes(connection, window);
const auto reply = base::Platform::XCB::MakeReplyPointer(
xcb_get_window_attributes_reply(
connection,
cookie,
nullptr));
if (!reply) {
return std::nullopt;
}
return reply->map_state == XCB_MAP_STATE_VIEWABLE;
}
std::optional<bool> XCBWindowHidden(xcb_window_t window) {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return std::nullopt;
}
const auto stateAtom = base::Platform::XCB::GetAtom(
connection,
"_NET_WM_STATE");
const auto stateHiddenAtom = base::Platform::XCB::GetAtom(
connection,
"_NET_WM_STATE_HIDDEN");
if (!stateAtom || !stateHiddenAtom) {
return std::nullopt;
}
const auto cookie = xcb_get_property(
connection,
false,
window,
stateAtom,
XCB_ATOM_ATOM,
0,
1024);
const auto reply = base::Platform::XCB::MakeReplyPointer(
xcb_get_property_reply(
connection,
cookie,
nullptr));
if (!reply) {
return std::nullopt;
}
if (reply->type != XCB_ATOM_ATOM || reply->format != 32) {
return std::nullopt;
}
const auto atomsStart = reinterpret_cast<xcb_atom_t*>(
xcb_get_property_value(reply.get()));
const auto states = std::vector<xcb_atom_t>(
atomsStart,
atomsStart + reply->length);
return ranges::contains(states, stateHiddenAtom);
}
QRect XCBWindowGeometry(xcb_window_t window) {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return {};
}
const auto cookie = xcb_get_geometry(connection, window);
const auto reply = base::Platform::XCB::MakeReplyPointer(
xcb_get_geometry_reply(
connection,
cookie,
nullptr));
if (!reply) {
return {};
}
return QRect(reply->x, reply->y, reply->width, reply->height);
}
std::optional<uint> XCBCurrentWorkspace() {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return std::nullopt;
}
const auto root = base::Platform::XCB::GetRootWindow(connection);
if (!root) {
return std::nullopt;
}
const auto currentDesktopAtom = base::Platform::XCB::GetAtom(
connection,
"_NET_CURRENT_DESKTOP");
if (!currentDesktopAtom) {
return std::nullopt;
}
const auto cookie = xcb_get_property(
connection,
false,
root,
currentDesktopAtom,
XCB_ATOM_CARDINAL,
0,
1024);
const auto reply = base::Platform::XCB::MakeReplyPointer(
xcb_get_property_reply(
connection,
cookie,
nullptr));
if (!reply) {
return std::nullopt;
}
return (reply->type == XCB_ATOM_CARDINAL)
? std::make_optional(
*reinterpret_cast<ulong*>(xcb_get_property_value(reply.get())))
: std::nullopt;
}
std::optional<uint> XCBWindowWorkspace(xcb_window_t window) {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return std::nullopt;
}
const auto desktopAtom = base::Platform::XCB::GetAtom(
connection,
"_NET_WM_DESKTOP");
if (!desktopAtom) {
return std::nullopt;
}
const auto cookie = xcb_get_property(
connection,
false,
window,
desktopAtom,
XCB_ATOM_CARDINAL,
0,
1024);
const auto reply = base::Platform::XCB::MakeReplyPointer(
xcb_get_property_reply(
connection,
cookie,
nullptr));
if (!reply) {
return std::nullopt;
}
return (reply->type == XCB_ATOM_CARDINAL)
? std::make_optional(
*reinterpret_cast<ulong*>(xcb_get_property_value(reply.get())))
: std::nullopt;
}
std::optional<bool> XCBIsOverlapped(
not_null<QWidget*> widget,
const QRect &rect) {
const auto window = widget->winId();
Expects(window != XCB_NONE);
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return std::nullopt;
}
const auto root = base::Platform::XCB::GetRootWindow(connection);
if (!root) {
return std::nullopt;
}
const auto windowWorkspace = XCBWindowWorkspace(window);
const auto currentWorkspace = XCBCurrentWorkspace();
if (windowWorkspace.has_value()
&& currentWorkspace.has_value()
&& *windowWorkspace != *currentWorkspace
&& *windowWorkspace != 0xFFFFFFFF) {
return true;
}
const auto windowGeometry = XCBWindowGeometry(window);
if (windowGeometry.isNull()) {
return std::nullopt;
}
const auto mappedRect = QRect(
rect.topLeft()
* widget->windowHandle()->devicePixelRatio()
+ windowGeometry.topLeft(),
rect.size() * widget->windowHandle()->devicePixelRatio());
const auto cookie = xcb_query_tree(connection, root);
const auto reply = base::Platform::XCB::MakeReplyPointer(
xcb_query_tree_reply(connection, cookie, nullptr));
if (!reply) {
return std::nullopt;
}
const auto tree = xcb_query_tree_children(reply.get());
auto aboveTheWindow = false;
for (auto i = 0, l = xcb_query_tree_children_length(reply.get()); i < l; ++i) {
if (window == tree[i]) {
aboveTheWindow = true;
continue;
}
if (!aboveTheWindow) {
continue;
}
const auto geometry = XCBWindowGeometry(tree[i]);
if (!mappedRect.intersects(geometry)) {
continue;
}
const auto workspace = XCBWindowWorkspace(tree[i]);
if (workspace.has_value()
&& windowWorkspace.has_value()
&& *workspace != *windowWorkspace
&& *workspace != 0xFFFFFFFF) {
continue;
}
const auto mapped = XCBWindowMapped(tree[i]);
if (mapped.has_value() && !*mapped) {
continue;
}
const auto hidden = XCBWindowHidden(tree[i]);
if (hidden.has_value() && *hidden) {
continue;
}
return true;
}
return false;
}
void SetXCBFrameExtents(not_null<QWidget*> widget, const QMargins &extents) {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return;
}
const auto frameExtentsAtom = base::Platform::XCB::GetAtom(
connection,
kXCBFrameExtentsAtomName);
if (!frameExtentsAtom) {
return;
}
if (extents.isNull()) {
free(
xcb_request_check(
connection,
xcb_delete_property_checked(
connection,
widget->winId(),
frameExtentsAtom)));
return;
}
const auto nativeExtents = extents
* widget->windowHandle()->devicePixelRatio();
const auto extentsVector = std::vector<uint>{
uint(nativeExtents.left()),
uint(nativeExtents.right()),
uint(nativeExtents.top()),
uint(nativeExtents.bottom()),
};
free(
xcb_request_check(
connection,
xcb_change_property_checked(
connection,
XCB_PROP_MODE_REPLACE,
widget->winId(),
frameExtentsAtom,
XCB_ATOM_CARDINAL,
32,
extentsVector.size(),
extentsVector.data())));
}
void ShowXCBWindowMenu(not_null<QWidget*> widget, const QPoint &point) {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return;
}
const auto root = base::Platform::XCB::GetRootWindow(connection);
if (!root) {
return;
}
const auto showWindowMenuAtom = base::Platform::XCB::GetAtom(
connection,
"_GTK_SHOW_WINDOW_MENU");
if (!showWindowMenuAtom) {
return;
}
const auto windowGeometry = XCBWindowGeometry(widget->winId());
if (windowGeometry.isNull()) {
return;
}
const auto globalPos = point
* widget->windowHandle()->devicePixelRatio()
+ windowGeometry.topLeft();
xcb_client_message_event_t xev;
xev.response_type = XCB_CLIENT_MESSAGE;
xev.type = showWindowMenuAtom;
xev.sequence = 0;
xev.window = widget->winId();
xev.format = 32;
xev.data.data32[0] = 0;
xev.data.data32[1] = globalPos.x();
xev.data.data32[2] = globalPos.y();
xev.data.data32[3] = 0;
xev.data.data32[4] = 0;
free(
xcb_request_check(
connection,
xcb_ungrab_pointer_checked(connection, XCB_CURRENT_TIME)));
free(
xcb_request_check(
connection,
xcb_send_event_checked(
connection,
false,
root,
XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT
| XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
reinterpret_cast<const char*>(&xev))));
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
#if defined QT_FEATURE_wayland && QT_CONFIG(wayland)
void ShowWaylandWindowMenu(not_null<QWidget*> widget, const QPoint &point) {
static const auto wl_proxy_marshal_array = [] {
void (*result)(
struct wl_proxy *p,
uint32_t opcode,
union wl_argument *args) = nullptr;
if (const auto lib = base::Platform::LoadLibrary(
"libwayland-client.so.0",
RTLD_NODELETE)) {
base::Platform::LoadSymbol(lib, "wl_proxy_marshal_array", result);
}
return result;
}();
if (!wl_proxy_marshal_array) {
return;
}
using namespace QNativeInterface;
using namespace QNativeInterface::Private;
const auto window = not_null(widget->windowHandle());
const auto native = qApp->nativeInterface<QWaylandApplication>();
const auto nativeWindow = window->nativeInterface<QWaylandWindow>();
if (!native || !nativeWindow) {
return;
}
const auto toplevel = nativeWindow->surfaceRole<xdg_toplevel>();
const auto seat = native->lastInputSeat();
if (!toplevel || !seat) {
return;
}
const auto pos = point
* window->devicePixelRatio()
/ window->handle()->devicePixelRatio();
wl_proxy_marshal_array(
reinterpret_cast<wl_proxy*>(toplevel),
4, // XDG_TOPLEVEL_SHOW_WINDOW_MENU
std::array{
wl_argument{ .o = reinterpret_cast<wl_object*>(seat) },
wl_argument{ .u = native->lastInputSerial() },
wl_argument{ .i = pos.x() },
wl_argument{ .i = pos.y() },
}.data());
}
#endif // wayland
} // namespace
bool IsApplicationActive() {
return QApplication::activeWindow() != nullptr;
}
bool TranslucentWindowsSupported() {
if (::Platform::IsWayland()) {
return true;
}
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (::Platform::IsX11()) {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return false;
}
const auto atom = base::Platform::XCB::GetAtom(
connection,
"_NET_WM_CM_S0");
if (!atom) {
return false;
}
const auto cookie = xcb_get_selection_owner(connection, atom);
const auto result = base::Platform::XCB::MakeReplyPointer(
xcb_get_selection_owner_reply(
connection,
cookie,
nullptr));
if (!result) {
return false;
}
return result->owner;
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return false;
}
void IgnoreAllActivation(not_null<QWidget*> widget) {
}
void ClearTransientParent(not_null<QWidget*> widget) {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (::Platform::IsX11()) {
const base::Platform::XCB::Connection connection;
if (!connection || xcb_connection_has_error(connection)) {
return;
}
free(
xcb_request_check(
connection,
xcb_delete_property_checked(
connection,
widget->winId(),
XCB_ATOM_WM_TRANSIENT_FOR)));
return;
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
}
std::optional<bool> IsOverlapped(
not_null<QWidget*> widget,
const QRect &rect) {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (::Platform::IsX11()) {
return XCBIsOverlapped(widget, rect);
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return std::nullopt;
}
bool WindowMarginsSupported() {
#if defined QT_FEATURE_wayland && QT_CONFIG(wayland)
static const auto WaylandResult = [] {
using namespace QNativeInterface::Private;
QWindow window;
window.create();
return bool(window.nativeInterface<QWaylandWindow>());
}();
if (WaylandResult) {
return true;
}
#endif // wayland
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
namespace XCB = base::Platform::XCB;
if (::Platform::IsX11()
&& XCB::IsSupportedByWM(
XCB::Connection(),
kXCBFrameExtentsAtomName)) {
return true;
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return false;
}
void SetWindowMargins(not_null<QWidget*> widget, const QMargins &margins) {
#if defined QT_FEATURE_wayland && QT_CONFIG(wayland)
using namespace QNativeInterface::Private;
const auto window = not_null(widget->windowHandle());
const auto platformWindow = not_null(window->handle());
if (const auto native = window->nativeInterface<QWaylandWindow>()) {
native->setCustomMargins(
margins
* window->devicePixelRatio()
/ platformWindow->devicePixelRatio());
return;
}
#endif // wayland
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (::Platform::IsX11()) {
SetXCBFrameExtents(widget, margins);
return;
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
}
void ShowWindowMenu(not_null<QWidget*> widget, const QPoint &point) {
#if defined QT_FEATURE_wayland && QT_CONFIG(wayland)
if (::Platform::IsWayland()) {
ShowWaylandWindowMenu(widget, point);
return;
}
#endif // wayland
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
if (::Platform::IsX11()) {
ShowXCBWindowMenu(widget, point);
return;
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
}
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,43 @@
// 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
//
#pragma once
#include "ui/platform/ui_platform_utility.h"
class QPainter;
class QPaintEvent;
namespace Ui {
namespace Platform {
inline void InitOnTopPanel(not_null<QWidget*> panel) {
}
inline void DeInitOnTopPanel(not_null<QWidget*> panel) {
}
inline void ReInitOnTopPanel(not_null<QWidget*> panel) {
}
inline void ShowOverAll(not_null<QWidget*> widget, bool canFocus) {
}
inline void AcceptAllMouseInput(not_null<QWidget*> widget) {
}
inline void DisableSystemWindowResize(not_null<QWidget*> widget, QSize ratio) {
}
inline constexpr bool UseMainQueueGeneric() {
return true;
}
inline void FixPopupMenuNativeEmojiPopup(not_null<PopupMenu*> menu) {
}
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,25 @@
// 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/linux/ui_window_linux.h"
namespace Ui::Platform {
std::unique_ptr<BasicWindowHelper> CreateSpecialWindowHelper(
not_null<RpWidget*> window) {
return nullptr;
}
bool NativeWindowFrameSupported() {
return true;
}
rpl::producer<FullScreenEvent> FullScreenEvents(
not_null<RpWidget*> window) {
return rpl::never<FullScreenEvent>();
}
} // namespace Ui::Platform

View File

@@ -0,0 +1,9 @@
// 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
//
#pragma once
#include "ui/platform/ui_platform_window.h"

View File

@@ -0,0 +1,143 @@
// 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/linux/ui_window_title_linux.h"
#include "base/platform/linux/base_linux_xdp_utilities.h"
#include "base/integration.h"
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
#include "base/platform/linux/base_linux_xsettings.h"
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
namespace Ui {
namespace Platform {
namespace {
class TitleControlsLayoutImpl : public TitleControlsLayout {
public:
TitleControlsLayoutImpl();
private:
[[nodiscard]] static TitleControls::Layout Get();
const rpl::lifetime _lifetime;
const base::Platform::XDP::SettingWatcher _settingWatcher;
};
TitleControlsLayoutImpl::TitleControlsLayoutImpl()
: TitleControlsLayout(Get())
, _lifetime([&] {
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
using base::Platform::XCB::XSettings;
if (const auto xSettings = XSettings::Instance()) {
return xSettings->registerCallbackForProperty(
"Gtk/DecorationLayout",
[=](xcb_connection_t *, const QByteArray &, const QVariant &) {
base::Integration::Instance().enterFromEventLoop([&] {
_variable = Get();
});
});
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
return rpl::lifetime();
}())
, _settingWatcher("org.gnome.desktop.wm.preferences", "button-layout", [=] {
base::Integration::Instance().enterFromEventLoop([&] {
_variable = Get();
});
}) {}
TitleControls::Layout TitleControlsLayoutImpl::Get() {
const auto convert = [](const QString &keywords) {
const auto toControl = [](const QString &keyword) {
if (keyword == qstr("minimize")) {
return TitleControls::Control::Minimize;
} else if (keyword == qstr("maximize")) {
return TitleControls::Control::Maximize;
} else if (keyword == qstr("close")) {
return TitleControls::Control::Close;
}
return TitleControls::Control::Unknown;
};
TitleControls::Layout result;
const auto splitted = keywords.split(':');
ranges::transform(
splitted[0].split(','),
ranges::back_inserter(result.left),
toControl);
if (splitted.size() > 1) {
ranges::transform(
splitted[1].split(','),
ranges::back_inserter(result.right),
toControl);
}
return result;
};
#ifndef DESKTOP_APP_DISABLE_X11_INTEGRATION
const auto xSettingsResult = [&]()
-> std::optional<TitleControls::Layout> {
using base::Platform::XCB::XSettings;
const auto xSettings = XSettings::Instance();
if (!xSettings) {
return std::nullopt;
}
const auto decorationLayout = xSettings->setting(
"Gtk/DecorationLayout");
if (!decorationLayout.isValid()) {
return std::nullopt;
}
return convert(decorationLayout.toString());
}();
if (xSettingsResult.has_value()) {
return *xSettingsResult;
}
#endif // !DESKTOP_APP_DISABLE_X11_INTEGRATION
const auto portalResult = [&]() -> std::optional<TitleControls::Layout> {
auto decorationLayout = base::Platform::XDP::ReadSetting(
"org.gnome.desktop.wm.preferences",
"button-layout");
if (!decorationLayout.has_value()) {
return std::nullopt;
}
return convert(
QString::fromStdString(decorationLayout->get_string(nullptr)));
}();
if (portalResult.has_value()) {
return *portalResult;
}
return TitleControls::Layout{
.right = {
TitleControls::Control::Minimize,
TitleControls::Control::Maximize,
TitleControls::Control::Close,
}
};
}
} // namespace
std::shared_ptr<TitleControlsLayout> TitleControlsLayout::Create() {
return std::make_shared<TitleControlsLayoutImpl>();
}
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,9 @@
// 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
//
#pragma once
#include "ui/platform/ui_platform_window_title.h"

View File

@@ -0,0 +1,41 @@
// 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
//
#pragma once
#include "ui/platform/ui_platform_utility.h"
#include "base/platform/base_platform_info.h"
#include <QtCore/QPoint>
namespace Ui {
namespace Platform {
inline bool TranslucentWindowsSupported() {
return true;
}
inline void ClearTransientParent(not_null<QWidget*> widget) {
}
inline constexpr bool UseMainQueueGeneric() {
return ::Platform::IsMacStoreBuild();
}
inline bool WindowMarginsSupported() {
return false;
}
inline void SetWindowMargins(not_null<QWidget*> widget, const QMargins &margins) {
}
inline void ShowWindowMenu(not_null<QWidget*> widget, const QPoint &point) {
}
inline void FixPopupMenuNativeEmojiPopup(not_null<PopupMenu*> menu) {
}
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,168 @@
// 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/mac/ui_utility_mac.h"
#include "ui/integration.h"
#include <QtGui/QPainter>
#include <QtGui/QtEvents>
#include <QtGui/QWindow>
#include <Cocoa/Cocoa.h>
#ifndef OS_MAC_STORE
extern "C" {
void _dispatch_main_queue_callback_4CF(mach_msg_header_t *msg);
} // extern "C"
#endif // OS_MAC_STORE
namespace Ui {
namespace Platform {
bool IsApplicationActive() {
return [[NSApplication sharedApplication] isActive];
}
void InitOnTopPanel(not_null<QWidget*> panel) {
Expects(!panel->windowHandle());
// Force creating windowHandle() without creating the platform window yet.
panel->setAttribute(Qt::WA_NativeWindow, true);
panel->windowHandle()->setProperty("_td_macNonactivatingPanelMask", QVariant(true));
panel->setAttribute(Qt::WA_NativeWindow, false);
panel->createWinId();
auto platformWindow = [reinterpret_cast<NSView*>(panel->winId()) window];
Assert([platformWindow isKindOfClass:[NSPanel class]]);
auto platformPanel = static_cast<NSPanel*>(platformWindow);
[platformPanel setBackgroundColor:[NSColor clearColor]];
[platformPanel setLevel:NSModalPanelWindowLevel];
[platformPanel setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces|NSWindowCollectionBehaviorStationary|NSWindowCollectionBehaviorFullScreenAuxiliary|NSWindowCollectionBehaviorIgnoresCycle];
[platformPanel setHidesOnDeactivate:NO];
//[platformPanel setFloatingPanel:YES];
Integration::Instance().activationFromTopPanel();
}
void DeInitOnTopPanel(not_null<QWidget*> panel) {
auto platformWindow = [reinterpret_cast<NSView*>(panel->winId()) window];
Assert([platformWindow isKindOfClass:[NSPanel class]]);
auto platformPanel = static_cast<NSPanel*>(platformWindow);
auto newBehavior = ([platformPanel collectionBehavior] & (~NSWindowCollectionBehaviorCanJoinAllSpaces)) | NSWindowCollectionBehaviorMoveToActiveSpace;
[platformPanel setCollectionBehavior:newBehavior];
}
void ReInitOnTopPanel(not_null<QWidget*> panel) {
auto platformWindow = [reinterpret_cast<NSView*>(panel->winId()) window];
Assert([platformWindow isKindOfClass:[NSPanel class]]);
auto platformPanel = static_cast<NSPanel*>(platformWindow);
auto newBehavior = ([platformPanel collectionBehavior] & (~NSWindowCollectionBehaviorMoveToActiveSpace)) | NSWindowCollectionBehaviorCanJoinAllSpaces;
[platformPanel setCollectionBehavior:newBehavior];
}
void ShowOverAll(not_null<QWidget*> widget, bool canFocus) {
NSWindow *wnd = [reinterpret_cast<NSView*>(widget->winId()) window];
[wnd setLevel:NSPopUpMenuWindowLevel];
if (!canFocus) {
[wnd setStyleMask:NSWindowStyleMaskUtilityWindow | NSWindowStyleMaskNonactivatingPanel];
[wnd setCollectionBehavior:NSWindowCollectionBehaviorMoveToActiveSpace|NSWindowCollectionBehaviorStationary|NSWindowCollectionBehaviorFullScreenAuxiliary|NSWindowCollectionBehaviorIgnoresCycle];
}
}
void AcceptAllMouseInput(not_null<QWidget*> widget) {
// https://github.com/telegramdesktop/tdesktop/issues/27025
//
// By default system clicks through fully transparent pixels,
// and starting with macOS 14.1 it counts the transparency
// incorrectly (as if `y` is mirrored), so when clicking
// on a reactions strip outside of the menu column the click
// is ignored and made on the underlying window, because at the
// bottom of the menu in the same place there is nothing, empty.
//
// We explicitly request all the input to disable this behavior.
//
// See https://stackoverflow.com/a/29451199 and comments.
NSWindow *window = [reinterpret_cast<NSView*>(widget->winId()) window];
[window setIgnoresMouseEvents:NO];
}
void DrainMainQueue() {
#ifndef OS_MAC_STORE
_dispatch_main_queue_callback_4CF(nullptr);
#endif // OS_MAC_STORE
}
void IgnoreAllActivation(not_null<QWidget*> widget) {
}
void DisableSystemWindowResize(not_null<QWidget*> widget, QSize ratio) {
const auto winId = widget->winId();
if (const auto view = reinterpret_cast<NSView*>(winId)) {
if (const auto window = [view window]) {
window.styleMask &= ~NSWindowStyleMaskResizable;
}
}
}
std::optional<bool> IsOverlapped(
not_null<QWidget*> widget,
const QRect &rect) {
NSWindow *window = [reinterpret_cast<NSView*>(widget->winId()) window];
Assert(window != nullptr);
if (![window isOnActiveSpace]) {
return true;
}
const auto nativeRect = QRect(
widget->mapToGlobal(rect.topLeft()),
rect.size()).toCGRect();
CGWindowID windowId = (CGWindowID)[window windowNumber];
const CGWindowListOption options = kCGWindowListExcludeDesktopElements
| kCGWindowListOptionOnScreenAboveWindow;
CFArrayRef windows = CGWindowListCopyWindowInfo(options, windowId);
if (!windows) {
return std::nullopt;
}
const auto guard = gsl::finally([&] {
CFRelease(windows);
});
NSMutableArray *list = (__bridge NSMutableArray*)windows;
for (NSDictionary *window in list) {
NSNumber *alphaValue = [window objectForKey:@"kCGWindowAlpha"];
const auto alpha = alphaValue ? [alphaValue doubleValue] : 1.;
if (alpha == 0.) {
continue;
}
NSString *owner = [window objectForKey:@"kCGWindowOwnerName"];
NSNumber *layerValue = [window objectForKey:@"kCGWindowLayer"];
const auto layer = layerValue ? [layerValue intValue] : 0;
if (owner && [owner isEqualToString:@"Dock"] && layer == 20) {
// It is always full screen.
continue;
}
CFDictionaryRef bounds = (__bridge CFDictionaryRef)[window objectForKey:@"kCGWindowBounds"];
if (!bounds) {
continue;
}
CGRect rect;
if (!CGRectMakeWithDictionaryRepresentation(bounds, &rect)) {
continue;
} else if (CGRectIntersectsRect(rect, nativeRect)) {
return true;
}
}
return false;
}
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,48 @@
// 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
//
#pragma once
#include "ui/platform/ui_platform_window.h"
namespace Ui::Platform {
class TitleWidget;
class WindowHelper final : public BasicWindowHelper {
public:
explicit WindowHelper(not_null<RpWidget*> window);
~WindowHelper();
not_null<RpWidget*> body() override;
QMargins frameMargins() override;
void setTitle(const QString &title) override;
void setTitleStyle(const style::WindowTitle &st) override;
void setMinimumSize(QSize size) override;
void setFixedSize(QSize size) override;
void setStaysOnTop(bool enabled) override;
void setGeometry(QRect rect) override;
void close() override;
const style::TextStyle &titleTextStyle() const override;
private:
class Private;
friend class Private;
void setupBodyTitleAreaEvents() override;
void init();
void updateCustomTitleVisibility(bool force = false);
const std::unique_ptr<Private> _private;
const not_null<TitleWidget*> _title;
const not_null<RpWidget*> _body;
bool _titleVisible = true;
};
} // namespace Ui::Platform

View File

@@ -0,0 +1,535 @@
// 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/mac/ui_window_mac.h"
#include "ui/platform/mac/ui_window_title_mac.h"
#include "ui/widgets/rp_window.h"
#include "ui/qt_object_factory.h"
#include "ui/ui_utility.h"
#include "base/qt/qt_common_adapters.h"
#include "base/platform/base_platform_info.h"
#include "styles/palette.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QAbstractNativeEventFilter>
#include <QtGui/QWindow>
#include <QtGui/QtEvents>
#include <QOpenGLWidget>
#include <Cocoa/Cocoa.h>
using FullScreenEvent = Ui::Platform::FullScreenEvent;
@interface WindowObserver : NSObject {
}
- (id) initWithHandler:(Fn<void(FullScreenEvent)>)handler;
- (void) windowWillEnterFullScreen:(NSNotification *)aNotification;
- (void) windowWillExitFullScreen:(NSNotification *)aNotification;
- (void) windowDidEnterFullScreen:(NSNotification *)aNotification;
- (void) windowDidExitFullScreen:(NSNotification *)aNotification;
@end // @interface WindowObserver
@implementation WindowObserver {
Fn<void(FullScreenEvent)> _handler;
}
- (id) initWithHandler:(Fn<void(FullScreenEvent)>)handler {
if (self = [super init]) {
_handler = std::move(handler);
}
return self;
}
- (void) windowWillEnterFullScreen:(NSNotification *)aNotification {
_handler(FullScreenEvent::WillEnter);
}
- (void) windowWillExitFullScreen:(NSNotification *)aNotification {
_handler(FullScreenEvent::WillExit);
}
- (void) windowDidEnterFullScreen:(NSNotification *)aNotification {
_handler(FullScreenEvent::DidEnter);
}
- (void) windowDidExitFullScreen:(NSNotification *)aNotification {
_handler(FullScreenEvent::DidExit);
}
@end // @implementation WindowObserver
namespace Ui::Platform {
namespace {
class LayerCreationChecker : public QObject {
public:
LayerCreationChecker(NSView * __weak view, Fn<void()> callback)
: _weakView(view)
, _callback(std::move(callback)) {
QCoreApplication::instance()->installEventFilter(this);
}
protected:
bool eventFilter(QObject *object, QEvent *event) override {
if (!_weakView || [_weakView layer] != nullptr) {
_callback();
}
return QObject::eventFilter(object, event);
}
private:
NSView * __weak _weakView = nil;
Fn<void()> _callback;
};
class EventFilter : public QObject, public QAbstractNativeEventFilter {
public:
EventFilter(
not_null<QObject*> parent,
Fn<bool()> checkStartDrag,
Fn<bool(void*)> checkPerformDrag)
: QObject(parent)
, _checkStartDrag(std::move(checkStartDrag))
, _checkPerformDrag(std::move(checkPerformDrag)) {
Expects(_checkPerformDrag != nullptr);
Expects(_checkStartDrag != nullptr);
}
bool nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) {
if (NSEvent *e = static_cast<NSEvent*>(message)) {
if ([e type] == NSEventTypeLeftMouseDown) {
_dragStarted = _checkStartDrag();
} else if (([e type] == NSEventTypeLeftMouseDragged)
&& _dragStarted) {
if (_checkPerformDrag([e window])) {
return true;
}
_dragStarted = false;
}
}
return false;
}
private:
bool _dragStarted = false;
Fn<bool()> _checkStartDrag;
Fn<bool(void*)> _checkPerformDrag;
};
} // namespace
class WindowHelper::Private final {
public:
explicit Private(not_null<WindowHelper*> owner);
~Private();
[[nodiscard]] int customTitleHeight() const;
[[nodiscard]] QRect controlsRect() const;
[[nodiscard]] bool checkNativeMove(void *nswindow) const;
void activateBeforeNativeMove();
void setStaysOnTop(bool enabled);
void setNativeTitleVisibility(bool visible);
void close();
private:
void init();
void initOpenGL();
void resolveWeakPointers();
void revalidateWeakPointers() const;
void initCustomTitle();
[[nodiscard]] Fn<void(FullScreenEvent)> handleFullScreenEventCallback();
void enforceStyle();
const not_null<WindowHelper*> _owner;
const WindowObserver *_observer = nullptr;
NSWindow * __weak _nativeWindow = nil;
NSView * __weak _nativeView = nil;
bool _hadNativeValues = false;
std::unique_ptr<LayerCreationChecker> _layerCreationChecker;
int _customTitleHeight = 0;
};
WindowHelper::Private::Private(not_null<WindowHelper*> owner)
: _owner(owner) {
init();
}
WindowHelper::Private::~Private() {
if (_observer) {
[_observer release];
}
}
int WindowHelper::Private::customTitleHeight() const {
return _customTitleHeight;
}
QRect WindowHelper::Private::controlsRect() const {
revalidateWeakPointers();
const auto button = [&](NSWindowButton type) {
auto view = [_nativeWindow standardWindowButton:type];
if (!view) {
return QRect();
}
auto result = [view frame];
for (auto parent = [view superview]; parent != nil; parent = [parent superview]) {
const auto origin = [parent frame].origin;
result.origin.x += origin.x;
result.origin.y += origin.y;
}
return QRect(result.origin.x, result.origin.y, result.size.width, result.size.height);
};
auto result = QRect();
const auto buttons = {
NSWindowCloseButton,
NSWindowMiniaturizeButton,
NSWindowZoomButton,
};
for (const auto type : buttons) {
result = result.united(button(type));
}
return QRect(
result.x(),
[_nativeWindow frame].size.height - result.y() - result.height(),
result.width(),
result.height());
}
bool WindowHelper::Private::checkNativeMove(void *nswindow) const {
revalidateWeakPointers();
if (_nativeWindow != nswindow
|| ([_nativeWindow styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) {
return false;
}
const auto cgReal = [NSEvent mouseLocation];
const auto real = QPointF(cgReal.x, cgReal.y);
const auto cgFrame = [_nativeWindow frame];
const auto frame = QRectF(cgFrame.origin.x, cgFrame.origin.y, cgFrame.size.width, cgFrame.size.height);
const auto border = QMarginsF{ 3., 3., 3., 3. };
return frame.marginsRemoved(border).contains(real);
}
void WindowHelper::Private::activateBeforeNativeMove() {
revalidateWeakPointers();
[_nativeWindow makeKeyAndOrderFront:_nativeWindow];
}
void WindowHelper::Private::setStaysOnTop(bool enabled) {
_owner->BasicWindowHelper::setStaysOnTop(enabled);
resolveWeakPointers();
initCustomTitle();
_owner->updateCustomTitleVisibility(true);
}
void WindowHelper::Private::setNativeTitleVisibility(bool visible) {
revalidateWeakPointers();
if (!_nativeWindow) {
return;
}
const auto value = visible ? NSWindowTitleVisible : NSWindowTitleHidden;
[_nativeWindow setTitleVisibility:value];
}
void WindowHelper::Private::close() {
const auto weak = base::make_weak(_owner->window());
QCloseEvent e;
qApp->sendEvent(_owner->window(), &e);
if (!e.isAccepted() || !weak) {
return;
}
revalidateWeakPointers();
if (_nativeWindow) {
[_nativeWindow close];
}
}
Fn<void(FullScreenEvent)> WindowHelper::Private::handleFullScreenEventCallback() {
return crl::guard(_owner->window(), [=](FullScreenEvent event) {
switch (event) {
case FullScreenEvent::WillEnter:
_owner->_titleVisible = false;
_owner->updateCustomTitleVisibility(true);
break;
case FullScreenEvent::WillExit:
enforceStyle();
_owner->_titleVisible = true;
_owner->updateCustomTitleVisibility(true);
break;
case FullScreenEvent::DidEnter:
break;
case FullScreenEvent::DidExit:
enforceStyle();
break;
}
});
}
void WindowHelper::Private::enforceStyle() {
revalidateWeakPointers();
if (_nativeWindow && _customTitleHeight > 0) {
[_nativeWindow setStyleMask:[_nativeWindow styleMask] | NSWindowStyleMaskFullSizeContentView];
}
}
void WindowHelper::Private::initOpenGL() {
//auto forceOpenGL = std::make_unique<QOpenGLWidget>(_owner->window());
}
void WindowHelper::Private::resolveWeakPointers() {
if (!_owner->window()->winId()) {
_owner->window()->createWinId();
}
_nativeView = reinterpret_cast<NSView*>(_owner->window()->winId());
_nativeWindow = _nativeView ? [_nativeView window] : nullptr;
_hadNativeValues = true;
Ensures(_nativeWindow != nullptr);
}
void WindowHelper::Private::revalidateWeakPointers() const {
if (_nativeWindow || !_hadNativeValues) {
return;
}
const_cast<Private*>(this)->resolveWeakPointers();
}
void WindowHelper::Private::initCustomTitle() {
if (![_nativeWindow respondsToSelector:@selector(contentLayoutRect)]
|| ![_nativeWindow respondsToSelector:@selector(setTitlebarAppearsTransparent:)]) {
return;
}
[_nativeWindow setTitlebarAppearsTransparent:YES];
if (_observer) {
[_observer release];
}
_observer = [[WindowObserver alloc] initWithHandler:handleFullScreenEventCallback()];
[[NSNotificationCenter defaultCenter] addObserver:_observer selector:@selector(windowWillEnterFullScreen:) name:NSWindowWillEnterFullScreenNotification object:_nativeWindow];
[[NSNotificationCenter defaultCenter] addObserver:_observer selector:@selector(windowWillExitFullScreen:) name:NSWindowWillExitFullScreenNotification object:_nativeWindow];
[[NSNotificationCenter defaultCenter] addObserver:_observer selector:@selector(windowDidExitFullScreen:) name:NSWindowDidExitFullScreenNotification object:_nativeWindow];
// Qt has bug with layer-backed widgets containing QOpenGLWidgets.
// See https://bugreports.qt.io/browse/QTBUG-64494
// Emulate custom title instead (code below).
//
// Tried to backport a fix, testing.
[_nativeWindow setStyleMask:[_nativeWindow styleMask] | NSWindowStyleMaskFullSizeContentView];
auto inner = [_nativeWindow contentLayoutRect];
auto full = [_nativeView frame];
_customTitleHeight = qMax(qRound(full.size.height - inner.size.height), 0);
// Qt still has some bug with layer-backed widgets containing QOpenGLWidgets.
// See https://github.com/telegramdesktop/tdesktop/issues/4150
// Tried to workaround it by catching the first moment we have CALayer created
// and explicitly setting contentsScale to window->backingScaleFactor there.
_layerCreationChecker = std::make_unique<LayerCreationChecker>(_nativeView, [=] {
if (_nativeView && _nativeWindow) {
if (CALayer *layer = [_nativeView layer]) {
[layer setContentsScale: [_nativeWindow backingScaleFactor]];
_layerCreationChecker = nullptr;
}
} else {
_layerCreationChecker = nullptr;
}
});
}
void WindowHelper::Private::init() {
initOpenGL();
resolveWeakPointers();
initCustomTitle();
}
WindowHelper::WindowHelper(not_null<RpWidget*> window)
: BasicWindowHelper(window)
, _private(std::make_unique<Private>(this))
, _title(Ui::CreateChild<TitleWidget>(
window.get(),
_private->customTitleHeight()))
, _body(Ui::CreateChild<RpWidget>(window.get())) {
init();
_title->setControlsRect(_private->controlsRect());
}
WindowHelper::~WindowHelper() {
}
not_null<RpWidget*> WindowHelper::body() {
return _body;
}
QMargins WindowHelper::frameMargins() {
const auto titleHeight = !_title->isHidden() ? _title->height() : 0;
return QMargins{ 0, titleHeight, 0, 0 };
}
void WindowHelper::setTitle(const QString &title) {
_title->setText(title);
window()->setWindowTitle(title);
}
void WindowHelper::setTitleStyle(const style::WindowTitle &st) {
_title->setStyle(st);
updateCustomTitleVisibility();
}
void WindowHelper::updateCustomTitleVisibility(bool force) {
const auto visible = !_title->shouldBeHidden() && _titleVisible;
if (!force && _title->isHidden() != visible) {
return;
}
_title->setVisible(visible);
_private->setNativeTitleVisibility(!_titleVisible);
}
void WindowHelper::setMinimumSize(QSize size) {
window()->setMinimumSize(size.width(), frameMargins().top() + size.height());
}
void WindowHelper::setFixedSize(QSize size) {
window()->setFixedSize(size.width(), frameMargins().top() + size.height());
}
void WindowHelper::setStaysOnTop(bool enabled) {
_private->setStaysOnTop(enabled);
}
void WindowHelper::setGeometry(QRect rect) {
window()->setGeometry(rect.marginsAdded(frameMargins()));
}
void WindowHelper::setupBodyTitleAreaEvents() {
const auto controls = _private->controlsRect();
qApp->installNativeEventFilter(new EventFilter(window(), [=] {
const auto point = body()->mapFromGlobal(QCursor::pos());
return (bodyTitleAreaHit(point) & WindowTitleHitTestFlag::Move);
}, [=](void *nswindow) {
const auto point = body()->mapFromGlobal(QCursor::pos());
if (_private->checkNativeMove(nswindow)
&& !controls.contains(point)
&& (bodyTitleAreaHit(point) & WindowTitleHitTestFlag::Move)) {
_private->activateBeforeNativeMove();
window()->windowHandle()->startSystemMove();
return true;
}
return false;
}));
}
void WindowHelper::close() {
_private->close();
}
const style::TextStyle &WindowHelper::titleTextStyle() const {
return _title->textStyle();
}
void WindowHelper::init() {
updateCustomTitleVisibility(true);
style::PaletteChanged(
) | rpl::on_next([=] {
Ui::ForceFullRepaint(window());
}, window()->lifetime());
rpl::combine(
window()->sizeValue(),
_title->heightValue(),
_title->shownValue()
) | rpl::on_next([=](QSize size, int titleHeight, bool shown) {
if (!shown) {
titleHeight = 0;
}
_body->setGeometry(
0,
titleHeight,
size.width(),
size.height() - titleHeight);
}, _body->lifetime());
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
setBodyTitleArea([](QPoint widgetPoint) {
using Flag = Ui::WindowTitleHitTestFlag;
return (widgetPoint.y() < 0)
? (Flag::Move | Flag::Maximize)
: Flag::None;
});
#endif // Qt >= 6.0.0
}
std::unique_ptr<BasicWindowHelper> CreateSpecialWindowHelper(
not_null<RpWidget*> window) {
return std::make_unique<WindowHelper>(window);
}
bool NativeWindowFrameSupported() {
return false;
}
rpl::producer<FullScreenEvent> FullScreenEvents(
not_null<RpWidget*> window) {
return [=](auto consumer) {
auto result = rpl::lifetime();
struct State {
~State() {
if (observer) {
[observer release];
}
}
WindowObserver *observer = nullptr;
};
const auto state = result.make_state<State>();
window->winIdValue() | rpl::on_next([=](WId winId) {
if (const auto was = base::take(state->observer)) {
[was release];
}
if (!winId) {
return;
}
const auto view = reinterpret_cast<NSView*>(winId);
const auto win = [view window];
Ensures(win != nullptr);
const auto handler = [=](FullScreenEvent event) {
consumer.put_next_copy(event);
};
state->observer = [[WindowObserver alloc] initWithHandler:handler];
const auto add = [&](NSNotificationName name, SEL selector) {
[[NSNotificationCenter defaultCenter]
addObserver:state->observer
selector:selector
name:name
object:win];
};
add(NSWindowWillEnterFullScreenNotification, @selector(windowWillEnterFullScreen:));
add(NSWindowWillExitFullScreenNotification, @selector(windowWillExitFullScreen:));
add(NSWindowDidEnterFullScreenNotification, @selector(windowDidEnterFullScreen:));
add(NSWindowDidExitFullScreenNotification, @selector(windowDidExitFullScreen:));
}, result);
return result;
};
}
} // namespace Ui::Platform

View File

@@ -0,0 +1,59 @@
// 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
//
#pragma once
#include "ui/text/text.h"
#include "ui/rp_widget.h"
#include "base/object_ptr.h"
#include <QtCore/QRect>
#include <QtCore/QPoint>
namespace style {
struct WindowTitle;
struct TextStyle;
} // namespace style
namespace Ui {
class PlainShadow;
namespace Platform {
class TitleWidget : public RpWidget {
public:
TitleWidget(not_null<RpWidget*> parent, int height);
~TitleWidget();
void setText(const QString &text);
void setStyle(const style::WindowTitle &st);
void setControlsRect(const QRect &rect);
[[nodiscard]] QString text() const;
[[nodiscard]] bool shouldBeHidden() const;
[[nodiscard]] const style::TextStyle &textStyle() const;
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
private:
not_null<RpWidget*> window() const;
void init(int height);
not_null<const style::WindowTitle*> _st;
std::unique_ptr<style::TextStyle> _textStyle;
object_ptr<Ui::PlainShadow> _shadow;
QString _text;
Ui::Text::String _string;
int _controlsRight = 0;
};
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,152 @@
// 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/mac/ui_window_title_mac.h"
#include "base/platform/base_platform_info.h"
#include "ui/platform/ui_platform_window_title.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/shadow.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "base/debug_log.h"
#include "styles/style_widgets.h"
#include "styles/palette.h"
#include <QtGui/QPainter>
#include <QtGui/QtEvents>
#include <QtGui/QWindow>
namespace Ui {
namespace Platform {
std::shared_ptr<TitleControlsLayout> TitleControlsLayout::Create() {
return std::shared_ptr<TitleControlsLayout>(new TitleControlsLayout({
.left = {
TitleControls::Control::Close,
TitleControls::Control::Minimize,
TitleControls::Control::Maximize,
}
}));
}
TitleWidget::TitleWidget(not_null<RpWidget*> parent, int height)
: RpWidget(parent)
, _st(&st::defaultWindowTitle)
, _shadow(this, st::titleShadow) {
init(height);
}
TitleWidget::~TitleWidget() = default;
void TitleWidget::setText(const QString &text) {
if (_text != text) {
_text = text;
_string.setText(textStyle(), text);
update();
}
}
void TitleWidget::setStyle(const style::WindowTitle &st) {
_st = &st;
update();
}
void TitleWidget::setControlsRect(const QRect &rect) {
_controlsRight = rect.left() * 2 + rect.width();
}
bool TitleWidget::shouldBeHidden() const {
return !_st->height;
}
const style::TextStyle &TitleWidget::textStyle() const {
return *_textStyle;
}
QString TitleWidget::text() const {
return _text;
}
not_null<RpWidget*> TitleWidget::window() const {
return static_cast<RpWidget*>(parentWidget());
}
void TitleWidget::init(int height) {
setAttribute(Qt::WA_OpaquePaintEvent);
window()->widthValue(
) | rpl::on_next([=](int width) {
setGeometry(0, 0, width, height);
}, lifetime());
const auto setFromFont = [&](const style::font &font) {
_textStyle = std::make_unique<style::TextStyle>(style::TextStyle{
.font = font,
});
};
const auto families = QStringList{
u".AppleSystemUIFont"_q,
u".SF NS Text"_q,
u"Helvetica Neue"_q,
};
for (auto family : families) {
auto font = QFont();
font.setFamily(family);
if (QFontInfo(font).family() == font.family()) {
static const auto logged = [&] {
LOG(("Title Font: %1").arg(family));
return true;
}();
const auto apple = (family == u".AppleSystemUIFont"_q);
setFromFont(style::font(
apple ? 13 : (height * 15) / 24,
apple ? style::FontFlag::Bold : style::FontFlag(),
family));
break;
}
}
if (!_textStyle) {
setFromFont(style::font(13, style::FontFlag::Semibold, 0));
}
}
void TitleWidget::paintEvent(QPaintEvent *e) {
Painter p(this);
const auto active = isActiveWindow();
p.fillRect(rect(), active ? _st->bgActive : _st->bg);
p.setPen(active ? _st->fgActive : _st->fg);
const auto full = _string.maxWidth();
const auto top = (height() - _textStyle->font->height) / 2;
if (::Platform::IsMac26_0OrGreater()
|| ((width() - _controlsRight * 2) < full)) {
const auto left = _controlsRight;
_string.drawElided(p, left, top, width() - left);
} else {
const auto left = (width() - full) / 2;
_string.draw(p, left, top, full);
}
}
void TitleWidget::resizeEvent(QResizeEvent *e) {
_shadow->setGeometry(0, height() - st::lineWidth, width(), st::lineWidth);
}
void TitleWidget::mouseDoubleClickEvent(QMouseEvent *e) {
const auto window = parentWidget();
if (window->windowState() == Qt::WindowMaximized) {
window->setWindowState(Qt::WindowNoState);
} else {
window->setWindowState(Qt::WindowMaximized);
}
}
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,57 @@
// 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
//
#pragma once
class QPoint;
class QPainter;
class QPaintEvent;
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Ui::Platform {
[[nodiscard]] bool IsApplicationActive();
[[nodiscard]] bool TranslucentWindowsSupported();
void InitOnTopPanel(not_null<QWidget*> panel);
void DeInitOnTopPanel(not_null<QWidget*> panel);
void ReInitOnTopPanel(not_null<QWidget*> panel);
void ShowOverAll(not_null<QWidget*> widget, bool canFocus = true);
void IgnoreAllActivation(not_null<QWidget*> widget);
void ClearTransientParent(not_null<QWidget*> widget);
void AcceptAllMouseInput(not_null<QWidget*> widget);
void DisableSystemWindowResize(not_null<QWidget*> widget, QSize ratio);
[[nodiscard]] std::optional<bool> IsOverlapped(
not_null<QWidget*> widget,
const QRect &rect);
[[nodiscard]] constexpr bool UseMainQueueGeneric();
void DrainMainQueue(); // Needed only if UseMainQueueGeneric() is false.
[[nodiscard]] bool WindowMarginsSupported();
void SetWindowMargins(not_null<QWidget*> widget, const QMargins &margins);
void ShowWindowMenu(not_null<QWidget*> widget, const QPoint &point);
void FixPopupMenuNativeEmojiPopup(not_null<PopupMenu*> menu);
} // namespace Ui::Platform
// Platform dependent implementations.
#if defined Q_OS_WINRT || defined Q_OS_WIN
#include "ui/platform/win/ui_utility_win.h"
#elif defined Q_OS_MAC // Q_OS_WINRT || Q_OS_WIN
#include "ui/platform/mac/ui_utility_mac.h"
#else // Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC
#include "ui/platform/linux/ui_utility_linux.h"
#endif // else for Q_OS_WINRT || Q_OS_WIN || Q_OS_MAC

View File

@@ -0,0 +1,690 @@
// 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 "ui/qt_object_factory.h"
#include "ui/ui_utility.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;
}
[[nodiscard]] std::array<QImage, 4> PrepareSides(
const style::Shadow &shadow) {
auto result = std::array<QImage, 4>();
const auto extend = shadow.extend;
const auto make = [&](
int index,
const style::icon &icon,
auto &&postprocess) {
result[index] = icon.instance(st::windowShadowFg->c);
auto p = QPainter(&result[index]);
p.setCompositionMode(QPainter::CompositionMode_Source);
postprocess(p, icon.width(), icon.height());
};
make(0, shadow.left, [&](QPainter &p, int width, int height) {
const auto skip = extend.left();
p.fillRect(skip, 0, width - skip, height, Qt::transparent);
});
make(1, shadow.top, [&](QPainter &p, int width, int height) {
const auto skip = extend.top();
p.fillRect(0, skip, width, height - skip, Qt::transparent);
});
make(2, shadow.right, [&](QPainter &p, int width, int height) {
const auto skip = extend.right();
p.fillRect(0, 0, width - skip, height, Qt::transparent);
});
make(3, shadow.bottom, [&](QPainter &p, int width, int height) {
const auto skip = extend.bottom();
p.fillRect(0, 0, width, height - skip, Qt::transparent);
});
return result;
}
[[nodiscard]] std::array<QImage, 4> PrepareCorners(
const style::Shadow &shadow,
int radius) {
auto result = std::array<QImage, 4>();
const auto extend = shadow.extend;
const auto make = [&](
int index,
const style::icon &icon,
auto &&postprocess) {
result[index] = icon.instance(st::windowShadowFg->c);
auto p = QPainter(&result[index]);
auto hq = PainterHighQualityEnabler(p);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.setBrush(Qt::transparent);
p.setPen(Qt::NoPen);
postprocess(p, icon.width(), icon.height());
};
make(0, shadow.topLeft, [&](QPainter &p, int width, int height) {
const auto skipx = extend.left();
const auto skipy = extend.top();
width += 2 * radius;
height += 2 * radius;
p.drawRoundedRect(skipx, skipy, width, height, radius, radius);
});
make(1, shadow.bottomLeft, [&](QPainter &p, int width, int height) {
const auto skipx = extend.left();
const auto skipy = extend.bottom() + 2 * radius;
width += 2 * radius;
height += 2 * radius;
p.drawRoundedRect(skipx, -skipy, width, height, radius, radius);
});
make(2, shadow.topRight, [&](QPainter &p, int width, int height) {
const auto skipx = extend.right() + 2 * radius;
const auto skipy = extend.top();
width += 2 * radius;
height += 2 * radius;
p.drawRoundedRect(-skipx, skipy, width, height, radius, radius);
});
make(3, shadow.bottomRight, [&](QPainter &p, int width, int height) {
const auto skipx = extend.right() + 2 * radius;
const auto skipy = extend.bottom() + 2 * radius;
width += 2 * radius;
height += 2 * radius;
p.drawRoundedRect(-skipx, -skipy, width, height, radius, radius);
});
return result;
}
} // 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::overrideSystemButtonOver(HitTestResult button) {
Expects(button == HitTestResult::None);
}
void BasicWindowHelper::overrideSystemButtonDown(HitTestResult button) {
Expects(button == HitTestResult::None);
}
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();
}
int BasicWindowHelper::manualRoundingRadius() const {
return 0;
}
void BasicWindowHelper::setBodyTitleArea(
Fn<WindowTitleHitTestFlags(QPoint)> testMethod) {
Expects(!_bodyTitleAreaTestMethod || testMethod);
if (!testMethod) {
return;
}
if (!_bodyTitleAreaTestMethod) {
setupBodyTitleAreaEvents();
}
_bodyTitleAreaTestMethod = std::move(testMethod);
}
const style::TextStyle &BasicWindowHelper::titleTextStyle() const {
return st::defaultWindowTitle.style;
}
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::on_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) {
const auto ee = static_cast<QMouseEvent*>(e.get());
if (ee->button() == Qt::LeftButton) {
_mousePressed = true;
} else if (ee->button() == Qt::RightButton) {
if (hitTest() & WindowTitleHitTestFlag::Menu) {
ShowWindowMenu(window(), ee->windowPos().toPoint());
}
}
} 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;
_mousePressCancelled = true;
const auto weak = QPointer(_window.get());
_window->windowHandle()->startSystemMove();
SendSynteticMouseEvent(
body().get(),
QEvent::MouseButtonRelease,
Qt::LeftButton);
if (weak) {
_mousePressCancelled = false;
}
}
}
}, 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)
, _sides(PrepareSides(Shadow()))
, _corners(PrepareCorners(Shadow(), Radius())) {
init();
}
void DefaultWindowHelper::init() {
if (WindowMarginsSupported()) {
window()->setAttribute(Qt::WA_TranslucentBackground);
}
_title->show();
rpl::combine(
window()->shownValue(),
_title->shownValue(),
_windowState.value()
) | rpl::filter([=](
bool shown,
bool titleShown,
Qt::WindowStates windowState) {
return shown;
}) | rpl::on_next([=](
bool shown,
bool titleShown,
Qt::WindowStates windowState) {
_lastGeometry = _body->mapToGlobal(_body->rect());
window()->windowHandle()->setFlag(Qt::FramelessWindowHint, titleShown);
updateWindowMargins();
if (_fixedSize) {
setFixedSize(*_fixedSize);
} else if (_minimumSize) {
setMinimumSize(*_minimumSize);
}
}, window()->lifetime());
_title->shownValue(
) | rpl::filter([=] {
return !window()->isHidden()
&& !window()->isMaximized()
&& !window()->isFullScreen();
}) | rpl::on_next([=] {
setGeometry(_lastGeometry);
}, window()->lifetime());
rpl::combine(
window()->widthValue(),
_windowState.value(),
_title->shownValue(),
_title->layout().value()
) | rpl::on_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(),
_title->layout().value()
) | rpl::on_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::on_next([=] {
Painter p(window());
paintBorders(p);
}, window()->lifetime());
window()->events() | rpl::on_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);
SendSynteticMouseEvent(
window().get(),
QEvent::MouseButtonRelease,
Qt::LeftButton);
}
} 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::on_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::on_next([=](QRect clip) {
Painter p(_roundingOverlay);
const auto skip = resizeArea();
const auto outer = window()->rect();
const auto rect = outer.marginsRemoved(skip);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
_roundRect.paint(p, rect, RectPart::AllCorners);
p.setCompositionMode(QPainter::CompositionMode_Source);
const auto outside = std::array{
QRect(0, 0, outer.width(), skip.top()),
QRect(0, skip.top(), skip.left(), outer.height() - skip.top()),
QRect(
outer.width() - skip.right(),
skip.top(),
skip.right(),
outer.height() - skip.top()),
QRect(
skip.left(),
outer.height() - skip.bottom(),
outer.width() - skip.left() - skip.right(),
skip.bottom())
};
for (const auto &part : outside) {
if (const auto fill = clip.intersected(part); !fill.isEmpty()) {
p.fillRect(fill, Qt::transparent);
}
}
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
Shadow::paint(p, rect, window()->width(), Shadow(), _sides, _corners);
}, _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 {
return WindowMarginsSupported() && TranslucentWindowsSupported();
}
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) {
_minimumSize = size;
window()->setMinimumSize(size.grownBy(bodyPadding()));
}
void DefaultWindowHelper::setFixedSize(QSize size) {
_fixedSize = size;
window()->setFixedSize(size.grownBy(bodyPadding()));
_title->setResizeEnabled(false);
}
void DefaultWindowHelper::setGeometry(QRect rect) {
window()->setGeometry(rect.marginsAdded(bodyPadding()));
}
int DefaultWindowHelper::manualRoundingRadius() const {
return _roundingOverlay ? Radius() : 0;
}
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::updateWindowMargins() {
if (hasShadow() && !_title->isHidden()) {
SetWindowMargins(window(), resizeArea());
_marginsSet = true;
} else if (_marginsSet) {
SetWindowMargins(window(), {});
_marginsSet = 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

View File

@@ -0,0 +1,158 @@
// 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
//
#pragma once
#include "base/flags.h"
#include "base/object_ptr.h"
#include "ui/round_rect.h"
namespace style {
struct WindowTitle;
struct TextStyle;
} // namespace style
namespace Ui {
class RpWidget;
class RpWindow;
enum class WindowTitleHitTestFlag;
using WindowTitleHitTestFlags = base::flags<WindowTitleHitTestFlag>;
namespace Platform {
struct HitTestRequest;
enum class HitTestResult;
class DefaultTitleWidget;
class BasicWindowHelper {
public:
explicit BasicWindowHelper(not_null<RpWidget*> window);
virtual ~BasicWindowHelper() = default;
[[nodiscard]] not_null<RpWidget*> window() const {
return _window;
}
virtual void initInWindow(not_null<RpWindow*> window);
[[nodiscard]] virtual not_null<RpWidget*> body();
[[nodiscard]] virtual QMargins frameMargins();
[[nodiscard]] virtual int additionalContentPadding() const;
[[nodiscard]] virtual auto additionalContentPaddingValue() const
-> rpl::producer<int>;
[[nodiscard]] virtual auto hitTestRequests() const
-> rpl::producer<not_null<HitTestRequest*>>;
[[nodiscard]] virtual auto systemButtonOver() const
-> rpl::producer<HitTestResult>;
[[nodiscard]] virtual auto systemButtonDown() const
-> rpl::producer<HitTestResult>;
virtual void overrideSystemButtonOver(HitTestResult button);
virtual void overrideSystemButtonDown(HitTestResult button);
virtual void setTitle(const QString &title);
virtual void setTitleStyle(const style::WindowTitle &st);
virtual void setNativeFrame(bool enabled);
virtual void setMinimumSize(QSize size);
virtual void setFixedSize(QSize size);
virtual void setStaysOnTop(bool enabled);
virtual void setGeometry(QRect rect);
virtual void showFullScreen();
virtual void showNormal();
virtual void close();
virtual int manualRoundingRadius() const;
void setBodyTitleArea(Fn<WindowTitleHitTestFlags(QPoint)> testMethod);
[[nodiscard]] bool mousePressCancelled() const {
return _mousePressCancelled;
}
[[nodiscard]] virtual const style::TextStyle &titleTextStyle() const;
protected:
[[nodiscard]] WindowTitleHitTestFlags bodyTitleAreaHit(
QPoint point) const {
return _bodyTitleAreaTestMethod
? _bodyTitleAreaTestMethod(point)
: WindowTitleHitTestFlag();
}
[[nodiscard]] QMargins nativeFrameMargins() const;
private:
virtual void setupBodyTitleAreaEvents();
const not_null<RpWidget*> _window;
Fn<WindowTitleHitTestFlags(QPoint)> _bodyTitleAreaTestMethod;
bool _mousePressed = false;
bool _mousePressCancelled = false;
};
class DefaultWindowHelper final : public QObject, public BasicWindowHelper {
public:
explicit DefaultWindowHelper(not_null<RpWidget*> window);
not_null<RpWidget*> body() override;
QMargins frameMargins() override;
void setTitle(const QString &title) override;
void setTitleStyle(const style::WindowTitle &st) override;
void setNativeFrame(bool enabled) override;
void setMinimumSize(QSize size) override;
void setFixedSize(QSize size) override;
void setGeometry(QRect rect) override;
int manualRoundingRadius() const override;
protected:
bool eventFilter(QObject *obj, QEvent *e) override;
private:
void init();
void updateRoundingOverlay();
[[nodiscard]] bool hasShadow() const;
[[nodiscard]] QMargins resizeArea() const;
[[nodiscard]] Qt::Edges edgesFromPos(const QPoint &pos) const;
void paintBorders(QPainter &p);
void updateWindowMargins();
void updateCursor(Qt::Edges edges);
[[nodiscard]] int titleHeight() const;
[[nodiscard]] QMargins bodyPadding() const;
const not_null<DefaultTitleWidget*> _title;
const not_null<RpWidget*> _body;
RoundRect _roundRect;
std::array<QImage, 4> _sides;
std::array<QImage, 4> _corners;
object_ptr<RpWidget> _roundingOverlay = { nullptr };
rpl::variable<Qt::WindowStates> _windowState = Qt::WindowNoState;
QRect _lastGeometry;
std::optional<QSize> _minimumSize;
std::optional<QSize> _fixedSize;
bool _marginsSet = false;
};
[[nodiscard]] std::unique_ptr<BasicWindowHelper> CreateSpecialWindowHelper(
not_null<RpWidget*> window);
[[nodiscard]] inline std::unique_ptr<BasicWindowHelper> CreateWindowHelper(
not_null<RpWidget*> window) {
if (auto special = CreateSpecialWindowHelper(window)) {
return special;
}
return std::make_unique<DefaultWindowHelper>(window);
}
[[nodiscard]] bool NativeWindowFrameSupported();
enum class FullScreenEvent {
WillEnter,
DidEnter,
WillExit,
DidExit,
};
[[nodiscard]] rpl::producer<FullScreenEvent> FullScreenEvents(
not_null<RpWidget*> window);
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,569 @@
// 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_title.h"
#include "ui/platform/ui_platform_utility.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/shadow.h"
#include "ui/ui_utility.h"
#include "ui/widgets/rp_window.h"
#include "styles/style_widgets.h"
#include "styles/palette.h"
#include "base/algorithm.h"
#include "base/event_filter.h"
#include "ui/integration.h"
#include "base/platform/base_platform_info.h"
#include <QtGui/QPainter>
#include <QtGui/QtEvents>
#include <QtGui/QWindow>
namespace Ui {
namespace Platform {
bool SemiNativeSystemButtonProcessing() {
return ::Platform::IsWindows11OrGreater();
}
void SetupSemiNativeSystemButtons(
not_null<TitleControls*> controls,
not_null<RpWindow*> window,
rpl::lifetime &lifetime,
Fn<bool()> filter) {
if (!SemiNativeSystemButtonProcessing()) {
return;
}
window->systemButtonOver(
) | rpl::filter([=](HitTestResult button) {
return !filter || filter() || (button == HitTestResult::None);
}) | rpl::on_next([=](HitTestResult button) {
controls->buttonOver(button);
}, lifetime);
window->systemButtonDown(
) | rpl::filter([=](HitTestResult button) {
return !filter || filter() || (button == HitTestResult::None);
}) | rpl::on_next([=](HitTestResult button) {
controls->buttonDown(button);
}, lifetime);
}
object_ptr<AbstractButton> IconTitleButtons::create(
not_null<QWidget*> parent,
TitleControl control,
const style::WindowTitle &st) {
const auto make = [&](
QPointer<IconButton> &my,
const style::IconButton &st) {
Expects(!my);
auto result = object_ptr<IconButton>(parent, st);
my = result.data();
return result;
};
switch (control) {
case TitleControl::Minimize:
return make(_minimize, st.minimize);
case TitleControl::Maximize:
return make(_maximizeRestore, st.maximize);
case TitleControl::Close:
return make(_close, st.close);
}
Unexpected("Control in IconTitleButtons::create.");
}
void IconTitleButtons::updateState(
bool active,
bool maximized,
const style::WindowTitle &st) {
if (_minimize) {
const auto minimize = active
? &st.minimizeIconActive
: &st.minimize.icon;
const auto minimizeOver = active
? &st.minimizeIconActiveOver
: &st.minimize.iconOver;
_minimize->setIconOverride(minimize, minimizeOver);
_minimize->setAccessibleName(Ui::Integration::Instance().phraseMinimize());
}
if (_maximizeRestore) {
if (maximized) {
const auto restore = active
? &st.restoreIconActive
: &st.restoreIcon;
const auto restoreOver = active
? &st.restoreIconActiveOver
: &st.restoreIconOver;
_maximizeRestore->setIconOverride(restore, restoreOver);
_maximizeRestore->setAccessibleName(Ui::Integration::Instance().phraseRestore());
} else {
const auto maximize = active
? &st.maximizeIconActive
: &st.maximize.icon;
const auto maximizeOver = active
? &st.maximizeIconActiveOver
: &st.maximize.iconOver;
_maximizeRestore->setIconOverride(maximize, maximizeOver);
_maximizeRestore->setAccessibleName(Ui::Integration::Instance().phraseMaximize());
}
}
if (_close) {
const auto close = active
? &st.closeIconActive
: &st.close.icon;
const auto closeOver = active
? &st.closeIconActiveOver
: &st.close.iconOver;
_close->setIconOverride(close, closeOver);
_close->setAccessibleName(Ui::Integration::Instance().phraseButtonClose());
}
}
TitleControls::TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize)
: TitleControls(
parent,
st,
std::make_unique<IconTitleButtons>(),
std::move(maximize)) {
}
TitleControls::TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
std::unique_ptr<AbstractTitleButtons> buttons,
Fn<void(bool maximized)> maximize)
: _st(&st)
, _layout(TitleControlsLayout::Instance())
, _buttons(std::move(buttons))
, _minimize(_buttons->create(parent, Control::Minimize, st))
, _maximizeRestore(_buttons->create(parent, Control::Maximize, st))
, _close(_buttons->create(parent, Control::Close, st))
, _maximizedState(parent->windowState()
& (Qt::WindowMaximized | Qt::WindowFullScreen))
, _activeState(parent->isActiveWindow()) {
init(std::move(maximize));
_close->paintRequest(
) | rpl::on_next([=] {
const auto active = window()->isActiveWindow();
if (_activeState != active) {
_activeState = active;
updateButtonsState();
}
}, _close->lifetime());
}
void TitleControls::setStyle(const style::WindowTitle &st) {
_st = &st;
updateButtonsState();
}
not_null<const style::WindowTitle*> TitleControls::st() const {
return _st;
}
TitleControlsLayout &TitleControls::layout() const {
return *_layout;
}
QRect TitleControls::geometry() const {
auto result = QRect();
const auto add = [&](auto &&control) {
if (!control->isHidden()) {
result = result.united(control->geometry());
}
};
add(_minimize);
add(_maximizeRestore);
add(_close);
return result;
}
not_null<RpWidget*> TitleControls::parent() const {
return static_cast<RpWidget*>(_close->parentWidget());
}
not_null<QWidget*> TitleControls::window() const {
return _close->window();
}
void TitleControls::init(Fn<void(bool maximized)> maximize) {
_minimize->setClickedCallback([=] {
const auto weak = base::make_weak(_minimize.data());
window()->setWindowState(
window()->windowState() | Qt::WindowMinimized);
if (weak) {
_minimize->clearState();
}
});
_minimize->setPointerCursor(false);
_maximizeRestore->setClickedCallback([=] {
const auto weak = base::make_weak(_maximizeRestore.data());
if (maximize) {
maximize(!_maximizedState);
} else {
window()->setWindowState(_maximizedState
? Qt::WindowNoState
: Qt::WindowMaximized);
}
if (weak) {
_maximizeRestore->clearState();
}
});
_maximizeRestore->setPointerCursor(false);
_close->setClickedCallback([=] {
const auto weak = base::make_weak(_close.data());
window()->close();
if (weak) {
_close->clearState();
}
});
_close->setPointerCursor(false);
rpl::combine(
parent()->widthValue(),
_layout->value()
) | rpl::on_next([=] {
updateControlsPosition();
}, _close->lifetime());
base::install_event_filter(window(), [=](not_null<QEvent*> e) {
if (e->type() == QEvent::WindowStateChange) {
handleWindowStateChanged(window()->windowState());
}
return base::EventFilterResult::Continue;
});
_activeState = parent()->isActiveWindow();
updateButtonsState();
}
void TitleControls::setResizeEnabled(bool enabled) {
_resizeEnabled = enabled;
updateControlsPosition();
}
void TitleControls::raise() {
_minimize->raise();
_maximizeRestore->raise();
_close->raise();
}
HitTestResult TitleControls::hitTest(QPoint point) const {
const auto test = [&](const object_ptr<AbstractButton> &button) {
return button && QRect(
button->mapTo(button->window(), QPoint()),
button->size()
).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<AbstractButton> &button,
HitTestResult buttonTestResult,
Control control) {
const auto over = (testResult == buttonTestResult);
if (const auto raw = button.data()) {
raw->setSynteticOver(over);
}
_buttons->notifySynteticOver(control, over);
};
update(_minimize, HitTestResult::Minimize, Control::Minimize);
update(
_maximizeRestore,
HitTestResult::MaximizeRestore,
Control::Maximize);
update(_close, HitTestResult::Close, Control::Close);
}
void TitleControls::buttonDown(HitTestResult testResult) {
const auto update = [&](
const object_ptr<AbstractButton> &button,
HitTestResult buttonTestResult) {
if (const auto raw = button.data()) {
raw->setSynteticDown(testResult == buttonTestResult);
}
};
update(_minimize, HitTestResult::Minimize);
update(_maximizeRestore, HitTestResult::MaximizeRestore);
update(_close, HitTestResult::Close);
}
AbstractButton *TitleControls::controlWidget(Control control) const {
switch (control) {
case Control::Minimize: return _minimize;
case Control::Maximize: return _maximizeRestore;
case Control::Close: return _close;
}
return nullptr;
}
void TitleControls::updateControlsPosition() {
auto controlsLayout = _layout->current();
auto &controlsLeft = controlsLayout.left;
auto &controlsRight = controlsLayout.right;
ranges::reverse(controlsRight);
if (_st->oneSideControls) {
const auto moveFromTo = [&](auto &from, auto &to) {
for (const auto control : from) {
if (!ranges::contains(to, control)) {
to.push_back(control);
}
}
from.clear();
};
if (controlsLayout.onLeft()) {
moveFromTo(controlsRight, controlsLeft);
} else {
moveFromTo(controlsLeft, controlsRight);
}
}
const auto controlPresent = [&](Control control) {
return ranges::contains(controlsLeft, control)
|| ranges::contains(controlsRight, control);
};
const auto eraseControl = [&](Control control) {
controlsLeft.erase(
ranges::remove(controlsLeft, control),
end(controlsLeft));
controlsRight.erase(
ranges::remove(controlsRight, control),
end(controlsRight));
};
if (!_resizeEnabled) {
eraseControl(Control::Maximize);
}
if (controlPresent(Control::Minimize)) {
_minimize->show();
} else {
_minimize->hide();
}
if (controlPresent(Control::Maximize)) {
_maximizeRestore->show();
} else {
_maximizeRestore->hide();
}
if (controlPresent(Control::Close)) {
_close->show();
} else {
_close->hide();
}
std::vector<Control> visitedControls;
const auto updateBySide = [&](
const std::vector<Control> &controls,
bool right) {
auto position = 0;
for (const auto &control : controls) {
const auto widget = controlWidget(control);
if (!widget || ranges::contains(visitedControls, control)) {
continue;
}
if (right) {
widget->moveToRight(position, 0);
} else {
widget->moveToLeft(position, 0);
}
position += widget->width();
visitedControls.push_back(control);
}
};
updateBySide(controlsLeft, false);
updateBySide(controlsRight, true);
}
void TitleControls::handleWindowStateChanged(Qt::WindowStates state) {
if (state & Qt::WindowMinimized) {
return;
}
auto maximized = (state & Qt::WindowMaximized)
|| (state & Qt::WindowFullScreen);
if (_maximizedState != maximized) {
_maximizedState = maximized;
updateButtonsState();
}
}
void TitleControls::updateButtonsState() {
_buttons->updateState(_activeState, _maximizedState, *_st);
}
std::shared_ptr<TitleControlsLayout> TitleControlsLayout::Instance() {
static std::weak_ptr<TitleControlsLayout> Weak;
auto result = Weak.lock();
if (!result) {
Weak = result = Create();
}
return result;
}
DefaultTitleWidget::DefaultTitleWidget(not_null<RpWidget*> parent)
: RpWidget(parent)
, _controls(this, st::defaultWindowTitle)
, _shadow(this, st::titleShadow) {
setAttribute(Qt::WA_OpaquePaintEvent);
}
not_null<const style::WindowTitle*> DefaultTitleWidget::st() const {
return _controls.st();
}
TitleControlsLayout &DefaultTitleWidget::layout() const {
return _controls.layout();
}
QRect DefaultTitleWidget::controlsGeometry() const {
return _controls.geometry();
}
void DefaultTitleWidget::setText(const QString &text) {
window()->setWindowTitle(text);
}
void DefaultTitleWidget::setStyle(const style::WindowTitle &st) {
_controls.setStyle(st);
update();
}
void DefaultTitleWidget::setResizeEnabled(bool enabled) {
_controls.setResizeEnabled(enabled);
}
void DefaultTitleWidget::paintEvent(QPaintEvent *e) {
const auto active = window()->isActiveWindow();
QPainter(this).fillRect(
e->rect(),
active ? _controls.st()->bgActive : _controls.st()->bg);
}
void DefaultTitleWidget::resizeEvent(QResizeEvent *e) {
_shadow->setGeometry(0, height() - st::lineWidth, width(), st::lineWidth);
}
void DefaultTitleWidget::mousePressEvent(QMouseEvent *e) {
if (e->button() == Qt::LeftButton) {
_mousePressed = true;
} else if (e->button() == Qt::RightButton) {
ShowWindowMenu(window(), e->windowPos().toPoint());
}
}
void DefaultTitleWidget::mouseReleaseEvent(QMouseEvent *e) {
if (e->button() == Qt::LeftButton) {
_mousePressed = false;
}
}
void DefaultTitleWidget::mouseMoveEvent(QMouseEvent *e) {
if (_mousePressed) {
window()->windowHandle()->startSystemMove();
SendSynteticMouseEvent(
this,
QEvent::MouseButtonRelease,
Qt::LeftButton);
}
}
void DefaultTitleWidget::mouseDoubleClickEvent(QMouseEvent *e) {
const auto state = window()->windowState();
if (state & Qt::WindowMaximized) {
window()->setWindowState(state & ~Qt::WindowMaximized);
} else {
window()->setWindowState(state | Qt::WindowMaximized);
}
}
SeparateTitleControls::SeparateTitleControls(
QWidget *parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize)
: wrap(parent)
, controls(&wrap, st, std::move(maximize)) {
}
SeparateTitleControls::SeparateTitleControls(
QWidget *parent,
const style::WindowTitle &st,
std::unique_ptr<AbstractTitleButtons> buttons,
Fn<void(bool maximized)> maximize)
: wrap(parent)
, controls(&wrap, st, std::move(buttons), std::move(maximize)) {
}
std::unique_ptr<SeparateTitleControls> SetupSeparateTitleControls(
not_null<RpWindow*> window,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize,
rpl::producer<int> controlsTop) {
return SetupSeparateTitleControls(
window,
std::make_unique<SeparateTitleControls>(
window->body(),
st,
std::move(maximize)),
std::move(controlsTop));
}
std::unique_ptr<SeparateTitleControls> SetupSeparateTitleControls(
not_null<RpWindow*> window,
std::unique_ptr<SeparateTitleControls> created,
rpl::producer<int> controlsTop) {
const auto raw = created.get();
auto &lifetime = raw->wrap.lifetime();
rpl::combine(
window->body()->widthValue(),
window->additionalContentPaddingValue(),
controlsTop ? std::move(controlsTop) : rpl::single(0)
) | rpl::on_next([=](int width, int padding, int top) {
raw->wrap.setGeometry(
0,
top,
width,
raw->controls.geometry().height());
}, lifetime);
window->hitTestRequests(
) | rpl::on_next([=](not_null<HitTestRequest*> request) {
const auto controlsResult = raw->controls.hitTest(request->point);
if (controlsResult != HitTestResult::None) {
request->result = controlsResult;
}
}, lifetime);
SetupSemiNativeSystemButtons(&raw->controls, window, lifetime);
return created;
}
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,252 @@
// 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
//
#pragma once
#include "ui/rp_widget.h"
#include "base/object_ptr.h"
#include <QtCore/QRect>
#include <QtCore/QPoint>
namespace style {
struct WindowTitle;
} // namespace style
namespace Ui {
class IconButton;
class AbstractButton;
class PlainShadow;
class RpWindow;
namespace Platform {
class TitleControls;
class TitleControlsLayout;
enum class HitTestResult {
None = 0,
Client,
Minimize,
MaximizeRestore,
Close,
Caption,
Top,
TopRight,
Right,
BottomRight,
Bottom,
BottomLeft,
Left,
TopLeft,
};
struct HitTestRequest {
QPoint point;
HitTestResult result = HitTestResult::Client;
};
[[nodiscard]] bool SemiNativeSystemButtonProcessing();
void SetupSemiNativeSystemButtons(
not_null<TitleControls*> controls,
not_null<RpWindow*> window,
rpl::lifetime &lifetime,
Fn<bool()> filter = nullptr);
enum class TitleControl {
Unknown,
Minimize,
Maximize,
Close,
};
struct TitleLayout {
[[nodiscard]] inline bool onLeft() const {
if (ranges::contains(left, TitleControl::Close)) {
return true;
} else if (ranges::contains(right, TitleControl::Close)) {
return false;
} else if (left.size() > right.size()) {
return true;
}
return false;
}
std::vector<TitleControl> left;
std::vector<TitleControl> right;
};
class AbstractTitleButtons {
public:
[[nodiscard]] virtual object_ptr<AbstractButton> create(
not_null<QWidget*> parent,
TitleControl control,
const style::WindowTitle &st) = 0;
virtual void updateState(
bool active,
bool maximized,
const style::WindowTitle &st) = 0;
virtual void notifySynteticOver(TitleControl control, bool over) = 0;
virtual ~AbstractTitleButtons() = default;
};
class IconTitleButtons final : public AbstractTitleButtons {
public:
object_ptr<AbstractButton> create(
not_null<QWidget*> parent,
TitleControl control,
const style::WindowTitle &st) override;
void updateState(
bool active,
bool maximized,
const style::WindowTitle &st) override;
void notifySynteticOver(TitleControl control, bool over) override {
}
private:
QPointer<IconButton> _minimize;
QPointer<IconButton> _maximizeRestore;
QPointer<IconButton> _close;
};
class TitleControls final {
public:
TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize = nullptr);
TitleControls(
not_null<RpWidget*> parent,
const style::WindowTitle &st,
std::unique_ptr<AbstractTitleButtons> buttons,
Fn<void(bool maximized)> maximize = nullptr);
void setStyle(const style::WindowTitle &st);
[[nodiscard]] not_null<const style::WindowTitle*> st() const;
[[nodiscard]] TitleControlsLayout &layout() const;
[[nodiscard]] QRect geometry() const;
void setResizeEnabled(bool enabled);
void raise();
[[nodiscard]] HitTestResult hitTest(QPoint point) const;
void buttonOver(HitTestResult testResult);
void buttonDown(HitTestResult testResult);
using Control = TitleControl;
using Layout = TitleLayout;
private:
[[nodiscard]] not_null<RpWidget*> parent() const;
[[nodiscard]] not_null<QWidget*> window() const;
[[nodiscard]] AbstractButton *controlWidget(Control control) const;
void init(Fn<void(bool maximized)> maximize);
void updateButtonsState();
void updateControlsPosition();
void handleWindowStateChanged(Qt::WindowStates state = Qt::WindowNoState);
not_null<const style::WindowTitle*> _st;
const std::shared_ptr<TitleControlsLayout> _layout;
const std::unique_ptr<AbstractTitleButtons> _buttons;
object_ptr<AbstractButton> _minimize;
object_ptr<AbstractButton> _maximizeRestore;
object_ptr<AbstractButton> _close;
bool _maximizedState = false;
bool _activeState = false;
bool _resizeEnabled = true;
};
class TitleControlsLayout {
public:
virtual ~TitleControlsLayout() = default;
[[nodiscard]] static std::shared_ptr<TitleControlsLayout> Instance();
[[nodiscard]] TitleLayout current() const {
return _variable.current();
}
[[nodiscard]] rpl::producer<TitleLayout> value() const {
return _variable.value();
}
[[nodiscard]] rpl::producer<TitleLayout> changes() const {
return _variable.changes();
}
protected:
TitleControlsLayout(TitleLayout layout) : _variable(layout) {}
rpl::variable<TitleLayout> _variable;
private:
[[nodiscard]] static std::shared_ptr<TitleControlsLayout> Create();
};
class DefaultTitleWidget : public RpWidget {
public:
explicit DefaultTitleWidget(not_null<RpWidget*> parent);
[[nodiscard]] not_null<const style::WindowTitle*> st() const;
[[nodiscard]] TitleControlsLayout &layout() const;
[[nodiscard]] QRect controlsGeometry() const;
void setText(const QString &text);
void setStyle(const style::WindowTitle &st);
void setResizeEnabled(bool enabled);
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void mouseDoubleClickEvent(QMouseEvent *e) override;
private:
TitleControls _controls;
object_ptr<Ui::PlainShadow> _shadow;
bool _mousePressed = false;
};
struct SeparateTitleControls {
SeparateTitleControls(
QWidget *parent,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize);
SeparateTitleControls(
QWidget *parent,
const style::WindowTitle &st,
std::unique_ptr<AbstractTitleButtons> buttons,
Fn<void(bool maximized)> maximize);
RpWidget wrap;
TitleControls controls;
};
[[nodiscard]] auto SetupSeparateTitleControls(
not_null<RpWindow*> window,
const style::WindowTitle &st,
Fn<void(bool maximized)> maximize = nullptr,
rpl::producer<int> controlsTop = nullptr)
-> std::unique_ptr<SeparateTitleControls>;
[[nodiscard]] auto SetupSeparateTitleControls(
not_null<RpWindow*> window,
std::unique_ptr<SeparateTitleControls> created,
rpl::producer<int> controlsTop = nullptr)
-> std::unique_ptr<SeparateTitleControls>;
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,184 @@
// 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/win/ui_utility_win.h"
#include "ui/widgets/popup_menu.h"
#include <QtWidgets/QApplication>
#include <QtGui/QWindow>
#include <QtCore/QAbstractNativeEventFilter>
#include <windows.h>
#include <wrl/client.h>
#include <Shobjidl.h>
#include "base/event_filter.h"
#include <QTimer>
#include <QScreen>
using namespace Microsoft::WRL;
namespace Ui::Platform {
bool IsApplicationActive() {
return QApplication::activeWindow() != nullptr;
}
void IgnoreAllActivation(not_null<QWidget*> widget) {
widget->createWinId();
const auto handle = reinterpret_cast<HWND>(widget->winId());
Assert(handle != nullptr);
ShowWindow(handle, SW_HIDE);
const auto style = GetWindowLongPtr(handle, GWL_EXSTYLE);
SetWindowLongPtr(
handle,
GWL_EXSTYLE,
style | WS_EX_NOACTIVATE | WS_EX_APPWINDOW);
ShowWindow(handle, SW_SHOW);
}
std::optional<bool> IsOverlapped(
not_null<QWidget*> widget,
const QRect &rect) {
const auto handle = HWND(widget->winId());
Expects(handle != nullptr);
ComPtr<IVirtualDesktopManager> virtualDesktopManager;
HRESULT hr = CoCreateInstance(
CLSID_VirtualDesktopManager,
nullptr,
CLSCTX_ALL,
IID_PPV_ARGS(&virtualDesktopManager));
if (SUCCEEDED(hr)) {
BOOL isCurrent;
hr = virtualDesktopManager->IsWindowOnCurrentVirtualDesktop(
handle,
&isCurrent);
if (SUCCEEDED(hr) && !isCurrent) {
return true;
}
}
const auto nativeRect = [&] {
const auto topLeft = [&] {
const auto qpoints = rect.topLeft()
* widget->windowHandle()->devicePixelRatio();
POINT result{
qpoints.x(),
qpoints.y(),
};
ClientToScreen(handle, &result);
return result;
}();
const auto bottomRight = [&] {
const auto qpoints = rect.bottomRight()
* widget->windowHandle()->devicePixelRatio();
POINT result{
qpoints.x(),
qpoints.y(),
};
ClientToScreen(handle, &result);
return result;
}();
return RECT{
topLeft.x,
topLeft.y,
bottomRight.x,
bottomRight.y,
};
}();
std::vector<HWND> visited;
for (auto curHandle = handle;
curHandle != nullptr && !ranges::contains(visited, curHandle);
curHandle = GetWindow(curHandle, GW_HWNDPREV)) {
visited.push_back(curHandle);
if (curHandle == handle) {
continue;
}
RECT testRect, intersection;
if (IsWindowVisible(curHandle)
&& GetWindowRect(curHandle, &testRect)
&& IntersectRect(&intersection, &nativeRect, &testRect)) {
return true;
}
}
return false;
}
void ShowWindowMenu(not_null<QWidget*> widget, const QPoint &point) {
const auto handle = HWND(widget->winId());
const auto mapped = point * widget->windowHandle()->devicePixelRatio();
POINT p{ mapped.x(), mapped.y() };
ClientToScreen(handle, &p);
SendMessage(
handle,
0x313 /* WM_POPUPSYSTEMMENU */,
0,
MAKELPARAM(p.x, p.y));
}
void FixPopupMenuNativeEmojiPopup(not_null<PopupMenu*> menu) {
// Windows native emoji selector, that can be called by Win+. shortcut,
// is behaving strangely within an input field in a popup menu.
//
// When the selector is shown and a mouse button is pressed the system
// sends two events "MousePress + MouseRelease" to the popup menu, even
// before the button is physically released. That way we hide the menu
// on this MousePress, that we shouldn't have received (in case of
// input field in the main window no such events are sent at all).
//
// To workaround this we detect a WM_MOUSELEAVE event that is sent to
// the popup menu when the selector is shown and skip all mouse press
// events while we don't receive mouse move events. If we receive mouse
// move events that means the selector was hidden and the mouse is
// captured by the popup menu again.
class Filter final : public QAbstractNativeEventFilter {
public:
explicit Filter(not_null<PopupMenu*> menu) : _menu(menu) {
}
bool nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) override {
const auto msg = static_cast<MSG*>(message);
switch (msg->message) {
case WM_MOUSELEAVE: if (msg->hwnd == hwnd()) {
_skipMouseDown = true;
} break;
case WM_MOUSEMOVE: if (msg->hwnd == hwnd()) {
_skipMouseDown = false;
} break;
case WM_LBUTTONDOWN:
case WM_LBUTTONDBLCLK: if (msg->hwnd == hwnd()) {
return _skipMouseDown;
}
}
return false;
}
private:
[[nodiscard]] HWND hwnd() const {
const auto top = _menu->window()->windowHandle();
return top ? reinterpret_cast<HWND>(top->winId()) : nullptr;
}
not_null<PopupMenu*> _menu;
bool _skipMouseDown = false;
};
QGuiApplication::instance()->installNativeEventFilter(
menu->lifetime().make_state<Filter>(menu));
}
} // namespace Ui::Platform

View File

@@ -0,0 +1,56 @@
// 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
//
#pragma once
#include "ui/platform/ui_platform_utility.h"
#include <QtCore/QPoint>
class QPainter;
class QPaintEvent;
namespace Ui {
namespace Platform {
inline bool TranslucentWindowsSupported() {
return true;
}
inline void InitOnTopPanel(not_null<QWidget*> panel) {
}
inline void DeInitOnTopPanel(not_null<QWidget*> panel) {
}
inline void ReInitOnTopPanel(not_null<QWidget*> panel) {
}
inline void ShowOverAll(not_null<QWidget*> widget, bool canFocus) {
}
inline void AcceptAllMouseInput(not_null<QWidget*> widget) {
}
inline void ClearTransientParent(not_null<QWidget*> widget) {
}
inline void DisableSystemWindowResize(not_null<QWidget*> widget, QSize ratio) {
}
inline constexpr bool UseMainQueueGeneric() {
return true;
}
inline bool WindowMarginsSupported() {
return false;
}
inline void SetWindowMargins(not_null<QWidget*> widget, const QMargins &margins) {
}
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,643 @@
// 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/win/ui_window_shadow_win.h"
#include "ui/rp_widget.h"
#include "ui/platform/win/ui_window_win.h"
#include "base/platform/base_platform_info.h"
#include "styles/style_widgets.h"
#include <QtGui/QPainter>
#include <QtGui/QScreen>
#include <QtWidgets/QApplication>
#include <windowsx.h>
// WM_POINTER support from Windows 8 onwards (WINVER >= 0x0602)
#ifndef WM_POINTERUPDATE
# define WM_NCPOINTERUPDATE 0x0241
# define WM_NCPOINTERDOWN 0x0242
# define WM_NCPOINTERUP 0x0243
# define WM_POINTERUPDATE 0x0245
# define WM_POINTERDOWN 0x0246
# define WM_POINTERUP 0x0247
# define WM_POINTERENTER 0x0249
# define WM_POINTERLEAVE 0x024A
# define WM_POINTERACTIVATE 0x024B
# define WM_POINTERCAPTURECHANGED 0x024C
# define WM_POINTERWHEEL 0x024E
# define WM_POINTERHWHEEL 0x024F
#endif // WM_POINTERUPDATE
namespace Ui {
namespace Platform {
namespace {
base::flat_map<HWND, not_null<WindowShadow*>> ShadowByHandle;
} // namespace
WindowShadow::WindowShadow(not_null<RpWidget*> window, QColor color)
: _window(window) {
setColor(color);
window->winIdValue(
) | rpl::on_next([=](WId id) {
destroy();
_handle = reinterpret_cast<HWND>(id);
create();
}, window->lifetime());
}
WindowShadow::~WindowShadow() {
destroy();
}
void WindowShadow::setColor(QColor value) {
_r = value.red();
_g = value.green();
_b = value.blue();
if (working()) {
updateColor();
}
}
void WindowShadow::updateColor() {
auto brush = getBrush(_alphas[0]);
for (auto i = 0; i != 4; ++i) {
auto graphics = Gdiplus::Graphics(_contexts[i]);
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
if ((i % 2) && _h || !(i % 2) && _w) {
const auto width = (i % 2) ? _size : _w;
const auto height = (i % 2) ? _h : _size;
graphics.FillRectangle(&brush, 0, 0, width, height);
}
}
initCorners();
_x = _y = _w = _h = 0;
update(Change::Moved | Change::Resized);
}
bool WindowShadow::working() const {
return (_handle != nullptr) && (_handles[0] != nullptr);
}
void WindowShadow::destroy() {
for (int i = 0; i < 4; ++i) {
if (_contexts[i]) {
DeleteDC(_contexts[i]);
_contexts[i] = nullptr;
}
if (_bitmaps[i]) {
DeleteObject(_bitmaps[i]);
_bitmaps[i] = nullptr;
}
if (_handles[i]) {
ShadowByHandle.remove(_handles[i]);
DestroyWindow(_handles[i]);
_handles[i] = nullptr;
}
}
if (_screenContext) {
ReleaseDC(nullptr, _screenContext);
_screenContext = nullptr;
}
}
void WindowShadow::create() {
if (!_handle) {
return;
}
initBlend();
_fullsize = st::windowShadow.width();
_shift = st::windowShadowShift;
auto cornersImage = QImage(
QSize(_fullsize, _fullsize),
QImage::Format_ARGB32_Premultiplied);
cornersImage.fill(QColor(0, 0, 0));
{
QPainter p(&cornersImage);
p.setCompositionMode(QPainter::CompositionMode_Source);
st::windowShadow.paint(p, 0, 0, _fullsize, QColor(255, 255, 255));
}
if (style::RightToLeft()) {
cornersImage = cornersImage.mirrored(true, false);
}
const auto pixels = cornersImage.bits();
const auto pixel = [&](int x, int y) {
if (x < 0 || y < 0) {
return 0;
}
const auto data = pixels
+ (cornersImage.bytesPerLine() * y)
+ (sizeof(uint32) * x);
return int(data[0]);
};
_metaSize = _fullsize + 2 * _shift;
_alphas.reserve(_metaSize);
_colors.reserve(_metaSize * _metaSize);
for (auto j = 0; j != _metaSize; ++j) {
for (auto i = 0; i != _metaSize; ++i) {
const auto value = pixel(i - 2 * _shift, j - 2 * _shift);
_colors.push_back(uchar(std::max(value, 1)));
}
}
auto previous = uchar(0);
for (auto i = 0; i != _metaSize; ++i) {
const auto alpha = _colors[(_metaSize - 1) * _metaSize + i];
if (alpha < previous) {
break;
}
_alphas.push_back(alpha);
previous = alpha;
}
_size = _alphas.size() - 2 * _shift;
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
Gdiplus::Status gdiRes = Gdiplus::GdiplusStartup(
&gdiplusToken,
&gdiplusStartupInput,
NULL);
if (gdiRes != Gdiplus::Ok) {
return;
}
_screenContext = GetDC(nullptr);
if (!_screenContext) {
return;
}
const auto avail = QApplication::primaryScreen()->availableGeometry();
_widthMax = std::max(avail.width(), 1);
_heightMax = std::max(avail.height(), 1);
static const auto instance = (HINSTANCE)GetModuleHandle(nullptr);
static const auto className = u"WindowShadow"_q;
static const auto wcharClassName = className.toStdWString();
static const auto registered = [] {
auto wc = WNDCLASSEX();
wc.cbSize = sizeof(wc);
wc.style = 0;
wc.lpfnWndProc = WindowCallback;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = instance;
wc.hIcon = 0;
wc.hCursor = 0;
wc.hbrBackground = 0;
wc.lpszMenuName = NULL;
wc.lpszClassName = wcharClassName.c_str();
wc.hIconSm = 0;
return RegisterClassEx(&wc) ? true : false;
}();
if (!registered) {
return;
}
for (auto i = 0; i != 4; ++i) {
_handles[i] = CreateWindowEx(
WS_EX_LAYERED | WS_EX_TOOLWINDOW,
wcharClassName.c_str(),
0,
WS_POPUP,
0,
0,
0,
0,
0,
0,
instance,
0);
if (!_handles[i]) {
destroy();
return;
}
ShadowByHandle.emplace(_handles[i], this);
SetWindowLongPtr(_handles[i], GWLP_HWNDPARENT, (LONG_PTR)_handle);
_contexts[i] = CreateCompatibleDC(_screenContext);
if (!_contexts[i]) {
destroy();
return;
}
const auto width = (i % 2) ? _size : _widthMax;
const auto height = (i % 2) ? _heightMax : _size;
_bitmaps[i] = CreateCompatibleBitmap(_screenContext, width, height);
if (!_bitmaps[i]) {
return;
}
SelectObject(_contexts[i], _bitmaps[i]);
}
updateColor();
}
void WindowShadow::initCorners(Directions directions) {
const auto hor = (directions & Direction::Horizontal);
const auto ver = (directions & Direction::Vertical);
auto graphics0 = Gdiplus::Graphics(_contexts[0]);
auto graphics1 = Gdiplus::Graphics(_contexts[1]);
auto graphics2 = Gdiplus::Graphics(_contexts[2]);
auto graphics3 = Gdiplus::Graphics(_contexts[3]);
graphics0.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
graphics1.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
graphics2.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
graphics3.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
auto brush = getBrush(_alphas[0]);
if (hor) {
graphics0.FillRectangle(&brush, 0, 0, _fullsize - (_size - _shift), 2 * _shift);
}
if (ver) {
graphics1.FillRectangle(&brush, 0, 0, _size, 2 * _shift);
graphics3.FillRectangle(&brush, 0, 0, _size, 2 * _shift);
graphics1.FillRectangle(&brush, _size - _shift, 2 * _shift, _shift, _fullsize);
graphics3.FillRectangle(&brush, 0, 2 * _shift, _shift, _fullsize);
}
if (hor) {
for (int j = 2 * _shift; j < _size; ++j) {
for (int k = 0; k < _fullsize - (_size - _shift); ++k) {
brush.SetColor(getColor(_colors[j * _metaSize + k + (_size + _shift)]));
graphics0.FillRectangle(&brush, k, j, 1, 1);
graphics2.FillRectangle(&brush, k, _size - (j - 2 * _shift) - 1, 1, 1);
}
}
for (int j = _size; j < _size + 2 * _shift; ++j) {
for (int k = 0; k < _fullsize - (_size - _shift); ++k) {
brush.SetColor(getColor(_colors[j * _metaSize + k + (_size + _shift)]));
graphics2.FillRectangle(&brush, k, _size - (j - 2 * _shift) - 1, 1, 1);
}
}
}
if (ver) {
for (int j = 2 * _shift; j < _fullsize + 2 * _shift; ++j) {
for (int k = _shift; k < _size; ++k) {
brush.SetColor(getColor(_colors[j * _metaSize + (k + _shift)]));
graphics1.FillRectangle(&brush, _size - k - 1, j, 1, 1);
graphics3.FillRectangle(&brush, k, j, 1, 1);
}
}
}
}
void WindowShadow::verCorners(int h, Gdiplus::Graphics *pgraphics1, Gdiplus::Graphics *pgraphics3) {
auto brush = getBrush(_alphas[0]);
pgraphics1->FillRectangle(&brush, _size - _shift, h - _fullsize, _shift, _fullsize);
pgraphics3->FillRectangle(&brush, 0, h - _fullsize, _shift, _fullsize);
for (int j = 0; j < _fullsize; ++j) {
for (int k = _shift; k < _size; ++k) {
brush.SetColor(getColor(_colors[(j + 2 * _shift) * _metaSize + k + _shift]));
pgraphics1->FillRectangle(&brush, _size - k - 1, h - j - 1, 1, 1);
pgraphics3->FillRectangle(&brush, k, h - j - 1, 1, 1);
}
}
}
void WindowShadow::horCorners(int w, Gdiplus::Graphics *pgraphics0, Gdiplus::Graphics *pgraphics2) {
auto brush = getBrush(_alphas[0]);
pgraphics0->FillRectangle(&brush, w - 2 * _size - (_fullsize - (_size - _shift)), 0, _fullsize - (_size - _shift), 2 * _shift);
for (int j = 2 * _shift; j < _size; ++j) {
for (int k = 0; k < _fullsize - (_size - _shift); ++k) {
brush.SetColor(getColor(_colors[j * _metaSize + k + (_size + _shift)]));
pgraphics0->FillRectangle(&brush, w - 2 * _size - k - 1, j, 1, 1);
pgraphics2->FillRectangle(&brush, w - 2 * _size - k - 1, _size - (j - 2 * _shift) - 1, 1, 1);
}
}
for (int j = _size; j < _size + 2 * _shift; ++j) {
for (int k = 0; k < _fullsize - (_size - _shift); ++k) {
brush.SetColor(getColor(_colors[j * _metaSize + k + (_size + _shift)]));
pgraphics2->FillRectangle(&brush, w - 2 * _size - k - 1, _size - (j - 2 * _shift) - 1, 1, 1);
}
}
}
Gdiplus::Color WindowShadow::getColor(uchar alpha) const {
return Gdiplus::Color(BYTE(::Platform::IsWindows11OrGreater() ? 1 : alpha), _r, _g, _b);
}
Gdiplus::SolidBrush WindowShadow::getBrush(uchar alpha) const {
return Gdiplus::SolidBrush(getColor(alpha));
}
Gdiplus::Pen WindowShadow::getPen(uchar alpha) const {
return Gdiplus::Pen(getColor(alpha));
}
void WindowShadow::update(Changes changes, WINDOWPOS *pos) {
if (!working()) {
return;
}
if (changes == Changes(Change::Activate)) {
for (int i = 0; i < 4; ++i) {
SetWindowPos(_handles[i], _handle, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
return;
}
if (changes & Change::Hidden) {
if (!_hidden) {
for (int i = 0; i < 4; ++i) {
_hidden = true;
ShowWindow(_handles[i], SW_HIDE);
}
}
return;
}
if (_window->isHidden()) {
return;
}
int x = _x, y = _y, w = _w, h = _h;
if (pos && (!(pos->flags & SWP_NOMOVE) || !(pos->flags & SWP_NOSIZE) || !(pos->flags & SWP_NOREPOSITION))) {
if (!(pos->flags & SWP_NOMOVE)) {
x = pos->x - _size;
y = pos->y - _size;
} else if (pos->flags & SWP_NOSIZE) {
for (int i = 0; i < 4; ++i) {
SetWindowPos(_handles[i], _handle, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
return;
}
if (!(pos->flags & SWP_NOSIZE)) {
w = pos->cx + 2 * _size;
h = pos->cy + 2 * _size;
}
} else {
RECT r;
GetWindowRect(_handle, &r);
x = r.left - _size;
y = r.top - _size;
w = r.right + _size - x;
h = r.bottom + _size - y;
}
if (h < 2 * _fullsize + 2 * _shift) {
h = 2 * _fullsize + 2 * _shift;
}
if (w < 2 * (_fullsize + _shift)) {
w = 2 * (_fullsize + _shift);
}
if (w != _w) {
int from = (_w > 2 * (_fullsize + _shift)) ? (_w - _size - _fullsize - _shift) : (_fullsize - (_size - _shift));
int to = w - _size - _fullsize - _shift;
if (w > _widthMax) {
from = _fullsize - (_size - _shift);
do {
_widthMax *= 2;
} while (w > _widthMax);
for (int i = 0; i < 4; i += 2) {
DeleteObject(_bitmaps[i]);
_bitmaps[i] = CreateCompatibleBitmap(_screenContext, _widthMax, _size);
SelectObject(_contexts[i], _bitmaps[i]);
}
initCorners(Direction::Horizontal);
}
Gdiplus::Graphics graphics0(_contexts[0]);
Gdiplus::Graphics graphics2(_contexts[2]);
graphics0.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
graphics2.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
auto brush = getBrush(_alphas[0]);
if (to > from) {
graphics0.FillRectangle(&brush, from, 0, to - from, 2 * _shift);
for (int i = 2 * _shift; i < _size; ++i) {
auto pen = getPen(_alphas[i]);
graphics0.DrawLine(&pen, from, i, to, i);
graphics2.DrawLine(&pen, from, _size - (i - 2 * _shift) - 1, to, _size - (i - 2 * _shift) - 1);
}
for (int i = _size; i < _size + 2 * _shift; ++i) {
auto pen = getPen(_alphas[i]);
graphics2.DrawLine(&pen, from, _size - (i - 2 * _shift) - 1, to, _size - (i - 2 * _shift) - 1);
}
}
if (_w > w) {
graphics0.FillRectangle(&brush, w - _size - _fullsize - _shift, 0, _fullsize - (_size - _shift), _size);
graphics2.FillRectangle(&brush, w - _size - _fullsize - _shift, 0, _fullsize - (_size - _shift), _size);
}
horCorners(w, &graphics0, &graphics2);
POINT p0 = { x + _size, y }, p2 = { x + _size, y + h - _size }, f = { 0, 0 };
SIZE s = { w - 2 * _size, _size };
updateWindow(0, &p0, &s);
updateWindow(2, &p2, &s);
} else if (x != _x || y != _y) {
POINT p0 = { x + _size, y }, p2 = { x + _size, y + h - _size };
updateWindow(0, &p0);
updateWindow(2, &p2);
} else if (h != _h) {
POINT p2 = { x + _size, y + h - _size };
updateWindow(2, &p2);
}
if (h != _h) {
int from = (_h > 2 * _fullsize + 2 * _shift) ? (_h - _fullsize) : (_fullsize + 2 * _shift);
int to = h - _fullsize;
if (h > _heightMax) {
from = (_fullsize + 2 * _shift);
do {
_heightMax *= 2;
} while (h > _heightMax);
for (int i = 1; i < 4; i += 2) {
DeleteObject(_bitmaps[i]);
_bitmaps[i] = CreateCompatibleBitmap(_contexts[i], _size, _heightMax);
SelectObject(_contexts[i], _bitmaps[i]);
}
initCorners(Direction::Vertical);
}
Gdiplus::Graphics graphics1(_contexts[1]), graphics3(_contexts[3]);
graphics1.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
graphics3.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
auto brush = getBrush(_alphas[0]);
if (to > from) {
graphics1.FillRectangle(&brush, _size - _shift, from, _shift, to - from);
graphics3.FillRectangle(&brush, 0, from, _shift, to - from);
for (int i = 2 * _shift; i < _size + _shift; ++i) {
auto pen = getPen(_alphas[i]);
graphics1.DrawLine(&pen, _size + _shift - i - 1, from, _size + _shift - i - 1, to);
graphics3.DrawLine(&pen, i - _shift, from, i - _shift, to);
}
}
if (_h > h) {
graphics1.FillRectangle(&brush, 0, h - _fullsize, _size, _fullsize);
graphics3.FillRectangle(&brush, 0, h - _fullsize, _size, _fullsize);
}
verCorners(h, &graphics1, &graphics3);
POINT p1 = { x + w - _size, y }, p3 = { x, y }, f = { 0, 0 };
SIZE s = { _size, h };
updateWindow(1, &p1, &s);
updateWindow(3, &p3, &s);
} else if (x != _x || y != _y) {
POINT p1 = { x + w - _size, y }, p3 = { x, y };
updateWindow(1, &p1);
updateWindow(3, &p3);
} else if (w != _w) {
POINT p1 = { x + w - _size, y };
updateWindow(1, &p1);
}
_x = x;
_y = y;
_w = w;
_h = h;
if (_hidden && (changes & Change::Shown)) {
for (int i = 0; i < 4; ++i) {
SetWindowPos(
_handles[i],
_handle,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW | SWP_NOACTIVATE);
}
_hidden = false;
}
}
void WindowShadow::initBlend() {
_blend.AlphaFormat = AC_SRC_ALPHA;
_blend.SourceConstantAlpha = 255;
_blend.BlendFlags = 0;
_blend.BlendOp = AC_SRC_OVER;
}
void WindowShadow::updateWindow(int i, POINT *p, SIZE *s) {
static POINT f = { 0, 0 };
if (s) {
UpdateLayeredWindow(
_handles[i],
(s ? _screenContext : nullptr),
p,
s,
(s ? _contexts[i] : nullptr),
(s ? (&f) : nullptr),
_noKeyColor,
&_blend,
ULW_ALPHA);
} else {
SetWindowPos(
_handles[i],
0,
p->x,
p->y,
0,
0,
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER);
}
}
void WindowShadow::setResizeEnabled(bool enabled) {
_resizeEnabled = enabled;
}
LRESULT CALLBACK WindowShadow::WindowCallback(
HWND hwnd,
UINT msg,
WPARAM wParam,
LPARAM lParam) {
const auto i = ShadowByHandle.find(hwnd);
return (i != end(ShadowByHandle))
? i->second->windowCallback(hwnd, msg, wParam, lParam)
: DefWindowProc(hwnd, msg, wParam, lParam);
}
LRESULT WindowShadow::windowCallback(
HWND hwnd,
UINT msg,
WPARAM wParam,
LPARAM lParam) {
if (!working()) {
return DefWindowProc(hwnd, msg, wParam, lParam);
}
switch (msg) {
case WM_CLOSE:
_window->close();
return 0;
case WM_NCHITTEST: {
if (!_resizeEnabled) {
return HTNOWHERE;
}
const auto yPos = GET_Y_LPARAM(lParam);
if (hwnd == _handles[0]) {
return HTTOP;
} else if (hwnd == _handles[1]) {
return (yPos < _y + _size)
? HTTOPRIGHT
: (yPos >= _y + _h - _size)
? HTBOTTOMRIGHT
: HTRIGHT;
} else if (hwnd == _handles[2]) {
return HTBOTTOM;
} else if (hwnd == _handles[3]) {
return (yPos < _y + _size)
? HTTOPLEFT
: (yPos >= _y + _h - _size)
? HTBOTTOMLEFT
: HTLEFT;
} else {
Unexpected("Handle in WindowShadow::windowCallback.");
}
} break;
case WM_NCACTIVATE:
return DefWindowProc(hwnd, msg, wParam, lParam);
case WM_NCLBUTTONDOWN:
case WM_NCLBUTTONUP:
case WM_NCLBUTTONDBLCLK:
case WM_NCMBUTTONDOWN:
case WM_NCMBUTTONUP:
case WM_NCMBUTTONDBLCLK:
case WM_NCRBUTTONDOWN:
case WM_NCRBUTTONUP:
case WM_NCRBUTTONDBLCLK:
case WM_NCXBUTTONDOWN:
case WM_NCXBUTTONUP:
case WM_NCXBUTTONDBLCLK:
case WM_NCMOUSEHOVER:
case WM_NCMOUSELEAVE:
case WM_NCMOUSEMOVE:
case WM_NCPOINTERUPDATE:
case WM_NCPOINTERDOWN:
case WM_NCPOINTERUP:
if (msg == WM_NCLBUTTONDOWN) {
::SetForegroundWindow(_handle);
}
return SendMessage(_handle, msg, wParam, lParam);
case WM_ACTIVATE:
if (wParam == WA_ACTIVE) {
if ((HWND)lParam != _handle) {
::SetForegroundWindow(hwnd);
::SetWindowPos(
_handle,
hwnd,
0,
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE);
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
default:
return DefWindowProc(hwnd, msg, wParam, lParam);
}
}
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,107 @@
// 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
//
#pragma once
#include "base/platform/win/base_windows_rpcndr_h.h"
#include "base/platform/win/base_windows_gdiplus_h.h"
#include "base/flags.h"
class QColor;
namespace Ui {
class RpWidget;
namespace Platform {
class WindowShadow final {
public:
WindowShadow(not_null<RpWidget*> window, QColor color);
~WindowShadow();
enum class Change {
Moved = (1 << 0),
Resized = (1 << 1),
Activate = (1 << 2),
Deactivate = (1 << 3),
Hidden = (1 << 4),
Shown = (1 << 5),
};
friend inline constexpr bool is_flag_type(Change) { return true; };
using Changes = base::flags<Change>;
void setColor(QColor color);
void update(Changes changes, WINDOWPOS *pos = nullptr);
void updateWindow(int i, POINT *p, SIZE *s = nullptr);
void setResizeEnabled(bool enabled);
private:
enum class Direction {
Horizontal = (1 << 0),
Vertical = (1 << 1),
All = Horizontal | Vertical,
};
friend inline constexpr bool is_flag_type(Direction) { return true; };
using Directions = base::flags<Direction>;
static LRESULT CALLBACK WindowCallback(
HWND hwnd,
UINT msg,
WPARAM wParam,
LPARAM lParam);
LRESULT windowCallback(
HWND hwnd,
UINT msg,
WPARAM wParam,
LPARAM lParam);
[[nodiscard]] bool working() const;
void destroy();
void create();
void initBlend();
void updateColor();
void initCorners(Directions directions = Direction::All);
void verCorners(int h, Gdiplus::Graphics *pgraphics1, Gdiplus::Graphics *pgraphics3);
void horCorners(int w, Gdiplus::Graphics *pgraphics0, Gdiplus::Graphics *pgraphics2);
[[nodiscard]] Gdiplus::Color getColor(uchar alpha) const;
[[nodiscard]] Gdiplus::SolidBrush getBrush(uchar alpha) const;
[[nodiscard]] Gdiplus::Pen getPen(uchar alpha) const;
const not_null<RpWidget*> _window;
HWND _handle = nullptr;
int _x = 0;
int _y = 0;
int _w = 0;
int _h = 0;
int _metaSize = 0;
int _fullsize = 0;
int _size = 0;
int _shift = 0;
std::vector<BYTE> _alphas;
std::vector<BYTE> _colors;
bool _hidden = true;
bool _resizeEnabled = true;
HWND _handles[4] = { nullptr };
HDC _contexts[4] = { nullptr };
HBITMAP _bitmaps[4] = { nullptr };
HDC _screenContext = nullptr;
int _widthMax = 0;
int _heightMax = 0;
BLENDFUNCTION _blend;
BYTE _r = 0;
BYTE _g = 0;
BYTE _b = 0;
COLORREF _noKeyColor = RGB(255, 255, 255);
};
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,262 @@
// 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/win/ui_window_title_win.h"
#include "ui/platform/win/ui_window_win.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/rp_window.h"
#include "ui/ui_utility.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/win/base_windows_safe_library.h"
#include "base/debug_log.h"
#include "styles/style_widgets.h"
#include "styles/palette.h"
#include <QtGui/QPainter>
#include <QtGui/QtEvents>
#include <QtGui/QWindow>
#include <windows.h>
#include <shellscalingapi.h>
namespace Ui {
namespace Platform {
namespace {
HRESULT(__stdcall *GetScaleFactorForMonitor)(
_In_ HMONITOR hMon,
_Out_ DEVICE_SCALE_FACTOR *pScale);
[[nodiscard]] bool ScaleQuerySupported() {
static const auto Result = [&] {
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
const auto shcore = base::Platform::SafeLoadLibrary(L"Shcore.dll");
return LOAD_SYMBOL(shcore, GetScaleFactorForMonitor);
#undef LOAD_SYMBOL
}();
return Result;
}
} // namespace
std::shared_ptr<TitleControlsLayout> TitleControlsLayout::Create() {
return std::shared_ptr<TitleControlsLayout>(new TitleControlsLayout({
.right = {
TitleControls::Control::Minimize,
TitleControls::Control::Maximize,
TitleControls::Control::Close,
}
}));
}
struct TitleWidget::PaddingHelper {
explicit PaddingHelper(QWidget *parent) : controlsParent(parent) {
}
RpWidget controlsParent;
rpl::variable<int> padding = 0;
};
TitleWidget::TitleWidget(not_null<RpWidget*> parent)
: RpWidget(parent)
, _paddingHelper(CheckTitlePaddingRequired()
? std::make_unique<PaddingHelper>(this)
: nullptr)
, _controls(
_paddingHelper ? &_paddingHelper->controlsParent : this,
st::defaultWindowTitle)
, _shadow(this, st::titleShadow) {
setAttribute(Qt::WA_OpaquePaintEvent);
parent->widthValue(
) | rpl::on_next([=](int width) {
refreshGeometryWithWidth(width);
}, lifetime());
}
void TitleWidget::initInWindow(not_null<RpWindow*> window) {
window->hitTestRequests(
) | rpl::filter([=](not_null<HitTestRequest*> request) {
return !isHidden() && geometry().contains(request->point);
}) | rpl::on_next([=](not_null<HitTestRequest*> request) {
request->result = hitTest(request->point);
}, lifetime());
SetupSemiNativeSystemButtons(&_controls, window, lifetime(), [=] {
return !isHidden() && (_controls.st()->height > 0);
});
}
TitleWidget::~TitleWidget() = default;
void TitleWidget::setText(const QString &text) {
window()->setWindowTitle(text);
}
void TitleWidget::setStyle(const style::WindowTitle &st) {
_controls.setStyle(st);
if (!st.shadow) {
_shadow.destroy();
} else if (!_shadow) {
_shadow.create(this, st::titleShadow);
updateShadowGeometry();
}
refreshGeometryWithWidth(window()->width());
}
void TitleWidget::updateShadowGeometry() {
const auto thickness = st::lineWidth;
_shadow->setGeometry(0, height() - thickness, width(), thickness);
}
void TitleWidget::refreshGeometryWithWidth(int width) {
const auto add = additionalPadding();
setGeometry(0, 0, width, _controls.st()->height + add);
if (_paddingHelper) {
_paddingHelper->controlsParent.setGeometry(
0,
add,
width,
_controls.st()->height);
}
update();
}
not_null<const style::WindowTitle*> TitleWidget::st() const {
return _controls.st();
}
void TitleWidget::setResizeEnabled(bool enabled) {
_controls.setResizeEnabled(enabled);
}
void TitleWidget::paintEvent(QPaintEvent *e) {
const auto active = window()->isActiveWindow();
QPainter(this).fillRect(
e->rect(),
active ? _controls.st()->bgActive : _controls.st()->bg);
}
void TitleWidget::resizeEvent(QResizeEvent *e) {
if (_shadow) {
updateShadowGeometry();
}
}
HitTestResult TitleWidget::hitTest(QPoint point) const {
const auto controlsResult = _controls.hitTest(point);
return (controlsResult != HitTestResult::None)
? controlsResult
: HitTestResult::Caption;
}
bool TitleWidget::additionalPaddingRequired() const {
return _paddingHelper && !isHidden();
}
void TitleWidget::refreshAdditionalPaddings() {
if (!additionalPaddingRequired()) {
return;
} else if (const auto handle = GetCurrentHandle(this)) {
refreshAdditionalPaddings(handle);
}
}
void TitleWidget::refreshAdditionalPaddings(HWND handle) {
if (!additionalPaddingRequired()) {
return;
}
auto placement = WINDOWPLACEMENT{
.length = sizeof(WINDOWPLACEMENT),
};
if (!GetWindowPlacement(handle, &placement)) {
LOG(("System Error: GetWindowPlacement failed."));
return;
}
refreshAdditionalPaddings(handle, placement);
}
void TitleWidget::refreshAdditionalPaddings(
HWND handle,
const WINDOWPLACEMENT &placement) {
if (!additionalPaddingRequired()) {
return;
}
auto geometry = RECT();
if (!GetWindowRect(handle, &geometry)) {
LOG(("System Error: GetWindowRect failed."));
return;
}
const auto normal = placement.rcNormalPosition;
const auto rounded = (normal.left == geometry.left)
&& (normal.right == geometry.right)
&& (normal.top == geometry.top)
&& (normal.bottom == geometry.bottom);
const auto padding = [&] {
if (!rounded) {
return 0;
}
const auto monitor = MonitorFromWindow(
handle,
MONITOR_DEFAULTTONEAREST);
if (!monitor) {
LOG(("System Error: MonitorFromWindow failed."));
return -1;
}
auto factor = DEVICE_SCALE_FACTOR();
if (!SUCCEEDED(GetScaleFactorForMonitor(monitor, &factor))) {
LOG(("System Error: GetScaleFactorForMonitor failed."));
return -1;
} else if (factor < 100 || factor > 500) {
LOG(("System Error: Bad scale factor %1.").arg(int(factor)));
return -1;
}
const auto pixels = (factor + 50) / 100;
return int(base::SafeRound(
pixels / window()->windowHandle()->devicePixelRatio()));
}();
if (padding < 0) {
return;
}
setAdditionalPadding(padding);
}
int TitleWidget::additionalPadding() const {
return _paddingHelper ? _paddingHelper->padding.current() : 0;
}
rpl::producer<int> TitleWidget::additionalPaddingValue() const {
return _paddingHelper ? _paddingHelper->padding.value() : rpl::single(0);
}
void TitleWidget::setAdditionalPadding(int padding) {
Expects(_paddingHelper != nullptr);
padding /= window()->devicePixelRatio();
if (_paddingHelper->padding.current() == padding) {
return;
}
_paddingHelper->padding = padding;
refreshGeometryWithWidth(window()->width());
}
void TitleWidget::setVisibleHook(bool visible) {
RpWidget::setVisibleHook(visible);
if (additionalPaddingRequired()) {
PostponeCall(this, [=] {
refreshAdditionalPaddings();
});
}
}
bool CheckTitlePaddingRequired() {
return ::Platform::IsWindows11OrGreater() && ScaleQuerySupported();
}
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,73 @@
// 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
//
#pragma once
#include "ui/platform/ui_platform_window_title.h"
#include "ui/rp_widget.h"
#include "base/object_ptr.h"
#include <QtCore/QRect>
#include <QtCore/QPoint>
#include <Windows.h> // HWND, WINDOWPLACEMENT
namespace style {
struct WindowTitle;
} // namespace style
namespace Ui {
class RpWindow;
class IconButton;
class PlainShadow;
namespace Platform {
class TitleWidget : public RpWidget {
public:
explicit TitleWidget(not_null<RpWidget*> parent);
~TitleWidget();
void initInWindow(not_null<RpWindow*> window);
void setText(const QString &text);
void setStyle(const style::WindowTitle &st);
[[nodiscard]] not_null<const style::WindowTitle*> st() const;
void setResizeEnabled(bool enabled);
void refreshAdditionalPaddings();
void refreshAdditionalPaddings(HWND handle);
void refreshAdditionalPaddings(
HWND handle,
const WINDOWPLACEMENT &placement);
[[nodiscard]] int additionalPadding() const;
[[nodiscard]] rpl::producer<int> additionalPaddingValue() const;
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void setVisibleHook(bool visible) override;
private:
struct PaddingHelper;
[[nodiscard]] HitTestResult hitTest(QPoint point) const;
[[nodiscard]] bool additionalPaddingRequired() const;
void refreshGeometryWithWidth(int width);
void setAdditionalPadding(int padding);
void updateShadowGeometry();
std::unique_ptr<PaddingHelper> _paddingHelper;
TitleControls _controls;
object_ptr<Ui::PlainShadow> _shadow;
};
[[nodiscard]] bool CheckTitlePaddingRequired();
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,973 @@
// 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/win/ui_window_win.h"
#include "ui/inactive_press.h"
#include "ui/platform/win/ui_window_title_win.h"
#include "ui/platform/win/ui_windows_direct_manipulation.h"
#include "ui/platform/ui_platform_utility.h"
#include "ui/widgets/rp_window.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/qt_object_factory.h"
#include "ui/ui_utility.h"
#include "base/platform/win/base_windows_safe_library.h"
#include "base/platform/base_platform_info.h"
#include "base/event_filter.h"
#include "base/integration.h"
#include "base/invoke_queued.h"
#include "base/debug_log.h"
#include "styles/palette.h"
#include "styles/style_widgets.h"
#include <QtCore/QPoint>
#include <QtGui/QWindow>
#include <QtWidgets/QStyleFactory>
#include <QtWidgets/QApplication>
#include <qpa/qplatformnativeinterface.h>
#include <qpa/qwindowsysteminterface.h>
#include <dwmapi.h>
#include <shellapi.h>
#include <uxtheme.h>
#include <windowsx.h>
Q_DECLARE_METATYPE(QMargins);
namespace Ui::Platform {
namespace {
constexpr auto kDWMWCP_ROUND = DWORD(2);
constexpr auto kDWMWCP_DONOTROUND = DWORD(1);
constexpr auto kDWMWA_WINDOW_CORNER_PREFERENCE = DWORD(33);
constexpr auto kDWMWA_CAPTION_COLOR = DWORD(35);
constexpr auto kDWMWA_TEXT_COLOR = DWORD(36);
UINT(__stdcall *GetDpiForWindow)(_In_ HWND hwnd);
int(__stdcall *GetSystemMetricsForDpi)(
_In_ int nIndex,
_In_ UINT dpi);
BOOL(__stdcall *AdjustWindowRectExForDpi)(
_Inout_ LPRECT lpRect,
_In_ DWORD dwStyle,
_In_ BOOL bMenu,
_In_ DWORD dwExStyle,
_In_ UINT dpi);
[[nodiscard]] bool GetDpiForWindowSupported() {
static const auto Result = [&] {
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
const auto user32 = base::Platform::SafeLoadLibrary(L"User32.dll");
return LOAD_SYMBOL(user32, GetDpiForWindow);
#undef LOAD_SYMBOL
}();
return Result;
}
[[nodiscard]] bool GetSystemMetricsForDpiSupported() {
static const auto Result = [&] {
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
const auto user32 = base::Platform::SafeLoadLibrary(L"User32.dll");
return LOAD_SYMBOL(user32, GetSystemMetricsForDpi);
#undef LOAD_SYMBOL
}();
return Result;
}
[[nodiscard]] bool AdjustWindowRectExForDpiSupported() {
static const auto Result = [&] {
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
const auto user32 = base::Platform::SafeLoadLibrary(L"User32.dll");
return LOAD_SYMBOL(user32, AdjustWindowRectExForDpi);
#undef LOAD_SYMBOL
}();
return Result;
}
[[nodiscard]] bool IsCompositionEnabled() {
auto result = BOOL(FALSE);
const auto success = (DwmIsCompositionEnabled(&result) == S_OK);
return success && result;
}
[[nodiscard]] HWND FindTaskbarWindow(LPRECT rcMon = nullptr) {
HWND hTaskbar = nullptr;
RECT rcTaskbar, rcMatch;
while ((hTaskbar = FindWindowEx(
nullptr,
hTaskbar,
L"Shell_TrayWnd",
nullptr)) != nullptr) {
if (!rcMon) {
break; // OK, return first found
}
if (GetWindowRect(hTaskbar, &rcTaskbar)
&& IntersectRect(&rcMatch, &rcTaskbar, rcMon)) {
break; // OK, taskbar match monitor
}
}
return hTaskbar;
}
[[nodiscard]] bool IsTaskbarAutoHidden(
LPRECT rcMon = nullptr,
PUINT pEdge = nullptr) {
HWND hTaskbar = FindTaskbarWindow(rcMon);
if (!hTaskbar) {
if (pEdge) {
*pEdge = (UINT)-1;
}
return false;
}
APPBARDATA state = {sizeof(state), hTaskbar};
APPBARDATA pos = {sizeof(pos), hTaskbar};
LRESULT lState = SHAppBarMessage(ABM_GETSTATE, &state);
bool bAutoHidden = (lState & ABS_AUTOHIDE);
if (SHAppBarMessage(ABM_GETTASKBARPOS, &pos)) {
if (pEdge) {
*pEdge = pos.uEdge;
}
} else {
DEBUG_LOG(("Failed to get taskbar pos"));
if (pEdge) {
*pEdge = ABE_BOTTOM;
}
}
return bAutoHidden;
}
void FixAeroSnap(HWND handle) {
SetWindowLongPtr(
handle,
GWL_STYLE,
GetWindowLongPtr(handle, GWL_STYLE) | WS_CAPTION | WS_THICKFRAME);
}
[[nodiscard]] Qt::KeyboardModifiers LookupModifiers() {
const auto check = [](int key) {
return (GetKeyState(key) & 0x8000) != 0;
};
auto result = Qt::KeyboardModifiers();
if (check(VK_SHIFT)) {
result |= Qt::ShiftModifier;
}
// NB AltGr key (i.e., VK_RMENU on some keyboard layout) is not handled.
if (check(VK_RMENU) || check(VK_MENU)) {
result |= Qt::AltModifier;
}
if (check(VK_CONTROL)) {
result |= Qt::ControlModifier;
}
if (check(VK_LWIN) || check(VK_RWIN)) {
result |= Qt::MetaModifier;
}
return result;
}
} // namespace
WindowHelper::WindowHelper(not_null<RpWidget*> window)
: BasicWindowHelper(window)
, NativeEventFilter(window)
, _title(Ui::CreateChild<TitleWidget>(window.get()))
, _body(Ui::CreateChild<RpWidget>(window.get())) {
if (!::Platform::IsWindows8OrGreater()) {
window->setWindowFlag(Qt::FramelessWindowHint);
}
init();
}
WindowHelper::~WindowHelper() = default;
void WindowHelper::initInWindow(not_null<RpWindow*> window) {
_title->initInWindow(window);
}
not_null<RpWidget*> WindowHelper::body() {
return _body;
}
QMargins WindowHelper::frameMargins() {
return _title->isHidden()
? BasicWindowHelper::nativeFrameMargins()
: QMargins{ 0, _title->height(), 0, 0 };
}
int WindowHelper::additionalContentPadding() const {
return _title->isHidden() ? 0 : _title->additionalPadding();
}
rpl::producer<int> WindowHelper::additionalContentPaddingValue() const {
return rpl::combine(
_title->shownValue(),
_title->additionalPaddingValue()
) | rpl::map([](bool shown, int padding) {
return shown ? padding : 0;
}) | rpl::distinct_until_changed();
}
void WindowHelper::setTitle(const QString &title) {
_title->setText(title);
// Windows shows them as FSI and PDI in squares(!)
window()->setWindowTitle(QString(title)
.replace(QChar(0x2068), QString())
.replace(QChar(0x2069), QString()));
}
void WindowHelper::setTitleStyle(const style::WindowTitle &st) {
_title->setStyle(st);
updateWindowFrameColors();
}
void WindowHelper::setNativeFrame(bool enabled) {
if (_handle && !::Platform::IsWindows8OrGreater()) {
window()->windowHandle()->setFlag(Qt::FramelessWindowHint, !enabled);
if (!enabled) {
FixAeroSnap(_handle);
}
}
_title->setVisible(!enabled);
if (_handle) {
updateShadow();
updateCornersRounding();
updateMargins();
updateWindowFrameColors();
fixMaximizedWindow();
SetWindowPos(
_handle,
0,
0,
0,
0,
0,
(SWP_FRAMECHANGED
| SWP_NOMOVE
| SWP_NOSIZE
| SWP_NOZORDER
| SWP_NOACTIVATE));
}
}
void WindowHelper::updateShadow() {
if (!_handle || _title->isHidden() || nativeResize()) {
_shadow.reset();
return;
}
_shadow.emplace(window(), st::windowShadowFg->c);
_shadow->setResizeEnabled(!fixedSize());
using Change = WindowShadow::Change;
const auto noShadowStates = (Qt::WindowMinimized | Qt::WindowMaximized);
if ((window()->windowState() & noShadowStates) || window()->isHidden()) {
_shadow->update(Change::Hidden);
} else {
_shadow->update(Change::Moved | Change::Resized | Change::Shown);
}
}
void WindowHelper::updateCornersRounding() {
if (!_handle || !::Platform::IsWindows11OrGreater()) {
return;
}
const auto preference = (_isFullScreen || _isMaximizedAndTranslucent)
? kDWMWCP_DONOTROUND
: kDWMWCP_ROUND;
DwmSetWindowAttribute(
_handle,
kDWMWA_WINDOW_CORNER_PREFERENCE,
&preference,
sizeof(preference));
}
void WindowHelper::setMinimumSize(QSize size) {
window()->setMinimumSize(size.width(), titleHeight() + size.height());
}
void WindowHelper::setFixedSize(QSize size) {
window()->setFixedSize(size.width(), titleHeight() + size.height());
_title->setResizeEnabled(false);
if (_shadow) {
_shadow->setResizeEnabled(false);
}
}
void WindowHelper::setGeometry(QRect rect) {
window()->setGeometry(rect.marginsAdded({ 0, titleHeight(), 0, 0 }));
}
void WindowHelper::showFullScreen() {
if (!_isFullScreen) {
_isFullScreen = true;
updateMargins();
updateCornersRounding();
updateCloaking();
}
window()->showFullScreen();
}
void WindowHelper::showNormal() {
window()->showNormal();
if (_isFullScreen) {
_isFullScreen = false;
updateMargins();
updateCornersRounding();
updateCloaking();
}
}
auto WindowHelper::hitTestRequests() const
-> rpl::producer<not_null<HitTestRequest*>> {
return _hitTestRequests.events();
}
rpl::producer<HitTestResult> WindowHelper::systemButtonOver() const {
return _systemButtonOver.events();
}
rpl::producer<HitTestResult> WindowHelper::systemButtonDown() const {
return _systemButtonDown.events();
}
void WindowHelper::overrideSystemButtonOver(HitTestResult button) {
_systemButtonOver.fire_copy(button);
}
void WindowHelper::overrideSystemButtonDown(HitTestResult button) {
_systemButtonDown.fire_copy(button);
}
void WindowHelper::init() {
_title->show();
window()->winIdValue() | rpl::on_next([=](WId winId) {
_handle = reinterpret_cast<HWND>(winId);
if (!::Platform::IsWindows8OrGreater()) {
const auto native = _title->isHidden();
window()->setWindowFlag(Qt::FramelessWindowHint, !native);
if (_handle && !native) {
FixAeroSnap(_handle);
}
}
if (_handle) {
_dpi = GetDpiForWindowSupported()
? GetDpiForWindow(_handle)
: 0;
updateWindowFrameColors();
updateShadow();
updateCornersRounding();
updateMargins();
if (window()->isHidden()) {
enableCloakingForHidden();
}
}
}, window()->lifetime());
style::PaletteChanged(
) | rpl::on_next([=] {
if (_shadow) {
_shadow->setColor(st::windowShadowFg->c);
}
updateWindowFrameColors();
Ui::ForceFullRepaint(window());
}, window()->lifetime());
rpl::combine(
window()->sizeValue(),
_title->heightValue(),
_title->shownValue()
) | rpl::on_next([=](
QSize size,
int titleHeight,
bool titleShown) {
_body->setGeometry(
0,
titleShown ? titleHeight : 0,
size.width(),
size.height() - (titleShown ? titleHeight : 0));
}, _body->lifetime());
_dpi.value() | rpl::on_next([=](uint dpi) {
updateMargins();
}, window()->lifetime());
if (_handle && !::Platform::IsWindows8OrGreater()) {
SetWindowTheme(_handle, L" ", L" ");
QApplication::setStyle(QStyleFactory::create("Windows"));
}
const auto handleStateChanged = [=](Qt::WindowState state) {
if (fixedSize() && (state & Qt::WindowMaximized)) {
crl::on_main(window().get(), [=] {
window()->setWindowState(
window()->windowState() & ~Qt::WindowMaximized);
});
}
if (state != Qt::WindowMinimized) {
const auto is = (state == Qt::WindowMaximized)
&& window()->testAttribute(Qt::WA_TranslucentBackground);
if (_isMaximizedAndTranslucent != is) {
_isMaximizedAndTranslucent = is;
updateCornersRounding();
}
}
};
Ui::Connect(
window()->windowHandle(),
&QWindow::windowStateChanged,
handleStateChanged);
ActivateDirectManipulation(window());
window()->shownValue() | rpl::filter([=](bool shown) {
return _handle && !shown;
}) | rpl::on_next([=] {
enableCloakingForHidden();
}, window()->lifetime());
}
bool WindowHelper::filterNativeEvent(
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result) {
Expects(_handle != nullptr);
if (handleSystemButtonEvent(msg, wParam, lParam, result)) {
return true;
}
switch (msg) {
case WM_ACTIVATE: {
if (LOWORD(wParam) == WA_CLICKACTIVE) {
Ui::MarkInactivePress(window(), true);
}
const auto active = (LOWORD(wParam) != WA_INACTIVE);
if (_shadow) {
if (active) {
_shadow->update(WindowShadow::Change::Activate);
} else {
_shadow->update(WindowShadow::Change::Deactivate);
}
}
updateWindowFrameColors(active);
window()->update();
_title->update();
} return false;
case WM_NCPAINT: {
if (::Platform::IsWindows8OrGreater() || _title->isHidden()) {
return false;
}
if (result) *result = 0;
} return true;
case WM_NCCALCSIZE: {
if (_title->isHidden() || window()->isFullScreen() || !wParam) {
return false;
}
const auto r = &((LPNCCALCSIZE_PARAMS)lParam)->rgrc[0];
const auto maximized = [&] {
WINDOWPLACEMENT wp;
wp.length = sizeof(WINDOWPLACEMENT);
return GetWindowPlacement(_handle, &wp)
&& (wp.showCmd == SW_SHOWMAXIMIZED);
}();
const auto addBorders = maximized || nativeResize();
if (addBorders) {
const auto dpi = _dpi.current();
const auto borderWidth = (GetSystemMetricsForDpiSupported() && dpi)
? GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi)
+ GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi)
: GetSystemMetrics(SM_CXSIZEFRAME)
+ GetSystemMetrics(SM_CXPADDEDBORDER);
const auto borderHeight = (GetSystemMetricsForDpiSupported() && dpi)
? GetSystemMetricsForDpi(SM_CYSIZEFRAME, dpi)
+ GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi)
: GetSystemMetrics(SM_CYSIZEFRAME)
+ GetSystemMetrics(SM_CXPADDEDBORDER);
r->left += borderWidth;
r->right -= borderWidth;
if (maximized) {
r->top += borderHeight;
}
r->bottom -= borderHeight;
}
if (maximized) {
const auto hMonitor = MonitorFromWindow(
_handle,
MONITOR_DEFAULTTONEAREST);
MONITORINFO mi;
mi.cbSize = sizeof(mi);
UINT uEdge = (UINT)-1;
if (GetMonitorInfo(hMonitor, &mi)
&& IsTaskbarAutoHidden(&mi.rcMonitor, &uEdge)) {
switch (uEdge) {
case ABE_LEFT: r->left += 1; break;
case ABE_RIGHT: r->right -= 1; break;
case ABE_TOP: r->top += 1; break;
case ABE_BOTTOM: r->bottom -= 1; break;
}
}
}
if (result) *result = addBorders ? 0 : WVR_REDRAW;
} return true;
case WM_NCRBUTTONUP: {
if (_title->isHidden()) {
return false;
}
POINT p{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
ScreenToClient(_handle, &p);
const auto mapped = QPoint(p.x, p.y)
/ window()->windowHandle()->devicePixelRatio();
ShowWindowMenu(window(), mapped);
if (result) *result = 0;
} return true;
case WM_NCACTIVATE: {
if (_title->isHidden()) {
return false;
}
if (IsCompositionEnabled()) {
const auto res = DefWindowProc(_handle, msg, wParam, -1);
if (result) *result = res;
} else {
// Thanks https://github.com/melak47/BorderlessWindow
if (result) *result = 1;
}
} return true;
case WM_WINDOWPOSCHANGING:
case WM_WINDOWPOSCHANGED: {
auto placement = WINDOWPLACEMENT{
.length = sizeof(WINDOWPLACEMENT),
};
if (!GetWindowPlacement(_handle, &placement)) {
LOG(("System Error: GetWindowPlacement failed."));
return false;
}
_title->refreshAdditionalPaddings(_handle, placement);
if (_shadow) {
if (placement.showCmd == SW_SHOWMAXIMIZED
|| placement.showCmd == SW_SHOWMINIMIZED) {
_shadow->update(WindowShadow::Change::Hidden);
} else {
_shadow->update(
WindowShadow::Change::Moved | WindowShadow::Change::Resized,
(WINDOWPOS*)lParam);
}
}
} return false;
case WM_SIZE: {
if (wParam == SIZE_MAXIMIZED
|| wParam == SIZE_RESTORED
|| wParam == SIZE_MINIMIZED) {
const auto now = window()->windowState();
if (wParam != SIZE_RESTORED
|| (now != Qt::WindowNoState
&& now != Qt::WindowFullScreen)) {
Qt::WindowState state = Qt::WindowNoState;
if (wParam == SIZE_MAXIMIZED) {
state = Qt::WindowMaximized;
} else if (wParam == SIZE_MINIMIZED) {
state = Qt::WindowMinimized;
}
window()->windowHandle()->windowStateChanged(state);
}
updateMargins();
_title->refreshAdditionalPaddings(_handle);
if (_shadow) {
const auto changes = (wParam == SIZE_MINIMIZED
|| wParam == SIZE_MAXIMIZED)
? WindowShadow::Change::Hidden
: (WindowShadow::Change::Resized
| WindowShadow::Change::Shown);
_shadow->update(changes);
}
}
} return false;
case WM_SHOWWINDOW: {
if (_shadow) {
const auto style = GetWindowLongPtr(_handle, GWL_STYLE);
const auto changes = WindowShadow::Change::Resized
| ((wParam && !(style & (WS_MAXIMIZE | WS_MINIMIZE)))
? WindowShadow::Change::Shown
: WindowShadow::Change::Hidden);
_shadow->update(changes);
}
} return false;
case WM_MOVE: {
_title->refreshAdditionalPaddings(_handle);
if (_shadow) {
_shadow->update(WindowShadow::Change::Moved);
}
} return false;
case WM_NCHITTEST: {
if (!result || _title->isHidden()) {
return false;
}
POINT p{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
ScreenToClient(_handle, &p);
const auto ratio = window()->windowHandle()->devicePixelRatio();
const auto mapped = QPoint(
int(std::floor(p.x / ratio)),
int(std::floor(p.y / ratio)));
*result = [&]() -> LRESULT {
if (!window()->rect().contains(mapped)) {
return DefWindowProc(_handle, msg, wParam, lParam);
}
const auto maximized = window()->isMaximized()
|| window()->isFullScreen();
const auto px = int(std::ceil(
st::windowTitleHeight
* window()->windowHandle()->devicePixelRatio()
/ 10.));
if (nativeResize() && !maximized && (mapped.y() < px)) {
return HTTOP;
}
auto request = HitTestRequest{
.point = mapped,
};
_hitTestRequests.fire(&request);
switch (const auto result = request.result) {
case HitTestResult::Client: return HTCLIENT;
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::Minimize:
case HitTestResult::MaximizeRestore:
case HitTestResult::Close: return systemButtonHitTest(result);
case HitTestResult::None:
default: return DefWindowProc(_handle, msg, wParam, lParam);
};
}();
_systemButtonOver.fire(systemButtonHitTest(*result));
} return true;
case WM_DPICHANGED: {
_dpi = LOWORD(wParam);
InvokeQueued(_title, [=] {
_title->refreshAdditionalPaddings(_handle);
});
} return false;
}
return false;
}
bool WindowHelper::fixedSize() const {
return window()->minimumSize() == window()->maximumSize();
}
bool WindowHelper::handleSystemButtonEvent(
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result) {
if (_title->isHidden()) {
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:
_systemButtonDown.fire((msg == WM_NCLBUTTONDOWN)
? systemButtonHitTest(testResult)
: HitTestResult::None);
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:
_systemButtonOver.fire(systemButtonHitTest(testResult));
if (overSysButton) {
if (result) *result = 0;
}
return overSysButton;
case WM_NCMOUSELEAVE:
_systemButtonOver.fire(HitTestResult::None);
return false;
}
return false;
}
int WindowHelper::systemButtonHitTest(HitTestResult result) const {
if (!SemiNativeSystemButtonProcessing()) {
return HTCLIENT;
}
switch (result) {
case HitTestResult::Minimize: return HTMINBUTTON;
case HitTestResult::MaximizeRestore: return HTMAXBUTTON;
case HitTestResult::Close: return HTCLOSE;
}
return HTTRANSPARENT;
}
HitTestResult WindowHelper::systemButtonHitTest(int result) const {
if (!SemiNativeSystemButtonProcessing()) {
return HitTestResult::None;
}
switch (result) {
case HTMINBUTTON: return HitTestResult::Minimize;
case HTMAXBUTTON: return HitTestResult::MaximizeRestore;
case HTCLOSE: return HitTestResult::Close;
}
return HitTestResult::None;
}
int WindowHelper::titleHeight() const {
return _title->isHidden() ? 0 : _title->height();
}
bool WindowHelper::nativeResize() const {
Expects(window()->windowHandle() != nullptr);
if (::Platform::IsWindows11OrGreater()) {
switch (window()->windowHandle()->surfaceType()) {
case QSurface::RasterSurface:
case QSurface::RasterGLSurface:
return window()->windowHandle()->format().alphaBufferSize() <= 0;
}
return true;
}
return false;
}
void WindowHelper::updateWindowFrameColors() {
updateWindowFrameColors(window()->isActiveWindow());
}
void WindowHelper::updateWindowFrameColors(bool active) {
if (!_handle || !::Platform::IsWindows11OrGreater()) {
return;
}
const auto bg = active
? _title->st()->bgActive->c
: _title->st()->bg->c;
COLORREF bgRef = RGB(bg.red(), bg.green(), bg.blue());
DwmSetWindowAttribute(
_handle,
kDWMWA_CAPTION_COLOR,
&bgRef,
sizeof(COLORREF));
const auto fg = active
? _title->st()->fgActive->c
: _title->st()->fg->c;
COLORREF fgRef = RGB(fg.red(), fg.green(), fg.blue());
DwmSetWindowAttribute(
_handle,
kDWMWA_TEXT_COLOR,
&fgRef,
sizeof(COLORREF));
}
void WindowHelper::updateCloaking() {
if (!_handle) {
return;
}
const auto enabled = window()->isHidden() && !_isFullScreen;
const auto flag = BOOL(enabled ? TRUE : FALSE);
DwmSetWindowAttribute(_handle, DWMWA_CLOAK, &flag, sizeof(flag));
}
void WindowHelper::enableCloakingForHidden() {
Expects(_handle != nullptr);
updateCloaking();
const auto qwindow = window()->windowHandle();
const auto firstExposeFilter = std::make_shared<QObject*>();
const auto filter = [=](not_null<QEvent*> e) {
if (e->type() == QEvent::Expose && qwindow->isExposed()) {
InvokeQueued(qwindow, [=] {
InvokeQueued(qwindow, [=] {
updateCloaking();
});
});
delete base::take(*firstExposeFilter);
}
return base::EventFilterResult::Continue;
};
*firstExposeFilter = base::install_event_filter(qwindow, filter);
}
void WindowHelper::updateMargins() {
if (!_handle || _updatingMargins) {
return;
}
_updatingMargins = true;
const auto guard = gsl::finally([&] { _updatingMargins = false; });
RECT r{};
const auto style = GetWindowLongPtr(_handle, GWL_STYLE);
const auto styleEx = GetWindowLongPtr(_handle, GWL_EXSTYLE);
const auto dpi = _dpi.current();
if (AdjustWindowRectExForDpiSupported() && dpi) {
AdjustWindowRectExForDpi(&r, style, false, styleEx, dpi);
} else {
AdjustWindowRectEx(&r, style, false, styleEx);
}
auto margins = nativeResize()
? QMargins(0, r.top, 0, 0)
: QMargins(r.left, r.top, -r.right, -r.bottom);
if (style & WS_MAXIMIZE) {
RECT w, m;
GetWindowRect(_handle , &w);
m = w;
HMONITOR hMonitor = MonitorFromRect(&w, MONITOR_DEFAULTTONEAREST);
if (hMonitor) {
MONITORINFO mi;
mi.cbSize = sizeof(mi);
GetMonitorInfo(hMonitor, &mi);
m = mi.rcWork;
}
_marginsDelta = QMargins(
w.left - m.left,
w.top - m.top,
m.right - w.right,
m.bottom - w.bottom);
margins.setLeft(margins.left() - _marginsDelta.left());
margins.setRight(margins.right() - _marginsDelta.right());
margins.setBottom(margins.bottom() - _marginsDelta.bottom());
margins.setTop(margins.top() - _marginsDelta.top());
} else if (!_marginsDelta.isNull()) {
RECT w;
GetWindowRect(_handle, &w);
SetWindowPos(
_handle,
0,
0,
0,
w.right - w.left - _marginsDelta.left() - _marginsDelta.right(),
w.bottom - w.top - _marginsDelta.top() - _marginsDelta.bottom(),
(SWP_NOMOVE
| SWP_NOSENDCHANGING
| SWP_NOZORDER
| SWP_NOACTIVATE
| SWP_NOREPOSITION));
_marginsDelta = QMargins();
}
if (_isFullScreen || _title->isHidden()) {
margins = QMargins();
if (_title->isHidden()) {
_marginsDelta = QMargins();
}
}
if (const auto native = QGuiApplication::platformNativeInterface()) {
native->setWindowProperty(
window()->windowHandle()->handle(),
"WindowsCustomMargins",
QVariant::fromValue<QMargins>(margins));
}
}
void WindowHelper::fixMaximizedWindow() {
if (!_handle) {
return;
}
const auto style = GetWindowLongPtr(_handle, GWL_STYLE);
if (style & WS_MAXIMIZE) {
auto w = RECT();
GetWindowRect(_handle, &w);
if (const auto hMonitor = MonitorFromRect(&w, MONITOR_DEFAULTTONEAREST)) {
MONITORINFO mi;
mi.cbSize = sizeof(mi);
GetMonitorInfo(hMonitor, &mi);
const auto m = mi.rcWork;
SetWindowPos(
_handle,
0,
0,
0,
m.right - m.left - _marginsDelta.left() - _marginsDelta.right(),
m.bottom - m.top - _marginsDelta.top() - _marginsDelta.bottom(),
SWP_NOMOVE | SWP_NOSENDCHANGING | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREPOSITION);
}
}
}
HWND GetCurrentHandle(not_null<QWidget*> widget) {
const auto toplevel = widget->window();
const auto window = toplevel->windowHandle();
return window ? GetCurrentHandle(window) : nullptr;
}
HWND GetCurrentHandle(not_null<QWindow*> window) {
return reinterpret_cast<HWND>(window->winId());
}
std::unique_ptr<BasicWindowHelper> CreateSpecialWindowHelper(
not_null<RpWidget*> window) {
return std::make_unique<WindowHelper>(window);
}
bool NativeWindowFrameSupported() {
return true;
}
rpl::producer<FullScreenEvent> FullScreenEvents(
not_null<RpWidget*> window) {
return rpl::never<FullScreenEvent>();
}
} // namespace Ui::Platform

View File

@@ -0,0 +1,94 @@
// 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
//
#pragma once
#include "ui/platform/ui_platform_window.h"
#include "ui/platform/win/ui_window_shadow_win.h"
#include "ui/platform/win/ui_windows_native_event_filter.h"
namespace Ui {
namespace Platform {
class TitleWidget;
struct HitTestRequest;
enum class HitTestResult;
class WindowHelper final : public BasicWindowHelper, public NativeEventFilter {
public:
explicit WindowHelper(not_null<RpWidget*> window);
~WindowHelper();
void initInWindow(not_null<RpWindow*> window) override;
not_null<RpWidget*> body() override;
QMargins frameMargins() override;
int additionalContentPadding() const override;
rpl::producer<int> additionalContentPaddingValue() const override;
void setTitle(const QString &title) override;
void setTitleStyle(const style::WindowTitle &st) override;
void setNativeFrame(bool enabled) override;
void setMinimumSize(QSize size) override;
void setFixedSize(QSize size) override;
void setGeometry(QRect rect) override;
void showFullScreen() override;
void showNormal() override;
[[nodiscard]] auto hitTestRequests() const
-> rpl::producer<not_null<HitTestRequest*>> override;
[[nodiscard]] auto systemButtonOver() const
-> rpl::producer<HitTestResult> override;
[[nodiscard]] auto systemButtonDown() const
-> rpl::producer<HitTestResult> override;
void overrideSystemButtonOver(HitTestResult button) override;
void overrideSystemButtonDown(HitTestResult button) override;
private:
void init();
void updateMargins();
void updateCloaking();
void enableCloakingForHidden();
void updateWindowFrameColors();
void updateWindowFrameColors(bool active);
void updateShadow();
void updateCornersRounding();
void fixMaximizedWindow();
[[nodiscard]] bool filterNativeEvent(
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result) override;
[[nodiscard]] bool handleSystemButtonEvent(
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result);
[[nodiscard]] bool fixedSize() const;
[[nodiscard]] int systemButtonHitTest(HitTestResult result) const;
[[nodiscard]] HitTestResult systemButtonHitTest(int result) const;
[[nodiscard]] int titleHeight() const;
[[nodiscard]] bool nativeResize() const;
const not_null<TitleWidget*> _title;
const not_null<RpWidget*> _body;
rpl::event_stream<not_null<HitTestRequest*>> _hitTestRequests;
rpl::event_stream<HitTestResult> _systemButtonOver;
rpl::event_stream<HitTestResult> _systemButtonDown;
std::optional<WindowShadow> _shadow;
rpl::variable<uint> _dpi;
QMargins _marginsDelta;
HWND _handle = nullptr;
bool _updatingMargins = false;
bool _isFullScreen = false;
bool _isMaximizedAndTranslucent = false;
};
[[nodiscard]] HWND GetCurrentHandle(not_null<QWidget*> widget);
[[nodiscard]] HWND GetCurrentHandle(not_null<QWindow*> window);
} // namespace Platform
} // namespace Ui

View File

@@ -0,0 +1,563 @@
// 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/win/ui_windows_direct_manipulation.h"
#include "base/integration.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/win/base_windows_safe_library.h"
#include "ui/platform/win/ui_window_win.h"
#include "ui/ui_utility.h" // kPixelToAngleDelta
#include "ui/rp_widget.h"
#include <qpa/qwindowsysteminterface.h>
#include <qpa/qwindowsysteminterface_p.h>
namespace Ui::Platform {
namespace {
[[nodiscard]] Qt::KeyboardModifiers LookupModifiers() {
const auto check = [](int key) {
return (GetKeyState(key) & 0x8000) != 0;
};
auto result = Qt::KeyboardModifiers();
if (check(VK_SHIFT)) {
result |= Qt::ShiftModifier;
}
// NB AltGr key (i.e., VK_RMENU on some keyboard layout) is not handled.
if (check(VK_RMENU) || check(VK_MENU)) {
result |= Qt::AltModifier;
}
if (check(VK_CONTROL)) {
result |= Qt::ControlModifier;
}
if (check(VK_LWIN) || check(VK_RWIN)) {
result |= Qt::MetaModifier;
}
return result;
}
UINT(__stdcall *GetDpiForWindow)(_In_ HWND hwnd);
BOOL(__stdcall *GetPointerType)(
UINT32 pointerId,
POINTER_INPUT_TYPE *pointerType);
[[nodiscard]] bool GetDpiForWindowSupported() {
static const auto Result = [&] {
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
const auto user32 = base::Platform::SafeLoadLibrary(L"User32.dll");
return LOAD_SYMBOL(user32, GetDpiForWindow);
#undef LOAD_SYMBOL
}();
return Result;
}
[[nodiscard]] bool GetPointerTypeSupported() {
static const auto Result = [&] {
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
const auto user32 = base::Platform::SafeLoadLibrary(L"User32.dll");
return LOAD_SYMBOL(user32, GetPointerType);
#undef LOAD_SYMBOL
}();
return Result;
}
} // namespace
class DirectManipulation::Handler
: public IDirectManipulationViewportEventHandler
, public IDirectManipulationInteractionEventHandler {
public:
Handler();
void setViewportSize(QSize size);
HRESULT STDMETHODCALLTYPE QueryInterface(
REFIID iid,
void **ppv) override;
ULONG STDMETHODCALLTYPE AddRef() override {
return ++_ref;
}
ULONG STDMETHODCALLTYPE Release() override {
if (--_ref == 0) {
delete this;
return 0;
}
return _ref;
}
[[nodiscard]] rpl::producer<bool> interacting() const {
return _interacting.value();
}
[[nodiscard]] rpl::producer<Event> events() const {
return _events.events();
}
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
private:
~Handler();
enum class State {
None,
Scroll,
Fling,
Pinch,
};
void transitionToState(State state);
HRESULT STDMETHODCALLTYPE OnViewportStatusChanged(
_In_ IDirectManipulationViewport* viewport,
_In_ DIRECTMANIPULATION_STATUS current,
_In_ DIRECTMANIPULATION_STATUS previous) override;
HRESULT STDMETHODCALLTYPE OnViewportUpdated(
_In_ IDirectManipulationViewport *viewport) override;
HRESULT STDMETHODCALLTYPE OnContentUpdated(
_In_ IDirectManipulationViewport *viewport,
_In_ IDirectManipulationContent *content) override;
HRESULT STDMETHODCALLTYPE OnInteraction(
_In_ IDirectManipulationViewport2 *viewport,
_In_ DIRECTMANIPULATION_INTERACTION_TYPE interaction) override;
State _state = State::None;
int _width = 0;
int _height = 0;
rpl::variable<bool> _interacting = false;
rpl::event_stream<Event> _events;
rpl::lifetime _lifetime;
float _scale = 1.0f;
float _xOffset = 0.f;
float _yOffset = 0.f;
bool _pendingScrollBegin = false;
std::atomic<ULONG> _ref = 1;
};
DirectManipulation::Handler::Handler() {
}
DirectManipulation::Handler::~Handler() {
}
void DirectManipulation::Handler::setViewportSize(QSize size) {
_width = size.width();
_height = size.height();
}
STDMETHODIMP DirectManipulation::Handler::QueryInterface(
REFIID iid,
void **ppv) {
if ((IID_IUnknown == iid) ||
(IID_IDirectManipulationViewportEventHandler == iid)) {
*ppv = static_cast<IDirectManipulationViewportEventHandler*>(this);
AddRef();
return S_OK;
}
if (IID_IDirectManipulationInteractionEventHandler == iid) {
*ppv = static_cast<IDirectManipulationInteractionEventHandler*>(
this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
void DirectManipulation::Handler::transitionToState(State state) {
if (_state == state) {
return;
}
const auto was = _state;
_state = state;
switch (was) {
case State::Scroll: {
if (state != State::Fling) {
_events.fire({ .type = Event::Type::ScrollStop });
}
} break;
case State::Fling: {
_events.fire({ .type = Event::Type::FlingStop });
} break;
case State::Pinch: {
// _events.fire({ .type = Event::Type::PinchStop });
} break;
}
switch (state) {
case State::Scroll: {
_pendingScrollBegin = true;
} break;
case State::Fling: {
Assert(was == State::Scroll);
_events.fire({ .type = Event::Type::FlingStart });
} break;
case State::Pinch: {
//_events.fire({ .type = Event::Type::PinchStart });
} break;
}
}
HRESULT DirectManipulation::Handler::OnViewportStatusChanged(
IDirectManipulationViewport *viewport,
DIRECTMANIPULATION_STATUS current,
DIRECTMANIPULATION_STATUS previous) {
Expects(viewport != nullptr);
if (current == previous) {
return S_OK;
} else if (current == DIRECTMANIPULATION_INERTIA) {
if (previous != DIRECTMANIPULATION_RUNNING
|| _state != State::Scroll) {
return S_OK;
}
transitionToState(State::Fling);
}
if (current == DIRECTMANIPULATION_RUNNING) {
if (previous == DIRECTMANIPULATION_INERTIA) {
transitionToState(State::None);
}
}
if (current != DIRECTMANIPULATION_READY) {
return S_OK;
}
if (_scale != 1.0f || _xOffset != 0. || _yOffset != 0.) {
const auto hr = viewport->ZoomToRect(0, 0, _width, _height, FALSE);
if (!SUCCEEDED(hr)) {
return hr;
}
}
_scale = 1.0f;
_xOffset = 0.0f;
_yOffset = 0.0f;
transitionToState(State::None);
return S_OK;
}
HRESULT DirectManipulation::Handler::OnViewportUpdated(
IDirectManipulationViewport *viewport) {
return S_OK;
}
HRESULT DirectManipulation::Handler::OnContentUpdated(
IDirectManipulationViewport *viewport,
IDirectManipulationContent *content) {
Expects(viewport != nullptr);
Expects(content != nullptr);
float xform[6];
const auto hr = content->GetContentTransform(xform, ARRAYSIZE(xform));
if (!SUCCEEDED(hr)) {
return hr;
}
float scale = xform[0];
float xOffset = xform[4];
float yOffset = xform[5];
if (scale == 0.0f) {
return hr;
} else if (qFuzzyCompare(scale, _scale)
&& xOffset == _xOffset
&& yOffset == _yOffset) {
return hr;
}
if (qFuzzyCompare(scale, 1.0f)) {
if (_state == State::None) {
transitionToState(State::Scroll);
}
} else {
transitionToState(State::Pinch);
}
auto getIntDeltaPart = [](float &was, float now) {
if (was < now) {
const auto result = std::floor(now - was);
was += result;
return int(result);
} else {
const auto result = std::floor(was - now);
was -= result;
return -int(result);
}
};
const auto d = QPoint(
getIntDeltaPart(_xOffset, xOffset),
getIntDeltaPart(_yOffset, yOffset));
if ((_state == State::Scroll || _state == State::Fling) && d.isNull()) {
return S_OK;
}
if (_state == State::Scroll) {
if (_pendingScrollBegin) {
_events.fire({ .type = Event::Type::ScrollStart, .delta = d });
_pendingScrollBegin = false;
} else {
_events.fire({ .type = Event::Type::Scroll, .delta = d });
}
} else if (_state == State::Fling) {
_events.fire({ .type = Event::Type::Fling, .delta = d });
} else {
//_events.fire({ .type = Event::Type::Pinch, .delta = ... });
}
_scale = scale;
return hr;
}
HRESULT DirectManipulation::Handler::OnInteraction(
IDirectManipulationViewport2 *viewport,
DIRECTMANIPULATION_INTERACTION_TYPE interaction) {
if (interaction == DIRECTMANIPULATION_INTERACTION_BEGIN) {
_interacting = true;
} else if (interaction == DIRECTMANIPULATION_INTERACTION_END) {
_interacting = false;
}
return S_OK;
}
DirectManipulation::DirectManipulation(not_null<RpWidget*> widget)
: NativeEventFilter(widget)
, _interacting([=] { _updateManager->Update(nullptr); }) {
widget->sizeValue() | rpl::on_next([=](QSize size) {
sizeUpdated(size * widget->devicePixelRatio());
}, _lifetime);
widget->winIdValue() | rpl::on_next([=](WId winId) {
destroy();
if (const auto hwnd = reinterpret_cast<HWND>(winId)) {
if (init(hwnd)) {
sizeUpdated(widget->size() * widget->devicePixelRatio());
} else {
destroy();
}
}
}, _lifetime);
}
DirectManipulation::~DirectManipulation() {
destroy();
}
void DirectManipulation::sizeUpdated(QSize nativeSize) {
if (const auto handler = _handler.get()) {
const auto r = QRect(QPoint(), nativeSize);
handler->setViewportSize(r.size());
if (const auto viewport = _viewport.get()) {
const auto rect = RECT{ r.x(), r.y(), r.right(), r.bottom() };
viewport->SetViewportRect(&rect);
}
}
}
bool DirectManipulation::init(HWND hwnd) {
if (!hwnd || !::Platform::IsWindows10OrGreater()) {
return false;
}
_manager = base::WinRT::TryCreateInstance<IDirectManipulationManager>(
CLSID_DirectManipulationManager);
if (!_manager) {
return false;
}
auto hr = S_OK;
hr = _manager->GetUpdateManager(IID_PPV_ARGS(_updateManager.put()));
if (!SUCCEEDED(hr) || !_updateManager) {
return false;
}
hr = _manager->CreateViewport(
nullptr,
hwnd,
IID_PPV_ARGS(_viewport.put()));
if (!SUCCEEDED(hr) || !_viewport) {
return false;
}
const auto configuration = DIRECTMANIPULATION_CONFIGURATION_INTERACTION
| DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X
| DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y
| DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA
| DIRECTMANIPULATION_CONFIGURATION_RAILS_X
| DIRECTMANIPULATION_CONFIGURATION_RAILS_Y;
hr = _viewport->ActivateConfiguration(configuration);
if (!SUCCEEDED(hr)) {
return false;
}
hr = _viewport->SetViewportOptions(
DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE);
if (!SUCCEEDED(hr)) {
return false;
}
_handler.attach(new Handler());
_handler->interacting(
) | rpl::on_next([=](bool interacting) {
base::Integration::Instance().enterFromEventLoop([&] {
if (interacting) {
_interacting.start();
} else {
_interacting.stop();
}
});
}, _handler->lifetime());
_handler->events() | rpl::on_next([=](Event &&event) {
base::Integration::Instance().enterFromEventLoop([&] {
_events.fire(std::move(event));
});
}, _handler->lifetime());
hr = _viewport->AddEventHandler(hwnd, _handler.get(), &_cookie);
if (!SUCCEEDED(hr)) {
return false;
}
RECT rect = { 0, 0, 1024, 1024 };
hr = _viewport->SetViewportRect(&rect);
if (!SUCCEEDED(hr)) {
return false;
}
hr = _manager->Activate(hwnd);
if (!SUCCEEDED(hr)) {
return false;
}
_managerHandle = hwnd;
hr = _viewport->Enable();
if (!SUCCEEDED(hr)) {
return false;
}
hr = _updateManager->Update(nullptr);
if (!SUCCEEDED(hr)) {
return false;
}
return true;
}
bool DirectManipulation::filterNativeEvent(
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result) {
switch (msg) {
case DM_POINTERHITTEST:
if (_viewport && GetPointerTypeSupported()) {
const auto id = UINT32(GET_POINTERID_WPARAM(wParam));
auto type = POINTER_INPUT_TYPE();
if (GetPointerType(id, &type) && type == PT_TOUCHPAD) {
_viewport->SetContact(id);
}
return true;
}
break;
}
return false;
}
auto DirectManipulation::events() const -> rpl::producer<Event> {
return _events.events();
}
void DirectManipulation::destroy() {
_interacting.stop();
if (_handler) {
_handler = nullptr;
}
if (_viewport) {
_viewport->Stop();
if (_cookie) {
_viewport->RemoveEventHandler(_cookie);
_cookie = 0;
}
_viewport->Abandon();
_viewport = nullptr;
}
if (_updateManager) {
_updateManager = nullptr;
}
if (_manager) {
if (_managerHandle) {
_manager->Deactivate(_managerHandle);
}
_manager = nullptr;
}
}
void ActivateDirectManipulation(not_null<RpWidget*> window) {
auto dm = std::make_unique<DirectManipulation>(window);
dm->events(
) | rpl::on_next([=](const DirectManipulationEvent &event) {
using Type = DirectManipulationEventType;
const auto send = [&](Qt::ScrollPhase phase) {
const auto windowHandle = window->windowHandle();
const auto hwnd = reinterpret_cast<HWND>(window->winId());
if (!windowHandle || !hwnd) {
return;
}
auto global = POINT();
::GetCursorPos(&global);
auto local = global;
::ScreenToClient(hwnd, &local);
const auto dpi = GetDpiForWindowSupported()
? GetDpiForWindow(hwnd)
: 0;
const auto scale = dpi ? (96. / dpi) : 1.;
const auto delta = QPointF(event.delta) * scale;
const auto inverted = true;
QWindowSystemInterface::handleWheelEvent(
windowHandle,
QWindowSystemInterfacePrivate::eventTime.elapsed(),
QPointF(local.x, local.y),
QPointF(global.x, global.y),
delta.toPoint(),
(delta * kPixelToAngleDelta).toPoint(),
LookupModifiers(),
phase,
Qt::MouseEventSynthesizedBySystem,
inverted);
};
switch (event.type) {
case Type::ScrollStart: send(Qt::ScrollBegin); break;
case Type::Scroll: send(Qt::ScrollUpdate); break;
case Type::FlingStart:
case Type::Fling: send(Qt::ScrollMomentum); break;
case Type::ScrollStop: send(Qt::ScrollEnd); break;
case Type::FlingStop: send(Qt::ScrollEnd); break;
}
}, window->lifetime());
window->lifetime().add([owned = std::move(dm)] {});
}
} // namespace Ui::Platform

View File

@@ -0,0 +1,76 @@
// 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
//
#pragma once
#include "base/platform/win/base_windows_winrt.h"
#include "ui/platform/win/ui_windows_native_event_filter.h"
#include "ui/effects/animations.h"
#include <QtCore/QPoint>
#include <windows.h>
#include <directmanipulation.h>
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Ui::Platform {
enum class DirectManipulationEventType {
ScrollStart,
Scroll,
ScrollStop,
FlingStart,
Fling,
FlingStop,
};
struct DirectManipulationEvent {
using Type = DirectManipulationEventType;
Type type = Type ();
QPoint delta;
};
class DirectManipulation final : public NativeEventFilter {
public:
explicit DirectManipulation(not_null<RpWidget*> widget);
~DirectManipulation();
using Event = DirectManipulationEvent;
[[nodiscard]] rpl::producer<Event> events() const;
private:
class Handler;
bool init(HWND hwnd);
void sizeUpdated(QSize nativeSize);
void destroy();
bool filterNativeEvent(
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result) override;
winrt::com_ptr<IDirectManipulationManager> _manager;
winrt::com_ptr<IDirectManipulationUpdateManager> _updateManager;
winrt::com_ptr<IDirectManipulationViewport> _viewport;
winrt::com_ptr<Handler> _handler;
HWND _managerHandle = nullptr;
DWORD _cookie = 0;
//bool has_animation_observer_ = false;
Ui::Animations::Basic _interacting;
rpl::event_stream<Event> _events;
rpl::lifetime _lifetime;
};
void ActivateDirectManipulation(not_null<RpWidget*> window);
} // namespace Ui::Platform

View File

@@ -0,0 +1,172 @@
// 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/win/ui_windows_native_event_filter.h"
#include "base/integration.h"
#include "ui/qt_object_factory.h"
#include "ui/rp_widget.h"
#include <QtCore/QAbstractNativeEventFilter>
#include <QtCore/QCoreApplication>
namespace Ui::Platform {
class NativeEventFilter::FilterSingleton final
: public QAbstractNativeEventFilter {
public:
void registerFilter(HWND handle, not_null<NativeEventFilter*> filter);
void unregisterFilter(HWND handle, not_null<NativeEventFilter*> filter);
bool nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) override;
private:
using Change = std::pair<HWND, not_null<NativeEventFilter*>>;
struct Filters {
not_null<NativeEventFilter*> first;
std::vector<not_null<NativeEventFilter*>> other;
};
base::flat_map<HWND, Filters> _filtersByHandle;
base::flat_set<Change> _adding;
base::flat_set<Change> _removing;
bool _processing = false;
};
void NativeEventFilter::FilterSingleton::registerFilter(
HWND handle,
not_null<NativeEventFilter*> filter) {
if (_processing) {
const auto change = std::make_pair(handle, filter);
_removing.remove(change);
_adding.emplace(change);
return;
}
const auto i = _filtersByHandle.find(handle);
if (i == end(_filtersByHandle)) {
_filtersByHandle.emplace(handle, Filters{ .first = filter });
} else {
i->second.other.push_back(filter);
}
}
void NativeEventFilter::FilterSingleton::unregisterFilter(
HWND handle,
not_null<NativeEventFilter*> filter) {
if (_processing) {
const auto change = std::make_pair(handle, filter);
_adding.remove(change);
_removing.emplace(change);
return;
}
const auto i = _filtersByHandle.find(handle);
if (i != end(_filtersByHandle)) {
if (i->second.first == filter) {
if (i->second.other.empty()) {
_filtersByHandle.erase(i);
} else {
i->second.first = i->second.other.back();
i->second.other.pop_back();
}
} else {
const auto j = ranges::find(i->second.other, filter);
if (j != end(i->second.other)) {
i->second.other.erase(j);
}
}
}
}
bool NativeEventFilter::FilterSingleton::nativeEventFilter(
const QByteArray &eventType,
void *message,
native_event_filter_result *result) {
const auto msg = static_cast<MSG*>(message);
const auto i = _filtersByHandle.find(msg->hwnd);
if (i == end(_filtersByHandle)) {
return false;
}
auto filtered = false;
_processing = true;
base::Integration::Instance().enterFromEventLoop([&] {
const auto first = i->second.first;
if (!_removing.contains(std::make_pair(msg->hwnd, first))) {
filtered = first->filterNativeEvent(
msg->message,
msg->wParam,
msg->lParam,
reinterpret_cast<LRESULT*>(result));
if (filtered) {
return;
}
}
for (const auto other : i->second.other) {
if (!_removing.contains(std::make_pair(msg->hwnd, other))) {
filtered = other->filterNativeEvent(
msg->message,
msg->wParam,
msg->lParam,
reinterpret_cast<LRESULT*>(result));
if (filtered) {
return;
}
}
}
});
_processing = false;
const auto destroyed = (msg->message == WM_DESTROY);
if (destroyed) {
_filtersByHandle.erase(i);
filtered = false;
}
for (const auto &change : _adding) {
if (!destroyed || change.first != msg->hwnd) {
registerFilter(change.first, change.second);
}
}
for (const auto &change : _removing) {
if (!destroyed || change.first != msg->hwnd) {
unregisterFilter(change.first, change.second);
}
}
return filtered;
}
NativeEventFilter::NativeEventFilter(not_null<RpWidget*> that) {
that->winIdValue() | rpl::on_next([=](WId winId) {
if (_hwnd) {
Singleton()->unregisterFilter(_hwnd, this);
}
_hwnd = reinterpret_cast<HWND>(winId);
if (_hwnd) {
Singleton()->registerFilter(_hwnd, this);
}
}, that->lifetime());
}
NativeEventFilter::~NativeEventFilter() {
if (_hwnd) {
Singleton()->unregisterFilter(_hwnd, this);
}
}
auto NativeEventFilter::Singleton() -> not_null<FilterSingleton*> {
static const auto Instance = [&] {
const auto application = QCoreApplication::instance();
const auto filter = Ui::CreateChild<FilterSingleton>(application);
application->installNativeEventFilter(filter);
return filter;
}();
return Instance;
}
} // namespace Ui::Platform

View File

@@ -0,0 +1,37 @@
// 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
//
#pragma once
#include <Windows.h>
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Ui::Platform {
class NativeEventFilter {
public:
explicit NativeEventFilter(not_null<RpWidget*> that);
virtual ~NativeEventFilter();
virtual bool filterNativeEvent(
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result) = 0;
private:
class FilterSingleton;
friend class FilterSingleton;
[[nodiscard]] static not_null<FilterSingleton*> Singleton();
HWND _hwnd = nullptr;
};
} // namespace Ui::Platform