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