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