Support scroll inside PopupMenu. Draw shadow over.

This commit is contained in:
John Preston 2021-09-09 13:57:48 +03:00
parent 02c1bd5674
commit 40c0a11705
3 changed files with 235 additions and 20 deletions

View file

@ -6,12 +6,15 @@
// //
#include "ui/widgets/popup_menu.h" #include "ui/widgets/popup_menu.h"
#include "ui/widgets/shadow.h"
#include "ui/image/image_prepare.h" #include "ui/image/image_prepare.h"
#include "ui/platform/ui_platform_utility.h" #include "ui/platform/ui_platform_utility.h"
#include "ui/widgets/menu//menu_item_base.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/ui_utility.h" #include "ui/ui_utility.h"
#include "ui/delayed_activation.h" #include "ui/delayed_activation.h"
#include "ui/painter.h"
#include "base/platform/base_platform_info.h" #include "base/platform/base_platform_info.h"
#include "base/qt_adapters.h" #include "base/qt_adapters.h"
@ -21,12 +24,151 @@
#include <QtWidgets/QApplication> #include <QtWidgets/QApplication>
namespace Ui { namespace Ui {
namespace {
constexpr auto kShadowCornerMultiplier = 3;
[[nodiscard]] not_null<QImage*> PrepareCachedShadow(
style::margins padding,
not_null<const style::Shadow*> shadow,
not_null<const RoundRect*> body,
rpl::lifetime &lifetime) {
const auto radius = st::roundRadiusSmall;
const auto side = radius * kShadowCornerMultiplier;
const auto middle = radius;
const auto size = side * 2 + middle;
const auto rect = QRect(0, 0, size, size);
const auto result = lifetime.make_state<QImage>(
rect.marginsAdded(padding).size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result->setDevicePixelRatio(style::DevicePixelRatio());
const auto render = [=] {
result->fill(Qt::transparent);
auto p = QPainter(result);
const auto inner = QRect(padding.left(), padding.top(), size, size);
const auto outerWidth = padding.left() + size + padding.right();
Shadow::paint(p, inner, outerWidth, *shadow);
p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
body->paint(p, inner);
};
render();
style::PaletteChanged(
) | rpl::start_with_next(render, lifetime);
return result;
}
void PaintCachedShadow(
QPainter &p,
QSize outer,
style::margins padding,
const QImage &cached) {
const auto fill = [&](
int dstx, int dsty, int dstw, int dsth,
int srcx, int srcy, int srcw, int srch) {
p.drawImage(
QRect(dstx, dsty, dstw, dsth),
cached,
QRect(
QPoint(srcx, srcy) * style::DevicePixelRatio(),
QSize(srcw, srch) * style::DevicePixelRatio()));
};
const auto paintCorner = [&](
int width, int height,
int dstx, int dsty,
int srcx, int srcy) {
fill(dstx, dsty, width, height, srcx, srcy, width, height);
};
const auto radius = st::roundRadiusSmall;
const auto side = radius * kShadowCornerMultiplier;
const auto middle = radius;
const auto size = side * 2 + middle;
paintCorner( // Top-Left
padding.left() + side,
padding.top() + side,
0,
0,
0,
0);
paintCorner( // Top-Right
side + padding.right(),
padding.top() + side,
outer.width() - side - padding.right(),
0,
padding.left() + size - side,
0);
paintCorner( // Bottom-Right
side + padding.right(),
side + padding.bottom(),
outer.width() - side - padding.right(),
outer.height() - side - padding.bottom(),
padding.left() + size - side,
padding.top() + size - side);
paintCorner( // Bottom-Left
padding.left() + side,
side + padding.bottom(),
0,
outer.height() - side - padding.bottom(),
0,
padding.top() + size - side);
const auto fillx = outer.width()
- padding.left()
- padding.right()
- 2 * side;
fill( // Top
padding.left() + side,
0,
fillx,
padding.top(),
padding.left() + side + (middle / 2),
0,
1,
padding.top());
fill( // Bottom
padding.left() + side,
outer.height() - padding.bottom(),
fillx,
padding.bottom(),
padding.left() + side + (middle / 2),
padding.top() + size,
1,
padding.bottom());
const auto filly = outer.height()
- padding.top()
- padding.bottom()
- 2 * side;
fill( // Left
0,
padding.top() + side,
padding.left(),
filly,
0,
padding.top() + side + (middle / 2),
padding.left(),
1);
fill( // Right
outer.width() - padding.right(),
padding.top() + side,
padding.right(),
filly,
padding.left() + size,
padding.top() + side + (middle / 2),
padding.right(),
1);
}
} // namespace
PopupMenu::PopupMenu(QWidget *parent, const style::PopupMenu &st) PopupMenu::PopupMenu(QWidget *parent, const style::PopupMenu &st)
: RpWidget(parent) : RpWidget(parent)
, _st(st) , _st(st)
, _roundRect(ImageRoundRadius::Small, _st.menu.itemBg) , _roundRect(ImageRoundRadius::Small, _st.menu.itemBg)
, _menu(this, _st.menu) { , _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(); init();
} }
@ -34,7 +176,12 @@ PopupMenu::PopupMenu(QWidget *parent, QMenu *menu, const style::PopupMenu &st)
: RpWidget(parent) : RpWidget(parent)
, _st(st) , _st(st)
, _roundRect(ImageRoundRadius::Small, _st.menu.itemBg) , _roundRect(ImageRoundRadius::Small, _st.menu.itemBg)
, _menu(this, menu, _st.menu) { , _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(); init();
for (const auto &action : actions()) { for (const auto &action : actions()) {
@ -55,6 +202,26 @@ void PopupMenu::init() {
hideMenu(true); hideMenu(true);
}, lifetime()); }, lifetime());
const auto paddingWrap = static_cast<PaddingWrap<Menu::Menu>*>(
_menu->parentWidget());
paddingWrap->paintRequest(
) | rpl::start_with_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());
_menu->resizesFromInner( _menu->resizesFromInner(
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
handleMenuResize(); handleMenuResize();
@ -118,15 +285,61 @@ void PopupMenu::checkSubmenuShow() {
} }
void PopupMenu::handleCompositingUpdate() { void PopupMenu::handleCompositingUpdate() {
_padding = _useTransparency ? _st.shadow.extend : style::margins(st::lineWidth, st::lineWidth, st::lineWidth, st::lineWidth); const auto line = st::lineWidth;
_menu->moveToLeft(_padding.left() + _st.scrollPadding.left(), _padding.top() + _st.scrollPadding.top()); _padding = _useTransparency
? _st.shadow.extend
: style::margins(line, line, line, line);
_scroll->moveToLeft(_padding.left(), _padding.top());
handleMenuResize(); handleMenuResize();
updateRoundingOverlay();
}
void PopupMenu::updateRoundingOverlay() {
if (!_useTransparency) {
_roundingOverlay.destroy();
return;
} else if (_roundingOverlay) {
return;
}
_roundingOverlay.create(this);
sizeValue(
) | rpl::start_with_next([=](QSize size) {
_roundingOverlay->setGeometry(QRect(QPoint(), size));
}, _roundingOverlay->lifetime());
const auto shadow = PrepareCachedShadow(
_padding,
&_st.shadow,
&_roundRect,
_roundingOverlay->lifetime());
_roundingOverlay->paintRequest(
) | rpl::start_with_next([=](QRect clip) {
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);
PaintCachedShadow(p, size(), _padding, *shadow);
}
}, _roundingOverlay->lifetime());
_roundingOverlay->setAttribute(Qt::WA_TransparentForMouseEvents);
} }
void PopupMenu::handleMenuResize() { void PopupMenu::handleMenuResize() {
auto newWidth = _padding.left() + _st.scrollPadding.left() + _menu->width() + _st.scrollPadding.right() + _padding.right(); 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(); auto newHeight = _padding.top() + _st.scrollPadding.top() + _menu->height() + _st.scrollPadding.bottom() + _padding.bottom();
resize(newWidth, newHeight); 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);
resize(newWidth, _padding.top() + scrollHeight + _padding.bottom());
_inner = rect().marginsRemoved(_padding); _inner = rect().marginsRemoved(_padding);
} }
@ -184,22 +397,18 @@ void PopupMenu::paintEvent(QPaintEvent *e) {
} else if (_showAnimation) { } else if (_showAnimation) {
_showAnimation->paintFrame(p, 0, 0, width(), 1., 1.); _showAnimation->paintFrame(p, 0, 0, width(), 1., 1.);
_showAnimation.reset(); _showAnimation.reset();
showChildren(); PostponeCall(this, [=] { showChildren(); });
} else { } else {
paintBg(p); paintBg(p);
} }
} }
void PopupMenu::paintBg(QPainter &p) { void PopupMenu::paintBg(QPainter &p) {
if (_useTransparency) { if (!_useTransparency) {
Shadow::paint(p, _inner, width(), _st.shadow);
_roundRect.paint(p, _inner);
} else {
p.fillRect(0, 0, width() - _padding.right(), _padding.top(), _st.shadow.fallback); p.fillRect(0, 0, width() - _padding.right(), _padding.top(), _st.shadow.fallback);
p.fillRect(width() - _padding.right(), 0, _padding.right(), height() - _padding.bottom(), _st.shadow.fallback); p.fillRect(width() - _padding.right(), 0, _padding.right(), height() - _padding.bottom(), _st.shadow.fallback);
p.fillRect(_padding.left(), height() - _padding.bottom(), width() - _padding.left(), _padding.bottom(), _st.shadow.fallback); p.fillRect(_padding.left(), height() - _padding.bottom(), width() - _padding.left(), _padding.bottom(), _st.shadow.fallback);
p.fillRect(0, _padding.top(), _padding.left(), height() - _padding.top(), _st.shadow.fallback); p.fillRect(0, _padding.top(), _padding.left(), height() - _padding.top(), _st.shadow.fallback);
p.fillRect(_inner, _st.menu.itemBg);
} }
} }
@ -491,16 +700,14 @@ QImage PopupMenu::grabForPanelAnimation() {
result.fill(Qt::transparent); result.fill(Qt::transparent);
{ {
QPainter p(&result); QPainter p(&result);
if (_useTransparency) { _grabbingForPanelAnimation = true;
_roundRect.paint(p, _inner); p.fillRect(_inner, _st.menu.itemBg);
} else {
p.fillRect(_inner, _st.menu.itemBg);
}
for (const auto child : children()) { for (const auto child : children()) {
if (const auto widget = qobject_cast<QWidget*>(child)) { if (const auto widget = qobject_cast<QWidget*>(child)) {
RenderWidget(p, widget, widget->pos()); RenderWidget(p, widget, widget->pos());
} }
} }
_grabbingForPanelAnimation = false;
} }
return result; return result;
} }

