395 lines
13 KiB
C++
395 lines
13 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 "TitleBar.h"
|
|
|
|
#include "core/TitleBar.h"
|
|
#include "core/FloatingWindow.h"
|
|
#include "core/Window_p.h"
|
|
#include "core/Utils_p.h"
|
|
#include "core/View_p.h"
|
|
#include "core/Logging_p.h"
|
|
#include "core/TitleBar_p.h"
|
|
#include "core/DockRegistry_p.h"
|
|
|
|
#include "qtwidgets/ViewFactory.h"
|
|
|
|
#include <QPainter>
|
|
#include <QStyle>
|
|
#include <QStyleOptionDockWidget>
|
|
#include <QHBoxLayout>
|
|
#include <QLabel>
|
|
#include <QTimer>
|
|
#include <QScopedValueRollback>
|
|
|
|
using namespace KDDockWidgets;
|
|
using namespace KDDockWidgets::QtWidgets;
|
|
|
|
Button::~Button()
|
|
{
|
|
}
|
|
|
|
void Button::paintEvent(QPaintEvent *)
|
|
{
|
|
QPainter p(this);
|
|
QStyleOptionToolButton opt;
|
|
opt.initFrom(this);
|
|
|
|
if (isEnabled() && underMouse()) {
|
|
if (isDown()) {
|
|
opt.state |= QStyle::State_Sunken;
|
|
} else {
|
|
opt.state |= QStyle::State_Raised;
|
|
}
|
|
style()->drawPrimitive(QStyle::PE_PanelButtonTool, &opt, &p, this);
|
|
}
|
|
|
|
opt.subControls = QStyle::SC_None;
|
|
opt.features = QStyleOptionToolButton::None;
|
|
opt.icon = icon();
|
|
opt.iconSize = iconSize();
|
|
|
|
// The first icon size is for scaling 1x, and is what QStyle expects. QStyle will pick ones
|
|
// with higher resolution automatically when needed.
|
|
const QList<QSize> iconSizes = opt.icon.availableSizes();
|
|
if (!iconSizes.isEmpty()) {
|
|
opt.iconSize = iconSizes.constFirst();
|
|
|
|
const qreal logicalFactor = logicalDpiX() / 96.0;
|
|
|
|
// On Linux there's dozens of window managers and ways of setting the scaling.
|
|
// Some window managers will just change the font dpi (which affects logical dpi), while
|
|
// others will only change the device pixel ratio. Take care of both cases.
|
|
// macOS is easier, as it never changes logical DPI.
|
|
// On Windows, with AA_EnableHighDpiScaling, logical DPI is always 96 and physical is
|
|
// manipulated instead.
|
|
#if defined(Q_OS_LINUX)
|
|
const qreal dpr = devicePixelRatioF();
|
|
const qreal combinedFactor = logicalFactor * dpr;
|
|
|
|
if (scalingFactorIsSupported(combinedFactor)) // Older Qt has rendering bugs with fractional
|
|
// factors
|
|
opt.iconSize = opt.iconSize * combinedFactor;
|
|
#elif defined(Q_OS_WIN) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
|
// Probably Windows could use the same code path as Linux, but I'm seeing too thick icons on
|
|
// Windows...
|
|
if (!QGuiApplication::testAttribute(Qt::AA_EnableHighDpiScaling)
|
|
&& scalingFactorIsSupported(logicalFactor)) // Older Qt has rendering bugs with
|
|
// fractional factors
|
|
opt.iconSize = opt.iconSize * logicalFactor;
|
|
#else
|
|
Q_UNUSED(logicalFactor);
|
|
#endif
|
|
}
|
|
|
|
style()->drawComplexControl(QStyle::CC_ToolButton, &opt, &p, this);
|
|
}
|
|
|
|
QSize Button::sizeHint() const
|
|
{
|
|
const int h = 24;
|
|
return QSize(h, h);
|
|
}
|
|
|
|
bool Button::event(QEvent *ev)
|
|
{
|
|
switch (ev->type()) {
|
|
case QEvent::MouseButtonPress:
|
|
case QEvent::MouseButtonRelease:
|
|
case QEvent::MouseButtonDblClick:
|
|
case QEvent::KeyPress:
|
|
case QEvent::KeyRelease: {
|
|
// A Button can trigger the deletion of its parent, in which case we use deleteLater
|
|
m_inEventHandler = true;
|
|
QPointer<QObject> guard = this;
|
|
const bool result = QToolButton::event(ev);
|
|
|
|
if (guard) // Button press can trigger deletion of Button
|
|
m_inEventHandler = false;
|
|
|
|
return result;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return QToolButton::event(ev);
|
|
}
|
|
|
|
class KDDockWidgets::QtWidgets::TitleBar::Private
|
|
{
|
|
public:
|
|
KDBindings::ScopedConnection titleChangedConnection;
|
|
KDBindings::ScopedConnection iconChangedConnection;
|
|
KDBindings::ScopedConnection screenChangedConnection;
|
|
KDBindings::ScopedConnection focusChangedConnection;
|
|
|
|
KDBindings::ScopedConnection closeButtonEnabledConnection;
|
|
KDBindings::ScopedConnection floatButtonToolTipConnection;
|
|
KDBindings::ScopedConnection floatButtonVisibleConnection;
|
|
KDBindings::ScopedConnection autoHideButtonConnection;
|
|
KDBindings::ScopedConnection minimizeButtonConnection;
|
|
KDBindings::ScopedConnection maximizeButtonConnection;
|
|
};
|
|
|
|
TitleBar::TitleBar(Core::TitleBar *controller, Core::View *parent)
|
|
: View(controller, Core::ViewType::TitleBar, View_qt::asQWidget(parent))
|
|
, Core::TitleBarViewInterface(controller)
|
|
, m_layout(new QHBoxLayout(this))
|
|
, d(new Private())
|
|
{
|
|
setAttribute(Qt::WA_StyledBackground, true);
|
|
setObjectName(QStringLiteral("KDDWTitleBar"));
|
|
}
|
|
|
|
TitleBar::TitleBar(QWidget *parent)
|
|
: View(new Core::TitleBar(this), Core::ViewType::TitleBar, parent)
|
|
, Core::TitleBarViewInterface(static_cast<Core::TitleBar *>(controller()))
|
|
, m_layout(new QHBoxLayout(this))
|
|
, d(new Private())
|
|
{
|
|
setAttribute(Qt::WA_StyledBackground, true);
|
|
setObjectName(QStringLiteral("KDDWTitleBar"));
|
|
m_titleBar->init();
|
|
}
|
|
|
|
TitleBar::~TitleBar()
|
|
{
|
|
delete d;
|
|
|
|
/// The window deletion might have been triggered by pressing a button, so use deleteLater()
|
|
for (auto button : { m_closeButton, m_floatButton, m_maximizeButton, m_minimizeButton, m_autoHideButton }) {
|
|
if (!button)
|
|
continue;
|
|
|
|
if (auto kddwButton = qobject_cast<Button *>(button); !kddwButton->m_inEventHandler) {
|
|
// Minor optimization. If the button is not in an event handler it's safe to delete immediately.
|
|
// This saves us from memory leaks at shutdown when using the below QTimer::singleShot() hack.
|
|
delete kddwButton;
|
|
continue;
|
|
}
|
|
|
|
button->setParent(nullptr);
|
|
if (usesQTBUG83030Workaround()) {
|
|
QTimer::singleShot(0, button, [button] {
|
|
/// Workaround for QTBUG-83030. QObject::deleteLater() is buggy with nested event loop
|
|
delete button;
|
|
});
|
|
} else {
|
|
button->deleteLater();
|
|
}
|
|
}
|
|
}
|
|
|
|
void TitleBar::init()
|
|
{
|
|
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
|
|
if (m_titleBar->titleBarIsFocusable())
|
|
setFocusPolicy(Qt::StrongFocus);
|
|
|
|
if (!hasCustomLayout()) {
|
|
m_dockWidgetIcon = new QLabel(this);
|
|
m_layout->addWidget(m_dockWidgetIcon, 0, Qt::AlignVCenter);
|
|
m_layout->addStretch();
|
|
updateMargins();
|
|
|
|
auto factory = static_cast<ViewFactory *>(Config::self().viewFactory());
|
|
m_maximizeButton = factory->createTitleBarButton(this, TitleBarButtonType::Maximize);
|
|
m_minimizeButton = factory->createTitleBarButton(this, TitleBarButtonType::Minimize);
|
|
m_floatButton = factory->createTitleBarButton(this, TitleBarButtonType::Float);
|
|
m_closeButton = factory->createTitleBarButton(this, TitleBarButtonType::Close);
|
|
m_autoHideButton = factory->createTitleBarButton(this, TitleBarButtonType::AutoHide);
|
|
|
|
m_layout->addWidget(m_autoHideButton, 0, Qt::AlignVCenter);
|
|
m_layout->addWidget(m_minimizeButton, 0, Qt::AlignVCenter);
|
|
m_layout->addWidget(m_maximizeButton, 0, Qt::AlignVCenter);
|
|
m_layout->addWidget(m_floatButton, 0, Qt::AlignVCenter);
|
|
m_layout->addWidget(m_closeButton, 0, Qt::AlignVCenter);
|
|
|
|
m_autoHideButton->setVisible(false);
|
|
|
|
connect(m_floatButton, &QAbstractButton::clicked, m_titleBar,
|
|
&Core::TitleBar::onFloatClicked);
|
|
connect(m_closeButton, &QAbstractButton::clicked, m_titleBar,
|
|
&Core::TitleBar::onCloseClicked);
|
|
connect(m_maximizeButton, &QAbstractButton::clicked, m_titleBar,
|
|
&Core::TitleBar::onMaximizeClicked);
|
|
connect(m_minimizeButton, &QAbstractButton::clicked, m_titleBar,
|
|
&Core::TitleBar::onMinimizeClicked);
|
|
connect(m_autoHideButton, &QAbstractButton::clicked, m_titleBar,
|
|
&Core::TitleBar::onAutoHideClicked);
|
|
|
|
m_minimizeButton->setToolTip(tr("Minimize"));
|
|
m_closeButton->setToolTip(tr("Close"));
|
|
|
|
m_floatButton->setVisible(m_titleBar->floatButtonVisible());
|
|
m_floatButton->setToolTip(m_titleBar->floatButtonToolTip());
|
|
|
|
d->closeButtonEnabledConnection = m_titleBar->dptr()->closeButtonChanged.connect([this](bool visible, bool enabled) { m_closeButton->setVisible(visible);
|
|
m_closeButton->setEnabled(enabled); });
|
|
d->floatButtonToolTipConnection = m_titleBar->dptr()->floatButtonToolTipChanged.connect([this](const QString &text) { m_floatButton->setToolTip(text); });
|
|
d->floatButtonVisibleConnection = m_titleBar->dptr()->floatButtonVisibleChanged.connect([this](bool visible) { m_floatButton->setVisible(visible); });
|
|
d->autoHideButtonConnection = m_titleBar->dptr()->autoHideButtonChanged.connect([this](bool visible, bool enabled, TitleBarButtonType type) { updateAutoHideButton(visible, enabled, type); });
|
|
d->minimizeButtonConnection = m_titleBar->dptr()->minimizeButtonChanged.connect([this](bool visible, bool enabled) { updateMinimizeButton(visible, enabled); });
|
|
d->maximizeButtonConnection = m_titleBar->dptr()->maximizeButtonChanged.connect([this](bool visible, bool enabled, TitleBarButtonType type) { updateMaximizeButton(visible, enabled, type); });
|
|
|
|
d->iconChangedConnection = m_titleBar->dptr()->iconChanged.connect([this] { if (m_titleBar->icon().isNull()) {
|
|
m_dockWidgetIcon->setPixmap(QPixmap());
|
|
} else {
|
|
const QPixmap pix = m_titleBar->icon().pixmap(QSize(28, 28));
|
|
m_dockWidgetIcon->setPixmap(pix);
|
|
}
|
|
update(); });
|
|
}
|
|
|
|
d->titleChangedConnection = m_titleBar->dptr()->titleChanged.connect([this] { update(); });
|
|
|
|
d->screenChangedConnection = DockRegistry::self()->dptr()->windowChangedScreen.connect([this](Core::Window::Ptr w) {
|
|
if (View::d->isInWindow(w))
|
|
updateMargins();
|
|
});
|
|
|
|
d->focusChangedConnection = m_titleBar->dptr()->isFocusedChanged.connect([this] {
|
|
Q_EMIT isFocusedChanged();
|
|
});
|
|
}
|
|
|
|
Core::TitleBar *TitleBar::titleBar() const
|
|
{
|
|
return m_titleBar;
|
|
}
|
|
|
|
void TitleBar::paintEvent(QPaintEvent *)
|
|
{
|
|
if (View::d->freed())
|
|
return;
|
|
|
|
QPainter p(this);
|
|
|
|
// Background is handled by QSS via WA_StyledBackground
|
|
QRect textRect = rect();
|
|
|
|
if (!iconRect().isEmpty())
|
|
textRect.setLeft(iconRect().right() + 4);
|
|
|
|
p.setPen(palette().color(QPalette::WindowText));
|
|
p.drawText(textRect, Qt::AlignCenter, m_titleBar->title());
|
|
}
|
|
|
|
void TitleBar::updateMinimizeButton(bool visible, bool enabled)
|
|
{
|
|
if (!m_minimizeButton)
|
|
return;
|
|
|
|
m_minimizeButton->setEnabled(enabled);
|
|
m_minimizeButton->setVisible(visible);
|
|
}
|
|
|
|
void TitleBar::updateAutoHideButton(bool visible, bool enabled, TitleBarButtonType type)
|
|
{
|
|
if (!m_autoHideButton)
|
|
return;
|
|
|
|
m_autoHideButton->setToolTip(type == TitleBarButtonType::AutoHide ? tr("Auto-hide")
|
|
: tr("Disable auto-hide"));
|
|
auto factory = Config::self().viewFactory();
|
|
m_autoHideButton->setIcon(factory->iconForButtonType(type, devicePixelRatioF()));
|
|
m_autoHideButton->setVisible(visible);
|
|
m_autoHideButton->setEnabled(enabled);
|
|
}
|
|
|
|
void TitleBar::updateMaximizeButton(bool visible, bool enabled, TitleBarButtonType type)
|
|
{
|
|
if (!m_maximizeButton)
|
|
return;
|
|
|
|
m_maximizeButton->setEnabled(enabled);
|
|
m_maximizeButton->setVisible(visible);
|
|
if (visible) {
|
|
auto factory = Config::self().viewFactory();
|
|
m_maximizeButton->setIcon(factory->iconForButtonType(type, devicePixelRatioF()));
|
|
m_maximizeButton->setToolTip(type == TitleBarButtonType::Normal ? tr("Restore")
|
|
: tr("Maximize"));
|
|
}
|
|
}
|
|
|
|
QRect TitleBar::iconRect() const
|
|
{
|
|
if (m_titleBar->icon().isNull()) {
|
|
return QRect(0, 0, 0, 0);
|
|
} else {
|
|
return QRect(3, 3, 30, 30);
|
|
}
|
|
}
|
|
|
|
int TitleBar::buttonAreaWidth() const
|
|
{
|
|
int smallestX = width();
|
|
|
|
for (auto button :
|
|
{ m_autoHideButton, m_minimizeButton, m_floatButton, m_maximizeButton, m_closeButton }) {
|
|
if (button && button->isVisible() && button->x() < smallestX)
|
|
smallestX = button->x();
|
|
}
|
|
|
|
return width() - smallestX;
|
|
}
|
|
|
|
void TitleBar::updateMargins()
|
|
{
|
|
const qreal factor = logicalDpiFactor(this);
|
|
m_layout->setContentsMargins(QMargins(4, 0, 4, 0) * factor);
|
|
m_layout->setSpacing(int(2 * factor));
|
|
}
|
|
|
|
void TitleBar::mouseDoubleClickEvent(QMouseEvent *e)
|
|
{
|
|
if (!m_titleBar)
|
|
return;
|
|
|
|
if (e->button() == Qt::LeftButton)
|
|
m_titleBar->onDoubleClicked();
|
|
}
|
|
|
|
QSize TitleBar::sizeHint() const
|
|
{
|
|
int height = fontMetrics().height() + 8;
|
|
return QSize(0, height);
|
|
}
|
|
|
|
void TitleBar::focusInEvent(QFocusEvent *ev)
|
|
{
|
|
if (View::d->freed())
|
|
return;
|
|
|
|
QWidget::focusInEvent(ev);
|
|
m_titleBar->focus(ev->reason());
|
|
}
|
|
|
|
#ifdef DOCKS_DEVELOPER_MODE
|
|
|
|
bool TitleBar::isCloseButtonVisible() const
|
|
{
|
|
return m_closeButton && m_closeButton->isVisible();
|
|
}
|
|
|
|
bool TitleBar::isCloseButtonEnabled() const
|
|
{
|
|
return m_closeButton && m_closeButton->isEnabled();
|
|
}
|
|
|
|
bool TitleBar::isFloatButtonVisible() const
|
|
{
|
|
return m_floatButton && m_floatButton->isVisible();
|
|
}
|
|
|
|
#endif
|