753 lines
26 KiB
C++
753 lines
26 KiB
C++
/*
|
|
This file is part of KDDockWidgets.
|
|
|
|
SPDX-FileCopyrightText: 2019 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
|
Author: Sérgio Martins <sergio.martins@kdab.com>
|
|
|
|
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
|
|
|
|
Contact KDAB at <info@kdab.com> for commercial licensing options.
|
|
*/
|
|
|
|
#include "WidgetResizeHandler_p.h"
|
|
#include "DragController_p.h"
|
|
#include "Config.h"
|
|
#include "Utils_p.h"
|
|
#include "View_p.h"
|
|
#include "Logging_p.h"
|
|
#include "DockRegistry_p.h"
|
|
#include "QtCompat_p.h"
|
|
|
|
#include "kddockwidgets/core/DockRegistry.h"
|
|
#include "kddockwidgets/core/MDILayout.h"
|
|
#include "kddockwidgets/core/TitleBar.h"
|
|
#include "kddockwidgets/core/FloatingWindow.h"
|
|
#include "kddockwidgets/core/Platform.h"
|
|
#include "core/ScopedValueRollback_p.h"
|
|
|
|
#include <cstdlib>
|
|
|
|
#if defined(Q_OS_WIN)
|
|
|
|
#if defined(KDDW_FRONTEND_QTWIDGETS)
|
|
#include "../qtcommon/Platform.h"
|
|
#include <QWidget>
|
|
#endif
|
|
|
|
#if defined(KDDW_QTGUI_TYPES)
|
|
#include "../qtcommon/Window_p.h"
|
|
#include <QGuiApplication>
|
|
#include <QtGui/private/qhighdpiscaling_p.h>
|
|
#endif
|
|
|
|
#endif
|
|
|
|
#if defined(Q_OS_WIN)
|
|
|
|
#include <windowsx.h>
|
|
#include <dwmapi.h>
|
|
#if defined(Q_CC_MSVC)
|
|
#pragma comment(lib, "Dwmapi.lib")
|
|
#pragma comment(lib, "User32.lib")
|
|
#endif
|
|
#endif
|
|
|
|
using namespace KDDockWidgets;
|
|
using namespace KDDockWidgets::Core;
|
|
|
|
bool WidgetResizeHandler::s_disableAllHandlers = false;
|
|
WidgetResizeHandler::WidgetResizeHandler(EventFilterMode filterMode, WindowMode windowMode,
|
|
View *target)
|
|
: m_usesGlobalEventFilter(filterMode == EventFilterMode::Global)
|
|
, m_isTopLevelWindowResizer(windowMode == WindowMode::TopLevel)
|
|
{
|
|
setTarget(target);
|
|
}
|
|
|
|
WidgetResizeHandler::~WidgetResizeHandler()
|
|
{
|
|
if (m_usesGlobalEventFilter) {
|
|
Platform::instance()->removeGlobalEventFilter(this);
|
|
} else if (mTargetGuard) {
|
|
mTarget->removeViewEventFilter(this);
|
|
}
|
|
|
|
restoreMouseCursor();
|
|
}
|
|
|
|
void WidgetResizeHandler::setAllowedResizeSides(CursorPositions sides)
|
|
{
|
|
mAllowedResizeSides = sides;
|
|
}
|
|
|
|
void WidgetResizeHandler::setResizeGap(int gap)
|
|
{
|
|
m_resizeGap = gap;
|
|
}
|
|
|
|
bool WidgetResizeHandler::isMDI() const
|
|
{
|
|
Core::Group *group = mTarget->asGroupController();
|
|
return group && group->isMDI();
|
|
}
|
|
|
|
bool WidgetResizeHandler::isResizing() const
|
|
{
|
|
return m_resizingInProgress;
|
|
}
|
|
|
|
int WidgetResizeHandler::widgetResizeHandlerMargin()
|
|
{
|
|
return 4; // pixels
|
|
}
|
|
|
|
bool WidgetResizeHandler::onMouseEvent(View *widget, MouseEvent *e)
|
|
{
|
|
if (s_disableAllHandlers || !widget || !mTargetGuard)
|
|
return false;
|
|
|
|
if (e->type() != Event::MouseButtonPress && e->type() != Event::MouseButtonRelease
|
|
&& e->type() != Event::MouseMove)
|
|
return false;
|
|
|
|
auto me = mouseEvent(e);
|
|
if (!me)
|
|
return false;
|
|
|
|
if (m_isTopLevelWindowResizer) {
|
|
// Case #1.0: Resizing FloatingWindow
|
|
|
|
if (!widget->isRootView() || !widget->equals(mTarget)) {
|
|
if (m_usesGlobalEventFilter) {
|
|
// Case #1.1: FloatingWindows on EGLFS
|
|
// EGLFS doesn't support storing mouse cursor shape per window, so we need to use
|
|
// global filter do detect mouse leaving the window
|
|
if (!m_resizingInProgress) {
|
|
const Point globalPos = Qt5Qt6Compat::eventGlobalPos(me);
|
|
updateCursor(cursorPosition(globalPos));
|
|
}
|
|
}
|
|
|
|
// Case #1.2: FloatingWindows on all other platforms
|
|
// Not needed to mess with the cursor, it gets set when moving over another window.
|
|
return false;
|
|
}
|
|
} else if (isMDI()) {
|
|
// Case #2: Resizing an embedded MDI "Window"
|
|
|
|
// Each Group has a WidgetResizeHandler instance.
|
|
// mTarget is the Group we want to resize.
|
|
// but 'o' might not be mTarget, because we're using a global event filter.
|
|
// The global event filter is required because we allow the cursor to be outside the group,
|
|
// a few pixels so we have a nice resize margin. Here we deal with the case where our
|
|
// mTarget, let's say "Group 1" is on top of "Group 2" but cursor is near "Group 2"'s
|
|
// margins, and would show resize cursor. We only want to continue if the cursor is near the
|
|
// margins of our own group (mTarget)
|
|
|
|
auto f = widget->d->firstParentOfType(ViewType::Group);
|
|
auto group = f ? f->view()->asGroupController() : nullptr;
|
|
if (group && group->isMDIWrapper()) {
|
|
// We don't care about the inner Option_MDINestable helper group
|
|
group = group->mdiFrame();
|
|
}
|
|
|
|
if (group && !group->view()->equals(mTarget)) {
|
|
auto groupParent =
|
|
group->view()->d->aboutToBeDestroyed() ? nullptr : group->view()->parentView();
|
|
auto targetParent = mTarget->d->aboutToBeDestroyed() ? nullptr : mTarget->parentView();
|
|
const bool areSiblings = groupParent && groupParent->equals(targetParent);
|
|
if (areSiblings) {
|
|
if (cursorPosition(Qt5Qt6Compat::eventGlobalPos(e)) == CursorPosition_Undefined)
|
|
restoreMouseCursor();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (e->type()) {
|
|
case Event::MouseButtonPress: {
|
|
if (mTarget->isMaximized())
|
|
break;
|
|
|
|
CursorPosition cursorPos = cursorPosition(Qt5Qt6Compat::eventGlobalPos(e));
|
|
updateCursor(cursorPos);
|
|
if (cursorPos == CursorPosition_Undefined)
|
|
return false;
|
|
|
|
const int m = widgetResizeHandlerMargin();
|
|
const Rect widgetRect = mTarget->rect().marginsAdded(Margins(m, m, m, m));
|
|
const Point cursorPoint = mTarget->mapFromGlobal(Qt5Qt6Compat::eventGlobalPos(e));
|
|
if (!widgetRect.contains(cursorPoint) || e->button() != Qt::LeftButton)
|
|
return false;
|
|
|
|
m_resizingInProgress = true;
|
|
if (isMDI())
|
|
DockRegistry::self()->dptr()->groupInMDIResizeChanged.emit();
|
|
mNewPosition = Qt5Qt6Compat::eventGlobalPos(e);
|
|
mCursorPos = cursorPos;
|
|
|
|
return true;
|
|
}
|
|
case Event::MouseButtonRelease: {
|
|
m_resizingInProgress = false;
|
|
if (isMDI()) {
|
|
DockRegistry::self()->dptr()->groupInMDIResizeChanged.emit();
|
|
// Usually in KDDW all geometry changes are done in the layout items, which propagate to
|
|
// the widgets When resizing a MDI however, we're resizing the widget directly. So
|
|
// update the corresponding layout item when we're finished.
|
|
auto group = mTarget->asGroupController();
|
|
group->mdiLayout()->setDockWidgetGeometry(group, group->geometry());
|
|
}
|
|
updateCursor(CursorPosition_Undefined);
|
|
auto cursorPos = cursorPosition(Qt5Qt6Compat::eventGlobalPos(e));
|
|
updateCursor(cursorPos);
|
|
if (mTarget->isMaximized() || !m_resizingInProgress || e->button() != Qt::LeftButton)
|
|
break;
|
|
|
|
mTarget->releaseMouse();
|
|
mTarget->releaseKeyboard();
|
|
if (m_eventFilteringStartsManually)
|
|
EventFilterInterface::setEnabled(false);
|
|
|
|
return true;
|
|
|
|
break;
|
|
}
|
|
case Event::MouseMove: {
|
|
if (mTarget->isMaximized())
|
|
break;
|
|
|
|
if (isMDI()) {
|
|
const Core::Group *groupBeingResized = DockRegistry::self()->groupInMDIResize();
|
|
const bool otherGroupBeingResized =
|
|
groupBeingResized && groupBeingResized->view() != mTarget;
|
|
if (otherGroupBeingResized) {
|
|
// only one at a time!
|
|
return false;
|
|
}
|
|
}
|
|
|
|
m_resizingInProgress = m_resizingInProgress && (e->buttons() & Qt::LeftButton);
|
|
const bool consumed = mouseMoveEvent(e);
|
|
return consumed;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool WidgetResizeHandler::mouseMoveEvent(MouseEvent *e)
|
|
{
|
|
const Point globalPos = Qt5Qt6Compat::eventGlobalPos(e);
|
|
if (!m_resizingInProgress) {
|
|
const CursorPosition pos = cursorPosition(globalPos);
|
|
updateCursor(pos);
|
|
return pos != CursorPosition_Undefined;
|
|
}
|
|
|
|
const Rect oldGeometry = mTarget->d->globalGeometry();
|
|
Rect newGeometry = oldGeometry;
|
|
|
|
Rect parentGeometry;
|
|
if (!mTarget->isRootView()) {
|
|
auto parent = mTarget->parentView();
|
|
parentGeometry = parent->d->globalGeometry();
|
|
}
|
|
|
|
{
|
|
int deltaWidth = 0;
|
|
int newWidth = 0;
|
|
const int maxWidth = mTarget->maxSizeHint().width();
|
|
const int minWidth = mTarget->minSize().width();
|
|
|
|
switch (mCursorPos) {
|
|
case CursorPosition_TopLeft:
|
|
case CursorPosition_Left:
|
|
case CursorPosition_BottomLeft: {
|
|
parentGeometry = parentGeometry.adjusted(m_resizeGap, 0, 0, 0);
|
|
deltaWidth = oldGeometry.left() - globalPos.x();
|
|
newWidth = bound(minWidth, mTarget->width() + deltaWidth, maxWidth);
|
|
deltaWidth = newWidth - mTarget->width();
|
|
if (deltaWidth != 0) {
|
|
newGeometry.setLeft(newGeometry.left() - deltaWidth);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case CursorPosition_TopRight:
|
|
case CursorPosition_Right:
|
|
case CursorPosition_BottomRight: {
|
|
parentGeometry = parentGeometry.adjusted(0, 0, -m_resizeGap, 0);
|
|
deltaWidth = globalPos.x() - newGeometry.right();
|
|
newWidth = bound(minWidth, mTarget->width() + deltaWidth, maxWidth);
|
|
deltaWidth = newWidth - mTarget->width();
|
|
if (deltaWidth != 0) {
|
|
newGeometry.setRight(oldGeometry.right() + deltaWidth);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
{
|
|
const int maxHeight = mTarget->maxSizeHint().height();
|
|
const int minHeight = mTarget->minSize().height();
|
|
int deltaHeight = 0;
|
|
int newHeight = 0;
|
|
switch (mCursorPos) {
|
|
case CursorPosition_TopLeft:
|
|
case CursorPosition_Top:
|
|
case CursorPosition_TopRight: {
|
|
parentGeometry = parentGeometry.adjusted(0, m_resizeGap, 0, 0);
|
|
deltaHeight = oldGeometry.top() - globalPos.y();
|
|
newHeight = bound(minHeight, mTarget->height() + deltaHeight, maxHeight);
|
|
deltaHeight = newHeight - mTarget->height();
|
|
if (deltaHeight != 0) {
|
|
newGeometry.setTop(newGeometry.top() - deltaHeight);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case CursorPosition_BottomLeft:
|
|
case CursorPosition_Bottom:
|
|
case CursorPosition_BottomRight: {
|
|
parentGeometry = parentGeometry.adjusted(0, 0, 0, -m_resizeGap);
|
|
deltaHeight = globalPos.y() - newGeometry.bottom();
|
|
newHeight = bound(minHeight, mTarget->height() + deltaHeight, maxHeight);
|
|
deltaHeight = newHeight - mTarget->height();
|
|
if (deltaHeight != 0) {
|
|
newGeometry.setBottom(oldGeometry.bottom() + deltaHeight);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newGeometry == mTarget->geometry()) {
|
|
// Nothing to do.
|
|
return true;
|
|
}
|
|
|
|
if (!mTarget->isRootView()) {
|
|
|
|
// Clip to parent's geometry.
|
|
newGeometry = newGeometry.intersected(parentGeometry);
|
|
|
|
// Back to local.
|
|
newGeometry.moveTopLeft(mTarget->mapFromGlobal(newGeometry.topLeft()) + mTarget->pos());
|
|
}
|
|
|
|
mTarget->setGeometry(newGeometry);
|
|
return true;
|
|
}
|
|
|
|
#ifdef KDDW_FRONTEND_QT_WINDOWS
|
|
|
|
void WidgetResizeHandler::requestNCCALCSIZE(HWND winId)
|
|
{
|
|
SetWindowPos(winId, 0, 0, 0, 0, 0,
|
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
|
}
|
|
|
|
/// Handler to enable Aero-snap
|
|
bool WidgetResizeHandler::handleWindowsNativeEvent(Core::FloatingWindow *fw,
|
|
const QByteArray &eventType, void *message,
|
|
Qt5Qt6Compat::qintptr *result)
|
|
{
|
|
if (eventType != "windows_generic_MSG")
|
|
return false;
|
|
|
|
auto msg = static_cast<MSG *>(message);
|
|
if (msg->message == WM_NCHITTEST) {
|
|
if (DragController::instance()->isInClientDrag()) {
|
|
// There's a non-native drag going on.
|
|
*result = 0;
|
|
return false;
|
|
}
|
|
|
|
const Rect htCaptionRect = fw->dragRect();
|
|
const bool ret = handleWindowsNativeEvent(fw->view()->window(), msg, result, htCaptionRect);
|
|
|
|
fw->setLastHitTest(*result);
|
|
return ret;
|
|
} else if (msg->message == WM_NCLBUTTONDBLCLK) {
|
|
if ((Config::self().flags() & Config::Flag_DoubleClickMaximizes)) {
|
|
return handleWindowsNativeEvent(fw->view()->window(), msg, result, {});
|
|
} else {
|
|
// Let the title bar handle it. It will re-dock the window.
|
|
if (Core::TitleBar *titleBar = fw->titleBar()) {
|
|
if (titleBar->isVisible()) { // can't be invisible afaik
|
|
titleBar->onDoubleClicked();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return handleWindowsNativeEvent(fw->view()->window(), msg, result, {});
|
|
}
|
|
|
|
bool WidgetResizeHandler::handleWindowsNativeEvent(Core::Window::Ptr w, MSG *msg,
|
|
Qt5Qt6Compat::qintptr *result,
|
|
const NativeFeatures &features)
|
|
{
|
|
if (msg->message == WM_NCCALCSIZE && features.hasShadow()) {
|
|
if (w->windowState() == WindowState::Minimized && w->hasBeenMinimizedDirectlyFromRestore()) {
|
|
// Qt is buggy with custom WM_NCCALCSIZE if window is minimized.
|
|
// Use full frame when minimized. We'll trigger WM_NCCALCSIZE when un-minimized.
|
|
return false;
|
|
}
|
|
|
|
*result = 0;
|
|
return true;
|
|
} else if (msg->message == WM_NCHITTEST && (features.hasResize() || features.hasDrag())) {
|
|
const int borderWidth = 8;
|
|
const bool hasFixedWidth = w->minWidth() == w->maxWidth();
|
|
const bool hasFixedHeight = w->minHeight() == w->maxHeight();
|
|
const bool hasFixedWidthOrHeight = hasFixedWidth || hasFixedHeight;
|
|
|
|
*result = 0;
|
|
const int xPos = GET_X_LPARAM(msg->lParam);
|
|
const int yPos = GET_Y_LPARAM(msg->lParam);
|
|
RECT rect;
|
|
GetWindowRect(reinterpret_cast<HWND>(w->handle()), &rect);
|
|
|
|
if (!hasFixedWidthOrHeight && xPos >= rect.left && xPos <= rect.left + borderWidth && yPos <= rect.bottom
|
|
&& yPos >= rect.bottom - borderWidth && features.hasResize()) {
|
|
*result = HTBOTTOMLEFT;
|
|
} else if (!hasFixedWidthOrHeight && xPos < rect.right && xPos >= rect.right - borderWidth && yPos <= rect.bottom
|
|
&& yPos >= rect.bottom - borderWidth && features.hasResize()) {
|
|
*result = HTBOTTOMRIGHT;
|
|
} else if (!hasFixedWidthOrHeight && xPos >= rect.left && xPos <= rect.left + borderWidth && yPos >= rect.top
|
|
&& yPos <= rect.top + borderWidth && features.hasResize()) {
|
|
*result = HTTOPLEFT;
|
|
} else if (!hasFixedWidthOrHeight && xPos <= rect.right && xPos >= rect.right - borderWidth && yPos >= rect.top
|
|
&& yPos < rect.top + borderWidth && features.hasResize()) {
|
|
*result = HTTOPRIGHT;
|
|
} else if (!hasFixedWidth && xPos >= rect.left && xPos <= rect.left + borderWidth
|
|
&& features.hasResize()) {
|
|
*result = HTLEFT;
|
|
} else if (!hasFixedHeight && yPos >= rect.top && yPos <= rect.top + borderWidth
|
|
&& features.hasResize()) {
|
|
*result = HTTOP;
|
|
} else if (!hasFixedHeight && yPos <= rect.bottom && yPos >= rect.bottom - borderWidth
|
|
&& features.hasResize()) {
|
|
*result = HTBOTTOM;
|
|
} else if (!hasFixedWidth && xPos <= rect.right && xPos >= rect.right - borderWidth
|
|
&& features.hasResize()) {
|
|
*result = HTRIGHT;
|
|
} else if (features.hasDrag()) {
|
|
const Point globalPosQt = w->fromNativePixels(Point(xPos, yPos));
|
|
// htCaptionRect is the rect on which we allow for Windows to do a native drag
|
|
const Rect htCaptionRect = features.htCaptionRect;
|
|
if (globalPosQt.y() >= htCaptionRect.top() && globalPosQt.y() <= htCaptionRect.bottom()
|
|
&& globalPosQt.x() >= htCaptionRect.left()
|
|
&& globalPosQt.x() <= htCaptionRect.right()) {
|
|
if (!Platform::instance()->inDisallowedDragView(
|
|
globalPosQt)) { // Just makes sure the mouse isn't over the close button, we
|
|
// don't allow drag in that case.
|
|
*result = HTCAPTION;
|
|
}
|
|
}
|
|
}
|
|
|
|
return *result != 0;
|
|
} else if (msg->message == WM_NCLBUTTONDBLCLK && features.hasMaximize()) {
|
|
// By returning false we accept Windows native action, a maximize.
|
|
// We could also call titleBar->onDoubleClicked(); here which will maximize if
|
|
// Flag_DoubleClickMaximizes is set, but there's a bug in QWidget::showMaximized() on
|
|
// Windows when we're covering the native title bar, the window is maximized with an offset.
|
|
// So instead, use a native maximize which works well
|
|
return false;
|
|
} else if (msg->message == WM_GETMINMAXINFO) {
|
|
// Qt doesn't work well with windows that don't have title bar but have native frames.
|
|
// When maximized they go out of bounds and the title bar is clipped, so catch
|
|
// WM_GETMINMAXINFO and patch the size
|
|
|
|
// According to microsoft docs it only works for the primary screen, but extrapolates for
|
|
// the others
|
|
auto screen = Platform::instance()->primaryScreen();
|
|
if (!screen || w->screen() != screen) {
|
|
return false;
|
|
}
|
|
|
|
DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
|
|
|
|
const Rect availableGeometry = screen->availableGeometry();
|
|
|
|
auto mmi = reinterpret_cast<MINMAXINFO *>(msg->lParam);
|
|
const double dpr = screen->devicePixelRatio();
|
|
|
|
mmi->ptMaxSize.y = int(availableGeometry.height() * dpr);
|
|
mmi->ptMaxSize.x =
|
|
int(availableGeometry.width() * dpr) - 1; // -1 otherwise it gets bogus size
|
|
mmi->ptMaxPosition.x = availableGeometry.x();
|
|
mmi->ptMaxPosition.y = availableGeometry.y();
|
|
|
|
mmi->ptMinTrackSize.x = int(w->minWidth() * dpr);
|
|
mmi->ptMinTrackSize.y = int(w->minHeight() * dpr);
|
|
|
|
*result = 0;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endif
|
|
|
|
void WidgetResizeHandler::setTarget(View *w)
|
|
{
|
|
if (w) {
|
|
mTarget = w;
|
|
mTargetGuard = w;
|
|
mTarget->setMouseTracking(true);
|
|
if (m_usesGlobalEventFilter) {
|
|
Platform::instance()->installGlobalEventFilter(this);
|
|
} else {
|
|
mTarget->installViewEventFilter(this);
|
|
}
|
|
} else {
|
|
KDDW_ERROR("Target widget is null!");
|
|
}
|
|
}
|
|
|
|
void WidgetResizeHandler::updateCursor(CursorPosition m)
|
|
{
|
|
if (!m_handlesMouseCursor)
|
|
return;
|
|
|
|
if (!mTargetGuard) {
|
|
// Our target was destroyed while we're processing mouse events, it's fine.
|
|
restoreMouseCursor();
|
|
return;
|
|
}
|
|
|
|
if (Platform::instance()->isQtWidgets()) {
|
|
// Need for updating cursor when we change child widget
|
|
const auto childViews = mTarget->childViews();
|
|
for (const auto &child : childViews) {
|
|
if (!child->hasAttribute(Qt::WA_SetCursor)) {
|
|
child->setCursor(Qt::ArrowCursor);
|
|
}
|
|
}
|
|
}
|
|
switch (m) {
|
|
case CursorPosition_TopLeft:
|
|
case CursorPosition_BottomRight:
|
|
setMouseCursor(Qt::SizeFDiagCursor);
|
|
break;
|
|
case CursorPosition_BottomLeft:
|
|
case CursorPosition_TopRight:
|
|
setMouseCursor(Qt::SizeBDiagCursor);
|
|
break;
|
|
case CursorPosition_Top:
|
|
case CursorPosition_Bottom:
|
|
setMouseCursor(Qt::SizeVerCursor);
|
|
break;
|
|
case CursorPosition_Left:
|
|
case CursorPosition_Right:
|
|
setMouseCursor(Qt::SizeHorCursor);
|
|
break;
|
|
case CursorPosition_Undefined:
|
|
restoreMouseCursor();
|
|
break;
|
|
case CursorPosition_All:
|
|
case CursorPosition_Horizontal:
|
|
case CursorPosition_Vertical:
|
|
// Doesn't happen
|
|
break;
|
|
}
|
|
}
|
|
|
|
void WidgetResizeHandler::setMouseCursor(Qt::CursorShape cursor)
|
|
{
|
|
if (m_usesGlobalEventFilter) {
|
|
Platform::instance()->setMouseCursor(cursor, /*discardLast=*/m_overrideCursorSet);
|
|
m_overrideCursorSet = true;
|
|
} else if (mTargetGuard) {
|
|
mTarget->setCursor(cursor);
|
|
}
|
|
}
|
|
|
|
void WidgetResizeHandler::restoreMouseCursor()
|
|
{
|
|
if (!m_handlesMouseCursor)
|
|
return;
|
|
|
|
if (m_usesGlobalEventFilter) {
|
|
if (m_overrideCursorSet) {
|
|
Platform::instance()->restoreMouseCursor();
|
|
m_overrideCursorSet = false;
|
|
}
|
|
} else if (mTargetGuard) {
|
|
mTarget->setCursor(Qt::ArrowCursor);
|
|
}
|
|
}
|
|
|
|
CursorPosition WidgetResizeHandler::cursorPosition_(Point globalPos) const
|
|
{
|
|
Point pos = mTarget->mapFromGlobal(globalPos);
|
|
|
|
const int x = pos.x();
|
|
const int y = pos.y();
|
|
const int margin = widgetResizeHandlerMargin();
|
|
|
|
QFlags<CursorPosition>::Int result = CursorPosition_Undefined;
|
|
if (y >= -margin && y <= mTarget->height() + margin) {
|
|
if (std::abs(x) <= margin)
|
|
result |= CursorPosition_Left;
|
|
else if (std::abs(x - (mTarget->width() - margin)) <= margin)
|
|
result |= CursorPosition_Right;
|
|
}
|
|
|
|
if (x >= -margin && x <= mTarget->width() + margin) {
|
|
if (std::abs(y) <= margin)
|
|
result |= CursorPosition_Top;
|
|
else if (std::abs(y - (mTarget->height() - margin)) <= margin)
|
|
result |= CursorPosition_Bottom;
|
|
}
|
|
|
|
// Filter out sides we don't allow
|
|
result = result & mAllowedResizeSides;
|
|
|
|
return static_cast<CursorPosition>(result);
|
|
}
|
|
|
|
CursorPosition WidgetResizeHandler::cursorPosition(Point globalPos) const
|
|
{
|
|
if (!mTargetGuard)
|
|
return CursorPosition_Undefined;
|
|
|
|
auto candidatePos = cursorPosition_(globalPos);
|
|
|
|
if (isMDI()) {
|
|
int result = int(candidatePos);
|
|
|
|
if (auto group = mTarget->asGroupController()) {
|
|
// Honour fixed size. Don't show mouse shape for resizing.
|
|
if (group->isFixedHeight())
|
|
result &= ~CursorPosition_Vertical;
|
|
|
|
if (group->isFixedWidth())
|
|
result &= ~CursorPosition_Horizontal;
|
|
} else {
|
|
KDDW_ERROR("WidgetResizeHandler::cursorPosition: Expected group");
|
|
}
|
|
|
|
return static_cast<CursorPosition>(result);
|
|
} else {
|
|
// For regular docking there's other mechanisms to enforce fixed size
|
|
return candidatePos;
|
|
}
|
|
}
|
|
|
|
/** static */
|
|
void WidgetResizeHandler::setupWindow(Core::Window::Ptr window)
|
|
{
|
|
// Does some minor setup on our QWindow.
|
|
// Like adding the drop shadow on Windows and two other workarounds.
|
|
#ifdef KDDW_FRONTEND_QT_WINDOWS
|
|
if (KDDockWidgets::usesAeroSnapWithCustomDecos()) {
|
|
const auto wid = HWND(window->handle());
|
|
window->onScreenChanged(nullptr, [](QObject *, Window::Ptr win) {
|
|
// Qt honors our frame hijacking usually... but when screen changes we must give it a
|
|
// nudge. Otherwise what Qt thinks is the client area is not what Windows knows it is.
|
|
// SetWindowPos() will trigger an NCCALCSIZE message, which Qt will intercept and take
|
|
// note of the margins we're using.
|
|
requestNCCALCSIZE(HWND(win->handle()));
|
|
});
|
|
|
|
const bool usesTransparentFloatingWindow =
|
|
Config::self().internalFlags() & Config::InternalFlag_UseTransparentFloatingWindow;
|
|
if (!usesTransparentFloatingWindow) {
|
|
// This enables the native drop shadow.
|
|
// Doesn't work well if the floating window has transparent round corners (shows weird
|
|
// white line).
|
|
|
|
MARGINS margins = { 0, 0, 0, 1 }; // arbitrary, just needs to be > 0 it seems
|
|
DwmExtendFrameIntoClientArea(wid, &margins);
|
|
}
|
|
}
|
|
#else
|
|
KDDW_UNUSED(window);
|
|
#endif // Q_OS_WIN
|
|
}
|
|
|
|
#ifdef KDDW_FRONTEND_QT_WINDOWS
|
|
bool WidgetResizeHandler::isInterestingNativeEvent(unsigned int nativeEvent)
|
|
{
|
|
switch (nativeEvent) {
|
|
case WM_NCHITTEST:
|
|
case WM_NCCALCSIZE:
|
|
case WM_NCLBUTTONDBLCLK:
|
|
case WM_GETMINMAXINFO:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if defined(Q_OS_WIN) && defined(KDDW_FRONTEND_QTWIDGETS)
|
|
bool NCHITTESTEventFilter::nativeEventFilter(const QByteArray &eventType, void *message,
|
|
Qt5Qt6Compat::qintptr *result)
|
|
|
|
{
|
|
if (eventType != "windows_generic_MSG" || !m_guard)
|
|
return false;
|
|
|
|
auto msg = static_cast<MSG *>(message);
|
|
if (msg->message != WM_NCHITTEST)
|
|
return false;
|
|
const WId wid = WId(msg->hwnd);
|
|
|
|
auto child = QtCommon::Platform_qt::instance()->qobjectAsView(QWidget::find(wid));
|
|
|
|
if (!child || !m_floatingWindow->equals(child->rootView()))
|
|
return false;
|
|
|
|
const bool parentIsDockWidget = [&child]() -> bool {
|
|
auto parent = child->parentView();
|
|
while (parent) {
|
|
if (parent->is(Core::ViewType::DockWidget))
|
|
return true;
|
|
parent = parent->parentView();
|
|
}
|
|
return false;
|
|
}();
|
|
if (parentIsDockWidget) {
|
|
// we don't want to filter this message for native widgets contained in the dock widget
|
|
return false;
|
|
}
|
|
|
|
const bool isThisWindow = m_floatingWindow->equals(child);
|
|
|
|
if (!isThisWindow) {
|
|
*result = HTTRANSPARENT;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
void WidgetResizeHandler::setEventFilterStartsManually()
|
|
{
|
|
m_eventFilteringStartsManually = true;
|
|
EventFilterInterface::setEnabled(false);
|
|
}
|
|
|
|
void WidgetResizeHandler::setHandlesMouseCursor(bool handles)
|
|
{
|
|
m_handlesMouseCursor = handles;
|
|
}
|