1 // This file is part of Desktop App Toolkit,
2 // a set of libraries for developing nice desktop applications.
3 //
4 // For license and copyright information please follow this link:
5 // https://github.com/desktop-app/legal/blob/master/LEGAL
6 //
7 #include "ui/widgets/inner_dropdown.h"
8
9 #include "ui/widgets/scroll_area.h"
10 #include "ui/widgets/shadow.h"
11 #include "ui/effects/panel_animation.h"
12 #include "ui/image/image_prepare.h"
13 #include "ui/ui_utility.h"
14
15 namespace Ui {
16
InnerDropdown(QWidget * parent,const style::InnerDropdown & st)17 InnerDropdown::InnerDropdown(
18 QWidget *parent,
19 const style::InnerDropdown &st)
20 : RpWidget(parent)
21 , _st(st)
22 , _roundRect(ImageRoundRadius::Small, _st.bg)
23 , _hideTimer([=] { hideAnimated(); })
24 , _scroll(this, _st.scroll) {
25 _scroll->scrolls(
__anonffaa29990202null26 ) | rpl::start_with_next([=] {
27 scrolled();
28 }, lifetime());
29
30 hide();
31
32 shownValue(
__anonffaa29990302(bool shown) 33 ) | rpl::filter([](bool shown) {
34 return shown;
35 }) | rpl::take(1) | rpl::map([=] {
36 // We can't invoke this before the window is created.
37 // So instead we start handling them on the first show().
38 return macWindowDeactivateEvents();
39 }) | rpl::flatten_latest(
__anonffaa29990502null40 ) | rpl::filter([=] {
41 return !isHidden();
42 }) | rpl::start_with_next([=] {
43 leaveEvent(nullptr);
44 }, lifetime());
45 }
46
doSetOwnedWidget(object_ptr<RpWidget> widget)47 QPointer<RpWidget> InnerDropdown::doSetOwnedWidget(
48 object_ptr<RpWidget> widget) {
49 auto result = QPointer<RpWidget>(widget);
50 widget->heightValue(
51 ) | rpl::skip(1) | rpl::start_with_next([=] {
52 resizeToContent();
53 }, widget->lifetime());
54 auto container = _scroll->setOwnedWidget(
55 object_ptr<Container>(
56 _scroll,
57 std::move(widget),
58 _st));
59 container->resizeToWidth(_scroll->width());
60 container->moveToLeft(0, 0);
61 container->show();
62 result->show();
63 return result;
64 }
65
setMaxHeight(int newMaxHeight)66 void InnerDropdown::setMaxHeight(int newMaxHeight) {
67 _maxHeight = newMaxHeight;
68 resizeToContent();
69 }
70
resizeToContent()71 void InnerDropdown::resizeToContent() {
72 auto newWidth = _st.padding.left() + _st.scrollMargin.left() + _st.scrollMargin.right() + _st.padding.right();
73 auto newHeight = _st.padding.top() + _st.scrollMargin.top() + _st.scrollMargin.bottom() + _st.padding.bottom();
74 if (auto widget = static_cast<Container*>(_scroll->widget())) {
75 widget->resizeToContent();
76 newWidth += widget->width();
77 newHeight += widget->height();
78 }
79 if (_maxHeight > 0) {
80 accumulate_min(newHeight, _maxHeight);
81 }
82 if (newWidth != width() || newHeight != height()) {
83 resize(newWidth, newHeight);
84 update();
85 finishAnimating();
86 }
87 }
88
resizeEvent(QResizeEvent * e)89 void InnerDropdown::resizeEvent(QResizeEvent *e) {
90 _scroll->setGeometry(rect().marginsRemoved(_st.padding).marginsRemoved(_st.scrollMargin));
91 if (auto widget = static_cast<TWidget*>(_scroll->widget())) {
92 widget->resizeToWidth(_scroll->width());
93 scrolled();
94 }
95 }
96
scrolled()97 void InnerDropdown::scrolled() {
98 if (auto widget = static_cast<TWidget*>(_scroll->widget())) {
99 int visibleTop = _scroll->scrollTop();
100 int visibleBottom = visibleTop + _scroll->height();
101 widget->setVisibleTopBottom(visibleTop, visibleBottom);
102 }
103 }
104
paintEvent(QPaintEvent * e)105 void InnerDropdown::paintEvent(QPaintEvent *e) {
106 QPainter p(this);
107
108 if (_a_show.animating()) {
109 if (auto opacity = _a_opacity.value(_hiding ? 0. : 1.)) {
110 // _a_opacity.current(ms)->opacityAnimationCallback()->_showAnimation.reset()
111 if (_showAnimation) {
112 _showAnimation->paintFrame(p, 0, 0, width(), _a_show.value(1.), opacity);
113 }
114 }
115 } else if (_a_opacity.animating()) {
116 p.setOpacity(_a_opacity.value(0.));
117 p.drawPixmap(0, 0, _cache);
118 } else if (_hiding || isHidden()) {
119 hideFinished();
120 } else if (_showAnimation) {
121 _showAnimation->paintFrame(p, 0, 0, width(), 1., 1.);
122 _showAnimation.reset();
123 showChildren();
124 } else {
125 if (!_cache.isNull()) _cache = QPixmap();
126 const auto inner = rect().marginsRemoved(_st.padding);
127 Shadow::paint(p, inner, width(), _st.shadow);
128 _roundRect.paint(p, inner);
129 }
130 }
131
enterEventHook(QEnterEvent * e)132 void InnerDropdown::enterEventHook(QEnterEvent *e) {
133 if (_autoHiding) {
134 showAnimated(_origin);
135 }
136 return RpWidget::enterEventHook(e);
137 }
138
leaveEventHook(QEvent * e)139 void InnerDropdown::leaveEventHook(QEvent *e) {
140 if (_autoHiding) {
141 if (_a_show.animating() || _a_opacity.animating()) {
142 hideAnimated();
143 } else {
144 _hideTimer.callOnce(300);
145 }
146 }
147 return RpWidget::leaveEventHook(e);
148 }
149
otherEnter()150 void InnerDropdown::otherEnter() {
151 if (_autoHiding) {
152 showAnimated(_origin);
153 }
154 }
155
otherLeave()156 void InnerDropdown::otherLeave() {
157 if (_autoHiding) {
158 if (_a_show.animating() || _a_opacity.animating()) {
159 hideAnimated();
160 } else {
161 _hideTimer.callOnce(0);
162 }
163 }
164 }
165
setOrigin(PanelAnimation::Origin origin)166 void InnerDropdown::setOrigin(PanelAnimation::Origin origin) {
167 _origin = origin;
168 }
169
showAnimated(PanelAnimation::Origin origin)170 void InnerDropdown::showAnimated(PanelAnimation::Origin origin) {
171 setOrigin(origin);
172 showAnimated();
173 }
174
showAnimated()175 void InnerDropdown::showAnimated() {
176 _hideTimer.cancel();
177 showStarted();
178 }
179
hideAnimated(HideOption option)180 void InnerDropdown::hideAnimated(HideOption option) {
181 if (isHidden()) return;
182 if (option == HideOption::IgnoreShow) {
183 _ignoreShowEvents = true;
184 }
185 if (_hiding) return;
186
187 _hideTimer.cancel();
188 startOpacityAnimation(true);
189 }
190
finishAnimating()191 void InnerDropdown::finishAnimating() {
192 if (_a_show.animating()) {
193 _a_show.stop();
194 showAnimationCallback();
195 }
196 if (_showAnimation) {
197 _showAnimation.reset();
198 showChildren();
199 }
200 if (_a_opacity.animating()) {
201 _a_opacity.stop();
202 opacityAnimationCallback();
203 }
204 }
205
showFast()206 void InnerDropdown::showFast() {
207 _hideTimer.cancel();
208 finishAnimating();
209 if (isHidden()) {
210 showChildren();
211 show();
212 }
213 _hiding = false;
214 }
215
hideFast()216 void InnerDropdown::hideFast() {
217 if (isHidden()) return;
218
219 _hideTimer.cancel();
220 finishAnimating();
221 _hiding = false;
222 hideFinished();
223 }
224
hideFinished()225 void InnerDropdown::hideFinished() {
226 _a_show.stop();
227 _showAnimation.reset();
228 _cache = QPixmap();
229 _ignoreShowEvents = false;
230 if (!isHidden()) {
231 const auto weak = Ui::MakeWeak(this);
232 if (const auto onstack = _hiddenCallback) {
233 onstack();
234 }
235 if (weak) {
236 hide();
237 }
238 }
239 }
240
prepareCache()241 void InnerDropdown::prepareCache() {
242 if (_a_opacity.animating()) return;
243
244 const auto animating = _a_show.animating();
245 auto showAnimation = base::take(_a_show);
246 auto showAnimationData = base::take(_showAnimation);
247 showChildren();
248 _cache = GrabWidget(this);
249 if (animating) {
250 hideChildren();
251 }
252 _showAnimation = base::take(showAnimationData);
253 _a_show = base::take(showAnimation);
254 }
255
startOpacityAnimation(bool hiding)256 void InnerDropdown::startOpacityAnimation(bool hiding) {
257 const auto weak = Ui::MakeWeak(this);
258 if (hiding) {
259 if (const auto onstack = _hideStartCallback) {
260 onstack();
261 }
262 } else if (const auto onstack = _showStartCallback) {
263 onstack();
264 }
265 if (!weak) {
266 return;
267 }
268
269 _hiding = false;
270 prepareCache();
271 _hiding = hiding;
272 hideChildren();
273 _a_opacity.start(
274 [=] { opacityAnimationCallback(); },
275 _hiding ? 1. : 0.,
276 _hiding ? 0. : 1.,
277 _st.duration);
278 }
279
showStarted()280 void InnerDropdown::showStarted() {
281 if (_ignoreShowEvents) return;
282 if (isHidden()) {
283 show();
284 startShowAnimation();
285 return;
286 } else if (!_hiding) {
287 return;
288 }
289 startOpacityAnimation(false);
290 }
291
startShowAnimation()292 void InnerDropdown::startShowAnimation() {
293 if (_showStartCallback) {
294 _showStartCallback();
295 }
296 if (!_a_show.animating()) {
297 auto opacityAnimation = base::take(_a_opacity);
298 showChildren();
299 auto cache = grabForPanelAnimation();
300 _a_opacity = base::take(opacityAnimation);
301
302 const auto pixelRatio = style::DevicePixelRatio();
303 _showAnimation = std::make_unique<PanelAnimation>(_st.animation, _origin);
304 auto inner = rect().marginsRemoved(_st.padding);
305 _showAnimation->setFinalImage(std::move(cache), QRect(inner.topLeft() * pixelRatio, inner.size() * pixelRatio));
306 _showAnimation->setCornerMasks(
307 Images::CornersMask(ImageRoundRadius::Small));
308 _showAnimation->start();
309 }
310 hideChildren();
311 _a_show.start([this] { showAnimationCallback(); }, 0., 1., _st.showDuration);
312 }
313
grabForPanelAnimation()314 QImage InnerDropdown::grabForPanelAnimation() {
315 SendPendingMoveResizeEvents(this);
316 const auto pixelRatio = style::DevicePixelRatio();
317 auto result = QImage(size() * pixelRatio, QImage::Format_ARGB32_Premultiplied);
318 result.setDevicePixelRatio(pixelRatio);
319 result.fill(Qt::transparent);
320 {
321 QPainter p(&result);
322 _roundRect.paint(p, rect().marginsRemoved(_st.padding));
323 for (const auto child : children()) {
324 if (const auto widget = qobject_cast<QWidget*>(child)) {
325 RenderWidget(p, widget, widget->pos());
326 }
327 }
328 }
329 return result;
330 }
331
opacityAnimationCallback()332 void InnerDropdown::opacityAnimationCallback() {
333 update();
334 if (!_a_opacity.animating()) {
335 if (_hiding) {
336 _hiding = false;
337 hideFinished();
338 } else if (!_a_show.animating()) {
339 showChildren();
340 }
341 }
342 }
343
showAnimationCallback()344 void InnerDropdown::showAnimationCallback() {
345 update();
346 }
347
eventFilter(QObject * obj,QEvent * e)348 bool InnerDropdown::eventFilter(QObject *obj, QEvent *e) {
349 if (e->type() == QEvent::Enter) {
350 otherEnter();
351 } else if (e->type() == QEvent::Leave) {
352 otherLeave();
353 } else if (e->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent*>(e)->button() == Qt::LeftButton) {
354 if (isHidden() || _hiding) {
355 otherEnter();
356 } else {
357 otherLeave();
358 }
359 }
360 return false;
361 }
362
resizeGetHeight(int newWidth)363 int InnerDropdown::resizeGetHeight(int newWidth) {
364 auto newHeight = _st.padding.top() + _st.scrollMargin.top() + _st.scrollMargin.bottom() + _st.padding.bottom();
365 if (auto widget = static_cast<TWidget*>(_scroll->widget())) {
366 auto containerWidth = newWidth - _st.padding.left() - _st.padding.right() - _st.scrollMargin.left() - _st.scrollMargin.right();
367 widget->resizeToWidth(containerWidth);
368 newHeight += widget->height();
369 }
370 if (_maxHeight > 0) {
371 accumulate_min(newHeight, _maxHeight);
372 }
373 return newHeight;
374 }
375
Container(QWidget * parent,object_ptr<TWidget> child,const style::InnerDropdown & st)376 InnerDropdown::Container::Container(QWidget *parent, object_ptr<TWidget> child, const style::InnerDropdown &st) : TWidget(parent)
377 , _child(std::move(child))
378 , _st(st) {
379 _child->setParent(this);
380 _child->moveToLeft(_st.scrollPadding.left(), _st.scrollPadding.top());
381 }
382
visibleTopBottomUpdated(int visibleTop,int visibleBottom)383 void InnerDropdown::Container::visibleTopBottomUpdated(
384 int visibleTop,
385 int visibleBottom) {
386 setChildVisibleTopBottom(_child, visibleTop, visibleBottom);
387 }
388
resizeToContent()389 void InnerDropdown::Container::resizeToContent() {
390 auto newWidth = _st.scrollPadding.left() + _st.scrollPadding.right();
391 auto newHeight = _st.scrollPadding.top() + _st.scrollPadding.bottom();
392 if (auto child = static_cast<TWidget*>(children().front())) {
393 newWidth += child->width();
394 newHeight += child->height();
395 }
396 if (newWidth != width() || newHeight != height()) {
397 resize(newWidth, newHeight);
398 }
399 }
400
resizeGetHeight(int newWidth)401 int InnerDropdown::Container::resizeGetHeight(int newWidth) {
402 auto innerWidth = newWidth - _st.scrollPadding.left() - _st.scrollPadding.right();
403 auto result = _st.scrollPadding.top() + _st.scrollPadding.bottom();
404 _child->resizeToWidth(innerWidth);
405 _child->moveToLeft(_st.scrollPadding.left(), _st.scrollPadding.top());
406 result += _child->height();
407 return result;
408 }
409
410 } // namespace Ui
411