Files
KDDockWidgets/src/private/FloatingWindow.cpp
Sergio Martins 7995a6d13b Windows: Fix dragging sometimes not showing indicators
DragController thought there was a resize going on.
The hardcoded margins in FloatingWindow::isInDragArea() arent
needed anymore.

iPlease enter the commit message for your changes. Lines starting
2020-10-02 17:05:16 +01:00

465 lines
14 KiB
C++

/*
This file is part of KDDockWidgets.
SPDX-FileCopyrightText: 2019-2020 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 "FloatingWindow_p.h"
#include "MainWindowBase.h"
#include "Logging_p.h"
#include "Frame_p.h"
#include "DropArea_p.h"
#include "TitleBar_p.h"
#include "WindowBeingDragged_p.h"
#include "Utils_p.h"
#include "WidgetResizeHandler_p.h"
#include "DockRegistry_p.h"
#include "Config.h"
#include "FrameworkWidgetFactory.h"
#include <QCloseEvent>
#include <QAbstractNativeEventFilter>
#include <QWindow>
#if defined(Q_OS_WIN) && defined(KDDOCKWIDGETS_QTWIDGETS)
# include <Windows.h>
# include <dwmapi.h>
# pragma comment(lib, "Dwmapi.lib")
#endif
using namespace KDDockWidgets;
#if defined(Q_OS_WIN) && defined(KDDOCKWIDGETS_QTWIDGETS)
namespace KDDockWidgets {
/**
* @brief Helper to rediriect WM_NCHITTEST from child widgets to the top-level widget
*
* To implement aero-snap the top-level window must respond to WM_NCHITTEST, we do that
* in FloatingWindow::nativeEvent(). But if the child widgets have a native handle, then
* the WM_NCHITTEST will go to them. They have to respond HTTRANSPARENT so the event
* is redirected.
*/
class NCHITTESTEventFilter : public QAbstractNativeEventFilter
{
public:
explicit NCHITTESTEventFilter(FloatingWindow *fw) : m_floatingWindow(fw) {}
bool nativeEventFilter(const QByteArray &eventType, void *message, long *result) override
{
if (eventType != "windows_generic_MSG" || !m_floatingWindow)
return false;
auto msg = static_cast<MSG *>(message);
if (msg->message != WM_NCHITTEST)
return false;
QWidget *child = QWidget::find(WId(msg->hwnd));
if (!child || child->window() != m_floatingWindow)
return false;
if (child != m_floatingWindow) {
*result = HTTRANSPARENT;
return true;
}
return false;
}
QPointer<FloatingWindow> m_floatingWindow;
};
}
#endif
static Qt::WindowFlags windowFlagsToUse()
{
if (KDDockWidgets::usesNativeDraggingAndResizing())
return Qt::Window;
if (Config::self().flags() & Config::Flag_internal_DontUseQtToolWindowsForFloatingWindows)
return Qt::Window;
return Qt::Tool;
}
static MainWindowBase* hackFindParentHarder(Frame *frame, MainWindowBase *candidateParent)
{
if (Config::self().flags() & Config::Flag_internal_DontUseParentForFloatingWindows) {
return nullptr;
}
// TODO: Using a parent helps the floating windows stay in front of the main window always.
// We're not receiving the parent via ctor argument as the app can have multiple-main windows,
// so use a hack here.
// Not quite clear what to do if the app supports multiple main windows though.
if (candidateParent)
return candidateParent;
const MainWindowBase::List windows = DockRegistry::self()->mainwindows();
if (windows.isEmpty())
return nullptr;
if (windows.size() == 1) {
return windows.first();
} else {
const QStringList affinities = frame ? frame->affinities() : QStringList();
const MainWindowBase::List mainWindows = DockRegistry::self()->mainWindowsWithAffinity(affinities);
if (mainWindows.isEmpty()) {
qWarning() << Q_FUNC_INFO << "No window with affinity" << affinities << "found";
return nullptr;
} else {
return mainWindows.first();
}
}
}
MainWindowBase *actualParent(MainWindowBase *candidate)
{
return (Config::self().flags() & Config::Flag_internal_DontUseParentForFloatingWindows)
? nullptr
: candidate;
}
FloatingWindow::FloatingWindow(MainWindowBase *parent)
: QWidgetAdapter(actualParent(parent), windowFlagsToUse())
, Draggable(this, KDDockWidgets::usesNativeDraggingAndResizing()) // FloatingWindow is only draggable when using a native title bar. Otherwise the KDDockWidgets::TitleBar is the draggable
, m_dropArea(new DropArea(this))
, m_titleBar(Config::self().frameworkWidgetFactory()->createTitleBar(this))
{
#if defined(Q_OS_WIN) && defined(KDDOCKWIDGETS_QTWIDGETS)
// On Windows with Qt 5.9 (and maybe earlier), the WM_NCALCSIZE isn't being processed unless we explicitly create the window.
// So create it now, otherwise floating dock widgets will show a native title bar until resized.
create();
if (KDDockWidgets::usesAeroSnapWithCustomDecos()) {
m_nchittestFilter = new NCHITTESTEventFilter(this);
qApp->installNativeEventFilter(m_nchittestFilter);
connect(windowHandle(), &QWindow::screenChanged, this, [this] {
// Qt honours our frame hijaking 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.
SetWindowPos(HWND(winId()), 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
});
// Show drop-shadow:
MARGINS margins = {0, 0, 0, 1}; // arbitrary, just needs to be > 0 it seems
DwmExtendFrameIntoClientArea(HWND(winId()), &margins);
}
#endif
DockRegistry::self()->registerNestedWindow(this);
qCDebug(creation) << "FloatingWindow()" << this;
maybeCreateResizeHandler();
updateTitleBarVisibility();
connect(m_dropArea, &MultiSplitter::visibleWidgetCountChanged, this, &FloatingWindow::onFrameCountChanged);
connect(m_dropArea, &MultiSplitter::visibleWidgetCountChanged, this, &FloatingWindow::numFramesChanged);
connect(m_dropArea, &MultiSplitter::visibleWidgetCountChanged, this, &FloatingWindow::onVisibleFrameCountChanged);
m_layoutDestroyedConnection = connect(m_dropArea, &QObject::destroyed, this, &FloatingWindow::scheduleDeleteLater);
}
FloatingWindow::FloatingWindow(Frame *frame, MainWindowBase *parent)
: FloatingWindow(hackFindParentHarder(frame, parent))
{
m_disableSetVisible = true;
// Adding a widget will trigger onFrameCountChanged, which triggers a setVisible(true).
// The problem with setVisible(true) will forget about or requested geometry and place the window at 0,0
// So disable the setVisible(true) call while in the ctor.
m_dropArea->addWidget(frame, KDDockWidgets::Location_OnTop, {});
m_disableSetVisible = false;
}
FloatingWindow::~FloatingWindow()
{
m_inDtor = true;
disconnect(m_layoutDestroyedConnection);
delete m_nchittestFilter;
DockRegistry::self()->unregisterNestedWindow(this);
qCDebug(creation) << "~FloatingWindow";
}
#if defined(Q_OS_WIN) && defined(KDDOCKWIDGETS_QTWIDGETS)
bool FloatingWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)
{
if (!m_inDtor && !m_deleteScheduled && KDDockWidgets::usesAeroSnapWithCustomDecos()) {
// To enable aero snap we need to tell Windows where's our custom title bar
if (WidgetResizeHandler::handleWindowsNativeEvent(this, eventType, message, result))
return true;
}
return QWidget::nativeEvent(eventType, message, result);
}
#endif
void FloatingWindow::maybeCreateResizeHandler()
{
if (!KDDockWidgets::usesNativeDraggingAndResizing()) {
setFlag(Qt::FramelessWindowHint, true);
#ifdef KDDOCKWIDGETS_QTWIDGETS
setWidgetResizeHandler(new WidgetResizeHandler(this));
#endif
}
}
std::unique_ptr<WindowBeingDragged> FloatingWindow::makeWindow()
{
return std::unique_ptr<WindowBeingDragged>(new WindowBeingDragged(this, this));
}
DockWidgetBase *FloatingWindow::singleDockWidget() const
{
const Frame::List frames = this->frames();
if (frames.size() == 1) {
Frame *frame = frames.first();
if (frame->hasSingleDockWidget())
return frame->dockWidgetAt(0);
}
return nullptr;
}
const Frame::List FloatingWindow::frames() const
{
Q_ASSERT(m_dropArea);
return m_dropArea->frames();
}
void FloatingWindow::setSuggestedGeometry(QRect suggestedRect, bool preserveCenter)
{
const Frame::List frames = this->frames();
if (frames.size() == 1) {
// Let's honour max-size when we have a single-frame.
// multi-frame cases are more complicated and we're not sure if we want the window to bounce around.
// single-frame is the most common case, like floating a dock widget, so let's do that first, it's also
// easy.
Frame *frame = frames[0];
const QSize waste = (minimumSize() - frame->minSize()).expandedTo(QSize(0, 0));
const QSize size = (frame->maxSizeHint() + waste).boundedTo(suggestedRect.size());
// Resize to new size but preserve center
const QPoint originalCenter = suggestedRect.center();
suggestedRect.setSize(size);
if (preserveCenter)
suggestedRect.moveCenter(originalCenter);
}
setGeometry(suggestedRect);
}
void FloatingWindow::scheduleDeleteLater()
{
m_deleteScheduled = true;
DockRegistry::self()->unregisterNestedWindow(this);
deleteLater();
}
MultiSplitter *FloatingWindow::multiSplitter() const
{
return m_dropArea;
}
bool FloatingWindow::isInDragArea(QPoint globalPoint) const
{
return dragRect().contains(globalPoint);
}
bool FloatingWindow::anyNonClosable() const
{
for (Frame *frame : frames()) {
if (frame->anyNonClosable())
return true;
}
return false;
}
bool FloatingWindow::anyNonDockable() const
{
for (Frame *frame : frames()) {
if (frame->anyNonDockable())
return true;
}
return false;
}
bool FloatingWindow::hasSingleFrame() const
{
return m_dropArea->numFrames() == 1;
}
bool FloatingWindow::hasSingleDockWidget() const
{
const Frame::List frames = this->frames();
if (frames.size() != 1)
return false;
Frame *frame = frames.first();
return frame->dockWidgetCount() == 1;
}
bool FloatingWindow::beingDeleted() const
{
if (m_deleteScheduled)
return true;
// TODO: Confusing logic
for (Frame *f : frames()) {
if (!f->beingDeletedLater())
return false;
}
return true;
}
void FloatingWindow::onFrameCountChanged(int count)
{
qCDebug(docking) << "FloatingWindow::onFrameCountChanged" << count;
if (count == 0) {
scheduleDeleteLater();
} else {
updateTitleBarVisibility();
if (count == 1) // if something was removed, then our single dock widget is floating, we need to check the QAction
dropArea()->updateFloatingActions();
}
}
void FloatingWindow::onVisibleFrameCountChanged(int count)
{
if (!m_disableSetVisible) {
qCDebug(hiding) << "FloatingWindow::onVisibleFrameCountChanged count=" << count;
setVisible(count > 0);
}
}
void FloatingWindow::updateTitleBarVisibility()
{
updateTitleAndIcon();
bool visible = true;
if (KDDockWidgets::usesNativeTitleBar()) {
visible = false;
} else {
const auto flags = Config::self().flags();
if ((flags & Config::Flag_HideTitleBarWhenTabsVisible) && !(flags & Config::Flag_AlwaysTitleBarWhenFloating)) {
if (hasSingleFrame()) {
visible = !frames().first()->hasTabsVisible();
}
}
for (Frame *frame : frames())
frame->updateTitleBarVisibility();
}
m_titleBar->setVisible(visible);
}
QStringList FloatingWindow::affinities() const
{
auto frames = this->frames();
return frames.isEmpty() ? QStringList() : frames.constFirst()->affinities();
}
void FloatingWindow::updateTitleAndIcon()
{
QString title;
QIcon icon;
if (hasSingleFrame()) {
const Frame *frame = frames().constFirst();
title = frame->title();
icon = frame->icon();
} else {
title = qApp->applicationName();
}
m_titleBar->setTitle(title);
m_titleBar->setIcon(icon);
if (KDDockWidgets::usesNativeTitleBar()) {
setWindowTitle(title);
setWindowIcon(icon);
}
}
void FloatingWindow::onCloseEvent(QCloseEvent *e)
{
qCDebug(closing) << "Frame::closeEvent";
if (e->spontaneous() && anyNonClosable()) {
// Event from the window system won't close us
e->ignore();
return;
}
e->accept(); // Accepted by default (will close unless ignored)
const Frame::List frames = this->frames();
for (Frame *frame : frames) {
qApp->sendEvent(frame, e);
if (!e->isAccepted())
break; // Stop when the first frame prevents closing
}
}
bool FloatingWindow::deserialize(const LayoutSaver::FloatingWindow &fw)
{
if (dropArea()->deserialize(fw.multiSplitterLayout)) {
updateTitleBarVisibility();
show();
return true;
} else {
return false;
}
}
LayoutSaver::FloatingWindow FloatingWindow::serialize() const
{
LayoutSaver::FloatingWindow fw;
fw.geometry = geometry();
fw.isVisible = isVisible();
fw.multiSplitterLayout = dropArea()->serialize();
fw.screenIndex = screenNumberForWidget(this);
fw.screenSize = screenSizeForWidget(this);
fw.affinities = affinities();
auto mainWindow = qobject_cast<MainWindowBase*>(parentWidget());
fw.parentIndex = mainWindow ? DockRegistry::self()->mainwindows().indexOf(mainWindow)
: -1;
return fw;
}
QRect FloatingWindow::dragRect() const
{
QRect rect;
if (m_titleBar->isVisible()) {
rect = m_titleBar->rect();
rect.moveTopLeft(m_titleBar->mapToGlobal(QPoint(0, 0)));
} else if (hasSingleFrame()) {
rect = frames().constFirst()->dragRect();
} else {
qWarning() << Q_FUNC_INFO << "Expected a title bar";
}
return rect;
}
bool FloatingWindow::event(QEvent *ev)
{
if (ev->type() == QEvent::ActivationChange) {
// Since QWidget is missing a signal for window activation
Q_EMIT activatedChanged();
}
return QWidgetAdapter::event(ev);
}