405 lines
12 KiB
C++
405 lines
12 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 "ui/widgets/tooltip.h"
|
|
|
|
#include "mainwindow.h"
|
|
#include "styles/style_widgets.h"
|
|
#include "platform/platform_specific.h"
|
|
|
|
namespace Ui {
|
|
|
|
Tooltip *TooltipInstance = nullptr;
|
|
|
|
bool AbstractTooltipShower::tooltipWindowActive() const {
|
|
if (auto window = App::wnd()) {
|
|
window->updateIsActive(0);
|
|
return window->isActive();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const style::Tooltip *AbstractTooltipShower::tooltipSt() const {
|
|
return &st::defaultTooltip;
|
|
}
|
|
|
|
AbstractTooltipShower::~AbstractTooltipShower() {
|
|
if (TooltipInstance && TooltipInstance->_shower == this) {
|
|
TooltipInstance->_shower = 0;
|
|
}
|
|
}
|
|
|
|
Tooltip::Tooltip() : TWidget(nullptr) {
|
|
TooltipInstance = this;
|
|
|
|
setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint) | Qt::BypassWindowManagerHint | Qt::NoDropShadowWindowHint | Qt::ToolTip);
|
|
setAttribute(Qt::WA_NoSystemBackground, true);
|
|
setAttribute(Qt::WA_TranslucentBackground, true);
|
|
|
|
_showTimer.setCallback([=] { performShow(); });
|
|
_hideByLeaveTimer.setCallback([=] { Hide(); });
|
|
|
|
connect(App::wnd()->windowHandle(), SIGNAL(activeChanged()), this, SLOT(onWndActiveChanged()));
|
|
}
|
|
|
|
void Tooltip::performShow() {
|
|
if (_shower) {
|
|
auto text = _shower->tooltipWindowActive() ? _shower->tooltipText() : QString();
|
|
if (text.isEmpty()) {
|
|
Hide();
|
|
} else {
|
|
TooltipInstance->popup(_shower->tooltipPos(), text, _shower->tooltipSt());
|
|
}
|
|
}
|
|
}
|
|
|
|
void Tooltip::onWndActiveChanged() {
|
|
if (!App::wnd() || !App::wnd()->windowHandle() || !App::wnd()->windowHandle()->isActive()) {
|
|
Hide();
|
|
}
|
|
}
|
|
|
|
bool Tooltip::eventFilter(QObject *o, QEvent *e) {
|
|
if (e->type() == QEvent::Leave) {
|
|
_hideByLeaveTimer.callOnce(10);
|
|
} else if (e->type() == QEvent::Enter) {
|
|
_hideByLeaveTimer.cancel();
|
|
} else if (e->type() == QEvent::MouseMove) {
|
|
if ((QCursor::pos() - _point).manhattanLength() > QApplication::startDragDistance()) {
|
|
Hide();
|
|
}
|
|
}
|
|
return TWidget::eventFilter(o, e);
|
|
}
|
|
|
|
Tooltip::~Tooltip() {
|
|
if (TooltipInstance == this) {
|
|
TooltipInstance = nullptr;
|
|
}
|
|
}
|
|
|
|
void Tooltip::popup(const QPoint &m, const QString &text, const style::Tooltip *st) {
|
|
if (!_isEventFilter) {
|
|
_isEventFilter = true;
|
|
QCoreApplication::instance()->installEventFilter(this);
|
|
}
|
|
|
|
_point = m;
|
|
_st = st;
|
|
_text = Text(_st->textStyle, text, _textPlainOptions, _st->widthMax, true);
|
|
|
|
_useTransparency = Platform::TranslucentWindowsSupported(_point);
|
|
setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency);
|
|
|
|
int32 addw = 2 * st::lineWidth + _st->textPadding.left() + _st->textPadding.right();
|
|
int32 addh = 2 * st::lineWidth + _st->textPadding.top() + _st->textPadding.bottom();
|
|
|
|
// count tooltip size
|
|
QSize s(addw + _text.maxWidth(), addh + _text.minHeight());
|
|
if (s.width() > _st->widthMax) {
|
|
s.setWidth(addw + _text.countWidth(_st->widthMax - addw));
|
|
s.setHeight(addh + _text.countHeight(s.width() - addw));
|
|
}
|
|
int32 maxh = addh + (_st->linesMax * _st->textStyle.font->height);
|
|
if (s.height() > maxh) {
|
|
s.setHeight(maxh);
|
|
}
|
|
|
|
// count tooltip position
|
|
QPoint p(m + _st->shift);
|
|
if (rtl()) {
|
|
p.setX(m.x() - s.width() - _st->shift.x());
|
|
}
|
|
if (s.width() < 2 * _st->shift.x()) {
|
|
p.setX(m.x() - (s.width() / 2));
|
|
}
|
|
|
|
// adjust tooltip position
|
|
QRect r(QApplication::desktop()->screenGeometry(m));
|
|
if (r.x() + r.width() - _st->skip < p.x() + s.width() && p.x() + s.width() > m.x()) {
|
|
p.setX(qMax(r.x() + r.width() - int32(_st->skip) - s.width(), m.x() - s.width()));
|
|
}
|
|
if (r.x() + _st->skip > p.x() && p.x() < m.x()) {
|
|
p.setX(qMin(m.x(), r.x() + int32(_st->skip)));
|
|
}
|
|
if (r.y() + r.height() - _st->skip < p.y() + s.height()) {
|
|
p.setY(m.y() - s.height() - _st->skip);
|
|
}
|
|
if (r.y() > p.x()) {
|
|
p.setY(qMin(m.y() + _st->shift.y(), r.y() + r.height() - s.height()));
|
|
}
|
|
|
|
setGeometry(QRect(p, s));
|
|
|
|
_hideByLeaveTimer.cancel();
|
|
show();
|
|
}
|
|
|
|
void Tooltip::paintEvent(QPaintEvent *e) {
|
|
Painter p(this);
|
|
|
|
if (_useTransparency) {
|
|
Platform::StartTranslucentPaint(p, e);
|
|
|
|
p.setPen(_st->textBorder);
|
|
p.setBrush(_st->textBg);
|
|
PainterHighQualityEnabler hq(p);
|
|
p.drawRoundedRect(QRectF(0.5, 0.5, width() - 1., height() - 1.), st::buttonRadius, st::buttonRadius);
|
|
} else {
|
|
p.fillRect(rect(), _st->textBg);
|
|
|
|
p.fillRect(QRect(0, 0, width(), st::lineWidth), _st->textBorder);
|
|
p.fillRect(QRect(0, height() - st::lineWidth, width(), st::lineWidth), _st->textBorder);
|
|
p.fillRect(QRect(0, st::lineWidth, st::lineWidth, height() - 2 * st::lineWidth), _st->textBorder);
|
|
p.fillRect(QRect(width() - st::lineWidth, st::lineWidth, st::lineWidth, height() - 2 * st::lineWidth), _st->textBorder);
|
|
}
|
|
int32 lines = qFloor((height() - 2 * st::lineWidth - _st->textPadding.top() - _st->textPadding.bottom()) / _st->textStyle.font->height);
|
|
|
|
p.setPen(_st->textFg);
|
|
_text.drawElided(p, st::lineWidth + _st->textPadding.left(), st::lineWidth + _st->textPadding.top(), width() - 2 * st::lineWidth - _st->textPadding.left() - _st->textPadding.right(), lines);
|
|
}
|
|
|
|
void Tooltip::hideEvent(QHideEvent *e) {
|
|
if (TooltipInstance == this) {
|
|
Hide();
|
|
}
|
|
}
|
|
|
|
void Tooltip::Show(int32 delay, const AbstractTooltipShower *shower) {
|
|
if (!TooltipInstance) {
|
|
new Tooltip();
|
|
}
|
|
TooltipInstance->_shower = shower;
|
|
if (delay >= 0) {
|
|
TooltipInstance->_showTimer.callOnce(delay);
|
|
} else {
|
|
TooltipInstance->performShow();
|
|
}
|
|
}
|
|
|
|
void Tooltip::Hide() {
|
|
if (auto instance = TooltipInstance) {
|
|
TooltipInstance = nullptr;
|
|
instance->_showTimer.cancel();
|
|
instance->_hideByLeaveTimer.cancel();
|
|
instance->hide();
|
|
InvokeQueued(instance, [instance] { instance->deleteLater(); });
|
|
}
|
|
}
|
|
|
|
ImportantTooltip::ImportantTooltip(QWidget *parent, object_ptr<TWidget> content, const style::ImportantTooltip &st) : TWidget(parent)
|
|
, _st(st)
|
|
, _content(std::move(content)) {
|
|
_content->setParent(this);
|
|
_hideTimer.setCallback([this] { toggleAnimated(false); });
|
|
hide();
|
|
}
|
|
|
|
void ImportantTooltip::pointAt(QRect area, RectParts side) {
|
|
if (_area == area && _side == side) {
|
|
return;
|
|
}
|
|
setArea(area);
|
|
countApproachSide(side);
|
|
updateGeometry();
|
|
update();
|
|
}
|
|
|
|
void ImportantTooltip::setArea(QRect area) {
|
|
Expects(parentWidget() != nullptr);
|
|
_area = area;
|
|
auto point = parentWidget()->mapToGlobal(_area.center());
|
|
_useTransparency = Platform::TranslucentWindowsSupported(point);
|
|
setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency);
|
|
|
|
auto contentWidth = parentWidget()->rect().marginsRemoved(_st.padding).width();
|
|
accumulate_min(contentWidth, _content->naturalWidth());
|
|
_content->resizeToWidth(contentWidth);
|
|
|
|
auto size = _content->rect().marginsAdded(_st.padding).size();
|
|
if (_useTransparency) {
|
|
size.setHeight(size.height() + _st.arrow);
|
|
}
|
|
if (size.width() < 2 * (_st.arrowSkipMin + _st.arrow)) {
|
|
size.setWidth(2 * (_st.arrowSkipMin + _st.arrow));
|
|
}
|
|
resize(size);
|
|
}
|
|
|
|
void ImportantTooltip::countApproachSide(RectParts preferSide) {
|
|
Expects(parentWidget() != nullptr);
|
|
auto requiredSpace = countInner().height() + _st.shift;
|
|
if (_useTransparency) {
|
|
requiredSpace += _st.arrow;
|
|
}
|
|
auto available = parentWidget()->rect();
|
|
auto availableAbove = _area.y() - available.y();
|
|
auto availableBelow = (available.y() + available.height()) - (_area.y() + _area.height());
|
|
auto allowedAbove = (availableAbove >= requiredSpace + _st.margin.top());
|
|
auto allowedBelow = (availableBelow >= requiredSpace + _st.margin.bottom());
|
|
if ((allowedAbove && allowedBelow) || (!allowedAbove && !allowedBelow)) {
|
|
_side = preferSide;
|
|
} else {
|
|
_side = (allowedAbove ? RectPart::Top : RectPart::Bottom) | (preferSide & (RectPart::Left | RectPart::Center | RectPart::Right));
|
|
}
|
|
if (_useTransparency) {
|
|
auto arrow = QImage(QSize(_st.arrow * 2, _st.arrow) * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied);
|
|
arrow.fill(Qt::transparent);
|
|
arrow.setDevicePixelRatio(cRetinaFactor());
|
|
{
|
|
Painter p(&arrow);
|
|
PainterHighQualityEnabler hq(p);
|
|
|
|
QPainterPath path;
|
|
path.moveTo(0, 0);
|
|
path.lineTo(2 * _st.arrow, 0);
|
|
path.lineTo(_st.arrow, _st.arrow);
|
|
path.lineTo(0, 0);
|
|
p.fillPath(path, _st.bg);
|
|
}
|
|
if (_side & RectPart::Bottom) {
|
|
arrow = std::move(arrow).transformed(QTransform(1, 0, 0, -1, 0, 0));
|
|
}
|
|
_arrow = App::pixmapFromImageInPlace(std::move(arrow));
|
|
}
|
|
}
|
|
|
|
void ImportantTooltip::toggleAnimated(bool visible) {
|
|
if (_visible == isHidden()) {
|
|
setVisible(_visible);
|
|
}
|
|
if (_visible != visible) {
|
|
updateGeometry();
|
|
_visible = visible;
|
|
refreshAnimationCache();
|
|
if (_visible) {
|
|
show();
|
|
} else if (isHidden()) {
|
|
return;
|
|
}
|
|
hideChildren();
|
|
_visibleAnimation.start([this] { animationCallback(); }, _visible ? 0. : 1., _visible ? 1. : 0., _st.duration, anim::easeOutCirc);
|
|
}
|
|
}
|
|
|
|
void ImportantTooltip::hideAfter(crl::time timeout) {
|
|
_hideTimer.callOnce(timeout);
|
|
}
|
|
|
|
void ImportantTooltip::animationCallback() {
|
|
updateGeometry();
|
|
update();
|
|
checkAnimationFinish();
|
|
}
|
|
|
|
void ImportantTooltip::refreshAnimationCache() {
|
|
if (_cache.isNull() && _useTransparency) {
|
|
auto animation = base::take(_visibleAnimation);
|
|
auto visible = std::exchange(_visible, true);
|
|
showChildren();
|
|
_cache = GrabWidget(this);
|
|
_visible = base::take(visible);
|
|
_visibleAnimation = base::take(animation);
|
|
}
|
|
}
|
|
|
|
void ImportantTooltip::toggleFast(bool visible) {
|
|
if (_visible == isHidden()) {
|
|
setVisible(_visible);
|
|
}
|
|
if (_visibleAnimation.animating() || _visible != visible) {
|
|
_visibleAnimation.finish();
|
|
_visible = visible;
|
|
checkAnimationFinish();
|
|
}
|
|
}
|
|
|
|
void ImportantTooltip::checkAnimationFinish() {
|
|
if (!_visibleAnimation.animating()) {
|
|
_cache = QPixmap();
|
|
showChildren();
|
|
setVisible(_visible);
|
|
if (_visible) {
|
|
update();
|
|
} else if (_hiddenCallback) {
|
|
_hiddenCallback();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ImportantTooltip::updateGeometry() {
|
|
Expects(parentWidget() != nullptr);
|
|
auto parent = parentWidget();
|
|
auto areaMiddle = _area.x() + (_area.width() / 2);
|
|
auto left = areaMiddle - (width() / 2);
|
|
if (_side & RectPart::Left) {
|
|
left = areaMiddle + _st.arrowSkip - width();
|
|
} else if (_side & RectPart::Right) {
|
|
left = areaMiddle - _st.arrowSkip;
|
|
}
|
|
accumulate_min(left, parent->width() - _st.margin.right() - width());
|
|
accumulate_max(left, _st.margin.left());
|
|
accumulate_max(left, areaMiddle + _st.arrow + _st.arrowSkipMin - width());
|
|
accumulate_min(left, areaMiddle - _st.arrow - _st.arrowSkipMin);
|
|
|
|
auto countTop = [this] {
|
|
auto shift = anim::interpolate(_st.shift, 0, _visibleAnimation.current(_visible ? 1. : 0.));
|
|
if (_side & RectPart::Top) {
|
|
return _area.y() - height() - shift;
|
|
}
|
|
return _area.y() + _area.height() + shift;
|
|
};
|
|
move(left, countTop());
|
|
}
|
|
|
|
void ImportantTooltip::resizeEvent(QResizeEvent *e) {
|
|
auto inner = countInner();
|
|
auto contentTop = _st.padding.top();
|
|
if (_useTransparency && (_side & RectPart::Bottom)) {
|
|
contentTop += _st.arrow;
|
|
}
|
|
_content->moveToLeft(_st.padding.left(), contentTop);
|
|
}
|
|
|
|
QRect ImportantTooltip::countInner() const {
|
|
return _content->geometry().marginsAdded(_st.padding);
|
|
}
|
|
|
|
void ImportantTooltip::paintEvent(QPaintEvent *e) {
|
|
Painter p(this);
|
|
|
|
auto inner = countInner();
|
|
if (_useTransparency) {
|
|
if (!_cache.isNull()) {
|
|
auto opacity = _visibleAnimation.current(_visible ? 1. : 0.);
|
|
p.setOpacity(opacity);
|
|
p.drawPixmap(0, 0, _cache);
|
|
} else {
|
|
if (!_visible) {
|
|
return;
|
|
}
|
|
p.setBrush(_st.bg);
|
|
p.setPen(Qt::NoPen);
|
|
{
|
|
PainterHighQualityEnabler hq(p);
|
|
p.drawRoundedRect(inner, _st.radius, _st.radius);
|
|
}
|
|
auto areaMiddle = _area.x() + (_area.width() / 2) - x();
|
|
auto arrowLeft = areaMiddle - _st.arrow;
|
|
if (_side & RectPart::Top) {
|
|
p.drawPixmapLeft(arrowLeft, inner.y() + inner.height(), width(), _arrow);
|
|
} else {
|
|
p.drawPixmapLeft(arrowLeft, inner.y() - _st.arrow, width(), _arrow);
|
|
}
|
|
}
|
|
} else {
|
|
p.fillRect(inner, QColor(_st.bg->c.red(), _st.bg->c.green(), _st.bg->c.blue()));
|
|
}
|
|
}
|
|
|
|
} // namespace Ui
|