labels.cpp 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  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/labels.h"
  8. #include "base/invoke_queued.h"
  9. #include "ui/text/text_entity.h"
  10. #include "ui/effects/animation_value.h"
  11. #include "ui/widgets/popup_menu.h"
  12. #include "ui/widgets/box_content_divider.h"
  13. #include "ui/basic_click_handlers.h" // UrlClickHandler
  14. #include "ui/inactive_press.h"
  15. #include "ui/painter.h"
  16. #include "ui/qt_weak_factory.h"
  17. #include "ui/integration.h"
  18. #include "ui/ui_utility.h"
  19. #include "base/qt/qt_common_adapters.h"
  20. #include "styles/style_layers.h"
  21. #include "styles/palette.h"
  22. #include <QtWidgets/QApplication>
  23. #include <QtGui/QClipboard>
  24. #include <QtGui/QDrag>
  25. #include <QtGui/QtEvents>
  26. #include <QtCore/QMimeData>
  27. namespace Ui {
  28. namespace {
  29. TextParseOptions _labelOptions = {
  30. TextParseMultiline, // flags
  31. 0, // maxw
  32. 0, // maxh
  33. Qt::LayoutDirectionAuto, // dir
  34. };
  35. TextParseOptions _labelMarkedOptions = {
  36. TextParseMultiline | TextParseLinks | TextParseHashtags | TextParseMentions | TextParseBotCommands | TextParseMarkdown, // flags
  37. 0, // maxw
  38. 0, // maxh
  39. Qt::LayoutDirectionAuto, // dir
  40. };
  41. } // namespace
  42. CrossFadeAnimation::CrossFadeAnimation(
  43. style::color bg,
  44. Data &&was,
  45. Data &&now)
  46. : _bg(bg) {
  47. const auto maxLines = qMax(was.lineWidths.size(), now.lineWidths.size());
  48. auto fillDataTill = [&](Data &data) {
  49. for (auto i = data.lineWidths.size(); i != maxLines; ++i) {
  50. data.lineWidths.push_back(-1);
  51. }
  52. };
  53. fillDataTill(was);
  54. fillDataTill(now);
  55. auto preparePart = [](const Data &data, int index, const Data &other) {
  56. auto result = CrossFadeAnimation::Part();
  57. auto lineWidth = data.lineWidths[index];
  58. if (lineWidth < 0) {
  59. lineWidth = other.lineWidths[index];
  60. }
  61. const auto pixelRatio = style::DevicePixelRatio();
  62. auto fullWidth = data.full.width() / pixelRatio;
  63. auto top = index * data.lineHeight + data.lineAddTop;
  64. auto left = 0;
  65. if (data.align & Qt::AlignHCenter) {
  66. left += (fullWidth - lineWidth) / 2;
  67. } else if (data.align & Qt::AlignRight) {
  68. left += (fullWidth - lineWidth);
  69. }
  70. auto snapshotRect = data.full.rect().intersected(QRect(left * pixelRatio, top * pixelRatio, lineWidth * pixelRatio, data.font->height * pixelRatio));
  71. if (!snapshotRect.isEmpty()) {
  72. result.snapshot = PixmapFromImage(data.full.copy(snapshotRect));
  73. result.snapshot.setDevicePixelRatio(pixelRatio);
  74. }
  75. result.position = data.position + QPoint(data.margin.left() + left, data.margin.top() + top);
  76. return result;
  77. };
  78. for (int i = 0; i != maxLines; ++i) {
  79. addLine(preparePart(was, i, now), preparePart(now, i, was));
  80. }
  81. }
  82. void CrossFadeAnimation::addLine(Part was, Part now) {
  83. _lines.push_back(Line(std::move(was), std::move(now)));
  84. }
  85. void CrossFadeAnimation::paintFrame(QPainter &p, float64 dt) {
  86. auto progress = anim::linear(1., dt);
  87. paintFrame(p, progress, 1. - progress, progress);
  88. }
  89. void CrossFadeAnimation::paintFrame(
  90. QPainter &p,
  91. float64 positionReady,
  92. float64 alphaWas,
  93. float64 alphaNow) {
  94. if (_lines.isEmpty()) return;
  95. for (const auto &line : std::as_const(_lines)) {
  96. paintLine(p, line, positionReady, alphaWas, alphaNow);
  97. }
  98. }
  99. void CrossFadeAnimation::paintLine(
  100. QPainter &p,
  101. const Line &line,
  102. float64 positionReady,
  103. float64 alphaWas,
  104. float64 alphaNow) {
  105. auto &snapshotWas = line.was.snapshot;
  106. auto &snapshotNow = line.now.snapshot;
  107. if (snapshotWas.isNull() && snapshotNow.isNull()) {
  108. // This can happen if both labels have an empty line or if one
  109. // label has an empty line where the second one already ended.
  110. // In this case lineWidth is zero and snapshot is null.
  111. return;
  112. }
  113. const auto pixelRatio = style::DevicePixelRatio();
  114. auto positionWas = line.was.position;
  115. auto positionNow = line.now.position;
  116. auto left = anim::interpolate(positionWas.x(), positionNow.x(), positionReady);
  117. auto topDelta = (snapshotNow.height() / pixelRatio) - (snapshotWas.height() / pixelRatio);
  118. auto widthDelta = (snapshotNow.width() / pixelRatio) - (snapshotWas.width() / pixelRatio);
  119. auto topWas = anim::interpolate(positionWas.y(), positionNow.y() + topDelta, positionReady);
  120. auto topNow = topWas - topDelta;
  121. p.setOpacity(alphaWas);
  122. if (!snapshotWas.isNull()) {
  123. p.drawPixmap(left, topWas, snapshotWas);
  124. if (topDelta > 0) {
  125. p.fillRect(left, topWas - topDelta, snapshotWas.width() / pixelRatio, topDelta, _bg);
  126. }
  127. if (widthDelta > 0) {
  128. p.fillRect(left + (snapshotWas.width() / pixelRatio), topNow, widthDelta, snapshotNow.height() / pixelRatio, _bg);
  129. }
  130. }
  131. p.setOpacity(alphaNow);
  132. if (!snapshotNow.isNull()) {
  133. p.drawPixmap(left, topNow, snapshotNow);
  134. if (topDelta < 0) {
  135. p.fillRect(left, topNow + topDelta, snapshotNow.width() / pixelRatio, -topDelta, _bg);
  136. }
  137. if (widthDelta < 0) {
  138. p.fillRect(left + (snapshotNow.width() / pixelRatio), topWas, -widthDelta, snapshotWas.height() / pixelRatio, _bg);
  139. }
  140. }
  141. }
  142. LabelSimple::LabelSimple(
  143. QWidget *parent,
  144. const style::LabelSimple &st,
  145. const QString &value)
  146. : RpWidget(parent)
  147. , _st(st) {
  148. setText(value);
  149. }
  150. void LabelSimple::setText(const QString &value, bool *outTextChanged) {
  151. if (_fullText == value) {
  152. if (outTextChanged) *outTextChanged = false;
  153. return;
  154. }
  155. _fullText = value;
  156. _fullTextWidth = _st.font->width(_fullText);
  157. if (!_st.maxWidth || _fullTextWidth <= _st.maxWidth) {
  158. _text = _fullText;
  159. _textWidth = _fullTextWidth;
  160. } else {
  161. auto newText = _st.font->elided(_fullText, _st.maxWidth);
  162. if (newText == _text) {
  163. if (outTextChanged) *outTextChanged = false;
  164. return;
  165. }
  166. _text = newText;
  167. _textWidth = _st.font->width(_text);
  168. }
  169. resize(_textWidth, _st.font->height);
  170. update();
  171. if (outTextChanged) *outTextChanged = true;
  172. }
  173. void LabelSimple::paintEvent(QPaintEvent *e) {
  174. Painter p(this);
  175. p.setFont(_st.font);
  176. p.setPen(_st.textFg);
  177. p.drawTextLeft(0, 0, width(), _text, _textWidth);
  178. }
  179. FlatLabel::FlatLabel(
  180. QWidget *parent,
  181. const style::FlatLabel &st,
  182. const style::PopupMenu &stMenu)
  183. : RpWidget(parent)
  184. , _text(st.minWidth ? st.minWidth : kQFixedMax)
  185. , _st(st)
  186. , _stMenu(stMenu) {
  187. init();
  188. }
  189. FlatLabel::FlatLabel(
  190. QWidget *parent,
  191. const QString &text,
  192. const style::FlatLabel &st,
  193. const style::PopupMenu &stMenu)
  194. : RpWidget(parent)
  195. , _text(st.minWidth ? st.minWidth : kQFixedMax)
  196. , _st(st)
  197. , _stMenu(stMenu) {
  198. setText(text);
  199. init();
  200. }
  201. FlatLabel::FlatLabel(
  202. QWidget *parent,
  203. rpl::producer<QString> &&text,
  204. const style::FlatLabel &st,
  205. const style::PopupMenu &stMenu)
  206. : RpWidget(parent)
  207. , _text(st.minWidth ? st.minWidth : kQFixedMax)
  208. , _st(st)
  209. , _stMenu(stMenu) {
  210. textUpdated();
  211. std::move(
  212. text
  213. ) | rpl::start_with_next([this](const QString &value) {
  214. setText(value);
  215. }, lifetime());
  216. init();
  217. }
  218. FlatLabel::FlatLabel(
  219. QWidget *parent,
  220. rpl::producer<TextWithEntities> &&text,
  221. const style::FlatLabel &st,
  222. const style::PopupMenu &stMenu,
  223. const Text::MarkedContext &context)
  224. : RpWidget(parent)
  225. , _text(st.minWidth ? st.minWidth : kQFixedMax)
  226. , _st(st)
  227. , _stMenu(stMenu)
  228. , _touchSelectTimer([=] { touchSelect(); }) {
  229. textUpdated();
  230. std::move(
  231. text
  232. ) | rpl::start_with_next([=](const TextWithEntities &value) {
  233. setMarkedText(value, context);
  234. }, lifetime());
  235. init();
  236. }
  237. void FlatLabel::init() {
  238. _contextCopyText = Integration::Instance().phraseContextCopyText();
  239. }
  240. void FlatLabel::textUpdated() {
  241. refreshSize();
  242. setMouseTracking(_selectable || _text.hasLinks());
  243. if (_text.hasSpoilers()) {
  244. _text.setSpoilerLinkFilter([weak = Ui::MakeWeak(this)](
  245. const ClickContext &context) {
  246. return (context.button == Qt::LeftButton) && weak;
  247. });
  248. }
  249. update();
  250. }
  251. void FlatLabel::setText(const QString &text) {
  252. _text.setText(_st.style, text, _labelOptions);
  253. textUpdated();
  254. }
  255. void FlatLabel::setMarkedText(
  256. const TextWithEntities &textWithEntities,
  257. Text::MarkedContext context) {
  258. context.repaint = [=] { update(); };
  259. _text.setMarkedText(
  260. _st.style,
  261. textWithEntities,
  262. _labelMarkedOptions,
  263. context);
  264. textUpdated();
  265. }
  266. void FlatLabel::setSelectable(bool selectable) {
  267. if (_selectable != selectable) {
  268. _selection = { 0, 0 };
  269. _savedSelection = { 0, 0 };
  270. _selectable = selectable;
  271. setMouseTracking(_selectable || _text.hasLinks());
  272. }
  273. }
  274. void FlatLabel::setDoubleClickSelectsParagraph(bool doubleClickSelectsParagraph) {
  275. _doubleClickSelectsParagraph = doubleClickSelectsParagraph;
  276. }
  277. void FlatLabel::setContextCopyText(const QString &copyText) {
  278. _contextCopyText = copyText;
  279. }
  280. void FlatLabel::setBreakEverywhere(bool breakEverywhere) {
  281. _breakEverywhere = breakEverywhere;
  282. }
  283. void FlatLabel::setTryMakeSimilarLines(bool tryMakeSimilarLines) {
  284. _tryMakeSimilarLines = tryMakeSimilarLines;
  285. }
  286. int FlatLabel::resizeGetHeight(int newWidth) {
  287. _allowedWidth = newWidth;
  288. _textWidth = countTextWidth();
  289. return countTextHeight(_textWidth);
  290. }
  291. int FlatLabel::textMaxWidth() const {
  292. return _text.maxWidth();
  293. }
  294. int FlatLabel::naturalWidth() const {
  295. return (_st.align == style::al_top) ? -1 : textMaxWidth();
  296. }
  297. QMargins FlatLabel::getMargins() const {
  298. return _st.margin;
  299. }
  300. int FlatLabel::countTextWidth() const {
  301. const auto available = _allowedWidth
  302. ? _allowedWidth
  303. : (_st.minWidth ? _st.minWidth : _text.maxWidth());
  304. if (_allowedWidth > 0
  305. && _allowedWidth < _text.maxWidth()
  306. && _tryMakeSimilarLines) {
  307. auto large = _allowedWidth;
  308. auto small = _allowedWidth / 2;
  309. const auto largeHeight = _text.countHeight(large, _breakEverywhere);
  310. while (large - small > 1) {
  311. const auto middle = (large + small) / 2;
  312. if (largeHeight == _text.countHeight(middle, _breakEverywhere)) {
  313. large = middle;
  314. } else {
  315. small = middle;
  316. }
  317. }
  318. return large;
  319. }
  320. return available;
  321. }
  322. int FlatLabel::countTextHeight(int textWidth) {
  323. _fullTextHeight = _text.countHeight(textWidth, _breakEverywhere);
  324. return _st.maxHeight
  325. ? qMin(_fullTextHeight, _st.maxHeight)
  326. : _fullTextHeight;
  327. }
  328. void FlatLabel::refreshSize() {
  329. int textWidth = countTextWidth();
  330. int textHeight = countTextHeight(textWidth);
  331. int fullWidth = _st.margin.left() + textWidth + _st.margin.right();
  332. int fullHeight = _st.margin.top() + textHeight + _st.margin.bottom();
  333. resize(fullWidth, fullHeight);
  334. }
  335. void FlatLabel::setLink(uint16 index, const ClickHandlerPtr &lnk) {
  336. _text.setLink(index, lnk);
  337. }
  338. void FlatLabel::setLinksTrusted() {
  339. static const auto TrustedLinksFilter = [](
  340. const ClickHandlerPtr &link,
  341. Qt::MouseButton button) {
  342. if (const auto url = dynamic_cast<UrlClickHandler*>(link.get())) {
  343. url->UrlClickHandler::onClick({ button });
  344. return false;
  345. }
  346. return true;
  347. };
  348. setClickHandlerFilter(TrustedLinksFilter);
  349. }
  350. void FlatLabel::setClickHandlerFilter(ClickHandlerFilter &&filter) {
  351. _clickHandlerFilter = std::move(filter);
  352. }
  353. void FlatLabel::overrideLinkClickHandler(Fn<void()> handler) {
  354. setClickHandlerFilter([=](
  355. const ClickHandlerPtr &link,
  356. Qt::MouseButton button) {
  357. if (button != Qt::LeftButton) {
  358. return true;
  359. }
  360. handler();
  361. return false;
  362. });
  363. }
  364. void FlatLabel::overrideLinkClickHandler(Fn<void(QString url)> handler) {
  365. setClickHandlerFilter([=](
  366. const ClickHandlerPtr &link,
  367. Qt::MouseButton button) {
  368. if (button != Qt::LeftButton) {
  369. return true;
  370. }
  371. handler(link->url());
  372. return false;
  373. });
  374. }
  375. void FlatLabel::setContextMenuHook(Fn<void(ContextMenuRequest)> hook) {
  376. _contextMenuHook = std::move(hook);
  377. }
  378. void FlatLabel::mouseMoveEvent(QMouseEvent *e) {
  379. _lastMousePos = e->globalPos();
  380. dragActionUpdate();
  381. }
  382. void FlatLabel::mousePressEvent(QMouseEvent *e) {
  383. if (_contextMenu) {
  384. e->accept();
  385. return; // ignore mouse press, that was hiding context menu
  386. }
  387. dragActionStart(e->globalPos(), e->button());
  388. }
  389. Text::StateResult FlatLabel::dragActionStart(const QPoint &p, Qt::MouseButton button) {
  390. _lastMousePos = p;
  391. auto state = dragActionUpdate();
  392. if (button != Qt::LeftButton) return state;
  393. ClickHandler::pressed();
  394. _dragAction = NoDrag;
  395. _dragWasInactive = WasInactivePress(window());
  396. if (_dragWasInactive) {
  397. MarkInactivePress(window(), false);
  398. }
  399. if (ClickHandler::getPressed()) {
  400. _dragStartPosition = mapFromGlobal(_lastMousePos);
  401. _dragAction = PrepareDrag;
  402. }
  403. if (!_selectable || _dragAction != NoDrag) {
  404. return state;
  405. }
  406. if (_trippleClickTimer.isActive() && (_lastMousePos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) {
  407. if (state.uponSymbol) {
  408. _selection = { state.symbol, state.symbol };
  409. _savedSelection = { 0, 0 };
  410. _dragSymbol = state.symbol;
  411. _dragAction = Selecting;
  412. _selectionType = TextSelectType::Paragraphs;
  413. updateHover(state);
  414. _trippleClickTimer.callOnce(QApplication::doubleClickInterval());
  415. update();
  416. }
  417. }
  418. if (_selectionType != TextSelectType::Paragraphs) {
  419. _dragSymbol = state.symbol;
  420. bool uponSelected = state.uponSymbol;
  421. if (uponSelected) {
  422. if (_dragSymbol < _selection.from || _dragSymbol >= _selection.to) {
  423. uponSelected = false;
  424. }
  425. }
  426. if (uponSelected) {
  427. _dragStartPosition = mapFromGlobal(_lastMousePos);
  428. _dragAction = PrepareDrag; // start text drag
  429. } else if (!_dragWasInactive) {
  430. if (state.afterSymbol) ++_dragSymbol;
  431. _selection = { _dragSymbol, _dragSymbol };
  432. _savedSelection = { 0, 0 };
  433. _dragAction = Selecting;
  434. update();
  435. }
  436. }
  437. return state;
  438. }
  439. Text::StateResult FlatLabel::dragActionFinish(const QPoint &p, Qt::MouseButton button) {
  440. _lastMousePos = p;
  441. auto state = dragActionUpdate();
  442. auto activated = ClickHandler::unpressed();
  443. if (_dragAction == Dragging) {
  444. activated = nullptr;
  445. } else if (_dragAction == PrepareDrag) {
  446. _selection = { 0, 0 };
  447. _savedSelection = { 0, 0 };
  448. update();
  449. }
  450. _dragAction = NoDrag;
  451. _selectionType = TextSelectType::Letters;
  452. if (activated) {
  453. // _clickHandlerFilter may delete `this`. In that case we don't want
  454. // to try to show a context menu or smth like that.
  455. crl::on_main(this, [=] {
  456. const auto guard = window();
  457. if (!_clickHandlerFilter
  458. || _clickHandlerFilter(activated, button)) {
  459. ActivateClickHandler(guard, activated, button);
  460. }
  461. });
  462. }
  463. if (QGuiApplication::clipboard()->supportsSelection()
  464. && !_selection.empty()) {
  465. TextUtilities::SetClipboardText(
  466. _text.toTextForMimeData(_selection),
  467. QClipboard::Selection);
  468. }
  469. return state;
  470. }
  471. void FlatLabel::mouseReleaseEvent(QMouseEvent *e) {
  472. dragActionFinish(e->globalPos(), e->button());
  473. if (!rect().contains(e->pos())) {
  474. leaveEvent(e);
  475. }
  476. }
  477. void FlatLabel::mouseDoubleClickEvent(QMouseEvent *e) {
  478. auto state = dragActionStart(e->globalPos(), e->button());
  479. if (((_dragAction == Selecting) || (_dragAction == NoDrag)) && _selectionType == TextSelectType::Letters) {
  480. if (state.uponSymbol) {
  481. _dragSymbol = state.symbol;
  482. _selectionType = _doubleClickSelectsParagraph ? TextSelectType::Paragraphs : TextSelectType::Words;
  483. if (_dragAction == NoDrag) {
  484. _dragAction = Selecting;
  485. _selection = { state.symbol, state.symbol };
  486. _savedSelection = { 0, 0 };
  487. }
  488. mouseMoveEvent(e);
  489. _trippleClickPoint = e->globalPos();
  490. _trippleClickTimer.callOnce(QApplication::doubleClickInterval());
  491. }
  492. }
  493. }
  494. void FlatLabel::enterEventHook(QEnterEvent *e) {
  495. _lastMousePos = QCursor::pos();
  496. dragActionUpdate();
  497. }
  498. void FlatLabel::leaveEventHook(QEvent *e) {
  499. ClickHandler::clearActive(this);
  500. }
  501. void FlatLabel::focusOutEvent(QFocusEvent *e) {
  502. if (!_selection.empty()) {
  503. if (_contextMenu) {
  504. _savedSelection = _selection;
  505. }
  506. _selection = { 0, 0 };
  507. update();
  508. }
  509. }
  510. void FlatLabel::focusInEvent(QFocusEvent *e) {
  511. if (!_savedSelection.empty()) {
  512. _selection = _savedSelection;
  513. _savedSelection = { 0, 0 };
  514. update();
  515. }
  516. }
  517. void FlatLabel::keyPressEvent(QKeyEvent *e) {
  518. e->ignore();
  519. if (e->key() == Qt::Key_Copy || (e->key() == Qt::Key_C && e->modifiers().testFlag(Qt::ControlModifier))) {
  520. if (!_selection.empty()) {
  521. copySelectedText();
  522. e->accept();
  523. }
  524. #ifdef Q_OS_MAC
  525. } else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
  526. auto selection = _selection.empty() ? (_contextMenu ? _savedSelection : _selection) : _selection;
  527. if (!selection.empty()) {
  528. TextUtilities::SetClipboardText(_text.toTextForMimeData(selection), QClipboard::FindBuffer);
  529. }
  530. #endif // Q_OS_MAC
  531. }
  532. }
  533. void FlatLabel::contextMenuEvent(QContextMenuEvent *e) {
  534. if (!_contextMenuHook && !_selectable && !_text.hasLinks()) {
  535. return;
  536. }
  537. showContextMenu(e, ContextMenuReason::FromEvent);
  538. }
  539. bool FlatLabel::eventHook(QEvent *e) {
  540. if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) {
  541. QTouchEvent *ev = static_cast<QTouchEvent*>(e);
  542. if (ev->device()->type() == base::TouchDevice::TouchScreen) {
  543. touchEvent(ev);
  544. return true;
  545. }
  546. }
  547. return RpWidget::eventHook(e);
  548. }
  549. void FlatLabel::touchEvent(QTouchEvent *e) {
  550. if (e->type() == QEvent::TouchCancel) { // cancel
  551. if (!_touchInProgress) return;
  552. _touchInProgress = false;
  553. _touchSelectTimer.cancel();
  554. _touchSelect = false;
  555. _dragAction = NoDrag;
  556. return;
  557. }
  558. if (!e->touchPoints().isEmpty()) {
  559. _touchPrevPos = _touchPos;
  560. _touchPos = e->touchPoints().cbegin()->screenPos().toPoint();
  561. }
  562. switch (e->type()) {
  563. case QEvent::TouchBegin: {
  564. if (_contextMenu) {
  565. e->accept();
  566. return; // ignore mouse press, that was hiding context menu
  567. }
  568. if (_touchInProgress) return;
  569. if (e->touchPoints().isEmpty()) return;
  570. _touchInProgress = true;
  571. _touchSelectTimer.callOnce(QApplication::startDragTime());
  572. _touchSelect = false;
  573. _touchStart = _touchPrevPos = _touchPos;
  574. } break;
  575. case QEvent::TouchUpdate: {
  576. if (!_touchInProgress) return;
  577. if (_touchSelect) {
  578. _lastMousePos = _touchPos;
  579. dragActionUpdate();
  580. }
  581. } break;
  582. case QEvent::TouchEnd: {
  583. if (!_touchInProgress) return;
  584. _touchInProgress = false;
  585. auto weak = MakeWeak(this);
  586. if (_touchSelect) {
  587. dragActionFinish(_touchPos, Qt::RightButton);
  588. QContextMenuEvent contextMenu(QContextMenuEvent::Mouse, mapFromGlobal(_touchPos), _touchPos);
  589. showContextMenu(&contextMenu, ContextMenuReason::FromTouch);
  590. } else { // one short tap -- like mouse click
  591. dragActionStart(_touchPos, Qt::LeftButton);
  592. dragActionFinish(_touchPos, Qt::LeftButton);
  593. }
  594. if (weak) {
  595. _touchSelectTimer.cancel();
  596. _touchSelect = false;
  597. }
  598. } break;
  599. }
  600. }
  601. void FlatLabel::showContextMenu(QContextMenuEvent *e, ContextMenuReason reason) {
  602. if (e->reason() == QContextMenuEvent::Mouse) {
  603. _lastMousePos = e->globalPos();
  604. } else {
  605. _lastMousePos = QCursor::pos();
  606. }
  607. const auto state = dragActionUpdate();
  608. const auto hasSelection = _selectable && !_selection.empty();
  609. const auto uponSelection = _selectable
  610. && ((reason == ContextMenuReason::FromTouch && hasSelection)
  611. || (state.uponSymbol
  612. && (state.symbol >= _selection.from)
  613. && (state.symbol < _selection.to)));
  614. _contextMenu = base::make_unique_q<PopupMenu>(this, _stMenu);
  615. const auto request = ContextMenuRequest{
  616. .menu = _contextMenu.get(),
  617. .link = ClickHandler::getActive(),
  618. .selection = _selectable ? _selection : TextSelection(),
  619. .uponSelection = uponSelection,
  620. .fullSelection = _selectable && _text.isFullSelection(_selection),
  621. };
  622. if (_contextMenuHook) {
  623. _contextMenuHook(request);
  624. } else {
  625. fillContextMenu(request);
  626. }
  627. if (_contextMenu->empty()) {
  628. _contextMenu = nullptr;
  629. } else {
  630. _contextMenu->popup(e->globalPos());
  631. e->accept();
  632. }
  633. }
  634. void FlatLabel::fillContextMenu(ContextMenuRequest request) {
  635. if (request.fullSelection && !_contextCopyText.isEmpty()) {
  636. request.menu->addAction(
  637. _contextCopyText,
  638. [=] { copyContextText(); });
  639. } else if (request.uponSelection && !request.fullSelection) {
  640. request.menu->addAction(
  641. Integration::Instance().phraseContextCopySelected(),
  642. [=] { copySelectedText(); });
  643. } else if (_selectable
  644. && request.selection.empty()
  645. && !_contextCopyText.isEmpty()) {
  646. request.menu->addAction(
  647. _contextCopyText,
  648. [=] { copyContextText(); });
  649. }
  650. if (request.link) {
  651. const auto label = request.link->copyToClipboardContextItemText();
  652. if (!label.isEmpty()) {
  653. request.menu->addAction(
  654. label,
  655. [text = request.link->copyToClipboardText()] {
  656. QGuiApplication::clipboard()->setText(text);
  657. });
  658. }
  659. }
  660. }
  661. void FlatLabel::copySelectedText() {
  662. const auto selection = _selection.empty() ? (_contextMenu ? _savedSelection : _selection) : _selection;
  663. if (!selection.empty()) {
  664. TextUtilities::SetClipboardText(_text.toTextForMimeData(selection));
  665. }
  666. }
  667. void FlatLabel::copyContextText() {
  668. TextUtilities::SetClipboardText(_text.toTextForMimeData());
  669. }
  670. void FlatLabel::touchSelect() {
  671. _touchSelect = true;
  672. dragActionStart(_touchPos, Qt::LeftButton);
  673. }
  674. void FlatLabel::executeDrag() {
  675. if (_dragAction != Dragging) return;
  676. auto state = getTextState(_dragStartPosition);
  677. bool uponSelected = state.uponSymbol && _selection.from <= state.symbol;
  678. if (uponSelected) {
  679. if (_dragSymbol < _selection.from || _dragSymbol >= _selection.to) {
  680. uponSelected = false;
  681. }
  682. }
  683. const auto pressedHandler = ClickHandler::getPressed();
  684. const auto selectedText = [&] {
  685. if (uponSelected) {
  686. return _text.toTextForMimeData(_selection);
  687. } else if (pressedHandler) {
  688. return TextForMimeData::Simple(pressedHandler->dragText());
  689. }
  690. return TextForMimeData();
  691. }();
  692. if (auto mimeData = TextUtilities::MimeDataFromText(selectedText)) {
  693. auto drag = new QDrag(window());
  694. drag->setMimeData(mimeData.release());
  695. drag->exec(Qt::CopyAction);
  696. // We don't receive mouseReleaseEvent when drag is finished.
  697. ClickHandler::unpressed();
  698. }
  699. }
  700. void FlatLabel::clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) {
  701. update();
  702. }
  703. void FlatLabel::clickHandlerPressedChanged(const ClickHandlerPtr &action, bool active) {
  704. update();
  705. }
  706. CrossFadeAnimation::Data FlatLabel::crossFadeData(
  707. style::color bg,
  708. QPoint basePosition) {
  709. auto result = CrossFadeAnimation::Data();
  710. result.full = GrabWidgetToImage(this, QRect(), bg->c);
  711. const auto textWidth = width() - _st.margin.left() - _st.margin.right();
  712. result.lineWidths = _text.countLineWidths(textWidth, {
  713. .breakEverywhere = _breakEverywhere,
  714. });
  715. result.lineHeight = _st.style.font->height;
  716. const auto addedHeight = (_st.style.lineHeight - result.lineHeight);
  717. if (addedHeight > 0) {
  718. result.lineAddTop = addedHeight / 2;
  719. result.lineHeight += addedHeight;
  720. }
  721. result.position = basePosition + pos();
  722. result.align = _st.align;
  723. result.font = _st.style.font;
  724. result.margin = _st.margin;
  725. return result;
  726. }
  727. std::unique_ptr<CrossFadeAnimation> FlatLabel::CrossFade(
  728. not_null<FlatLabel*> from,
  729. not_null<FlatLabel*> to,
  730. style::color bg,
  731. QPoint fromPosition,
  732. QPoint toPosition) {
  733. return std::make_unique<CrossFadeAnimation>(
  734. bg,
  735. from->crossFadeData(bg, fromPosition),
  736. to->crossFadeData(bg, toPosition));
  737. }
  738. Text::StateResult FlatLabel::dragActionUpdate() {
  739. auto m = mapFromGlobal(_lastMousePos);
  740. auto state = getTextState(m);
  741. updateHover(state);
  742. if (_dragAction == PrepareDrag && (m - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
  743. _dragAction = Dragging;
  744. InvokeQueued(this, [=] { executeDrag(); });
  745. }
  746. return state;
  747. }
  748. void FlatLabel::updateHover(const Text::StateResult &state) {
  749. bool lnkChanged = ClickHandler::setActive(state.link, this);
  750. if (!_selectable) {
  751. refreshCursor(state.uponSymbol);
  752. return;
  753. }
  754. Qt::CursorShape cur = style::cur_default;
  755. if (_dragAction == NoDrag) {
  756. if (state.link) {
  757. cur = style::cur_pointer;
  758. } else if (state.uponSymbol) {
  759. cur = style::cur_text;
  760. }
  761. } else {
  762. if (_dragAction == Selecting) {
  763. uint16 second = state.symbol;
  764. if (state.afterSymbol && _selectionType == TextSelectType::Letters) {
  765. ++second;
  766. }
  767. auto selection = _text.adjustSelection({ qMin(second, _dragSymbol), qMax(second, _dragSymbol) }, _selectionType);
  768. if (_selection != selection) {
  769. _selection = selection;
  770. _savedSelection = { 0, 0 };
  771. setFocus();
  772. update();
  773. }
  774. } else if (_dragAction == Dragging) {
  775. }
  776. if (ClickHandler::getPressed()) {
  777. cur = style::cur_pointer;
  778. } else if (_dragAction == Selecting) {
  779. cur = style::cur_text;
  780. }
  781. }
  782. if (_dragAction == Selecting) {
  783. // checkSelectingScroll();
  784. } else {
  785. // noSelectingScroll();
  786. }
  787. if (_dragAction == NoDrag && (lnkChanged || cur != _cursor)) {
  788. setCursor(_cursor = cur);
  789. }
  790. }
  791. void FlatLabel::refreshCursor(bool uponSymbol) {
  792. if (_dragAction != NoDrag) {
  793. return;
  794. }
  795. bool needTextCursor = _selectable && uponSymbol;
  796. style::cursor newCursor = needTextCursor ? style::cur_text : style::cur_default;
  797. if (ClickHandler::getActive()) {
  798. newCursor = style::cur_pointer;
  799. }
  800. if (newCursor != _cursor) {
  801. _cursor = newCursor;
  802. setCursor(_cursor);
  803. }
  804. }
  805. Text::StateResult FlatLabel::getTextState(const QPoint &m) const {
  806. Text::StateRequestElided request;
  807. request.align = _st.align;
  808. if (_selectable) {
  809. request.flags |= Text::StateRequest::Flag::LookupSymbol;
  810. }
  811. const auto textWidth = _textWidth
  812. ? _textWidth
  813. : (width() - _st.margin.left() - _st.margin.right());
  814. const auto useWidth = !(_st.align & Qt::AlignLeft)
  815. ? textWidth
  816. : std::min(textWidth, _text.maxWidth());
  817. Text::StateResult state;
  818. bool heightExceeded = _st.maxHeight && (_st.maxHeight < _fullTextHeight || useWidth < _text.maxWidth());
  819. bool renderElided = _breakEverywhere || heightExceeded;
  820. if (renderElided) {
  821. auto lineHeight = qMax(_st.style.lineHeight, _st.style.font->height);
  822. auto lines = _st.maxHeight ? qMax(_st.maxHeight / lineHeight, 1) : ((height() / lineHeight) + 2);
  823. request.lines = lines;
  824. if (_breakEverywhere) {
  825. request.flags |= Text::StateRequest::Flag::BreakEverywhere;
  826. }
  827. state = _text.getStateElided(m - QPoint(_st.margin.left(), _st.margin.top()), useWidth, request);
  828. } else {
  829. state = _text.getState(m - QPoint(_st.margin.left(), _st.margin.top()), useWidth, request);
  830. }
  831. return state;
  832. }
  833. void FlatLabel::setOpacity(float64 o) {
  834. _opacity = o;
  835. update();
  836. }
  837. void FlatLabel::setTextColorOverride(std::optional<QColor> color) {
  838. _textColorOverride = color;
  839. update();
  840. }
  841. void FlatLabel::paintEvent(QPaintEvent *e) {
  842. Painter p(this);
  843. p.setOpacity(_opacity);
  844. if (_textColorOverride) {
  845. p.setPen(*_textColorOverride);
  846. } else {
  847. p.setPen(_st.textFg);
  848. }
  849. p.setTextPalette(_st.palette);
  850. const auto textWidth = _textWidth
  851. ? _textWidth
  852. : (width() - _st.margin.left() - _st.margin.right());
  853. const auto textLeft = _textWidth
  854. ? ((_st.align & Qt::AlignLeft)
  855. ? _st.margin.left()
  856. : (_st.align & Qt::AlignHCenter)
  857. ? ((width() - _textWidth) / 2)
  858. : (width() - _st.margin.right() - _textWidth))
  859. : _st.margin.left();
  860. const auto selection = !_selection.empty()
  861. ? _selection
  862. : _contextMenu
  863. ? _savedSelection
  864. : _selection;
  865. const auto heightExceeded = _st.maxHeight
  866. && (_st.maxHeight < _fullTextHeight || textWidth < _text.maxWidth());
  867. const auto renderElided = _breakEverywhere || heightExceeded;
  868. const auto lineHeight = qMax(_st.style.lineHeight, _st.style.font->height);
  869. const auto elisionHeight = !renderElided
  870. ? 0
  871. : _st.maxHeight
  872. ? qMax(_st.maxHeight, lineHeight)
  873. : height();
  874. const auto paused = _animationsPausedCallback
  875. ? _animationsPausedCallback()
  876. : WhichAnimationsPaused::None;
  877. _text.draw(p, {
  878. .position = { textLeft, _st.margin.top() },
  879. .availableWidth = textWidth,
  880. .align = _st.align,
  881. .clip = e->rect(),
  882. .palette = &_st.palette,
  883. .spoiler = Text::DefaultSpoilerCache(),
  884. .now = crl::now(),
  885. .pausedEmoji = (paused == WhichAnimationsPaused::CustomEmoji
  886. || paused == WhichAnimationsPaused::All),
  887. .pausedSpoiler = (paused == WhichAnimationsPaused::Spoiler
  888. || paused == WhichAnimationsPaused::All),
  889. .selection = selection,
  890. .elisionHeight = elisionHeight,
  891. .elisionBreakEverywhere = renderElided && _breakEverywhere,
  892. });
  893. }
  894. DividerLabel::DividerLabel(
  895. QWidget *parent,
  896. object_ptr<RpWidget> &&child,
  897. const style::margins &padding,
  898. RectParts parts)
  899. : PaddingWrap(parent, std::move(child), padding)
  900. , _background(this, st::boxDividerHeight, st::boxDividerBg, parts) {
  901. }
  902. int DividerLabel::naturalWidth() const {
  903. return -1;
  904. }
  905. void DividerLabel::resizeEvent(QResizeEvent *e) {
  906. _background->lower();
  907. _background->setGeometry(rect());
  908. return PaddingWrap::resizeEvent(e);
  909. }
  910. } // namespace Ui