View file

@ -17,6 +17,8 @@
namespace Ui { namespace Ui {
class ScrollArea;
class PopupMenu : public RpWidget { class PopupMenu : public RpWidget {
public: public:
PopupMenu(QWidget *parent, const style::PopupMenu &st = st::defaultPopupMenu); PopupMenu(QWidget *parent, const style::PopupMenu &st = st::defaultPopupMenu);
@ -52,7 +54,7 @@ public:
} }
[[nodiscard]] not_null<Menu::Menu*> menu() const { [[nodiscard]] not_null<Menu::Menu*> menu() const {
return _menu.data(); return _menu;
} }
~PopupMenu(); ~PopupMenu();
@ -113,11 +115,14 @@ private:
int actionTop, int actionTop,
TriggeredSource source); TriggeredSource source);
void showMenu(const QPoint &p, PopupMenu *parent, TriggeredSource source); void showMenu(const QPoint &p, PopupMenu *parent, TriggeredSource source);
void updateRoundingOverlay();
const style::PopupMenu &_st; const style::PopupMenu &_st;
RoundRect _roundRect; RoundRect _roundRect;
object_ptr<Menu::Menu> _menu; object_ptr<ScrollArea> _scroll;
not_null<Menu::Menu*> _menu;
object_ptr<RpWidget> _roundingOverlay = { nullptr };
base::flat_map< base::flat_map<
not_null<QAction*>, not_null<QAction*>,
@ -144,6 +149,7 @@ private:
bool _triggering = false; bool _triggering = false;
bool _deleteLater = false; bool _deleteLater = false;
bool _reactivateParent = true; bool _reactivateParent = true;
bool _grabbingForPanelAnimation = false;
Fn<void()> _destroyedCallback; Fn<void()> _destroyedCallback;

View file

@ -245,6 +245,7 @@ Menu {
PopupMenu { PopupMenu {
shadow: Shadow; shadow: Shadow;
scrollPadding: margins; scrollPadding: margins;
maxHeight: pixels;
animation: PanelAnimation; animation: PanelAnimation;
menu: Menu; menu: Menu;
@ -866,6 +867,7 @@ defaultPopupMenu: PopupMenu {
animation: defaultPanelAnimation; animation: defaultPanelAnimation;
scrollPadding: margins(0px, 8px, 0px, 8px); scrollPadding: margins(0px, 8px, 0px, 8px);
maxHeight: 0px;
menu: defaultMenu; menu: defaultMenu;