2017-06-18 11:08:14 +00:00
|
|
|
/*
|
|
|
|
This file is part of Telegram Desktop,
|
2018-01-03 10:23:14 +00:00
|
|
|
the official desktop application for the Telegram messaging service.
|
2017-06-18 11:08:14 +00:00
|
|
|
|
2018-01-03 10:23:14 +00:00
|
|
|
For license and copyright information please follow this link:
|
|
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
2017-06-18 11:08:14 +00:00
|
|
|
*/
|
2018-01-09 17:08:31 +00:00
|
|
|
#include "history/view/history_view_list_widget.h"
|
2017-06-18 11:08:14 +00:00
|
|
|
|
2021-01-31 04:22:46 +00:00
|
|
|
#include "base/unixtime.h"
|
2017-06-22 15:11:41 +00:00
|
|
|
#include "history/history_message.h"
|
2017-12-18 15:44:50 +00:00
|
|
|
#include "history/history_item_components.h"
|
2018-01-27 17:26:24 +00:00
|
|
|
#include "history/history_item_text.h"
|
2019-08-02 13:21:09 +00:00
|
|
|
#include "history/view/media/history_view_media.h"
|
|
|
|
#include "history/view/media/history_view_sticker.h"
|
2018-01-25 10:10:52 +00:00
|
|
|
#include "history/view/history_view_context_menu.h"
|
2018-01-11 19:33:26 +00:00
|
|
|
#include "history/view/history_view_element.h"
|
2018-01-21 14:49:42 +00:00
|
|
|
#include "history/view/history_view_message.h"
|
2018-01-09 17:08:31 +00:00
|
|
|
#include "history/view/history_view_service_message.h"
|
2018-01-27 13:59:24 +00:00
|
|
|
#include "history/view/history_view_cursor_state.h"
|
2017-06-23 19:28:42 +00:00
|
|
|
#include "chat_helpers/message_field.h"
|
2017-06-21 21:38:31 +00:00
|
|
|
#include "mainwindow.h"
|
2017-06-24 10:11:29 +00:00
|
|
|
#include "mainwidget.h"
|
2020-11-10 16:38:21 +00:00
|
|
|
#include "core/click_handler_types.h"
|
2017-07-05 18:11:31 +00:00
|
|
|
#include "apiwrap.h"
|
2021-07-26 03:59:25 +00:00
|
|
|
#include "layout/layout_selection.h"
|
2021-05-26 21:04:18 +00:00
|
|
|
#include "window/window_adaptive.h"
|
2019-06-06 10:21:40 +00:00
|
|
|
#include "window/window_session_controller.h"
|
2018-01-23 16:51:12 +00:00
|
|
|
#include "window/window_peer_menu.h"
|
2019-07-24 11:45:24 +00:00
|
|
|
#include "main/main_session.h"
|
2020-10-26 08:54:59 +00:00
|
|
|
#include "boxes/confirm_box.h"
|
2017-06-24 10:11:29 +00:00
|
|
|
#include "ui/widgets/popup_menu.h"
|
2019-03-15 15:15:56 +00:00
|
|
|
#include "ui/toast/toast.h"
|
2019-09-16 11:14:06 +00:00
|
|
|
#include "ui/inactive_press.h"
|
2021-07-02 15:29:13 +00:00
|
|
|
#include "ui/effects/path_shift_gradient.h"
|
2021-08-26 15:02:21 +00:00
|
|
|
#include "ui/chat/chat_theme.h"
|
2021-09-02 22:08:57 +00:00
|
|
|
#include "ui/chat/chat_style.h"
|
2017-06-21 21:38:31 +00:00
|
|
|
#include "lang/lang_keys.h"
|
2019-01-07 12:55:49 +00:00
|
|
|
#include "boxes/peers/edit_participant_box.h"
|
2018-01-04 10:22:53 +00:00
|
|
|
#include "data/data_session.h"
|
2019-04-15 11:54:03 +00:00
|
|
|
#include "data/data_folder.h"
|
2018-01-14 16:02:25 +00:00
|
|
|
#include "data/data_media_types.h"
|
2018-12-18 10:45:06 +00:00
|
|
|
#include "data/data_document.h"
|
2019-01-04 11:09:48 +00:00
|
|
|
#include "data/data_peer.h"
|
2020-11-10 16:38:21 +00:00
|
|
|
#include "data/data_user.h"
|
|
|
|
#include "data/data_chat.h"
|
|
|
|
#include "data/data_channel.h"
|
2021-06-17 23:28:09 +00:00
|
|
|
#include "data/data_file_click_handler.h"
|
2019-09-13 06:06:02 +00:00
|
|
|
#include "facades.h"
|
2020-10-10 09:15:37 +00:00
|
|
|
#include "styles/style_chat.h"
|
2017-06-18 11:08:14 +00:00
|
|
|
|
2019-09-04 07:19:15 +00:00
|
|
|
#include <QtWidgets/QApplication>
|
|
|
|
#include <QtCore/QMimeData>
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
namespace HistoryView {
|
2017-06-18 11:08:14 +00:00
|
|
|
namespace {
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
constexpr auto kPreloadedScreensCount = 4;
|
|
|
|
constexpr auto kPreloadIfLessThanScreens = 2;
|
|
|
|
constexpr auto kPreloadedScreensCountFull
|
|
|
|
= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
|
2020-05-28 14:32:10 +00:00
|
|
|
constexpr auto kClearUserpicsAfter = 50;
|
2017-06-18 11:08:14 +00:00
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
2018-01-27 13:59:24 +00:00
|
|
|
ListWidget::MouseState::MouseState() : pointState(PointState::Outside) {
|
|
|
|
}
|
|
|
|
|
|
|
|
ListWidget::MouseState::MouseState(
|
|
|
|
FullMsgId itemId,
|
|
|
|
int height,
|
|
|
|
QPoint point,
|
|
|
|
PointState pointState)
|
|
|
|
: itemId(itemId)
|
|
|
|
, height(height)
|
|
|
|
, point(point)
|
|
|
|
, pointState(pointState) {
|
|
|
|
}
|
|
|
|
|
2021-07-20 09:13:32 +00:00
|
|
|
const crl::time ListWidget::kItemRevealDuration = crl::time(150);
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
template <ListWidget::EnumItemsDirection direction, typename Method>
|
|
|
|
void ListWidget::enumerateItems(Method method) {
|
2017-06-22 01:31:02 +00:00
|
|
|
constexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom);
|
|
|
|
|
|
|
|
// No displayed messages in this history.
|
|
|
|
if (_items.empty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (_visibleBottom <= _itemsTop || _itemsTop + _itemsHeight <= _visibleTop) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
const auto beginning = begin(_items);
|
|
|
|
const auto ending = end(_items);
|
|
|
|
auto from = TopToBottom
|
|
|
|
? std::lower_bound(
|
|
|
|
beginning,
|
|
|
|
ending,
|
|
|
|
_visibleTop,
|
|
|
|
[this](auto &elem, int top) {
|
2018-01-13 12:45:11 +00:00
|
|
|
return this->itemTop(elem) + elem->height() <= top;
|
2018-01-09 17:08:31 +00:00
|
|
|
})
|
|
|
|
: std::upper_bound(
|
|
|
|
beginning,
|
|
|
|
ending,
|
|
|
|
_visibleBottom,
|
|
|
|
[this](int bottom, auto &elem) {
|
2018-01-13 12:45:11 +00:00
|
|
|
return this->itemTop(elem) + elem->height() >= bottom;
|
2018-01-09 17:08:31 +00:00
|
|
|
});
|
|
|
|
auto wasEnd = (from == ending);
|
2017-06-22 01:31:02 +00:00
|
|
|
if (wasEnd) {
|
|
|
|
--from;
|
|
|
|
}
|
|
|
|
if (TopToBottom) {
|
2018-01-13 12:45:11 +00:00
|
|
|
Assert(itemTop(from->get()) + from->get()->height() > _visibleTop);
|
2017-06-22 01:31:02 +00:00
|
|
|
} else {
|
2017-08-17 09:06:26 +00:00
|
|
|
Assert(itemTop(from->get()) < _visibleBottom);
|
2017-06-22 01:31:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
while (true) {
|
2018-01-10 13:13:33 +00:00
|
|
|
auto view = from->get();
|
|
|
|
auto itemtop = itemTop(view);
|
2018-01-13 12:45:11 +00:00
|
|
|
auto itembottom = itemtop + view->height();
|
2017-06-22 01:31:02 +00:00
|
|
|
|
|
|
|
// Binary search should've skipped all the items that are above / below the visible area.
|
|
|
|
if (TopToBottom) {
|
2017-08-17 09:06:26 +00:00
|
|
|
Assert(itembottom > _visibleTop);
|
2017-06-22 01:31:02 +00:00
|
|
|
} else {
|
2017-08-17 09:06:26 +00:00
|
|
|
Assert(itemtop < _visibleBottom);
|
2017-06-22 01:31:02 +00:00
|
|
|
}
|
|
|
|
|
2018-01-10 13:13:33 +00:00
|
|
|
if (!method(view, itemtop, itembottom)) {
|
2017-06-22 01:31:02 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Skip all the items that are below / above the visible area.
|
|
|
|
if (TopToBottom) {
|
|
|
|
if (itembottom >= _visibleBottom) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (itemtop <= _visibleTop) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (TopToBottom) {
|
2018-01-09 17:08:31 +00:00
|
|
|
if (++from == ending) {
|
2017-06-22 01:31:02 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
2018-01-09 17:08:31 +00:00
|
|
|
if (from == beginning) {
|
2017-06-22 01:31:02 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
--from;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename Method>
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::enumerateUserpics(Method method) {
|
2017-06-22 01:31:02 +00:00
|
|
|
// Find and remember the top of an attached messages pack
|
|
|
|
// -1 means we didn't find an attached to next message yet.
|
|
|
|
int lowestAttachedItemTop = -1;
|
|
|
|
|
2018-01-11 19:33:26 +00:00
|
|
|
auto userpicCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {
|
2017-06-22 01:31:02 +00:00
|
|
|
// Skip all service messages.
|
2018-01-10 13:13:33 +00:00
|
|
|
auto message = view->data()->toHistoryMessage();
|
2017-06-22 01:31:02 +00:00
|
|
|
if (!message) return true;
|
|
|
|
|
2018-01-13 12:45:11 +00:00
|
|
|
if (lowestAttachedItemTop < 0 && view->isAttachedToNext()) {
|
|
|
|
lowestAttachedItemTop = itemtop + view->marginTop();
|
2017-06-22 01:31:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Call method on a userpic for all messages that have it and for those who are not showing it
|
|
|
|
// because of their attachment to the next message if they are bottom-most visible.
|
2018-01-13 12:45:11 +00:00
|
|
|
if (view->displayFromPhoto() || (view->hasFromPhoto() && itembottom >= _visibleBottom)) {
|
2017-06-22 01:31:02 +00:00
|
|
|
if (lowestAttachedItemTop < 0) {
|
2018-01-13 12:45:11 +00:00
|
|
|
lowestAttachedItemTop = itemtop + view->marginTop();
|
2017-06-22 01:31:02 +00:00
|
|
|
}
|
|
|
|
// Attach userpic to the bottom of the visible area with the same margin as the last message.
|
|
|
|
auto userpicMinBottomSkip = st::historyPaddingBottom + st::msgMargin.bottom();
|
2018-01-13 12:45:11 +00:00
|
|
|
auto userpicBottom = qMin(itembottom - view->marginBottom(), _visibleBottom - userpicMinBottomSkip);
|
2017-06-22 01:31:02 +00:00
|
|
|
|
|
|
|
// Do not let the userpic go above the attached messages pack top line.
|
|
|
|
userpicBottom = qMax(userpicBottom, lowestAttachedItemTop + st::msgPhotoSize);
|
|
|
|
|
|
|
|
// Call the template callback function that was passed
|
|
|
|
// and return if it finished everything it needed.
|
2018-01-11 15:51:59 +00:00
|
|
|
if (!method(view, userpicBottom - st::msgPhotoSize)) {
|
2017-06-22 01:31:02 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Forget the found top of the pack, search for the next one from scratch.
|
2018-01-13 12:45:11 +00:00
|
|
|
if (!view->isAttachedToNext()) {
|
2017-06-22 01:31:02 +00:00
|
|
|
lowestAttachedItemTop = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
enumerateItems<EnumItemsDirection::TopToBottom>(userpicCallback);
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename Method>
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::enumerateDates(Method method) {
|
2017-06-22 01:31:02 +00:00
|
|
|
// Find and remember the bottom of an single-day messages pack
|
|
|
|
// -1 means we didn't find a same-day with previous message yet.
|
|
|
|
auto lowestInOneDayItemBottom = -1;
|
|
|
|
|
2018-01-11 19:33:26 +00:00
|
|
|
auto dateCallback = [&](not_null<Element*> view, int itemtop, int itembottom) {
|
2018-01-10 13:13:33 +00:00
|
|
|
const auto item = view->data();
|
2018-01-23 11:47:38 +00:00
|
|
|
if (lowestInOneDayItemBottom < 0 && view->isInOneDayWithPrevious()) {
|
2018-01-13 12:45:11 +00:00
|
|
|
lowestInOneDayItemBottom = itembottom - view->marginBottom();
|
2017-06-22 01:31:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Call method on a date for all messages that have it and for those who are not showing it
|
|
|
|
// because they are in a one day together with the previous message if they are top-most visible.
|
2018-01-23 11:47:38 +00:00
|
|
|
if (view->displayDate() || (!item->isEmpty() && itemtop <= _visibleTop)) {
|
2017-06-22 01:31:02 +00:00
|
|
|
if (lowestInOneDayItemBottom < 0) {
|
2018-01-13 12:45:11 +00:00
|
|
|
lowestInOneDayItemBottom = itembottom - view->marginBottom();
|
2017-06-22 01:31:02 +00:00
|
|
|
}
|
|
|
|
// Attach date to the top of the visible area with the same margin as it has in service message.
|
|
|
|
auto dateTop = qMax(itemtop, _visibleTop) + st::msgServiceMargin.top();
|
|
|
|
|
|
|
|
// Do not let the date go below the single-day messages pack bottom line.
|
|
|
|
auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
|
|
|
|
dateTop = qMin(dateTop, lowestInOneDayItemBottom - dateHeight);
|
|
|
|
|
|
|
|
// Call the template callback function that was passed
|
|
|
|
// and return if it finished everything it needed.
|
2018-01-11 15:51:59 +00:00
|
|
|
if (!method(view, itemtop, dateTop)) {
|
2017-06-22 01:31:02 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Forget the found bottom of the pack, search for the next one from scratch.
|
2018-01-23 11:47:38 +00:00
|
|
|
if (!view->isInOneDayWithPrevious()) {
|
2017-06-22 01:31:02 +00:00
|
|
|
lowestInOneDayItemBottom = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
};
|
|
|
|
|
|
|
|
enumerateItems<EnumItemsDirection::BottomToTop>(dateCallback);
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
ListWidget::ListWidget(
|
2017-10-05 15:35:52 +00:00
|
|
|
QWidget *parent,
|
2019-06-06 10:21:40 +00:00
|
|
|
not_null<Window::SessionController*> controller,
|
2018-01-09 17:08:31 +00:00
|
|
|
not_null<ListDelegate*> delegate)
|
2017-10-05 15:35:52 +00:00
|
|
|
: RpWidget(parent)
|
2018-01-09 17:08:31 +00:00
|
|
|
, _delegate(delegate)
|
2017-06-21 21:38:31 +00:00
|
|
|
, _controller(controller)
|
2018-01-10 13:13:33 +00:00
|
|
|
, _context(_delegate->listContext())
|
2018-01-30 12:13:46 +00:00
|
|
|
, _itemAverageHeight(itemMinimalHeight())
|
2021-09-03 10:17:07 +00:00
|
|
|
, _pathGradient(
|
|
|
|
MakePathShiftGradient(
|
|
|
|
controller->chatStyle(),
|
|
|
|
[=] { update(); }))
|
2018-01-26 15:40:11 +00:00
|
|
|
, _scrollDateCheck([this] { scrollDateCheck(); })
|
2018-02-02 12:51:18 +00:00
|
|
|
, _applyUpdatedScrollState([this] { applyUpdatedScrollState(); })
|
2018-02-16 17:59:35 +00:00
|
|
|
, _selectEnabled(_delegate->listAllowsMultiSelect())
|
|
|
|
, _highlightTimer([this] { updateHighlightedMessage(); }) {
|
2017-06-18 11:08:14 +00:00
|
|
|
setMouseTracking(true);
|
2017-06-21 21:38:31 +00:00
|
|
|
_scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); });
|
2019-07-24 11:13:51 +00:00
|
|
|
session().data().viewRepaintRequest(
|
2018-01-18 11:46:45 +00:00
|
|
|
) | rpl::start_with_next([this](auto view) {
|
2018-01-21 14:49:42 +00:00
|
|
|
if (view->delegate() == this) {
|
2018-01-19 17:10:58 +00:00
|
|
|
repaintItem(view);
|
|
|
|
}
|
|
|
|
}, lifetime());
|
2019-07-24 11:13:51 +00:00
|
|
|
session().data().viewResizeRequest(
|
2018-01-19 17:10:58 +00:00
|
|
|
) | rpl::start_with_next([this](auto view) {
|
2018-01-21 14:49:42 +00:00
|
|
|
if (view->delegate() == this) {
|
2018-01-21 19:21:08 +00:00
|
|
|
resizeItem(view);
|
2018-01-19 17:10:58 +00:00
|
|
|
}
|
|
|
|
}, lifetime());
|
2019-07-24 11:13:51 +00:00
|
|
|
session().data().itemViewRefreshRequest(
|
2018-01-19 17:10:58 +00:00
|
|
|
) | rpl::start_with_next([this](auto item) {
|
|
|
|
if (const auto view = viewForItem(item)) {
|
|
|
|
refreshItem(view);
|
|
|
|
}
|
|
|
|
}, lifetime());
|
2019-07-24 11:13:51 +00:00
|
|
|
session().data().viewLayoutChanged(
|
2018-01-21 19:52:44 +00:00
|
|
|
) | rpl::start_with_next([this](auto view) {
|
|
|
|
if (view->delegate() == this) {
|
|
|
|
if (view->isUnderCursor()) {
|
2018-01-26 15:40:11 +00:00
|
|
|
mouseActionUpdate();
|
2018-01-21 19:52:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}, lifetime());
|
2019-07-24 11:13:51 +00:00
|
|
|
session().data().animationPlayInlineRequest(
|
2018-01-19 17:10:58 +00:00
|
|
|
) | rpl::start_with_next([this](auto item) {
|
|
|
|
if (const auto view = viewForItem(item)) {
|
|
|
|
if (const auto media = view->media()) {
|
2018-01-21 14:49:42 +00:00
|
|
|
media->playAnimation();
|
2018-01-19 17:10:58 +00:00
|
|
|
}
|
|
|
|
}
|
2017-12-22 07:05:20 +00:00
|
|
|
}, lifetime());
|
2020-06-04 16:53:37 +00:00
|
|
|
|
|
|
|
session().downloaderTaskFinished(
|
|
|
|
) | rpl::start_with_next([=] {
|
|
|
|
update();
|
|
|
|
}, lifetime());
|
|
|
|
|
2019-07-24 11:13:51 +00:00
|
|
|
session().data().itemRemoved(
|
2020-10-23 13:35:43 +00:00
|
|
|
) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
|
|
|
|
itemRemoved(item);
|
|
|
|
}, lifetime());
|
|
|
|
|
2019-07-24 11:13:51 +00:00
|
|
|
subscribe(session().data().queryItemVisibility(), [this](const Data::Session::ItemVisibilityQuery &query) {
|
2018-01-10 13:13:33 +00:00
|
|
|
if (const auto view = viewForItem(query.item)) {
|
|
|
|
const auto top = itemTop(view);
|
|
|
|
if (top >= 0
|
2018-01-13 12:45:11 +00:00
|
|
|
&& top + view->height() > _visibleTop
|
2018-01-10 13:13:33 +00:00
|
|
|
&& top < _visibleBottom) {
|
2018-01-09 17:08:31 +00:00
|
|
|
*query.isVisible = true;
|
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
});
|
2021-05-27 00:44:12 +00:00
|
|
|
|
|
|
|
controller->adaptive().chatWideValue(
|
|
|
|
) | rpl::start_with_next([=](bool wide) {
|
|
|
|
_isChatWide = wide;
|
|
|
|
}, lifetime());
|
2018-01-09 17:08:31 +00:00
|
|
|
}
|
|
|
|
|
2019-07-24 11:45:24 +00:00
|
|
|
Main::Session &ListWidget::session() const {
|
2019-07-24 11:13:51 +00:00
|
|
|
return _controller->session();
|
|
|
|
}
|
|
|
|
|
2020-06-11 16:33:15 +00:00
|
|
|
not_null<Window::SessionController*> ListWidget::controller() const {
|
|
|
|
return _controller;
|
|
|
|
}
|
|
|
|
|
2018-01-25 10:10:52 +00:00
|
|
|
not_null<ListDelegate*> ListWidget::delegate() const {
|
|
|
|
return _delegate;
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::refreshViewer() {
|
|
|
|
_viewerLifetime.destroy();
|
|
|
|
_delegate->listSource(
|
|
|
|
_aroundPosition,
|
|
|
|
_idsLimit,
|
|
|
|
_idsLimit
|
|
|
|
) | rpl::start_with_next([=](Data::MessagesSlice &&slice) {
|
2021-07-19 17:01:39 +00:00
|
|
|
std::swap(_slice, slice);
|
|
|
|
refreshRows(slice);
|
2018-01-09 17:08:31 +00:00
|
|
|
}, _viewerLifetime);
|
|
|
|
}
|
|
|
|
|
2021-07-19 17:01:39 +00:00
|
|
|
void ListWidget::refreshRows(const Data::MessagesSlice &old) {
|
2018-01-09 17:08:31 +00:00
|
|
|
saveScrollState();
|
|
|
|
|
2021-07-19 17:01:39 +00:00
|
|
|
const auto addedToEndFrom = (old.skippedAfter == 0
|
|
|
|
&& (_slice.skippedAfter == 0)
|
|
|
|
&& !old.ids.empty())
|
|
|
|
? ranges::find(_slice.ids, old.ids.back())
|
|
|
|
: end(_slice.ids);
|
|
|
|
const auto addedToEndCount = std::max(
|
|
|
|
int(end(_slice.ids) - addedToEndFrom),
|
|
|
|
1
|
|
|
|
) - 1;
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
_items.clear();
|
|
|
|
_items.reserve(_slice.ids.size());
|
2020-10-02 16:26:04 +00:00
|
|
|
auto nearestIndex = -1;
|
2018-01-09 17:08:31 +00:00
|
|
|
for (const auto &fullId : _slice.ids) {
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto item = session().data().message(fullId)) {
|
2020-10-02 16:26:04 +00:00
|
|
|
if (_slice.nearestToAround == fullId) {
|
|
|
|
nearestIndex = int(_items.size());
|
|
|
|
}
|
2018-01-10 13:13:33 +00:00
|
|
|
_items.push_back(enforceViewForItem(item));
|
2018-01-09 17:08:31 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-19 17:01:39 +00:00
|
|
|
for (auto e = end(_items), i = e - addedToEndCount; i != e; ++i) {
|
|
|
|
_itemRevealPending.emplace(*i);
|
|
|
|
}
|
2020-10-02 16:26:04 +00:00
|
|
|
updateAroundPositionFromNearest(nearestIndex);
|
2018-01-09 17:08:31 +00:00
|
|
|
|
2018-01-19 17:10:58 +00:00
|
|
|
updateItemsGeometry();
|
2018-02-02 12:51:18 +00:00
|
|
|
checkUnreadBarCreation();
|
2018-01-09 17:08:31 +00:00
|
|
|
restoreScrollState();
|
2021-07-19 17:01:39 +00:00
|
|
|
if (!_itemsRevealHeight) {
|
|
|
|
mouseActionUpdate(QCursor::pos());
|
|
|
|
}
|
2021-07-20 18:18:53 +00:00
|
|
|
if (_emptyInfo) {
|
|
|
|
_emptyInfo->setVisible(isEmpty());
|
|
|
|
}
|
2018-02-04 19:57:03 +00:00
|
|
|
_delegate->listContentRefreshed();
|
|
|
|
}
|
|
|
|
|
2018-09-21 16:28:46 +00:00
|
|
|
std::optional<int> ListWidget::scrollTopForPosition(
|
2018-02-04 19:57:03 +00:00
|
|
|
Data::MessagePosition position) const {
|
|
|
|
if (position == Data::MaxMessagePosition) {
|
|
|
|
if (loadedAtBottom()) {
|
|
|
|
return height();
|
|
|
|
}
|
2018-09-21 16:28:46 +00:00
|
|
|
return std::nullopt;
|
2018-02-16 15:46:24 +00:00
|
|
|
} else if (_items.empty()
|
|
|
|
|| isBelowPosition(position)
|
|
|
|
|| isAbovePosition(position)) {
|
2018-09-21 16:28:46 +00:00
|
|
|
return std::nullopt;
|
2018-02-16 15:46:24 +00:00
|
|
|
}
|
|
|
|
const auto index = findNearestItem(position);
|
|
|
|
const auto view = _items[index];
|
Remove unused variable
The following are commits related to removed variables.
apiwrap.cpp
e050e27: kSaveDraftBeforeQuitTimeout
app.cpp
113f665: serviceImageCacheSize
boxes/auto_download_box.cpp
a0c6104: checked(Source source, Type type)
boxes/background_preview_box.cpp
b6edf45: resultBytesPerPixel
fe21b5a: ms
boxes/calendar_box.cpp
ae97704: yearIndex, monthIndex
99bb093: ms
boxes/connection_box.cpp
f794d8d: ping
boxes/dictionaries_manager.cpp
8353867: session
boxes/peer_list_box.cpp
2ce2a14: grayedWidth
boxes/peers/add_participants_box.cpp
07e010d: chat, channel
boxes/self_destruction_box.cpp
fe9f02e: count
chat_helpers/emoji_suggestions_widget.cpp
a12bc60: is(QLatin1String string)
chat_helpers/field_autocomplete.cpp
8c7a35c: atwidth, hashwidth
chat_helpers/gifs_list_widget.cpp
ff65734: inlineItems
3d846fc: newSelected
d1687ab: kSaveDraftBeforeQuitTimeout
chat_helpers/stickers_dice_pack.cpp
c83e297: kZeroDiceDocumentId
chat_helpers/stickers_emoji_pack.cpp
d298953: length
chat_helpers/stickers_list_widget.cpp
eb75859: index, x
core/crash_reports.cpp
5940ae6: LaunchedDateTimeStr, LaunchedBinaryName
data/data_changes.cpp
3c4e959:clearRealtime
data/data_cloud_file.cpp
4b354b0: fromCloud, cacheTag
data/data_document_media.cpp
7db5359: kMaxVideoFrameArea
data/data_messages.cpp
794e315: wasCount
data/data_photo_media.cpp
e27d2bc: index
data/data_wall_paper.cpp
b6edf45: resultBytesPerPixel
data/data_types.cpp
aa8f62d: kWebDocumentCacheTag, kStorageCacheMask
history/admin_log/history_admin_log_inner.cpp
794e315: canDelete, canForward
history/history_location_manager.cpp
60f45ab: kCoordPrecision
9f90d3a: kMaxHttpRedirects
history/history_message.cpp
cedf8a6: kPinnedMessageTextLimit
history/history_widget.cpp
b305924: serviceColor
efa5fc4: hasForward
5e7aa4f: kTabbedSelectorToggleTooltipTimeoutMs, kTabbedSelectorToggleTooltipCount
history/view/history_view_context_menu.cpp
fe1a90b: isVideoLink, isVoiceLink, isAudioLink
settings.cpp
e2f54eb: defaultRecent
settings/settings_folders.cpp
e8bf5bb: kRefreshSuggestedTimeout
ui/filter_icon_panel.cpp
c4a0bc1: kDelayedHideTimeoutMs
window/themes/window_theme_preview.cpp
ef927c8: mutedCounter
-----
Modified variables
boxes/stickers_box.cpp
554eb3a: _rows[pressedIndex] -> set
data/data_notify_settings.cpp
734c410: muteForSeconds -> muteUntil
history/view/history_view_list_widget.cpp
07528be: _items[index] -> view
e5f3bed: fromState, tillState
history/history.cpp
cd3c1c6: kStatusShowClientsideRecordVideo -> kStatusShowClientsideRecordVoice
storage/download_manager_mtproto.cpp
ae8fb14: _queues[dcId] -> queue
storage/localstorage.cpp
357caf8: MTP::Environment::Production -> production
2020-07-02 10:42:30 +00:00
|
|
|
return scrollTopForView(view);
|
2018-02-16 15:46:24 +00:00
|
|
|
}
|
|
|
|
|
2018-09-21 16:28:46 +00:00
|
|
|
std::optional<int> ListWidget::scrollTopForView(
|
2018-02-16 15:46:24 +00:00
|
|
|
not_null<Element*> view) const {
|
|
|
|
if (view->isHiddenByGroup()) {
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto group = session().data().groups().find(view->data())) {
|
2020-09-16 08:25:57 +00:00
|
|
|
if (const auto leader = viewForItem(group->items.front())) {
|
2018-02-16 15:46:24 +00:00
|
|
|
if (!leader->isHiddenByGroup()) {
|
|
|
|
return scrollTopForView(leader);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-02-04 19:57:03 +00:00
|
|
|
}
|
2018-02-16 15:46:24 +00:00
|
|
|
const auto top = view->y();
|
|
|
|
const auto height = view->height();
|
|
|
|
const auto available = _visibleBottom - _visibleTop;
|
|
|
|
return top - std::max((available - height) / 2, 0);
|
2018-02-04 19:57:03 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 12:31:15 +00:00
|
|
|
void ListWidget::scrollTo(
|
2018-02-04 19:57:03 +00:00
|
|
|
int scrollTop,
|
|
|
|
Data::MessagePosition attachPosition,
|
|
|
|
int delta,
|
|
|
|
AnimatedScroll type) {
|
2019-04-02 09:13:30 +00:00
|
|
|
_scrollToAnimation.stop();
|
2020-10-21 12:31:15 +00:00
|
|
|
if (!delta || _items.empty() || type == AnimatedScroll::None) {
|
2018-02-04 19:57:03 +00:00
|
|
|
_delegate->listScrollTo(scrollTop);
|
|
|
|
return;
|
|
|
|
}
|
2020-09-15 15:30:34 +00:00
|
|
|
const auto transition = (type == AnimatedScroll::Full)
|
|
|
|
? anim::sineInOut
|
|
|
|
: anim::easeOutCubic;
|
|
|
|
if (delta > 0 && scrollTop == height() - (_visibleBottom - _visibleTop)) {
|
|
|
|
// Animated scroll to bottom.
|
|
|
|
_scrollToAnimation.start(
|
|
|
|
[=] { scrollToAnimationCallback(FullMsgId(), 0); },
|
|
|
|
-delta,
|
|
|
|
0,
|
|
|
|
st::slideDuration,
|
|
|
|
transition);
|
|
|
|
return;
|
|
|
|
}
|
2018-02-04 19:57:03 +00:00
|
|
|
const auto index = findNearestItem(attachPosition);
|
|
|
|
Assert(index >= 0 && index < int(_items.size()));
|
|
|
|
const auto attachTo = _items[index];
|
|
|
|
const auto attachToId = attachTo->data()->fullId();
|
|
|
|
const auto initial = scrollTop - delta;
|
|
|
|
_delegate->listScrollTo(initial);
|
|
|
|
|
|
|
|
const auto attachToTop = itemTop(attachTo);
|
|
|
|
const auto relativeStart = initial - attachToTop;
|
|
|
|
const auto relativeFinish = scrollTop - attachToTop;
|
|
|
|
_scrollToAnimation.start(
|
2019-04-02 09:13:30 +00:00
|
|
|
[=] { scrollToAnimationCallback(attachToId, relativeFinish); },
|
2018-02-04 19:57:03 +00:00
|
|
|
relativeStart,
|
|
|
|
relativeFinish,
|
|
|
|
st::slideDuration,
|
|
|
|
transition);
|
|
|
|
}
|
|
|
|
|
2020-08-31 13:41:24 +00:00
|
|
|
bool ListWidget::animatedScrolling() const {
|
|
|
|
return _scrollToAnimation.animating();
|
|
|
|
}
|
|
|
|
|
2019-04-02 09:13:30 +00:00
|
|
|
void ListWidget::scrollToAnimationCallback(
|
|
|
|
FullMsgId attachToId,
|
|
|
|
int relativeTo) {
|
2020-09-15 15:30:34 +00:00
|
|
|
if (!attachToId) {
|
|
|
|
// Animated scroll to bottom.
|
2021-09-27 08:13:57 +00:00
|
|
|
const auto current = int(base::SafeRound(
|
|
|
|
_scrollToAnimation.value(0)));
|
2020-09-15 15:30:34 +00:00
|
|
|
_delegate->listScrollTo(height()
|
|
|
|
- (_visibleBottom - _visibleTop)
|
|
|
|
+ current);
|
|
|
|
return;
|
|
|
|
}
|
2019-07-24 11:13:51 +00:00
|
|
|
const auto attachTo = session().data().message(attachToId);
|
2018-02-04 19:57:03 +00:00
|
|
|
const auto attachToView = viewForItem(attachTo);
|
|
|
|
if (!attachToView) {
|
2019-04-02 09:13:30 +00:00
|
|
|
_scrollToAnimation.stop();
|
2018-02-04 19:57:03 +00:00
|
|
|
} else {
|
2021-09-27 08:13:57 +00:00
|
|
|
const auto current = int(base::SafeRound(_scrollToAnimation.value(
|
2019-04-02 09:13:30 +00:00
|
|
|
relativeTo)));
|
2018-02-04 19:57:03 +00:00
|
|
|
_delegate->listScrollTo(itemTop(attachToView) + current);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::isAbovePosition(Data::MessagePosition position) const {
|
2018-02-16 15:46:24 +00:00
|
|
|
if (_items.empty() || loadedAtBottom()) {
|
2018-02-04 19:57:03 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return _items.back()->data()->position() < position;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::isBelowPosition(Data::MessagePosition position) const {
|
2018-02-16 15:46:24 +00:00
|
|
|
if (_items.empty() || loadedAtTop()) {
|
2018-02-04 19:57:03 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return _items.front()->data()->position() > position;
|
2018-01-09 17:08:31 +00:00
|
|
|
}
|
|
|
|
|
2018-02-16 17:59:35 +00:00
|
|
|
void ListWidget::highlightMessage(FullMsgId itemId) {
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto item = session().data().message(itemId)) {
|
2018-02-16 17:59:35 +00:00
|
|
|
if (const auto view = viewForItem(item)) {
|
2019-02-19 06:57:53 +00:00
|
|
|
_highlightStart = crl::now();
|
2018-02-16 17:59:35 +00:00
|
|
|
_highlightedMessageId = itemId;
|
|
|
|
_highlightTimer.callEach(AnimationTimerDelta);
|
|
|
|
|
2020-12-30 08:55:11 +00:00
|
|
|
repaintHighlightedItem(view);
|
2018-02-16 17:59:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-31 12:14:32 +00:00
|
|
|
void ListWidget::showAroundPosition(
|
|
|
|
Data::MessagePosition position,
|
|
|
|
Fn<bool()> overrideInitialScroll) {
|
|
|
|
_aroundPosition = position;
|
|
|
|
_aroundIndex = -1;
|
|
|
|
_overrideInitialScroll = std::move(overrideInitialScroll);
|
|
|
|
refreshViewer();
|
|
|
|
}
|
|
|
|
|
2020-12-30 08:55:11 +00:00
|
|
|
void ListWidget::repaintHighlightedItem(not_null<const Element*> view) {
|
|
|
|
if (view->isHiddenByGroup()) {
|
|
|
|
if (const auto group = session().data().groups().find(view->data())) {
|
|
|
|
if (const auto leader = viewForItem(group->items.front())) {
|
|
|
|
if (!leader->isHiddenByGroup()) {
|
|
|
|
repaintItem(leader);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
repaintItem(view);
|
|
|
|
}
|
|
|
|
|
2018-02-16 17:59:35 +00:00
|
|
|
void ListWidget::updateHighlightedMessage() {
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto item = session().data().message(_highlightedMessageId)) {
|
2018-02-16 17:59:35 +00:00
|
|
|
if (const auto view = viewForItem(item)) {
|
2020-12-30 08:55:11 +00:00
|
|
|
repaintHighlightedItem(view);
|
2018-02-16 17:59:35 +00:00
|
|
|
auto duration = st::activeFadeInDuration + st::activeFadeOutDuration;
|
2019-02-19 06:57:53 +00:00
|
|
|
if (crl::now() - _highlightStart <= duration) {
|
2018-02-16 17:59:35 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_highlightTimer.cancel();
|
|
|
|
_highlightedMessageId = FullMsgId();
|
|
|
|
}
|
|
|
|
|
2021-01-31 20:49:25 +00:00
|
|
|
void ListWidget::clearHighlightedMessage() {
|
|
|
|
_highlightedMessageId = FullMsgId();
|
|
|
|
updateHighlightedMessage();
|
|
|
|
}
|
|
|
|
|
2018-02-02 12:51:18 +00:00
|
|
|
void ListWidget::checkUnreadBarCreation() {
|
2020-09-15 15:30:34 +00:00
|
|
|
if (!_bar.element) {
|
|
|
|
if (auto data = _delegate->listMessagesBar(_items); data.bar.element) {
|
|
|
|
_bar = std::move(data.bar);
|
|
|
|
_barText = std::move(data.text);
|
2021-08-30 08:27:08 +00:00
|
|
|
if (!_bar.hidden) {
|
|
|
|
_bar.element->createUnreadBar(_barText.value());
|
|
|
|
const auto i = ranges::find(_items, not_null{ _bar.element });
|
|
|
|
Assert(i != end(_items));
|
|
|
|
refreshAttachmentsAtIndex(i - begin(_items));
|
|
|
|
}
|
2018-02-02 12:51:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::saveScrollState() {
|
|
|
|
if (!_scrollTopState.item) {
|
|
|
|
_scrollTopState = countScrollState();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::restoreScrollState() {
|
2020-08-31 13:41:24 +00:00
|
|
|
if (_items.empty()) {
|
|
|
|
return;
|
|
|
|
} else if (_overrideInitialScroll
|
|
|
|
&& base::take(_overrideInitialScroll)()) {
|
|
|
|
_scrollTopState = ScrollTopState();
|
2021-08-30 08:41:59 +00:00
|
|
|
_scrollInited = true;
|
2018-01-09 17:08:31 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-02-02 12:51:18 +00:00
|
|
|
if (!_scrollTopState.item) {
|
2021-08-30 08:27:08 +00:00
|
|
|
if (!_bar.element || _bar.hidden || !_bar.focus || _scrollInited) {
|
2018-02-02 12:51:18 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-09-29 15:05:38 +00:00
|
|
|
_scrollInited = true;
|
2020-09-15 15:30:34 +00:00
|
|
|
_scrollTopState.item = _bar.element->data()->position();
|
2018-02-02 12:51:18 +00:00
|
|
|
_scrollTopState.shift = st::lineWidth + st::historyUnreadBarMargin;
|
|
|
|
}
|
2018-01-09 17:08:31 +00:00
|
|
|
const auto index = findNearestItem(_scrollTopState.item);
|
|
|
|
if (index >= 0) {
|
2018-01-10 13:13:33 +00:00
|
|
|
const auto view = _items[index];
|
|
|
|
auto newVisibleTop = itemTop(view) + _scrollTopState.shift;
|
2018-01-09 17:08:31 +00:00
|
|
|
if (_visibleTop != newVisibleTop) {
|
|
|
|
_delegate->listScrollTo(newVisibleTop);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_scrollTopState = ScrollTopState();
|
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
Element *ListWidget::viewForItem(FullMsgId itemId) const {
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto item = session().data().message(itemId)) {
|
2018-01-26 15:40:11 +00:00
|
|
|
return viewForItem(item);
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2018-01-11 19:33:26 +00:00
|
|
|
Element *ListWidget::viewForItem(const HistoryItem *item) const {
|
2018-01-10 13:13:33 +00:00
|
|
|
if (item) {
|
|
|
|
if (const auto i = _views.find(item); i != _views.end()) {
|
|
|
|
return i->second.get();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2018-01-11 19:33:26 +00:00
|
|
|
not_null<Element*> ListWidget::enforceViewForItem(
|
2018-01-10 13:13:33 +00:00
|
|
|
not_null<HistoryItem*> item) {
|
|
|
|
if (const auto view = viewForItem(item)) {
|
|
|
|
return view;
|
|
|
|
}
|
|
|
|
const auto [i, ok] = _views.emplace(
|
|
|
|
item,
|
2018-01-21 14:49:42 +00:00
|
|
|
item->createView(this));
|
2018-01-10 13:13:33 +00:00
|
|
|
return i->second.get();
|
|
|
|
}
|
|
|
|
|
2020-10-02 16:26:04 +00:00
|
|
|
void ListWidget::updateAroundPositionFromNearest(int nearestIndex) {
|
2020-09-04 16:11:36 +00:00
|
|
|
if (nearestIndex < 0) {
|
|
|
|
_aroundIndex = -1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto isGoodIndex = [&](int index) {
|
|
|
|
Expects(index >= 0 && index < _items.size());
|
|
|
|
|
|
|
|
return _delegate->listIsGoodForAroundPosition(_items[index]);
|
|
|
|
};
|
|
|
|
_aroundIndex = [&] {
|
|
|
|
for (auto index = nearestIndex; index < _items.size(); ++index) {
|
|
|
|
if (isGoodIndex(index)) {
|
|
|
|
return index;
|
|
|
|
}
|
2018-02-05 20:19:51 +00:00
|
|
|
}
|
2020-09-04 16:11:36 +00:00
|
|
|
for (auto index = nearestIndex; index != 0;) {
|
|
|
|
if (isGoodIndex(--index)) {
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}();
|
|
|
|
if (_aroundIndex < 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto newPosition = _items[_aroundIndex]->data()->position();
|
|
|
|
if (_aroundPosition != newPosition) {
|
|
|
|
_aroundPosition = newPosition;
|
|
|
|
crl::on_main(this, [=] { refreshViewer(); });
|
2018-01-09 17:08:31 +00:00
|
|
|
}
|
|
|
|
}
|
2017-07-05 18:11:31 +00:00
|
|
|
|
2020-09-15 18:19:06 +00:00
|
|
|
Element *ListWidget::viewByPosition(Data::MessagePosition position) const {
|
|
|
|
const auto index = findNearestItem(position);
|
|
|
|
return (index < 0 || _items[index]->data()->position() != position)
|
|
|
|
? nullptr
|
|
|
|
: _items[index].get();
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
int ListWidget::findNearestItem(Data::MessagePosition position) const {
|
|
|
|
if (_items.empty()) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
const auto after = ranges::find_if(
|
|
|
|
_items,
|
2018-01-11 19:33:26 +00:00
|
|
|
[&](not_null<Element*> view) {
|
2018-01-10 13:13:33 +00:00
|
|
|
return (view->data()->position() >= position);
|
2018-01-09 17:08:31 +00:00
|
|
|
});
|
|
|
|
return (after == end(_items))
|
|
|
|
? int(_items.size() - 1)
|
|
|
|
: int(after - begin(_items));
|
2017-06-18 11:08:14 +00:00
|
|
|
}
|
|
|
|
|
2018-02-02 12:51:18 +00:00
|
|
|
HistoryItemsList ListWidget::collectVisibleItems() const {
|
|
|
|
auto result = HistoryItemsList();
|
|
|
|
const auto from = std::lower_bound(
|
|
|
|
begin(_items),
|
|
|
|
end(_items),
|
|
|
|
_visibleTop,
|
|
|
|
[this](auto &elem, int top) {
|
|
|
|
return this->itemTop(elem) + elem->height() <= top;
|
|
|
|
});
|
|
|
|
const auto to = std::lower_bound(
|
|
|
|
begin(_items),
|
|
|
|
end(_items),
|
|
|
|
_visibleBottom,
|
|
|
|
[this](auto &elem, int bottom) {
|
|
|
|
return this->itemTop(elem) < bottom;
|
|
|
|
});
|
|
|
|
result.reserve(to - from);
|
|
|
|
for (auto i = from; i != to; ++i) {
|
|
|
|
result.push_back((*i)->data());
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::visibleTopBottomUpdated(
|
2017-09-13 16:57:44 +00:00
|
|
|
int visibleTop,
|
|
|
|
int visibleBottom) {
|
2018-02-02 12:51:18 +00:00
|
|
|
if (!(visibleTop < visibleBottom)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto initializing = !(_visibleTop < _visibleBottom);
|
|
|
|
const auto scrolledUp = (visibleTop < _visibleTop);
|
2017-06-18 11:08:14 +00:00
|
|
|
_visibleTop = visibleTop;
|
|
|
|
_visibleBottom = visibleBottom;
|
|
|
|
|
2020-05-28 14:32:10 +00:00
|
|
|
// Unload userpics.
|
|
|
|
if (_userpics.size() > kClearUserpicsAfter) {
|
|
|
|
_userpicsCache = std::move(_userpics);
|
|
|
|
}
|
|
|
|
|
2018-02-02 12:51:18 +00:00
|
|
|
if (initializing) {
|
|
|
|
checkUnreadBarCreation();
|
|
|
|
}
|
2017-06-18 13:08:49 +00:00
|
|
|
updateVisibleTopItem();
|
2017-06-22 01:31:02 +00:00
|
|
|
if (scrolledUp) {
|
|
|
|
_scrollDateCheck.call();
|
|
|
|
} else {
|
|
|
|
scrollDateHideByTimer();
|
|
|
|
}
|
2020-06-25 14:17:37 +00:00
|
|
|
_controller->floatPlayerAreaUpdated();
|
2018-02-02 12:51:18 +00:00
|
|
|
_applyUpdatedScrollState.call();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::applyUpdatedScrollState() {
|
|
|
|
checkMoveToOtherViewer();
|
|
|
|
_delegate->listVisibleItemsChanged(collectVisibleItems());
|
2017-06-18 11:08:14 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::updateVisibleTopItem() {
|
2017-06-23 19:28:42 +00:00
|
|
|
if (_visibleBottom == height()) {
|
2017-06-18 13:08:49 +00:00
|
|
|
_visibleTopItem = nullptr;
|
2018-01-09 17:08:31 +00:00
|
|
|
} else if (_items.empty()) {
|
|
|
|
_visibleTopItem = nullptr;
|
|
|
|
_visibleTopFromItem = _visibleTop;
|
2017-06-23 19:28:42 +00:00
|
|
|
} else {
|
2018-01-09 17:08:31 +00:00
|
|
|
_visibleTopItem = findItemByY(_visibleTop);
|
|
|
|
_visibleTopFromItem = _visibleTop - itemTop(_visibleTopItem);
|
2017-06-18 13:08:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
bool ListWidget::displayScrollDate() const {
|
2017-06-21 21:38:31 +00:00
|
|
|
return (_visibleTop <= height() - 2 * (_visibleBottom - _visibleTop));
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::scrollDateCheck() {
|
2017-06-21 21:38:31 +00:00
|
|
|
if (!_visibleTopItem) {
|
|
|
|
_scrollDateLastItem = nullptr;
|
|
|
|
_scrollDateLastItemTop = 0;
|
|
|
|
scrollDateHide();
|
|
|
|
} else if (_visibleTopItem != _scrollDateLastItem || _visibleTopFromItem != _scrollDateLastItemTop) {
|
|
|
|
// Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem).
|
|
|
|
if (_scrollDateLastItem && !_scrollDateShown) {
|
|
|
|
toggleScrollDateShown();
|
|
|
|
}
|
|
|
|
_scrollDateLastItem = _visibleTopItem;
|
|
|
|
_scrollDateLastItemTop = _visibleTopFromItem;
|
2021-05-23 00:00:24 +00:00
|
|
|
_scrollDateHideTimer.callOnce(st::historyScrollDateHideTimeout);
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::scrollDateHideByTimer() {
|
2017-06-21 21:38:31 +00:00
|
|
|
_scrollDateHideTimer.cancel();
|
2018-02-21 23:59:56 +00:00
|
|
|
if (!_scrollDateLink || ClickHandler::getPressed() != _scrollDateLink) {
|
|
|
|
scrollDateHide();
|
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::scrollDateHide() {
|
2017-06-21 21:38:31 +00:00
|
|
|
if (_scrollDateShown) {
|
|
|
|
toggleScrollDateShown();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-21 23:59:56 +00:00
|
|
|
void ListWidget::keepScrollDateForNow() {
|
|
|
|
if (!_scrollDateShown
|
|
|
|
&& _scrollDateLastItem
|
|
|
|
&& _scrollDateOpacity.animating()) {
|
|
|
|
toggleScrollDateShown();
|
|
|
|
}
|
2021-05-23 00:00:24 +00:00
|
|
|
_scrollDateHideTimer.callOnce(st::historyScrollDateHideTimeout);
|
2018-02-21 23:59:56 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::toggleScrollDateShown() {
|
2017-06-21 21:38:31 +00:00
|
|
|
_scrollDateShown = !_scrollDateShown;
|
|
|
|
auto from = _scrollDateShown ? 0. : 1.;
|
|
|
|
auto to = _scrollDateShown ? 1. : 0.;
|
|
|
|
_scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration);
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::repaintScrollDateCallback() {
|
2017-06-21 21:38:31 +00:00
|
|
|
auto updateTop = _visibleTop;
|
|
|
|
auto updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();
|
|
|
|
update(0, updateTop, width(), updateHeight);
|
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
auto ListWidget::collectSelectedItems() const -> SelectedItems {
|
|
|
|
auto transformation = [&](const auto &item) {
|
|
|
|
const auto [itemId, selection] = item;
|
|
|
|
auto result = SelectedItem(itemId);
|
|
|
|
result.canDelete = selection.canDelete;
|
|
|
|
result.canForward = selection.canForward;
|
2019-08-09 17:58:58 +00:00
|
|
|
result.canSendNow = selection.canSendNow;
|
2018-01-26 15:40:11 +00:00
|
|
|
return result;
|
|
|
|
};
|
|
|
|
auto items = SelectedItems();
|
|
|
|
if (hasSelectedItems()) {
|
|
|
|
items.reserve(_selected.size());
|
|
|
|
std::transform(
|
|
|
|
_selected.begin(),
|
|
|
|
_selected.end(),
|
|
|
|
std::back_inserter(items),
|
|
|
|
transformation);
|
|
|
|
}
|
|
|
|
return items;
|
|
|
|
}
|
|
|
|
|
|
|
|
MessageIdsList ListWidget::collectSelectedIds() const {
|
|
|
|
const auto selected = collectSelectedItems();
|
2021-03-13 12:12:08 +00:00
|
|
|
return ranges::views::all(
|
2018-01-26 15:40:11 +00:00
|
|
|
selected
|
2021-03-13 12:12:08 +00:00
|
|
|
) | ranges::views::transform([](const SelectedItem &item) {
|
2018-01-26 15:40:11 +00:00
|
|
|
return item.msgId;
|
|
|
|
}) | ranges::to_vector;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::pushSelectedItems() {
|
|
|
|
_delegate->listSelectionChanged(collectSelectedItems());
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::removeItemSelection(
|
|
|
|
const SelectedMap::const_iterator &i) {
|
|
|
|
Expects(i != _selected.cend());
|
|
|
|
|
|
|
|
_selected.erase(i);
|
|
|
|
if (_selected.empty()) {
|
|
|
|
update();
|
|
|
|
}
|
|
|
|
pushSelectedItems();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::hasSelectedText() const {
|
|
|
|
return (_selectedTextItem != nullptr) && !hasSelectedItems();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::hasSelectedItems() const {
|
|
|
|
return !_selected.empty();
|
|
|
|
}
|
|
|
|
|
2018-01-27 13:59:24 +00:00
|
|
|
bool ListWidget::overSelectedItems() const {
|
|
|
|
if (_overState.pointState == PointState::GroupPart) {
|
|
|
|
return _overItemExact
|
|
|
|
&& _selected.contains(_overItemExact->fullId());
|
|
|
|
} else if (_overState.pointState == PointState::Inside) {
|
|
|
|
return _overElement
|
|
|
|
&& isSelectedAsGroup(_selected, _overElement->data());
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-01-27 17:26:24 +00:00
|
|
|
bool ListWidget::isSelectedGroup(
|
|
|
|
const SelectedMap &applyTo,
|
|
|
|
not_null<const Data::Group*> group) const {
|
2021-09-08 10:53:54 +00:00
|
|
|
for (const auto &other : group->items) {
|
2018-01-27 17:26:24 +00:00
|
|
|
if (!applyTo.contains(other->fullId())) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
2018-01-27 13:59:24 +00:00
|
|
|
|
|
|
|
bool ListWidget::isSelectedAsGroup(
|
|
|
|
const SelectedMap &applyTo,
|
|
|
|
not_null<HistoryItem*> item) const {
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto group = session().data().groups().find(item)) {
|
2018-01-27 17:26:24 +00:00
|
|
|
return isSelectedGroup(applyTo, group);
|
2018-01-27 13:59:24 +00:00
|
|
|
}
|
|
|
|
return applyTo.contains(item->fullId());
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::isGoodForSelection(
|
2018-01-26 15:40:11 +00:00
|
|
|
SelectedMap &applyTo,
|
2018-01-27 13:59:24 +00:00
|
|
|
not_null<HistoryItem*> item,
|
|
|
|
int &totalCount) const {
|
2019-08-08 22:39:42 +00:00
|
|
|
if (!_delegate->listIsItemGoodForSelection(item)) {
|
2018-01-26 15:40:11 +00:00
|
|
|
return false;
|
2018-01-27 13:59:24 +00:00
|
|
|
} else if (!applyTo.contains(item->fullId())) {
|
|
|
|
++totalCount;
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
2018-01-27 13:59:24 +00:00
|
|
|
return (totalCount <= MaxSelectedItems);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::addToSelection(
|
|
|
|
SelectedMap &applyTo,
|
|
|
|
not_null<HistoryItem*> item) const {
|
|
|
|
const auto itemId = item->fullId();
|
2018-01-26 15:40:11 +00:00
|
|
|
auto [iterator, ok] = applyTo.try_emplace(
|
|
|
|
itemId,
|
|
|
|
SelectionData());
|
|
|
|
if (!ok) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
iterator->second.canDelete = item->canDelete();
|
|
|
|
iterator->second.canForward = item->allowsForward();
|
2019-08-09 17:58:58 +00:00
|
|
|
iterator->second.canSendNow = item->allowsSendNow();
|
2018-01-26 15:40:11 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-01-27 13:59:24 +00:00
|
|
|
bool ListWidget::removeFromSelection(
|
|
|
|
SelectedMap &applyTo,
|
|
|
|
FullMsgId itemId) const {
|
|
|
|
return applyTo.remove(itemId);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::changeSelection(
|
|
|
|
SelectedMap &applyTo,
|
|
|
|
not_null<HistoryItem*> item,
|
|
|
|
SelectAction action) const {
|
|
|
|
const auto itemId = item->fullId();
|
|
|
|
if (action == SelectAction::Invert) {
|
|
|
|
action = applyTo.contains(itemId)
|
|
|
|
? SelectAction::Deselect
|
|
|
|
: SelectAction::Select;
|
|
|
|
}
|
|
|
|
if (action == SelectAction::Select) {
|
|
|
|
auto already = int(applyTo.size());
|
|
|
|
if (isGoodForSelection(applyTo, item, already)) {
|
|
|
|
addToSelection(applyTo, item);
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
2018-01-27 13:59:24 +00:00
|
|
|
} else {
|
|
|
|
removeFromSelection(applyTo, itemId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::changeSelectionAsGroup(
|
|
|
|
SelectedMap &applyTo,
|
|
|
|
not_null<HistoryItem*> item,
|
|
|
|
SelectAction action) const {
|
2019-07-24 11:13:51 +00:00
|
|
|
const auto group = session().data().groups().find(item);
|
2018-01-27 13:59:24 +00:00
|
|
|
if (!group) {
|
|
|
|
return changeSelection(applyTo, item, action);
|
|
|
|
}
|
|
|
|
if (action == SelectAction::Invert) {
|
|
|
|
action = isSelectedAsGroup(applyTo, item)
|
|
|
|
? SelectAction::Deselect
|
|
|
|
: SelectAction::Select;
|
|
|
|
}
|
|
|
|
auto already = int(applyTo.size());
|
|
|
|
const auto canSelect = [&] {
|
2021-09-08 10:53:54 +00:00
|
|
|
for (const auto &other : group->items) {
|
2018-01-27 13:59:24 +00:00
|
|
|
if (!isGoodForSelection(applyTo, other, already)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}();
|
|
|
|
if (action == SelectAction::Select && canSelect) {
|
2021-09-08 10:53:54 +00:00
|
|
|
for (const auto &other : group->items) {
|
2018-01-27 13:59:24 +00:00
|
|
|
addToSelection(applyTo, other);
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
} else {
|
2021-09-08 10:53:54 +00:00
|
|
|
for (const auto &other : group->items) {
|
2018-01-27 13:59:24 +00:00
|
|
|
removeFromSelection(applyTo, other->fullId());
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::isItemUnderPressSelected() const {
|
|
|
|
return itemUnderPressSelection() != _selected.end();
|
|
|
|
}
|
|
|
|
|
|
|
|
auto ListWidget::itemUnderPressSelection() -> SelectedMap::iterator {
|
2018-01-27 13:59:24 +00:00
|
|
|
return (_pressState.itemId
|
|
|
|
&& _pressState.pointState != PointState::Outside)
|
2018-01-26 15:40:11 +00:00
|
|
|
? _selected.find(_pressState.itemId)
|
|
|
|
: _selected.end();
|
|
|
|
}
|
|
|
|
|
2018-01-27 19:21:41 +00:00
|
|
|
bool ListWidget::isInsideSelection(
|
|
|
|
not_null<const Element*> view,
|
|
|
|
not_null<HistoryItem*> exactItem,
|
|
|
|
const MouseState &state) const {
|
|
|
|
if (!_selected.empty()) {
|
|
|
|
if (state.pointState == PointState::GroupPart) {
|
|
|
|
return _selected.contains(exactItem->fullId());
|
|
|
|
} else {
|
|
|
|
return isSelectedAsGroup(_selected, view->data());
|
|
|
|
}
|
|
|
|
} else if (_selectedTextItem
|
|
|
|
&& _selectedTextItem == view->data()
|
|
|
|
&& state.pointState != PointState::Outside) {
|
|
|
|
StateRequest stateRequest;
|
2019-06-12 13:26:04 +00:00
|
|
|
stateRequest.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
|
2018-01-27 19:21:41 +00:00
|
|
|
const auto dragState = view->textState(
|
|
|
|
state.point,
|
|
|
|
stateRequest);
|
|
|
|
if (dragState.cursor == CursorState::Text
|
|
|
|
&& base::in_range(
|
|
|
|
dragState.symbol,
|
|
|
|
_selectedTextRange.from,
|
|
|
|
_selectedTextRange.to)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
auto ListWidget::itemUnderPressSelection() const
|
|
|
|
-> SelectedMap::const_iterator {
|
2018-01-27 13:59:24 +00:00
|
|
|
return (_pressState.itemId
|
|
|
|
&& _pressState.pointState != PointState::Outside)
|
2018-01-26 15:40:11 +00:00
|
|
|
? _selected.find(_pressState.itemId)
|
|
|
|
: _selected.end();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::requiredToStartDragging(
|
|
|
|
not_null<Element*> view) const {
|
2018-01-27 13:59:24 +00:00
|
|
|
if (_mouseCursorState == CursorState::Date) {
|
2018-01-26 15:40:11 +00:00
|
|
|
return true;
|
2019-08-02 15:06:44 +00:00
|
|
|
} else if (const auto media = view->media()) {
|
|
|
|
if (media->dragItem()) {
|
|
|
|
return true;
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-01-27 13:59:24 +00:00
|
|
|
bool ListWidget::isPressInSelectedText(TextState state) const {
|
|
|
|
if (state.cursor != CursorState::Text) {
|
2018-01-26 15:40:11 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (!hasSelectedText()
|
|
|
|
|| !_selectedTextItem
|
|
|
|
|| _selectedTextItem->fullId() != _pressState.itemId) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
auto from = _selectedTextRange.from;
|
|
|
|
auto to = _selectedTextRange.to;
|
|
|
|
return (state.symbol >= from && state.symbol < to);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::cancelSelection() {
|
|
|
|
clearSelected();
|
|
|
|
clearTextSelection();
|
|
|
|
}
|
|
|
|
|
2018-01-28 15:08:34 +00:00
|
|
|
void ListWidget::selectItem(not_null<HistoryItem*> item) {
|
|
|
|
if (const auto view = viewForItem(item)) {
|
|
|
|
clearTextSelection();
|
|
|
|
changeSelection(
|
|
|
|
_selected,
|
|
|
|
item,
|
|
|
|
SelectAction::Select);
|
|
|
|
pushSelectedItems();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::selectItemAsGroup(not_null<HistoryItem*> item) {
|
|
|
|
if (const auto view = viewForItem(item)) {
|
|
|
|
clearTextSelection();
|
|
|
|
changeSelectionAsGroup(
|
|
|
|
_selected,
|
|
|
|
item,
|
|
|
|
SelectAction::Select);
|
|
|
|
pushSelectedItems();
|
2020-05-18 18:35:06 +00:00
|
|
|
update();
|
2018-01-28 15:08:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
void ListWidget::clearSelected() {
|
|
|
|
if (_selected.empty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (hasSelectedText()) {
|
|
|
|
repaintItem(_selected.begin()->first);
|
|
|
|
_selected.clear();
|
|
|
|
} else {
|
|
|
|
_selected.clear();
|
|
|
|
pushSelectedItems();
|
|
|
|
update();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::clearTextSelection() {
|
|
|
|
if (_selectedTextItem) {
|
|
|
|
if (const auto view = viewForItem(_selectedTextItem)) {
|
|
|
|
repaintItem(view);
|
|
|
|
}
|
|
|
|
_selectedTextItem = nullptr;
|
|
|
|
_selectedTextRange = TextSelection();
|
2019-04-08 15:10:06 +00:00
|
|
|
_selectedText = TextForMimeData();
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::setTextSelection(
|
|
|
|
not_null<Element*> view,
|
|
|
|
TextSelection selection) {
|
|
|
|
clearSelected();
|
|
|
|
const auto item = view->data();
|
|
|
|
if (_selectedTextItem != item) {
|
|
|
|
clearTextSelection();
|
|
|
|
_selectedTextItem = view->data();
|
|
|
|
}
|
|
|
|
_selectedTextRange = selection;
|
|
|
|
_selectedText = (selection.from != selection.to)
|
|
|
|
? view->selectedText(selection)
|
2019-04-08 15:10:06 +00:00
|
|
|
: TextForMimeData();
|
2018-01-26 15:40:11 +00:00
|
|
|
repaintItem(view);
|
2019-04-08 15:10:06 +00:00
|
|
|
if (!_wasSelectedText && !_selectedText.empty()) {
|
2018-01-26 15:40:11 +00:00
|
|
|
_wasSelectedText = true;
|
|
|
|
setFocus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-04 19:57:03 +00:00
|
|
|
bool ListWidget::loadedAtTopKnown() const {
|
|
|
|
return !!_slice.skippedBefore;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::loadedAtTop() const {
|
|
|
|
return _slice.skippedBefore && (*_slice.skippedBefore == 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::loadedAtBottomKnown() const {
|
|
|
|
return !!_slice.skippedAfter;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ListWidget::loadedAtBottom() const {
|
|
|
|
return _slice.skippedAfter && (*_slice.skippedAfter == 0);
|
|
|
|
}
|
|
|
|
|
2018-02-05 20:19:51 +00:00
|
|
|
bool ListWidget::isEmpty() const {
|
2021-07-19 17:01:39 +00:00
|
|
|
return loadedAtTop()
|
|
|
|
&& loadedAtBottom()
|
|
|
|
&& (_itemsHeight + _itemsRevealHeight == 0);
|
2018-02-05 20:19:51 +00:00
|
|
|
}
|
|
|
|
|
2018-01-30 12:13:46 +00:00
|
|
|
int ListWidget::itemMinimalHeight() const {
|
|
|
|
return st::msgMarginTopAttached
|
|
|
|
+ st::msgPhotoSize
|
|
|
|
+ st::msgMargin.bottom();
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::checkMoveToOtherViewer() {
|
|
|
|
auto visibleHeight = (_visibleBottom - _visibleTop);
|
|
|
|
if (width() <= 0
|
|
|
|
|| visibleHeight <= 0
|
|
|
|
|| _items.empty()
|
|
|
|
|| _aroundIndex < 0
|
|
|
|
|| _scrollTopState.item) {
|
|
|
|
return;
|
2017-07-05 13:11:08 +00:00
|
|
|
}
|
2017-11-20 19:54:05 +00:00
|
|
|
|
2020-09-04 16:11:36 +00:00
|
|
|
auto topItemIndex = findItemIndexByY(_visibleTop);
|
|
|
|
auto bottomItemIndex = findItemIndexByY(_visibleBottom);
|
2018-01-09 17:08:31 +00:00
|
|
|
auto preloadedHeight = kPreloadedScreensCountFull * visibleHeight;
|
2018-01-30 12:13:46 +00:00
|
|
|
auto preloadedCount = preloadedHeight / _itemAverageHeight;
|
2018-01-09 17:08:31 +00:00
|
|
|
auto preloadIdsLimitMin = (preloadedCount / 2) + 1;
|
|
|
|
auto preloadIdsLimit = preloadIdsLimitMin
|
2018-01-30 12:13:46 +00:00
|
|
|
+ (visibleHeight / _itemAverageHeight);
|
2018-01-09 17:08:31 +00:00
|
|
|
|
|
|
|
auto preloadBefore = kPreloadIfLessThanScreens * visibleHeight;
|
|
|
|
auto before = _slice.skippedBefore;
|
2018-01-29 17:13:24 +00:00
|
|
|
auto preloadTop = (_visibleTop < preloadBefore);
|
|
|
|
auto topLoaded = before && (*before == 0);
|
|
|
|
auto after = _slice.skippedAfter;
|
2018-01-09 17:08:31 +00:00
|
|
|
auto preloadBottom = (height() - _visibleBottom < preloadBefore);
|
2018-01-29 17:13:24 +00:00
|
|
|
auto bottomLoaded = after && (*after == 0);
|
2018-01-09 17:08:31 +00:00
|
|
|
|
|
|
|
auto minScreenDelta = kPreloadedScreensCount
|
|
|
|
- kPreloadIfLessThanScreens;
|
|
|
|
auto minUniversalIdDelta = (minScreenDelta * visibleHeight)
|
2018-01-30 12:13:46 +00:00
|
|
|
/ _itemAverageHeight;
|
2020-09-04 16:11:36 +00:00
|
|
|
const auto preloadAroundMessage = [&](int index) {
|
|
|
|
Expects(index >= 0 && index < _items.size());
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
auto preloadRequired = false;
|
2020-09-04 16:11:36 +00:00
|
|
|
auto itemPosition = _items[index]->data()->position();
|
2018-01-09 17:08:31 +00:00
|
|
|
|
|
|
|
if (!preloadRequired) {
|
|
|
|
preloadRequired = (_idsLimit < preloadIdsLimitMin);
|
2017-07-05 18:11:31 +00:00
|
|
|
}
|
2018-01-09 17:08:31 +00:00
|
|
|
if (!preloadRequired) {
|
|
|
|
Assert(_aroundIndex >= 0);
|
2020-09-04 16:11:36 +00:00
|
|
|
auto delta = std::abs(index - _aroundIndex);
|
2018-01-09 17:08:31 +00:00
|
|
|
preloadRequired = (delta >= minUniversalIdDelta);
|
2017-07-05 18:11:31 +00:00
|
|
|
}
|
2018-01-09 17:08:31 +00:00
|
|
|
if (preloadRequired) {
|
|
|
|
_idsLimit = preloadIdsLimit;
|
|
|
|
_aroundPosition = itemPosition;
|
2020-09-04 16:11:36 +00:00
|
|
|
_aroundIndex = index;
|
2018-01-09 17:08:31 +00:00
|
|
|
refreshViewer();
|
|
|
|
}
|
|
|
|
};
|
2017-07-05 18:11:31 +00:00
|
|
|
|
2020-09-04 16:11:36 +00:00
|
|
|
const auto findGoodAbove = [&](int index) {
|
|
|
|
Expects(index >= 0 && index < _items.size());
|
|
|
|
|
|
|
|
for (; index != _items.size(); ++index) {
|
|
|
|
if (_delegate->listIsGoodForAroundPosition(_items[index])) {
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
};
|
|
|
|
const auto findGoodBelow = [&](int index) {
|
|
|
|
Expects(index >= 0 && index < _items.size());
|
|
|
|
|
|
|
|
for (++index; index != 0;) {
|
|
|
|
if (_delegate->listIsGoodForAroundPosition(_items[--index])) {
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
};
|
2018-01-09 17:08:31 +00:00
|
|
|
if (preloadTop && !topLoaded) {
|
2020-09-04 16:11:36 +00:00
|
|
|
const auto goodAboveIndex = findGoodAbove(topItemIndex);
|
|
|
|
const auto goodIndex = (goodAboveIndex >= 0)
|
|
|
|
? goodAboveIndex
|
|
|
|
: findGoodBelow(topItemIndex);
|
|
|
|
if (goodIndex >= 0) {
|
|
|
|
preloadAroundMessage(goodIndex);
|
|
|
|
}
|
2018-01-09 17:08:31 +00:00
|
|
|
} else if (preloadBottom && !bottomLoaded) {
|
2020-09-04 16:11:36 +00:00
|
|
|
const auto goodBelowIndex = findGoodBelow(bottomItemIndex);
|
|
|
|
const auto goodIndex = (goodBelowIndex >= 0)
|
|
|
|
? goodBelowIndex
|
|
|
|
: findGoodAbove(bottomItemIndex);
|
|
|
|
if (goodIndex >= 0) {
|
|
|
|
preloadAroundMessage(goodIndex);
|
|
|
|
}
|
2017-07-05 18:11:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
QString ListWidget::tooltipText() const {
|
2018-01-27 13:59:24 +00:00
|
|
|
const auto item = (_overElement && _mouseAction == MouseAction::None)
|
|
|
|
? _overElement->data().get()
|
2018-01-26 15:40:11 +00:00
|
|
|
: nullptr;
|
2018-01-27 13:59:24 +00:00
|
|
|
if (_mouseCursorState == CursorState::Date && item) {
|
2021-01-22 02:05:24 +00:00
|
|
|
return HistoryView::DateTooltipText(_overElement);
|
2018-01-27 13:59:24 +00:00
|
|
|
} else if (_mouseCursorState == CursorState::Forwarded && item) {
|
2018-01-26 15:40:11 +00:00
|
|
|
if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
|
2019-04-08 11:53:08 +00:00
|
|
|
return forwarded->text.toString();
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
} else if (const auto link = ClickHandler::getActive()) {
|
|
|
|
return link->tooltip();
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
return QString();
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
QPoint ListWidget::tooltipPos() const {
|
2017-06-21 21:38:31 +00:00
|
|
|
return _mousePosition;
|
|
|
|
}
|
|
|
|
|
2019-09-16 11:14:06 +00:00
|
|
|
bool ListWidget::tooltipWindowActive() const {
|
2019-12-31 13:48:44 +00:00
|
|
|
return Ui::AppInFocus() && Ui::InFocusChain(window());
|
2019-09-16 11:14:06 +00:00
|
|
|
}
|
|
|
|
|
2018-01-21 14:49:42 +00:00
|
|
|
Context ListWidget::elementContext() {
|
|
|
|
return _delegate->listContext();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_ptr<Element> ListWidget::elementCreate(
|
2020-06-16 16:53:44 +00:00
|
|
|
not_null<HistoryMessage*> message,
|
|
|
|
Element *replacing) {
|
|
|
|
return std::make_unique<Message>(this, message, replacing);
|
2018-01-21 14:49:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_ptr<Element> ListWidget::elementCreate(
|
2020-06-16 16:53:44 +00:00
|
|
|
not_null<HistoryService*> message,
|
|
|
|
Element *replacing) {
|
|
|
|
return std::make_unique<Service>(this, message, replacing);
|
2018-01-21 14:49:42 +00:00
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
bool ListWidget::elementUnderCursor(
|
|
|
|
not_null<const HistoryView::Element*> view) {
|
2018-01-27 13:59:24 +00:00
|
|
|
return (_overElement == view);
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
|
2019-02-19 06:57:53 +00:00
|
|
|
crl::time ListWidget::elementHighlightTime(
|
2020-12-30 08:55:11 +00:00
|
|
|
not_null<const HistoryItem*> item) {
|
|
|
|
if (item->fullId() == _highlightedMessageId) {
|
2018-02-16 17:59:35 +00:00
|
|
|
if (_highlightTimer.isActive()) {
|
2019-02-19 06:57:53 +00:00
|
|
|
return crl::now() - _highlightStart;
|
2018-02-16 17:59:35 +00:00
|
|
|
}
|
|
|
|
}
|
2019-02-19 06:57:53 +00:00
|
|
|
return crl::time(0);
|
2018-02-16 17:59:35 +00:00
|
|
|
}
|
|
|
|
|
2018-03-08 21:21:27 +00:00
|
|
|
bool ListWidget::elementInSelectionMode() {
|
|
|
|
return hasSelectedItems() || !_dragSelected.empty();
|
|
|
|
}
|
|
|
|
|
2019-05-14 09:50:44 +00:00
|
|
|
bool ListWidget::elementIntersectsRange(
|
|
|
|
not_null<const Element*> view,
|
|
|
|
int from,
|
|
|
|
int till) {
|
|
|
|
Expects(view->delegate() == this);
|
|
|
|
|
|
|
|
const auto top = itemTop(view);
|
|
|
|
const auto bottom = top + view->height();
|
|
|
|
return (top < till && bottom > from);
|
|
|
|
}
|
|
|
|
|
2019-08-01 12:55:14 +00:00
|
|
|
void ListWidget::elementStartStickerLoop(not_null<const Element*> view) {
|
2019-08-01 11:42:24 +00:00
|
|
|
}
|
|
|
|
|
2020-01-14 11:55:18 +00:00
|
|
|
void ListWidget::elementShowPollResults(
|
2020-11-02 06:55:02 +00:00
|
|
|
not_null<PollData*> poll,
|
|
|
|
FullMsgId context) {
|
|
|
|
_controller->showPollResults(poll, context);
|
2020-01-14 11:55:18 +00:00
|
|
|
}
|
|
|
|
|
2021-06-16 20:24:35 +00:00
|
|
|
void ListWidget::elementOpenPhoto(
|
|
|
|
not_null<PhotoData*> photo,
|
|
|
|
FullMsgId context) {
|
|
|
|
_controller->openPhoto(photo, context);
|
|
|
|
}
|
|
|
|
|
2021-06-16 21:31:15 +00:00
|
|
|
void ListWidget::elementOpenDocument(
|
|
|
|
not_null<DocumentData*> document,
|
|
|
|
FullMsgId context,
|
|
|
|
bool showInMediaView) {
|
|
|
|
_controller->openDocument(document, context, showInMediaView);
|
|
|
|
}
|
|
|
|
|
2021-06-18 06:20:49 +00:00
|
|
|
void ListWidget::elementCancelUpload(const FullMsgId &context) {
|
|
|
|
if (const auto item = session().data().message(context)) {
|
2021-06-18 06:39:10 +00:00
|
|
|
_controller->cancelUploadLayer(item);
|
2021-06-18 06:20:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-30 10:11:05 +00:00
|
|
|
void ListWidget::elementShowTooltip(
|
|
|
|
const TextWithEntities &text,
|
|
|
|
Fn<void()> hiddenCallback) {
|
2020-04-29 12:36:51 +00:00
|
|
|
}
|
|
|
|
|
2020-06-23 17:21:58 +00:00
|
|
|
bool ListWidget::elementIsGifPaused() {
|
|
|
|
return _controller->isGifPausedAtLeastFor(Window::GifPauseReason::Any);
|
|
|
|
}
|
|
|
|
|
2020-08-28 12:48:54 +00:00
|
|
|
bool ListWidget::elementHideReply(not_null<const Element*> view) {
|
|
|
|
return _delegate->listElementHideReply(view);
|
|
|
|
}
|
|
|
|
|
2020-09-22 11:30:15 +00:00
|
|
|
bool ListWidget::elementShownUnread(not_null<const Element*> view) {
|
|
|
|
return _delegate->listElementShownUnread(view);
|
|
|
|
}
|
|
|
|
|
2020-11-10 16:38:21 +00:00
|
|
|
void ListWidget::elementSendBotCommand(
|
|
|
|
const QString &command,
|
|
|
|
const FullMsgId &context) {
|
2020-11-10 18:51:20 +00:00
|
|
|
_delegate->listSendBotCommand(command, context);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::elementHandleViaClick(not_null<UserData*> bot) {
|
|
|
|
_delegate->listHandleViaClick(bot);
|
2020-11-10 16:38:21 +00:00
|
|
|
}
|
|
|
|
|
2021-05-27 00:44:12 +00:00
|
|
|
bool ListWidget::elementIsChatWide() {
|
|
|
|
return _isChatWide;
|
|
|
|
}
|
|
|
|
|
2021-07-02 15:29:13 +00:00
|
|
|
not_null<Ui::PathShiftGradient*> ListWidget::elementPathShiftGradient() {
|
|
|
|
return _pathGradient.get();
|
|
|
|
}
|
|
|
|
|
2021-07-26 14:37:19 +00:00
|
|
|
void ListWidget::elementReplyTo(const FullMsgId &to) {
|
|
|
|
replyToMessageRequestNotify(to);
|
|
|
|
}
|
|
|
|
|
2021-09-14 16:55:35 +00:00
|
|
|
void ListWidget::elementStartInteraction(not_null<const Element*> view) {
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::saveState(not_null<ListMemento*> memento) {
|
|
|
|
memento->setAroundPosition(_aroundPosition);
|
|
|
|
auto state = countScrollState();
|
|
|
|
if (state.item) {
|
|
|
|
memento->setIdsLimit(_idsLimit);
|
|
|
|
memento->setScrollTopState(state);
|
2017-07-05 18:11:31 +00:00
|
|
|
}
|
2017-06-18 11:08:14 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::restoreState(not_null<ListMemento*> memento) {
|
|
|
|
_aroundPosition = memento->aroundPosition();
|
|
|
|
_aroundIndex = -1;
|
|
|
|
if (const auto limit = memento->idsLimit()) {
|
|
|
|
_idsLimit = limit;
|
2017-06-24 17:05:32 +00:00
|
|
|
}
|
2020-09-08 11:19:44 +00:00
|
|
|
_scrollTopState = memento->scrollTopState();
|
2018-01-09 17:08:31 +00:00
|
|
|
refreshViewer();
|
2017-06-24 17:05:32 +00:00
|
|
|
}
|
|
|
|
|
2018-01-19 17:10:58 +00:00
|
|
|
void ListWidget::updateItemsGeometry() {
|
|
|
|
const auto count = int(_items.size());
|
|
|
|
const auto first = [&] {
|
|
|
|
for (auto i = 0; i != count; ++i) {
|
|
|
|
const auto view = _items[i].get();
|
2018-01-30 13:17:50 +00:00
|
|
|
if (view->isHidden()) {
|
2018-01-19 17:10:58 +00:00
|
|
|
view->setDisplayDate(false);
|
2017-07-05 18:11:31 +00:00
|
|
|
} else {
|
2018-01-13 12:45:11 +00:00
|
|
|
view->setDisplayDate(true);
|
2020-10-22 07:53:56 +00:00
|
|
|
view->setAttachToPrevious(false);
|
2018-01-19 17:10:58 +00:00
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return count;
|
|
|
|
}();
|
2018-01-21 19:21:08 +00:00
|
|
|
refreshAttachmentsFromTill(first, count);
|
2017-06-18 13:08:49 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::updateSize() {
|
2018-02-02 12:51:18 +00:00
|
|
|
resizeToWidth(width(), _minHeight);
|
2017-06-18 13:08:49 +00:00
|
|
|
updateVisibleTopItem();
|
2017-06-18 11:08:14 +00:00
|
|
|
}
|
|
|
|
|
2018-02-02 12:51:18 +00:00
|
|
|
void ListWidget::resizeToWidth(int newWidth, int minHeight) {
|
|
|
|
_minHeight = minHeight;
|
|
|
|
TWidget::resizeToWidth(newWidth);
|
|
|
|
restoreScrollPosition();
|
|
|
|
}
|
|
|
|
|
2021-07-19 17:01:39 +00:00
|
|
|
void ListWidget::startItemRevealAnimations() {
|
2021-09-08 10:53:54 +00:00
|
|
|
for (const auto &view : base::take(_itemRevealPending)) {
|
2021-07-19 17:01:39 +00:00
|
|
|
if (const auto height = view->height()) {
|
|
|
|
if (!_itemRevealAnimations.contains(view)) {
|
|
|
|
auto &animation = _itemRevealAnimations[view];
|
|
|
|
animation.startHeight = height;
|
|
|
|
_itemsRevealHeight += height;
|
|
|
|
animation.animation.start(
|
|
|
|
[=] { revealItemsCallback(); },
|
|
|
|
0.,
|
|
|
|
1.,
|
|
|
|
kItemRevealDuration,
|
|
|
|
anim::easeOutCirc);
|
2021-08-17 11:55:16 +00:00
|
|
|
if (view->data()->out()) {
|
2021-08-27 20:44:47 +00:00
|
|
|
_delegate->listChatTheme()->rotateComplexGradientBackground();
|
2021-08-17 11:55:16 +00:00
|
|
|
}
|
2021-07-19 17:01:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::revealItemsCallback() {
|
|
|
|
auto revealHeight = 0;
|
|
|
|
for (auto i = begin(_itemRevealAnimations)
|
|
|
|
; i != end(_itemRevealAnimations);) {
|
|
|
|
if (!i->second.animation.animating()) {
|
|
|
|
i = _itemRevealAnimations.erase(i);
|
|
|
|
} else {
|
|
|
|
revealHeight += anim::interpolate(
|
|
|
|
i->second.startHeight,
|
|
|
|
0,
|
|
|
|
i->second.animation.value(1.));
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (_itemsRevealHeight != revealHeight) {
|
2021-07-20 09:13:32 +00:00
|
|
|
updateVisibleTopItem();
|
|
|
|
if (_visibleTopItem) {
|
|
|
|
// We're not at the bottom.
|
|
|
|
revealHeight = 0;
|
|
|
|
_itemRevealAnimations.clear();
|
|
|
|
}
|
2021-07-19 17:01:39 +00:00
|
|
|
const auto old = std::exchange(_itemsRevealHeight, revealHeight);
|
|
|
|
const auto delta = old - _itemsRevealHeight;
|
|
|
|
_itemsHeight += delta;
|
|
|
|
_itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom)
|
|
|
|
? (_minHeight - _itemsHeight - st::historyPaddingBottom)
|
|
|
|
: 0;
|
|
|
|
const auto wasHeight = height();
|
2021-07-20 17:18:58 +00:00
|
|
|
const auto nowHeight = _itemsTop
|
|
|
|
+ _itemsHeight
|
|
|
|
+ st::historyPaddingBottom;
|
2021-07-19 17:01:39 +00:00
|
|
|
if (wasHeight != nowHeight) {
|
|
|
|
resize(width(), nowHeight);
|
|
|
|
}
|
|
|
|
update();
|
2021-07-20 09:13:32 +00:00
|
|
|
restoreScrollPosition();
|
|
|
|
updateVisibleTopItem();
|
2021-07-19 17:01:39 +00:00
|
|
|
|
|
|
|
if (!_itemsRevealHeight) {
|
|
|
|
mouseActionUpdate(QCursor::pos());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
int ListWidget::resizeGetHeight(int newWidth) {
|
2017-06-18 11:08:14 +00:00
|
|
|
update();
|
|
|
|
|
2018-01-19 17:10:58 +00:00
|
|
|
const auto resizeAllItems = (_itemsWidth != newWidth);
|
2017-06-18 11:08:14 +00:00
|
|
|
auto newHeight = 0;
|
2018-01-13 12:45:11 +00:00
|
|
|
for (auto &view : _items) {
|
|
|
|
view->setY(newHeight);
|
2018-01-19 17:10:58 +00:00
|
|
|
if (view->pendingResize() || resizeAllItems) {
|
|
|
|
newHeight += view->resizeGetHeight(newWidth);
|
|
|
|
} else {
|
|
|
|
newHeight += view->height();
|
|
|
|
}
|
2017-06-18 13:08:49 +00:00
|
|
|
}
|
2018-01-30 12:13:46 +00:00
|
|
|
if (newHeight > 0) {
|
|
|
|
_itemAverageHeight = std::max(
|
|
|
|
itemMinimalHeight(),
|
|
|
|
newHeight / int(_items.size()));
|
|
|
|
}
|
2021-07-19 17:01:39 +00:00
|
|
|
startItemRevealAnimations();
|
2018-01-19 17:10:58 +00:00
|
|
|
_itemsWidth = newWidth;
|
2021-07-19 17:01:39 +00:00
|
|
|
_itemsHeight = newHeight - _itemsRevealHeight;
|
2018-02-02 12:51:18 +00:00
|
|
|
_itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom)
|
|
|
|
? (_minHeight - _itemsHeight - st::historyPaddingBottom)
|
|
|
|
: 0;
|
2017-06-21 21:38:31 +00:00
|
|
|
return _itemsTop + _itemsHeight + st::historyPaddingBottom;
|
2017-06-18 11:08:14 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::restoreScrollPosition() {
|
|
|
|
auto newVisibleTop = _visibleTopItem
|
|
|
|
? (itemTop(_visibleTopItem) + _visibleTopFromItem)
|
|
|
|
: ScrollMax;
|
|
|
|
_delegate->listScrollTo(newVisibleTop);
|
2017-06-23 19:28:42 +00:00
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
TextSelection ListWidget::computeRenderSelection(
|
|
|
|
not_null<const SelectedMap*> selected,
|
|
|
|
not_null<const Element*> view) const {
|
|
|
|
const auto itemSelection = [&](not_null<HistoryItem*> item) {
|
|
|
|
auto i = selected->find(item->fullId());
|
|
|
|
if (i != selected->end()) {
|
|
|
|
return FullSelection;
|
|
|
|
}
|
|
|
|
return TextSelection();
|
|
|
|
};
|
|
|
|
const auto item = view->data();
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto group = session().data().groups().find(item)) {
|
2020-09-16 08:25:57 +00:00
|
|
|
if (group->items.front() != item) {
|
2018-01-26 15:40:11 +00:00
|
|
|
return TextSelection();
|
|
|
|
}
|
|
|
|
auto result = TextSelection();
|
|
|
|
auto allFullSelected = true;
|
|
|
|
const auto count = int(group->items.size());
|
|
|
|
for (auto i = 0; i != count; ++i) {
|
|
|
|
if (itemSelection(group->items[i]) == FullSelection) {
|
|
|
|
result = AddGroupItemSelection(result, i);
|
|
|
|
} else {
|
|
|
|
allFullSelected = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (allFullSelected) {
|
|
|
|
return FullSelection;
|
|
|
|
}
|
|
|
|
const auto leaderSelection = itemSelection(item);
|
|
|
|
if (leaderSelection != FullSelection
|
|
|
|
&& leaderSelection != TextSelection()) {
|
|
|
|
return leaderSelection;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
return itemSelection(item);
|
|
|
|
}
|
|
|
|
|
|
|
|
TextSelection ListWidget::itemRenderSelection(
|
|
|
|
not_null<const Element*> view) const {
|
2018-01-27 16:41:06 +00:00
|
|
|
if (!_dragSelected.empty()) {
|
2018-01-26 15:40:11 +00:00
|
|
|
const auto i = _dragSelected.find(view->data()->fullId());
|
|
|
|
if (i != _dragSelected.end()) {
|
|
|
|
return (_dragSelectAction == DragSelectAction::Selecting)
|
|
|
|
? FullSelection
|
|
|
|
: TextSelection();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!_selected.empty() || !_dragSelected.empty()) {
|
|
|
|
return computeRenderSelection(&_selected, view);
|
|
|
|
} else if (view->data() == _selectedTextItem) {
|
|
|
|
return _selectedTextRange;
|
|
|
|
}
|
|
|
|
return TextSelection();
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::paintEvent(QPaintEvent *e) {
|
2017-06-21 21:38:31 +00:00
|
|
|
if (Ui::skipPaintEvent(this, e)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-28 14:32:10 +00:00
|
|
|
const auto guard = gsl::finally([&] {
|
|
|
|
_userpicsCache.clear();
|
|
|
|
});
|
|
|
|
|
2017-06-18 11:08:14 +00:00
|
|
|
Painter p(this);
|
|
|
|
|
2021-07-02 15:29:13 +00:00
|
|
|
_pathGradient->startFrame(
|
|
|
|
0,
|
|
|
|
width(),
|
|
|
|
std::min(st::msgMaxWidth / 2, width() / 2));
|
|
|
|
|
2017-06-18 11:08:14 +00:00
|
|
|
auto clip = e->rect();
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
auto from = std::lower_bound(begin(_items), end(_items), clip.top(), [this](auto &elem, int top) {
|
2018-01-13 12:45:11 +00:00
|
|
|
return this->itemTop(elem) + elem->height() <= top;
|
2018-01-09 17:08:31 +00:00
|
|
|
});
|
|
|
|
auto to = std::lower_bound(begin(_items), end(_items), clip.top() + clip.height(), [this](auto &elem, int bottom) {
|
|
|
|
return this->itemTop(elem) < bottom;
|
|
|
|
});
|
2021-08-27 20:44:47 +00:00
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
if (from != end(_items)) {
|
|
|
|
auto top = itemTop(from->get());
|
2021-08-27 20:44:47 +00:00
|
|
|
auto context = controller()->preparePaintContext({
|
|
|
|
.theme = _delegate->listChatTheme(),
|
|
|
|
.visibleAreaTop = _visibleTop,
|
|
|
|
.visibleAreaTopGlobal = mapToGlobal(QPoint(0, _visibleTop)).y(),
|
2021-09-06 10:18:14 +00:00
|
|
|
.visibleAreaWidth = width(),
|
2021-08-27 20:44:47 +00:00
|
|
|
.clip = clip,
|
|
|
|
}).translated(0, -top);
|
2018-01-09 17:08:31 +00:00
|
|
|
p.translate(0, top);
|
|
|
|
for (auto i = from; i != to; ++i) {
|
2018-01-13 12:45:11 +00:00
|
|
|
const auto view = *i;
|
2021-09-02 22:08:57 +00:00
|
|
|
context.outbg = view->hasOutLayout();
|
2021-08-24 17:01:34 +00:00
|
|
|
context.selection = itemRenderSelection(view);
|
2021-08-19 14:22:12 +00:00
|
|
|
view->draw(p, context);
|
2018-01-13 12:45:11 +00:00
|
|
|
const auto height = view->height();
|
2018-01-09 17:08:31 +00:00
|
|
|
top += height;
|
2021-08-19 14:22:12 +00:00
|
|
|
context.viewport.translate(0, -height);
|
|
|
|
context.clip.translate(0, -height);
|
2018-01-09 17:08:31 +00:00
|
|
|
p.translate(0, height);
|
|
|
|
}
|
|
|
|
p.translate(0, -top);
|
2017-06-22 01:31:02 +00:00
|
|
|
|
2018-01-11 19:33:26 +00:00
|
|
|
enumerateUserpics([&](not_null<Element*> view, int userpicTop) {
|
2018-01-09 17:08:31 +00:00
|
|
|
// stop the enumeration if the userpic is below the painted rect
|
|
|
|
if (userpicTop >= clip.top() + clip.height()) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-06-22 01:31:02 +00:00
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
// paint the userpic if it intersects the painted rect
|
|
|
|
if (userpicTop + st::msgPhotoSize > clip.top()) {
|
2018-01-11 15:51:59 +00:00
|
|
|
const auto message = view->data()->toHistoryMessage();
|
|
|
|
Assert(message != nullptr);
|
|
|
|
|
2019-03-15 15:15:56 +00:00
|
|
|
if (const auto from = message->displayFrom()) {
|
|
|
|
from->paintUserpicLeft(
|
|
|
|
p,
|
2020-05-28 14:32:10 +00:00
|
|
|
_userpics[from],
|
2019-03-15 15:15:56 +00:00
|
|
|
st::historyPhotoLeft,
|
|
|
|
userpicTop,
|
|
|
|
view->width(),
|
|
|
|
st::msgPhotoSize);
|
|
|
|
} else if (const auto info = message->hiddenForwardedInfo()) {
|
|
|
|
info->userpic.paint(
|
|
|
|
p,
|
|
|
|
st::historyPhotoLeft,
|
|
|
|
userpicTop,
|
|
|
|
view->width(),
|
|
|
|
st::msgPhotoSize);
|
|
|
|
} else {
|
|
|
|
Unexpected("Corrupt forwarded information in message.");
|
|
|
|
}
|
2018-01-09 17:08:31 +00:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
2017-06-22 01:31:02 +00:00
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
|
2019-04-02 09:13:30 +00:00
|
|
|
auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);
|
2018-01-11 19:33:26 +00:00
|
|
|
enumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) {
|
2018-01-09 17:08:31 +00:00
|
|
|
// stop the enumeration if the date is above the painted rect
|
|
|
|
if (dateTop + dateHeight <= clip.top()) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-06-22 01:31:02 +00:00
|
|
|
|
2018-01-23 11:47:38 +00:00
|
|
|
const auto displayDate = view->displayDate();
|
2018-01-11 15:51:59 +00:00
|
|
|
auto dateInPlace = displayDate;
|
2018-01-09 17:08:31 +00:00
|
|
|
if (dateInPlace) {
|
2018-01-11 15:51:59 +00:00
|
|
|
const auto correctDateTop = itemtop + st::msgServiceMargin.top();
|
2018-01-09 17:08:31 +00:00
|
|
|
dateInPlace = (dateTop < correctDateTop + dateHeight);
|
|
|
|
}
|
|
|
|
//bool noFloatingDate = (item->date.date() == lastDate && displayDate);
|
|
|
|
//if (noFloatingDate) {
|
|
|
|
// if (itemtop < showFloatingBefore) {
|
|
|
|
// noFloatingDate = false;
|
|
|
|
// }
|
|
|
|
//}
|
|
|
|
|
|
|
|
// paint the date if it intersects the painted rect
|
|
|
|
if (dateTop < clip.top() + clip.height()) {
|
|
|
|
auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;
|
|
|
|
if (opacity > 0.) {
|
|
|
|
p.setOpacity(opacity);
|
|
|
|
int dateY = /*noFloatingDate ? itemtop :*/ (dateTop - st::msgServiceMargin.top());
|
2018-01-13 12:45:11 +00:00
|
|
|
int width = view->width();
|
2018-01-23 11:47:38 +00:00
|
|
|
if (const auto date = view->Get<HistoryView::DateBadge>()) {
|
2021-09-03 10:17:07 +00:00
|
|
|
date->paint(p, context.st, dateY, width, _isChatWide);
|
2018-01-09 17:08:31 +00:00
|
|
|
} else {
|
2021-09-03 10:17:07 +00:00
|
|
|
ServiceMessagePainter::PaintDate(
|
2018-01-23 11:47:38 +00:00
|
|
|
p,
|
2021-09-03 10:17:07 +00:00
|
|
|
context.st,
|
|
|
|
ItemDateText(
|
|
|
|
view->data(),
|
|
|
|
IsItemScheduledUntilOnline(view->data())),
|
2018-01-23 11:47:38 +00:00
|
|
|
dateY,
|
2021-05-27 00:44:12 +00:00
|
|
|
width,
|
|
|
|
_isChatWide);
|
2017-06-22 01:31:02 +00:00
|
|
|
}
|
|
|
|
}
|
2018-01-09 17:08:31 +00:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
2017-06-18 13:08:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
void ListWidget::applyDragSelection() {
|
|
|
|
applyDragSelection(_selected);
|
|
|
|
clearDragSelection();
|
|
|
|
pushSelectedItems();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::applyDragSelection(SelectedMap &applyTo) const {
|
|
|
|
if (_dragSelectAction == DragSelectAction::Selecting) {
|
2021-09-08 10:53:54 +00:00
|
|
|
for (const auto &itemId : _dragSelected) {
|
2018-01-27 13:59:24 +00:00
|
|
|
if (applyTo.size() >= MaxSelectedItems) {
|
|
|
|
break;
|
|
|
|
} else if (!applyTo.contains(itemId)) {
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto item = session().data().message(itemId)) {
|
2018-01-27 13:59:24 +00:00
|
|
|
addToSelection(applyTo, item);
|
|
|
|
}
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
} else if (_dragSelectAction == DragSelectAction::Deselecting) {
|
2021-09-08 10:53:54 +00:00
|
|
|
for (const auto &itemId : _dragSelected) {
|
2018-01-27 13:59:24 +00:00
|
|
|
removeFromSelection(applyTo, itemId);
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-08 15:10:06 +00:00
|
|
|
TextForMimeData ListWidget::getSelectedText() const {
|
2018-01-26 15:40:11 +00:00
|
|
|
auto selected = _selected;
|
|
|
|
|
|
|
|
if (_mouseAction == MouseAction::Selecting && !_dragSelected.empty()) {
|
|
|
|
applyDragSelection(selected);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (selected.empty()) {
|
|
|
|
if (const auto view = viewForItem(_selectedTextItem)) {
|
|
|
|
return view->selectedText(_selectedTextRange);
|
|
|
|
}
|
|
|
|
return _selectedText;
|
|
|
|
}
|
|
|
|
|
2018-01-27 17:26:24 +00:00
|
|
|
const auto timeFormat = qsl(", [dd.MM.yy hh:mm]\n");
|
|
|
|
auto groups = base::flat_set<not_null<const Data::Group*>>();
|
|
|
|
auto fullSize = 0;
|
|
|
|
auto texts = std::vector<std::pair<
|
|
|
|
not_null<HistoryItem*>,
|
2019-04-08 15:10:06 +00:00
|
|
|
TextForMimeData>>();
|
2018-01-27 17:26:24 +00:00
|
|
|
texts.reserve(selected.size());
|
|
|
|
|
|
|
|
const auto wrapItem = [&](
|
|
|
|
not_null<HistoryItem*> item,
|
2019-04-08 15:10:06 +00:00
|
|
|
TextForMimeData &&unwrapped) {
|
2018-02-03 19:52:35 +00:00
|
|
|
auto time = ItemDateTime(item).toString(timeFormat);
|
2019-04-08 15:10:06 +00:00
|
|
|
auto part = TextForMimeData();
|
2018-01-27 17:26:24 +00:00
|
|
|
auto size = item->author()->name.size()
|
|
|
|
+ time.size()
|
2019-04-08 15:10:06 +00:00
|
|
|
+ unwrapped.expanded.size();
|
|
|
|
part.reserve(size);
|
|
|
|
part.append(item->author()->name).append(time);
|
|
|
|
part.append(std::move(unwrapped));
|
2019-04-02 09:13:30 +00:00
|
|
|
texts.emplace_back(std::move(item), std::move(part));
|
2018-01-27 17:26:24 +00:00
|
|
|
fullSize += size;
|
|
|
|
};
|
|
|
|
const auto addItem = [&](not_null<HistoryItem*> item) {
|
|
|
|
wrapItem(item, HistoryItemText(item));
|
|
|
|
};
|
|
|
|
const auto addGroup = [&](not_null<const Data::Group*> group) {
|
|
|
|
Expects(!group->items.empty());
|
|
|
|
|
|
|
|
wrapItem(group->items.back(), HistoryGroupText(group));
|
|
|
|
};
|
|
|
|
|
2021-03-13 08:26:58 +00:00
|
|
|
for (const auto &[itemId, data] : selected) {
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto item = session().data().message(itemId)) {
|
|
|
|
if (const auto group = session().data().groups().find(item)) {
|
2018-01-27 17:26:24 +00:00
|
|
|
if (groups.contains(group)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (isSelectedGroup(selected, group)) {
|
|
|
|
groups.emplace(group);
|
|
|
|
addGroup(group);
|
|
|
|
} else {
|
|
|
|
addItem(item);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
addItem(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ranges::sort(texts, [&](
|
2019-04-08 15:10:06 +00:00
|
|
|
const std::pair<not_null<HistoryItem*>, TextForMimeData> &a,
|
|
|
|
const std::pair<not_null<HistoryItem*>, TextForMimeData> &b) {
|
2018-01-27 17:26:24 +00:00
|
|
|
return _delegate->listIsLessInOrder(a.first, b.first);
|
|
|
|
});
|
2018-01-26 15:40:11 +00:00
|
|
|
|
2019-04-08 15:10:06 +00:00
|
|
|
auto result = TextForMimeData();
|
|
|
|
auto sep = qstr("\n\n");
|
|
|
|
result.reserve(fullSize + (texts.size() - 1) * sep.size());
|
2018-01-27 17:26:24 +00:00
|
|
|
for (auto i = begin(texts), e = end(texts); i != e;) {
|
2019-04-08 15:10:06 +00:00
|
|
|
result.append(std::move(i->second));
|
2018-01-27 17:26:24 +00:00
|
|
|
if (++i != e) {
|
2019-04-08 15:10:06 +00:00
|
|
|
result.append(sep);
|
2018-01-27 17:26:24 +00:00
|
|
|
}
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2020-10-26 08:54:59 +00:00
|
|
|
MessageIdsList ListWidget::getSelectedIds() const {
|
2018-01-26 15:40:11 +00:00
|
|
|
return collectSelectedIds();
|
2017-07-04 13:31:18 +00:00
|
|
|
}
|
|
|
|
|
2020-10-26 08:54:59 +00:00
|
|
|
SelectedItems ListWidget::getSelectedItems() const {
|
|
|
|
return collectSelectedItems();
|
|
|
|
}
|
|
|
|
|
2020-09-04 16:11:36 +00:00
|
|
|
int ListWidget::findItemIndexByY(int y) const {
|
2018-01-09 17:08:31 +00:00
|
|
|
Expects(!_items.empty());
|
|
|
|
|
|
|
|
if (y < _itemsTop) {
|
2020-09-04 16:11:36 +00:00
|
|
|
return 0;
|
2018-01-09 17:08:31 +00:00
|
|
|
}
|
|
|
|
auto i = std::lower_bound(
|
|
|
|
begin(_items),
|
|
|
|
end(_items),
|
|
|
|
y,
|
|
|
|
[this](auto &elem, int top) {
|
2020-09-04 16:11:36 +00:00
|
|
|
return this->itemTop(elem) + elem->height() <= top;
|
|
|
|
});
|
|
|
|
return std::min(int(i - begin(_items)), int(_items.size() - 1));
|
|
|
|
}
|
|
|
|
|
|
|
|
not_null<Element*> ListWidget::findItemByY(int y) const {
|
|
|
|
return _items[findItemIndexByY(y)];
|
2018-01-09 17:08:31 +00:00
|
|
|
}
|
2017-06-24 18:12:15 +00:00
|
|
|
|
2018-01-11 19:33:26 +00:00
|
|
|
Element *ListWidget::strictFindItemByY(int y) const {
|
2018-01-09 17:08:31 +00:00
|
|
|
if (_items.empty()) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
return (y >= _itemsTop && y < _itemsTop + _itemsHeight)
|
|
|
|
? findItemByY(y).get()
|
|
|
|
: nullptr;
|
2017-06-18 11:08:14 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
auto ListWidget::countScrollState() const -> ScrollTopState {
|
2020-08-28 14:10:03 +00:00
|
|
|
if (_items.empty() || _visibleBottom == height()) {
|
2018-01-09 17:08:31 +00:00
|
|
|
return { Data::MessagePosition(), 0 };
|
|
|
|
}
|
|
|
|
auto topItem = findItemByY(_visibleTop);
|
|
|
|
return {
|
2018-01-10 13:13:33 +00:00
|
|
|
topItem->data()->position(),
|
2018-01-09 17:08:31 +00:00
|
|
|
_visibleTop - itemTop(topItem)
|
|
|
|
};
|
2017-06-23 19:28:42 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::keyPressEvent(QKeyEvent *e) {
|
2017-07-05 13:11:08 +00:00
|
|
|
if (e->key() == Qt::Key_Escape || e->key() == Qt::Key_Back) {
|
2018-01-27 16:41:06 +00:00
|
|
|
if (hasSelectedText() || hasSelectedItems()) {
|
|
|
|
cancelSelection();
|
|
|
|
} else {
|
|
|
|
_delegate->listCancelRequest();
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
} else if (e == QKeySequence::Copy
|
|
|
|
&& (hasSelectedText() || hasSelectedItems())) {
|
2019-09-16 11:14:06 +00:00
|
|
|
TextUtilities::SetClipboardText(getSelectedText());
|
2017-06-23 19:28:42 +00:00
|
|
|
#ifdef Q_OS_MAC
|
2018-01-25 10:10:52 +00:00
|
|
|
} else if (e->key() == Qt::Key_E
|
|
|
|
&& e->modifiers().testFlag(Qt::ControlModifier)) {
|
2019-09-16 11:14:06 +00:00
|
|
|
TextUtilities::SetClipboardText(getSelectedText(), QClipboard::FindBuffer);
|
2017-06-23 19:28:42 +00:00
|
|
|
#endif // Q_OS_MAC
|
2018-01-27 16:41:06 +00:00
|
|
|
} else if (e == QKeySequence::Delete) {
|
|
|
|
_delegate->listDeleteRequest();
|
2017-06-23 19:28:42 +00:00
|
|
|
} else {
|
|
|
|
e->ignore();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::mouseDoubleClickEvent(QMouseEvent *e) {
|
2017-06-24 10:11:29 +00:00
|
|
|
mouseActionStart(e->globalPos(), e->button());
|
2018-01-26 15:40:11 +00:00
|
|
|
trySwitchToWordSelection();
|
2020-09-01 13:09:54 +00:00
|
|
|
if (!ClickHandler::getActive()
|
|
|
|
&& !ClickHandler::getPressed()
|
|
|
|
&& (_mouseCursorState == CursorState::None
|
|
|
|
|| _mouseCursorState == CursorState::Date)
|
|
|
|
&& _selected.empty()
|
|
|
|
&& _overElement
|
|
|
|
&& IsServerMsgId(_overElement->data()->id)) {
|
|
|
|
mouseActionCancel();
|
|
|
|
replyToMessageRequestNotify(_overElement->data()->fullId());
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::trySwitchToWordSelection() {
|
|
|
|
auto selectingSome = (_mouseAction == MouseAction::Selecting)
|
|
|
|
&& hasSelectedText();
|
|
|
|
auto willSelectSome = (_mouseAction == MouseAction::None)
|
|
|
|
&& !hasSelectedItems();
|
2018-01-27 13:59:24 +00:00
|
|
|
auto checkSwitchToWordSelection = _overElement
|
2018-01-26 15:40:11 +00:00
|
|
|
&& (_mouseSelectType == TextSelectType::Letters)
|
|
|
|
&& (selectingSome || willSelectSome);
|
|
|
|
if (checkSwitchToWordSelection) {
|
|
|
|
switchToWordSelection();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::switchToWordSelection() {
|
2018-01-27 13:59:24 +00:00
|
|
|
Expects(_overElement != nullptr);
|
2017-06-24 10:11:29 +00:00
|
|
|
|
2018-01-27 13:59:24 +00:00
|
|
|
StateRequest request;
|
2019-06-12 13:26:04 +00:00
|
|
|
request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
|
2018-01-27 13:59:24 +00:00
|
|
|
auto dragState = _overElement->textState(_pressState.point, request);
|
|
|
|
if (dragState.cursor != CursorState::Text) {
|
2018-01-26 15:40:11 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
_mouseTextSymbol = dragState.symbol;
|
|
|
|
_mouseSelectType = TextSelectType::Words;
|
|
|
|
if (_mouseAction == MouseAction::None) {
|
|
|
|
_mouseAction = MouseAction::Selecting;
|
2018-01-27 13:59:24 +00:00
|
|
|
setTextSelection(_overElement, TextSelection(
|
2018-01-26 15:40:11 +00:00
|
|
|
dragState.symbol,
|
|
|
|
dragState.symbol
|
|
|
|
));
|
|
|
|
}
|
|
|
|
mouseActionUpdate();
|
|
|
|
|
|
|
|
_trippleClickPoint = _mousePosition;
|
2019-02-19 06:57:53 +00:00
|
|
|
_trippleClickStartTime = crl::now();
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::validateTrippleClickStartTime() {
|
|
|
|
if (_trippleClickStartTime) {
|
2019-02-19 06:57:53 +00:00
|
|
|
const auto elapsed = (crl::now() - _trippleClickStartTime);
|
2018-01-26 15:40:11 +00:00
|
|
|
if (elapsed >= QApplication::doubleClickInterval()) {
|
|
|
|
_trippleClickStartTime = 0;
|
2017-06-24 10:11:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::contextMenuEvent(QContextMenuEvent *e) {
|
2017-06-24 10:11:29 +00:00
|
|
|
showContextMenu(e);
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
2017-06-24 10:11:29 +00:00
|
|
|
if (e->reason() == QContextMenuEvent::Mouse) {
|
|
|
|
mouseActionUpdate(e->globalPos());
|
|
|
|
}
|
|
|
|
|
2019-07-25 18:55:11 +00:00
|
|
|
auto request = ContextMenuRequest(_controller);
|
2019-07-24 14:00:30 +00:00
|
|
|
|
2018-01-25 10:10:52 +00:00
|
|
|
request.link = ClickHandler::getActive();
|
2018-01-27 13:59:24 +00:00
|
|
|
request.view = _overElement;
|
2018-01-28 15:08:34 +00:00
|
|
|
request.item = _overItemExact
|
|
|
|
? _overItemExact
|
|
|
|
: _overElement
|
|
|
|
? _overElement->data().get()
|
|
|
|
: nullptr;
|
|
|
|
request.pointState = _overState.pointState;
|
2018-01-26 15:40:11 +00:00
|
|
|
request.selectedText = _selectedText;
|
2018-01-28 15:08:34 +00:00
|
|
|
request.selectedItems = collectSelectedItems();
|
2018-01-27 19:21:41 +00:00
|
|
|
request.overSelection = showFromTouch
|
|
|
|
|| (_overElement && isInsideSelection(
|
|
|
|
_overElement,
|
|
|
|
_overItemExact ? _overItemExact : _overElement->data().get(),
|
|
|
|
_overState));
|
2017-06-24 10:11:29 +00:00
|
|
|
|
2018-01-25 10:10:52 +00:00
|
|
|
_menu = FillContextMenu(this, request);
|
2021-01-13 01:10:25 +00:00
|
|
|
if (_menu && !_menu->empty()) {
|
2017-06-24 10:11:29 +00:00
|
|
|
_menu->popup(e->globalPos());
|
|
|
|
e->accept();
|
2018-01-25 10:10:52 +00:00
|
|
|
} else if (_menu) {
|
|
|
|
_menu = nullptr;
|
2017-06-24 10:11:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::mousePressEvent(QMouseEvent *e) {
|
2017-06-21 21:38:31 +00:00
|
|
|
if (_menu) {
|
|
|
|
e->accept();
|
|
|
|
return; // ignore mouse press, that was hiding context menu
|
|
|
|
}
|
|
|
|
mouseActionStart(e->globalPos(), e->button());
|
2017-06-18 11:08:14 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::mouseMoveEvent(QMouseEvent *e) {
|
2018-02-21 23:59:56 +00:00
|
|
|
static auto lastGlobalPosition = e->globalPos();
|
|
|
|
auto reallyMoved = (lastGlobalPosition != e->globalPos());
|
2017-06-21 21:38:31 +00:00
|
|
|
auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
|
|
|
|
if (!buttonsPressed && _mouseAction != MouseAction::None) {
|
|
|
|
mouseReleaseEvent(e);
|
|
|
|
}
|
2018-02-21 23:59:56 +00:00
|
|
|
if (reallyMoved) {
|
|
|
|
lastGlobalPosition = e->globalPos();
|
|
|
|
if (!buttonsPressed
|
|
|
|
|| (_scrollDateLink
|
|
|
|
&& ClickHandler::getPressed() == _scrollDateLink)) {
|
|
|
|
keepScrollDateForNow();
|
|
|
|
}
|
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
mouseActionUpdate(e->globalPos());
|
2017-06-18 11:08:14 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::mouseReleaseEvent(QMouseEvent *e) {
|
2017-06-21 21:38:31 +00:00
|
|
|
mouseActionFinish(e->globalPos(), e->button());
|
|
|
|
if (!rect().contains(e->pos())) {
|
|
|
|
leaveEvent(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::enterEventHook(QEvent *e) {
|
2017-06-21 21:38:31 +00:00
|
|
|
mouseActionUpdate(QCursor::pos());
|
|
|
|
return TWidget::enterEventHook(e);
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::leaveEventHook(QEvent *e) {
|
2018-01-27 13:59:24 +00:00
|
|
|
if (const auto view = _overElement) {
|
|
|
|
if (_overState.pointState != PointState::Outside) {
|
2018-01-26 15:40:11 +00:00
|
|
|
repaintItem(view);
|
2018-01-27 13:59:24 +00:00
|
|
|
_overState.pointState = PointState::Outside;
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
2017-06-21 23:54:38 +00:00
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
ClickHandler::clearActive();
|
|
|
|
Ui::Tooltip::Hide();
|
|
|
|
if (!ClickHandler::getPressed() && _cursor != style::cur_default) {
|
|
|
|
_cursor = style::cur_default;
|
|
|
|
setCursor(_cursor);
|
|
|
|
}
|
|
|
|
return TWidget::leaveEventHook(e);
|
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
void ListWidget::updateDragSelection() {
|
|
|
|
if (!_overState.itemId || !_pressState.itemId) {
|
|
|
|
clearDragSelection();
|
|
|
|
return;
|
2018-01-27 13:59:24 +00:00
|
|
|
} else if (_items.empty() || !_overElement || !_selectEnabled) {
|
2018-01-26 15:40:11 +00:00
|
|
|
return;
|
|
|
|
}
|
2019-07-24 11:13:51 +00:00
|
|
|
const auto pressItem = session().data().message(_pressState.itemId);
|
2018-01-26 15:40:11 +00:00
|
|
|
if (!pressItem) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-01-27 13:59:24 +00:00
|
|
|
const auto overView = _overElement;
|
2018-01-26 15:40:11 +00:00
|
|
|
const auto pressView = viewForItem(pressItem);
|
|
|
|
const auto selectingUp = _delegate->listIsLessInOrder(
|
|
|
|
overView->data(),
|
|
|
|
pressItem);
|
2018-01-27 16:41:06 +00:00
|
|
|
if (selectingUp != _dragSelectDirectionUp) {
|
|
|
|
_dragSelectDirectionUp = selectingUp;
|
|
|
|
_dragSelectAction = DragSelectAction::None;
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
const auto fromView = selectingUp ? overView : pressView;
|
|
|
|
const auto tillView = selectingUp ? pressView : overView;
|
Remove unused variable
The following are commits related to removed variables.
apiwrap.cpp
e050e27: kSaveDraftBeforeQuitTimeout
app.cpp
113f665: serviceImageCacheSize
boxes/auto_download_box.cpp
a0c6104: checked(Source source, Type type)
boxes/background_preview_box.cpp
b6edf45: resultBytesPerPixel
fe21b5a: ms
boxes/calendar_box.cpp
ae97704: yearIndex, monthIndex
99bb093: ms
boxes/connection_box.cpp
f794d8d: ping
boxes/dictionaries_manager.cpp
8353867: session
boxes/peer_list_box.cpp
2ce2a14: grayedWidth
boxes/peers/add_participants_box.cpp
07e010d: chat, channel
boxes/self_destruction_box.cpp
fe9f02e: count
chat_helpers/emoji_suggestions_widget.cpp
a12bc60: is(QLatin1String string)
chat_helpers/field_autocomplete.cpp
8c7a35c: atwidth, hashwidth
chat_helpers/gifs_list_widget.cpp
ff65734: inlineItems
3d846fc: newSelected
d1687ab: kSaveDraftBeforeQuitTimeout
chat_helpers/stickers_dice_pack.cpp
c83e297: kZeroDiceDocumentId
chat_helpers/stickers_emoji_pack.cpp
d298953: length
chat_helpers/stickers_list_widget.cpp
eb75859: index, x
core/crash_reports.cpp
5940ae6: LaunchedDateTimeStr, LaunchedBinaryName
data/data_changes.cpp
3c4e959:clearRealtime
data/data_cloud_file.cpp
4b354b0: fromCloud, cacheTag
data/data_document_media.cpp
7db5359: kMaxVideoFrameArea
data/data_messages.cpp
794e315: wasCount
data/data_photo_media.cpp
e27d2bc: index
data/data_wall_paper.cpp
b6edf45: resultBytesPerPixel
data/data_types.cpp
aa8f62d: kWebDocumentCacheTag, kStorageCacheMask
history/admin_log/history_admin_log_inner.cpp
794e315: canDelete, canForward
history/history_location_manager.cpp
60f45ab: kCoordPrecision
9f90d3a: kMaxHttpRedirects
history/history_message.cpp
cedf8a6: kPinnedMessageTextLimit
history/history_widget.cpp
b305924: serviceColor
efa5fc4: hasForward
5e7aa4f: kTabbedSelectorToggleTooltipTimeoutMs, kTabbedSelectorToggleTooltipCount
history/view/history_view_context_menu.cpp
fe1a90b: isVideoLink, isVoiceLink, isAudioLink
settings.cpp
e2f54eb: defaultRecent
settings/settings_folders.cpp
e8bf5bb: kRefreshSuggestedTimeout
ui/filter_icon_panel.cpp
c4a0bc1: kDelayedHideTimeoutMs
window/themes/window_theme_preview.cpp
ef927c8: mutedCounter
-----
Modified variables
boxes/stickers_box.cpp
554eb3a: _rows[pressedIndex] -> set
data/data_notify_settings.cpp
734c410: muteForSeconds -> muteUntil
history/view/history_view_list_widget.cpp
07528be: _items[index] -> view
e5f3bed: fromState, tillState
history/history.cpp
cd3c1c6: kStatusShowClientsideRecordVideo -> kStatusShowClientsideRecordVoice
storage/download_manager_mtproto.cpp
ae8fb14: _queues[dcId] -> queue
storage/localstorage.cpp
357caf8: MTP::Environment::Production -> production
2020-07-02 10:42:30 +00:00
|
|
|
const auto fromState = selectingUp ? _overState : _pressState;
|
|
|
|
const auto tillState = selectingUp ? _pressState : _overState;
|
|
|
|
updateDragSelection(fromView, fromState, tillView, tillState);
|
2018-01-27 16:41:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::updateDragSelection(
|
|
|
|
const Element *fromView,
|
|
|
|
const MouseState &fromState,
|
|
|
|
const Element *tillView,
|
|
|
|
const MouseState &tillState) {
|
|
|
|
Expects(fromView != nullptr || tillView != nullptr);
|
|
|
|
|
|
|
|
const auto delta = QApplication::startDragDistance();
|
|
|
|
|
|
|
|
const auto includeFrom = [&] (
|
|
|
|
not_null<const Element*> view,
|
|
|
|
const MouseState &state) {
|
|
|
|
const auto bottom = view->height() - view->marginBottom();
|
|
|
|
return (state.point.y() < bottom - delta);
|
|
|
|
};
|
|
|
|
const auto includeTill = [&] (
|
|
|
|
not_null<const Element*> view,
|
|
|
|
const MouseState &state) {
|
|
|
|
const auto top = view->marginTop();
|
|
|
|
return (state.point.y() >= top + delta);
|
|
|
|
};
|
|
|
|
const auto includeSingleItem = [&] (
|
|
|
|
not_null<const Element*> view,
|
|
|
|
const MouseState &state1,
|
|
|
|
const MouseState &state2) {
|
|
|
|
const auto top = view->marginTop();
|
|
|
|
const auto bottom = view->height() - view->marginBottom();
|
|
|
|
const auto y1 = std::min(state1.point.y(), state2.point.y());
|
|
|
|
const auto y2 = std::max(state1.point.y(), state2.point.y());
|
|
|
|
return (y1 < bottom - delta && y2 >= top + delta)
|
|
|
|
? (y2 - y1 >= delta)
|
|
|
|
: false;
|
|
|
|
};
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
const auto from = [&] {
|
2018-01-27 16:41:06 +00:00
|
|
|
const auto result = fromView ? ranges::find(
|
|
|
|
_items,
|
|
|
|
fromView,
|
|
|
|
[](auto view) { return view.get(); }) : end(_items);
|
|
|
|
return (result == end(_items))
|
|
|
|
? begin(_items)
|
|
|
|
: (fromView == tillView || includeFrom(fromView, fromState))
|
|
|
|
? result
|
|
|
|
: (result + 1);
|
2018-01-26 15:40:11 +00:00
|
|
|
}();
|
2018-01-27 16:41:06 +00:00
|
|
|
const auto till = [&] {
|
|
|
|
if (fromView == tillView) {
|
|
|
|
return (from == end(_items))
|
|
|
|
? from
|
|
|
|
: includeSingleItem(fromView, fromState, tillState)
|
|
|
|
? (from + 1)
|
|
|
|
: from;
|
|
|
|
}
|
|
|
|
const auto result = tillView ? ranges::find(
|
2018-01-26 15:40:11 +00:00
|
|
|
_items,
|
|
|
|
tillView,
|
2018-01-27 16:41:06 +00:00
|
|
|
[](auto view) { return view.get(); }) : end(_items);
|
|
|
|
return (result == end(_items))
|
|
|
|
? end(_items)
|
|
|
|
: includeTill(tillView, tillState)
|
|
|
|
? (result + 1)
|
|
|
|
: result;
|
|
|
|
}();
|
|
|
|
if (from < till) {
|
|
|
|
updateDragSelection(from, till);
|
|
|
|
} else {
|
|
|
|
clearDragSelection();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::updateDragSelection(
|
|
|
|
std::vector<not_null<Element*>>::const_iterator from,
|
|
|
|
std::vector<not_null<Element*>>::const_iterator till) {
|
|
|
|
Expects(from < till);
|
|
|
|
|
2019-07-24 11:13:51 +00:00
|
|
|
const auto &groups = session().data().groups();
|
2018-01-27 13:59:24 +00:00
|
|
|
const auto changeItem = [&](not_null<HistoryItem*> item, bool add) {
|
|
|
|
const auto itemId = item->fullId();
|
|
|
|
if (add) {
|
|
|
|
_dragSelected.emplace(itemId);
|
|
|
|
} else {
|
|
|
|
_dragSelected.remove(itemId);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const auto changeGroup = [&](not_null<HistoryItem*> item, bool add) {
|
|
|
|
if (const auto group = groups.find(item)) {
|
2021-09-08 10:53:54 +00:00
|
|
|
for (const auto &item : group->items) {
|
2019-08-08 22:39:42 +00:00
|
|
|
if (!_delegate->listIsItemGoodForSelection(item)) {
|
2018-01-27 16:41:06 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2021-09-08 10:53:54 +00:00
|
|
|
for (const auto &item : group->items) {
|
2018-01-27 13:59:24 +00:00
|
|
|
changeItem(item, add);
|
|
|
|
}
|
2019-08-08 22:39:42 +00:00
|
|
|
} else if (_delegate->listIsItemGoodForSelection(item)) {
|
2018-01-27 13:59:24 +00:00
|
|
|
changeItem(item, add);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const auto changeView = [&](not_null<Element*> view, bool add) {
|
2018-01-30 13:17:50 +00:00
|
|
|
if (!view->isHidden()) {
|
2018-01-27 13:59:24 +00:00
|
|
|
changeGroup(view->data(), add);
|
|
|
|
}
|
|
|
|
};
|
2018-01-26 15:40:11 +00:00
|
|
|
for (auto i = begin(_items); i != from; ++i) {
|
2018-01-27 13:59:24 +00:00
|
|
|
changeView(*i, false);
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
for (auto i = from; i != till; ++i) {
|
2018-01-27 13:59:24 +00:00
|
|
|
changeView(*i, true);
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
|
|
|
for (auto i = till; i != end(_items); ++i) {
|
2018-01-27 13:59:24 +00:00
|
|
|
changeView(*i, false);
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
2018-01-27 16:41:06 +00:00
|
|
|
|
|
|
|
ensureDragSelectAction(from, till);
|
|
|
|
update();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::ensureDragSelectAction(
|
|
|
|
std::vector<not_null<Element*>>::const_iterator from,
|
|
|
|
std::vector<not_null<Element*>>::const_iterator till) {
|
|
|
|
if (_dragSelectAction != DragSelectAction::None) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto start = _dragSelectDirectionUp ? (till - 1) : from;
|
|
|
|
const auto startId = (*start)->data()->fullId();
|
|
|
|
_dragSelectAction = _selected.contains(startId)
|
|
|
|
? DragSelectAction::Deselecting
|
|
|
|
: DragSelectAction::Selecting;
|
2018-01-26 15:40:11 +00:00
|
|
|
if (!_wasSelectedText
|
|
|
|
&& !_dragSelected.empty()
|
|
|
|
&& _dragSelectAction == DragSelectAction::Selecting) {
|
|
|
|
_wasSelectedText = true;
|
|
|
|
setFocus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::clearDragSelection() {
|
|
|
|
_dragSelectAction = DragSelectAction::None;
|
|
|
|
if (!_dragSelected.empty()) {
|
|
|
|
_dragSelected.clear();
|
|
|
|
update();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::mouseActionStart(
|
|
|
|
const QPoint &globalPosition,
|
|
|
|
Qt::MouseButton button) {
|
|
|
|
mouseActionUpdate(globalPosition);
|
|
|
|
if (button != Qt::LeftButton) {
|
|
|
|
return;
|
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
|
|
|
|
ClickHandler::pressed();
|
2018-01-26 15:40:11 +00:00
|
|
|
if (_pressState != _overState) {
|
|
|
|
if (_pressState.itemId != _overState.itemId) {
|
|
|
|
repaintItem(_pressState.itemId);
|
|
|
|
}
|
|
|
|
_pressState = _overState;
|
|
|
|
repaintItem(_overState.itemId);
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
2018-01-27 19:21:41 +00:00
|
|
|
_pressItemExact = _overItemExact;
|
|
|
|
const auto pressElement = _overElement;
|
2017-06-21 21:38:31 +00:00
|
|
|
|
|
|
|
_mouseAction = MouseAction::None;
|
2019-09-16 11:14:06 +00:00
|
|
|
_pressWasInactive = Ui::WasInactivePress(_controller->widget());
|
|
|
|
if (_pressWasInactive) {
|
|
|
|
Ui::MarkInactivePress(_controller->widget(), false);
|
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
|
|
|
|
if (ClickHandler::getPressed()) {
|
|
|
|
_mouseAction = MouseAction::PrepareDrag;
|
2018-01-27 13:59:24 +00:00
|
|
|
} else if (hasSelectedItems()) {
|
|
|
|
if (overSelectedItems()) {
|
|
|
|
_mouseAction = MouseAction::PrepareDrag;
|
|
|
|
} else if (!_pressWasInactive) {
|
|
|
|
_mouseAction = MouseAction::PrepareSelect;
|
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
2018-01-27 19:21:41 +00:00
|
|
|
if (_mouseAction == MouseAction::None && pressElement) {
|
2018-01-26 15:40:11 +00:00
|
|
|
validateTrippleClickStartTime();
|
2018-01-27 13:59:24 +00:00
|
|
|
TextState dragState;
|
2018-01-26 15:40:11 +00:00
|
|
|
auto startDistance = (globalPosition - _trippleClickPoint).manhattanLength();
|
|
|
|
auto validStartPoint = startDistance < QApplication::startDragDistance();
|
|
|
|
if (_trippleClickStartTime != 0 && validStartPoint) {
|
2018-01-27 13:59:24 +00:00
|
|
|
StateRequest request;
|
2019-06-12 13:26:04 +00:00
|
|
|
request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
|
2018-01-27 19:21:41 +00:00
|
|
|
dragState = pressElement->textState(_pressState.point, request);
|
2018-01-27 13:59:24 +00:00
|
|
|
if (dragState.cursor == CursorState::Text) {
|
2018-01-27 19:21:41 +00:00
|
|
|
setTextSelection(pressElement, TextSelection(
|
2018-01-26 15:40:11 +00:00
|
|
|
dragState.symbol,
|
|
|
|
dragState.symbol
|
|
|
|
));
|
2017-06-21 21:38:31 +00:00
|
|
|
_mouseTextSymbol = dragState.symbol;
|
|
|
|
_mouseAction = MouseAction::Selecting;
|
|
|
|
_mouseSelectType = TextSelectType::Paragraphs;
|
2018-01-26 15:40:11 +00:00
|
|
|
mouseActionUpdate();
|
2019-02-19 06:57:53 +00:00
|
|
|
_trippleClickStartTime = crl::now();
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
2018-01-27 19:21:41 +00:00
|
|
|
} else if (pressElement) {
|
2018-01-27 13:59:24 +00:00
|
|
|
StateRequest request;
|
2019-06-12 13:26:04 +00:00
|
|
|
request.flags = Ui::Text::StateRequest::Flag::LookupSymbol;
|
2018-01-27 19:21:41 +00:00
|
|
|
dragState = pressElement->textState(_pressState.point, request);
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
if (_mouseSelectType != TextSelectType::Paragraphs) {
|
2018-01-26 15:40:11 +00:00
|
|
|
_mouseTextSymbol = dragState.symbol;
|
|
|
|
if (isPressInSelectedText(dragState)) {
|
|
|
|
_mouseAction = MouseAction::PrepareDrag; // start text drag
|
|
|
|
} else if (!_pressWasInactive) {
|
2020-10-06 10:46:19 +00:00
|
|
|
if (requiredToStartDragging(pressElement)
|
|
|
|
&& _pressState.pointState != PointState::Outside) {
|
2018-01-26 15:40:11 +00:00
|
|
|
_mouseAction = MouseAction::PrepareDrag;
|
|
|
|
} else {
|
2017-06-22 15:11:41 +00:00
|
|
|
if (dragState.afterSymbol) ++_mouseTextSymbol;
|
2018-01-27 16:41:06 +00:00
|
|
|
if (!hasSelectedItems()
|
|
|
|
&& _overState.pointState != PointState::Outside) {
|
2018-01-27 19:21:41 +00:00
|
|
|
setTextSelection(pressElement, TextSelection(
|
2018-01-26 15:40:11 +00:00
|
|
|
_mouseTextSymbol,
|
|
|
|
_mouseTextSymbol));
|
|
|
|
_mouseAction = MouseAction::Selecting;
|
|
|
|
} else {
|
|
|
|
_mouseAction = MouseAction::PrepareSelect;
|
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-01-27 19:21:41 +00:00
|
|
|
if (!pressElement) {
|
2017-06-21 21:38:31 +00:00
|
|
|
_mouseAction = MouseAction::None;
|
|
|
|
} else if (_mouseAction == MouseAction::None) {
|
2018-01-26 15:40:11 +00:00
|
|
|
mouseActionCancel();
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
void ListWidget::mouseActionUpdate(const QPoint &globalPosition) {
|
|
|
|
_mousePosition = globalPosition;
|
|
|
|
mouseActionUpdate();
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
void ListWidget::mouseActionCancel() {
|
2018-01-27 13:59:24 +00:00
|
|
|
_pressState = MouseState();
|
2018-01-27 19:21:41 +00:00
|
|
|
_pressItemExact = nullptr;
|
2017-06-21 21:38:31 +00:00
|
|
|
_mouseAction = MouseAction::None;
|
2018-01-26 15:40:11 +00:00
|
|
|
clearDragSelection();
|
2017-06-21 21:38:31 +00:00
|
|
|
_wasSelectedText = false;
|
2018-01-09 17:08:31 +00:00
|
|
|
//_widget->noSelectingScroll(); // #TODO select scroll
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
void ListWidget::mouseActionFinish(
|
|
|
|
const QPoint &globalPosition,
|
|
|
|
Qt::MouseButton button) {
|
|
|
|
mouseActionUpdate(globalPosition);
|
2017-06-21 21:38:31 +00:00
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
auto pressState = base::take(_pressState);
|
2018-01-27 19:21:41 +00:00
|
|
|
base::take(_pressItemExact);
|
2018-01-26 15:40:11 +00:00
|
|
|
repaintItem(pressState.itemId);
|
|
|
|
|
2018-01-27 13:59:24 +00:00
|
|
|
const auto toggleByHandler = [&](const ClickHandlerPtr &handler) {
|
2021-02-16 13:50:41 +00:00
|
|
|
// If we are in selecting items mode perhaps we want to
|
|
|
|
// toggle selection instead of activating the pressed link.
|
|
|
|
return _overElement
|
|
|
|
&& _overElement->toggleSelectionByHandlerClick(handler);
|
2018-01-27 13:59:24 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
auto activated = ClickHandler::unpressed();
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
auto simpleSelectionChange = pressState.itemId
|
|
|
|
&& !_pressWasInactive
|
|
|
|
&& (button != Qt::RightButton)
|
2018-01-27 13:59:24 +00:00
|
|
|
&& (_mouseAction == MouseAction::PrepareSelect
|
|
|
|
|| _mouseAction == MouseAction::PrepareDrag);
|
2018-01-26 15:40:11 +00:00
|
|
|
auto needItemSelectionToggle = simpleSelectionChange
|
2018-01-27 13:59:24 +00:00
|
|
|
&& (!activated || toggleByHandler(activated))
|
2018-01-26 15:40:11 +00:00
|
|
|
&& hasSelectedItems();
|
|
|
|
auto needTextSelectionClear = simpleSelectionChange
|
|
|
|
&& hasSelectedText();
|
2017-06-21 21:38:31 +00:00
|
|
|
|
|
|
|
_wasSelectedText = false;
|
|
|
|
|
2018-01-27 13:59:24 +00:00
|
|
|
if (_mouseAction == MouseAction::Dragging
|
|
|
|
|| _mouseAction == MouseAction::Selecting
|
|
|
|
|| needItemSelectionToggle) {
|
|
|
|
activated = nullptr;
|
|
|
|
} else if (activated) {
|
2017-06-21 21:38:31 +00:00
|
|
|
mouseActionCancel();
|
2019-09-16 11:14:06 +00:00
|
|
|
ActivateClickHandler(window(), activated, {
|
2018-07-09 18:13:48 +00:00
|
|
|
button,
|
2020-11-10 16:38:21 +00:00
|
|
|
QVariant::fromValue(ClickHandlerContext{
|
|
|
|
.itemId = pressState.itemId,
|
|
|
|
.elementDelegate = [weak = Ui::MakeWeak(this)] {
|
|
|
|
return weak
|
|
|
|
? (ElementDelegate*)weak
|
|
|
|
: nullptr;
|
|
|
|
},
|
2021-02-25 15:12:51 +00:00
|
|
|
.sessionWindow = base::make_weak(_controller.get()),
|
2020-11-10 16:38:21 +00:00
|
|
|
})
|
2018-07-09 18:13:48 +00:00
|
|
|
});
|
2017-06-21 21:38:31 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
if (needItemSelectionToggle) {
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto item = session().data().message(pressState.itemId)) {
|
2018-01-27 13:59:24 +00:00
|
|
|
clearTextSelection();
|
|
|
|
if (pressState.pointState == PointState::GroupPart) {
|
|
|
|
changeSelection(
|
|
|
|
_selected,
|
|
|
|
_overItemExact ? _overItemExact : item,
|
|
|
|
SelectAction::Invert);
|
|
|
|
} else {
|
|
|
|
changeSelectionAsGroup(
|
|
|
|
_selected,
|
|
|
|
item,
|
|
|
|
SelectAction::Invert);
|
|
|
|
}
|
|
|
|
pushSelectedItems();
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
} else if (needTextSelectionClear) {
|
|
|
|
clearTextSelection();
|
2017-06-21 21:38:31 +00:00
|
|
|
} else if (_mouseAction == MouseAction::Selecting) {
|
2018-01-26 15:40:11 +00:00
|
|
|
if (!_dragSelected.empty()) {
|
|
|
|
applyDragSelection();
|
|
|
|
} else if (_selectedTextItem && !_pressWasInactive) {
|
|
|
|
if (_selectedTextRange.from == _selectedTextRange.to) {
|
|
|
|
clearTextSelection();
|
2021-02-03 03:31:11 +00:00
|
|
|
_controller->widget()->setInnerFocus();
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_mouseAction = MouseAction::None;
|
|
|
|
_mouseSelectType = TextSelectType::Letters;
|
2018-01-09 17:08:31 +00:00
|
|
|
//_widget->noSelectingScroll(); // #TODO select scroll
|
2017-06-21 21:38:31 +00:00
|
|
|
|
2020-11-06 05:12:18 +00:00
|
|
|
if (QGuiApplication::clipboard()->supportsSelection()
|
|
|
|
&& _selectedTextItem
|
2018-01-26 15:40:11 +00:00
|
|
|
&& _selectedTextRange.from != _selectedTextRange.to) {
|
|
|
|
if (const auto view = viewForItem(_selectedTextItem)) {
|
2019-09-16 11:14:06 +00:00
|
|
|
TextUtilities::SetClipboardText(
|
2018-01-29 18:09:08 +00:00
|
|
|
view->selectedText(_selectedTextRange),
|
2018-01-26 15:40:11 +00:00
|
|
|
QClipboard::Selection);
|
2020-11-06 05:12:18 +00:00
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
void ListWidget::mouseActionUpdate() {
|
2017-06-21 21:38:31 +00:00
|
|
|
auto mousePosition = mapFromGlobal(_mousePosition);
|
2021-01-23 03:29:50 +00:00
|
|
|
auto point = QPoint(
|
|
|
|
std::clamp(mousePosition.x(), 0, width()),
|
|
|
|
std::clamp(mousePosition.y(), _visibleTop, _visibleBottom));
|
2017-06-21 21:38:31 +00:00
|
|
|
|
2018-01-10 13:13:33 +00:00
|
|
|
const auto view = strictFindItemByY(point.y());
|
|
|
|
const auto item = view ? view->data().get() : nullptr;
|
2018-01-26 15:40:11 +00:00
|
|
|
const auto itemPoint = mapPointToItem(point, view);
|
2018-01-27 16:41:06 +00:00
|
|
|
_overState = MouseState(
|
2018-01-26 15:40:11 +00:00
|
|
|
item ? item->fullId() : FullMsgId(),
|
|
|
|
view ? view->height() : 0,
|
|
|
|
itemPoint,
|
2018-01-27 16:41:06 +00:00
|
|
|
view ? view->pointState(itemPoint) : PointState::Outside);
|
2018-01-27 13:59:24 +00:00
|
|
|
if (_overElement != view) {
|
|
|
|
repaintItem(_overElement);
|
|
|
|
_overElement = view;
|
|
|
|
repaintItem(_overElement);
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
|
2018-01-27 13:59:24 +00:00
|
|
|
TextState dragState;
|
2017-06-21 21:38:31 +00:00
|
|
|
ClickHandlerHost *lnkhost = nullptr;
|
2018-01-27 13:59:24 +00:00
|
|
|
auto inTextSelection = (_overState.pointState != PointState::Outside)
|
2018-01-26 15:40:11 +00:00
|
|
|
&& (_overState.itemId == _pressState.itemId)
|
|
|
|
&& hasSelectedText();
|
2018-01-10 13:13:33 +00:00
|
|
|
if (view) {
|
2018-01-26 15:40:11 +00:00
|
|
|
auto cursorDeltaLength = [&] {
|
2018-01-27 13:59:24 +00:00
|
|
|
auto cursorDelta = (_overState.point - _pressState.point);
|
2018-01-26 15:40:11 +00:00
|
|
|
return cursorDelta.manhattanLength();
|
|
|
|
};
|
|
|
|
auto dragStartLength = [] {
|
|
|
|
return QApplication::startDragDistance();
|
|
|
|
};
|
|
|
|
if (_overState.itemId != _pressState.itemId
|
|
|
|
|| cursorDeltaLength() >= dragStartLength()) {
|
2017-06-21 21:38:31 +00:00
|
|
|
if (_mouseAction == MouseAction::PrepareDrag) {
|
|
|
|
_mouseAction = MouseAction::Dragging;
|
|
|
|
InvokeQueued(this, [this] { performDrag(); });
|
2018-01-26 15:40:11 +00:00
|
|
|
} else if (_mouseAction == MouseAction::PrepareSelect) {
|
|
|
|
_mouseAction = MouseAction::Selecting;
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
}
|
2018-01-27 13:59:24 +00:00
|
|
|
StateRequest request;
|
2017-06-22 01:31:02 +00:00
|
|
|
if (_mouseAction == MouseAction::Selecting) {
|
2019-06-12 13:26:04 +00:00
|
|
|
request.flags |= Ui::Text::StateRequest::Flag::LookupSymbol;
|
2017-06-22 01:31:02 +00:00
|
|
|
} else {
|
2018-01-26 15:40:11 +00:00
|
|
|
inTextSelection = false;
|
2017-06-22 01:31:02 +00:00
|
|
|
}
|
2018-02-21 23:59:56 +00:00
|
|
|
|
|
|
|
const auto dateHeight = st::msgServicePadding.bottom()
|
|
|
|
+ st::msgServiceFont->height
|
|
|
|
+ st::msgServicePadding.top();
|
2019-04-02 09:13:30 +00:00
|
|
|
const auto scrollDateOpacity = _scrollDateOpacity.value(_scrollDateShown ? 1. : 0.);
|
2018-02-21 23:59:56 +00:00
|
|
|
enumerateDates([&](not_null<Element*> view, int itemtop, int dateTop) {
|
|
|
|
// stop enumeration if the date is above our point
|
|
|
|
if (dateTop + dateHeight <= point.y()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto displayDate = view->displayDate();
|
|
|
|
auto dateInPlace = displayDate;
|
|
|
|
if (dateInPlace) {
|
|
|
|
const auto correctDateTop = itemtop + st::msgServiceMargin.top();
|
|
|
|
dateInPlace = (dateTop < correctDateTop + dateHeight);
|
|
|
|
}
|
|
|
|
|
|
|
|
// stop enumeration if we've found a date under the cursor
|
|
|
|
if (dateTop <= point.y()) {
|
|
|
|
auto opacity = (dateInPlace/* || noFloatingDate*/) ? 1. : scrollDateOpacity;
|
|
|
|
if (opacity > 0.) {
|
|
|
|
auto dateWidth = 0;
|
|
|
|
if (const auto date = view->Get<HistoryView::DateBadge>()) {
|
|
|
|
dateWidth = date->width;
|
|
|
|
} else {
|
|
|
|
dateWidth = st::msgServiceFont->width(langDayOfMonthFull(view->dateTime().date()));
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
2018-02-21 23:59:56 +00:00
|
|
|
dateWidth += st::msgServicePadding.left() + st::msgServicePadding.right();
|
|
|
|
auto dateLeft = st::msgServiceMargin.left();
|
|
|
|
auto maxwidth = view->width();
|
2021-05-27 00:44:12 +00:00
|
|
|
if (_isChatWide) {
|
2018-02-21 23:59:56 +00:00
|
|
|
maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
|
|
|
|
}
|
|
|
|
auto widthForDate = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left();
|
2018-01-26 15:40:11 +00:00
|
|
|
|
2018-02-21 23:59:56 +00:00
|
|
|
dateLeft += (widthForDate - dateWidth) / 2;
|
2018-01-26 15:40:11 +00:00
|
|
|
|
2018-02-21 23:59:56 +00:00
|
|
|
if (point.x() >= dateLeft && point.x() < dateLeft + dateWidth) {
|
|
|
|
_scrollDateLink = _delegate->listDateLink(view);
|
2018-01-27 13:59:24 +00:00
|
|
|
dragState = TextState(
|
|
|
|
nullptr,
|
2018-02-21 23:59:56 +00:00
|
|
|
_scrollDateLink);
|
2019-07-24 11:13:51 +00:00
|
|
|
_overItemExact = session().data().message(dragState.itemId);
|
2018-01-26 15:40:11 +00:00
|
|
|
lnkhost = view;
|
|
|
|
}
|
2018-02-21 23:59:56 +00:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
if (!dragState.link) {
|
|
|
|
dragState = view->textState(itemPoint, request);
|
2019-07-24 11:13:51 +00:00
|
|
|
_overItemExact = session().data().message(dragState.itemId);
|
2018-02-21 23:59:56 +00:00
|
|
|
lnkhost = view;
|
|
|
|
if (!dragState.link
|
|
|
|
&& itemPoint.x() >= st::historyPhotoLeft
|
|
|
|
&& itemPoint.x() < st::historyPhotoLeft + st::msgPhotoSize) {
|
|
|
|
if (view->hasFromPhoto()) {
|
|
|
|
enumerateUserpics([&](not_null<Element*> view, int userpicTop) {
|
|
|
|
// stop enumeration if the userpic is below our point
|
|
|
|
if (userpicTop > point.y()) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// stop enumeration if we've found a userpic under the cursor
|
|
|
|
if (point.y() >= userpicTop && point.y() < userpicTop + st::msgPhotoSize) {
|
|
|
|
const auto message = view->data()->toHistoryMessage();
|
|
|
|
Assert(message != nullptr);
|
|
|
|
|
2021-01-20 14:09:45 +00:00
|
|
|
dragState = TextState(nullptr, view->fromPhotoLink());
|
2019-07-24 11:13:51 +00:00
|
|
|
_overItemExact = session().data().message(dragState.itemId);
|
2018-02-21 23:59:56 +00:00
|
|
|
lnkhost = view;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-06-21 23:54:38 +00:00
|
|
|
auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost);
|
|
|
|
if (lnkChanged || dragState.cursor != _mouseCursorState) {
|
2017-06-21 21:38:31 +00:00
|
|
|
Ui::Tooltip::Hide();
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
if (dragState.link
|
2018-01-27 13:59:24 +00:00
|
|
|
|| dragState.cursor == CursorState::Date
|
|
|
|
|| dragState.cursor == CursorState::Forwarded) {
|
2017-06-21 21:38:31 +00:00
|
|
|
Ui::Tooltip::Show(1000, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (_mouseAction == MouseAction::None) {
|
2017-06-21 23:54:38 +00:00
|
|
|
_mouseCursorState = dragState.cursor;
|
2018-01-26 15:40:11 +00:00
|
|
|
auto cursor = computeMouseCursor();
|
|
|
|
if (_cursor != cursor) {
|
|
|
|
setCursor((_cursor = cursor));
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
2018-01-10 13:13:33 +00:00
|
|
|
} else if (view) {
|
2017-06-21 21:38:31 +00:00
|
|
|
if (_mouseAction == MouseAction::Selecting) {
|
2018-01-26 15:40:11 +00:00
|
|
|
if (inTextSelection) {
|
2017-06-21 23:54:38 +00:00
|
|
|
auto second = dragState.symbol;
|
2018-01-26 15:40:11 +00:00
|
|
|
if (dragState.afterSymbol
|
|
|
|
&& _mouseSelectType == TextSelectType::Letters) {
|
2017-06-21 21:38:31 +00:00
|
|
|
++second;
|
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
auto selection = TextSelection(
|
|
|
|
qMin(second, _mouseTextSymbol),
|
|
|
|
qMax(second, _mouseTextSymbol)
|
|
|
|
);
|
2017-06-23 19:28:42 +00:00
|
|
|
if (_mouseSelectType != TextSelectType::Letters) {
|
2018-01-26 15:40:11 +00:00
|
|
|
selection = view->adjustSelection(
|
|
|
|
selection,
|
|
|
|
_mouseSelectType);
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
setTextSelection(view, selection);
|
|
|
|
clearDragSelection();
|
|
|
|
} else if (_pressState.itemId) {
|
|
|
|
updateDragSelection();
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
} else if (_mouseAction == MouseAction::Dragging) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Voice message seek support.
|
2018-01-27 13:59:24 +00:00
|
|
|
if (_pressState.pointState != PointState::Outside
|
|
|
|
&& ClickHandler::getPressed()) {
|
2019-07-24 11:13:51 +00:00
|
|
|
if (const auto item = session().data().message(_pressState.itemId)) {
|
2018-01-26 15:40:11 +00:00
|
|
|
if (const auto view = viewForItem(item)) {
|
|
|
|
auto adjustedPoint = mapPointToItem(point, view);
|
|
|
|
view->updatePressed(adjustedPoint);
|
|
|
|
}
|
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
//if (_mouseAction == MouseAction::Selecting) {
|
|
|
|
// _widget->checkSelectingScroll(mousePos);
|
|
|
|
//} else {
|
|
|
|
// _widget->noSelectingScroll();
|
2018-01-09 17:08:31 +00:00
|
|
|
//} // #TODO select scroll
|
2018-01-26 15:40:11 +00:00
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
style::cursor ListWidget::computeMouseCursor() const {
|
|
|
|
if (ClickHandler::getPressed() || ClickHandler::getActive()) {
|
|
|
|
return style::cur_pointer;
|
|
|
|
} else if (!hasSelectedItems()
|
2018-01-27 13:59:24 +00:00
|
|
|
&& (_mouseCursorState == CursorState::Text)) {
|
2018-01-26 15:40:11 +00:00
|
|
|
return style::cur_text;
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
2018-01-26 15:40:11 +00:00
|
|
|
return style::cur_default;
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
|
2018-01-27 19:21:41 +00:00
|
|
|
std::unique_ptr<QMimeData> ListWidget::prepareDrag() {
|
|
|
|
if (_mouseAction != MouseAction::Dragging) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
auto pressedHandler = ClickHandler::getPressed();
|
|
|
|
if (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.get())) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
|
2019-07-24 11:13:51 +00:00
|
|
|
const auto pressedItem = session().data().message(_pressState.itemId);
|
2018-01-27 19:21:41 +00:00
|
|
|
const auto pressedView = viewForItem(pressedItem);
|
|
|
|
const auto uponSelected = pressedView && isInsideSelection(
|
|
|
|
pressedView,
|
|
|
|
_pressItemExact ? _pressItemExact : pressedItem,
|
|
|
|
_pressState);
|
|
|
|
|
2019-04-08 15:10:06 +00:00
|
|
|
auto urls = QList<QUrl>();
|
|
|
|
const auto selectedText = [&] {
|
2018-01-27 19:21:41 +00:00
|
|
|
if (uponSelected) {
|
|
|
|
return getSelectedText();
|
|
|
|
} else if (pressedHandler) {
|
2019-04-08 15:10:06 +00:00
|
|
|
//if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') {
|
|
|
|
// urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o
|
|
|
|
//}
|
|
|
|
return TextForMimeData::Simple(pressedHandler->dragText());
|
|
|
|
}
|
|
|
|
return TextForMimeData();
|
2018-01-27 19:21:41 +00:00
|
|
|
}();
|
2019-09-16 11:14:06 +00:00
|
|
|
if (auto mimeData = TextUtilities::MimeDataFromText(selectedText)) {
|
2018-01-27 19:21:41 +00:00
|
|
|
clearDragSelection();
|
|
|
|
// _widget->noSelectingScroll(); #TODO scroll
|
|
|
|
|
|
|
|
if (!urls.isEmpty()) {
|
|
|
|
mimeData->setUrls(urls);
|
|
|
|
}
|
2021-05-26 21:04:18 +00:00
|
|
|
if (uponSelected && !_controller->adaptive().isOneColumn()) {
|
2018-01-27 19:21:41 +00:00
|
|
|
const auto canForwardAll = [&] {
|
|
|
|
for (const auto &[itemId, data] : _selected) {
|
|
|
|
if (!data.canForward) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}();
|
|
|
|
auto items = canForwardAll
|
2020-10-26 08:54:59 +00:00
|
|
|
? collectSelectedIds()
|
2018-01-27 19:21:41 +00:00
|
|
|
: MessageIdsList();
|
|
|
|
if (!items.empty()) {
|
2019-07-24 11:13:51 +00:00
|
|
|
session().data().setMimeForwardIds(std::move(items));
|
2018-01-27 19:21:41 +00:00
|
|
|
mimeData->setData(qsl("application/x-td-forward"), "1");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return mimeData;
|
|
|
|
} else if (pressedView) {
|
|
|
|
auto forwardIds = MessageIdsList();
|
|
|
|
const auto exactItem = _pressItemExact
|
|
|
|
? _pressItemExact
|
|
|
|
: pressedItem;
|
|
|
|
if (_mouseCursorState == CursorState::Date) {
|
2019-08-30 13:17:46 +00:00
|
|
|
if (_overElement->data()->allowsForward()) {
|
|
|
|
forwardIds = session().data().itemOrItsGroup(
|
|
|
|
_overElement->data());
|
|
|
|
}
|
2018-01-27 19:21:41 +00:00
|
|
|
} else if (_pressState.pointState == PointState::GroupPart) {
|
2019-08-30 13:17:46 +00:00
|
|
|
if (exactItem->allowsForward()) {
|
|
|
|
forwardIds = MessageIdsList(1, exactItem->fullId());
|
|
|
|
}
|
2018-01-27 19:21:41 +00:00
|
|
|
} else if (const auto media = pressedView->media()) {
|
2019-08-30 13:17:46 +00:00
|
|
|
if (pressedView->data()->allowsForward()
|
|
|
|
&& (media->dragItemByHandler(pressedHandler)
|
|
|
|
|| media->dragItem())) {
|
2018-01-27 19:21:41 +00:00
|
|
|
forwardIds = MessageIdsList(1, exactItem->fullId());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (forwardIds.empty()) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
2019-07-24 11:13:51 +00:00
|
|
|
session().data().setMimeForwardIds(std::move(forwardIds));
|
2018-01-27 19:21:41 +00:00
|
|
|
auto result = std::make_unique<QMimeData>();
|
|
|
|
result->setData(qsl("application/x-td-forward"), "1");
|
|
|
|
if (const auto media = pressedView->media()) {
|
|
|
|
if (const auto document = media->getDocument()) {
|
2020-04-09 14:02:09 +00:00
|
|
|
const auto filepath = document->filepath(true);
|
2018-01-27 19:21:41 +00:00
|
|
|
if (!filepath.isEmpty()) {
|
|
|
|
QList<QUrl> urls;
|
|
|
|
urls.push_back(QUrl::fromLocalFile(filepath));
|
|
|
|
result->setUrls(urls);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::performDrag() {
|
|
|
|
if (auto mimeData = prepareDrag()) {
|
|
|
|
// This call enters event loop and can destroy any QObject.
|
2021-05-25 12:42:26 +00:00
|
|
|
_controller->widget()->launchDrag(
|
|
|
|
std::move(mimeData),
|
|
|
|
crl::guard(this, [=] { mouseActionUpdate(QCursor::pos()); }));;
|
2018-01-27 19:21:41 +00:00
|
|
|
}
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
|
2018-01-11 19:33:26 +00:00
|
|
|
int ListWidget::itemTop(not_null<const Element*> view) const {
|
2018-01-10 13:13:33 +00:00
|
|
|
return _itemsTop + view->y();
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
|
2018-01-11 19:33:26 +00:00
|
|
|
void ListWidget::repaintItem(const Element *view) {
|
2018-01-10 13:13:33 +00:00
|
|
|
if (!view) {
|
2017-06-21 21:38:31 +00:00
|
|
|
return;
|
|
|
|
}
|
2020-01-24 09:50:20 +00:00
|
|
|
const auto top = itemTop(view);
|
|
|
|
const auto range = view->verticalRepaintRange();
|
|
|
|
update(0, top + range.top, width(), range.height);
|
2017-06-21 21:38:31 +00:00
|
|
|
}
|
|
|
|
|
2018-01-26 15:40:11 +00:00
|
|
|
void ListWidget::repaintItem(FullMsgId itemId) {
|
|
|
|
if (const auto view = viewForItem(itemId)) {
|
|
|
|
repaintItem(view);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-21 19:21:08 +00:00
|
|
|
void ListWidget::resizeItem(not_null<Element*> view) {
|
|
|
|
const auto index = ranges::find(_items, view) - begin(_items);
|
|
|
|
if (index < int(_items.size())) {
|
|
|
|
refreshAttachmentsAtIndex(index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::refreshAttachmentsAtIndex(int index) {
|
|
|
|
Expects(index >= 0 && index < _items.size());
|
|
|
|
|
|
|
|
const auto from = [&] {
|
|
|
|
if (index > 0) {
|
|
|
|
for (auto i = index - 1; i != 0; --i) {
|
2018-01-30 13:17:50 +00:00
|
|
|
if (!_items[i]->isHidden()) {
|
2018-01-21 19:21:08 +00:00
|
|
|
return i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return index;
|
|
|
|
}();
|
|
|
|
const auto till = [&] {
|
2018-01-27 13:59:24 +00:00
|
|
|
const auto count = int(_items.size());
|
|
|
|
for (auto i = index + 1; i != count; ++i) {
|
2018-01-30 13:17:50 +00:00
|
|
|
if (!_items[i]->isHidden()) {
|
2018-01-21 19:21:08 +00:00
|
|
|
return i + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return index + 1;
|
|
|
|
}();
|
|
|
|
refreshAttachmentsFromTill(from, till);
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::refreshAttachmentsFromTill(int from, int till) {
|
|
|
|
Expects(from >= 0 && from <= till && till <= int(_items.size()));
|
|
|
|
|
|
|
|
if (from == till) {
|
2018-02-05 20:19:51 +00:00
|
|
|
updateSize();
|
2018-01-21 19:21:08 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
auto view = _items[from].get();
|
|
|
|
for (auto i = from + 1; i != till; ++i) {
|
|
|
|
const auto next = _items[i].get();
|
2018-01-30 13:17:50 +00:00
|
|
|
if (next->isHidden()) {
|
2018-01-21 19:21:08 +00:00
|
|
|
next->setDisplayDate(false);
|
|
|
|
} else {
|
2018-02-03 19:52:35 +00:00
|
|
|
const auto viewDate = view->dateTime();
|
|
|
|
const auto nextDate = next->dateTime();
|
2018-01-21 19:21:08 +00:00
|
|
|
next->setDisplayDate(nextDate.date() != viewDate.date());
|
|
|
|
auto attached = next->computeIsAttachToPrevious(view);
|
|
|
|
next->setAttachToPrevious(attached);
|
|
|
|
view->setAttachToNext(attached);
|
|
|
|
view = next;
|
|
|
|
}
|
|
|
|
}
|
2020-10-22 07:53:56 +00:00
|
|
|
if (till == int(_items.size())) {
|
|
|
|
_items.back()->setAttachToNext(false);
|
|
|
|
}
|
2018-01-21 19:21:08 +00:00
|
|
|
updateSize();
|
|
|
|
}
|
|
|
|
|
2018-01-19 17:10:58 +00:00
|
|
|
void ListWidget::refreshItem(not_null<const Element*> view) {
|
2018-01-21 09:05:30 +00:00
|
|
|
const auto i = ranges::find(_items, view);
|
|
|
|
const auto index = i - begin(_items);
|
|
|
|
if (index < int(_items.size())) {
|
|
|
|
const auto item = view->data();
|
2018-03-01 17:17:39 +00:00
|
|
|
const auto was = [&]() -> std::unique_ptr<Element> {
|
|
|
|
if (const auto i = _views.find(item); i != end(_views)) {
|
|
|
|
auto result = std::move(i->second);
|
|
|
|
_views.erase(i);
|
2018-03-06 17:07:42 +00:00
|
|
|
return result;
|
2018-03-01 17:17:39 +00:00
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}();
|
2018-01-21 09:05:30 +00:00
|
|
|
const auto [i, ok] = _views.emplace(
|
|
|
|
item,
|
2018-01-21 14:49:42 +00:00
|
|
|
item->createView(this));
|
2018-01-21 09:05:30 +00:00
|
|
|
const auto now = i->second.get();
|
|
|
|
_items[index] = now;
|
|
|
|
|
|
|
|
viewReplaced(view, i->second.get());
|
|
|
|
|
2018-01-21 19:21:08 +00:00
|
|
|
refreshAttachmentsAtIndex(index);
|
2018-01-21 09:05:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::viewReplaced(not_null<const Element*> was, Element *now) {
|
|
|
|
if (_visibleTopItem == was) _visibleTopItem = now;
|
|
|
|
if (_scrollDateLastItem == was) _scrollDateLastItem = now;
|
2018-01-27 13:59:24 +00:00
|
|
|
if (_overElement == was) _overElement = now;
|
2020-09-15 15:30:34 +00:00
|
|
|
if (_bar.element == was.get()) {
|
|
|
|
const auto bar = _bar.element->Get<UnreadBar>();
|
|
|
|
_bar.element = now;
|
2020-02-20 14:16:42 +00:00
|
|
|
if (now && bar) {
|
2020-09-15 15:30:34 +00:00
|
|
|
_bar.element->createUnreadBar(_barText.value());
|
2018-02-02 12:51:18 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-19 17:01:39 +00:00
|
|
|
const auto i = _itemRevealPending.find(was);
|
|
|
|
if (i != end(_itemRevealPending)) {
|
|
|
|
_itemRevealPending.erase(i);
|
|
|
|
if (now) {
|
|
|
|
_itemRevealPending.emplace(now);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const auto j = _itemRevealAnimations.find(was);
|
|
|
|
if (j != end(_itemRevealAnimations)) {
|
|
|
|
auto data = std::move(j->second);
|
|
|
|
_itemRevealAnimations.erase(j);
|
|
|
|
if (now) {
|
|
|
|
_itemRevealAnimations.emplace(now, std::move(data));
|
|
|
|
} else {
|
|
|
|
revealItemsCallback();
|
|
|
|
}
|
|
|
|
}
|
2018-01-21 09:05:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::itemRemoved(not_null<const HistoryItem*> item) {
|
2018-01-26 15:40:11 +00:00
|
|
|
if (_selectedTextItem == item) {
|
|
|
|
clearTextSelection();
|
|
|
|
}
|
2018-01-27 13:59:24 +00:00
|
|
|
if (_overItemExact == item) {
|
|
|
|
_overItemExact = nullptr;
|
|
|
|
}
|
2018-01-27 19:21:41 +00:00
|
|
|
if (_pressItemExact == item) {
|
|
|
|
_pressItemExact = nullptr;
|
|
|
|
}
|
2018-01-21 09:05:30 +00:00
|
|
|
const auto i = _views.find(item);
|
|
|
|
if (i == end(_views)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto view = i->second.get();
|
|
|
|
_items.erase(
|
|
|
|
ranges::remove(_items, view, [](auto view) { return view.get(); }),
|
|
|
|
end(_items));
|
|
|
|
viewReplaced(view, nullptr);
|
|
|
|
_views.erase(i);
|
|
|
|
|
|
|
|
updateItemsGeometry();
|
2018-01-19 17:10:58 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
QPoint ListWidget::mapPointToItem(
|
|
|
|
QPoint point,
|
2018-01-11 19:33:26 +00:00
|
|
|
const Element *view) const {
|
2018-01-10 13:13:33 +00:00
|
|
|
if (!view) {
|
2017-06-21 21:38:31 +00:00
|
|
|
return QPoint();
|
|
|
|
}
|
2018-01-10 13:13:33 +00:00
|
|
|
return point - QPoint(0, itemTop(view));
|
2017-06-18 11:08:14 +00:00
|
|
|
}
|
|
|
|
|
2020-06-01 09:24:50 +00:00
|
|
|
rpl::producer<FullMsgId> ListWidget::editMessageRequested() const {
|
|
|
|
return _requestedToEditMessage.events();
|
|
|
|
}
|
|
|
|
|
2021-01-31 04:22:46 +00:00
|
|
|
void ListWidget::editMessageRequestNotify(FullMsgId item) const {
|
2020-06-01 09:24:50 +00:00
|
|
|
_requestedToEditMessage.fire(std::move(item));
|
|
|
|
}
|
|
|
|
|
2021-01-31 04:22:46 +00:00
|
|
|
bool ListWidget::lastMessageEditRequestNotify() const {
|
|
|
|
const auto now = base::unixtime::now();
|
|
|
|
auto proj = [&](not_null<Element*> view) {
|
|
|
|
return view->data()->allowsEdit(now);
|
|
|
|
};
|
2021-03-13 12:12:08 +00:00
|
|
|
const auto &list = ranges::views::reverse(_items);
|
2021-01-31 04:22:46 +00:00
|
|
|
const auto it = ranges::find_if(list, std::move(proj));
|
|
|
|
if (it == end(list)) {
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
const auto item =
|
|
|
|
session().data().groups().findItemToEdit((*it)->data()).get();
|
|
|
|
editMessageRequestNotify(item->fullId());
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-01 06:07:37 +00:00
|
|
|
rpl::producer<FullMsgId> ListWidget::replyToMessageRequested() const {
|
|
|
|
return _requestedToReplyToMessage.events();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::replyToMessageRequestNotify(FullMsgId item) {
|
|
|
|
_requestedToReplyToMessage.fire(std::move(item));
|
|
|
|
}
|
|
|
|
|
2020-09-08 11:19:44 +00:00
|
|
|
rpl::producer<FullMsgId> ListWidget::readMessageRequested() const {
|
|
|
|
return _requestedToReadMessage.events();
|
|
|
|
}
|
|
|
|
|
2021-01-31 20:49:25 +00:00
|
|
|
rpl::producer<FullMsgId> ListWidget::showMessageRequested() const {
|
|
|
|
return _requestedToShowMessage.events();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ListWidget::replyNextMessage(FullMsgId fullId, bool next) {
|
|
|
|
const auto reply = [&](Element *view) {
|
|
|
|
if (view) {
|
|
|
|
const auto newFullId = view->data()->fullId();
|
|
|
|
replyToMessageRequestNotify(newFullId);
|
|
|
|
_requestedToShowMessage.fire_copy(newFullId);
|
|
|
|
} else {
|
|
|
|
replyToMessageRequestNotify(FullMsgId());
|
|
|
|
clearHighlightedMessage();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const auto replyFirst = [&] {
|
|
|
|
reply(next ? nullptr : _items.back().get());
|
|
|
|
};
|
|
|
|
if (!fullId) {
|
|
|
|
replyFirst();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto proj = [&](not_null<Element*> view) {
|
|
|
|
return view->data()->fullId() == fullId;
|
|
|
|
};
|
2021-03-13 12:12:08 +00:00
|
|
|
const auto &list = ranges::views::reverse(_items);
|
2021-01-31 20:49:25 +00:00
|
|
|
const auto it = ranges::find_if(list, std::move(proj));
|
|
|
|
if (it == end(list)) {
|
|
|
|
replyFirst();
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
const auto nextIt = it + (next ? -1 : 1);
|
|
|
|
if (nextIt == end(list)) {
|
|
|
|
return;
|
|
|
|
} else if (next && (it == begin(list))) {
|
|
|
|
reply(nullptr);
|
|
|
|
} else {
|
|
|
|
reply(nextIt->get());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-20 18:18:53 +00:00
|
|
|
void ListWidget::setEmptyInfoWidget(base::unique_qptr<Ui::RpWidget> &&w) {
|
|
|
|
_emptyInfo = std::move(w);
|
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
ListWidget::~ListWidget() = default;
|
2017-06-18 11:08:14 +00:00
|
|
|
|
2020-10-26 08:54:59 +00:00
|
|
|
void ConfirmDeleteSelectedItems(not_null<ListWidget*> widget) {
|
|
|
|
const auto items = widget->getSelectedItems();
|
|
|
|
if (items.empty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (const auto &item : items) {
|
|
|
|
if (!item.canDelete) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const auto weak = Ui::MakeWeak(widget);
|
2021-06-13 07:37:52 +00:00
|
|
|
auto box = Box<DeleteMessagesBox>(
|
2020-10-26 08:54:59 +00:00
|
|
|
&widget->controller()->session(),
|
2021-06-13 07:37:52 +00:00
|
|
|
widget->getSelectedIds());
|
2020-10-26 08:54:59 +00:00
|
|
|
box->setDeleteConfirmedCallback([=] {
|
|
|
|
if (const auto strong = weak.data()) {
|
|
|
|
strong->cancelSelection();
|
|
|
|
}
|
|
|
|
});
|
2021-06-13 07:37:52 +00:00
|
|
|
widget->controller()->show(std::move(box));
|
2020-10-26 08:54:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void ConfirmForwardSelectedItems(not_null<ListWidget*> widget) {
|
|
|
|
const auto items = widget->getSelectedItems();
|
|
|
|
if (items.empty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
for (const auto &item : items) {
|
|
|
|
if (!item.canForward) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
auto ids = widget->getSelectedIds();
|
|
|
|
const auto weak = Ui::MakeWeak(widget);
|
|
|
|
Window::ShowForwardMessagesBox(widget->controller(), std::move(ids), [=] {
|
|
|
|
if (const auto strong = weak.data()) {
|
|
|
|
strong->cancelSelection();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void ConfirmSendNowSelectedItems(not_null<ListWidget*> widget) {
|
|
|
|
const auto items = widget->getSelectedItems();
|
|
|
|
if (items.empty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const auto navigation = widget->controller();
|
|
|
|
const auto history = [&]() -> History* {
|
|
|
|
auto result = (History*)nullptr;
|
|
|
|
auto &data = navigation->session().data();
|
|
|
|
for (const auto &item : items) {
|
|
|
|
if (!item.canSendNow) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
const auto message = data.message(item.msgId);
|
|
|
|
if (message) {
|
|
|
|
result = message->history();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}();
|
|
|
|
if (!history) {
|
|
|
|
return;
|
|
|
|
}
|
2021-07-20 15:48:53 +00:00
|
|
|
const auto clearSelection = [weak = Ui::MakeWeak(widget)] {
|
|
|
|
if (const auto strong = weak.data()) {
|
|
|
|
strong->cancelSelection();
|
|
|
|
}
|
|
|
|
};
|
2020-10-26 08:54:59 +00:00
|
|
|
Window::ShowSendNowMessagesBox(
|
|
|
|
navigation,
|
|
|
|
history,
|
|
|
|
widget->getSelectedIds(),
|
2021-07-20 15:48:53 +00:00
|
|
|
clearSelection);
|
2020-10-26 08:54:59 +00:00
|
|
|
}
|
|
|
|
|
2018-01-09 17:08:31 +00:00
|
|
|
} // namespace HistoryView
|