tooltip.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. // This file is part of Desktop App Toolkit,
  2. // a set of libraries for developing nice desktop applications.
  3. //
  4. // For license and copyright information please follow this link:
  5. // https://github.com/desktop-app/legal/blob/master/LEGAL
  6. //
  7. #include "ui/widgets/tooltip.h"
  8. #include "ui/ui_utility.h"
  9. #include "ui/painter.h"
  10. #include "ui/platform/ui_platform_utility.h"
  11. #include "ui/widgets/labels.h"
  12. #include "base/invoke_queued.h"
  13. #include "styles/style_widgets.h"
  14. #include <QtGui/QScreen>
  15. #include <QtGui/QWindow>
  16. #include <QtWidgets/QApplication>
  17. namespace Ui {
  18. Tooltip *TooltipInstance = nullptr;
  19. const style::Tooltip *AbstractTooltipShower::tooltipSt() const {
  20. return &st::defaultTooltip;
  21. }
  22. AbstractTooltipShower::~AbstractTooltipShower() {
  23. if (TooltipInstance && TooltipInstance->_shower == this) {
  24. TooltipInstance->_shower = 0;
  25. }
  26. }
  27. Tooltip::Tooltip() : RpWidget(nullptr) {
  28. TooltipInstance = this;
  29. setWindowFlags(Qt::WindowFlags(Qt::FramelessWindowHint) | Qt::BypassWindowManagerHint | Qt::NoDropShadowWindowHint | Qt::ToolTip);
  30. setAttribute(Qt::WA_NoSystemBackground, true);
  31. setAttribute(Qt::WA_TranslucentBackground, true);
  32. _showTimer.setCallback([=] { performShow(); });
  33. _hideByLeaveTimer.setCallback([=] { Hide(); });
  34. }
  35. void Tooltip::performShow() {
  36. if (_shower) {
  37. auto text = _shower->tooltipWindowActive()
  38. ? _shower->tooltipText()
  39. : QString();
  40. if (text.isEmpty()) {
  41. Hide();
  42. } else {
  43. TooltipInstance->popup(_shower->tooltipPos(), text, _shower->tooltipSt());
  44. }
  45. }
  46. }
  47. bool Tooltip::eventFilter(QObject *o, QEvent *e) {
  48. if (e->type() == QEvent::Leave) {
  49. _hideByLeaveTimer.callOnce(10);
  50. } else if (e->type() == QEvent::Enter) {
  51. _hideByLeaveTimer.cancel();
  52. } else if (e->type() == QEvent::MouseMove) {
  53. if ((QCursor::pos() - _point).manhattanLength() > QApplication::startDragDistance()) {
  54. Hide();
  55. }
  56. }
  57. return RpWidget::eventFilter(o, e);
  58. }
  59. Tooltip::~Tooltip() {
  60. if (TooltipInstance == this) {
  61. TooltipInstance = nullptr;
  62. }
  63. }
  64. void Tooltip::popup(const QPoint &m, const QString &text, const style::Tooltip *st) {
  65. if (!_isEventFilter) {
  66. _isEventFilter = true;
  67. QCoreApplication::instance()->installEventFilter(this);
  68. }
  69. _point = m;
  70. _st = st;
  71. _text = Text::String(_st->textStyle, text, kPlainTextOptions, _st->widthMax);
  72. _useTransparency = Platform::TranslucentWindowsSupported();
  73. setAttribute(Qt::WA_OpaquePaintEvent, !_useTransparency);
  74. int32 addw = 2 * st::lineWidth + _st->textPadding.left() + _st->textPadding.right();
  75. int32 addh = 2 * st::lineWidth + _st->textPadding.top() + _st->textPadding.bottom();
  76. // count tooltip size
  77. QSize s(addw + _text.maxWidth(), addh + _text.minHeight());
  78. if (s.width() > _st->widthMax) {
  79. s.setWidth(addw + _text.countWidth(_st->widthMax - addw));
  80. s.setHeight(addh + _text.countHeight(s.width() - addw));
  81. }
  82. int32 maxh = addh + (_st->linesMax * _st->textStyle.font->height);
  83. if (s.height() > maxh) {
  84. s.setHeight(maxh);
  85. }
  86. // count tooltip position
  87. QPoint p(m + _st->shift);
  88. if (style::RightToLeft()) {
  89. p.setX(m.x() - s.width() - _st->shift.x());
  90. }
  91. if (s.width() < 2 * _st->shift.x()) {
  92. p.setX(m.x() - (s.width() / 2));
  93. }
  94. const auto screen = QGuiApplication::screenAt(m);
  95. if (screen) {
  96. #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) && !defined(Q_OS_MAC)
  97. setScreen(screen);
  98. #else // Qt >= 6.0.0
  99. createWinId();
  100. windowHandle()->setScreen(screen);
  101. #endif // Qt < 6.0.0
  102. // adjust tooltip position
  103. const auto r = screen->availableGeometry();
  104. if (r.x() + r.width() - _st->skip < p.x() + s.width() && p.x() + s.width() > m.x()) {
  105. p.setX(qMax(r.x() + r.width() - int32(_st->skip) - s.width(), m.x() - s.width()));
  106. }
  107. if (r.x() + _st->skip > p.x() && p.x() < m.x()) {
  108. p.setX(qMin(m.x(), r.x() + int32(_st->skip)));
  109. }
  110. if (r.y() + r.height() - _st->skip < p.y() + s.height()) {
  111. p.setY(m.y() - s.height() - _st->skip);
  112. }
  113. if (r.y() > p.x()) {
  114. p.setY(qMin(m.y() + _st->shift.y(), r.y() + r.height() - s.height()));
  115. }
  116. }
  117. move(p);
  118. setFixedSize(s);
  119. _hideByLeaveTimer.cancel();
  120. show();
  121. }
  122. void Tooltip::paintEvent(QPaintEvent *e) {
  123. Painter p(this);
  124. if (_useTransparency) {
  125. p.setPen(_st->textBorder);
  126. p.setBrush(_st->textBg);
  127. PainterHighQualityEnabler hq(p);
  128. p.drawRoundedRect(QRectF(0.5, 0.5, width() - 1., height() - 1.), st::roundRadiusSmall, st::roundRadiusSmall);
  129. } else {
  130. p.fillRect(rect(), _st->textBg);
  131. p.fillRect(QRect(0, 0, width(), st::lineWidth), _st->textBorder);
  132. p.fillRect(QRect(0, height() - st::lineWidth, width(), st::lineWidth), _st->textBorder);
  133. p.fillRect(QRect(0, st::lineWidth, st::lineWidth, height() - 2 * st::lineWidth), _st->textBorder);
  134. p.fillRect(QRect(width() - st::lineWidth, st::lineWidth, st::lineWidth, height() - 2 * st::lineWidth), _st->textBorder);
  135. }
  136. const auto lines = (height() - 2 * st::lineWidth - _st->textPadding.top() - _st->textPadding.bottom())
  137. / _st->textStyle.font->height;
  138. p.setPen(_st->textFg);
  139. _text.drawElided(p, st::lineWidth + _st->textPadding.left(), st::lineWidth + _st->textPadding.top(), width() - 2 * st::lineWidth - _st->textPadding.left() - _st->textPadding.right(), lines);
  140. }
  141. void Tooltip::hideEvent(QHideEvent *e) {
  142. if (TooltipInstance == this) {
  143. Hide();
  144. }
  145. }
  146. void Tooltip::Show(int32 delay, const AbstractTooltipShower *shower) {
  147. if (!TooltipInstance) {
  148. new Tooltip();
  149. }
  150. TooltipInstance->_shower = shower;
  151. if (delay >= 0) {
  152. TooltipInstance->_showTimer.callOnce(delay);
  153. } else {
  154. TooltipInstance->performShow();
  155. }
  156. }
  157. void Tooltip::Hide() {
  158. if (auto instance = TooltipInstance) {
  159. TooltipInstance = nullptr;
  160. instance->_showTimer.cancel();
  161. instance->_hideByLeaveTimer.cancel();
  162. instance->hide();
  163. InvokeQueued(instance, [=] { instance->deleteLater(); });
  164. }
  165. }
  166. ImportantTooltip::ImportantTooltip(
  167. QWidget *parent,
  168. object_ptr<RpWidget> content,
  169. const style::ImportantTooltip &st)
  170. : RpWidget(parent)
  171. , _st(st)
  172. , _content(std::move(content)) {
  173. _content->setParent(this);
  174. _hideTimer.setCallback([this] { toggleAnimated(false); });
  175. hide();
  176. _content->widthValue(
  177. ) | rpl::start_with_next([=] {
  178. resizeToContent();
  179. }, lifetime());
  180. }
  181. void ImportantTooltip::pointAt(
  182. QRect area,
  183. RectParts side,
  184. Fn<QPoint(QSize)> countPosition) {
  185. if (_area == area
  186. && _side == side
  187. && !_countPosition
  188. && !countPosition) {
  189. return;
  190. }
  191. _countPosition = std::move(countPosition);
  192. _area = area;
  193. countApproachSide(side);
  194. resizeToContent();
  195. update();
  196. }
  197. void ImportantTooltip::resizeToContent() {
  198. auto size = _content->rect().marginsAdded(_st.padding).size();
  199. size.setHeight(size.height() + _st.arrow);
  200. if (size.width() < 2 * (_st.arrowSkipMin + _st.arrow)) {
  201. size.setWidth(2 * (_st.arrowSkipMin + _st.arrow));
  202. }
  203. if (_side & RectPart::Right) {
  204. size.setWidth(size.width() + _st.arrow);
  205. }
  206. setFixedSize(size);
  207. updateGeometry();
  208. }
  209. void ImportantTooltip::countApproachSide(RectParts preferSide) {
  210. Expects(parentWidget() != nullptr);
  211. auto requiredSpace = countInner().height() + _st.shift + _st.arrow;
  212. auto available = parentWidget()->rect();
  213. auto availableAbove = _area.y() - available.y();
  214. auto availableBelow = (available.y() + available.height()) - (_area.y() + _area.height());
  215. auto allowedAbove = (availableAbove >= requiredSpace + _st.margin.top());
  216. auto allowedBelow = (availableBelow >= requiredSpace + _st.margin.bottom());
  217. if ((allowedAbove && allowedBelow) || (!allowedAbove && !allowedBelow)) {
  218. _side = preferSide;
  219. } else if (preferSide & RectPart::Right) {
  220. _side = preferSide;
  221. } else {
  222. _side = (allowedAbove ? RectPart::Top : RectPart::Bottom)
  223. | (preferSide & (RectPart::Left | RectPart::Center));
  224. }
  225. auto arrow = QImage(
  226. QSize(_st.arrow * 2, _st.arrow) * style::DevicePixelRatio(),
  227. QImage::Format_ARGB32_Premultiplied);
  228. arrow.fill(Qt::transparent);
  229. arrow.setDevicePixelRatio(style::DevicePixelRatio());
  230. {
  231. Painter p(&arrow);
  232. PainterHighQualityEnabler hq(p);
  233. QPainterPath path;
  234. path.moveTo(0, 0);
  235. path.lineTo(2 * _st.arrow, 0);
  236. path.lineTo(_st.arrow, _st.arrow);
  237. path.lineTo(0, 0);
  238. p.fillPath(path, _st.bg);
  239. }
  240. if (_side & RectPart::Bottom) {
  241. arrow = std::move(arrow).transformed(QTransform(1, 0, 0, -1, 0, 0));
  242. } else if (_side & RectPart::Right) {
  243. arrow = std::move(arrow).transformed(QTransform().rotate(-90));
  244. }
  245. _arrow = PixmapFromImage(std::move(arrow));
  246. }
  247. void ImportantTooltip::toggleAnimated(bool visible) {
  248. if (_visible == isHidden()) {
  249. setVisible(_visible);
  250. }
  251. if (_visible != visible) {
  252. updateGeometry();
  253. _visible = visible;
  254. refreshAnimationCache();
  255. if (_visible) {
  256. show();
  257. } else if (isHidden()) {
  258. return;
  259. }
  260. hideChildren();
  261. _visibleAnimation.start([this] { animationCallback(); }, _visible ? 0. : 1., _visible ? 1. : 0., _st.duration, anim::easeOutCirc);
  262. }
  263. }
  264. void ImportantTooltip::hideAfter(crl::time timeout) {
  265. _hideTimer.callOnce(timeout);
  266. }
  267. void ImportantTooltip::animationCallback() {
  268. updateGeometry();
  269. update();
  270. checkAnimationFinish();
  271. }
  272. void ImportantTooltip::refreshAnimationCache() {
  273. if (!_cache.isNull()) {
  274. return;
  275. }
  276. auto animation = base::take(_visibleAnimation);
  277. auto visible = std::exchange(_visible, true);
  278. showChildren();
  279. _cache = GrabWidget(this);
  280. _visible = base::take(visible);
  281. _visibleAnimation = base::take(animation);
  282. }
  283. void ImportantTooltip::toggleFast(bool visible) {
  284. if (_visible == isHidden()) {
  285. setVisible(_visible);
  286. }
  287. if (_visibleAnimation.animating() || _visible != visible) {
  288. _visibleAnimation.stop();
  289. _visible = visible;
  290. checkAnimationFinish();
  291. }
  292. }
  293. void ImportantTooltip::checkAnimationFinish() {
  294. if (!_visibleAnimation.animating()) {
  295. _cache = QPixmap();
  296. showChildren();
  297. setVisible(_visible);
  298. if (_visible) {
  299. update();
  300. } else if (_hiddenCallback) {
  301. _hiddenCallback();
  302. }
  303. }
  304. }
  305. QPoint ImportantTooltip::countPosition() const {
  306. Expects(parentWidget() != nullptr);
  307. auto parent = parentWidget();
  308. auto areaMiddle = _area.x() + (_area.width() / 2);
  309. auto left = areaMiddle - (width() / 2);
  310. if (_side & RectPart::Left) {
  311. left = areaMiddle + _st.arrowSkip - width();
  312. } else if (_side & RectPart::Right) {
  313. left = areaMiddle - _st.arrowSkip;
  314. }
  315. accumulate_min(left, parent->width() - _st.margin.right() - width());
  316. accumulate_max(left, _st.margin.left());
  317. accumulate_max(left, areaMiddle + _st.arrow + _st.arrowSkipMin - width());
  318. accumulate_min(left, areaMiddle - _st.arrow - _st.arrowSkipMin);
  319. const auto top = (_side & RectPart::Top)
  320. ? (_area.y() - height())
  321. : (_area.y() + _area.height());
  322. return { left, top };
  323. }
  324. void ImportantTooltip::updateGeometry() {
  325. const auto position = _countPosition
  326. ? _countPosition(size())
  327. : countPosition();
  328. const auto isTop = (_side & RectPart::Top);
  329. const auto isBottom = (_side & RectPart::Bottom);
  330. const auto shift = anim::interpolate(
  331. (isTop || (_side & RectPart::Left)) ? -_st.shift : _st.shift,
  332. 0,
  333. _visibleAnimation.value(_visible ? 1. : 0.));
  334. move(
  335. position.x() + (isTop || isBottom ? 0 : shift),
  336. position.y() + (isTop || isBottom ? shift : 0));
  337. }
  338. void ImportantTooltip::resizeEvent(QResizeEvent *e) {
  339. auto contentTop = _st.padding.top();
  340. if (_side & RectPart::Bottom) {
  341. contentTop += _st.arrow;
  342. }
  343. _content->moveToLeft(_st.padding.left(), contentTop);
  344. }
  345. QRect ImportantTooltip::countInner() const {
  346. return _content->geometry().marginsAdded(_st.padding);
  347. }
  348. void ImportantTooltip::paintEvent(QPaintEvent *e) {
  349. Painter p(this);
  350. auto inner = countInner();
  351. if (!_cache.isNull()) {
  352. auto opacity = _visibleAnimation.value(_visible ? 1. : 0.);
  353. p.setOpacity(opacity);
  354. p.drawPixmap(0, 0, _cache);
  355. } else {
  356. if (!_visible) {
  357. return;
  358. }
  359. p.setBrush(_st.bg);
  360. p.setPen(Qt::NoPen);
  361. {
  362. PainterHighQualityEnabler hq(p);
  363. p.drawRoundedRect(inner, _st.radius, _st.radius);
  364. }
  365. auto areaMiddle = _area.x() + (_area.width() / 2) - x();
  366. auto arrowLeft = areaMiddle - _st.arrow;
  367. if (_side & RectPart::Top) {
  368. p.drawPixmapLeft(arrowLeft, inner.y() + inner.height(), width(), _arrow);
  369. } else if (_side & RectPart::Bottom) {
  370. p.drawPixmapLeft(arrowLeft, inner.y() - _st.arrow, width(), _arrow);
  371. } else if (_side & RectPart::Right) {
  372. p.drawPixmapLeft(
  373. inner.x() + inner.width(),
  374. inner.y() + (inner.height() - _st.arrow) / 2,
  375. width(),
  376. _arrow);
  377. }
  378. }
  379. }
  380. [[nodiscard]] int FindNiceTooltipWidth(
  381. int minWidth,
  382. int maxWidth,
  383. Fn<int(int width)> heightForWidth) {
  384. Expects(minWidth >= 0);
  385. Expects(maxWidth >= minWidth);
  386. const auto desired = heightForWidth(maxWidth);
  387. while (maxWidth - minWidth > 1) {
  388. const auto middle = (minWidth + maxWidth) / 2;
  389. if (heightForWidth(middle) > desired) {
  390. minWidth = middle;
  391. } else {
  392. maxWidth = middle;
  393. }
  394. }
  395. return maxWidth;
  396. }
  397. object_ptr<FlatLabel> MakeNiceTooltipLabel(
  398. QWidget *parent,
  399. rpl::producer<TextWithEntities> &&text,
  400. int maxWidth,
  401. const style::FlatLabel &st,
  402. const style::PopupMenu &stMenu) {
  403. Expects(st.minWidth > 0);
  404. Expects(st.minWidth < maxWidth);
  405. auto result = object_ptr<FlatLabel>(
  406. parent,
  407. rpl::duplicate(text),
  408. st,
  409. stMenu);
  410. const auto raw = result.data();
  411. std::move(text) | rpl::start_with_next([=, &st] {
  412. raw->resizeToWidth(qMin(maxWidth, raw->textMaxWidth()));
  413. const auto desired = raw->textMaxWidth();
  414. if (desired <= maxWidth) {
  415. raw->resizeToWidth(desired);
  416. return;
  417. }
  418. raw->resizeToWidth(maxWidth);
  419. const auto niceWidth = FindNiceTooltipWidth(
  420. st.minWidth,
  421. maxWidth,
  422. [&](int width) {
  423. raw->resizeToWidth(width);
  424. return raw->heightNoMargins();
  425. });
  426. raw->resizeToWidth(niceWidth);
  427. }, raw->lifetime());
  428. return result;
  429. }
  430. } // namespace Ui