1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4 
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "history/history_drag_area.h"
9 
10 #include "base/event_filter.h"
11 #include "ui/boxes/confirm_box.h"
12 #include "boxes/sticker_set_box.h"
13 #include "inline_bots/inline_bot_result.h"
14 #include "inline_bots/inline_bot_layout_item.h"
15 #include "dialogs/ui/dialogs_layout.h"
16 #include "history/history_widget.h"
17 #include "storage/localstorage.h"
18 #include "lang/lang_keys.h"
19 #include "ui/widgets/shadow.h"
20 #include "ui/ui_utility.h"
21 #include "ui/cached_round_corners.h"
22 #include "mainwindow.h"
23 #include "apiwrap.h"
24 #include "mainwidget.h"
25 #include "storage/storage_media_prepare.h"
26 #include "styles/style_chat_helpers.h"
27 #include "styles/style_layers.h"
28 
29 namespace {
30 
31 constexpr auto kDragAreaEvents = {
32 	QEvent::DragEnter,
33 	QEvent::DragLeave,
34 	QEvent::Drop,
35 	QEvent::MouseButtonRelease,
36 	QEvent::Leave,
37 };
38 
InnerRect(not_null<Ui::RpWidget * > widget)39 inline auto InnerRect(not_null<Ui::RpWidget*> widget) {
40 	return QRect(
41 		st::dragPadding.left(),
42 		st::dragPadding.top(),
43 		widget->width() - st::dragPadding.left() - st::dragPadding.right(),
44 		widget->height() - st::dragPadding.top() - st::dragPadding.bottom());
45 }
46 
47 } // namespace
48 
SetupDragAreaToContainer(not_null<Ui::RpWidget * > container,Fn<bool (not_null<const QMimeData * >)> && dragEnterFilter,Fn<void (bool)> && setAcceptDropsField,Fn<void ()> && updateControlsGeometry,DragArea::CallbackComputeState && computeState,bool hideSubtext)49 DragArea::Areas DragArea::SetupDragAreaToContainer(
50 		not_null<Ui::RpWidget*> container,
51 		Fn<bool(not_null<const QMimeData*>)> &&dragEnterFilter,
52 		Fn<void(bool)> &&setAcceptDropsField,
53 		Fn<void()> &&updateControlsGeometry,
54 		DragArea::CallbackComputeState &&computeState,
55 		bool hideSubtext) {
56 
57 	using DragState = Storage::MimeDataState;
58 
59 	auto &lifetime = container->lifetime();
60 	container->setAcceptDrops(true);
61 
62 	const auto attachDragDocument =
63 		Ui::CreateChild<DragArea>(container.get());
64 	const auto attachDragPhoto = Ui::CreateChild<DragArea>(container.get());
65 
66 	attachDragDocument->hide();
67 	attachDragPhoto->hide();
68 
69 	attachDragDocument->raise();
70 	attachDragPhoto->raise();
71 
72 	const auto attachDragState =
73 		lifetime.make_state<DragState>(DragState::None);
74 
75 	const auto width = [=] {
76 		return container->width();
77 	};
78 	const auto height = [=] {
79 		return container->height();
80 	};
81 
82 	const auto horizontalMargins = st::dragMargin.left()
83 		+ st::dragMargin.right();
84 	const auto verticalMargins = st::dragMargin.top()
85 		+ st::dragMargin.bottom();
86 	const auto resizeToFull = [=](not_null<DragArea*> w) {
87 		w->resize(width() - horizontalMargins, height() - verticalMargins);
88 	};
89 	const auto moveToTop = [=](not_null<DragArea*> w) {
90 		w->move(st::dragMargin.left(), st::dragMargin.top());
91 	};
92 	const auto updateAttachGeometry = crl::guard(container, [=] {
93 		if (updateControlsGeometry) {
94 			updateControlsGeometry();
95 		}
96 
97 		switch (*attachDragState) {
98 		case DragState::Files:
99 			resizeToFull(attachDragDocument);
100 			moveToTop(attachDragDocument);
101 		break;
102 		case DragState::PhotoFiles:
103 			attachDragDocument->resize(
104 				width() - horizontalMargins,
105 				(height() - verticalMargins) / 2);
106 			moveToTop(attachDragDocument);
107 			attachDragPhoto->resize(
108 				attachDragDocument->width(),
109 				attachDragDocument->height());
110 			attachDragPhoto->move(
111 				st::dragMargin.left(),
112 				height()
113 					- attachDragPhoto->height()
114 					- st::dragMargin.bottom());
115 		break;
116 		case DragState::Image:
117 			resizeToFull(attachDragPhoto);
118 			moveToTop(attachDragPhoto);
119 		break;
120 		}
121 	});
122 
123 	const auto updateDragAreas = [=] {
124 		if (setAcceptDropsField) {
125 			setAcceptDropsField(*attachDragState == DragState::None);
126 		}
127 		updateAttachGeometry();
128 
129 		switch (*attachDragState) {
130 		case DragState::None:
131 			attachDragDocument->otherLeave();
132 			attachDragPhoto->otherLeave();
133 		break;
134 		case DragState::Files:
135 			attachDragDocument->setText(
136 				tr::lng_drag_files_here(tr::now),
137 				hideSubtext
138 					? QString()
139 					: tr::lng_drag_to_send_files(tr::now));
140 			attachDragDocument->otherEnter();
141 			attachDragPhoto->hideFast();
142 		break;
143 		case DragState::PhotoFiles:
144 			attachDragDocument->setText(
145 				tr::lng_drag_images_here(tr::now),
146 				hideSubtext
147 					? QString()
148 					: tr::lng_drag_to_send_no_compression(tr::now));
149 			attachDragPhoto->setText(
150 				tr::lng_drag_photos_here(tr::now),
151 				hideSubtext
152 					? QString()
153 					: tr::lng_drag_to_send_quick(tr::now));
154 			attachDragDocument->otherEnter();
155 			attachDragPhoto->otherEnter();
156 		break;
157 		case DragState::Image:
158 			attachDragPhoto->setText(
159 				tr::lng_drag_images_here(tr::now),
160 				hideSubtext
161 					? QString()
162 					: tr::lng_drag_to_send_quick(tr::now));
163 			attachDragDocument->hideFast();
164 			attachDragPhoto->otherEnter();
165 		break;
166 		};
167 	};
168 
169 	container->sizeValue(
170 	) | rpl::start_with_next(updateAttachGeometry, lifetime);
171 
172 	const auto resetDragStateIfNeeded = [=] {
173 		if (*attachDragState != DragState::None
174 			|| !attachDragPhoto->isHidden()
175 			|| !attachDragDocument->isHidden()) {
176 			*attachDragState = DragState::None;
177 			updateDragAreas();
178 		}
179 	};
180 
181 	const auto dragEnterEvent = [=](QDragEnterEvent *e) {
182 		if (dragEnterFilter && !dragEnterFilter(e->mimeData())) {
183 			return;
184 		}
185 
186 		*attachDragState = computeState
187 			? computeState(e->mimeData())
188 			: Storage::ComputeMimeDataState(e->mimeData());
189 		updateDragAreas();
190 
191 		if (*attachDragState != DragState::None) {
192 			e->setDropAction(Qt::IgnoreAction);
193 			e->accept();
194 		}
195 	};
196 
197 	const auto dragLeaveEvent = [=](QDragLeaveEvent *e) {
198 		resetDragStateIfNeeded();
199 	};
200 
201 	const auto dropEvent = [=](QDropEvent *e) {
202 		// Hide fast to avoid visual bugs in resizable boxes.
203 		attachDragDocument->hideFast();
204 		attachDragPhoto->hideFast();
205 
206 		*attachDragState = DragState::None;
207 		updateDragAreas();
208 		e->acceptProposedAction();
209 	};
210 
211 	const auto processDragEvents = [=](not_null<QEvent*> event) {
212 		switch (event->type()) {
213 		case QEvent::DragEnter:
214 			dragEnterEvent(static_cast<QDragEnterEvent*>(event.get()));
215 			return true;
216 		case QEvent::DragLeave:
217 			dragLeaveEvent(static_cast<QDragLeaveEvent*>(event.get()));
218 			return true;
219 		case QEvent::Drop:
220 			dropEvent(static_cast<QDropEvent*>(event.get()));
221 			return true;
222 		};
223 		return false;
224 	};
225 
226 	container->events(
227 	) | rpl::filter([=](not_null<QEvent*> event) {
228 		return ranges::contains(kDragAreaEvents, event->type());
229 	}) | rpl::start_with_next([=](not_null<QEvent*> event) {
230 		const auto type = event->type();
231 
232 		if (processDragEvents(event)) {
233 			return;
234 		} else if (type == QEvent::Leave
235 			|| type == QEvent::MouseButtonRelease) {
236 			resetDragStateIfNeeded();
237 		}
238 	}, lifetime);
239 
240 	const auto eventFilter = [=](not_null<QEvent*> event) {
241 		processDragEvents(event);
242 		return base::EventFilterResult::Continue;
243 	};
244 	base::install_event_filter(attachDragDocument, eventFilter);
245 	base::install_event_filter(attachDragPhoto, eventFilter);
246 
247 	updateDragAreas();
248 
249 	return {
250 		.document = attachDragDocument,
251 		.photo = attachDragPhoto,
252 	};
253 }
254 
DragArea(QWidget * parent)255 DragArea::DragArea(QWidget *parent) : Ui::RpWidget(parent) {
256 	setMouseTracking(true);
257 	setAcceptDrops(true);
258 }
259 
overlaps(const QRect & globalRect)260 bool DragArea::overlaps(const QRect &globalRect) {
261 	if (isHidden() || _a_opacity.animating()) {
262 		return false;
263 	}
264 
265 	const auto inner = InnerRect(this);
266 	const auto testRect = QRect(
267 		mapFromGlobal(globalRect.topLeft()),
268 		globalRect.size());
269 	const auto h = QMargins(st::boxRadius, 0, st::boxRadius, 0);
270 	const auto v = QMargins(0, st::boxRadius, 0, st::boxRadius);
271 	return inner.marginsRemoved(h).contains(testRect)
272 		|| inner.marginsRemoved(v).contains(testRect);
273 }
274 
275 
mouseMoveEvent(QMouseEvent * e)276 void DragArea::mouseMoveEvent(QMouseEvent *e) {
277 	if (_hiding) {
278 		return;
279 	}
280 
281 	setIn(InnerRect(this).contains(e->pos()));
282 }
283 
dragMoveEvent(QDragMoveEvent * e)284 void DragArea::dragMoveEvent(QDragMoveEvent *e) {
285 	setIn(InnerRect(this).contains(e->pos()));
286 	e->setDropAction(_in ? Qt::CopyAction : Qt::IgnoreAction);
287 	e->accept();
288 }
289 
setIn(bool in)290 void DragArea::setIn(bool in) {
291 	if (_in != in) {
292 		_in = in;
293 		_a_in.start(
294 			[=] { update(); },
295 			_in ? 0. : 1.,
296 			_in ? 1. : 0.,
297 			st::boxDuration);
298 	}
299 }
300 
setText(const QString & text,const QString & subtext)301 void DragArea::setText(const QString &text, const QString &subtext) {
302 	_text = text;
303 	_subtext = subtext;
304 	update();
305 }
306 
paintEvent(QPaintEvent * e)307 void DragArea::paintEvent(QPaintEvent *e) {
308 	Painter p(this);
309 
310 	const auto opacity = _a_opacity.value(_hiding ? 0. : 1.);
311 	if (!_a_opacity.animating() && _hiding) {
312 		return;
313 	}
314 	p.setOpacity(opacity);
315 	const auto inner = InnerRect(this);
316 
317 	if (!_cache.isNull()) {
318 		p.drawPixmapLeft(
319 			inner.x() - st::boxRoundShadow.extend.left(),
320 			inner.y() - st::boxRoundShadow.extend.top(),
321 			width(),
322 			_cache);
323 		return;
324 	}
325 
326 	Ui::Shadow::paint(p, inner, width(), st::boxRoundShadow);
327 	Ui::FillRoundRect(p, inner, st::boxBg, Ui::BoxCorners);
328 
329 	p.setPen(anim::pen(
330 		st::dragColor,
331 		st::dragDropColor,
332 		_a_in.value(_in ? 1. : 0.)));
333 
334 	p.setFont(st::dragFont);
335 	const auto rText = QRect(
336 		0,
337 		(height() - st::dragHeight) / 2,
338 		width(),
339 		st::dragFont->height);
340 	p.drawText(rText, _text, QTextOption(style::al_top));
341 
342 	p.setFont(st::dragSubfont);
343 	const auto rSubtext = QRect(
344 		0,
345 		(height() + st::dragHeight) / 2 - st::dragSubfont->height,
346 		width(),
347 		st::dragSubfont->height * 2);
348 	p.drawText(rSubtext, _subtext, QTextOption(style::al_top));
349 }
350 
dragEnterEvent(QDragEnterEvent * e)351 void DragArea::dragEnterEvent(QDragEnterEvent *e) {
352 	e->setDropAction(Qt::IgnoreAction);
353 	e->accept();
354 }
355 
dragLeaveEvent(QDragLeaveEvent * e)356 void DragArea::dragLeaveEvent(QDragLeaveEvent *e) {
357 	setIn(false);
358 }
359 
dropEvent(QDropEvent * e)360 void DragArea::dropEvent(QDropEvent *e) {
361 	if (e->isAccepted() && _droppedCallback) {
362 		_droppedCallback(e->mimeData());
363 	}
364 }
365 
otherEnter()366 void DragArea::otherEnter() {
367 	showStart();
368 }
369 
otherLeave()370 void DragArea::otherLeave() {
371 	hideStart();
372 }
373 
hideFast()374 void DragArea::hideFast() {
375 	_a_opacity.stop();
376 	hide();
377 }
378 
hideStart()379 void DragArea::hideStart() {
380 	if (_hiding || isHidden()) {
381 		return;
382 	}
383 	if (_cache.isNull()) {
384 		_cache = Ui::GrabWidget(
385 			this,
386 			InnerRect(this).marginsAdded(st::boxRoundShadow.extend));
387 	}
388 	_hiding = true;
389 	setIn(false);
390 	_a_opacity.start(
391 		[=] { opacityAnimationCallback(); },
392 		1.,
393 		0.,
394 		st::boxDuration);
395 }
396 
hideFinish()397 void DragArea::hideFinish() {
398 	hide();
399 	_in = false;
400 	_a_in.stop();
401 }
402 
showStart()403 void DragArea::showStart() {
404 	if (!_hiding && !isHidden()) {
405 		return;
406 	}
407 	_hiding = false;
408 	if (_cache.isNull()) {
409 		_cache = Ui::GrabWidget(
410 			this,
411 			InnerRect(this).marginsAdded(st::boxRoundShadow.extend));
412 	}
413 	show();
414 	_a_opacity.start(
415 		[=] { opacityAnimationCallback(); },
416 		0.,
417 		1.,
418 		st::boxDuration);
419 }
420 
opacityAnimationCallback()421 void DragArea::opacityAnimationCallback() {
422 	update();
423 	if (!_a_opacity.animating()) {
424 		_cache = QPixmap();
425 		if (_hiding) {
426 			hideFinish();
427 		}
428 	}
429 }
430