AyuGramDesktop/Telegram/SourceFiles/history/history_admin_log_inner.cpp
John Preston 836fe119d9 Improve display of admin event log entries.
Don't show the Forward, Delete and Go to message in MediaView.
Better handle round video floating player in admin event log.
2017-06-30 09:21:45 +03:00

1306 lines
45 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop version of Telegram messaging app, see https://telegram.org
Telegram Desktop is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
In addition, as a special exception, the copyright holders give permission
to link the code of portions of this program with the OpenSSL library.
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
*/
#include "history/history_admin_log_inner.h"
#include "styles/style_history.h"
#include "history/history_media_types.h"
#include "history/history_message.h"
#include "history/history_service_layout.h"
#include "history/history_admin_log_section.h"
#include "chat_helpers/message_field.h"
#include "mainwindow.h"
#include "mainwidget.h"
#include "window/window_controller.h"
#include "auth_session.h"
#include "ui/widgets/popup_menu.h"
#include "core/file_utilities.h"
#include "lang/lang_keys.h"
namespace AdminLog {
namespace {
constexpr auto kScrollDateHideTimeout = 1000;
constexpr auto kEventsFirstPage = 20;
constexpr auto kEventsPerPage = 50;
} // namespace
template <InnerWidget::EnumItemsDirection direction, typename Method>
void InnerWidget::enumerateItems(Method method) {
constexpr auto TopToBottom = (direction == EnumItemsDirection::TopToBottom);
// No displayed messages in this history.
if (_items.empty()) {
return;
}
if (_visibleBottom <= _itemsTop || _itemsTop + _itemsHeight <= _visibleTop) {
return;
}
auto begin = std::rbegin(_items), end = std::rend(_items);
auto from = TopToBottom ? std::lower_bound(begin, end, _visibleTop, [this](auto &elem, int top) {
return this->itemTop(elem) + elem->height() <= top;
}) : std::upper_bound(begin, end, _visibleBottom, [this](int bottom, auto &elem) {
return this->itemTop(elem) + elem->height() >= bottom;
});
auto wasEnd = (from == end);
if (wasEnd) {
--from;
}
if (TopToBottom) {
t_assert(itemTop(from->get()) + from->get()->height() > _visibleTop);
} else {
t_assert(itemTop(from->get()) < _visibleBottom);
}
while (true) {
auto item = from->get();
auto itemtop = itemTop(item);
auto itembottom = itemtop + item->height();
// Binary search should've skipped all the items that are above / below the visible area.
if (TopToBottom) {
t_assert(itembottom > _visibleTop);
} else {
t_assert(itemtop < _visibleBottom);
}
if (!method(item, itemtop, itembottom)) {
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) {
if (++from == end) {
break;
}
} else {
if (from == begin) {
break;
}
--from;
}
}
}
template <typename Method>
void InnerWidget::enumerateUserpics(Method method) {
// 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;
auto userpicCallback = [this, &lowestAttachedItemTop, &method](HistoryItem *item, int itemtop, int itembottom) {
// Skip all service messages.
auto message = item->toHistoryMessage();
if (!message) return true;
if (lowestAttachedItemTop < 0 && message->isAttachedToNext()) {
lowestAttachedItemTop = itemtop + message->marginTop();
}
// 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.
if (message->displayFromPhoto() || (message->hasFromPhoto() && itembottom >= _visibleBottom)) {
if (lowestAttachedItemTop < 0) {
lowestAttachedItemTop = itemtop + message->marginTop();
}
// Attach userpic to the bottom of the visible area with the same margin as the last message.
auto userpicMinBottomSkip = st::historyPaddingBottom + st::msgMargin.bottom();
auto userpicBottom = qMin(itembottom - message->marginBottom(), _visibleBottom - userpicMinBottomSkip);
// 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.
if (!method(message, userpicBottom - st::msgPhotoSize)) {
return false;
}
}
// Forget the found top of the pack, search for the next one from scratch.
if (!message->isAttachedToNext()) {
lowestAttachedItemTop = -1;
}
return true;
};
enumerateItems<EnumItemsDirection::TopToBottom>(userpicCallback);
}
template <typename Method>
void InnerWidget::enumerateDates(Method method) {
// 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;
auto dateCallback = [this, &lowestInOneDayItemBottom, &method](HistoryItem *item, int itemtop, int itembottom) {
if (lowestInOneDayItemBottom < 0 && item->isInOneDayWithPrevious()) {
lowestInOneDayItemBottom = itembottom - item->marginBottom();
}
// 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.
if (item->displayDate() || (!item->isEmpty() && itemtop <= _visibleTop)) {
if (lowestInOneDayItemBottom < 0) {
lowestInOneDayItemBottom = itembottom - item->marginBottom();
}
// 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.
if (!method(item, itemtop, dateTop)) {
return false;
}
}
// Forget the found bottom of the pack, search for the next one from scratch.
if (!item->isInOneDayWithPrevious()) {
lowestInOneDayItemBottom = -1;
}
return true;
};
enumerateItems<EnumItemsDirection::BottomToTop>(dateCallback);
}
InnerWidget::InnerWidget(QWidget *parent, gsl::not_null<Window::Controller*> controller, gsl::not_null<ChannelData*> channel, base::lambda<void(int top)> scrollTo) : TWidget(parent)
, _controller(controller)
, _channel(channel)
, _history(App::history(channel))
, _scrollTo(std::move(scrollTo))
, _scrollDateCheck([this] { scrollDateCheck(); })
, _emptyText(st::historyAdminLogEmptyWidth - st::historyAdminLogEmptyPadding.left() - st::historyAdminLogEmptyPadding.left()) {
setMouseTracking(true);
_scrollDateHideTimer.setCallback([this] { scrollDateHideByTimer(); });
subscribe(AuthSession::Current().data().repaintLogEntry(), [this](gsl::not_null<const HistoryItem*> historyItem) {
if (_history == historyItem->history()) {
repaintItem(historyItem);
}
});
subscribe(AuthSession::Current().data().pendingHistoryResize(), [this] { handlePendingHistoryResize(); });
subscribe(AuthSession::Current().data().queryItemVisibility(), [this](const AuthSessionData::ItemVisibilityQuery &query) {
if (_history != query.item->history() || !query.item->isLogEntry() || !isVisible()) {
return;
}
auto top = itemTop(query.item);
if (top >= 0 && top + query.item->height() > _visibleTop && top < _visibleBottom) {
*query.isVisible = true;
}
});
updateEmptyText();
}
void InnerWidget::setVisibleTopBottom(int visibleTop, int visibleBottom) {
auto scrolledUp = (visibleTop < _visibleTop);
_visibleTop = visibleTop;
_visibleBottom = visibleBottom;
updateVisibleTopItem();
checkPreloadMore();
if (scrolledUp) {
_scrollDateCheck.call();
} else {
scrollDateHideByTimer();
}
_controller->floatPlayerAreaUpdated().notify(true);
}
void InnerWidget::updateVisibleTopItem() {
if (_visibleBottom == height()) {
_visibleTopItem = nullptr;
} else {
auto begin = std::rbegin(_items), end = std::rend(_items);
auto from = std::lower_bound(begin, end, _visibleTop, [this](auto &&elem, int top) {
return this->itemTop(elem) + elem->height() <= top;
});
if (from != end) {
_visibleTopItem = *from;
_visibleTopFromItem = _visibleTop - _visibleTopItem->y();
} else {
_visibleTopItem = nullptr;
_visibleTopFromItem = _visibleTop;
}
}
}
bool InnerWidget::displayScrollDate() const {
return (_visibleTop <= height() - 2 * (_visibleBottom - _visibleTop));
}
void InnerWidget::scrollDateCheck() {
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;
_scrollDateHideTimer.callOnce(kScrollDateHideTimeout);
}
}
void InnerWidget::scrollDateHideByTimer() {
_scrollDateHideTimer.cancel();
scrollDateHide();
}
void InnerWidget::scrollDateHide() {
if (_scrollDateShown) {
toggleScrollDateShown();
}
}
void InnerWidget::toggleScrollDateShown() {
_scrollDateShown = !_scrollDateShown;
auto from = _scrollDateShown ? 0. : 1.;
auto to = _scrollDateShown ? 1. : 0.;
_scrollDateOpacity.start([this] { repaintScrollDateCallback(); }, from, to, st::historyDateFadeDuration);
}
void InnerWidget::repaintScrollDateCallback() {
auto updateTop = _visibleTop;
auto updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();
update(0, updateTop, width(), updateHeight);
}
void InnerWidget::checkPreloadMore() {
if (_visibleTop + PreloadHeightsCount * (_visibleBottom - _visibleTop) > height()) {
preloadMore(Direction::Down);
}
if (_visibleTop < PreloadHeightsCount * (_visibleBottom - _visibleTop)) {
preloadMore(Direction::Up);
}
}
void InnerWidget::applyFilter(MTPDchannelAdminLogEventsFilter::Flags flags, const std::vector<gsl::not_null<UserData*>> &admins) {
_filterFlags = flags;
_filterAdmins = admins;
updateEmptyText();
}
void InnerWidget::updateEmptyText() {
auto options = _defaultOptions;
options.flags |= TextParseMono; // For italic :/
auto hasFilter = (_filterFlags != 0) || !_filterAdmins.empty();
auto text = TextWithEntities { lang(hasFilter ? lng_admin_log_no_results_title : lng_admin_log_no_events_title) };
text.entities.append(EntityInText(EntityInTextBold, 0, text.text.size()));
text.text.append(qstr("\n\n") + lang(hasFilter ? lng_admin_log_no_results_text : lng_admin_log_no_events_text));
_emptyText.setMarkedText(st::defaultTextStyle, text, options);
}
QString InnerWidget::tooltipText() const {
if (_mouseCursorState == HistoryInDateCursorState && _mouseAction == MouseAction::None) {
if (auto item = App::hoveredItem()) {
auto dateText = item->date.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat));
return dateText;
}
} else if (_mouseCursorState == HistoryInForwardedCursorState && _mouseAction == MouseAction::None) {
if (auto item = App::hoveredItem()) {
if (auto forwarded = item->Get<HistoryMessageForwarded>()) {
return forwarded->_text.originalText(AllTextSelection, ExpandLinksNone);
}
}
} else if (auto lnk = ClickHandler::getActive()) {
return lnk->tooltip();
}
return QString();
}
QPoint InnerWidget::tooltipPos() const {
return _mousePosition;
}
void InnerWidget::saveState(gsl::not_null<SectionMemento*> memento) {
memento->setItems(std::move(_items), std::move(_itemsByIds), _upLoaded, _downLoaded);
memento->setIdManager(std::move(_idManager));
_upLoaded = _downLoaded = true; // Don't load or handle anything anymore.
}
void InnerWidget::restoreState(gsl::not_null<SectionMemento*> memento) {
_items = memento->takeItems();
_itemsByIds = memento->takeItemsByIds();
_idManager = memento->takeIdManager();
_upLoaded = memento->upLoaded();
_downLoaded = memento->downLoaded();
updateMinMaxIds();
updateSize();
}
void InnerWidget::preloadMore(Direction direction) {
auto &requestId = (direction == Direction::Up) ? _preloadUpRequestId : _preloadDownRequestId;
auto &loadedFlag = (direction == Direction::Up) ? _upLoaded : _downLoaded;
if (requestId != 0 || loadedFlag) {
return;
}
auto flags = MTPchannels_GetAdminLog::Flags(0);
auto filter = MTP_channelAdminLogEventsFilter(MTP_flags(_filterFlags));
if (_filterFlags != 0) {
flags |= MTPchannels_GetAdminLog::Flag::f_events_filter;
}
auto admins = QVector<MTPInputUser>(0);
if (!_filterAdmins.empty()) {
admins.reserve(_filterAdmins.size());
for (auto &admin : _filterAdmins) {
admins.push_back(admin->inputUser);
}
flags |= MTPchannels_GetAdminLog::Flag::f_admins;
}
auto query = QString();
auto maxId = (direction == Direction::Up) ? _minId : 0;
auto minId = (direction == Direction::Up) ? 0 : _maxId;
auto perPage = _items.empty() ? kEventsFirstPage : kEventsPerPage;
requestId = request(MTPchannels_GetAdminLog(MTP_flags(flags), _channel->inputChannel, MTP_string(query), filter, MTP_vector<MTPInputUser>(admins), MTP_long(maxId), MTP_long(minId), MTP_int(perPage))).done([this, &requestId, &loadedFlag, direction](const MTPchannels_AdminLogResults &result) {
Expects(result.type() == mtpc_channels_adminLogResults);
requestId = 0;
auto &results = result.c_channels_adminLogResults();
App::feedUsers(results.vusers);
App::feedChats(results.vchats);
if (loadedFlag) {
return;
}
auto &events = results.vevents.v;
if (!events.empty()) {
auto oldItemsCount = _items.size();
_items.reserve(oldItemsCount + events.size() * 2);
for_const (auto &event, events) {
t_assert(event.type() == mtpc_channelAdminLogEvent);
auto &data = event.c_channelAdminLogEvent();
if (_itemsByIds.find(data.vid.v) != _itemsByIds.cend()) {
continue;
}
auto count = 0;
GenerateItems(_history, _idManager, data, [this, id = data.vid.v, &count](HistoryItemOwned item) {
_itemsByIds.emplace(id, item.get());
_items.push_back(std::move(item));
++count;
});
if (count > 1) {
// Reverse the inner order of the added messages, because we load events
// from bottom to top but inside one event they go from top to bottom.
auto full = _items.size();
auto from = full - count;
for (auto i = 0, toReverse = count / 2; i != toReverse; ++i) {
std::swap(_items[from + i], _items[full - i - 1]);
}
}
}
auto newItemsCount = _items.size();
if (newItemsCount != oldItemsCount) {
for (auto i = oldItemsCount; i != newItemsCount + 1; ++i) {
if (i > 0) {
auto item = _items[i - 1].get();
if (i == newItemsCount) {
item->setLogEntryDisplayDate(true);
} else {
auto previous = _items[i].get();
item->setLogEntryDisplayDate(item->date.date() != previous->date.date());
auto attachToPrevious = item->computeIsAttachToPrevious(previous);
item->setLogEntryAttachToPrevious(attachToPrevious);
previous->setLogEntryAttachToNext(attachToPrevious);
}
}
}
updateMinMaxIds();
itemsAdded(direction);
}
} else {
loadedFlag = true;
}
update();
}).fail([this, &requestId, &loadedFlag](const RPCError &error) {
requestId = 0;
loadedFlag = true;
update();
}).send();
}
void InnerWidget::updateMinMaxIds() {
if (_itemsByIds.empty()) {
_maxId = _minId = 0;
} else {
_maxId = (--_itemsByIds.end())->first;
_minId = _itemsByIds.begin()->first;
if (_minId == 1) {
_upLoaded = true;
}
}
}
void InnerWidget::itemsAdded(Direction direction) {
updateSize();
}
void InnerWidget::updateSize() {
TWidget::resizeToWidth(width());
restoreScrollPosition();
updateVisibleTopItem();
checkPreloadMore();
}
int InnerWidget::resizeGetHeight(int newWidth) {
update();
auto newHeight = 0;
for (auto &item : base::reversed(_items)) {
item->setY(newHeight);
newHeight += item->resizeGetHeight(newWidth);
}
_itemsHeight = newHeight;
_itemsTop = (_minHeight > _itemsHeight + st::historyPaddingBottom) ? (_minHeight - _itemsHeight - st::historyPaddingBottom) : 0;
return _itemsTop + _itemsHeight + st::historyPaddingBottom;
}
void InnerWidget::restoreScrollPosition() {
auto newVisibleTop = _visibleTopItem ? (itemTop(_visibleTopItem) + _visibleTopFromItem) : ScrollMax;
_scrollTo(newVisibleTop);
}
void InnerWidget::paintEvent(QPaintEvent *e) {
if (Ui::skipPaintEvent(this, e)) {
return;
}
Painter p(this);
auto ms = getms();
auto clip = e->rect();
if (_items.empty() && _upLoaded && _downLoaded) {
paintEmpty(p);
} else {
auto begin = std::rbegin(_items), end = std::rend(_items);
auto from = std::lower_bound(begin, end, clip.top(), [this](auto &elem, int top) {
return this->itemTop(elem) + elem->height() <= top;
});
auto to = std::lower_bound(begin, end, clip.top() + clip.height(), [this](auto &elem, int bottom) {
return this->itemTop(elem) < bottom;
});
if (from != end) {
auto top = itemTop(from->get());
p.translate(0, top);
for (auto i = from; i != to; ++i) {
auto selection = (*i == _selectedItem) ? _selectedText : TextSelection();
(*i)->draw(p, clip.translated(0, -top), selection, ms);
auto height = (*i)->height();
top += height;
p.translate(0, height);
}
p.translate(0, -top);
enumerateUserpics([&p, &clip](gsl::not_null<HistoryMessage*> message, int userpicTop) {
// stop the enumeration if the userpic is below the painted rect
if (userpicTop >= clip.top() + clip.height()) {
return false;
}
// paint the userpic if it intersects the painted rect
if (userpicTop + st::msgPhotoSize > clip.top()) {
message->from()->paintUserpicLeft(p, st::historyPhotoLeft, userpicTop, message->width(), st::msgPhotoSize);
}
return true;
});
auto dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
auto scrollDateOpacity = _scrollDateOpacity.current(ms, _scrollDateShown ? 1. : 0.);
enumerateDates([&p, &clip, scrollDateOpacity, dateHeight/*, lastDate, showFloatingBefore*/](gsl::not_null<HistoryItem*> item, int itemtop, int dateTop) {
// stop the enumeration if the date is above the painted rect
if (dateTop + dateHeight <= clip.top()) {
return false;
}
bool displayDate = item->displayDate();
bool dateInPlace = displayDate;
if (dateInPlace) {
int correctDateTop = itemtop + st::msgServiceMargin.top();
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());
int width = item->width();
if (auto date = item->Get<HistoryMessageDate>()) {
date->paint(p, dateY, width);
} else {
HistoryLayout::ServiceMessagePainter::paintDate(p, item->date, dateY, width);
}
}
}
return true;
});
}
}
}
void InnerWidget::paintEmpty(Painter &p) {
style::font font(st::msgServiceFont);
auto rectWidth = st::historyAdminLogEmptyWidth;
auto innerWidth = rectWidth - st::historyAdminLogEmptyPadding.left() - st::historyAdminLogEmptyPadding.right();
auto rectHeight = st::historyAdminLogEmptyPadding.top() + _emptyText.countHeight(innerWidth) + st::historyAdminLogEmptyPadding.bottom();
auto rect = QRect((width() - rectWidth) / 2, (height() - rectHeight) / 3, rectWidth, rectHeight);
HistoryLayout::ServiceMessagePainter::paintBubble(p, rect.x(), rect.y(), rect.width(), rect.height());
p.setPen(st::msgServiceFg);
_emptyText.draw(p, rect.x() + st::historyAdminLogEmptyPadding.left(), rect.y() + st::historyAdminLogEmptyPadding.top(), innerWidth, style::al_top);
}
TextWithEntities InnerWidget::getSelectedText() const {
return _selectedItem ? _selectedItem->selectedText(_selectedText) : TextWithEntities();
}
void InnerWidget::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape && _cancelledCallback) {
_cancelledCallback();
} else if (e == QKeySequence::Copy && _selectedItem != nullptr) {
copySelectedText();
#ifdef Q_OS_MAC
} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
setToClipboard(getSelectedText(), QClipboard::FindBuffer);
#endif // Q_OS_MAC
} else {
e->ignore();
}
}
void InnerWidget::mouseDoubleClickEvent(QMouseEvent *e) {
mouseActionStart(e->globalPos(), e->button());
if (((_mouseAction == MouseAction::Selecting && _selectedItem != nullptr) || (_mouseAction == MouseAction::None)) && _mouseSelectType == TextSelectType::Letters && _mouseActionItem) {
HistoryStateRequest request;
request.flags |= Text::StateRequest::Flag::LookupSymbol;
auto dragState = _mouseActionItem->getState(_dragStartPosition, request);
if (dragState.cursor == HistoryInTextCursorState) {
_mouseTextSymbol = dragState.symbol;
_mouseSelectType = TextSelectType::Words;
if (_mouseAction == MouseAction::None) {
_mouseAction = MouseAction::Selecting;
auto selection = TextSelection { dragState.symbol, dragState.symbol };
repaintItem(std::exchange(_selectedItem, _mouseActionItem));
_selectedText = selection;
}
mouseMoveEvent(e);
_trippleClickPoint = e->globalPos();
_trippleClickTimer.callOnce(QApplication::doubleClickInterval());
}
}
}
void InnerWidget::contextMenuEvent(QContextMenuEvent *e) {
showContextMenu(e);
}
void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
if (_menu) {
_menu->deleteLater();
_menu = 0;
}
if (e->reason() == QContextMenuEvent::Mouse) {
mouseActionUpdate(e->globalPos());
}
// -1 - has selection, but no over, 0 - no selection, 1 - over text
auto isUponSelected = 0;
auto hasSelected = 0;
if (_selectedItem) {
isUponSelected = -1;
auto selFrom = _selectedText.from;
auto selTo = _selectedText.to;
hasSelected = (selTo > selFrom) ? 1 : 0;
if (App::mousedItem() && App::mousedItem() == App::hoveredItem()) {
auto mousePos = mapPointToItem(mapFromGlobal(_mousePosition), App::mousedItem());
HistoryStateRequest request;
request.flags |= Text::StateRequest::Flag::LookupSymbol;
auto dragState = App::mousedItem()->getState(mousePos, request);
if (dragState.cursor == HistoryInTextCursorState && dragState.symbol >= selFrom && dragState.symbol < selTo) {
isUponSelected = 1;
}
}
}
if (showFromTouch && hasSelected && isUponSelected < hasSelected) {
isUponSelected = hasSelected;
}
_menu = new Ui::PopupMenu(nullptr);
_contextMenuLink = ClickHandler::getActive();
auto item = App::hoveredItem() ? App::hoveredItem() : App::hoveredLinkItem();
auto lnkPhoto = dynamic_cast<PhotoClickHandler*>(_contextMenuLink.data());
auto lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLink.data());
bool lnkIsVideo = lnkDocument ? lnkDocument->document()->isVideo() : false;
bool lnkIsAudio = lnkDocument ? (lnkDocument->document()->voice() != nullptr) : false;
bool lnkIsSong = lnkDocument ? (lnkDocument->document()->song() != nullptr) : false;
if (lnkPhoto || lnkDocument) {
if (isUponSelected > 0) {
_menu->addAction(lang(lng_context_copy_selected), [this] { copySelectedText(); })->setEnabled(true);
}
if (lnkPhoto) {
_menu->addAction(lang(lng_context_save_image), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, photo = lnkPhoto->photo()] {
savePhotoToFile(photo);
}))->setEnabled(true);
_menu->addAction(lang(lng_context_copy_image), [this, photo = lnkPhoto->photo()] {
copyContextImage(photo);
})->setEnabled(true);
} else {
auto document = lnkDocument->document();
if (document->loading()) {
_menu->addAction(lang(lng_context_cancel_download), [this] { cancelContextDownload(); })->setEnabled(true);
} else {
if (document->loaded() && document->isGifv()) {
if (!cAutoPlayGif()) {
_menu->addAction(lang(lng_context_open_gif), [this] { openContextGif(); })->setEnabled(true);
}
}
if (!document->filepath(DocumentData::FilePathResolveChecked).isEmpty()) {
_menu->addAction(lang((cPlatform() == dbipMac || cPlatform() == dbipMacOld) ? lng_context_show_in_finder : lng_context_show_in_folder), [this] { showContextInFolder(); })->setEnabled(true);
}
_menu->addAction(lang(lnkIsVideo ? lng_context_save_video : (lnkIsAudio ? lng_context_save_audio : (lnkIsSong ? lng_context_save_audio_file : lng_context_save_file))), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
saveDocumentToFile(document);
}))->setEnabled(true);
}
}
if (App::hoveredLinkItem()) {
App::contextItem(App::hoveredLinkItem());
}
} else { // maybe cursor on some text history item?
bool canDelete = item && item->canDelete() && (item->id > 0 || !item->serviceMsg());
bool canForward = item && item->canForward();
auto msg = dynamic_cast<HistoryMessage*>(item);
if (isUponSelected > 0) {
_menu->addAction(lang(lng_context_copy_selected), [this] { copySelectedText(); })->setEnabled(true);
} else {
if (item && !isUponSelected) {
auto mediaHasTextForCopy = false;
if (auto media = (msg ? msg->getMedia() : nullptr)) {
mediaHasTextForCopy = media->hasTextForCopy();
if (media->type() == MediaTypeWebPage && static_cast<HistoryWebPage*>(media)->attach()) {
media = static_cast<HistoryWebPage*>(media)->attach();
}
if (media->type() == MediaTypeSticker) {
if (auto document = media->getDocument()) {
if (document->sticker() && document->sticker()->set.type() != mtpc_inputStickerSetEmpty) {
_menu->addAction(lang(document->sticker()->setInstalled() ? lng_context_pack_info : lng_context_pack_add), [this] { showStickerPackInfo(); });
}
_menu->addAction(lang(lng_context_save_image), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
saveDocumentToFile(document);
}))->setEnabled(true);
}
} else if (media->type() == MediaTypeGif && !_contextMenuLink) {
if (auto document = media->getDocument()) {
if (document->loading()) {
_menu->addAction(lang(lng_context_cancel_download), [this] { cancelContextDownload(); })->setEnabled(true);
} else {
if (document->isGifv()) {
if (!cAutoPlayGif()) {
_menu->addAction(lang(lng_context_open_gif), [this] { openContextGif(); })->setEnabled(true);
}
}
if (!document->filepath(DocumentData::FilePathResolveChecked).isEmpty()) {
_menu->addAction(lang((cPlatform() == dbipMac || cPlatform() == dbipMacOld) ? lng_context_show_in_finder : lng_context_show_in_folder), [this] { showContextInFolder(); })->setEnabled(true);
}
_menu->addAction(lang(lng_context_save_file), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
saveDocumentToFile(document);
}))->setEnabled(true);
}
}
}
}
if (msg && !_contextMenuLink && (!msg->emptyText() || mediaHasTextForCopy)) {
_menu->addAction(lang(lng_context_copy_text), [this] { copyContextText(); })->setEnabled(true);
}
}
}
auto linkCopyToClipboardText = _contextMenuLink ? _contextMenuLink->copyToClipboardContextItemText() : QString();
if (!linkCopyToClipboardText.isEmpty()) {
_menu->addAction(linkCopyToClipboardText, [this] { copyContextUrl(); })->setEnabled(true);
}
App::contextItem(item);
}
if (_menu->actions().isEmpty()) {
delete base::take(_menu);
} else {
connect(_menu, &QObject::destroyed, this, [this](QObject *object) {
if (_menu == object) {
_menu = nullptr;
}
});
_menu->popup(e->globalPos());
e->accept();
}
}
void InnerWidget::savePhotoToFile(PhotoData *photo) {
if (!photo || !photo->date || !photo->loaded()) return;
auto filter = qsl("JPEG Image (*.jpg);;") + FileDialog::AllFilesFilter();
FileDialog::GetWritePath(lang(lng_save_photo), filter, filedialogDefaultName(qsl("photo"), qsl(".jpg")), base::lambda_guarded(this, [this, photo](const QString &result) {
if (!result.isEmpty()) {
photo->full->pix().toImage().save(result, "JPG");
}
}));
}
void InnerWidget::saveDocumentToFile(DocumentData *document) {
DocumentSaveClickHandler::doSave(document, true);
}
void InnerWidget::copyContextImage(PhotoData *photo) {
if (!photo || !photo->date || !photo->loaded()) return;
QApplication::clipboard()->setPixmap(photo->full->pix());
}
void InnerWidget::copySelectedText() {
setToClipboard(getSelectedText());
}
void InnerWidget::copyContextUrl() {
if (_contextMenuLink) {
_contextMenuLink->copyToClipboard();
}
}
void InnerWidget::showStickerPackInfo() {
if (!App::contextItem()) return;
if (auto media = App::contextItem()->getMedia()) {
if (auto doc = media->getDocument()) {
if (auto sticker = doc->sticker()) {
if (sticker->set.type() != mtpc_inputStickerSetEmpty) {
App::main()->stickersBox(sticker->set);
}
}
}
}
}
void InnerWidget::cancelContextDownload() {
if (auto lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLink.data())) {
lnkDocument->document()->cancel();
} else if (auto item = App::contextItem()) {
if (auto media = item->getMedia()) {
if (auto doc = media->getDocument()) {
doc->cancel();
}
}
}
}
void InnerWidget::showContextInFolder() {
QString filepath;
if (auto lnkDocument = dynamic_cast<DocumentClickHandler*>(_contextMenuLink.data())) {
filepath = lnkDocument->document()->filepath(DocumentData::FilePathResolveChecked);
} else if (auto item = App::contextItem()) {
if (auto media = item->getMedia()) {
if (auto doc = media->getDocument()) {
filepath = doc->filepath(DocumentData::FilePathResolveChecked);
}
}
}
if (!filepath.isEmpty()) {
File::ShowInFolder(filepath);
}
}
void InnerWidget::openContextGif() {
if (auto item = App::contextItem()) {
if (auto media = item->getMedia()) {
if (auto document = media->getDocument()) {
_controller->window()->showDocument(document, item);
}
}
}
}
void InnerWidget::copyContextText() {
auto item = App::contextItem();
if (!item || (item->getMedia() && item->getMedia()->type() == MediaTypeSticker)) {
return;
}
setToClipboard(item->selectedText(FullSelection));
}
void InnerWidget::setToClipboard(const TextWithEntities &forClipboard, QClipboard::Mode mode) {
if (auto data = MimeDataFromTextWithEntities(forClipboard)) {
QApplication::clipboard()->setMimeData(data.release(), mode);
}
}
void InnerWidget::mousePressEvent(QMouseEvent *e) {
if (_menu) {
e->accept();
return; // ignore mouse press, that was hiding context menu
}
mouseActionStart(e->globalPos(), e->button());
}
void InnerWidget::mouseMoveEvent(QMouseEvent *e) {
auto buttonsPressed = (e->buttons() & (Qt::LeftButton | Qt::MiddleButton));
if (!buttonsPressed && _mouseAction != MouseAction::None) {
mouseReleaseEvent(e);
}
mouseActionUpdate(e->globalPos());
}
void InnerWidget::mouseReleaseEvent(QMouseEvent *e) {
mouseActionFinish(e->globalPos(), e->button());
if (!rect().contains(e->pos())) {
leaveEvent(e);
}
}
void InnerWidget::enterEventHook(QEvent *e) {
mouseActionUpdate(QCursor::pos());
return TWidget::enterEventHook(e);
}
void InnerWidget::leaveEventHook(QEvent *e) {
if (auto item = App::hoveredItem()) {
repaintItem(item);
App::hoveredItem(nullptr);
}
ClickHandler::clearActive();
Ui::Tooltip::Hide();
if (!ClickHandler::getPressed() && _cursor != style::cur_default) {
_cursor = style::cur_default;
setCursor(_cursor);
}
return TWidget::leaveEventHook(e);
}
void InnerWidget::mouseActionStart(const QPoint &screenPos, Qt::MouseButton button) {
mouseActionUpdate(screenPos);
if (button != Qt::LeftButton) return;
ClickHandler::pressed();
if (App::pressedItem() != App::hoveredItem()) {
repaintItem(App::pressedItem());
App::pressedItem(App::hoveredItem());
repaintItem(App::pressedItem());
}
_mouseAction = MouseAction::None;
_mouseActionItem = App::mousedItem();
_dragStartPosition = mapPointToItem(mapFromGlobal(screenPos), _mouseActionItem);
_pressWasInactive = _controller->window()->wasInactivePress();
if (_pressWasInactive) _controller->window()->setInactivePress(false);
if (ClickHandler::getPressed()) {
_mouseAction = MouseAction::PrepareDrag;
}
if (_mouseAction == MouseAction::None && _mouseActionItem) {
HistoryTextState dragState;
if (_trippleClickTimer.isActive() && (screenPos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) {
HistoryStateRequest request;
request.flags = Text::StateRequest::Flag::LookupSymbol;
dragState = _mouseActionItem->getState(_dragStartPosition, request);
if (dragState.cursor == HistoryInTextCursorState) {
auto selection = TextSelection { dragState.symbol, dragState.symbol };
repaintItem(std::exchange(_selectedItem, _mouseActionItem));
_selectedText = selection;
_mouseTextSymbol = dragState.symbol;
_mouseAction = MouseAction::Selecting;
_mouseSelectType = TextSelectType::Paragraphs;
mouseActionUpdate(_mousePosition);
_trippleClickTimer.callOnce(QApplication::doubleClickInterval());
}
} else if (App::pressedItem()) {
HistoryStateRequest request;
request.flags = Text::StateRequest::Flag::LookupSymbol;
dragState = _mouseActionItem->getState(_dragStartPosition, request);
}
if (_mouseSelectType != TextSelectType::Paragraphs) {
if (App::pressedItem()) {
_mouseTextSymbol = dragState.symbol;
auto uponSelected = (dragState.cursor == HistoryInTextCursorState);
if (uponSelected) {
if (!_selectedItem || _selectedItem != _mouseActionItem) {
uponSelected = false;
} else if (_mouseTextSymbol < _selectedText.from || _mouseTextSymbol >= _selectedText.to) {
uponSelected = false;
}
}
if (uponSelected) {
_mouseAction = MouseAction::PrepareDrag; // start text drag
} else if (!_pressWasInactive) {
if (dragState.afterSymbol) ++_mouseTextSymbol;
auto selection = TextSelection { _mouseTextSymbol, _mouseTextSymbol };
repaintItem(std::exchange(_selectedItem, _mouseActionItem));
_selectedText = selection;
_mouseAction = MouseAction::Selecting;
repaintItem(_mouseActionItem);
}
}
}
}
if (!_mouseActionItem) {
_mouseAction = MouseAction::None;
} else if (_mouseAction == MouseAction::None) {
_mouseActionItem = nullptr;
}
}
void InnerWidget::mouseActionUpdate(const QPoint &screenPos) {
_mousePosition = screenPos;
updateSelected();
}
void InnerWidget::mouseActionCancel() {
_mouseActionItem = nullptr;
_mouseAction = MouseAction::None;
_dragStartPosition = QPoint(0, 0);
_wasSelectedText = false;
//_widget->noSelectingScroll(); // TODO
}
void InnerWidget::mouseActionFinish(const QPoint &screenPos, Qt::MouseButton button) {
mouseActionUpdate(screenPos);
ClickHandlerPtr activated = ClickHandler::unpressed();
if (_mouseAction == MouseAction::Dragging) {
activated.clear();
}
if (App::pressedItem()) {
repaintItem(App::pressedItem());
App::pressedItem(nullptr);
}
_wasSelectedText = false;
if (activated) {
mouseActionCancel();
App::activateClickHandler(activated, button);
return;
}
if (_mouseAction == MouseAction::PrepareDrag && !_pressWasInactive && button != Qt::RightButton) {
repaintItem(base::take(_selectedItem));
} else if (_mouseAction == MouseAction::Selecting) {
if (_selectedItem && !_pressWasInactive) {
if (_selectedText.from == _selectedText.to) {
_selectedItem = nullptr;
App::wnd()->setInnerFocus();
}
}
}
_mouseAction = MouseAction::None;
_mouseActionItem = nullptr;
_mouseSelectType = TextSelectType::Letters;
//_widget->noSelectingScroll(); // TODO
#if defined Q_OS_LINUX32 || defined Q_OS_LINUX64
if (_selectedItem && _selectedText.from != _selectedText.to) {
setToClipboard(_selectedItem->selectedText(_selectedText), QClipboard::Selection);
}
#endif // Q_OS_LINUX32 || Q_OS_LINUX64
}
void InnerWidget::updateSelected() {
auto mousePosition = mapFromGlobal(_mousePosition);
auto point = QPoint(snap(mousePosition.x(), 0, width()), snap(mousePosition.y(), _visibleTop, _visibleBottom));
auto itemPoint = QPoint();
auto begin = std::rbegin(_items), end = std::rend(_items);
auto from = (point.y() >= _itemsTop && point.y() < _itemsTop + _itemsHeight) ? std::lower_bound(begin, end, point.y(), [this](auto &elem, int top) {
return this->itemTop(elem) + elem->height() <= top;
}) : end;
auto item = (from != end) ? from->get() : nullptr;
if (item) {
App::mousedItem(item);
itemPoint = mapPointToItem(point, item);
if (item->hasPoint(itemPoint)) {
if (App::hoveredItem() != item) {
repaintItem(App::hoveredItem());
App::hoveredItem(item);
repaintItem(App::hoveredItem());
}
} else if (App::hoveredItem()) {
repaintItem(App::hoveredItem());
App::hoveredItem(nullptr);
}
}
HistoryTextState dragState;
ClickHandlerHost *lnkhost = nullptr;
auto selectingText = (item == _mouseActionItem && item == App::hoveredItem() && _selectedItem);
if (item) {
if (item != _mouseActionItem || (itemPoint - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
if (_mouseAction == MouseAction::PrepareDrag) {
_mouseAction = MouseAction::Dragging;
InvokeQueued(this, [this] { performDrag(); });
}
}
HistoryStateRequest request;
if (_mouseAction == MouseAction::Selecting) {
request.flags |= Text::StateRequest::Flag::LookupSymbol;
} else {
selectingText = false;
}
dragState = item->getState(itemPoint, request);
lnkhost = item;
if (!dragState.link && itemPoint.x() >= st::historyPhotoLeft && itemPoint.x() < st::historyPhotoLeft + st::msgPhotoSize) {
if (auto message = item->toHistoryMessage()) {
if (message->hasFromPhoto()) {
enumerateUserpics([&dragState, &lnkhost, &point](gsl::not_null<HistoryMessage*> message, int userpicTop) -> bool {
// 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) {
dragState.link = message->from()->openLink();
lnkhost = message;
return false;
}
return true;
});
}
}
}
}
auto lnkChanged = ClickHandler::setActive(dragState.link, lnkhost);
if (lnkChanged || dragState.cursor != _mouseCursorState) {
Ui::Tooltip::Hide();
}
if (dragState.link || dragState.cursor == HistoryInDateCursorState || dragState.cursor == HistoryInForwardedCursorState) {
Ui::Tooltip::Show(1000, this);
}
auto cursor = style::cur_default;
if (_mouseAction == MouseAction::None) {
_mouseCursorState = dragState.cursor;
if (dragState.link) {
cursor = style::cur_pointer;
} else if (_mouseCursorState == HistoryInTextCursorState) {
cursor = style::cur_text;
} else if (_mouseCursorState == HistoryInDateCursorState) {
// cursor = style::cur_cross;
}
} else if (item) {
if (_mouseAction == MouseAction::Selecting) {
if (selectingText) {
auto second = dragState.symbol;
if (dragState.afterSymbol && _mouseSelectType == TextSelectType::Letters) {
++second;
}
auto selection = TextSelection { qMin(second, _mouseTextSymbol), qMax(second, _mouseTextSymbol) };
if (_mouseSelectType != TextSelectType::Letters) {
selection = _mouseActionItem->adjustSelection(selection, _mouseSelectType);
}
if (_selectedText != selection) {
_selectedText = selection;
repaintItem(_mouseActionItem);
}
if (!_wasSelectedText && (selection.from != selection.to)) {
_wasSelectedText = true;
setFocus();
}
}
} else if (_mouseAction == MouseAction::Dragging) {
}
if (ClickHandler::getPressed()) {
cursor = style::cur_pointer;
} else if (_mouseAction == MouseAction::Selecting && _selectedItem) {
cursor = style::cur_text;
}
}
// Voice message seek support.
if (auto pressedItem = App::pressedLinkItem()) {
if (!pressedItem->detached()) {
if (pressedItem->history() == _history) {
auto adjustedPoint = mapPointToItem(point, pressedItem);
pressedItem->updatePressed(adjustedPoint);
}
}
}
//if (_mouseAction == MouseAction::Selecting) {
// _widget->checkSelectingScroll(mousePos);
//} else {
// _widget->noSelectingScroll();
//} // TODO
if (_mouseAction == MouseAction::None && (lnkChanged || cursor != _cursor)) {
setCursor(_cursor = cursor);
}
}
void InnerWidget::performDrag() {
if (_mouseAction != MouseAction::Dragging) return;
auto uponSelected = false;
//if (_mouseActionItem) {
// if (!_selected.isEmpty() && _selected.cbegin().value() == FullSelection) {
// uponSelected = _selected.contains(_mouseActionItem);
// } else {
// HistoryStateRequest request;
// request.flags |= Text::StateRequest::Flag::LookupSymbol;
// auto dragState = _mouseActionItem->getState(_dragStartPosition.x(), _dragStartPosition.y(), request);
// uponSelected = (dragState.cursor == HistoryInTextCursorState);
// if (uponSelected) {
// if (_selected.isEmpty() ||
// _selected.cbegin().value() == FullSelection ||
// _selected.cbegin().key() != _mouseActionItem
// ) {
// uponSelected = false;
// } else {
// uint16 selFrom = _selected.cbegin().value().from, selTo = _selected.cbegin().value().to;
// if (dragState.symbol < selFrom || dragState.symbol >= selTo) {
// uponSelected = false;
// }
// }
// }
// }
//}
//auto pressedHandler = ClickHandler::getPressed();
//if (dynamic_cast<VoiceSeekClickHandler*>(pressedHandler.data())) {
// return;
//}
//TextWithEntities sel;
//QList<QUrl> urls;
//if (uponSelected) {
// sel = getSelectedText();
//} else if (pressedHandler) {
// sel = { pressedHandler->dragText(), EntitiesInText() };
// //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
// //}
//}
//if (auto mimeData = mimeDataFromTextWithEntities(sel)) {
// updateDragSelection(0, 0, false);
// _widget->noSelectingScroll();
// if (!urls.isEmpty()) mimeData->setUrls(urls);
// if (uponSelected && !Adaptive::OneColumn()) {
// auto selectedState = getSelectionState();
// if (selectedState.count > 0 && selectedState.count == selectedState.canForwardCount) {
// mimeData->setData(qsl("application/x-td-forward-selected"), "1");
// }
// }
// _controller->window()->launchDrag(std::move(mimeData));
// return;
//} else {
// auto forwardMimeType = QString();
// auto pressedMedia = static_cast<HistoryMedia*>(nullptr);
// if (auto pressedItem = App::pressedItem()) {
// pressedMedia = pressedItem->getMedia();
// if (_mouseCursorState == HistoryInDateCursorState || (pressedMedia && pressedMedia->dragItem())) {
// forwardMimeType = qsl("application/x-td-forward-pressed");
// }
// }
// if (auto pressedLnkItem = App::pressedLinkItem()) {
// if ((pressedMedia = pressedLnkItem->getMedia())) {
// if (forwardMimeType.isEmpty() && pressedMedia->dragItemByHandler(pressedHandler)) {
// forwardMimeType = qsl("application/x-td-forward-pressed-link");
// }
// }
// }
// if (!forwardMimeType.isEmpty()) {
// auto mimeData = std::make_unique<QMimeData>();
// mimeData->setData(forwardMimeType, "1");
// if (auto document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) {
// auto filepath = document->filepath(DocumentData::FilePathResolveChecked);
// if (!filepath.isEmpty()) {
// QList<QUrl> urls;
// urls.push_back(QUrl::fromLocalFile(filepath));
// mimeData->setUrls(urls);
// }
// }
// // This call enters event loop and can destroy any QObject.
// _controller->window()->launchDrag(std::move(mimeData));
// return;
// }
//} // TODO
}
int InnerWidget::itemTop(gsl::not_null<const HistoryItem*> item) const {
return _itemsTop + item->y();
}
void InnerWidget::repaintItem(const HistoryItem *item) {
if (!item) {
return;
}
update(0, itemTop(item), width(), item->height());
}
QPoint InnerWidget::mapPointToItem(QPoint point, const HistoryItem *item) const {
if (!item) {
return QPoint();
}
return point - QPoint(0, itemTop(item));
}
void InnerWidget::handlePendingHistoryResize() {
if (_history->hasPendingResizedItems()) {
_history->resizeGetHeight(width());
updateSize();
}
}
InnerWidget::~InnerWidget() = default;
} // namespace AdminLog