tdesktop/Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp

459 lines
13 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "statistics/widgets/point_details_widget.h"
#include "data/data_channel_earn.h" // Data::kEarnMultiplier.
#include "info/channel_statistics/earn/earn_format.h"
#include "lang/lang_keys.h"
#include "statistics/statistics_common.h"
#include "statistics/statistics_format_values.h"
#include "statistics/view/stack_linear_chart_common.h"
#include "ui/cached_round_corners.h"
#include "ui/effects/ripple_animation.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "styles/style_layers.h"
#include "styles/style_statistics.h"
#include <QtCore/QDateTime>
#include <QtCore/QLocale>
namespace Statistic {
namespace {
[[nodiscard]] QString FormatWeek(float64 timestamp) {
constexpr auto kSevenDays = 3600 * 24 * 7;
timestamp /= 1000;
return LangDayMonth(timestamp)
+ ' '
+ QChar(8212)
+ ' '
+ LangDayMonthYear(timestamp + kSevenDays);
}
void PaintShadow(QPainter &p, int radius, const QRect &r) {
constexpr auto kHorizontalOffset = 1;
constexpr auto kHorizontalOffset2 = 2;
constexpr auto kVerticalOffset = 2;
constexpr auto kVerticalOffset2 = 3;
constexpr auto kOpacityStep = 0.2;
constexpr auto kOpacityStep2 = 0.4;
const auto hOffset = style::ConvertScale(kHorizontalOffset);
const auto hOffset2 = style::ConvertScale(kHorizontalOffset2);
const auto vOffset = style::ConvertScale(kVerticalOffset);
const auto vOffset2 = style::ConvertScale(kVerticalOffset2);
const auto opacity = p.opacity();
auto hq = PainterHighQualityEnabler(p);
p.setOpacity(opacity);
p.drawRoundedRect(r + QMarginsF(0, hOffset, 0, hOffset), radius, radius);
p.setOpacity(opacity * kOpacityStep);
p.drawRoundedRect(r + QMarginsF(hOffset, 0, hOffset, 0), radius, radius);
p.setOpacity(opacity * kOpacityStep2);
p.drawRoundedRect(
r + QMarginsF(hOffset2, 0, hOffset2, 0),
radius,
radius);
p.setOpacity(opacity * kOpacityStep);
p.drawRoundedRect(r + QMarginsF(0, 0, 0, vOffset), radius, radius);
p.setOpacity(opacity * kOpacityStep2);
p.drawRoundedRect(r + QMarginsF(0, 0, 0, vOffset2), radius, radius);
p.setOpacity(opacity);
}
} // namespace
void PaintDetails(
QPainter &p,
const Data::StatisticalChart::Line &line,
int absoluteValue,
const QRect &rect) {
auto name = Ui::Text::String(
st::statisticsDetailsPopupStyle,
line.name);
auto value = Ui::Text::String(
st::statisticsDetailsPopupStyle,
Lang::FormatCountDecimal(absoluteValue));
const auto nameWidth = name.maxWidth();
const auto valueWidth = value.maxWidth();
const auto width = valueWidth
+ rect::m::sum::h(st::statisticsDetailsPopupMargins)
+ rect::m::sum::h(st::statisticsDetailsPopupPadding)
+ st::statisticsDetailsPopupPadding.left() // Between strings.
+ nameWidth;
const auto height = st::statisticsDetailsPopupStyle.font->height
+ rect::m::sum::v(st::statisticsDetailsPopupMargins)
+ rect::m::sum::v(st::statisticsDetailsPopupPadding);
const auto fullRect = QRect(
rect.x() + rect.width() - width,
rect.y(),
width,
height);
const auto innerRect = fullRect - st::statisticsDetailsPopupPadding;
const auto textRect = innerRect - st::statisticsDetailsPopupMargins;
p.setBrush(st::shadowFg);
p.setPen(Qt::NoPen);
PaintShadow(p, st::boxRadius, innerRect);
Ui::FillRoundRect(p, innerRect, st::boxBg, Ui::BoxCorners);
const auto lineY = textRect.y();
const auto valueContext = Ui::Text::PaintContext{
.position = QPoint(rect::right(textRect) - valueWidth, lineY),
.outerWidth = textRect.width(),
.availableWidth = valueWidth,
};
const auto nameContext = Ui::Text::PaintContext{
.position = QPoint(textRect.x(), lineY),
.outerWidth = textRect.width(),
.availableWidth = textRect.width() - valueWidth,
};
p.setPen(st::boxTextFg);
name.draw(p, nameContext);
p.setPen(line.color);
value.draw(p, valueContext);
}
PointDetailsWidget::PointDetailsWidget(
not_null<Ui::RpWidget*> parent,
const Data::StatisticalChart &chartData,
float64 maxAbsoluteValue,
bool zoomEnabled)
: Ui::AbstractButton(parent)
, _zoomEnabled(zoomEnabled)
, _chartData(chartData)
, _textStyle(st::statisticsDetailsPopupStyle)
, _headerStyle(st::statisticsDetailsPopupHeaderStyle)
, _valueIcon(chartData.currencyRate ? &st::statisticsCurrencyIcon : nullptr) {
if (zoomEnabled) {
rpl::single(rpl::empty_value()) | rpl::then(
style::PaletteChanged()
) | rpl::start_with_next([=] {
const auto w = st::statisticsDetailsArrowShift;
const auto stroke = style::ConvertScaleExact(
st::statisticsDetailsArrowStroke);
_arrow = QImage(
QSize(w + stroke, w * 2 + stroke) * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
_arrow.setDevicePixelRatio(style::DevicePixelRatio());
_arrow.fill(Qt::transparent);
{
auto p = QPainter(&_arrow);
const auto hq = PainterHighQualityEnabler(p);
const auto s = stroke / 2.;
p.setPen(QPen(st::windowSubTextFg, stroke));
p.drawLine(QLineF(s, s, w, w + s));
p.drawLine(QLineF(s, s + w * 2, w, w + s));
}
invalidateCache();
}, lifetime());
}
_maxPercentageWidth = [&] {
if (_chartData.hasPercentages) {
const auto maxPercentageText = Ui::Text::String(
_textStyle,
u"10000%"_q);
return maxPercentageText.maxWidth();
}
return 0;
}();
const auto calculatedWidth = [&]{
const auto maxValueText = Ui::Text::String(
_textStyle,
Lang::FormatCountDecimal(maxAbsoluteValue));
const auto maxValueTextWidth = maxValueText.maxWidth();
auto maxNameTextWidth = 0;
for (const auto &dataLine : _chartData.lines) {
const auto maxNameText = Ui::Text::String(
_textStyle,
dataLine.name);
maxNameTextWidth = std::max(
maxNameText.maxWidth(),
maxNameTextWidth);
}
{
const auto maxHeaderText = Ui::Text::String(
_headerStyle,
_chartData.weekFormat
? FormatWeek(_chartData.x.front())
: LangDetailedDayMonth(_chartData.x.front() / 1000));
maxNameTextWidth = std::max(
maxHeaderText.maxWidth()
+ st::statisticsDetailsPopupPadding.left(),
maxNameTextWidth);
}
return maxValueTextWidth
+ rect::m::sum::h(st::statisticsDetailsPopupMargins)
+ rect::m::sum::h(st::statisticsDetailsPopupPadding)
+ st::statisticsDetailsPopupPadding.left() // Between strings.
+ maxNameTextWidth
+ (_valueIcon ? _valueIcon->width() : 0)
+ _maxPercentageWidth;
}();
sizeValue(
) | rpl::start_with_next([=](const QSize &s) {
const auto fullRect = s.isNull()
? Rect(Size(calculatedWidth))
: Rect(s);
_innerRect = fullRect - st::statisticsDetailsPopupPadding;
_textRect = _innerRect - st::statisticsDetailsPopupMargins;
invalidateCache();
}, lifetime());
resize(calculatedWidth, height());
resizeHeight();
}
void PointDetailsWidget::setLineAlpha(int lineId, float64 alpha) {
for (auto &line : _lines) {
if (line.id == lineId) {
if (line.alpha != alpha) {
line.alpha = alpha;
resizeHeight();
invalidateCache();
update();
}
return;
}
}
}
void PointDetailsWidget::resizeHeight() {
resize(
width(),
lineYAt(_chartData.lines.size() + (_chartData.currencyRate ? 1 : 0))
+ st::statisticsDetailsPopupMargins.bottom());
}
int PointDetailsWidget::xIndex() const {
return _xIndex;
}
void PointDetailsWidget::setXIndex(int xIndex) {
_xIndex = xIndex;
if (xIndex < 0) {
return;
}
{
constexpr auto kOneDay = 3600 * 24 * 1000;
const auto timestamp = _chartData.x[xIndex];
_header.setText(
_headerStyle,
(timestamp < kOneDay)
? _chartData.getDayString(xIndex)
: _chartData.weekFormat
? FormatWeek(timestamp)
: LangDetailedDayMonth(timestamp / 1000));
}
_lines.clear();
_lines.reserve(_chartData.lines.size());
auto hasPositiveValues = false;
const auto parts = _maxPercentageWidth
? PiePartsPercentageByIndices(
_chartData,
nullptr,
{ float64(xIndex), float64(xIndex) }).parts
: std::vector<PiePartData::Part>();
const auto multiplier = float64(Data::kEarnMultiplier);
for (auto i = 0; i < _chartData.lines.size(); i++) {
const auto &dataLine = _chartData.lines[i];
auto textLine = Line();
textLine.id = dataLine.id;
if (_maxPercentageWidth) {
textLine.percentage.setText(_textStyle, parts[i].percentageText);
}
textLine.name.setText(_textStyle, dataLine.name);
textLine.value.setText(
_textStyle,
Lang::FormatCountDecimal(dataLine.y[xIndex]));
hasPositiveValues |= (dataLine.y[xIndex] > 0);
textLine.valueColor = QColor(dataLine.color);
if (_chartData.currencyRate) {
auto copy = Line();
copy.id = dataLine.id * 100;
copy.valueColor = QColor(dataLine.color);
copy.name.setText(
_textStyle,
tr::lng_channel_earn_chart_overriden_detail_currency(
tr::now));
copy.value.setText(
_textStyle,
QString::number(dataLine.y[xIndex] / multiplier));
_lines.push_back(std::move(copy));
textLine.name.setText(
_textStyle,
tr::lng_channel_earn_chart_overriden_detail_usd(tr::now));
textLine.value.setText(
_textStyle,
Info::ChannelEarn::ToUsd(
dataLine.y[xIndex],
_chartData.currencyRate));
}
_lines.push_back(std::move(textLine));
}
const auto clickable = _zoomEnabled && hasPositiveValues;
_hasPositiveValues = hasPositiveValues;
QWidget::setAttribute(
Qt::WA_TransparentForMouseEvents,
!clickable);
invalidateCache();
}
void PointDetailsWidget::setAlpha(float64 alpha) {
_alpha = alpha;
update();
}
float64 PointDetailsWidget::alpha() const {
return _alpha;
}
int PointDetailsWidget::lineYAt(int index) const {
auto linesHeight = 0.;
for (auto i = 0; i < index; i++) {
const auto alpha = (i >= _lines.size()) ? 1. : _lines[i].alpha;
linesHeight += alpha
* (_textStyle.font->height
+ st::statisticsDetailsPopupMidLineSpace);
}
return _textRect.y()
+ _headerStyle.font->height
+ st::statisticsDetailsPopupMargins.bottom() / 2
+ std::ceil(linesHeight);
}
void PointDetailsWidget::invalidateCache() {
_cache = QImage();
}
void PointDetailsWidget::mousePressEvent(QMouseEvent *e) {
AbstractButton::mousePressEvent(e);
const auto position = e->pos() - _innerRect.topLeft();
if (!_ripple) {
_ripple = std::make_unique<Ui::RippleAnimation>(
st::defaultRippleAnimation,
Ui::RippleAnimation::RoundRectMask(
_innerRect.size(),
st::boxRadius),
[=] { update(); });
}
_ripple->add(position);
}
void PointDetailsWidget::mouseReleaseEvent(QMouseEvent *e) {
AbstractButton::mouseReleaseEvent(e);
if (_ripple) {
_ripple->lastStop();
}
}
void PointDetailsWidget::paintEvent(QPaintEvent *e) {
auto painter = QPainter(this);
if (_cache.isNull()) {
_cache = QImage(
size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
_cache.setDevicePixelRatio(style::DevicePixelRatio());
_cache.fill(Qt::transparent);
auto p = QPainter(&_cache);
p.setBrush(st::shadowFg);
p.setPen(Qt::NoPen);
PaintShadow(p, st::boxRadius, _innerRect);
Ui::FillRoundRect(p, _innerRect, st::boxBg, Ui::BoxCorners);
if (_ripple) {
_ripple->paint(p, _innerRect.left(), _innerRect.top(), width());
if (_ripple->empty()) {
_ripple.reset();
}
}
p.setPen(st::boxTextFg);
const auto headerContext = Ui::Text::PaintContext{
.position = _textRect.topLeft(),
.availableWidth = _textRect.width(),
};
_header.draw(p, headerContext);
for (auto i = 0; i < _lines.size(); i++) {
const auto &line = _lines[i];
const auto lineY = lineYAt(i);
const auto valueWidth = line.value.maxWidth();
const auto valueContext = Ui::Text::PaintContext{
.position = QPoint(
rect::right(_textRect) - valueWidth,
lineY),
.outerWidth = _textRect.width(),
.availableWidth = valueWidth,
};
if (!i && _valueIcon) {
_valueIcon->paint(
p,
valueContext.position.x() - _valueIcon->width(),
lineY,
valueContext.outerWidth,
line.valueColor);
}
const auto nameContext = Ui::Text::PaintContext{
.position = QPoint(
_textRect.x() + _maxPercentageWidth,
lineY),
.outerWidth = _textRect.width(),
.availableWidth = _textRect.width() - valueWidth,
};
p.setOpacity(line.alpha * line.alpha);
p.setPen(st::boxTextFg);
if (_maxPercentageWidth) {
const auto percentageContext = Ui::Text::PaintContext{
.position = QPoint(_textRect.x(), lineY),
.outerWidth = _textRect.width(),
.availableWidth = _textRect.width() - valueWidth,
};
line.percentage.draw(p, percentageContext);
}
line.name.draw(p, nameContext);
p.setPen(line.valueColor);
line.value.draw(p, valueContext);
}
if (_zoomEnabled && _hasPositiveValues) {
const auto s = _arrow.size() / style::DevicePixelRatio();
const auto x = rect::right(_textRect) - s.width();
const auto y = _textRect.y()
+ (_headerStyle.font->ascent - s.height());
p.drawImage(x, y, _arrow);
}
}
if (_alpha < 1.) {
painter.setOpacity(_alpha);
}
painter.drawImage(0, 0, _cache);
if (_ripple) {
invalidateCache();
}
}
} // namespace Statistic