lib_ui/ui/platform/win/ui_utility_win.cpp
John Preston 8db6dcf125 Workaround Wayland popup menu bug.
When hiding a child popup first the app receives ApplicationDeactivate
event and in a short time (a couple of ms) ApplicationActivate.

But the first event hides all popups, so the parent popup gets closed too.

Delay handling of ApplicationDeactivate event in this specific case.
2023-07-12 22:05:12 +04:00

216 lines
5.6 KiB
C++

// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/platform/win/ui_utility_win.h"
#include "base/platform/win/base_windows_h.h"
#include "ui/widgets/popup_menu.h"
#include <QtWidgets/QApplication>
#include <QtGui/QWindow>
#include <QtCore/QAbstractNativeEventFilter>
#include <wrl/client.h>
#include <Shobjidl.h>
using namespace Microsoft::WRL;
namespace Ui::Platform {
bool IsApplicationActive() {
return QApplication::activeWindow() != nullptr;
}
void UpdateOverlayed(not_null<QWidget*> widget) {
const auto wm = widget->testAttribute(Qt::WA_Mapped);
const auto wv = widget->testAttribute(Qt::WA_WState_Visible);
if (!wm) widget->setAttribute(Qt::WA_Mapped, true);
if (!wv) widget->setAttribute(Qt::WA_WState_Visible, true);
widget->update();
QEvent e(QEvent::UpdateRequest);
QGuiApplication::sendEvent(widget, &e);
if (!wm) widget->setAttribute(Qt::WA_Mapped, false);
if (!wv) widget->setAttribute(Qt::WA_WState_Visible, false);
}
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));
}
namespace internal {
TitleControls::Layout TitleControlsLayout() {
return TitleControls::Layout{
.right = {
TitleControls::Control::Minimize,
TitleControls::Control::Maximize,
TitleControls::Control::Close,
}
};
}
} // namespace internal
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,
long *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));
}
void RegisterChildPopupHiding() {
}
bool SkipApplicationDeactivateEvent() {
return false;
}
void GotApplicationActivateEvent() {
}
} // namespace Ui::Platform