Floating dates that appear animated when scrolling messages added.

This commit is contained in:
John Preston 2016-06-10 13:21:09 +03:00
parent 6da62f902b
commit 16429b3008
7 changed files with 303 additions and 85 deletions

View File

@ -2594,20 +2594,7 @@ int HistoryMessageDate::height() const {
}
void HistoryMessageDate::paint(Painter &p, int y, int w) const {
int left = st::msgServiceMargin.left();
int maxwidth = w;
if (Adaptive::Wide()) {
maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
}
w = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left();
left += (w - _width - st::msgServicePadding.left() - st::msgServicePadding.right()) / 2;
int height = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();
App::roundRect(p, left, y + st::msgServiceMargin.top(), _width + st::msgServicePadding.left() + st::msgServicePadding.left(), height, App::msgServiceBg(), ServiceCorners);
p.setFont(st::msgServiceFont);
p.setPen(st::msgServiceColor);
p.drawText(left + st::msgServicePadding.left(), y + st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->ascent, _text);
HistoryLayout::ServiceMessagePainter::paintDate(p, _text, _width, y, w);
}
void HistoryMediaPtr::reset(HistoryMedia *p) {
@ -2717,17 +2704,7 @@ void HistoryItem::detachFast() {
}
void HistoryItem::previousItemChanged() {
if (displayDate()) {
if (!Has<HistoryMessageDate>()) {
AddComponents(HistoryMessageDate::Bit());
Get<HistoryMessageDate>()->init(date);
setPendingInitDimensions();
}
} else if (Has<HistoryMessageDate>()) {
RemoveComponents(HistoryMessageDate::Bit());
setPendingInitDimensions();
}
recountDisplayDate();
recountAttachToPrevious();
}
@ -2879,13 +2856,24 @@ void HistoryItem::clipCallback(ClipReaderNotification notification) {
}
}
bool HistoryItem::displayDate() const {
if (isEmpty()) return false;
void HistoryItem::recountDisplayDate() {
bool displayingDate = ([this]() {
if (isEmpty()) return false;
if (auto prev = previous()) {
return prev->isEmpty() || (prev->date.date() != date.date());
if (auto prev = previous()) {
return prev->isEmpty() || (prev->date.date() != date.date());
}
return true;
})();
if (displayingDate && !Has<HistoryMessageDate>()) {
AddComponents(HistoryMessageDate::Bit());
Get<HistoryMessageDate>()->init(date);
setPendingInitDimensions();
} else if (!displayingDate && Has<HistoryMessageDate>()) {
RemoveComponents(HistoryMessageDate::Bit());
setPendingInitDimensions();
}
return true;
}
HistoryItem::~HistoryItem() {
@ -7196,9 +7184,9 @@ void HistoryMessage::draw(Painter &p, const QRect &r, TextSelection selection, u
int dateh = 0, unreadbarh = 0;
if (auto date = Get<HistoryMessageDate>()) {
dateh = date->height();
if (r.intersects(QRect(0, 0, _history->width, dateh))) {
date->paint(p, 0, _history->width);
}
//if (r.intersects(QRect(0, 0, _history->width, dateh))) {
// date->paint(p, 0, _history->width);
//}
}
if (auto unreadbar = Get<HistoryMessageUnreadBar>()) {
unreadbarh = unreadbar->height();
@ -7986,9 +7974,9 @@ void HistoryService::draw(Painter &p, const QRect &r, TextSelection selection, u
int dateh = 0, unreadbarh = 0;
if (auto date = Get<HistoryMessageDate>()) {
dateh = date->height();
if (r.intersects(QRect(0, 0, _history->width, dateh))) {
date->paint(p, 0, _history->width);
}
//if (r.intersects(QRect(0, 0, _history->width, dateh))) {
// date->paint(p, 0, _history->width);
//}
p.translate(0, dateh);
height -= dateh;
}

View File

@ -1386,6 +1386,13 @@ public:
bool isAttachedToPrevious() const {
return _flags & MTPDmessage_ClientFlag::f_attach_to_previous;
}
bool displayDate() const {
return Has<HistoryMessageDate>();
}
bool isInOneDayWithPrevious() const {
return !isEmpty() && !displayDate();
}
bool isEmpty() const {
return _text.isEmpty() && !_media;
@ -1432,7 +1439,7 @@ protected:
// this should be used only in previousItemChanged()
// to add required bits to the Composer mask
// after that always use Has<HistoryMessageDate>()
bool displayDate() const;
void recountDisplayDate();
// this should be used only in previousItemChanged() or when
// HistoryMessageDate or HistoryMessageUnreadBar bit is changed in the Composer mask

View File

@ -23,6 +23,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
#include "data/data_abstract_structure.h"
#include "mainwidget.h"
#include "lang.h"
namespace HistoryLayout {
namespace {
@ -126,6 +127,23 @@ void paintBubblePart(Painter &p, int x, int y, int width, int height, SideStyle
p.fillRect(x, y, width, height, App::msgServiceBg());
}
void paintPreparedDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w) {
int left = st::msgServiceMargin.left();
int maxwidth = w;
if (Adaptive::Wide()) {
maxwidth = qMin(maxwidth, int32(st::msgMaxWidth + 2 * st::msgPhotoSkip + 2 * st::msgMargin.left()));
}
w = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left();
left += (w - dateTextWidth - st::msgServicePadding.left() - st::msgServicePadding.right()) / 2;
int height = st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();
App::roundRect(p, left, y + st::msgServiceMargin.top(), dateTextWidth + st::msgServicePadding.left() + st::msgServicePadding.left(), height, App::msgServiceBg(), ServiceCorners);
p.setFont(st::msgServiceFont);
p.setPen(st::msgServiceColor);
p.drawText(left + st::msgServicePadding.left(), y + st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->ascent, dateText);
}
} // namepsace
void ServiceMessagePainter::paint(Painter &p, const HistoryService *message, const PaintContext &context, int height) {
@ -177,6 +195,16 @@ void ServiceMessagePainter::paint(Painter &p, const HistoryService *message, con
textstyleRestore();
}
void ServiceMessagePainter::paintDate(Painter &p, const QDateTime &date, int y, int w) {
auto dateText = langDayOfMonthFull(date.date());
auto dateTextWidth = st::msgServiceFont->width(dateText);
paintPreparedDate(p, dateText, dateTextWidth, y, w);
}
void ServiceMessagePainter::paintDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w) {
paintPreparedDate(p, dateText, dateTextWidth, y, w);
}
void ServiceMessagePainter::paintBubble(Painter &p, int left, int width, const Text &text, const QRect &textRect) {
createCircleMasks();

View File

@ -37,6 +37,9 @@ class ServiceMessagePainter {
public:
static void paint(Painter &p, const HistoryService *message, const PaintContext &context, int height);
static void paintDate(Painter &p, const QDateTime &date, int y, int w);
static void paintDate(Painter &p, const QString &dateText, int dateTextWidth, int y, int w);
private:
static void paintBubble(Painter &p, int left, int width, const Text &text, const QRect &textRect);
static QVector<int> countLineWidths(const Text &text, const QRect &textRect);

View File

@ -30,6 +30,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
#include "ui/buttons/history_down_button.h"
#include "inline_bots/inline_bot_result.h"
#include "data/data_drafts.h"
#include "history/history_service_layout.h"
#include "lang.h"
#include "application.h"
#include "mainwidget.h"
@ -90,6 +91,8 @@ public:
};
constexpr int ScrollDateHideTimeout = 1000;
} // namespace
// flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html
@ -108,6 +111,8 @@ HistoryInner::HistoryInner(HistoryWidget *historyWidget, ScrollArea *scroll, His
_trippleClickTimer.setSingleShot(true);
connect(&_scrollDateHideTimer, SIGNAL(timeout()), this, SLOT(onScrollDateHide()));
notifyIsBotChanged();
setMouseTracking(true);
@ -164,22 +169,18 @@ namespace {
}
template <typename Method>
void HistoryInner::enumerateUserpicsInHistory(History *h, int htop, Method method) {
void HistoryInner::enumerateItemsInHistory(History *history, int historytop, Method method) {
// no displayed messages in this history
if (htop < 0 || h->isEmpty() || !h->canHaveFromPhotos() || _visibleAreaBottom <= htop) {
if (historytop < 0 || history->isEmpty() || _visibleAreaBottom <= historytop) {
return;
}
// find and remember the bottom of an attached messages pack
// -1 means we didn't find an attached to previous message yet
int lowestAttachedItemBottom = -1;
// binary search for blockIndex of the first block that is not completely below the visible area
int blockIndex = binarySearchBlocksOrItems(h->blocks, _visibleAreaBottom - htop);
int blockIndex = binarySearchBlocksOrItems(history->blocks, _visibleAreaBottom - historytop);
// binary search for itemIndex of the first item that is not completely below the visible area
HistoryBlock *block = h->blocks.at(blockIndex);
int blocktop = htop + block->y;
HistoryBlock *block = history->blocks.at(blockIndex);
int blocktop = historytop + block->y;
int itemIndex = binarySearchBlocksOrItems(block->items, _visibleAreaBottom - blocktop);
while (true) {
@ -191,35 +192,8 @@ void HistoryInner::enumerateUserpicsInHistory(History *h, int htop, Method metho
// binary search should've skipped all the items that are below the visible area
t_assert(itemtop < _visibleAreaBottom);
// skip all service messages
if (HistoryMessage *message = item->toHistoryMessage()) {
if (lowestAttachedItemBottom < 0 && message->isAttachedToPrevious()) {
lowestAttachedItemBottom = itembottom - message->marginBottom();
}
// draw userpic for all messages that have it and for those who are not showing it
// because of their attachment to the previous message if they are top-most visible
if (message->displayFromPhoto() || (message->hasFromPhoto() && itemtop <= _visibleAreaTop)) {
if (lowestAttachedItemBottom < 0) {
lowestAttachedItemBottom = itembottom - message->marginBottom();
}
// attach userpic to the top of the visible area with the same margin as it is from the left side
int userpicTop = qMax(itemtop + message->marginTop(), _visibleAreaTop + st::msgMargin.left());
// do not let the userpic go below the attached messages pack bottom line
userpicTop = qMin(userpicTop, lowestAttachedItemBottom - int(st::msgPhotoSize));
// call the template callback function that was passed
// and return if it finished everything it needed
if (!method(message, userpicTop)) {
return;
}
}
// forget the found bottom of the pack, search for the next one from scratch
if (!message->isAttachedToPrevious()) {
lowestAttachedItemBottom = -1;
}
if (!method(item, itemtop, itembottom)) {
return;
}
// skip all the items that are above the visible area
@ -236,13 +210,126 @@ void HistoryInner::enumerateUserpicsInHistory(History *h, int htop, Method metho
if (--blockIndex < 0) {
return;
} else {
block = h->blocks.at(blockIndex);
blocktop = htop + block->y;
block = history->blocks.at(blockIndex);
blocktop = historytop + block->y;
itemIndex = block->items.size() - 1;
}
}
}
template <typename Method>
void HistoryInner::enumerateUserpics(Method method) {
if ((!_history || !_history->canHaveFromPhotos()) && (!_migrated || !_migrated->canHaveFromPhotos())) {
return;
}
// find and remember the bottom of an attached messages pack
// -1 means we didn't find an attached to previous message yet
int lowestAttachedItemBottom = -1;
auto userpicCallback = [this, &lowestAttachedItemBottom, &method](HistoryItem *item, int itemtop, int itembottom) {
// skip all service messages
auto message = item->toHistoryMessage();
if (!message) return true;
if (lowestAttachedItemBottom < 0 && message->isAttachedToPrevious()) {
lowestAttachedItemBottom = itembottom - message->marginBottom();
}
// 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 previous message if they are top-most visible
if (message->displayFromPhoto() || (message->hasFromPhoto() && itemtop <= _visibleAreaTop)) {
if (lowestAttachedItemBottom < 0) {
lowestAttachedItemBottom = itembottom - message->marginBottom();
}
// attach userpic to the top of the visible area with the same margin as it is from the left side
int userpicTop = qMax(itemtop + message->marginTop(), _visibleAreaTop + st::msgMargin.left());
// do not let the userpic go below the attached messages pack bottom line
userpicTop = qMin(userpicTop, lowestAttachedItemBottom - st::msgPhotoSize);
// call the template callback function that was passed
// and return if it finished everything it needed
if (!method(message, userpicTop)) {
return false;
}
}
// forget the found bottom of the pack, search for the next one from scratch
if (!message->isAttachedToPrevious()) {
lowestAttachedItemBottom = -1;
}
return true;
};
auto movedToMigratedHistoryCallback = [&lowestAttachedItemBottom]() {
// reset the found bottom of the pack when moved from _history to _migrated enumeration
lowestAttachedItemBottom = -1;
};
enumerateItems(userpicCallback, movedToMigratedHistoryCallback);
}
template <typename Method>
void HistoryInner::enumerateDates(Method method) {
int drawtop = historyDrawTop();
// find and remember the bottom of an single-day messages pack
// -1 means we didn't find a same-day with previous message yet
int lowestInOneDayItemBottom = -1;
auto dateCallback = [this, &lowestInOneDayItemBottom, &method, drawtop](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 <= _visibleAreaTop)) {
// skip the date of history migrate item if it will be in migrated
if (itemtop < drawtop && item->history() == _history) {
if (itemtop > _visibleAreaTop) {
// previous item (from the _migrated history) is drawing date now
return false;
} else if (item == _history->blocks.front()->items.front() && item->isGroupMigrate()
&& _migrated->blocks.back()->items.back()->isGroupMigrate()) {
// this item is completely invisible and should be completely ignored
return false;
}
}
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
int dateTop = qMax(itemtop, _visibleAreaTop) + st::msgServiceMargin.top();
// do not let the date go below the single-day messages pack bottom line
int 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;
};
auto movedToMigratedHistoryCallback = [&lowestInOneDayItemBottom]() {
// reset the found bottom of the pack when moved from _history to _migrated enumeration
lowestInOneDayItemBottom = -1;
};
enumerateItems(dateCallback, movedToMigratedHistoryCallback);
}
void HistoryInner::paintEvent(QPaintEvent *e) {
if (!App::main() || (App::wnd() && App::wnd()->contentOverlapped(this, e))) {
return;
@ -386,7 +473,7 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
}
if (mtop >= 0 || htop >= 0) {
enumerateUserpics([&p, &r](HistoryMessage *message, int userpicTop) -> bool {
enumerateUserpics([&p, &r](HistoryMessage *message, int userpicTop) {
// stop the enumeration if the userpic is above the painted rect
if (userpicTop + st::msgPhotoSize <= r.top()) {
return false;
@ -398,6 +485,37 @@ void HistoryInner::paintEvent(QPaintEvent *e) {
}
return true;
});
auto scrollDateOpacity = _scrollDateOpacity.current(ms, _scrollDateShown ? 1. : 0.);
enumerateDates([&p, &r, scrollDateOpacity](HistoryItem *item, int itemtop, int dateTop) {
int dateHeight = st::msgServicePadding.bottom() + st::msgServiceFont->height + st::msgServicePadding.top();
// stop the enumeration if the date is above the painted rect
if (dateTop + dateHeight <= r.top()) {
return false;
}
bool dateInPlace = item->displayDate();
if (dateInPlace) {
int correctDateTop = itemtop + st::msgServiceMargin.top();
dateInPlace = (dateTop < correctDateTop + dateHeight);
}
// paint the date if it intersects the painted rect
if (dateTop < r.top() + r.height()) {
auto opacity = dateInPlace ? 1. : scrollDateOpacity;
if (opacity > 0.) {
p.setOpacity(opacity);
int dateY = dateTop - st::msgServiceMargin.top(), width = item->history()->width;
if (auto date = item->Get<HistoryMessageDate>()) {
date->paint(p, dateY, width);
} else {
HistoryLayout::ServiceMessagePainter::paintDate(p, item->date, dateY, width);
}
}
}
return true;
});
}
}
}
@ -1494,6 +1612,46 @@ void HistoryInner::visibleAreaUpdated(int top, int bottom) {
}
}
}
_scrollDateCheck.call();
}
void HistoryInner::onScrollDateCheck() {
if (!_history) return;
auto newScrollDateItem = _history->scrollTopItem ? _history->scrollTopItem : (_migrated ? _migrated->scrollTopItem : nullptr);
auto newScrollDateItemTop = _history->scrollTopItem ? _history->scrollTopOffset : (_migrated ? _migrated->scrollTopOffset : 0);
if (!newScrollDateItem) {
_scrollDateLastItem = nullptr;
_scrollDateLastItemTop = 0;
onScrollDateHide();
} else if (newScrollDateItem != _scrollDateLastItem || newScrollDateItemTop != _scrollDateLastItemTop) {
// Show scroll date only if it is not the initial onScroll() event (with empty _scrollDateLastItem).
if (_scrollDateLastItem && !_scrollDateShown) {
toggleScrollDateShown();
}
_scrollDateLastItem = newScrollDateItem;
_scrollDateLastItemTop = newScrollDateItemTop;
_scrollDateHideTimer.start(ScrollDateHideTimeout);
}
}
void HistoryInner::onScrollDateHide() {
_scrollDateHideTimer.stop();
if (_scrollDateShown) {
toggleScrollDateShown();
}
}
void HistoryInner::toggleScrollDateShown() {
_scrollDateShown = !_scrollDateShown;
auto from = _scrollDateShown ? 0. : 1.;
auto to = _scrollDateShown ? 1. : 0.;
START_ANIMATION(_scrollDateOpacity, func(this, &HistoryInner::repaintScrollDateCallback), from, to, st::btnAttachEmoji.duration, anim::linear);
}
void HistoryInner::repaintScrollDateCallback() {
int updateTop = _visibleAreaTop;
int updateHeight = st::msgServiceMargin.top() + st::msgServicePadding.top() + st::msgServiceFont->height + st::msgServicePadding.bottom();
update(0, updateTop, width(), updateHeight);
}
void HistoryInner::updateSize() {

View File

@ -138,6 +138,11 @@ public slots:
void onTouchScrollTimer();
void onDragExec();
private slots:
void onScrollDateCheck();
void onScrollDateHide();
private:
void touchResetSpeed();
@ -152,6 +157,9 @@ private:
void setToClipboard(const TextWithEntities &forClipboard);
void toggleScrollDateShown();
void repaintScrollDateCallback();
PeerData *_peer = nullptr;
History *_migrated = nullptr;
History *_history = nullptr;
@ -249,19 +257,45 @@ private:
int _visibleAreaTop = 0;
int _visibleAreaBottom = 0;
bool _scrollDateShown = false;
FloatAnimation _scrollDateOpacity;
SingleDelayedCall _scrollDateCheck = { this, "onScrollDateCheck" };
SingleTimer _scrollDateHideTimer;
HistoryItem *_scrollDateLastItem = nullptr;
int _scrollDateLastItemTop = 0;
// this function finds all history items that are displayed and calls template method
// for each found message (from the bottom to the top) in the passed history with passed top offset
//
// method has "bool (*Method)(HistoryItem *item, int itemtop, int itembottom)" signature
// if it returns false the enumeration stops immidiately
template <typename Method>
void enumerateItemsInHistory(History *history, int historytop, Method method);
template <typename Method, typename SwitchToMigratedCallback>
void enumerateItems(Method method, SwitchToMigratedCallback switchToMigrated) {
enumerateItemsInHistory(_history, historyTop(), method);
if (_migrated) {
switchToMigrated();
enumerateItemsInHistory(_migrated, migratedTop(), method);
}
}
// this function finds all userpics on the left that are displayed and calls template method
// for each found userpic (from the bottom to the top) in the passed history with passed top offset
// for each found userpic (from the bottom to the top) using enumerateItems() method
//
// method has "bool (*Method)(HistoryMessage *message, int userpicTop)" signature
// if it returns false the enumeration stops immidiately
template <typename Method>
void enumerateUserpicsInHistory(History *h, int htop, Method method);
void enumerateUserpics(Method method);
// this function finds all date elements that are displayed and calls template method
// for each found date element (from the bottom to the top) using enumerateItems() method
//
// method has "bool (*Method)(HistoryItem *item, int itemtop, int dateTop)" signature
// if it returns false the enumeration stops immidiately
template <typename Method>
void enumerateUserpics(Method method) {
enumerateUserpicsInHistory(_history, historyTop(), method);
enumerateUserpicsInHistory(_migrated, migratedTop(), method);
}
void enumerateDates(Method method);
};

View File

@ -1031,7 +1031,7 @@ enum class MTPDmessage_ClientFlag : int32 {
f_clientside_unread = (1 << 22),
// update this when adding new client side flags
MIN_FIELD = (1 << 23),
MIN_FIELD = (1 << 22),
};
DEFINE_MTP_CLIENT_FLAGS(MTPDmessage)