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 "info/media/info_media_list_widget.h"
9
10 #include "info/info_controller.h"
11 #include "overview/overview_layout.h"
12 #include "layout/layout_mosaic.h"
13 #include "layout/layout_selection.h"
14 #include "data/data_media_types.h"
15 #include "data/data_photo.h"
16 #include "data/data_document.h"
17 #include "data/data_session.h"
18 #include "data/data_file_click_handler.h"
19 #include "data/data_file_origin.h"
20 #include "history/history_item.h"
21 #include "history/history.h"
22 #include "history/view/history_view_cursor_state.h"
23 #include "history/view/history_view_service_message.h"
24 #include "window/window_session_controller.h"
25 #include "window/window_peer_menu.h"
26 #include "ui/widgets/popup_menu.h"
27 #include "ui/controls/delete_message_context_action.h"
28 #include "ui/chat/chat_style.h"
29 #include "ui/cached_round_corners.h"
30 #include "ui/ui_utility.h"
31 #include "ui/inactive_press.h"
32 #include "lang/lang_keys.h"
33 #include "main/main_session.h"
34 #include "mainwidget.h"
35 #include "mainwindow.h"
36 #include "styles/style_overview.h"
37 #include "styles/style_info.h"
38 #include "base/platform/base_platform_info.h"
39 #include "base/weak_ptr.h"
40 #include "media/player/media_player_instance.h"
41 #include "boxes/delete_messages_box.h"
42 #include "boxes/peer_list_controllers.h"
43 #include "core/file_utilities.h"
44 #include "facades.h"
45
46 #include <QtWidgets/QApplication>
47 #include <QtGui/QClipboard>
48
49 namespace Info {
50 namespace Media {
51 namespace {
52
53 constexpr auto kFloatingHeaderAlpha = 0.9;
54 constexpr auto kPreloadedScreensCount = 4;
55 constexpr auto kPreloadIfLessThanScreens = 2;
56 constexpr auto kPreloadedScreensCountFull
57 = kPreloadedScreensCount + 1 + kPreloadedScreensCount;
58 constexpr auto kMediaCountForSearch = 10;
59
GetUniversalId(FullMsgId itemId)60 UniversalMsgId GetUniversalId(FullMsgId itemId) {
61 return (itemId.channel != 0)
62 ? UniversalMsgId(itemId.msg)
63 : UniversalMsgId(itemId.msg - ServerMaxMsgId);
64 }
65
GetUniversalId(not_null<const HistoryItem * > item)66 UniversalMsgId GetUniversalId(not_null<const HistoryItem*> item) {
67 return GetUniversalId(item->fullId());
68 }
69
GetUniversalId(not_null<const BaseLayout * > layout)70 UniversalMsgId GetUniversalId(not_null<const BaseLayout*> layout) {
71 return GetUniversalId(layout->getItem()->fullId());
72 }
73
HasFloatingHeader(Type type)74 bool HasFloatingHeader(Type type) {
75 switch (type) {
76 case Type::Photo:
77 case Type::GIF:
78 case Type::Video:
79 case Type::RoundFile:
80 case Type::RoundVoiceFile:
81 case Type::MusicFile:
82 return false;
83 case Type::File:
84 case Type::Link:
85 return true;
86 }
87 Unexpected("Type in HasFloatingHeader()");
88 }
89
90 } // namespace
91
92 struct ListWidget::Context {
93 Overview::Layout::PaintContext layoutContext;
94 not_null<SelectedMap*> selected;
95 not_null<SelectedMap*> dragSelected;
96 DragSelectAction dragSelectAction;
97 };
98
99 struct ListWidget::DateBadge {
100 DateBadge(Type type, Fn<void()> checkCallback, Fn<void()> hideCallback);
101
102 SingleQueuedInvokation check;
103 base::Timer hideTimer;
104 Ui::Animations::Simple opacity;
105 Ui::CornersPixmaps corners;
106 bool goodType = false;
107 bool shown = false;
108 QString text;
109 int textWidth = 0;
110 QRect rect;
111 };
112
113 class ListWidget::Section {
114 public:
Section(Type type)115 Section(Type type)
116 : _type(type)
117 , _hasFloatingHeader(HasFloatingHeader(type))
118 , _mosaic(st::emojiPanWidth - st::inlineResultsLeft) {
119 }
120
121 bool addItem(not_null<BaseLayout*> item);
122 void finishSection();
123
empty() const124 bool empty() const {
125 return _items.empty();
126 }
127
minId() const128 UniversalMsgId minId() const {
129 Expects(!empty());
130
131 return _items.back().first;
132 }
maxId() const133 UniversalMsgId maxId() const {
134 Expects(!empty());
135
136 return _items.front().first;
137 }
138
setTop(int top)139 void setTop(int top) {
140 _top = top;
141 }
top() const142 int top() const {
143 return _top;
144 }
145 void resizeToWidth(int newWidth);
height() const146 int height() const {
147 return _height;
148 }
149
bottom() const150 int bottom() const {
151 return top() + height();
152 }
153
154 bool removeItem(UniversalMsgId universalId);
155 FoundItem findItemNearId(UniversalMsgId universalId) const;
156 FoundItem findItemDetails(not_null<BaseLayout*> item) const;
157 FoundItem findItemByPoint(QPoint point) const;
158
159 void paint(
160 Painter &p,
161 const Context &context,
162 QRect clip,
163 int outerWidth) const;
164
165 void paintFloatingHeader(Painter &p, int visibleTop, int outerWidth);
166
167 static int MinItemHeight(Type type, int width);
168
169 private:
170 using Items = base::flat_map<
171 UniversalMsgId,
172 not_null<BaseLayout*>,
173 std::greater<>>;
174 int headerHeight() const;
175 void appendItem(not_null<BaseLayout*> item);
176 void setHeader(not_null<BaseLayout*> item);
177 bool belongsHere(not_null<BaseLayout*> item) const;
178 Items::iterator findItemAfterTop(int top);
179 Items::const_iterator findItemAfterTop(int top) const;
180 Items::const_iterator findItemAfterBottom(
181 Items::const_iterator from,
182 int bottom) const;
183 QRect findItemRect(not_null<const BaseLayout*> item) const;
184 FoundItem completeResult(
185 not_null<BaseLayout*> item,
186 bool exact) const;
187 TextSelection itemSelection(
188 not_null<const BaseLayout*> item,
189 const Context &context) const;
190
191 int recountHeight();
192 void refreshHeight();
193
194 Type _type = Type::Photo;
195 bool _hasFloatingHeader = false;
196 Ui::Text::String _header;
197 Items _items;
198 int _itemsLeft = 0;
199 int _itemsTop = 0;
200 int _itemWidth = 0;
201 int _itemsInRow = 1;
202 mutable int _rowsCount = 0;
203 int _top = 0;
204 int _height = 0;
205
206 Mosaic::Layout::MosaicLayout<BaseLayout> _mosaic;
207
208 };
209
IsAfter(const MouseState & a,const MouseState & b)210 bool ListWidget::IsAfter(
211 const MouseState &a,
212 const MouseState &b) {
213 if (a.itemId != b.itemId) {
214 return (a.itemId < b.itemId);
215 }
216 auto xAfter = a.cursor.x() - b.cursor.x();
217 auto yAfter = a.cursor.y() - b.cursor.y();
218 return (xAfter + yAfter >= 0);
219 }
220
SkipSelectFromItem(const MouseState & state)221 bool ListWidget::SkipSelectFromItem(const MouseState &state) {
222 if (state.cursor.y() >= state.size.height()
223 || state.cursor.x() >= state.size.width()) {
224 return true;
225 }
226 return false;
227 }
228
SkipSelectTillItem(const MouseState & state)229 bool ListWidget::SkipSelectTillItem(const MouseState &state) {
230 if (state.cursor.x() < 0 || state.cursor.y() < 0) {
231 return true;
232 }
233 return false;
234 }
235
CachedItem(std::unique_ptr<BaseLayout> item)236 ListWidget::CachedItem::CachedItem(std::unique_ptr<BaseLayout> item)
237 : item(std::move(item)) {
238 }
239
240 ListWidget::CachedItem::CachedItem(CachedItem &&other) = default;
241
242 ListWidget::CachedItem &ListWidget::CachedItem::operator=(
243 CachedItem && other) = default;
244
245 ListWidget::CachedItem::~CachedItem() = default;
246
DateBadge(Type type,Fn<void ()> checkCallback,Fn<void ()> hideCallback)247 ListWidget::DateBadge::DateBadge(
248 Type type,
249 Fn<void()> checkCallback,
250 Fn<void()> hideCallback)
251 : check(std::move(checkCallback))
252 , hideTimer(std::move(hideCallback))
253 , goodType(type == Type::Photo
254 || type == Type::Video
255 || type == Type::GIF) {
256 }
257
addItem(not_null<BaseLayout * > item)258 bool ListWidget::Section::addItem(not_null<BaseLayout*> item) {
259 if (_items.empty() || belongsHere(item)) {
260 if (_items.empty()) setHeader(item);
261 appendItem(item);
262 return true;
263 }
264 return false;
265 }
266
finishSection()267 void ListWidget::Section::finishSection() {
268 if (_type == Type::GIF) {
269 _mosaic.setOffset(st::infoMediaSkip, headerHeight());
270 _mosaic.setRightSkip(st::infoMediaSkip);
271 const auto items = ranges::views::values(_items) | ranges::to_vector;
272 _mosaic.addItems(items);
273 }
274 }
275
setHeader(not_null<BaseLayout * > item)276 void ListWidget::Section::setHeader(not_null<BaseLayout*> item) {
277 auto text = [&] {
278 auto date = item->dateTime().date();
279 switch (_type) {
280 case Type::Photo:
281 case Type::GIF:
282 case Type::Video:
283 case Type::RoundFile:
284 case Type::RoundVoiceFile:
285 case Type::File:
286 return langMonthFull(date);
287
288 case Type::Link:
289 return langDayOfMonthFull(date);
290
291 case Type::MusicFile:
292 return QString();
293 }
294 Unexpected("Type in ListWidget::Section::setHeader()");
295 }();
296 _header.setText(st::infoMediaHeaderStyle, text);
297 }
298
belongsHere(not_null<BaseLayout * > item) const299 bool ListWidget::Section::belongsHere(
300 not_null<BaseLayout*> item) const {
301 Expects(!_items.empty());
302
303 auto date = item->dateTime().date();
304 auto myDate = _items.back().second->dateTime().date();
305
306 switch (_type) {
307 case Type::Photo:
308 case Type::GIF:
309 case Type::Video:
310 case Type::RoundFile:
311 case Type::RoundVoiceFile:
312 case Type::File:
313 return date.year() == myDate.year()
314 && date.month() == myDate.month();
315
316 case Type::Link:
317 return date.year() == myDate.year()
318 && date.month() == myDate.month()
319 && date.day() == myDate.day();
320
321 case Type::MusicFile:
322 return true;
323 }
324 Unexpected("Type in ListWidget::Section::belongsHere()");
325 }
326
appendItem(not_null<BaseLayout * > item)327 void ListWidget::Section::appendItem(not_null<BaseLayout*> item) {
328 _items.emplace(GetUniversalId(item), item);
329 }
330
removeItem(UniversalMsgId universalId)331 bool ListWidget::Section::removeItem(UniversalMsgId universalId) {
332 if (auto it = _items.find(universalId); it != _items.end()) {
333 it = _items.erase(it);
334 refreshHeight();
335 return true;
336 }
337 return false;
338 }
339
findItemRect(not_null<const BaseLayout * > item) const340 QRect ListWidget::Section::findItemRect(
341 not_null<const BaseLayout*> item) const {
342 auto position = item->position();
343 if (!_mosaic.empty()) {
344 return _mosaic.findRect(position);
345 }
346 auto top = position / _itemsInRow;
347 auto indexInRow = position % _itemsInRow;
348 auto left = _itemsLeft
349 + indexInRow * (_itemWidth + st::infoMediaSkip);
350 return QRect(left, top, _itemWidth, item->height());
351 }
352
completeResult(not_null<BaseLayout * > item,bool exact) const353 auto ListWidget::Section::completeResult(
354 not_null<BaseLayout*> item,
355 bool exact) const -> FoundItem {
356 return { item, findItemRect(item), exact };
357 }
358
findItemByPoint(QPoint point) const359 auto ListWidget::Section::findItemByPoint(
360 QPoint point) const -> FoundItem {
361 Expects(!_items.empty());
362 if (!_mosaic.empty()) {
363 const auto found = _mosaic.findByPoint(point);
364 Assert(found.index != -1);
365 const auto item = _mosaic.itemAt(found.index);
366 const auto rect = findItemRect(item);
367 return { item, rect, found.exact };
368 }
369 auto itemIt = findItemAfterTop(point.y());
370 if (itemIt == _items.end()) {
371 --itemIt;
372 }
373 auto item = itemIt->second;
374 auto rect = findItemRect(item);
375 if (point.y() >= rect.top()) {
376 auto shift = floorclamp(
377 point.x(),
378 (_itemWidth + st::infoMediaSkip),
379 0,
380 _itemsInRow);
381 while (shift-- && itemIt != _items.end()) {
382 ++itemIt;
383 }
384 if (itemIt == _items.end()) {
385 --itemIt;
386 }
387 item = itemIt->second;
388 rect = findItemRect(item);
389 }
390 return { item, rect, rect.contains(point) };
391 }
392
findItemNearId(UniversalMsgId universalId) const393 auto ListWidget::Section::findItemNearId(UniversalMsgId universalId) const
394 -> FoundItem {
395 Expects(!_items.empty());
396
397 auto itemIt = ranges::lower_bound(
398 _items,
399 universalId,
400 std::greater<>(),
401 [](const auto &item) -> UniversalMsgId { return item.first; });
402 if (itemIt == _items.end()) {
403 --itemIt;
404 }
405 auto item = itemIt->second;
406 auto exact = (GetUniversalId(item) == universalId);
407 return { item, findItemRect(item), exact };
408 }
409
findItemDetails(not_null<BaseLayout * > item) const410 auto ListWidget::Section::findItemDetails(not_null<BaseLayout*> item) const
411 -> FoundItem {
412 return { item, findItemRect(item), true };
413 }
414
findItemAfterTop(int top)415 auto ListWidget::Section::findItemAfterTop(
416 int top) -> Items::iterator {
417 Expects(_mosaic.empty());
418 return ranges::lower_bound(
419 _items,
420 top,
421 std::less_equal<>(),
422 [this](const auto &item) {
423 auto itemTop = item.second->position() / _itemsInRow;
424 return itemTop + item.second->height();
425 });
426 }
427
findItemAfterTop(int top) const428 auto ListWidget::Section::findItemAfterTop(
429 int top) const -> Items::const_iterator {
430 Expects(_mosaic.empty());
431 return ranges::lower_bound(
432 _items,
433 top,
434 std::less_equal<>(),
435 [this](const auto &item) {
436 auto itemTop = item.second->position() / _itemsInRow;
437 return itemTop + item.second->height();
438 });
439 }
440
findItemAfterBottom(Items::const_iterator from,int bottom) const441 auto ListWidget::Section::findItemAfterBottom(
442 Items::const_iterator from,
443 int bottom) const -> Items::const_iterator {
444 Expects(_mosaic.empty());
445 return ranges::lower_bound(
446 from,
447 _items.end(),
448 bottom,
449 std::less<>(),
450 [this](const auto &item) {
451 auto itemTop = item.second->position() / _itemsInRow;
452 return itemTop;
453 });
454 }
455
paint(Painter & p,const Context & context,QRect clip,int outerWidth) const456 void ListWidget::Section::paint(
457 Painter &p,
458 const Context &context,
459 QRect clip,
460 int outerWidth) const {
461 auto header = headerHeight();
462 if (QRect(0, 0, outerWidth, header).intersects(clip)) {
463 p.setPen(st::infoMediaHeaderFg);
464 _header.drawLeftElided(
465 p,
466 st::infoMediaHeaderPosition.x(),
467 st::infoMediaHeaderPosition.y(),
468 outerWidth - 2 * st::infoMediaHeaderPosition.x(),
469 outerWidth);
470 }
471 auto localContext = context.layoutContext;
472 localContext.isAfterDate = (header > 0);
473
474 if (!_mosaic.empty()) {
475 auto paintItem = [&](not_null<BaseLayout*> item, QPoint point) {
476 p.translate(point.x(), point.y());
477 item->paint(
478 p,
479 clip.translated(-point),
480 itemSelection(item, context),
481 &localContext);
482 p.translate(-point.x(), -point.y());
483 };
484 _mosaic.paint(std::move(paintItem), clip);
485 return;
486 }
487
488 auto fromIt = findItemAfterTop(clip.y());
489 auto tillIt = findItemAfterBottom(
490 fromIt,
491 clip.y() + clip.height());
492 for (auto it = fromIt; it != tillIt; ++it) {
493 auto item = it->second;
494 auto rect = findItemRect(item);
495 localContext.isAfterDate = (header > 0)
496 && (rect.y() <= header + _itemsTop);
497 if (rect.intersects(clip)) {
498 p.translate(rect.topLeft());
499 item->paint(
500 p,
501 clip.translated(-rect.topLeft()),
502 itemSelection(item, context),
503 &localContext);
504 p.translate(-rect.topLeft());
505 }
506 }
507 }
508
paintFloatingHeader(Painter & p,int visibleTop,int outerWidth)509 void ListWidget::Section::paintFloatingHeader(
510 Painter &p,
511 int visibleTop,
512 int outerWidth) {
513 if (!_hasFloatingHeader) {
514 return;
515 }
516 const auto headerTop = st::infoMediaHeaderPosition.y() / 2;
517 if (visibleTop <= (_top + headerTop)) {
518 return;
519 }
520 const auto header = headerHeight();
521 const auto headerLeft = st::infoMediaHeaderPosition.x();
522 const auto floatingTop = std::min(
523 visibleTop,
524 bottom() - header + headerTop);
525 p.save();
526 p.resetTransform();
527 p.setOpacity(kFloatingHeaderAlpha);
528 p.fillRect(QRect(0, floatingTop, outerWidth, header), st::boxBg);
529 p.setOpacity(1.0);
530 p.setPen(st::infoMediaHeaderFg);
531 _header.drawLeftElided(
532 p,
533 headerLeft,
534 floatingTop + headerTop,
535 outerWidth - 2 * headerLeft,
536 outerWidth);
537 p.restore();
538 }
539
itemSelection(not_null<const BaseLayout * > item,const Context & context) const540 TextSelection ListWidget::Section::itemSelection(
541 not_null<const BaseLayout*> item,
542 const Context &context) const {
543 auto universalId = GetUniversalId(item);
544 auto dragSelectAction = context.dragSelectAction;
545 if (dragSelectAction != DragSelectAction::None) {
546 auto i = context.dragSelected->find(universalId);
547 if (i != context.dragSelected->end()) {
548 return (dragSelectAction == DragSelectAction::Selecting)
549 ? FullSelection
550 : TextSelection();
551 }
552 }
553 auto i = context.selected->find(universalId);
554 return (i == context.selected->cend())
555 ? TextSelection()
556 : i->second.text;
557 }
558
headerHeight() const559 int ListWidget::Section::headerHeight() const {
560 return _header.isEmpty() ? 0 : st::infoMediaHeaderHeight;
561 }
562
resizeToWidth(int newWidth)563 void ListWidget::Section::resizeToWidth(int newWidth) {
564 auto minWidth = st::infoMediaMinGridSize + st::infoMediaSkip * 2;
565 if (newWidth < minWidth) {
566 return;
567 }
568
569 auto resizeOneColumn = [&](int itemsLeft, int itemWidth) {
570 _itemsLeft = itemsLeft;
571 _itemsTop = 0;
572 _itemsInRow = 1;
573 _itemWidth = itemWidth;
574 for (auto &item : _items) {
575 item.second->resizeGetHeight(_itemWidth);
576 }
577 };
578 switch (_type) {
579 case Type::Photo:
580 case Type::Video:
581 case Type::RoundFile: {
582 _itemsLeft = st::infoMediaSkip;
583 _itemsTop = st::infoMediaSkip;
584 _itemsInRow = (newWidth - _itemsLeft)
585 / (st::infoMediaMinGridSize + st::infoMediaSkip);
586 _itemWidth = ((newWidth - _itemsLeft) / _itemsInRow)
587 - st::infoMediaSkip;
588 for (auto &item : _items) {
589 item.second->resizeGetHeight(_itemWidth);
590 }
591 } break;
592
593 case Type::GIF: {
594 _mosaic.setFullWidth(newWidth - st::infoMediaSkip);
595 } break;
596
597 case Type::RoundVoiceFile:
598 case Type::MusicFile:
599 resizeOneColumn(0, newWidth);
600 break;
601 case Type::File:
602 case Type::Link: {
603 auto itemsLeft = st::infoMediaHeaderPosition.x();
604 auto itemWidth = newWidth - 2 * itemsLeft;
605 resizeOneColumn(itemsLeft, itemWidth);
606 } break;
607 }
608
609 refreshHeight();
610 }
611
MinItemHeight(Type type,int width)612 int ListWidget::Section::MinItemHeight(Type type, int width) {
613 auto &songSt = st::overviewFileLayout;
614 switch (type) {
615 case Type::Photo:
616 case Type::GIF:
617 case Type::Video:
618 case Type::RoundFile: {
619 auto itemsLeft = st::infoMediaSkip;
620 auto itemsInRow = (width - itemsLeft)
621 / (st::infoMediaMinGridSize + st::infoMediaSkip);
622 return (st::infoMediaMinGridSize + st::infoMediaSkip) / itemsInRow;
623 } break;
624
625 case Type::RoundVoiceFile:
626 return songSt.songPadding.top() + songSt.songThumbSize + songSt.songPadding.bottom() + st::lineWidth;
627 case Type::File:
628 return songSt.filePadding.top() + songSt.fileThumbSize + songSt.filePadding.bottom() + st::lineWidth;
629 case Type::MusicFile:
630 return songSt.songPadding.top() + songSt.songThumbSize + songSt.songPadding.bottom();
631 case Type::Link:
632 return st::linksPhotoSize + st::linksMargin.top() + st::linksMargin.bottom() + st::linksBorder;
633 }
634 Unexpected("Type in ListWidget::Section::MinItemHeight()");
635 }
636
recountHeight()637 int ListWidget::Section::recountHeight() {
638 auto result = headerHeight();
639
640 switch (_type) {
641 case Type::Photo:
642 case Type::Video:
643 case Type::RoundFile: {
644 auto itemHeight = _itemWidth + st::infoMediaSkip;
645 auto index = 0;
646 result += _itemsTop;
647 for (auto &item : _items) {
648 item.second->setPosition(_itemsInRow * result + index);
649 if (++index == _itemsInRow) {
650 result += itemHeight;
651 index = 0;
652 }
653 }
654 if (_items.size() % _itemsInRow) {
655 _rowsCount = int(_items.size()) / _itemsInRow + 1;
656 result += itemHeight;
657 } else {
658 _rowsCount = int(_items.size()) / _itemsInRow;
659 }
660 } break;
661
662 case Type::GIF: {
663 return _mosaic.countDesiredHeight(0) + result;
664 } break;
665
666 case Type::RoundVoiceFile:
667 case Type::File:
668 case Type::MusicFile:
669 case Type::Link:
670 for (auto &item : _items) {
671 item.second->setPosition(result);
672 result += item.second->height();
673 }
674 _rowsCount = _items.size();
675 break;
676 }
677
678 return result;
679 }
680
refreshHeight()681 void ListWidget::Section::refreshHeight() {
682 _height = recountHeight();
683 }
684
ListWidget(QWidget * parent,not_null<AbstractController * > controller)685 ListWidget::ListWidget(
686 QWidget *parent,
687 not_null<AbstractController*> controller)
688 : RpWidget(parent)
689 , _controller(controller)
690 , _peer(_controller->key().peer())
691 , _migrated(_controller->migrated())
692 , _type(_controller->section().mediaType())
693 , _slice(sliceKey(_universalAroundId))
694 , _dateBadge(std::make_unique<DateBadge>(
695 _type,
696 [=] { scrollDateCheck(); },
__anon86c7e6610a02null697 [=] { scrollDateHide(); })) {
698 setMouseTracking(true);
699 start();
700 }
701
session() const702 Main::Session &ListWidget::session() const {
703 return _controller->session();
704 }
705
start()706 void ListWidget::start() {
707 _controller->setSearchEnabledByContent(false);
708 style::PaletteChanged(
709 ) | rpl::start_with_next([=] {
710 invalidatePaletteCache();
711 }, lifetime());
712
713 session().downloaderTaskFinished(
714 ) | rpl::start_with_next([=] {
715 update();
716 }, lifetime());
717
718 session().data().itemLayoutChanged(
719 ) | rpl::start_with_next([this](auto item) {
720 itemLayoutChanged(item);
721 }, lifetime());
722
723 session().data().itemRemoved(
724 ) | rpl::start_with_next([this](auto item) {
725 itemRemoved(item);
726 }, lifetime());
727
728 session().data().itemRepaintRequest(
729 ) | rpl::start_with_next([this](auto item) {
730 repaintItem(item);
731 }, lifetime());
732
733 _controller->mediaSourceQueryValue(
734 ) | rpl::start_with_next([this]{
735 restart();
736 }, lifetime());
737 }
738
scrollToRequests() const739 rpl::producer<int> ListWidget::scrollToRequests() const {
740 return _scrollToRequests.events();
741 }
742
selectedListValue() const743 rpl::producer<SelectedItems> ListWidget::selectedListValue() const {
744 return _selectedListStream.events_starting_with(
745 collectSelectedItems());
746 }
747
getCurrentSongGeometry()748 QRect ListWidget::getCurrentSongGeometry() {
749 const auto type = AudioMsgId::Type::Song;
750 const auto current = ::Media::Player::instance()->current(type);
751 const auto fullMsgId = current.contextId();
752 if (fullMsgId && isPossiblyMyId(fullMsgId)) {
753 if (const auto item = findItemById(GetUniversalId(fullMsgId))) {
754 return item->geometry;
755 }
756 }
757 return QRect(0, 0, width(), 0);
758 }
759
restart()760 void ListWidget::restart() {
761 mouseActionCancel();
762
763 _overLayout = nullptr;
764 _sections.clear();
765 _layouts.clear();
766 _heavyLayouts.clear();
767
768 _universalAroundId = kDefaultAroundId;
769 _idsLimit = kMinimalIdsLimit;
770 _slice = SparseIdsMergedSlice(sliceKey(_universalAroundId));
771
772 refreshViewer();
773 }
774
itemRemoved(not_null<const HistoryItem * > item)775 void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
776 if (!isMyItem(item)) {
777 return;
778 }
779 auto id = GetUniversalId(item);
780
781 auto needHeightRefresh = false;
782 auto sectionIt = findSectionByItem(id);
783 if (sectionIt != _sections.end()) {
784 if (sectionIt->removeItem(id)) {
785 if (sectionIt->empty()) {
786 _sections.erase(sectionIt);
787 }
788 needHeightRefresh = true;
789 }
790 }
791
792 if (isItemLayout(item, _overLayout)) {
793 _overLayout = nullptr;
794 }
795
796 if (const auto i = _layouts.find(id); i != _layouts.end()) {
797 _heavyLayouts.remove(i->second.item.get());
798 _layouts.erase(i);
799 }
800 _dragSelected.remove(id);
801
802 if (const auto i = _selected.find(id); i != _selected.cend()) {
803 removeItemSelection(i);
804 }
805
806 if (needHeightRefresh) {
807 refreshHeight();
808 }
809 mouseActionUpdate(_mousePosition);
810 }
811
computeFullId(UniversalMsgId universalId) const812 FullMsgId ListWidget::computeFullId(
813 UniversalMsgId universalId) const {
814 Expects(universalId != 0);
815
816 return (universalId > 0)
817 ? FullMsgId(peerToChannel(_peer->id), universalId)
818 : FullMsgId(NoChannel, ServerMaxMsgId + universalId);
819 }
820
collectSelectedItems() const821 auto ListWidget::collectSelectedItems() const -> SelectedItems {
822 auto convert = [&](
823 UniversalMsgId universalId,
824 const SelectionData &selection) {
825 auto result = SelectedItem(computeFullId(universalId));
826 result.canDelete = selection.canDelete;
827 result.canForward = selection.canForward;
828 return result;
829 };
830 auto transformation = [&](const auto &item) {
831 return convert(item.first, item.second);
832 };
833 auto items = SelectedItems(_type);
834 if (hasSelectedItems()) {
835 items.list.reserve(_selected.size());
836 std::transform(
837 _selected.begin(),
838 _selected.end(),
839 std::back_inserter(items.list),
840 transformation);
841 }
842 return items;
843 }
844
collectSelectedIds() const845 MessageIdsList ListWidget::collectSelectedIds() const {
846 const auto selected = collectSelectedItems();
847 return ranges::views::all(
848 selected.list
849 ) | ranges::views::transform([](const SelectedItem &item) {
850 return item.msgId;
851 }) | ranges::to_vector;
852 }
853
pushSelectedItems()854 void ListWidget::pushSelectedItems() {
855 _selectedListStream.fire(collectSelectedItems());
856 }
857
hasSelected() const858 bool ListWidget::hasSelected() const {
859 return !_selected.empty();
860 }
861
isSelectedItem(const SelectedMap::const_iterator & i) const862 bool ListWidget::isSelectedItem(
863 const SelectedMap::const_iterator &i) const {
864 return (i != _selected.end())
865 && (i->second.text == FullSelection);
866 }
867
removeItemSelection(const SelectedMap::const_iterator & i)868 void ListWidget::removeItemSelection(
869 const SelectedMap::const_iterator &i) {
870 Expects(i != _selected.cend());
871 _selected.erase(i);
872 if (_selected.empty()) {
873 update();
874 }
875 pushSelectedItems();
876 }
877
hasSelectedText() const878 bool ListWidget::hasSelectedText() const {
879 return hasSelected()
880 && !hasSelectedItems();
881 }
882
hasSelectedItems() const883 bool ListWidget::hasSelectedItems() const {
884 return isSelectedItem(_selected.cbegin());
885 }
886
itemLayoutChanged(not_null<const HistoryItem * > item)887 void ListWidget::itemLayoutChanged(
888 not_null<const HistoryItem*> item) {
889 if (isItemLayout(item, _overLayout)) {
890 mouseActionUpdate();
891 }
892 }
893
repaintItem(const HistoryItem * item)894 void ListWidget::repaintItem(const HistoryItem *item) {
895 if (item && isMyItem(item)) {
896 repaintItem(GetUniversalId(item));
897 }
898 }
899
repaintItem(UniversalMsgId universalId)900 void ListWidget::repaintItem(UniversalMsgId universalId) {
901 if (auto item = findItemById(universalId)) {
902 repaintItem(item->geometry);
903 }
904 }
905
repaintItem(const BaseLayout * item)906 void ListWidget::repaintItem(const BaseLayout *item) {
907 if (item) {
908 repaintItem(GetUniversalId(item));
909 }
910 }
911
repaintItem(not_null<const BaseLayout * > item)912 void ListWidget::repaintItem(not_null<const BaseLayout*> item) {
913 repaintItem(GetUniversalId(item));
914 }
915
repaintItem(QRect itemGeometry)916 void ListWidget::repaintItem(QRect itemGeometry) {
917 rtlupdate(itemGeometry);
918 }
919
isMyItem(not_null<const HistoryItem * > item) const920 bool ListWidget::isMyItem(not_null<const HistoryItem*> item) const {
921 auto peer = item->history()->peer;
922 return (_peer == peer) || (_migrated == peer);
923 }
924
isPossiblyMyId(FullMsgId fullId) const925 bool ListWidget::isPossiblyMyId(FullMsgId fullId) const {
926 return fullId.channel
927 ? (_peer->isChannel() && peerToChannel(_peer->id) == fullId.channel)
928 : (!_peer->isChannel() || _migrated);
929 }
930
isItemLayout(not_null<const HistoryItem * > item,BaseLayout * layout) const931 bool ListWidget::isItemLayout(
932 not_null<const HistoryItem*> item,
933 BaseLayout *layout) const {
934 return layout && (layout->getItem() == item);
935 }
936
invalidatePaletteCache()937 void ListWidget::invalidatePaletteCache() {
938 for (auto &layout : _layouts) {
939 layout.second.item->invalidateCache();
940 }
941 }
942
registerHeavyItem(not_null<const BaseLayout * > item)943 void ListWidget::registerHeavyItem(not_null<const BaseLayout*> item) {
944 if (!_heavyLayouts.contains(item)) {
945 _heavyLayouts.emplace(item);
946 _heavyLayoutsInvalidated = true;
947 }
948 }
949
unregisterHeavyItem(not_null<const BaseLayout * > item)950 void ListWidget::unregisterHeavyItem(not_null<const BaseLayout*> item) {
951 const auto i = _heavyLayouts.find(item);
952 if (i != _heavyLayouts.end()) {
953 _heavyLayouts.erase(i);
954 _heavyLayoutsInvalidated = true;
955 }
956 }
957
itemVisible(not_null<const BaseLayout * > item)958 bool ListWidget::itemVisible(not_null<const BaseLayout*> item) {
959 if (const auto &found = findItemById(GetUniversalId(item))) {
960 const auto geometry = found->geometry;
961 return (geometry.top() < _visibleBottom)
962 && (geometry.top() + geometry.height() > _visibleTop);
963 }
964 return true;
965 }
966
openPhoto(not_null<PhotoData * > photo,FullMsgId id)967 void ListWidget::openPhoto(not_null<PhotoData*> photo, FullMsgId id) {
968 _controller->parentController()->openPhoto(photo, id);
969 }
970
openDocument(not_null<DocumentData * > document,FullMsgId id,bool showInMediaView)971 void ListWidget::openDocument(
972 not_null<DocumentData*> document,
973 FullMsgId id,
974 bool showInMediaView) {
975 _controller->parentController()->openDocument(
976 document,
977 id,
978 showInMediaView);
979 }
980
sliceKey(UniversalMsgId universalId) const981 SparseIdsMergedSlice::Key ListWidget::sliceKey(
982 UniversalMsgId universalId) const {
983 using Key = SparseIdsMergedSlice::Key;
984 if (_migrated) {
985 return Key(_peer->id, _migrated->id, universalId);
986 }
987 if (universalId < 0) {
988 // Convert back to plain id for non-migrated histories.
989 universalId = universalId + ServerMaxMsgId;
990 }
991 return Key(_peer->id, 0, universalId);
992 }
993
refreshViewer()994 void ListWidget::refreshViewer() {
995 _viewerLifetime.destroy();
996 const auto idForViewer = sliceKey(_universalAroundId).universalId;
997 _controller->mediaSource(
998 idForViewer,
999 _idsLimit,
1000 _idsLimit
1001 ) | rpl::start_with_next([=](SparseIdsMergedSlice &&slice) {
1002 if (!slice.fullCount()) {
1003 // Don't display anything while full count is unknown.
1004 return;
1005 }
1006 _slice = std::move(slice);
1007 if (auto nearest = _slice.nearest(idForViewer)) {
1008 _universalAroundId = GetUniversalId(*nearest);
1009 }
1010 refreshRows();
1011 }, _viewerLifetime);
1012 }
1013
getLayout(UniversalMsgId universalId)1014 BaseLayout *ListWidget::getLayout(UniversalMsgId universalId) {
1015 auto it = _layouts.find(universalId);
1016 if (it == _layouts.end()) {
1017 if (auto layout = createLayout(universalId, _type)) {
1018 layout->initDimensions();
1019 it = _layouts.emplace(
1020 universalId,
1021 std::move(layout)).first;
1022 } else {
1023 return nullptr;
1024 }
1025 }
1026 it->second.stale = false;
1027 return it->second.item.get();
1028 }
1029
getExistingLayout(UniversalMsgId universalId) const1030 BaseLayout *ListWidget::getExistingLayout(
1031 UniversalMsgId universalId) const {
1032 auto it = _layouts.find(universalId);
1033 return (it != _layouts.end())
1034 ? it->second.item.get()
1035 : nullptr;
1036 }
1037
createLayout(UniversalMsgId universalId,Type type)1038 std::unique_ptr<BaseLayout> ListWidget::createLayout(
1039 UniversalMsgId universalId,
1040 Type type) {
1041 auto item = session().data().message(computeFullId(universalId));
1042 if (!item) {
1043 return nullptr;
1044 }
1045 auto getPhoto = [&]() -> PhotoData* {
1046 if (const auto media = item->media()) {
1047 return media->photo();
1048 }
1049 return nullptr;
1050 };
1051 auto getFile = [&]() -> DocumentData* {
1052 if (auto media = item->media()) {
1053 return media->document();
1054 }
1055 return nullptr;
1056 };
1057
1058 auto &songSt = st::overviewFileLayout;
1059 using namespace Overview::Layout;
1060 switch (type) {
1061 case Type::Photo:
1062 if (const auto photo = getPhoto()) {
1063 return std::make_unique<Photo>(this, item, photo);
1064 }
1065 return nullptr;
1066 case Type::GIF:
1067 if (const auto file = getFile()) {
1068 return std::make_unique<Gif>(this, item, file);
1069 }
1070 return nullptr;
1071 case Type::Video:
1072 if (const auto file = getFile()) {
1073 return std::make_unique<Video>(this, item, file);
1074 }
1075 return nullptr;
1076 case Type::File:
1077 if (const auto file = getFile()) {
1078 return std::make_unique<Document>(this, item, file, songSt);
1079 }
1080 return nullptr;
1081 case Type::MusicFile:
1082 if (const auto file = getFile()) {
1083 return std::make_unique<Document>(this, item, file, songSt);
1084 }
1085 return nullptr;
1086 case Type::RoundVoiceFile:
1087 if (const auto file = getFile()) {
1088 return std::make_unique<Voice>(this, item, file, songSt);
1089 }
1090 return nullptr;
1091 case Type::Link:
1092 return std::make_unique<Link>(this, item, item->media());
1093 case Type::RoundFile:
1094 return nullptr;
1095 }
1096 Unexpected("Type in ListWidget::createLayout()");
1097 }
1098
refreshRows()1099 void ListWidget::refreshRows() {
1100 saveScrollState();
1101
1102 markLayoutsStale();
1103
1104
1105 _sections.clear();
1106 auto section = Section(_type);
1107 auto count = _slice.size();
1108 for (auto i = count; i != 0;) {
1109 auto universalId = GetUniversalId(_slice[--i]);
1110 if (auto layout = getLayout(universalId)) {
1111 if (!section.addItem(layout)) {
1112 section.finishSection();
1113 _sections.push_back(std::move(section));
1114 section = Section(_type);
1115 section.addItem(layout);
1116 }
1117 }
1118 }
1119 if (!section.empty()) {
1120 section.finishSection();
1121 _sections.push_back(std::move(section));
1122 }
1123
1124 if (auto count = _slice.fullCount()) {
1125 if (*count > kMediaCountForSearch) {
1126 _controller->setSearchEnabledByContent(true);
1127 }
1128 }
1129
1130 clearStaleLayouts();
1131
1132 resizeToWidth(width());
1133 restoreScrollState();
1134 mouseActionUpdate();
1135 }
1136
markLayoutsStale()1137 void ListWidget::markLayoutsStale() {
1138 for (auto &layout : _layouts) {
1139 layout.second.stale = true;
1140 }
1141 }
1142
preventAutoHide() const1143 bool ListWidget::preventAutoHide() const {
1144 return (_contextMenu != nullptr) || (_actionBoxWeak != nullptr);
1145 }
1146
saveState(not_null<Memento * > memento)1147 void ListWidget::saveState(not_null<Memento*> memento) {
1148 if (_universalAroundId != kDefaultAroundId) {
1149 auto state = countScrollState();
1150 if (state.item) {
1151 memento->setAroundId(computeFullId(_universalAroundId));
1152 memento->setIdsLimit(_idsLimit);
1153 memento->setScrollTopItem(computeFullId(state.item));
1154 memento->setScrollTopShift(state.shift);
1155 }
1156 }
1157 }
1158
restoreState(not_null<Memento * > memento)1159 void ListWidget::restoreState(not_null<Memento*> memento) {
1160 if (auto limit = memento->idsLimit()) {
1161 auto wasAroundId = memento->aroundId();
1162 if (isPossiblyMyId(wasAroundId)) {
1163 _idsLimit = limit;
1164 _universalAroundId = GetUniversalId(wasAroundId);
1165 _scrollTopState.item = GetUniversalId(memento->scrollTopItem());
1166 _scrollTopState.shift = memento->scrollTopShift();
1167 refreshViewer();
1168 }
1169 }
1170 }
1171
resizeGetHeight(int newWidth)1172 int ListWidget::resizeGetHeight(int newWidth) {
1173 if (newWidth > 0) {
1174 for (auto §ion : _sections) {
1175 section.resizeToWidth(newWidth);
1176 }
1177 }
1178 return recountHeight();
1179 }
1180
findItemByPoint(QPoint point) const1181 auto ListWidget::findItemByPoint(QPoint point) const -> FoundItem {
1182 Expects(!_sections.empty());
1183 auto sectionIt = findSectionAfterTop(point.y());
1184 if (sectionIt == _sections.end()) {
1185 --sectionIt;
1186 }
1187 auto shift = QPoint(0, sectionIt->top());
1188 return foundItemInSection(
1189 sectionIt->findItemByPoint(point - shift),
1190 *sectionIt);
1191 }
1192
findItemById(UniversalMsgId universalId)1193 auto ListWidget::findItemById(
1194 UniversalMsgId universalId) -> std::optional<FoundItem> {
1195 auto sectionIt = findSectionByItem(universalId);
1196 if (sectionIt != _sections.end()) {
1197 auto item = sectionIt->findItemNearId(universalId);
1198 if (item.exact) {
1199 return foundItemInSection(item, *sectionIt);
1200 }
1201 }
1202 return std::nullopt;
1203 }
1204
findItemDetails(not_null<BaseLayout * > item)1205 auto ListWidget::findItemDetails(not_null<BaseLayout*> item)
1206 -> FoundItem {
1207 const auto sectionIt = findSectionByItem(GetUniversalId(item));
1208 Assert(sectionIt != _sections.end());
1209 return foundItemInSection(sectionIt->findItemDetails(item), *sectionIt);
1210 }
1211
foundItemInSection(const FoundItem & item,const Section & section) const1212 auto ListWidget::foundItemInSection(
1213 const FoundItem &item,
1214 const Section §ion) const
1215 -> FoundItem {
1216 return {
1217 item.layout,
1218 item.geometry.translated(0, section.top()),
1219 item.exact };
1220 }
1221
visibleTopBottomUpdated(int visibleTop,int visibleBottom)1222 void ListWidget::visibleTopBottomUpdated(
1223 int visibleTop,
1224 int visibleBottom) {
1225 _visibleTop = visibleTop;
1226 _visibleBottom = visibleBottom;
1227
1228 checkMoveToOtherViewer();
1229 clearHeavyItems();
1230
1231 if (_dateBadge->goodType) {
1232 updateDateBadgeFor(_visibleTop);
1233 if (!_visibleTop) {
1234 if (_dateBadge->shown) {
1235 scrollDateHide();
1236 } else {
1237 update(_dateBadge->rect);
1238 }
1239 } else {
1240 _dateBadge->check.call();
1241 }
1242 }
1243 }
1244
updateDateBadgeFor(int top)1245 void ListWidget::updateDateBadgeFor(int top) {
1246 if (_sections.empty()) {
1247 return;
1248 }
1249 const auto layout = findItemByPoint({ st::infoMediaSkip, top }).layout;
1250 const auto rectHeight = st::msgServiceMargin.top()
1251 + st::msgServicePadding.top()
1252 + st::msgServiceFont->height
1253 + st::msgServicePadding.bottom();
1254
1255 _dateBadge->text = ItemDateText(layout->getItem(), false);
1256 _dateBadge->textWidth = st::msgServiceFont->width(_dateBadge->text);
1257 _dateBadge->rect = QRect(0, top, width(), rectHeight);
1258 }
1259
scrollDateCheck()1260 void ListWidget::scrollDateCheck() {
1261 if (!_dateBadge->shown) {
1262 toggleScrollDateShown();
1263 }
1264 _dateBadge->hideTimer.callOnce(st::infoScrollDateHideTimeout);
1265 }
1266
scrollDateHide()1267 void ListWidget::scrollDateHide() {
1268 if (_dateBadge->shown) {
1269 toggleScrollDateShown();
1270 }
1271 }
1272
toggleScrollDateShown()1273 void ListWidget::toggleScrollDateShown() {
1274 _dateBadge->shown = !_dateBadge->shown;
1275 _dateBadge->opacity.start(
1276 [=] { update(_dateBadge->rect); },
1277 _dateBadge->shown ? 0. : 1.,
1278 _dateBadge->shown ? 1. : 0.,
1279 st::infoDateFadeDuration);
1280 }
1281
checkMoveToOtherViewer()1282 void ListWidget::checkMoveToOtherViewer() {
1283 auto visibleHeight = (_visibleBottom - _visibleTop);
1284 if (width() <= 0
1285 || visibleHeight <= 0
1286 || _sections.empty()
1287 || _scrollTopState.item) {
1288 return;
1289 }
1290
1291 auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop });
1292 auto bottomItem = findItemByPoint({ st::infoMediaSkip, _visibleBottom });
1293
1294 auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
1295 auto minItemHeight = Section::MinItemHeight(_type, width());
1296 auto preloadedCount = preloadedHeight / minItemHeight;
1297 auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
1298 auto preloadIdsLimit = preloadIdsLimitMin
1299 + (visibleHeight / minItemHeight);
1300
1301 auto preloadBefore = kPreloadIfLessThanScreens * visibleHeight;
1302 auto after = _slice.skippedAfter();
1303 auto preloadTop = (_visibleTop < preloadBefore);
1304 auto topLoaded = after && (*after == 0);
1305 auto before = _slice.skippedBefore();
1306 auto preloadBottom = (height() - _visibleBottom < preloadBefore);
1307 auto bottomLoaded = before && (*before == 0);
1308
1309 auto minScreenDelta = kPreloadedScreensCount
1310 - kPreloadIfLessThanScreens;
1311 auto minUniversalIdDelta = (minScreenDelta * visibleHeight)
1312 / minItemHeight;
1313 auto preloadAroundItem = [&](const FoundItem &item) {
1314 auto preloadRequired = false;
1315 auto universalId = GetUniversalId(item.layout);
1316 if (!preloadRequired) {
1317 preloadRequired = (_idsLimit < preloadIdsLimitMin);
1318 }
1319 if (!preloadRequired) {
1320 auto delta = _slice.distance(
1321 sliceKey(_universalAroundId),
1322 sliceKey(universalId));
1323 Assert(delta != std::nullopt);
1324 preloadRequired = (qAbs(*delta) >= minUniversalIdDelta);
1325 }
1326 if (preloadRequired) {
1327 _idsLimit = preloadIdsLimit;
1328 _universalAroundId = universalId;
1329 refreshViewer();
1330 }
1331 };
1332
1333 if (preloadTop && !topLoaded) {
1334 preloadAroundItem(topItem);
1335 } else if (preloadBottom && !bottomLoaded) {
1336 preloadAroundItem(bottomItem);
1337 }
1338 }
1339
clearHeavyItems()1340 void ListWidget::clearHeavyItems() {
1341 const auto visibleHeight = _visibleBottom - _visibleTop;
1342 if (!visibleHeight) {
1343 return;
1344 }
1345 _heavyLayoutsInvalidated = false;
1346 const auto above = _visibleTop - visibleHeight;
1347 const auto below = _visibleBottom + visibleHeight;
1348 for (auto i = _heavyLayouts.begin(); i != _heavyLayouts.end();) {
1349 const auto item = const_cast<BaseLayout*>(i->get());
1350 const auto rect = findItemDetails(item).geometry;
1351 if (rect.top() + rect.height() <= above || rect.top() >= below) {
1352 i = _heavyLayouts.erase(i);
1353 item->clearHeavyPart();
1354 if (_heavyLayoutsInvalidated) {
1355 break;
1356 }
1357 } else {
1358 ++i;
1359 }
1360 }
1361 if (_heavyLayoutsInvalidated) {
1362 clearHeavyItems();
1363 }
1364 }
1365
countScrollState() const1366 auto ListWidget::countScrollState() const -> ScrollTopState {
1367 if (_sections.empty()) {
1368 return { 0, 0 };
1369 }
1370 auto topItem = findItemByPoint({ st::infoMediaSkip, _visibleTop });
1371 return {
1372 GetUniversalId(topItem.layout),
1373 _visibleTop - topItem.geometry.y()
1374 };
1375 }
1376
saveScrollState()1377 void ListWidget::saveScrollState() {
1378 if (!_scrollTopState.item) {
1379 _scrollTopState = countScrollState();
1380 }
1381 }
1382
restoreScrollState()1383 void ListWidget::restoreScrollState() {
1384 if (_sections.empty() || !_scrollTopState.item) {
1385 return;
1386 }
1387 auto sectionIt = findSectionByItem(_scrollTopState.item);
1388 if (sectionIt == _sections.end()) {
1389 --sectionIt;
1390 }
1391 auto item = foundItemInSection(
1392 sectionIt->findItemNearId(_scrollTopState.item),
1393 *sectionIt);
1394 auto newVisibleTop = item.geometry.y() + _scrollTopState.shift;
1395 if (_visibleTop != newVisibleTop) {
1396 _scrollToRequests.fire_copy(newVisibleTop);
1397 }
1398 _scrollTopState = ScrollTopState();
1399 }
1400
padding() const1401 QMargins ListWidget::padding() const {
1402 return st::infoMediaMargin;
1403 }
1404
paintEvent(QPaintEvent * e)1405 void ListWidget::paintEvent(QPaintEvent *e) {
1406 Painter p(this);
1407
1408 auto outerWidth = width();
1409 auto clip = e->rect();
1410 auto ms = crl::now();
1411 auto fromSectionIt = findSectionAfterTop(clip.y());
1412 auto tillSectionIt = findSectionAfterBottom(
1413 fromSectionIt,
1414 clip.y() + clip.height());
1415 auto context = Context {
1416 Overview::Layout::PaintContext(ms, hasSelectedItems()),
1417 &_selected,
1418 &_dragSelected,
1419 _dragSelectAction
1420 };
1421 for (auto it = fromSectionIt; it != tillSectionIt; ++it) {
1422 auto top = it->top();
1423 p.translate(0, top);
1424 it->paint(p, context, clip.translated(0, -top), outerWidth);
1425 p.translate(0, -top);
1426 }
1427 if (fromSectionIt != _sections.end()) {
1428 fromSectionIt->paintFloatingHeader(p, _visibleTop, outerWidth);
1429 }
1430
1431 if (_dateBadge->goodType && clip.intersects(_dateBadge->rect)) {
1432 const auto scrollDateOpacity =
1433 _dateBadge->opacity.value(_dateBadge->shown ? 1. : 0.);
1434 if (scrollDateOpacity > 0.) {
1435 p.setOpacity(scrollDateOpacity);
1436 if (_dateBadge->corners.p[0].isNull()) {
1437 _dateBadge->corners = Ui::PrepareCornerPixmaps(
1438 Ui::HistoryServiceMsgRadius(),
1439 st::roundedBg,
1440 nullptr);
1441 }
1442 HistoryView::ServiceMessagePainter::PaintDate(
1443 p,
1444 st::roundedBg,
1445 _dateBadge->corners,
1446 st::roundedFg,
1447 _dateBadge->text,
1448 _dateBadge->textWidth,
1449 _visibleTop,
1450 outerWidth,
1451 false);
1452 }
1453 }
1454 }
1455
mousePressEvent(QMouseEvent * e)1456 void ListWidget::mousePressEvent(QMouseEvent *e) {
1457 if (_contextMenu) {
1458 e->accept();
1459 return; // ignore mouse press, that was hiding context menu
1460 }
1461 mouseActionStart(e->globalPos(), e->button());
1462 }
1463
mouseMoveEvent(QMouseEvent * e)1464 void ListWidget::mouseMoveEvent(QMouseEvent *e) {
1465 auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
1466 if (!buttonsPressed && _mouseAction != MouseAction::None) {
1467 mouseReleaseEvent(e);
1468 }
1469 mouseActionUpdate(e->globalPos());
1470 }
1471
mouseReleaseEvent(QMouseEvent * e)1472 void ListWidget::mouseReleaseEvent(QMouseEvent *e) {
1473 mouseActionFinish(e->globalPos(), e->button());
1474 if (!rect().contains(e->pos())) {
1475 leaveEvent(e);
1476 }
1477 }
1478
mouseDoubleClickEvent(QMouseEvent * e)1479 void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {
1480 mouseActionStart(e->globalPos(), e->button());
1481 trySwitchToWordSelection();
1482 }
1483
showContextMenu(QContextMenuEvent * e,ContextMenuSource source)1484 void ListWidget::showContextMenu(
1485 QContextMenuEvent *e,
1486 ContextMenuSource source) {
1487 if (_contextMenu) {
1488 _contextMenu = nullptr;
1489 repaintItem(_contextUniversalId);
1490 }
1491 if (e->reason() == QContextMenuEvent::Mouse) {
1492 mouseActionUpdate(e->globalPos());
1493 }
1494
1495 auto item = session().data().message(computeFullId(_overState.itemId));
1496 if (!item || !_overState.inside) {
1497 return;
1498 }
1499 auto universalId = _contextUniversalId = _overState.itemId;
1500
1501 enum class SelectionState {
1502 NoSelectedItems,
1503 NotOverSelectedItems,
1504 OverSelectedItems,
1505 NotOverSelectedText,
1506 OverSelectedText,
1507 };
1508 auto overSelected = SelectionState::NoSelectedItems;
1509 if (source == ContextMenuSource::Touch) {
1510 if (hasSelectedItems()) {
1511 overSelected = SelectionState::OverSelectedItems;
1512 } else if (hasSelectedText()) {
1513 overSelected = SelectionState::OverSelectedItems;
1514 }
1515 } else if (hasSelectedText()) {
1516 // #TODO text selection
1517 } else if (hasSelectedItems()) {
1518 auto it = _selected.find(_overState.itemId);
1519 if (isSelectedItem(it) && _overState.inside) {
1520 overSelected = SelectionState::OverSelectedItems;
1521 } else {
1522 overSelected = SelectionState::NotOverSelectedItems;
1523 }
1524 }
1525
1526 auto canDeleteAll = [&] {
1527 return ranges::none_of(_selected, [](auto &&item) {
1528 return !item.second.canDelete;
1529 });
1530 };
1531 auto canForwardAll = [&] {
1532 return ranges::none_of(_selected, [](auto &&item) {
1533 return !item.second.canForward;
1534 });
1535 };
1536
1537 auto link = ClickHandler::getActive();
1538
1539 const auto itemFullId = item->fullId();
1540 const auto owner = &session().data();
1541 _contextMenu = base::make_unique_q<Ui::PopupMenu>(this);
1542 _contextMenu->addAction(
1543 tr::lng_context_to_msg(tr::now),
1544 [=] {
1545 if (const auto item = owner->message(itemFullId)) {
1546 _controller->parentController()->showPeerHistoryAtItem(item);
1547 }
1548 });
1549
1550 auto photoLink = dynamic_cast<PhotoClickHandler*>(link.get());
1551 auto fileLink = dynamic_cast<DocumentClickHandler*>(link.get());
1552 if (photoLink || fileLink) {
1553 auto [isVideo, isVoice, isAudio] = [&] {
1554 if (fileLink) {
1555 auto document = fileLink->document();
1556 return std::make_tuple(
1557 document->isVideoFile(),
1558 document->isVoiceMessage(),
1559 document->isAudioFile()
1560 );
1561 }
1562 return std::make_tuple(false, false, false);
1563 }();
1564
1565 if (photoLink) {
1566 } else {
1567 if (auto document = fileLink->document()) {
1568 if (document->loading()) {
1569 _contextMenu->addAction(
1570 tr::lng_context_cancel_download(tr::now),
1571 [document] {
1572 document->cancel();
1573 });
1574 } else {
1575 auto filepath = document->filepath(true);
1576 if (!filepath.isEmpty()) {
1577 auto handler = App::LambdaDelayed(
1578 st::defaultDropdownMenu.menu.ripple.hideDuration,
1579 this,
1580 [filepath] {
1581 File::ShowInFolder(filepath);
1582 });
1583 _contextMenu->addAction(
1584 (Platform::IsMac()
1585 ? tr::lng_context_show_in_finder(tr::now)
1586 : tr::lng_context_show_in_folder(tr::now)),
1587 std::move(handler));
1588 }
1589 auto handler = App::LambdaDelayed(
1590 st::defaultDropdownMenu.menu.ripple.hideDuration,
1591 this,
1592 [=] {
1593 DocumentSaveClickHandler::Save(
1594 itemFullId,
1595 document,
1596 DocumentSaveClickHandler::Mode::ToNewFile);
1597 });
1598 _contextMenu->addAction(
1599 (isVideo
1600 ? tr::lng_context_save_video(tr::now)
1601 : isVoice
1602 ? tr::lng_context_save_audio(tr::now)
1603 : isAudio
1604 ? tr::lng_context_save_audio_file(tr::now)
1605 : tr::lng_context_save_file(tr::now)),
1606 std::move(handler));
1607 }
1608 }
1609 }
1610 } else if (link) {
1611 const auto actionText = link->copyToClipboardContextItemText();
1612 if (!actionText.isEmpty()) {
1613 _contextMenu->addAction(
1614 actionText,
1615 [text = link->copyToClipboardText()] {
1616 QGuiApplication::clipboard()->setText(text);
1617 });
1618 }
1619 }
1620 if (overSelected == SelectionState::OverSelectedItems) {
1621 if (canForwardAll()) {
1622 _contextMenu->addAction(
1623 tr::lng_context_forward_selected(tr::now),
1624 crl::guard(this, [this] {
1625 forwardSelected();
1626 }));
1627 }
1628 if (canDeleteAll()) {
1629 _contextMenu->addAction(
1630 tr::lng_context_delete_selected(tr::now),
1631 crl::guard(this, [this] {
1632 deleteSelected();
1633 }));
1634 }
1635 _contextMenu->addAction(
1636 tr::lng_context_clear_selection(tr::now),
1637 crl::guard(this, [this] {
1638 clearSelected();
1639 }));
1640 } else {
1641 if (overSelected != SelectionState::NotOverSelectedItems) {
1642 if (item->allowsForward()) {
1643 _contextMenu->addAction(
1644 tr::lng_context_forward_msg(tr::now),
1645 crl::guard(this, [this, universalId] {
1646 forwardItem(universalId);
1647 }));
1648 }
1649 if (item->canDelete()) {
1650 _contextMenu->addAction(Ui::DeleteMessageContextAction(
1651 _contextMenu->menu(),
1652 [=] { deleteItem(universalId); },
1653 item->ttlDestroyAt(),
1654 [=] { _contextMenu = nullptr; }));
1655 }
1656 }
1657 _contextMenu->addAction(
1658 tr::lng_context_select_msg(tr::now),
1659 crl::guard(this, [this, universalId] {
1660 if (hasSelectedText()) {
1661 clearSelected();
1662 } else if (_selected.size() == MaxSelectedItems) {
1663 return;
1664 } else if (_selected.empty()) {
1665 update();
1666 }
1667 applyItemSelection(universalId, FullSelection);
1668 }));
1669 }
1670
1671 _contextMenu->setDestroyedCallback(crl::guard(
1672 this,
1673 [this, universalId] {
1674 mouseActionUpdate(QCursor::pos());
1675 repaintItem(universalId);
1676 _checkForHide.fire({});
1677 }));
1678 _contextMenu->popup(e->globalPos());
1679 e->accept();
1680 }
1681
contextMenuEvent(QContextMenuEvent * e)1682 void ListWidget::contextMenuEvent(QContextMenuEvent *e) {
1683 showContextMenu(
1684 e,
1685 (e->reason() == QContextMenuEvent::Mouse)
1686 ? ContextMenuSource::Mouse
1687 : ContextMenuSource::Other);
1688 }
1689
forwardSelected()1690 void ListWidget::forwardSelected() {
1691 if (auto items = collectSelectedIds(); !items.empty()) {
1692 forwardItems(std::move(items));
1693 }
1694 }
1695
forwardItem(UniversalMsgId universalId)1696 void ListWidget::forwardItem(UniversalMsgId universalId) {
1697 if (const auto item = session().data().message(computeFullId(universalId))) {
1698 forwardItems({ 1, item->fullId() });
1699 }
1700 }
1701
forwardItems(MessageIdsList && items)1702 void ListWidget::forwardItems(MessageIdsList &&items) {
1703 auto callback = [weak = Ui::MakeWeak(this)] {
1704 if (const auto strong = weak.data()) {
1705 strong->clearSelected();
1706 }
1707 };
1708 setActionBoxWeak(Window::ShowForwardMessagesBox(
1709 _controller,
1710 std::move(items),
1711 std::move(callback)));
1712 }
1713
deleteSelected()1714 void ListWidget::deleteSelected() {
1715 if (const auto box = deleteItems(collectSelectedIds())) {
1716 const auto weak = Ui::MakeWeak(this);
1717 box->setDeleteConfirmedCallback([=]{
1718 if (const auto strong = weak.data()) {
1719 strong->clearSelected();
1720 }
1721 });
1722 }
1723 }
1724
deleteItem(UniversalMsgId universalId)1725 void ListWidget::deleteItem(UniversalMsgId universalId) {
1726 if (const auto item = session().data().message(computeFullId(universalId))) {
1727 deleteItems({ 1, item->fullId() });
1728 }
1729 }
1730
deleteItems(MessageIdsList && items)1731 DeleteMessagesBox *ListWidget::deleteItems(MessageIdsList &&items) {
1732 if (!items.empty()) {
1733 const auto box = Ui::show(
1734 Box<DeleteMessagesBox>(
1735 &_controller->session(),
1736 std::move(items))).data();
1737 setActionBoxWeak(box);
1738 return box;
1739 }
1740 return nullptr;
1741 }
1742
setActionBoxWeak(QPointer<Ui::RpWidget> box)1743 void ListWidget::setActionBoxWeak(QPointer<Ui::RpWidget> box) {
1744 if ((_actionBoxWeak = box)) {
1745 _actionBoxWeakLifetime = _actionBoxWeak->alive(
1746 ) | rpl::start_with_done([weak = Ui::MakeWeak(this)]{
1747 if (weak) {
1748 weak->_checkForHide.fire({});
1749 }
1750 });
1751 }
1752 }
1753
trySwitchToWordSelection()1754 void ListWidget::trySwitchToWordSelection() {
1755 auto selectingSome = (_mouseAction == MouseAction::Selecting)
1756 && hasSelectedText();
1757 auto willSelectSome = (_mouseAction == MouseAction::None)
1758 && !hasSelectedItems();
1759 auto checkSwitchToWordSelection = _overLayout
1760 && (_mouseSelectType == TextSelectType::Letters)
1761 && (selectingSome || willSelectSome);
1762 if (checkSwitchToWordSelection) {
1763 switchToWordSelection();
1764 }
1765 }
1766
switchToWordSelection()1767 void ListWidget::switchToWordSelection() {
1768 Expects(_overLayout != nullptr);
1769
1770 StateRequest request;
1771 request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
1772 auto dragState = _overLayout->getState(_pressState.cursor, request);
1773 if (dragState.cursor != CursorState::Text) {
1774 return;
1775 }
1776 _mouseTextSymbol = dragState.symbol;
1777 _mouseSelectType = TextSelectType::Words;
1778 if (_mouseAction == MouseAction::None) {
1779 _mouseAction = MouseAction::Selecting;
1780 clearSelected();
1781 auto selStatus = TextSelection {
1782 dragState.symbol,
1783 dragState.symbol
1784 };
1785 applyItemSelection(_overState.itemId, selStatus);
1786 }
1787 mouseActionUpdate();
1788
1789 _trippleClickPoint = _mousePosition;
1790 _trippleClickStartTime = crl::now();
1791 }
1792
applyItemSelection(UniversalMsgId universalId,TextSelection selection)1793 void ListWidget::applyItemSelection(
1794 UniversalMsgId universalId,
1795 TextSelection selection) {
1796 if (changeItemSelection(
1797 _selected,
1798 universalId,
1799 selection)) {
1800 repaintItem(universalId);
1801 pushSelectedItems();
1802 }
1803 }
1804
toggleItemSelection(UniversalMsgId universalId)1805 void ListWidget::toggleItemSelection(UniversalMsgId universalId) {
1806 auto it = _selected.find(universalId);
1807 if (it == _selected.cend()) {
1808 applyItemSelection(universalId, FullSelection);
1809 } else {
1810 removeItemSelection(it);
1811 }
1812 }
1813
changeItemSelection(SelectedMap & selected,UniversalMsgId universalId,TextSelection selection) const1814 bool ListWidget::changeItemSelection(
1815 SelectedMap &selected,
1816 UniversalMsgId universalId,
1817 TextSelection selection) const {
1818 auto changeExisting = [&](auto it) {
1819 if (it == selected.cend()) {
1820 return false;
1821 } else if (it->second.text != selection) {
1822 it->second.text = selection;
1823 return true;
1824 }
1825 return false;
1826 };
1827 if (selected.size() < MaxSelectedItems) {
1828 auto [iterator, ok] = selected.try_emplace(
1829 universalId,
1830 selection);
1831 if (ok) {
1832 auto item = session().data().message(computeFullId(universalId));
1833 if (!item) {
1834 selected.erase(iterator);
1835 return false;
1836 }
1837 iterator->second.canDelete = item->canDelete();
1838 iterator->second.canForward = item->allowsForward();
1839 return true;
1840 }
1841 return changeExisting(iterator);
1842 }
1843 return changeExisting(selected.find(universalId));
1844 }
1845
isItemUnderPressSelected() const1846 bool ListWidget::isItemUnderPressSelected() const {
1847 return itemUnderPressSelection() != _selected.end();
1848 }
1849
itemUnderPressSelection()1850 auto ListWidget::itemUnderPressSelection() -> SelectedMap::iterator {
1851 return (_pressState.itemId && _pressState.inside)
1852 ? _selected.find(_pressState.itemId)
1853 : _selected.end();
1854 }
1855
itemUnderPressSelection() const1856 auto ListWidget::itemUnderPressSelection() const
1857 -> SelectedMap::const_iterator {
1858 return (_pressState.itemId && _pressState.inside)
1859 ? _selected.find(_pressState.itemId)
1860 : _selected.end();
1861 }
1862
requiredToStartDragging(not_null<BaseLayout * > layout) const1863 bool ListWidget::requiredToStartDragging(
1864 not_null<BaseLayout*> layout) const {
1865 if (_mouseCursorState == CursorState::Date) {
1866 return true;
1867 }
1868 // return dynamic_cast<Sticker*>(layout->getMedia());
1869 return false;
1870 }
1871
isPressInSelectedText(TextState state) const1872 bool ListWidget::isPressInSelectedText(TextState state) const {
1873 if (state.cursor != CursorState::Text) {
1874 return false;
1875 }
1876 if (!hasSelectedText()
1877 || !isItemUnderPressSelected()) {
1878 return false;
1879 }
1880 auto pressedSelection = itemUnderPressSelection();
1881 auto from = pressedSelection->second.text.from;
1882 auto to = pressedSelection->second.text.to;
1883 return (state.symbol >= from && state.symbol < to);
1884 }
1885
clearSelected()1886 void ListWidget::clearSelected() {
1887 if (_selected.empty()) {
1888 return;
1889 }
1890 if (hasSelectedText()) {
1891 repaintItem(_selected.begin()->first);
1892 _selected.clear();
1893 } else {
1894 _selected.clear();
1895 pushSelectedItems();
1896 update();
1897 }
1898 }
1899
validateTrippleClickStartTime()1900 void ListWidget::validateTrippleClickStartTime() {
1901 if (_trippleClickStartTime) {
1902 auto elapsed = (crl::now() - _trippleClickStartTime);
1903 if (elapsed >= QApplication::doubleClickInterval()) {
1904 _trippleClickStartTime = 0;
1905 }
1906 }
1907 }
1908
enterEventHook(QEnterEvent * e)1909 void ListWidget::enterEventHook(QEnterEvent *e) {
1910 mouseActionUpdate(QCursor::pos());
1911 return RpWidget::enterEventHook(e);
1912 }
1913
leaveEventHook(QEvent * e)1914 void ListWidget::leaveEventHook(QEvent *e) {
1915 if (const auto item = _overLayout) {
1916 if (_overState.inside) {
1917 repaintItem(item);
1918 _overState.inside = false;
1919 }
1920 }
1921 ClickHandler::clearActive();
1922 if (!ClickHandler::getPressed() && _cursor != style::cur_default) {
1923 _cursor = style::cur_default;
1924 setCursor(_cursor);
1925 }
1926 return RpWidget::leaveEventHook(e);
1927 }
1928
clampMousePosition(QPoint position) const1929 QPoint ListWidget::clampMousePosition(QPoint position) const {
1930 return {
1931 std::clamp(position.x(), 0, qMax(0, width() - 1)),
1932 std::clamp(position.y(), _visibleTop, _visibleBottom - 1)
1933 };
1934 }
1935
mouseActionUpdate(const QPoint & globalPosition)1936 void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
1937 if (_sections.empty() || _visibleBottom <= _visibleTop) {
1938 return;
1939 }
1940
1941 _mousePosition = globalPosition;
1942
1943 auto local = mapFromGlobal(_mousePosition);
1944 auto point = clampMousePosition(local);
1945 auto [layout, geometry, inside] = findItemByPoint(point);
1946 auto state = MouseState{
1947 GetUniversalId(layout),
1948 geometry.size(),
1949 point - geometry.topLeft(),
1950 inside
1951 };
1952 if (_overLayout != layout) {
1953 repaintItem(_overLayout);
1954 _overLayout = layout;
1955 repaintItem(geometry);
1956 }
1957 _overState = state;
1958
1959 TextState dragState;
1960 ClickHandlerHost *lnkhost = nullptr;
1961 auto inTextSelection = _overState.inside
1962 && (_overState.itemId == _pressState.itemId)
1963 && hasSelectedText();
1964 if (_overLayout) {
1965 auto cursorDeltaLength = [&] {
1966 auto cursorDelta = (_overState.cursor - _pressState.cursor);
1967 return cursorDelta.manhattanLength();
1968 };
1969 auto dragStartLength = [] {
1970 return QApplication::startDragDistance();
1971 };
1972 if (_overState.itemId != _pressState.itemId
1973 || cursorDeltaLength() >= dragStartLength()) {
1974 if (_mouseAction == MouseAction::PrepareDrag) {
1975 _mouseAction = MouseAction::Dragging;
1976 InvokeQueued(this, [this] { performDrag(); });
1977 } else if (_mouseAction == MouseAction::PrepareSelect) {
1978 _mouseAction = MouseAction::Selecting;
1979 }
1980 }
1981 StateRequest request;
1982 if (_mouseAction == MouseAction::Selecting) {
1983 request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
1984 } else {
1985 inTextSelection = false;
1986 }
1987 dragState = _overLayout->getState(_overState.cursor, request);
1988 lnkhost = _overLayout;
1989 }
1990 ClickHandler::setActive(dragState.link, lnkhost);
1991
1992 if (_mouseAction == MouseAction::None) {
1993 _mouseCursorState = dragState.cursor;
1994 auto cursor = computeMouseCursor();
1995 if (_cursor != cursor) {
1996 setCursor(_cursor = cursor);
1997 }
1998 } else if (_mouseAction == MouseAction::Selecting) {
1999 if (inTextSelection) {
2000 auto second = dragState.symbol;
2001 if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {
2002 ++second;
2003 }
2004 auto selState = TextSelection {
2005 qMin(second, _mouseTextSymbol),
2006 qMax(second, _mouseTextSymbol)
2007 };
2008 if (_mouseSelectType != TextSelectType::Letters) {
2009 selState = _overLayout->adjustSelection(selState, _mouseSelectType);
2010 }
2011 applyItemSelection(_overState.itemId, selState);
2012 auto hasSelection = (selState == FullSelection)
2013 || (selState.from != selState.to);
2014 if (!_wasSelectedText && hasSelection) {
2015 _wasSelectedText = true;
2016 setFocus();
2017 }
2018 clearDragSelection();
2019 } else if (_pressState.itemId) {
2020 updateDragSelection();
2021 }
2022 } else if (_mouseAction == MouseAction::Dragging) {
2023 }
2024
2025 // #TODO scroll by drag
2026 //if (_mouseAction == MouseAction::Selecting) {
2027 // _widget->checkSelectingScroll(mousePos);
2028 //} else {
2029 // clearDragSelection();
2030 // _widget->noSelectingScroll();
2031 //}
2032 }
2033
computeMouseCursor() const2034 style::cursor ListWidget::computeMouseCursor() const {
2035 if (ClickHandler::getPressed() || ClickHandler::getActive()) {
2036 return style::cur_pointer;
2037 } else if (!hasSelectedItems()
2038 && (_mouseCursorState == CursorState::Text)) {
2039 return style::cur_text;
2040 }
2041 return style::cur_default;
2042 }
2043
updateDragSelection()2044 void ListWidget::updateDragSelection() {
2045 auto fromState = _pressState;
2046 auto tillState = _overState;
2047 auto swapStates = IsAfter(fromState, tillState);
2048 if (swapStates) {
2049 std::swap(fromState, tillState);
2050 }
2051 if (!fromState.itemId || !tillState.itemId) {
2052 clearDragSelection();
2053 return;
2054 }
2055 auto fromId = SkipSelectFromItem(fromState)
2056 ? (fromState.itemId - 1)
2057 : fromState.itemId;
2058 auto tillId = SkipSelectTillItem(tillState)
2059 ? tillState.itemId
2060 : (tillState.itemId - 1);
2061 for (auto i = _dragSelected.begin(); i != _dragSelected.end();) {
2062 auto itemId = i->first;
2063 if (itemId > fromId || itemId <= tillId) {
2064 i = _dragSelected.erase(i);
2065 } else {
2066 ++i;
2067 }
2068 }
2069 for (auto &layoutItem : _layouts) {
2070 auto &&universalId = layoutItem.first;
2071 if (universalId <= fromId && universalId > tillId) {
2072 changeItemSelection(
2073 _dragSelected,
2074 universalId,
2075 FullSelection);
2076 }
2077 }
2078 _dragSelectAction = [&] {
2079 if (_dragSelected.empty()) {
2080 return DragSelectAction::None;
2081 }
2082 auto &[firstDragItem, data] = swapStates
2083 ? _dragSelected.front()
2084 : _dragSelected.back();
2085 if (isSelectedItem(_selected.find(firstDragItem))) {
2086 return DragSelectAction::Deselecting;
2087 } else {
2088 return DragSelectAction::Selecting;
2089 }
2090 }();
2091 if (!_wasSelectedText
2092 && !_dragSelected.empty()
2093 && _dragSelectAction == DragSelectAction::Selecting) {
2094 _wasSelectedText = true;
2095 setFocus();
2096 }
2097 update();
2098 }
2099
clearDragSelection()2100 void ListWidget::clearDragSelection() {
2101 _dragSelectAction = DragSelectAction::None;
2102 if (!_dragSelected.empty()) {
2103 _dragSelected.clear();
2104 update();
2105 }
2106 }
2107
mouseActionStart(const QPoint & globalPosition,Qt::MouseButton button)2108 void ListWidget::mouseActionStart(
2109 const QPoint &globalPosition,
2110 Qt::MouseButton button) {
2111 mouseActionUpdate(globalPosition);
2112 if (button != Qt::LeftButton) {
2113 return;
2114 }
2115
2116 ClickHandler::pressed();
2117 if (_pressState != _overState) {
2118 if (_pressState.itemId != _overState.itemId) {
2119 repaintItem(_pressState.itemId);
2120 }
2121 _pressState = _overState;
2122 repaintItem(_overLayout);
2123 }
2124 auto pressLayout = _overLayout;
2125
2126 _mouseAction = MouseAction::None;
2127 _pressWasInactive = Ui::WasInactivePress(
2128 _controller->parentController()->widget());
2129 if (_pressWasInactive) {
2130 Ui::MarkInactivePress(
2131 _controller->parentController()->widget(),
2132 false);
2133 }
2134
2135 if (ClickHandler::getPressed() && !hasSelected()) {
2136 _mouseAction = MouseAction::PrepareDrag;
2137 } else if (hasSelectedItems()) {
2138 if (isItemUnderPressSelected() && ClickHandler::getPressed()) {
2139 // In shared media overview drag only by click handlers.
2140 _mouseAction = MouseAction::PrepareDrag; // start items drag
2141 } else if (!_pressWasInactive) {
2142 _mouseAction = MouseAction::PrepareSelect; // start items select
2143 }
2144 }
2145 if (_mouseAction == MouseAction::None && pressLayout) {
2146 validateTrippleClickStartTime();
2147 TextState dragState;
2148 auto startDistance = (globalPosition - _trippleClickPoint).manhattanLength();
2149 auto validStartPoint = startDistance < QApplication::startDragDistance();
2150 if (_trippleClickStartTime != 0 && validStartPoint) {
2151 StateRequest request;
2152 request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
2153 dragState = pressLayout->getState(_pressState.cursor, request);
2154 if (dragState.cursor == CursorState::Text) {
2155 TextSelection selStatus = { dragState.symbol, dragState.symbol };
2156 if (selStatus != FullSelection && !hasSelectedItems()) {
2157 clearSelected();
2158 applyItemSelection(_pressState.itemId, selStatus);
2159 _mouseTextSymbol = dragState.symbol;
2160 _mouseAction = MouseAction::Selecting;
2161 _mouseSelectType = TextSelectType::Paragraphs;
2162 mouseActionUpdate(_mousePosition);
2163 _trippleClickStartTime = crl::now();
2164 }
2165 }
2166 } else {
2167 StateRequest request;
2168 request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
2169 dragState = pressLayout->getState(_pressState.cursor, request);
2170 }
2171 if (_mouseSelectType != TextSelectType::Paragraphs) {
2172 if (_pressState.inside) {
2173 _mouseTextSymbol = dragState.symbol;
2174 if (isPressInSelectedText(dragState)) {
2175 _mouseAction = MouseAction::PrepareDrag; // start text drag
2176 } else if (!_pressWasInactive) {
2177 if (requiredToStartDragging(pressLayout)) {
2178 _mouseAction = MouseAction::PrepareDrag;
2179 } else {
2180 if (dragState.afterSymbol) ++_mouseTextSymbol;
2181 TextSelection selStatus = { _mouseTextSymbol, _mouseTextSymbol };
2182 if (selStatus != FullSelection && !hasSelectedItems()) {
2183 clearSelected();
2184 applyItemSelection(_pressState.itemId, selStatus);
2185 _mouseAction = MouseAction::Selecting;
2186 repaintItem(pressLayout);
2187 } else {
2188 _mouseAction = MouseAction::PrepareSelect;
2189 }
2190 }
2191 }
2192 } else if (!_pressWasInactive) {
2193 _mouseAction = MouseAction::PrepareSelect; // start items select
2194 }
2195 }
2196 }
2197
2198 if (!pressLayout) {
2199 _mouseAction = MouseAction::None;
2200 } else if (_mouseAction == MouseAction::None) {
2201 mouseActionCancel();
2202 }
2203 }
2204
mouseActionCancel()2205 void ListWidget::mouseActionCancel() {
2206 _pressState = MouseState();
2207 _mouseAction = MouseAction::None;
2208 clearDragSelection();
2209 _wasSelectedText = false;
2210 // _widget->noSelectingScroll(); // #TODO scroll by drag
2211 }
2212
performDrag()2213 void ListWidget::performDrag() {
2214 if (_mouseAction != MouseAction::Dragging) return;
2215
2216 auto uponSelected = false;
2217 if (_pressState.itemId && _pressState.inside) {
2218 if (hasSelectedItems()) {
2219 uponSelected = isItemUnderPressSelected();
2220 } else if (auto pressLayout = getExistingLayout(
2221 _pressState.itemId)) {
2222 StateRequest request;
2223 request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
2224 auto dragState = pressLayout->getState(
2225 _pressState.cursor,
2226 request);
2227 uponSelected = isPressInSelectedText(dragState);
2228 }
2229 }
2230 auto pressedHandler = ClickHandler::getPressed();
2231
2232 if (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.get())) {
2233 return;
2234 }
2235
2236 TextWithEntities sel;
2237 //QList<QUrl> urls;
2238 if (uponSelected) {
2239 // sel = getSelectedText();
2240 } else if (pressedHandler) {
2241 sel = { pressedHandler->dragText(), EntitiesInText() };
2242 //if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
2243 // urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
2244 //}
2245 }
2246 //if (auto mimeData = MimeDataFromText(sel)) {
2247 // clearDragSelection();
2248 // _widget->noSelectingScroll();
2249
2250 // if (!urls.isEmpty()) mimeData->setUrls(urls);
2251 // if (uponSelected && !Adaptive::OneColumn()) {
2252 // auto selectedState = getSelectionState();
2253 // if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
2254 // session().data().setMimeForwardIds(collectSelectedIds());
2255 // mimeData->setData(qsl("application/x-td-forward"), "1");
2256 // }
2257 // }
2258 // _controller->parentController()->window()->launchDrag(std::move(mimeData));
2259 // return;
2260 //} else {
2261 // auto forwardMimeType = QString();
2262 // auto pressedMedia = static_cast<HistoryView::Media*>(nullptr);
2263 // if (auto pressedItem = _pressState.layout) {
2264 // pressedMedia = pressedItem->getMedia();
2265 // if (_mouseCursorState == CursorState::Date || (pressedMedia && pressedMedia->dragItem())) {
2266 // session().data().setMimeForwardIds(session().data().itemOrItsGroup(pressedItem));
2267 // forwardMimeType = qsl("application/x-td-forward");
2268 // }
2269 // }
2270 // if (auto pressedLnkItem = App::pressedLinkItem()) {
2271 // if ((pressedMedia = pressedLnkItem->getMedia())) {
2272 // if (forwardMimeType.isEmpty() && pressedMedia->dragItemByHandler(pressedHandler)) {
2273 // session().data().setMimeForwardIds({ 1, pressedLnkItem->fullId() });
2274 // forwardMimeType = qsl("application/x-td-forward");
2275 // }
2276 // }
2277 // }
2278 // if (!forwardMimeType.isEmpty()) {
2279 // auto mimeData = std::make_unique<QMimeData>();
2280 // mimeData->setData(forwardMimeType, "1");
2281 // if (auto document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) {
2282 // auto filepath = document->filepath(true);
2283 // if (!filepath.isEmpty()) {
2284 // QList<QUrl> urls;
2285 // urls.push_back(QUrl::fromLocalFile(filepath));
2286 // mimeData->setUrls(urls);
2287 // }
2288 // }
2289
2290 // // This call enters event loop and can destroy any QObject.
2291 // _controller->parentController()->window()->launchDrag(std::move(mimeData));
2292 // return;
2293 // }
2294 //}
2295 }
2296
mouseActionFinish(const QPoint & globalPosition,Qt::MouseButton button)2297 void ListWidget::mouseActionFinish(
2298 const QPoint &globalPosition,
2299 Qt::MouseButton button) {
2300 mouseActionUpdate(globalPosition);
2301
2302 auto pressState = base::take(_pressState);
2303 repaintItem(pressState.itemId);
2304
2305 auto simpleSelectionChange = pressState.itemId
2306 && pressState.inside
2307 && !_pressWasInactive
2308 && (button != Qt::RightButton)
2309 && (_mouseAction == MouseAction::PrepareDrag
2310 || _mouseAction == MouseAction::PrepareSelect);
2311 auto needSelectionToggle = simpleSelectionChange
2312 && hasSelectedItems();
2313 auto needSelectionClear = simpleSelectionChange
2314 && hasSelectedText();
2315
2316 auto activated = ClickHandler::unpressed();
2317 if (_mouseAction == MouseAction::Dragging
2318 || _mouseAction == MouseAction::Selecting) {
2319 activated = nullptr;
2320 } else if (needSelectionToggle) {
2321 activated = nullptr;
2322 }
2323
2324 _wasSelectedText = false;
2325 if (activated) {
2326 mouseActionCancel();
2327 const auto found = findItemById(pressState.itemId);
2328 const auto fullId = found
2329 ? found->layout->getItem()->fullId()
2330 : FullMsgId();
2331 ActivateClickHandler(window(), activated, {
2332 button,
2333 QVariant::fromValue(ClickHandlerContext{
2334 .itemId = fullId,
2335 .sessionWindow = base::make_weak(
2336 _controller->parentController().get()),
2337 })
2338 });
2339 return;
2340 }
2341
2342 if (needSelectionToggle) {
2343 toggleItemSelection(pressState.itemId);
2344 } else if (needSelectionClear) {
2345 clearSelected();
2346 } else if (_mouseAction == MouseAction::Selecting) {
2347 if (!_dragSelected.empty()) {
2348 applyDragSelection();
2349 } else if (!_selected.empty() && !_pressWasInactive) {
2350 auto selection = _selected.cbegin()->second;
2351 if (selection.text != FullSelection
2352 && selection.text.from == selection.text.to) {
2353 clearSelected();
2354 //_controller->parentController()->window()->setInnerFocus(); // #TODO focus
2355 }
2356 }
2357 }
2358 _mouseAction = MouseAction::None;
2359 _mouseSelectType = TextSelectType::Letters;
2360 //_widget->noSelectingScroll(); // #TODO scroll by drag
2361 //_widget->updateTopBarSelection();
2362
2363 //if (QGuiApplication::clipboard()->supportsSelection() && hasSelectedText()) { // #TODO linux clipboard
2364 // TextUtilities::SetClipboardText(_selected.cbegin()->first->selectedText(_selected.cbegin()->second), QClipboard::Selection);
2365 //}
2366 }
2367
applyDragSelection()2368 void ListWidget::applyDragSelection() {
2369 applyDragSelection(_selected);
2370 clearDragSelection();
2371 pushSelectedItems();
2372 }
2373
applyDragSelection(SelectedMap & applyTo) const2374 void ListWidget::applyDragSelection(SelectedMap &applyTo) const {
2375 if (_dragSelectAction == DragSelectAction::Selecting) {
2376 for (auto &[universalId,data] : _dragSelected) {
2377 changeItemSelection(applyTo, universalId, FullSelection);
2378 }
2379 } else if (_dragSelectAction == DragSelectAction::Deselecting) {
2380 for (auto &[universalId,data] : _dragSelected) {
2381 applyTo.remove(universalId);
2382 }
2383 }
2384 }
2385
refreshHeight()2386 void ListWidget::refreshHeight() {
2387 resize(width(), recountHeight());
2388 }
2389
recountHeight()2390 int ListWidget::recountHeight() {
2391 if (_sections.empty()) {
2392 if (auto count = _slice.fullCount()) {
2393 if (*count == 0) {
2394 return 0;
2395 }
2396 }
2397 }
2398 auto cachedPadding = padding();
2399 auto result = cachedPadding.top();
2400 for (auto §ion : _sections) {
2401 section.setTop(result);
2402 result += section.height();
2403 }
2404 return result + cachedPadding.bottom();
2405 }
2406
mouseActionUpdate()2407 void ListWidget::mouseActionUpdate() {
2408 mouseActionUpdate(_mousePosition);
2409 }
2410
clearStaleLayouts()2411 void ListWidget::clearStaleLayouts() {
2412 for (auto i = _layouts.begin(); i != _layouts.end();) {
2413 if (i->second.stale) {
2414 if (i->second.item.get() == _overLayout) {
2415 _overLayout = nullptr;
2416 }
2417 _heavyLayouts.erase(i->second.item.get());
2418 i = _layouts.erase(i);
2419 } else {
2420 ++i;
2421 }
2422 }
2423 }
2424
findSectionByItem(UniversalMsgId universalId)2425 auto ListWidget::findSectionByItem(
2426 UniversalMsgId universalId) -> std::vector<Section>::iterator {
2427 return ranges::lower_bound(
2428 _sections,
2429 universalId,
2430 std::greater<>(),
2431 [](const Section §ion) { return section.minId(); });
2432 }
2433
findSectionAfterTop(int top)2434 auto ListWidget::findSectionAfterTop(
2435 int top) -> std::vector<Section>::iterator {
2436 return ranges::lower_bound(
2437 _sections,
2438 top,
2439 std::less_equal<>(),
2440 [](const Section §ion) { return section.bottom(); });
2441 }
2442
findSectionAfterTop(int top) const2443 auto ListWidget::findSectionAfterTop(
2444 int top) const -> std::vector<Section>::const_iterator {
2445 return ranges::lower_bound(
2446 _sections,
2447 top,
2448 std::less_equal<>(),
2449 [](const Section §ion) { return section.bottom(); });
2450 }
2451
findSectionAfterBottom(std::vector<Section>::const_iterator from,int bottom) const2452 auto ListWidget::findSectionAfterBottom(
2453 std::vector<Section>::const_iterator from,
2454 int bottom) const -> std::vector<Section>::const_iterator {
2455 return ranges::lower_bound(
2456 from,
2457 _sections.end(),
2458 bottom,
2459 std::less<>(),
2460 [](const Section §ion) { return section.top(); });
2461 }
2462
~ListWidget()2463 ListWidget::~ListWidget() {
2464 if (_contextMenu) {
2465 // We don't want it to be called after ListWidget is destroyed.
2466 _contextMenu->setDestroyedCallback(nullptr);
2467 }
2468 }
2469
2470 } // namespace Media
2471 } // namespace Info
2472