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
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:
616
Telegram/lib_ui/ui/platform/linux/ui_utility_linux.cpp
Normal file
616
Telegram/lib_ui/ui/platform/linux/ui_utility_linux.cpp
Normal 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
|
||||
43
Telegram/lib_ui/ui/platform/linux/ui_utility_linux.h
Normal file
43
Telegram/lib_ui/ui/platform/linux/ui_utility_linux.h
Normal 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
|
||||
25
Telegram/lib_ui/ui/platform/linux/ui_window_linux.cpp
Normal file
25
Telegram/lib_ui/ui/platform/linux/ui_window_linux.cpp
Normal 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
|
||||
9
Telegram/lib_ui/ui/platform/linux/ui_window_linux.h
Normal file
9
Telegram/lib_ui/ui/platform/linux/ui_window_linux.h
Normal 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"
|
||||
143
Telegram/lib_ui/ui/platform/linux/ui_window_title_linux.cpp
Normal file
143
Telegram/lib_ui/ui/platform/linux/ui_window_title_linux.cpp
Normal 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
|
||||
@@ -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"
|
||||
41
Telegram/lib_ui/ui/platform/mac/ui_utility_mac.h
Normal file
41
Telegram/lib_ui/ui/platform/mac/ui_utility_mac.h
Normal 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
|
||||
168
Telegram/lib_ui/ui/platform/mac/ui_utility_mac.mm
Normal file
168
Telegram/lib_ui/ui/platform/mac/ui_utility_mac.mm
Normal 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
|
||||
48
Telegram/lib_ui/ui/platform/mac/ui_window_mac.h
Normal file
48
Telegram/lib_ui/ui/platform/mac/ui_window_mac.h
Normal 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
|
||||
535
Telegram/lib_ui/ui/platform/mac/ui_window_mac.mm
Normal file
535
Telegram/lib_ui/ui/platform/mac/ui_window_mac.mm
Normal 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
|
||||
59
Telegram/lib_ui/ui/platform/mac/ui_window_title_mac.h
Normal file
59
Telegram/lib_ui/ui/platform/mac/ui_window_title_mac.h
Normal 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
|
||||
152
Telegram/lib_ui/ui/platform/mac/ui_window_title_mac.mm
Normal file
152
Telegram/lib_ui/ui/platform/mac/ui_window_title_mac.mm
Normal 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
|
||||
57
Telegram/lib_ui/ui/platform/ui_platform_utility.h
Normal file
57
Telegram/lib_ui/ui/platform/ui_platform_utility.h
Normal 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
|
||||
690
Telegram/lib_ui/ui/platform/ui_platform_window.cpp
Normal file
690
Telegram/lib_ui/ui/platform/ui_platform_window.cpp
Normal 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
|
||||
158
Telegram/lib_ui/ui/platform/ui_platform_window.h
Normal file
158
Telegram/lib_ui/ui/platform/ui_platform_window.h
Normal 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
|
||||
569
Telegram/lib_ui/ui/platform/ui_platform_window_title.cpp
Normal file
569
Telegram/lib_ui/ui/platform/ui_platform_window_title.cpp
Normal 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
|
||||
252
Telegram/lib_ui/ui/platform/ui_platform_window_title.h
Normal file
252
Telegram/lib_ui/ui/platform/ui_platform_window_title.h
Normal 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
|
||||
184
Telegram/lib_ui/ui/platform/win/ui_utility_win.cpp
Normal file
184
Telegram/lib_ui/ui/platform/win/ui_utility_win.cpp
Normal 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
|
||||
56
Telegram/lib_ui/ui/platform/win/ui_utility_win.h
Normal file
56
Telegram/lib_ui/ui/platform/win/ui_utility_win.h
Normal 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
|
||||
643
Telegram/lib_ui/ui/platform/win/ui_window_shadow_win.cpp
Normal file
643
Telegram/lib_ui/ui/platform/win/ui_window_shadow_win.cpp
Normal 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
|
||||
107
Telegram/lib_ui/ui/platform/win/ui_window_shadow_win.h
Normal file
107
Telegram/lib_ui/ui/platform/win/ui_window_shadow_win.h
Normal 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
|
||||
262
Telegram/lib_ui/ui/platform/win/ui_window_title_win.cpp
Normal file
262
Telegram/lib_ui/ui/platform/win/ui_window_title_win.cpp
Normal 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
|
||||
73
Telegram/lib_ui/ui/platform/win/ui_window_title_win.h
Normal file
73
Telegram/lib_ui/ui/platform/win/ui_window_title_win.h
Normal 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
|
||||
973
Telegram/lib_ui/ui/platform/win/ui_window_win.cpp
Normal file
973
Telegram/lib_ui/ui/platform/win/ui_window_win.cpp
Normal 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
|
||||
94
Telegram/lib_ui/ui/platform/win/ui_window_win.h
Normal file
94
Telegram/lib_ui/ui/platform/win/ui_window_win.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user