lib_ui/ui/widgets/popup_menu.cpp

1364 lines
36 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/widgets/popup_menu.h"
#include "base/platform/base_platform_info.h"
#include "base/invoke_queued.h"
#include "ui/image/image_prepare.h"
#include "ui/platform/ui_platform_utility.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/menu/menu_item_base.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/padding_wrap.h"
#include "ui/delayed_activation.h"
#include "ui/painter.h"
#include "ui/integration.h"
#include "ui/screen_reader_mode.h"
#include "ui/ui_utility.h"
#include <QtGui/QtEvents>
#include <QtGui/QPainter>
#include <QtGui/QScreen>
#include <QtGui/QWindow>
#include <QtWidgets/QApplication>
#include <private/qapplication_p.h>
#include <qpa/qplatformwindow_p.h>
namespace Ui {
PopupMenu::PopupMenu(QWidget *parent, const style::PopupMenu &st)
: RpWidget(parent)
, _st(st)
, _roundRect(_st.radius, _st.menu.itemBg)
, _boxShadow(_st.shadow)
, _scroll(this, st::defaultMultiSelect.scroll)
, _menu(_scroll->setOwnedWidget(
object_ptr<PaddingWrap<Menu::Menu>>(
_scroll.data(),
object_ptr<Menu::Menu>(_scroll.data(), _st.menu),
_st.scrollPadding))->entity()) {
init();
}
PopupMenu::PopupMenu(QWidget *parent, QMenu *menu, const style::PopupMenu &st)
: RpWidget(parent)
, _st(st)
, _roundRect(_st.radius, _st.menu.itemBg)
, _boxShadow(_st.shadow)
, _scroll(this, st::defaultMultiSelect.scroll)
, _menu(_scroll->setOwnedWidget(
object_ptr<PaddingWrap<Menu::Menu>>(
_scroll.data(),
object_ptr<Menu::Menu>(_scroll.data(), menu, _st.menu),
_st.scrollPadding))->entity()) {
init();
for (const auto &action : actions()) {
if (const auto submenu = action->menu()) {
_submenus.emplace(
action,
base::make_unique_q<PopupMenu>(parentWidget(), submenu, st)
).first->second->deleteOnHide(false);
}
}
}
void PopupMenu::init() {
using namespace rpl::mappers;
Integration::Instance().forcePopupMenuHideRequests(
) | rpl::on_next([=] {
hideMenu(true);
}, lifetime());
_touchBeginCounter = Integration::Instance().touchCounterNow();
installEventFilter(this);
setupMenuWidget();
setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint)
| Qt::BypassWindowManagerHint
| Qt::Popup
| Qt::NoDropShadowWindowHint);
setMouseTracking(true);
hide();
setAttribute(Qt::WA_NoSystemBackground, true);
_useTransparency = Platform::TranslucentWindowsSupported();
if (_useTransparency) {
setAttribute(Qt::WA_TranslucentBackground, true);
} else {
setAttribute(Qt::WA_TranslucentBackground, false);
setAttribute(Qt::WA_OpaquePaintEvent, true);
}
}
not_null<PopupMenu*> PopupMenu::ensureSubmenu(
not_null<QAction*> action,
const style::PopupMenu &st) {
const auto &list = actions();
const auto found = ranges::find(list, action) != end(list);
if (!found && _stashedContent) {
const auto &stashedList = _stashedContent->menu->actions();
Assert(ranges::find(stashedList, action) != end(stashedList));
} else {
Assert(found);
}
const auto j = _submenus.find(action);
if (j != end(_submenus)) {
return j->second.get();
}
const auto result = _submenus.emplace(
action,
base::make_unique_q<PopupMenu>(parentWidget(), st)
).first->second.get();
result->deleteOnHide(false);
return result;
}
void PopupMenu::removeSubmenu(not_null<QAction*> action) {
const auto menu = _submenus.take(action);
if (menu && menu->get() == _activeSubmenu) {
base::take(_activeSubmenu)->hideMenu(true);
}
}
void PopupMenu::checkSubmenuShow() {
if (_activeSubmenu) {
return;
} else if (const auto item = _menu->findSelectedAction()) {
if (item->lastTriggeredSource() == Menu::TriggeredSource::Mouse) {
if (_submenus.contains(item->action())) {
item->setClicked(Menu::TriggeredSource::Mouse);
}
}
}
}
void PopupMenu::validateCompositingSupport() {
const auto line = st::lineWidth;
const auto &additional = _additionalMenuPadding;
if (!_useTransparency) {
_padding = QMargins(
std::max(line, additional.left()),
std::max(line, additional.top()),
std::max(line, additional.right()),
std::max(line, additional.bottom()));
_margins = QMargins();
} else {
const auto ext = _boxShadow.extend();
_padding = QMargins(
std::max(ext.left(), additional.left()),
std::max(ext.top(), additional.top()),
std::max(ext.right(), additional.right()),
std::max(ext.bottom(), additional.bottom()));
_margins = _padding - (additional - _additionalMenuMargins);
}
Platform::SetWindowMargins(this, _margins);
_scroll->moveToLeft(_padding.left(), _padding.top());
handleMenuResize();
updateRoundingOverlay();
}
void PopupMenu::updateRoundingOverlay() {
if (!_useTransparency) {
_roundingOverlay.destroy();
return;
} else if (_roundingOverlay) {
return;
}
_roundingOverlay.create(this);
sizeValue(
) | rpl::on_next([=](QSize size) {
_roundingOverlay->setGeometry(QRect(QPoint(), size));
}, _roundingOverlay->lifetime());
_roundingOverlay->paintRequest(
) | rpl::on_next([=](QRect clip) {
if (_inner.isEmpty()) {
return;
}
auto p = QPainter(_roundingOverlay.data());
auto hq = PainterHighQualityEnabler(p);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
_roundRect.paint(p, _inner, RectPart::AllCorners);
if (!_grabbingForPanelAnimation) {
p.setCompositionMode(QPainter::CompositionMode_SourceOver);
_boxShadow.paint(p, _inner, _st.radius);
}
}, _roundingOverlay->lifetime());
_roundingOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
}
void PopupMenu::handleMenuResize() {
if (_switchState) {
return;
}
auto newWidth = _padding.left() + _st.scrollPadding.left() + _menu->width() + _st.scrollPadding.right() + _padding.right();
auto newHeight = _padding.top() + _st.scrollPadding.top() + _menu->height() + _st.scrollPadding.bottom() + _padding.bottom();
const auto wantedHeight = newHeight - _padding.top() - _padding.bottom();
const auto scrollHeight = _st.maxHeight
? std::min(_st.maxHeight, wantedHeight)
: wantedHeight;
_scroll->resize(
newWidth - _padding.left() - _padding.right(),
scrollHeight);
{
const auto newSize = QSize(
newWidth,
_padding.top() + scrollHeight + _padding.bottom());
setFixedSize(newSize);
resize(newSize);
}
_inner = rect().marginsRemoved(_padding);
}
not_null<QAction*> PopupMenu::addAction(
base::unique_qptr<Menu::ItemBase> widget) {
return _menu->addAction(std::move(widget));
}
not_null<QAction*> PopupMenu::addAction(
const QString &text,
Fn<void()> callback,
const style::icon *icon,
const style::icon *iconOver) {
return _menu->addAction(text, std::move(callback), icon, iconOver);
}
not_null<QAction*> PopupMenu::addAction(
const QString &text,
std::unique_ptr<PopupMenu> submenu,
const style::icon *icon,
const style::icon *iconOver) {
const auto action = _menu->addAction(
text,
std::make_unique<QMenu>(),
icon,
iconOver);
const auto saved = _submenus.emplace(
action,
base::unique_qptr<PopupMenu>(submenu.release())
).first->second.get();
// Reparent under our own parent (like ensureSubmenu and the QMenu
// constructor do), but keep the window flags: the single-argument
// QWidget::setParent() resets them, which strips the Qt::Popup type set
// in init() and demotes the submenu to a plain child widget. Such a widget
// has no windowHandle() after createWinId(), so prepareGeometryFor() can't
// show it.
saved->setParent(parentWidget(), saved->windowFlags());
saved->deleteOnHide(false);
return action;
}
not_null<QAction*> PopupMenu::addSeparator(const style::MenuSeparator *st) {
return _menu->addSeparator(st);
}
not_null<QAction*> PopupMenu::insertAction(
int position,
base::unique_qptr<Menu::ItemBase> widget) {
return _menu->insertAction(position, std::move(widget));
}
void PopupMenu::removeAction(int position) {
const auto i = _submenus.find(_menu->actions()[position]);
if (i != end(_submenus)) {
_submenus.erase(i);
}
_menu->removeAction(position);
}
void PopupMenu::clearActions() {
_submenus.clear();
return _menu->clearActions();
}
void PopupMenu::setTopShift(int topShift) {
_topShift = topShift;
}
void PopupMenu::setForceWidth(int forceWidth) {
_menu->setForceWidth(forceWidth);
if (_stashedContent) {
_stashedContent->menu->setForceWidth(forceWidth);
}
}
const std::vector<not_null<QAction*>> &PopupMenu::actions() const {
return _menu->actions();
}
bool PopupMenu::empty() const {
return _menu->empty();
}
void PopupMenu::paintEvent(QPaintEvent *e) {
QPainter p(this);
if (_a_show.animating()) {
const auto opacity = _a_opacity.value(_hiding ? 0. : 1.);
const auto progress = _a_show.value(1.);
const auto state = (opacity > 0.)
? _showAnimation->paintFrame(p, 0, 0, width(), progress, opacity)
: PanelAnimation::PaintState();
_showStateChanges.fire({
.opacity = state.opacity,
.widthProgress = state.widthProgress,
.heightProgress = state.heightProgress,
.appearingWidth = state.width,
.appearingHeight = state.height,
.appearing = true,
});
} else if (_a_opacity.animating()) {
if (_showAnimation) {
_showAnimation.reset();
_showStateChanges.fire({
.toggling = true,
});
}
p.setOpacity(_a_opacity.value(0.));
p.drawPixmap(0, 0, _cache);
} else if (_hiding || isHidden()) {
hideFinished();
} else if (_showAnimation) {
_showAnimation->paintFrame(p, 0, 0, width(), 1., 1.);
_showAnimation.reset();
_showStateChanges.fire({});
PostponeCall(this, [=] {
showChildren();
_animatePhase = AnimatePhase::Shown;
Platform::AcceptAllMouseInput(this);
});
} else {
paintBg(p);
}
}
void PopupMenu::paintBg(QPainter &p) {
if (!_useTransparency) {
p.fillRect(0, 0, width() - _padding.right(), _padding.top(), _st.shadowFallback);
p.fillRect(width() - _padding.right(), 0, _padding.right(), height() - _padding.bottom(), _st.shadowFallback);
p.fillRect(_padding.left(), height() - _padding.bottom(), width() - _padding.left(), _padding.bottom(), _st.shadowFallback);
p.fillRect(0, _padding.top(), _padding.left(), height() - _padding.top(), _st.shadowFallback);
}
}
void PopupMenu::handleActivated(const Menu::CallbackData &data) {
if (data.source == TriggeredSource::Mouse) {
if (!popupSubmenuFromAction(data)) {
if (const auto currentSubmenu = base::take(_activeSubmenu)) {
currentSubmenu->hideMenu(true);
}
}
}
}
void PopupMenu::handleTriggered(const Menu::CallbackData &data) {
if (!popupSubmenuFromAction(data)) {
_triggering = true;
if (!data.preventClose) {
hideMenu();
}
auto weak = base::make_weak(this);
data.action->trigger();
if (weak) {
_triggering = false;
if (_deleteLater) {
_deleteLater = false;
deleteLater();
}
}
}
}
bool PopupMenu::popupSubmenuFromAction(const Menu::CallbackData &data) {
if (!data.action) {
return false;
}
if (const auto i = _submenus.find(data.action); i != end(_submenus)) {
const auto submenu = i->second.get();
if (_activeSubmenu != submenu) {
popupSubmenu(data.action, submenu, data.actionTop, data.source);
}
return true;
}
return false;
}
void PopupMenu::popupSubmenu(
not_null<QAction*> action,
not_null<PopupMenu*> submenu,
int actionTop,
TriggeredSource source) {
if (auto currentSubmenu = base::take(_activeSubmenu)) {
currentSubmenu->hideMenu(true);
}
if (submenu) {
const auto padding = _useTransparency
? _boxShadow.extend()
: QMargins(st::lineWidth, 0, st::lineWidth, 0);
QPoint p(_inner.x() + (style::RightToLeft() ? padding.right() : (_inner.width() - padding.left())), _inner.y() + actionTop);
_activeSubmenu = submenu;
_activeSubmenu->menu()->clearSelection();
_activeSubmenu->setAccessibleName(action->text());
if (_activeSubmenu->prepareGeometryFor(
geometry().topLeft() + p,
this,
_menu->itemForAction(action))) {
_activeSubmenu->showPrepared(source);
_menu->setChildShownAction(action);
} else {
_activeSubmenu = nullptr;
}
}
}
void PopupMenu::forwardKeyPress(not_null<QKeyEvent*> e) {
if (!handleKeyPress(e->key())) {
_menu->handleKeyPress(e);
}
}
bool PopupMenu::handleKeyPress(int key) {
if (_activeSubmenu) {
_activeSubmenu->handleKeyPress(key);
return true;
} else if (key == Qt::Key_Escape) {
hideMenu(_parent ? true : false);
return true;
} else if (key == (style::RightToLeft() ? Qt::Key_Right : Qt::Key_Left)) {
if (_parent) {
hideMenu(true);
return true;
}
} else if (key == (style::RightToLeft() ? Qt::Key_Left : Qt::Key_Right)) {
if (const auto item = _menu->findSelectedAction()) {
if (_submenus.contains(item->action())) {
item->setClicked(Menu::TriggeredSource::Keyboard);
}
}
}
return false;
}
void PopupMenu::handleMouseMove(QPoint globalPosition) {
if (_parent) {
_parent->forwardMouseMove(globalPosition);
}
}
void PopupMenu::handleMousePress(QPoint globalPosition) {
if (_parent) {
_parent->forwardMousePress(globalPosition);
} else {
hideMenu();
}
}
void PopupMenu::handleMouseRelease(QPoint globalPosition) {
if (_parent) {
_parent->forwardMouseRelease(globalPosition);
} else {
hideMenu();
}
}
void PopupMenu::focusOutEvent(QFocusEvent *e) {
if (!InFocusChain(this)) {
hideMenu();
}
}
void PopupMenu::hideEvent(QHideEvent *e) {
if (_deleteOnHide) {
if (_triggering) {
_deleteLater = true;
} else {
deleteLater();
}
}
}
void PopupMenu::keyPressEvent(QKeyEvent *e) {
forwardKeyPress(e);
}
void PopupMenu::mouseMoveEvent(QMouseEvent *e) {
forwardMouseMove(e->globalPos());
}
void PopupMenu::mousePressEvent(QMouseEvent *e) {
// Mouse presses, synthesized from touch events,
// should be ignored, if the touch, that caused
// them, started before the menu was created.
if (e->source() != Qt::MouseEventSynthesizedBySystem
|| (Integration::Instance().touchCounterNow()
> _touchBeginCounter)) {
forwardMousePress(e->globalPos());
}
}
bool PopupMenu::eventFilter(QObject *o, QEvent *e) {
const auto type = e->type();
if (type == QEvent::TouchBegin
|| type == QEvent::TouchUpdate
|| type == QEvent::TouchEnd) {
if (o == windowHandle() && isActiveWindow()) {
const auto event = static_cast<QTouchEvent*>(e);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
e->setAccepted(
QApplicationPrivate::translateRawTouchEvent(
this,
event->device(),
event->touchPoints(),
event->timestamp()));
#elif QT_VERSION < QT_VERSION_CHECK(6, 2, 0) // Qt < 6.0.0
e->setAccepted(
QApplicationPrivate::translateRawTouchEvent(
this,
event->pointingDevice(),
const_cast<QList<QEventPoint> &>(event->points()),
event->timestamp()));
#else // Qt < 6.2.0
e->setAccepted(
QApplicationPrivate::translateRawTouchEvent(this, event));
#endif
return e->isAccepted();
}
}
return false;
}
void PopupMenu::hideMenu(bool fast) {
if (isHidden() || (_hiding && !fast)) {
return;
}
if (_parent && !_a_opacity.animating()) {
_parent->childHiding(this);
}
if (fast) {
hideFast();
} else {
hideAnimated();
if (_parent) {
_parent->hideMenu();
}
}
if (_activeSubmenu) {
_activeSubmenu->hideMenu(fast);
}
}
void PopupMenu::childHiding(PopupMenu *child) {
if (_activeSubmenu && _activeSubmenu == child) {
_activeSubmenu = nullptr;
}
if (!_activeSubmenu) {
_menu->setChildShownAction(nullptr);
}
if (!_hiding && !isHidden()) {
raise();
activateWindow();
}
}
void PopupMenu::setOrigin(PanelAnimation::Origin origin) {
_origin = _forcedOrigin.value_or(origin);
}
void PopupMenu::setForcedOrigin(PanelAnimation::Origin origin) {
_forcedOrigin = origin;
}
void PopupMenu::setForcedVerticalOrigin(VerticalOrigin origin) {
_forcedVerticalOrigin = origin;
}
void PopupMenu::setAdditionalMenuPadding(
QMargins padding,
QMargins margins) {
Expects(padding.left() >= margins.left()
&& padding.right() >= margins.right()
&& padding.top() >= margins.top()
&& padding.bottom() >= margins.bottom());
if (_additionalMenuPadding != padding
|| _additionalMenuMargins != margins) {
_additionalMenuPadding = padding;
_additionalMenuMargins = margins;
_roundingOverlay.destroy();
}
}
void PopupMenu::showAnimated(PanelAnimation::Origin origin) {
setOrigin(origin);
showStarted();
}
void PopupMenu::hideAnimated() {
if (isHidden() || _hiding) {
return;
}
if (!_keepingDelayedActivationPaused) {
_keepingDelayedActivationPaused = true;
KeepDelayedActivationPaused(true);
}
startOpacityAnimation(true);
}
void PopupMenu::hideFast() {
if (isHidden()) return;
_a_opacity.stop();
hideFinished();
}
void PopupMenu::hideFinished() {
_hiding = false;
_a_show.stop();
_cache = QPixmap();
_animatePhase = AnimatePhase::Hidden;
if (!isHidden()) {
hide();
}
}
void PopupMenu::prepareCache() {
if (_a_opacity.animating()) return;
auto showAnimation = base::take(_a_show);
auto showAnimationData = base::take(_showAnimation);
if (showAnimation.animating()) {
_showStateChanges.fire({});
}
showChildren();
_cache = GrabWidget(this);
_showAnimation = base::take(showAnimationData);
_a_show = base::take(showAnimation);
if (_a_show.animating()) {
fireCurrentShowState();
}
}
void PopupMenu::startOpacityAnimation(bool hiding) {
if (!_useTransparency) {
_a_opacity.stop();
_hiding = hiding;
if (_hiding) {
InvokeQueued(this, [=] {
if (_hiding) {
hideFinished();
}
});
} else {
update();
}
return;
}
_hiding = false;
prepareCache();
_hiding = hiding;
_animatePhase = hiding
? AnimatePhase::StartHide
: AnimatePhase::StartShow;
hideChildren();
_a_opacity.start(
[=] { opacityAnimationCallback(); },
_hiding ? 1. : 0.,
_hiding ? 0. : 1.,
_st.duration);
}
void PopupMenu::showStarted() {
if (isHidden()) {
show();
startShowAnimation();
return;
} else if (!_hiding) {
return;
}
startOpacityAnimation(false);
}
void PopupMenu::startShowAnimation() {
if (!_useTransparency) {
_a_show.stop();
update();
return;
}
if (!_a_show.animating()) {
auto opacityAnimation = base::take(_a_opacity);
showChildren();
auto cache = grabForPanelAnimation();
_a_opacity = base::take(opacityAnimation);
const auto pixelRatio = style::DevicePixelRatio();
_showAnimation = std::make_unique<PanelAnimation>(_st.animation, _origin);
_showAnimation->setFinalImage(std::move(cache), QRect(_inner.topLeft() * pixelRatio, _inner.size() * pixelRatio), _st.radius);
if (_useTransparency) {
_showAnimation->setCornerMasks(Images::CornersMask(_st.radius));
} else {
_showAnimation->setSkipShadow(true);
}
_showAnimation->start();
}
_animatePhase = AnimatePhase::StartShow;
hideChildren();
_a_show.start([this] { showAnimationCallback(); }, 0., 1., _st.showDuration);
fireCurrentShowState();
}
void PopupMenu::fireCurrentShowState() {
const auto state = _showAnimation->computeState(
_a_show.value(1.),
_a_opacity.value(1.));
_showStateChanges.fire({
.opacity = state.opacity,
.widthProgress = state.widthProgress,
.heightProgress = state.heightProgress,
.appearingWidth = state.width,
.appearingHeight = state.height,
.appearing = true,
});
}
void PopupMenu::opacityAnimationCallback() {
update();
if (!_a_opacity.animating()) {
if (_hiding) {
hideFinished();
} else {
showChildren();
_animatePhase = AnimatePhase::Shown;
}
}
}
void PopupMenu::showAnimationCallback() {
update();
}
QImage PopupMenu::grabForPanelAnimation() {
SendPendingMoveResizeEvents(this);
const auto pixelRatio = style::DevicePixelRatio();
auto result = QImage(size() * pixelRatio, QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(pixelRatio);
result.fill(Qt::transparent);
{
QPainter p(&result);
_grabbingForPanelAnimation = true;
p.fillRect(_inner, _st.menu.itemBg);
for (const auto child : children()) {
if (const auto widget = qobject_cast<QWidget*>(child)) {
RenderWidget(p, widget, widget->pos());
}
}
_grabbingForPanelAnimation = false;
}
return result;
}
void PopupMenu::deleteOnHide(bool del) {
_deleteOnHide = del;
}
QPoint PopupMenu::ConstrainToParentScreen(
not_null<PopupMenu*> menu,
QPoint globalPos) {
auto result = globalPos;
if (const auto parent = menu->parentWidget()) {
if (const auto parentScreen = parent->window()->screen()) {
const auto r = parentScreen->availableGeometry();
if (!r.contains(result)) {
result.setX(std::clamp(result.x(), r.left(), r.right()));
result.setY(std::clamp(result.y(), r.top(), r.bottom()));
}
}
}
return result;
}
void PopupMenu::popup(const QPoint &p) {
if (!empty() && prepareGeometryFor(p)) {
popupPrepared();
return;
}
_hiding = false;
_a_opacity.stop();
_a_show.stop();
_cache = QPixmap();
hide();
if (_deleteOnHide) {
deleteLater();
}
}
void PopupMenu::popupPrepared() {
showPrepared(TriggeredSource::Mouse);
}
PanelAnimation::Origin PopupMenu::preparedOrigin() const {
return _origin;
}
QMargins PopupMenu::preparedPadding() const {
return _padding;
}
QMargins PopupMenu::additionalMenuPadding() const {
return _additionalMenuPadding;
}
QMargins PopupMenu::additionalMenuMargins() const {
return _additionalMenuMargins;
}
QMargins PopupMenu::preparedMargins() const {
return _margins;
}
bool PopupMenu::useTransparency() const {
return _useTransparency;
}
int PopupMenu::scrollTop() const {
return _scroll->scrollTop();
}
rpl::producer<int> PopupMenu::scrollTopValue() const {
return _scroll->scrollTopValue();
}
rpl::producer<PopupMenu::ShowState> PopupMenu::showStateValue() const {
return _showStateChanges.events();
}
bool PopupMenu::prepareGeometryFor(const QPoint &p) {
return prepareGeometryFor(p, nullptr, nullptr);
}
bool PopupMenu::prepareGeometryFor(
const QPoint &p,
PopupMenu *parent,
QWidget *parentActionWidget) {
if (_clearLastSeparator) {
_menu->clearLastSeparator();
for (const auto &[action, submenu] : _submenus) {
submenu->menu()->clearLastSeparator();
}
}
_parent = parent;
const auto screen = QGuiApplication::screenAt(p);
createWinId();
windowHandle()->removeEventFilter(this);
windowHandle()->installEventFilter(this);
if (_parent) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
setScreen(_parent->screen());
#else // Qt >= 6.0.0
windowHandle()->setScreen(_parent->screen());
#endif // Qt < 6.0.0
} else if (screen) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
setScreen(screen);
#else // Qt >= 6.0.0
windowHandle()->setScreen(screen);
#endif // Qt < 6.0.0
}
validateCompositingSupport();
using Origin = PanelAnimation::Origin;
auto origin = Origin::TopLeft;
const auto forceLeft = _forcedOrigin
&& (*_forcedOrigin == Origin::TopLeft
|| *_forcedOrigin == Origin::BottomLeft);
const auto forceTop = (_forcedVerticalOrigin
&& (*_forcedVerticalOrigin == VerticalOrigin::Top))
|| (_forcedOrigin
&& (*_forcedOrigin == Origin::TopLeft
|| *_forcedOrigin == Origin::TopRight));
const auto forceRight = _forcedOrigin
&& (*_forcedOrigin == Origin::TopRight
|| *_forcedOrigin == Origin::BottomRight);
const auto forceBottom = (_forcedVerticalOrigin
&& (*_forcedVerticalOrigin == VerticalOrigin::Bottom))
|| (_forcedOrigin
&& (*_forcedOrigin == Origin::BottomLeft
|| *_forcedOrigin == Origin::BottomRight));
auto w = p - QPoint(
std::max(
_additionalMenuPadding.left() - _boxShadow.extend().left(),
0),
_padding.top() - _topShift);
auto r = screen ? screen->availableGeometry() : QRect();
#if QT_VERSION >= QT_VERSION_CHECK(6, 11, 0) && defined QT_FEATURE_wayland && QT_CONFIG(wayland)
using namespace QNativeInterface::Private;
if (const auto native
= windowHandle()->nativeInterface<QWaylandWindow>()) {
const auto padding = _additionalMenuPadding - _additionalMenuMargins;
base::take(r);
if (_parent) {
// we must have an action to position the submenu around
Assert(parentActionWidget != nullptr);
native->setParentControlGeometry(
QRect(
parentActionWidget->mapTo(
parentActionWidget->window(),
QPoint()),
parentActionWidget->size())
+ _st.scrollPadding);
} else if (padding.top()) {
// provide the compositor with a range for flip_y so it uses
// the cursor point instead of the padding's top point
native->setParentControlGeometry(
QRect(
p
- parentWidget()->window()->pos()
- QPoint(padding.left(), padding.top()),
QSize(1, padding.top())));
windowHandle()->setProperty(
"_q_waylandPopupAnchor",
QVariant::fromValue(Qt::TopEdge | Qt::LeftEdge));
}
native->setExtendedWindowType(_parent
? QWaylandWindow::SubMenu
: QWaylandWindow::Menu);
}
#endif // Qt >= 6.11.0 && wayland
const auto parentWidth = _parent ? _parent->inner().width() : 0;
if (style::RightToLeft()) {
const auto badLeft = !r.isNull() && w.x() - width() < r.x() - _margins.left();
if (forceRight || (badLeft && !forceLeft)) {
if (_parent && (r.isNull() || w.x() + parentWidth - _margins.left() - _margins.right() + width() - _margins.right() <= r.x() + r.width())) {
w.setX(w.x() + parentWidth - _margins.left() - _margins.right());
} else {
w.setX(r.x() - _margins.left());
}
} else {
w.setX(w.x() - width());
}
} else {
const auto badLeft = !r.isNull() && w.x() + width() - _margins.right() > r.x() + r.width();
if (forceRight || (badLeft && !forceLeft)) {
if (_parent && (r.isNull() || w.x() - parentWidth + _margins.left() + _margins.right() - width() + _margins.right() >= r.x() - _margins.left())) {
w.setX(w.x() + _margins.left() + _margins.right() - parentWidth - width() + _margins.left() + _margins.right());
} else {
w.setX(p.x() - width() + std::max(
_additionalMenuPadding.right() - _boxShadow.extend().right(),
0));
}
origin = PanelAnimation::Origin::TopRight;
}
}
const auto badTop = !r.isNull() && w.y() + height() - _margins.bottom() > r.y() + r.height();
if (forceBottom || (badTop && !forceTop)) {
if (_parent) {
w.setY(r.y() + r.height() - height() + _margins.bottom());
} else {
w.setY(p.y() - height() + _margins.bottom());
origin = (origin == PanelAnimation::Origin::TopRight)
? PanelAnimation::Origin::BottomRight
: PanelAnimation::Origin::BottomLeft;
}
}
if (!r.isNull()) {
if (w.x() + width() - _margins.right() > r.x() + r.width()) {
w.setX(r.x() + r.width() + _margins.right() - width());
}
if (w.x() + _margins.left() < r.x()) {
w.setX(r.x() - _margins.left());
}
if (w.y() + height() - _margins.bottom() > r.y() + r.height()) {
w.setY(r.y() + r.height() + _margins.bottom() - height());
}
if (w.y() + _margins.top() < r.y()) {
w.setY(r.y() - _margins.top());
}
}
move(w);
setOrigin(origin);
return true;
}
void PopupMenu::showPrepared(TriggeredSource source) {
startShowAnimation();
if (::Platform::IsWindows()) {
ForceFullRepaintSync(this);
}
show();
Platform::ShowOverAll(this);
raise();
activateWindow();
if (Ui::ScreenReaderModeActive()) {
_menu->setShowSource(TriggeredSource::Keyboard);
} else {
_menu->setShowSource(source);
}
}
void PopupMenu::setClearLastSeparator(bool clear) {
_clearLastSeparator = clear;
}
void PopupMenu::finishSwitchAnimation() {
if (!_switchState) {
return;
}
const auto positionShift = _switchState->positionShift;
const auto baseY = _switchState->baseY;
_switchState->overlay.destroy();
_switchState.reset();
_scroll->show();
handleMenuResize();
if (positionShift) {
move(x(), baseY + positionShift);
}
}
void PopupMenu::setupMenuWidget() {
const auto paddingWrap = static_cast<PaddingWrap<Menu::Menu>*>(
_menu->parentWidget());
paddingWrap->paintRequest(
) | rpl::on_next([=](QRect clip) {
const auto top = clip.intersected(
QRect(0, 0, paddingWrap->width(), _st.scrollPadding.top()));
const auto bottom = clip.intersected(QRect(
0,
paddingWrap->height() - _st.scrollPadding.bottom(),
paddingWrap->width(),
_st.scrollPadding.bottom()));
auto p = QPainter(paddingWrap);
if (!top.isEmpty()) {
p.fillRect(top, _st.menu.itemBg);
}
if (!bottom.isEmpty()) {
p.fillRect(bottom, _st.menu.itemBg);
}
}, paddingWrap->lifetime());
rpl::combine(
_scroll->scrollTopValue(),
_scroll->heightValue(),
_menu->heightValue()
) | rpl::on_next([=](int scrollTop, int scrollHeight, int) {
const auto scrollBottom = scrollTop + scrollHeight;
paddingWrap->setVisibleTopBottom(scrollTop, scrollBottom);
}, paddingWrap->lifetime());
_menu->scrollToRequests(
) | rpl::on_next([=](ScrollToRequest request) {
_scroll->scrollTo({
request.ymin ? (_st.scrollPadding.top() + request.ymin) : 0,
(request.ymax == _menu->height()
? paddingWrap->height()
: (_st.scrollPadding.top() + request.ymax)),
});
}, _menu->lifetime());
_menu->resizesFromInner(
) | rpl::on_next([=] {
handleMenuResize();
}, _menu->lifetime());
_menu->setActivatedCallback([this](const Menu::CallbackData &data) {
handleActivated(data);
});
_menu->setTriggeredCallback([this](const Menu::CallbackData &data) {
handleTriggered(data);
});
_menu->setKeyPressDelegate([this](int key) {
return handleKeyPress(key);
});
_menu->setMouseMoveDelegate([this](QPoint globalPosition) {
handleMouseMove(globalPosition);
});
_menu->setMousePressDelegate([this](QPoint globalPosition) {
handleMousePress(globalPosition);
});
_menu->setMouseReleaseDelegate([this](QPoint globalPosition) {
handleMouseRelease(globalPosition);
});
}
void PopupMenu::swapWithStashed() {
Assert(_stashedContent != nullptr);
// Take current content out of scroll.
auto currentWrap = _scroll->takeWidget<QWidget>();
auto currentMenu = _menu;
// Restore stashed content into scroll.
// Subscriptions and delegates are still alive on the stashed widgets.
_scroll->setOwnedWidget(std::move(_stashedContent->wrap));
_menu = _stashedContent->menu;
// Stash current content.
_stashedContent->wrap = std::move(currentWrap);
_stashedContent->menu = currentMenu;
// _submenus stays shared — actions from both pages use it.
}
void PopupMenu::stashContent(Fn<void(not_null<PopupMenu*>)> fillNew) {
if (_switchState) {
_switchState->animation.stop();
_switchState->overlay.destroy();
_switchState.reset();
_scroll->show();
}
// Stash current content. _submenus stays shared.
_stashedContent = std::make_unique<StashedContent>(StashedContent{
.wrap = _scroll->takeWidget<QWidget>(),
.menu = _menu,
});
// Create new menu in scroll.
auto wrap = object_ptr<PaddingWrap<Menu::Menu>>(
_scroll.data(),
object_ptr<Menu::Menu>(_scroll.data(), _st.menu),
_st.scrollPadding);
_menu = wrap->entity();
_scroll->setOwnedWidget(std::move(wrap));
setupMenuWidget();
// Fill new page.
fillNew(this);
// Equalize widths between stashed and new page.
const auto maxWidth = std::max(
_menu->width(),
_stashedContent->menu->width());
_menu->setForceWidth(maxWidth);
_stashedContent->menu->setForceWidth(maxWidth);
handleMenuResize();
}
void PopupMenu::swapStashed(SwitchDirection direction) {
if (!_stashedContent) {
return;
}
if (_switchState && _switchState->animation.animating()) {
const auto raw = _switchState.get();
const auto progress = raw->animation.value(1.);
raw->animation.stop();
swapWithStashed();
std::swap(raw->oldSnapshot, raw->newSnapshot);
std::swap(raw->fromScrollHeight, raw->toScrollHeight);
raw->baseY = y();
raw->positionShift = computePositionShift(raw->toScrollHeight);
raw->direction = direction;
startSwitchAnimation(raw, 1. - progress);
return;
}
if (_switchState) {
_switchState->overlay.destroy();
_switchState.reset();
_scroll->show();
handleMenuResize();
}
SendPendingMoveResizeEvents(this);
const auto oldPixmap = GrabWidget(_menu);
const auto scrollWidth = _scroll->width();
const auto oldScrollHeight = _scroll->height();
// Swap widgets.
swapWithStashed();
SendPendingMoveResizeEvents(_menu);
const auto newPixmap = GrabWidget(_menu);
const auto newMenuHeight = _menu->height();
const auto wantedHeight = _st.scrollPadding.top()
+ newMenuHeight
+ _st.scrollPadding.bottom();
const auto newScrollHeight = _st.maxHeight
? std::min(_st.maxHeight, wantedHeight)
: wantedHeight;
_switchState = std::make_unique<SwitchState>();
_switchState->direction = direction;
_switchState->fromScrollHeight = oldScrollHeight;
_switchState->oldSnapshot = oldPixmap;
_switchState->newSnapshot = newPixmap;
_switchState->toScrollHeight = newScrollHeight;
_scroll->hide();
// Resize popup to max(old, new) height once upfront instead of
// calling setFixedSize/resize on every frame. Per-frame resizing of
// translucent top-level windows can cause compositor jitter on macOS.
// The visual height transition is achieved by animating _inner and
// clipping the overlay; the extra transparent area is invisible.
const auto maxScrollHeight = std::max(oldScrollHeight, newScrollHeight);
{
const auto maxSize = QSize(
_padding.left() + scrollWidth + _padding.right(),
_padding.top() + maxScrollHeight + _padding.bottom());
setFixedSize(maxSize);
resize(maxSize);
}
_switchState->baseY = y();
_switchState->positionShift = computePositionShift(newScrollHeight);
_inner = QRect(
_padding.left(),
_padding.top(),
scrollWidth,
oldScrollHeight);
const auto raw = _switchState.get();
raw->overlay.create(this);
raw->overlay->setAttribute(Qt::WA_TransparentForMouseEvents);
raw->overlay->move(_padding.left(), _padding.top());
raw->overlay->resize(scrollWidth, oldScrollHeight);
raw->overlay->show();
if (_roundingOverlay) {
_roundingOverlay->raise();
}
const auto scrollPadding = _st.scrollPadding;
raw->overlay->paintRequest(
) | rpl::on_next([=](QRect clip) {
if (!_switchState || _switchState.get() != raw) {
return;
}
if (!raw->overlay->width() || !raw->overlay->height()) {
return;
}
auto p = QPainter(raw->overlay.data());
p.fillRect(raw->overlay->rect(), _st.menu.itemBg);
const auto progress = raw->animation.value(1.);
const auto dir = (raw->direction == SwitchDirection::LeftToRight)
? 1
: -1;
const auto shift = anim::interpolate(0, scrollWidth, progress);
p.drawPixmap(
scrollPadding.left() - dir * shift,
scrollPadding.top(),
raw->oldSnapshot);
p.drawPixmap(
scrollPadding.left() + dir * (scrollWidth - shift),
scrollPadding.top(),
raw->newSnapshot);
}, raw->overlay->lifetime());
startSwitchAnimation(raw, 0.);
}
void PopupMenu::startSwitchAnimation(
not_null<SwitchState*> raw,
float64 from) {
const auto scrollWidth = raw->overlay->width();
raw->animation.start([=] {
if (!_switchState || _switchState.get() != raw) {
return;
}
const auto progress = raw->animation.value(1.);
const auto h = anim::interpolate(
raw->fromScrollHeight,
raw->toScrollHeight,
progress);
if (raw->positionShift) {
const auto shift = anim::interpolate(
0,
raw->positionShift,
progress);
move(x(), raw->baseY + shift);
}
raw->overlay->resize(scrollWidth, h);
raw->overlay->update();
_inner = QRect(
_padding.left(),
_padding.top(),
scrollWidth,
h);
update();
if (!raw->animation.animating()) {
PostponeCall(this, [=] {
if (_switchState.get() == raw) {
finishSwitchAnimation();
}
});
}
}, from, 1., _st.showDuration * (1. - from), anim::sineInOut);
}
bool PopupMenu::hasStashedContent() const {
return _stashedContent != nullptr;
}
int PopupMenu::computePositionShift(int targetScrollHeight) const {
const auto screen = QGuiApplication::screenAt(
QPoint(x() + width() / 2, y() + height() / 2));
if (!screen) {
return 0;
}
const auto r = screen->availableGeometry();
const auto targetH = _padding.top()
+ targetScrollHeight
+ _padding.bottom();
auto targetY = y();
if (targetY + targetH - _margins.bottom() > r.y() + r.height()) {
targetY = r.y() + r.height() + _margins.bottom() - targetH;
}
if (targetY + _margins.top() < r.y()) {
targetY = r.y() - _margins.top();
}
return targetY - y();
}
RpWidget *PopupMenu::accessibilityParent() const {
return qobject_cast<RpWidget*>(parentWidget());
}
PopupMenu::~PopupMenu() {
_stashedContent.reset();
for (const auto &[action, submenu] : base::take(_submenus)) {
delete submenu;
}
if (const auto parent = parentWidget()) {
const auto focused = QApplication::focusWidget();
if (_reactivateParent
&& focused != nullptr
&& Ui::InFocusChain(parent->window())) {
ActivateWindowDelayed(parent);
}
}
if (_keepingDelayedActivationPaused) {
KeepDelayedActivationPaused(false);
}
if (_destroyedCallback) {
_destroyedCallback();
}
}
} // namespace Ui