init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
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/chart_header_widget.h"
|
||||
|
||||
#include "ui/painter.h"
|
||||
#include "styles/style_statistics.h"
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
Header::Header(not_null<Ui::RpWidget*> parent)
|
||||
: Ui::RpWidget(parent)
|
||||
, _height(st::statisticsChartHeaderHeight) {
|
||||
}
|
||||
|
||||
QString Header::title() const {
|
||||
return _title.toString();
|
||||
}
|
||||
|
||||
void Header::setTitle(QString title) {
|
||||
_title.setText(st::statisticsHeaderTitleTextStyle, std::move(title));
|
||||
}
|
||||
|
||||
int Header::resizeGetHeight(int newWidth) {
|
||||
return _height;
|
||||
}
|
||||
|
||||
void Header::setSubTitle(QString subTitle) {
|
||||
_height = subTitle.isEmpty()
|
||||
? st::statisticsHeaderTitleTextStyle.font->height
|
||||
: st::statisticsChartHeaderHeight;
|
||||
_subTitle.setText(
|
||||
st::statisticsHeaderDatesTextStyle,
|
||||
std::move(subTitle));
|
||||
}
|
||||
|
||||
void Header::paintEvent(QPaintEvent *e) {
|
||||
auto p = Painter(this);
|
||||
|
||||
p.fillRect(rect(), st::boxBg);
|
||||
|
||||
p.setPen(st::windowActiveTextFg);
|
||||
_title.drawLeftElided(p, 0, 0, width(), width());
|
||||
|
||||
p.setPen(st::windowSubTextFg);
|
||||
_subTitle.drawLeftElided(p, 0, _infoTop, width(), width());
|
||||
}
|
||||
|
||||
void Header::resizeEvent(QResizeEvent *e) {
|
||||
_infoTop = e->size().height()
|
||||
- st::statisticsHeaderDatesTextStyle.font->height;
|
||||
}
|
||||
|
||||
} // namespace Statistic
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
class Header final : public Ui::RpWidget {
|
||||
public:
|
||||
explicit Header(not_null<Ui::RpWidget*> parent);
|
||||
|
||||
[[nodiscard]] QString title() const;
|
||||
void setTitle(QString title);
|
||||
void setSubTitle(QString subTitle);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void resizeEvent(QResizeEvent *e) override;
|
||||
|
||||
int resizeGetHeight(int newWidth) override;
|
||||
|
||||
private:
|
||||
Ui::Text::String _title;
|
||||
Ui::Text::String _subTitle;
|
||||
int _infoTop = 0;
|
||||
int _height = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Statistic
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
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/chart_lines_filter_widget.h"
|
||||
|
||||
#include "ui/abstract_button.h"
|
||||
#include "ui/effects/animations.h"
|
||||
#include "ui/effects/shake_animation.h"
|
||||
#include "ui/painter.h"
|
||||
#include "ui/rect.h"
|
||||
#include "styles/style_basic.h"
|
||||
#include "styles/style_statistics.h"
|
||||
#include "styles/style_widgets.h"
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
class ChartLinesFilterWidget::FlatCheckbox final : public Ui::AbstractButton {
|
||||
public:
|
||||
FlatCheckbox(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const QString &text,
|
||||
QColor activeColor);
|
||||
|
||||
void shake();
|
||||
void setChecked(bool value, bool animated);
|
||||
[[nodiscard]] bool checked() const;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
|
||||
private:
|
||||
const QColor _inactiveTextColor;
|
||||
const QColor _activeColor;
|
||||
const QColor _inactiveColor;
|
||||
Ui::Text::String _text;
|
||||
|
||||
Ui::Animations::Simple _animation;
|
||||
|
||||
struct {
|
||||
Ui::Animations::Simple animation;
|
||||
int shift = 0;
|
||||
} _shake;
|
||||
|
||||
bool _checked = true;
|
||||
|
||||
};
|
||||
|
||||
ChartLinesFilterWidget::FlatCheckbox::FlatCheckbox(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const QString &text,
|
||||
QColor activeColor)
|
||||
: Ui::AbstractButton(parent)
|
||||
, _inactiveTextColor(st::premiumButtonFg->c)
|
||||
, _activeColor(activeColor)
|
||||
, _inactiveColor(st::boxBg->c)
|
||||
, _text(st::statisticsDetailsPopupStyle, text) {
|
||||
const auto &margins = st::statisticsChartFlatCheckboxMargins;
|
||||
const auto h = _text.minHeight() + rect::m::sum::v(margins) * 2;
|
||||
resize(
|
||||
_text.maxWidth()
|
||||
+ rect::m::sum::h(margins)
|
||||
+ h
|
||||
+ st::statisticsChartFlatCheckboxCheckWidth * 3
|
||||
- st::statisticsChartFlatCheckboxShrinkkWidth,
|
||||
h);
|
||||
}
|
||||
|
||||
void ChartLinesFilterWidget::FlatCheckbox::setChecked(
|
||||
bool value,
|
||||
bool animated) {
|
||||
if (_checked == value) {
|
||||
return;
|
||||
}
|
||||
_checked = value;
|
||||
if (!animated) {
|
||||
_animation.stop();
|
||||
} else {
|
||||
const auto from = value ? 0. : 1.;
|
||||
const auto to = value ? 1. : 0.;
|
||||
_animation.start([=] { update(); }, from, to, st::shakeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
bool ChartLinesFilterWidget::FlatCheckbox::checked() const {
|
||||
return _checked;
|
||||
}
|
||||
|
||||
void ChartLinesFilterWidget::FlatCheckbox::shake() {
|
||||
if (_shake.animation.animating()) {
|
||||
return;
|
||||
}
|
||||
_shake.animation.start(Ui::DefaultShakeCallback([=](int shift) {
|
||||
_shake.shift = shift;
|
||||
update();
|
||||
}), 0., 1., st::shakeDuration);
|
||||
}
|
||||
|
||||
void ChartLinesFilterWidget::FlatCheckbox::paintEvent(QPaintEvent *e) {
|
||||
auto p = QPainter(this);
|
||||
|
||||
const auto progress = _animation.value(_checked ? 1. : 0.);
|
||||
|
||||
p.translate(_shake.shift, 0);
|
||||
|
||||
const auto checkWidth = st::statisticsChartFlatCheckboxCheckWidth;
|
||||
const auto r = rect() - st::statisticsChartFlatCheckboxMargins;
|
||||
const auto heightHalf = r.height() / 2.;
|
||||
const auto textX = anim::interpolate(
|
||||
r.center().x() - _text.maxWidth() / 2.,
|
||||
r.x() + heightHalf + checkWidth * 5,
|
||||
progress);
|
||||
const auto textY = (r - st::statisticsChartFlatCheckboxMargins).y();
|
||||
p.fillRect(r, Qt::transparent);
|
||||
|
||||
constexpr auto kCheckPartProgress = 0.5;
|
||||
const auto checkProgress = progress / kCheckPartProgress;
|
||||
const auto textColor = (progress <= kCheckPartProgress)
|
||||
? anim::color(_activeColor, _inactiveTextColor, checkProgress)
|
||||
: _inactiveTextColor;
|
||||
const auto fillColor = (progress <= kCheckPartProgress)
|
||||
? anim::color(_inactiveColor, _activeColor, checkProgress)
|
||||
: _activeColor;
|
||||
|
||||
p.setPen(QPen(_activeColor, st::statisticsChartLineWidth));
|
||||
p.setBrush(fillColor);
|
||||
const auto radius = r.height() / 2.;
|
||||
{
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.drawRoundedRect(r, radius, radius);
|
||||
}
|
||||
|
||||
p.setPen(textColor);
|
||||
const auto textContext = Ui::Text::PaintContext{
|
||||
.position = QPoint(textX, textY),
|
||||
.availableWidth = width(),
|
||||
};
|
||||
_text.draw(p, textContext);
|
||||
|
||||
if (progress > kCheckPartProgress) {
|
||||
auto hq = PainterHighQualityEnabler(p);
|
||||
p.setPen(QPen(textColor, st::statisticsChartLineWidth));
|
||||
const auto bounceProgress = checkProgress - 1.;
|
||||
const auto start = QPoint(
|
||||
r.x() + heightHalf + checkWidth,
|
||||
textY + _text.style()->font->ascent);
|
||||
p.translate(start);
|
||||
p.drawLine({}, -QPoint(checkWidth, checkWidth) * bounceProgress);
|
||||
p.drawLine({}, QPoint(checkWidth, -checkWidth) * bounceProgress * 2);
|
||||
}
|
||||
}
|
||||
|
||||
ChartLinesFilterWidget::ChartLinesFilterWidget(
|
||||
not_null<Ui::RpWidget*> parent)
|
||||
: Ui::RpWidget(parent) {
|
||||
}
|
||||
|
||||
void ChartLinesFilterWidget::resizeToWidth(int outerWidth) {
|
||||
auto maxRight = 0;
|
||||
for (auto i = 0; i < _buttons.size(); i++) {
|
||||
const auto raw = _buttons[i].get();
|
||||
if (!i) {
|
||||
raw->move(0, 0);
|
||||
} else {
|
||||
const auto prevRaw = _buttons[i - 1].get();
|
||||
const auto prevLeft = rect::right(prevRaw);
|
||||
const auto isOut = (prevLeft + raw->width() > outerWidth);
|
||||
const auto left = isOut ? 0 : prevLeft;
|
||||
const auto top = isOut ? rect::bottom(prevRaw) : prevRaw->y();
|
||||
raw->move(left, top);
|
||||
}
|
||||
maxRight = std::max(maxRight, rect::right(raw));
|
||||
}
|
||||
if (!_buttons.empty()) {
|
||||
resize(maxRight, rect::bottom(_buttons.back().get()));
|
||||
}
|
||||
}
|
||||
|
||||
void ChartLinesFilterWidget::fillButtons(
|
||||
const std::vector<ButtonData> &buttonsData) {
|
||||
_buttons.clear();
|
||||
|
||||
_buttons.reserve(buttonsData.size());
|
||||
for (auto i = 0; i < buttonsData.size(); i++) {
|
||||
const auto &buttonData = buttonsData[i];
|
||||
auto button = base::make_unique_q<FlatCheckbox>(
|
||||
this,
|
||||
buttonData.text,
|
||||
buttonData.color);
|
||||
button->show();
|
||||
if (buttonData.disabled) {
|
||||
button->setChecked(false, false);
|
||||
}
|
||||
const auto id = buttonData.id;
|
||||
button->setClickedCallback([=, raw = button.get()] {
|
||||
const auto checked = !raw->checked();
|
||||
if (!checked) {
|
||||
const auto cancel = [&] {
|
||||
for (const auto &b : _buttons) {
|
||||
if (b.get() == raw) {
|
||||
continue;
|
||||
}
|
||||
if (b->checked()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}();
|
||||
if (cancel) {
|
||||
raw->shake();
|
||||
return;
|
||||
}
|
||||
}
|
||||
raw->setChecked(checked, true);
|
||||
_buttonEnabledChanges.fire({ .id = id, .enabled = checked });
|
||||
});
|
||||
|
||||
_buttons.push_back(std::move(button));
|
||||
}
|
||||
}
|
||||
|
||||
auto ChartLinesFilterWidget::buttonEnabledChanges() const
|
||||
-> rpl::producer<ChartLinesFilterWidget::Entry> {
|
||||
return _buttonEnabledChanges.events();
|
||||
}
|
||||
|
||||
} // namespace Statistic
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/rp_widget.h"
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
class ChartLinesFilterWidget final : public Ui::RpWidget {
|
||||
public:
|
||||
ChartLinesFilterWidget(not_null<Ui::RpWidget*> parent);
|
||||
|
||||
struct ButtonData final {
|
||||
QString text;
|
||||
QColor color;
|
||||
int id = 0;
|
||||
bool disabled = false;
|
||||
};
|
||||
|
||||
void fillButtons(const std::vector<ButtonData> &buttonsData);
|
||||
|
||||
void resizeToWidth(int outerWidth);
|
||||
|
||||
struct Entry final {
|
||||
int id = 0;
|
||||
bool enabled = 0;
|
||||
};
|
||||
[[nodiscard]] rpl::producer<Entry> buttonEnabledChanges() const;
|
||||
|
||||
private:
|
||||
class FlatCheckbox;
|
||||
|
||||
std::vector<base::unique_qptr<FlatCheckbox>> _buttons;
|
||||
|
||||
rpl::event_stream<Entry> _buttonEnabledChanges;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Statistic
|
||||
527
Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp
Normal file
527
Telegram/SourceFiles/statistics/widgets/point_details_widget.cpp
Normal file
@@ -0,0 +1,527 @@
|
||||
/*
|
||||
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 "base/debug_log.h"
|
||||
#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/statistics_graphics.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,
|
||||
bool zoomEnabled)
|
||||
: Ui::AbstractButton(parent)
|
||||
, _zoomEnabled(zoomEnabled)
|
||||
, _chartData(chartData)
|
||||
, _textStyle(st::statisticsDetailsPopupStyle)
|
||||
, _headerStyle(st::statisticsDetailsPopupHeaderStyle) {
|
||||
if (zoomEnabled) {
|
||||
rpl::single(rpl::empty_value()) | rpl::then(
|
||||
style::PaletteChanged()
|
||||
) | rpl::on_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 hasUsdLine = (_chartData.currencyRate != 0)
|
||||
&& (_chartData.currency != Data::StatisticalCurrency::None)
|
||||
&& (_chartData.lines.size() == 1);
|
||||
|
||||
const auto maxValueTextWidth = [&] {
|
||||
if (hasUsdLine) {
|
||||
auto maxValueWidth = 0;
|
||||
const auto multiplier = float64(kOneStarInNano);
|
||||
for (const auto &value : _chartData.lines.front().y) {
|
||||
const auto valueText = Ui::Text::String(
|
||||
_textStyle,
|
||||
Lang::FormatExactCountDecimal(value / multiplier));
|
||||
const auto usdText = Ui::Text::String(
|
||||
_textStyle,
|
||||
Info::ChannelEarn::ToUsd(
|
||||
value / multiplier,
|
||||
_chartData.currencyRate,
|
||||
0));
|
||||
const auto width = std::max(
|
||||
usdText.maxWidth(),
|
||||
valueText.maxWidth());
|
||||
if (width > maxValueWidth) {
|
||||
maxValueWidth = width;
|
||||
}
|
||||
}
|
||||
return maxValueWidth;
|
||||
}
|
||||
const auto maxAbsoluteValue = [&] {
|
||||
auto maxValue = ChartValue(0);
|
||||
for (const auto &l : _chartData.lines) {
|
||||
maxValue = std::max(l.maxValue, maxValue);
|
||||
}
|
||||
return maxValue;
|
||||
}();
|
||||
const auto maxValueText = Ui::Text::String(
|
||||
_textStyle,
|
||||
Lang::FormatCountDecimal(maxAbsoluteValue));
|
||||
return maxValueText.maxWidth();
|
||||
}();
|
||||
|
||||
const auto calculatedWidth = [&]{
|
||||
auto maxNameTextWidth = 0;
|
||||
const auto isCredits
|
||||
= _chartData.currency == Data::StatisticalCurrency::Credits;
|
||||
for (const auto &dataLine : _chartData.lines) {
|
||||
const auto maxNameText = Ui::Text::String(
|
||||
_textStyle,
|
||||
dataLine.name);
|
||||
maxNameTextWidth = std::max(
|
||||
maxNameText.maxWidth(),
|
||||
maxNameTextWidth);
|
||||
if (hasUsdLine) {
|
||||
const auto text = isCredits
|
||||
? tr::lng_channel_earn_chart_overriden_detail_credits
|
||||
: tr::lng_channel_earn_chart_overriden_detail_currency;
|
||||
const auto currency = Ui::Text::String(
|
||||
_textStyle,
|
||||
text(tr::now));
|
||||
const auto usd = Ui::Text::String(
|
||||
_textStyle,
|
||||
tr::lng_channel_earn_chart_overriden_detail_usd(
|
||||
tr::now));
|
||||
maxNameTextWidth = std::max(
|
||||
std::max(currency.maxWidth(), usd.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.isNull()
|
||||
? 0
|
||||
: _valueIcon.width() / style::DevicePixelRatio())
|
||||
+ _maxPercentageWidth;
|
||||
}();
|
||||
sizeValue(
|
||||
) | rpl::on_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;
|
||||
}
|
||||
if (xIndex >= _chartData.x.size()) {
|
||||
LOG((u"xIndex out of bounds: %1, max: %2"_q)
|
||||
.arg(xIndex)
|
||||
.arg(_chartData.x.size() - 1));
|
||||
xIndex = _chartData.x.size() - 1;
|
||||
}
|
||||
{
|
||||
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 isCredits
|
||||
= (_chartData.currency == Data::StatisticalCurrency::Credits);
|
||||
for (auto i = 0; i < _chartData.lines.size(); i++) {
|
||||
const auto &dataLine = _chartData.lines[i];
|
||||
Assert(xIndex < dataLine.y.size());
|
||||
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,
|
||||
(isCredits
|
||||
? tr::lng_channel_earn_chart_overriden_detail_credits
|
||||
: tr::lng_channel_earn_chart_overriden_detail_currency)(
|
||||
tr::now));
|
||||
const auto provided = dataLine.y[xIndex];
|
||||
const auto value = isCredits
|
||||
? CreditsAmount(provided, CreditsType::Stars)
|
||||
: CreditsAmount(
|
||||
provided / kOneStarInNano,
|
||||
provided % kOneStarInNano,
|
||||
CreditsType::Ton);
|
||||
copy.value.setText(
|
||||
_textStyle,
|
||||
Lang::FormatCreditsAmountDecimal(value));
|
||||
_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(value, _chartData.currencyRate, 0));
|
||||
}
|
||||
_lines.push_back(std::move(textLine));
|
||||
}
|
||||
if (_chartData.currencyRate && _valueIcon.isNull()) {
|
||||
_valueIcon = ChartCurrencyIcon(_chartData, _lines.front().valueColor);
|
||||
}
|
||||
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.isNull()) {
|
||||
p.drawImage(
|
||||
valueContext.position.x()
|
||||
- _valueIcon.width() / style::DevicePixelRatio(),
|
||||
lineY + st::lineWidth,
|
||||
_valueIcon);
|
||||
}
|
||||
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
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
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
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "data/data_statistics_chart.h"
|
||||
#include "ui/abstract_button.h"
|
||||
|
||||
namespace Ui {
|
||||
class RippleAnimation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Statistic {
|
||||
|
||||
void PaintDetails(
|
||||
QPainter &p,
|
||||
const Data::StatisticalChart::Line &line,
|
||||
int absoluteValue,
|
||||
const QRect &rect);
|
||||
|
||||
class PointDetailsWidget : public Ui::AbstractButton {
|
||||
public:
|
||||
PointDetailsWidget(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const Data::StatisticalChart &chartData,
|
||||
bool zoomEnabled);
|
||||
|
||||
[[nodiscard]] int xIndex() const;
|
||||
void setXIndex(int xIndex);
|
||||
void setAlpha(float64 alpha);
|
||||
[[nodiscard]] float64 alpha() const;
|
||||
void setLineAlpha(int lineId, float64 alpha);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||
|
||||
private:
|
||||
const bool _zoomEnabled;
|
||||
const Data::StatisticalChart &_chartData;
|
||||
const style::TextStyle &_textStyle;
|
||||
const style::TextStyle &_headerStyle;
|
||||
Ui::Text::String _header;
|
||||
QImage _valueIcon;
|
||||
|
||||
void invalidateCache();
|
||||
|
||||
[[nodiscard]] int lineYAt(int index) const;
|
||||
|
||||
void resizeHeight();
|
||||
|
||||
struct Line final {
|
||||
int id = 0;
|
||||
Ui::Text::String name;
|
||||
Ui::Text::String value;
|
||||
Ui::Text::String percentage;
|
||||
QColor valueColor;
|
||||
float64 alpha = 1.;
|
||||
};
|
||||
|
||||
bool _hasPositiveValues = true;
|
||||
|
||||
int _maxPercentageWidth = 0;
|
||||
|
||||
QRect _innerRect;
|
||||
QRect _textRect;
|
||||
QImage _arrow;
|
||||
|
||||
QImage _cache;
|
||||
|
||||
int _xIndex = -1;
|
||||
float64 _alpha = 1.;
|
||||
|
||||
std::vector<Line> _lines;
|
||||
|
||||
std::unique_ptr<Ui::RippleAnimation> _ripple;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Statistic
|
||||
Reference in New Issue
Block a user