906 lines
		
	
	
	
		
			25 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			906 lines
		
	
	
	
		
			25 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // This file is part of Desktop App Toolkit,
 | |
| // a set of libraries for developing nice desktop applications.
 | |
| //
 | |
| // For license and copyright information please follow this link:
 | |
| // https://github.com/desktop-app/legal/blob/master/LEGAL
 | |
| //
 | |
| #include "ui/widgets/labels.h"
 | |
| 
 | |
| #include "base/invoke_queued.h"
 | |
| #include "ui/text/text_entity.h"
 | |
| #include "ui/effects/animation_value.h"
 | |
| #include "ui/widgets/popup_menu.h"
 | |
| #include "ui/widgets/box_content_divider.h"
 | |
| #include "ui/basic_click_handlers.h" // UrlClickHandler
 | |
| #include "ui/inactive_press.h"
 | |
| 
 | |
| #include <QtWidgets/QApplication>
 | |
| #include <QtGui/QClipboard>
 | |
| #include <QtGui/QDrag>
 | |
| #include <QtGui/QtEvents>
 | |
| #include <QtCore/QMimeData>
 | |
| 
 | |
| namespace Ui {
 | |
| namespace {
 | |
| 
 | |
| TextParseOptions _labelOptions = {
 | |
| 	TextParseMultiline, // flags
 | |
| 	0, // maxw
 | |
| 	0, // maxh
 | |
| 	Qt::LayoutDirectionAuto, // dir
 | |
| };
 | |
| 
 | |
| TextParseOptions _labelMarkedOptions = {
 | |
| 	TextParseMultiline | TextParseRichText | TextParseLinks | TextParseHashtags | TextParseMentions | TextParseBotCommands | TextParseMarkdown, // flags
 | |
| 	0, // maxw
 | |
| 	0, // maxh
 | |
| 	Qt::LayoutDirectionAuto, // dir
 | |
| };
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| CrossFadeAnimation::CrossFadeAnimation(
 | |
| 		style::color bg,
 | |
| 		Data &&was,
 | |
| 		Data &&now)
 | |
| : _bg(bg) {
 | |
| 	const auto maxLines = qMax(was.lineWidths.size(), now.lineWidths.size());
 | |
| 	auto fillDataTill = [&](Data &data) {
 | |
| 		for (auto i = data.lineWidths.size(); i != maxLines; ++i) {
 | |
| 			data.lineWidths.push_back(-1);
 | |
| 		}
 | |
| 	};
 | |
| 	fillDataTill(was);
 | |
| 	fillDataTill(now);
 | |
| 	auto preparePart = [](const Data &data, int index, const Data &other) {
 | |
| 		auto result = CrossFadeAnimation::Part();
 | |
| 		auto lineWidth = data.lineWidths[index];
 | |
| 		if (lineWidth < 0) {
 | |
| 			lineWidth = other.lineWidths[index];
 | |
| 		}
 | |
| 		const auto pixelRatio = style::DevicePixelRatio();
 | |
| 		auto fullWidth = data.full.width() / pixelRatio;
 | |
| 		auto top = index * data.lineHeight + data.lineAddTop;
 | |
| 		auto left = 0;
 | |
| 		if (data.align & Qt::AlignHCenter) {
 | |
| 			left += (fullWidth - lineWidth) / 2;
 | |
| 		} else if (data.align & Qt::AlignRight) {
 | |
| 			left += (fullWidth - lineWidth);
 | |
| 		}
 | |
| 		auto snapshotRect = data.full.rect().intersected(QRect(left * pixelRatio, top * pixelRatio, lineWidth * pixelRatio, data.font->height * pixelRatio));
 | |
| 		if (!snapshotRect.isEmpty()) {
 | |
| 			result.snapshot = PixmapFromImage(data.full.copy(snapshotRect));
 | |
| 			result.snapshot.setDevicePixelRatio(pixelRatio);
 | |
| 		}
 | |
| 		result.position = data.position + QPoint(data.margin.left() + left, data.margin.top() + top);
 | |
| 		return result;
 | |
| 	};
 | |
| 	for (int i = 0; i != maxLines; ++i) {
 | |
| 		addLine(preparePart(was, i, now), preparePart(now, i, was));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void CrossFadeAnimation::addLine(Part was, Part now) {
 | |
| 	_lines.push_back(Line(std::move(was), std::move(now)));
 | |
| }
 | |
| 
 | |
| void CrossFadeAnimation::paintFrame(QPainter &p, float64 dt) {
 | |
| 	auto progress = anim::linear(1., dt);
 | |
| 	paintFrame(p, progress, 1. - progress, progress);
 | |
| }
 | |
| 
 | |
| void CrossFadeAnimation::paintFrame(
 | |
| 		QPainter &p,
 | |
| 		float64 positionReady,
 | |
| 		float64 alphaWas,
 | |
| 		float64 alphaNow) {
 | |
| 	if (_lines.isEmpty()) return;
 | |
| 
 | |
| 	for (const auto &line : std::as_const(_lines)) {
 | |
| 		paintLine(p, line, positionReady, alphaWas, alphaNow);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void CrossFadeAnimation::paintLine(
 | |
| 		QPainter &p,
 | |
| 		const Line &line,
 | |
| 		float64 positionReady,
 | |
| 		float64 alphaWas,
 | |
| 		float64 alphaNow) {
 | |
| 	auto &snapshotWas = line.was.snapshot;
 | |
| 	auto &snapshotNow = line.now.snapshot;
 | |
| 	if (snapshotWas.isNull() && snapshotNow.isNull()) {
 | |
| 		// This can happen if both labels have an empty line or if one
 | |
| 		// label has an empty line where the second one already ended.
 | |
| 		// In this case lineWidth is zero and snapshot is null.
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	const auto pixelRatio = style::DevicePixelRatio();
 | |
| 	auto positionWas = line.was.position;
 | |
| 	auto positionNow = line.now.position;
 | |
| 	auto left = anim::interpolate(positionWas.x(), positionNow.x(), positionReady);
 | |
| 	auto topDelta = (snapshotNow.height() / pixelRatio) - (snapshotWas.height() / pixelRatio);
 | |
| 	auto widthDelta = (snapshotNow.width() / pixelRatio) - (snapshotWas.width() / pixelRatio);
 | |
| 	auto topWas = anim::interpolate(positionWas.y(), positionNow.y() + topDelta, positionReady);
 | |
| 	auto topNow = topWas - topDelta;
 | |
| 
 | |
| 	p.setOpacity(alphaWas);
 | |
| 	if (!snapshotWas.isNull()) {
 | |
| 		p.drawPixmap(left, topWas, snapshotWas);
 | |
| 		if (topDelta > 0) {
 | |
| 			p.fillRect(left, topWas - topDelta, snapshotWas.width() / pixelRatio, topDelta, _bg);
 | |
| 		}
 | |
| 		if (widthDelta > 0) {
 | |
| 			p.fillRect(left + (snapshotWas.width() / pixelRatio), topNow, widthDelta, snapshotNow.height() / pixelRatio, _bg);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	p.setOpacity(alphaNow);
 | |
| 	if (!snapshotNow.isNull()) {
 | |
| 		p.drawPixmap(left, topNow, snapshotNow);
 | |
| 		if (topDelta < 0) {
 | |
| 			p.fillRect(left, topNow + topDelta, snapshotNow.width() / pixelRatio, -topDelta, _bg);
 | |
| 		}
 | |
| 		if (widthDelta < 0) {
 | |
| 			p.fillRect(left + (snapshotNow.width() / pixelRatio), topWas, -widthDelta, snapshotWas.height() / pixelRatio, _bg);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| LabelSimple::LabelSimple(
 | |
| 	QWidget *parent,
 | |
| 	const style::LabelSimple &st,
 | |
| 	const QString &value)
 | |
| : RpWidget(parent)
 | |
| , _st(st) {
 | |
| 	setText(value);
 | |
| }
 | |
| 
 | |
| void LabelSimple::setText(const QString &value, bool *outTextChanged) {
 | |
| 	if (_fullText == value) {
 | |
| 		if (outTextChanged) *outTextChanged = false;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	_fullText = value;
 | |
| 	_fullTextWidth = _st.font->width(_fullText);
 | |
| 	if (!_st.maxWidth || _fullTextWidth <= _st.maxWidth) {
 | |
| 		_text = _fullText;
 | |
| 		_textWidth = _fullTextWidth;
 | |
| 	} else {
 | |
| 		auto newText = _st.font->elided(_fullText, _st.maxWidth);
 | |
| 		if (newText == _text) {
 | |
| 			if (outTextChanged) *outTextChanged = false;
 | |
| 			return;
 | |
| 		}
 | |
| 		_text = newText;
 | |
| 		_textWidth = _st.font->width(_text);
 | |
| 	}
 | |
| 	resize(_textWidth, _st.font->height);
 | |
| 	update();
 | |
| 	if (outTextChanged) *outTextChanged = true;
 | |
| }
 | |
| 
 | |
| void LabelSimple::paintEvent(QPaintEvent *e) {
 | |
| 	Painter p(this);
 | |
| 
 | |
| 	p.setFont(_st.font);
 | |
| 	p.setPen(_st.textFg);
 | |
| 	p.drawTextLeft(0, 0, width(), _text, _textWidth);
 | |
| }
 | |
| 
 | |
| FlatLabel::FlatLabel(QWidget *parent, const style::FlatLabel &st)
 | |
| : RpWidget(parent)
 | |
| , _text(st.minWidth ? st.minWidth : QFIXED_MAX)
 | |
| , _st(st) {
 | |
| 	init();
 | |
| }
 | |
| 
 | |
| FlatLabel::FlatLabel(
 | |
| 	QWidget *parent,
 | |
| 	const QString &text,
 | |
| 	const style::FlatLabel &st)
 | |
| : RpWidget(parent)
 | |
| , _text(st.minWidth ? st.minWidth : QFIXED_MAX)
 | |
| , _st(st) {
 | |
| 	setText(text);
 | |
| 	init();
 | |
| }
 | |
| 
 | |
| FlatLabel::FlatLabel(
 | |
| 	QWidget *parent,
 | |
| 	rpl::producer<QString> &&text,
 | |
| 	const style::FlatLabel &st)
 | |
| : RpWidget(parent)
 | |
| , _text(st.minWidth ? st.minWidth : QFIXED_MAX)
 | |
| , _st(st) {
 | |
| 	textUpdated();
 | |
| 	std::move(
 | |
| 		text
 | |
| 	) | rpl::start_with_next([this](const QString &value) {
 | |
| 		setText(value);
 | |
| 	}, lifetime());
 | |
| 	init();
 | |
| }
 | |
| 
 | |
| FlatLabel::FlatLabel(
 | |
| 	QWidget *parent,
 | |
| 	rpl::producer<TextWithEntities> &&text,
 | |
| 	const style::FlatLabel &st)
 | |
| : RpWidget(parent)
 | |
| , _text(st.minWidth ? st.minWidth : QFIXED_MAX)
 | |
| , _st(st)
 | |
| , _touchSelectTimer([=] { touchSelect(); }) {
 | |
| 	textUpdated();
 | |
| 	std::move(
 | |
| 		text
 | |
| 	) | rpl::start_with_next([this](const TextWithEntities &value) {
 | |
| 		setMarkedText(value);
 | |
| 	}, lifetime());
 | |
| 	init();
 | |
| }
 | |
| 
 | |
| void FlatLabel::init() {
 | |
| 	_contextCopyText = Integration::Instance().phraseContextCopyText();
 | |
| }
 | |
| 
 | |
| void FlatLabel::textUpdated() {
 | |
| 	refreshSize();
 | |
| 	setMouseTracking(_selectable || _text.hasLinks());
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void FlatLabel::setText(const QString &text) {
 | |
| 	_text.setText(_st.style, text, _labelOptions);
 | |
| 	textUpdated();
 | |
| }
 | |
| 
 | |
| void FlatLabel::setRichText(const QString &text) {
 | |
| 	_text.setRichText(_st.style, text, _labelOptions);
 | |
| 	textUpdated();
 | |
| }
 | |
| 
 | |
| void FlatLabel::setMarkedText(const TextWithEntities &textWithEntities) {
 | |
| 	_text.setMarkedText(_st.style, textWithEntities, _labelMarkedOptions);
 | |
| 	textUpdated();
 | |
| }
 | |
| 
 | |
| void FlatLabel::setSelectable(bool selectable) {
 | |
| 	_selectable = selectable;
 | |
| 	setMouseTracking(_selectable || _text.hasLinks());
 | |
| }
 | |
| 
 | |
| void FlatLabel::setDoubleClickSelectsParagraph(bool doubleClickSelectsParagraph) {
 | |
| 	_doubleClickSelectsParagraph = doubleClickSelectsParagraph;
 | |
| }
 | |
| 
 | |
| void FlatLabel::setContextCopyText(const QString ©Text) {
 | |
| 	_contextCopyText = copyText;
 | |
| }
 | |
| 
 | |
| void FlatLabel::setBreakEverywhere(bool breakEverywhere) {
 | |
| 	_breakEverywhere = breakEverywhere;
 | |
| }
 | |
| 
 | |
| void FlatLabel::setTryMakeSimilarLines(bool tryMakeSimilarLines) {
 | |
| 	_tryMakeSimilarLines = tryMakeSimilarLines;
 | |
| }
 | |
| 
 | |
| int FlatLabel::resizeGetHeight(int newWidth) {
 | |
| 	_allowedWidth = newWidth;
 | |
| 	_textWidth = countTextWidth();
 | |
| 	return countTextHeight(_textWidth);
 | |
| }
 | |
| 
 | |
| int FlatLabel::naturalWidth() const {
 | |
| 	return _text.maxWidth();
 | |
| }
 | |
| 
 | |
| QMargins FlatLabel::getMargins() const {
 | |
| 	return _st.margin;
 | |
| }
 | |
| 
 | |
| int FlatLabel::countTextWidth() const {
 | |
| 	const auto available = _allowedWidth
 | |
| 		? _allowedWidth
 | |
| 		: (_st.minWidth ? _st.minWidth : _text.maxWidth());
 | |
| 	if (_allowedWidth > 0
 | |
| 		&& _allowedWidth < _text.maxWidth()
 | |
| 		&& _tryMakeSimilarLines) {
 | |
| 		auto large = _allowedWidth;
 | |
| 		auto small = _allowedWidth / 2;
 | |
| 		const auto largeHeight = _text.countHeight(large, _breakEverywhere);
 | |
| 		while (large - small > 1) {
 | |
| 			const auto middle = (large + small) / 2;
 | |
| 			if (largeHeight == _text.countHeight(middle, _breakEverywhere)) {
 | |
| 				large = middle;
 | |
| 			} else {
 | |
| 				small = middle;
 | |
| 			}
 | |
| 		}
 | |
| 		return large;
 | |
| 	}
 | |
| 	return available;
 | |
| }
 | |
| 
 | |
| int FlatLabel::countTextHeight(int textWidth) {
 | |
| 	_fullTextHeight = _text.countHeight(textWidth, _breakEverywhere);
 | |
| 	return _st.maxHeight
 | |
| 		? qMin(_fullTextHeight, _st.maxHeight)
 | |
| 		: _fullTextHeight;
 | |
| }
 | |
| 
 | |
| void FlatLabel::refreshSize() {
 | |
| 	int textWidth = countTextWidth();
 | |
| 	int textHeight = countTextHeight(textWidth);
 | |
| 	int fullWidth = _st.margin.left() + textWidth + _st.margin.right();
 | |
| 	int fullHeight = _st.margin.top() + textHeight + _st.margin.bottom();
 | |
| 	resize(fullWidth, fullHeight);
 | |
| }
 | |
| 
 | |
| void FlatLabel::setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk) {
 | |
| 	_text.setLink(lnkIndex, lnk);
 | |
| }
 | |
| 
 | |
| void FlatLabel::setLinksTrusted() {
 | |
| 	static const auto TrustedLinksFilter = [](
 | |
| 			const ClickHandlerPtr &link,
 | |
| 			Qt::MouseButton button) {
 | |
| 		if (const auto url = dynamic_cast<UrlClickHandler*>(link.get())) {
 | |
| 			url->UrlClickHandler::onClick({ button });
 | |
| 			return false;
 | |
| 		}
 | |
| 		return true;
 | |
| 	};
 | |
| 	setClickHandlerFilter(TrustedLinksFilter);
 | |
| }
 | |
| 
 | |
| void FlatLabel::setClickHandlerFilter(ClickHandlerFilter &&filter) {
 | |
| 	_clickHandlerFilter = std::move(filter);
 | |
| }
 | |
| 
 | |
| void FlatLabel::mouseMoveEvent(QMouseEvent *e) {
 | |
| 	_lastMousePos = e->globalPos();
 | |
| 	dragActionUpdate();
 | |
| }
 | |
| 
 | |
| void FlatLabel::mousePressEvent(QMouseEvent *e) {
 | |
| 	if (_contextMenu) {
 | |
| 		e->accept();
 | |
| 		return; // ignore mouse press, that was hiding context menu
 | |
| 	}
 | |
| 	dragActionStart(e->globalPos(), e->button());
 | |
| }
 | |
| 
 | |
| Text::StateResult FlatLabel::dragActionStart(const QPoint &p, Qt::MouseButton button) {
 | |
| 	_lastMousePos = p;
 | |
| 	auto state = dragActionUpdate();
 | |
| 
 | |
| 	if (button != Qt::LeftButton) return state;
 | |
| 
 | |
| 	ClickHandler::pressed();
 | |
| 	_dragAction = NoDrag;
 | |
| 	_dragWasInactive = WasInactivePress(window());
 | |
| 	if (_dragWasInactive) {
 | |
| 		MarkInactivePress(window(), false);
 | |
| 	}
 | |
| 
 | |
| 	if (ClickHandler::getPressed()) {
 | |
| 		_dragStartPosition = mapFromGlobal(_lastMousePos);
 | |
| 		_dragAction = PrepareDrag;
 | |
| 	}
 | |
| 	if (!_selectable || _dragAction != NoDrag) {
 | |
| 		return state;
 | |
| 	}
 | |
| 
 | |
| 	if (_trippleClickTimer.isActive() && (_lastMousePos - _trippleClickPoint).manhattanLength() < QApplication::startDragDistance()) {
 | |
| 		if (state.uponSymbol) {
 | |
| 			_selection = { state.symbol, state.symbol };
 | |
| 			_savedSelection = { 0, 0 };
 | |
| 			_dragSymbol = state.symbol;
 | |
| 			_dragAction = Selecting;
 | |
| 			_selectionType = TextSelectType::Paragraphs;
 | |
| 			updateHover(state);
 | |
| 			_trippleClickTimer.callOnce(QApplication::doubleClickInterval());
 | |
| 			update();
 | |
| 		}
 | |
| 	}
 | |
| 	if (_selectionType != TextSelectType::Paragraphs) {
 | |
| 		_dragSymbol = state.symbol;
 | |
| 		bool uponSelected = state.uponSymbol;
 | |
| 		if (uponSelected) {
 | |
| 			if (_dragSymbol < _selection.from || _dragSymbol >= _selection.to) {
 | |
| 				uponSelected = false;
 | |
| 			}
 | |
| 		}
 | |
| 		if (uponSelected) {
 | |
| 			_dragStartPosition = mapFromGlobal(_lastMousePos);
 | |
| 			_dragAction = PrepareDrag; // start text drag
 | |
| 		} else if (!_dragWasInactive) {
 | |
| 			if (state.afterSymbol) ++_dragSymbol;
 | |
| 			_selection = { _dragSymbol, _dragSymbol };
 | |
| 			_savedSelection = { 0, 0 };
 | |
| 			_dragAction = Selecting;
 | |
| 			update();
 | |
| 		}
 | |
| 	}
 | |
| 	return state;
 | |
| }
 | |
| 
 | |
| Text::StateResult FlatLabel::dragActionFinish(const QPoint &p, Qt::MouseButton button) {
 | |
| 	_lastMousePos = p;
 | |
| 	auto state = dragActionUpdate();
 | |
| 
 | |
| 	auto activated = ClickHandler::unpressed();
 | |
| 	if (_dragAction == Dragging) {
 | |
| 		activated = nullptr;
 | |
| 	} else if (_dragAction == PrepareDrag) {
 | |
| 		_selection = { 0, 0 };
 | |
| 		_savedSelection = { 0, 0 };
 | |
| 		update();
 | |
| 	}
 | |
| 	_dragAction = NoDrag;
 | |
| 	_selectionType = TextSelectType::Letters;
 | |
| 
 | |
| 	if (activated) {
 | |
| 		// _clickHandlerFilter may delete `this`. In that case we don't want
 | |
| 		// to try to show a context menu or smth like that.
 | |
| 		crl::on_main(this, [=] {
 | |
| 			const auto guard = window();
 | |
| 			if (!_clickHandlerFilter
 | |
| 				|| _clickHandlerFilter(activated, button)) {
 | |
| 				ActivateClickHandler(guard, activated, button);
 | |
| 			}
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	if (QGuiApplication::clipboard()->supportsSelection()
 | |
| 		&& !_selection.empty()) {
 | |
| 		TextUtilities::SetClipboardText(
 | |
| 			_text.toTextForMimeData(_selection),
 | |
| 			QClipboard::Selection);
 | |
| 	}
 | |
| 
 | |
| 	return state;
 | |
| }
 | |
| 
 | |
| void FlatLabel::mouseReleaseEvent(QMouseEvent *e) {
 | |
| 	dragActionFinish(e->globalPos(), e->button());
 | |
| 	if (!rect().contains(e->pos())) {
 | |
| 		leaveEvent(e);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::mouseDoubleClickEvent(QMouseEvent *e) {
 | |
| 	auto state = dragActionStart(e->globalPos(), e->button());
 | |
| 	if (((_dragAction == Selecting) || (_dragAction == NoDrag)) && _selectionType == TextSelectType::Letters) {
 | |
| 		if (state.uponSymbol) {
 | |
| 			_dragSymbol = state.symbol;
 | |
| 			_selectionType = _doubleClickSelectsParagraph ? TextSelectType::Paragraphs : TextSelectType::Words;
 | |
| 			if (_dragAction == NoDrag) {
 | |
| 				_dragAction = Selecting;
 | |
| 				_selection = { state.symbol, state.symbol };
 | |
| 				_savedSelection = { 0, 0 };
 | |
| 			}
 | |
| 			mouseMoveEvent(e);
 | |
| 
 | |
| 			_trippleClickPoint = e->globalPos();
 | |
| 			_trippleClickTimer.callOnce(QApplication::doubleClickInterval());
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::enterEventHook(QEvent *e) {
 | |
| 	_lastMousePos = QCursor::pos();
 | |
| 	dragActionUpdate();
 | |
| }
 | |
| 
 | |
| void FlatLabel::leaveEventHook(QEvent *e) {
 | |
| 	ClickHandler::clearActive(this);
 | |
| }
 | |
| 
 | |
| void FlatLabel::focusOutEvent(QFocusEvent *e) {
 | |
| 	if (!_selection.empty()) {
 | |
| 		if (_contextMenu) {
 | |
| 			_savedSelection = _selection;
 | |
| 		}
 | |
| 		_selection = { 0, 0 };
 | |
| 		update();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::focusInEvent(QFocusEvent *e) {
 | |
| 	if (!_savedSelection.empty()) {
 | |
| 		_selection = _savedSelection;
 | |
| 		_savedSelection = { 0, 0 };
 | |
| 		update();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::keyPressEvent(QKeyEvent *e) {
 | |
| 	e->ignore();
 | |
| 	if (e->key() == Qt::Key_Copy || (e->key() == Qt::Key_C && e->modifiers().testFlag(Qt::ControlModifier))) {
 | |
| 		if (!_selection.empty()) {
 | |
| 			copySelectedText();
 | |
| 			e->accept();
 | |
| 		}
 | |
| #ifdef Q_OS_MAC
 | |
| 	} else if (e->key() == Qt::Key_E && e->modifiers().testFlag(Qt::ControlModifier)) {
 | |
| 		auto selection = _selection.empty() ? (_contextMenu ? _savedSelection : _selection) : _selection;
 | |
| 		if (!selection.empty()) {
 | |
| 			TextUtilities::SetClipboardText(_text.toTextForMimeData(selection), QClipboard::FindBuffer);
 | |
| 		}
 | |
| #endif // Q_OS_MAC
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::contextMenuEvent(QContextMenuEvent *e) {
 | |
| 	if (!_selectable && !_text.hasLinks()) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	showContextMenu(e, ContextMenuReason::FromEvent);
 | |
| }
 | |
| 
 | |
| bool FlatLabel::eventHook(QEvent *e) {
 | |
| 	if (e->type() == QEvent::TouchBegin || e->type() == QEvent::TouchUpdate || e->type() == QEvent::TouchEnd || e->type() == QEvent::TouchCancel) {
 | |
| 		QTouchEvent *ev = static_cast<QTouchEvent*>(e);
 | |
| 		if (ev->device()->type() == QTouchDevice::TouchScreen) {
 | |
| 			touchEvent(ev);
 | |
| 			return true;
 | |
| 		}
 | |
| 	}
 | |
| 	return RpWidget::eventHook(e);
 | |
| }
 | |
| 
 | |
| void FlatLabel::touchEvent(QTouchEvent *e) {
 | |
| 	if (e->type() == QEvent::TouchCancel) { // cancel
 | |
| 		if (!_touchInProgress) return;
 | |
| 		_touchInProgress = false;
 | |
| 		_touchSelectTimer.cancel();
 | |
| 		_touchSelect = false;
 | |
| 		_dragAction = NoDrag;
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	if (!e->touchPoints().isEmpty()) {
 | |
| 		_touchPrevPos = _touchPos;
 | |
| 		_touchPos = e->touchPoints().cbegin()->screenPos().toPoint();
 | |
| 	}
 | |
| 
 | |
| 	switch (e->type()) {
 | |
| 	case QEvent::TouchBegin: {
 | |
| 		if (_contextMenu) {
 | |
| 			e->accept();
 | |
| 			return; // ignore mouse press, that was hiding context menu
 | |
| 		}
 | |
| 		if (_touchInProgress) return;
 | |
| 		if (e->touchPoints().isEmpty()) return;
 | |
| 
 | |
| 		_touchInProgress = true;
 | |
| 		_touchSelectTimer.callOnce(QApplication::startDragTime());
 | |
| 		_touchSelect = false;
 | |
| 		_touchStart = _touchPrevPos = _touchPos;
 | |
| 	} break;
 | |
| 
 | |
| 	case QEvent::TouchUpdate: {
 | |
| 		if (!_touchInProgress) return;
 | |
| 		if (_touchSelect) {
 | |
| 			_lastMousePos = _touchPos;
 | |
| 			dragActionUpdate();
 | |
| 		}
 | |
| 	} break;
 | |
| 
 | |
| 	case QEvent::TouchEnd: {
 | |
| 		if (!_touchInProgress) return;
 | |
| 		_touchInProgress = false;
 | |
| 		auto weak = MakeWeak(this);
 | |
| 		if (_touchSelect) {
 | |
| 			dragActionFinish(_touchPos, Qt::RightButton);
 | |
| 			QContextMenuEvent contextMenu(QContextMenuEvent::Mouse, mapFromGlobal(_touchPos), _touchPos);
 | |
| 			showContextMenu(&contextMenu, ContextMenuReason::FromTouch);
 | |
| 		} else { // one short tap -- like mouse click
 | |
| 			dragActionStart(_touchPos, Qt::LeftButton);
 | |
| 			dragActionFinish(_touchPos, Qt::LeftButton);
 | |
| 		}
 | |
| 		if (weak) {
 | |
| 			_touchSelectTimer.cancel();
 | |
| 			_touchSelect = false;
 | |
| 		}
 | |
| 	} break;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::showContextMenu(QContextMenuEvent *e, ContextMenuReason reason) {
 | |
| 	if (e->reason() == QContextMenuEvent::Mouse) {
 | |
| 		_lastMousePos = e->globalPos();
 | |
| 	} else {
 | |
| 		_lastMousePos = QCursor::pos();
 | |
| 	}
 | |
| 	auto state = dragActionUpdate();
 | |
| 
 | |
| 	const auto hasSelection = _selectable && !_selection.empty();
 | |
| 	const auto uponSelection = _selectable
 | |
| 		&& ((reason == ContextMenuReason::FromTouch && hasSelection)
 | |
| 			|| (state.uponSymbol
 | |
| 				&& (state.symbol >= _selection.from)
 | |
| 				&& (state.symbol < _selection.to)));
 | |
| 	const auto fullSelection = _selectable
 | |
| 		&& _text.isFullSelection(_selection);
 | |
| 
 | |
| 	_contextMenu = base::make_unique_q<PopupMenu>(this);
 | |
| 
 | |
| 	if (fullSelection && !_contextCopyText.isEmpty()) {
 | |
| 		_contextMenu->addAction(
 | |
| 			_contextCopyText,
 | |
| 			[=] { copyContextText(); });
 | |
| 	} else if (uponSelection && !fullSelection) {
 | |
| 		_contextMenu->addAction(
 | |
| 			Integration::Instance().phraseContextCopySelected(),
 | |
| 			[=] { copySelectedText(); });
 | |
| 	} else if (_selectable && !hasSelection && !_contextCopyText.isEmpty()) {
 | |
| 		_contextMenu->addAction(
 | |
| 			_contextCopyText,
 | |
| 			[=] { copyContextText(); });
 | |
| 	}
 | |
| 
 | |
| 	if (const auto link = ClickHandler::getActive()) {
 | |
| 		const auto actionText = link->copyToClipboardContextItemText();
 | |
| 		if (!actionText.isEmpty()) {
 | |
| 			_contextMenu->addAction(
 | |
| 				actionText,
 | |
| 				[text = link->copyToClipboardText()] {
 | |
| 					QGuiApplication::clipboard()->setText(text);
 | |
| 				});
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if (_contextMenu->empty()) {
 | |
| 		_contextMenu = nullptr;
 | |
| 	} else {
 | |
| 		_contextMenu->popup(e->globalPos());
 | |
| 		e->accept();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::copySelectedText() {
 | |
| 	const auto selection = _selection.empty() ? (_contextMenu ? _savedSelection : _selection) : _selection;
 | |
| 	if (!selection.empty()) {
 | |
| 		TextUtilities::SetClipboardText(_text.toTextForMimeData(selection));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::copyContextText() {
 | |
| 	TextUtilities::SetClipboardText(_text.toTextForMimeData());
 | |
| }
 | |
| 
 | |
| void FlatLabel::touchSelect() {
 | |
| 	_touchSelect = true;
 | |
| 	dragActionStart(_touchPos, Qt::LeftButton);
 | |
| }
 | |
| 
 | |
| void FlatLabel::executeDrag() {
 | |
| 	if (_dragAction != Dragging) return;
 | |
| 
 | |
| 	auto state = getTextState(_dragStartPosition);
 | |
| 	bool uponSelected = state.uponSymbol && _selection.from <= state.symbol;
 | |
| 	if (uponSelected) {
 | |
| 		if (_dragSymbol < _selection.from || _dragSymbol >= _selection.to) {
 | |
| 			uponSelected = false;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	const auto pressedHandler = ClickHandler::getPressed();
 | |
| 	const auto selectedText = [&] {
 | |
| 		if (uponSelected) {
 | |
| 			return _text.toTextForMimeData(_selection);
 | |
| 		} else if (pressedHandler) {
 | |
| 			return TextForMimeData::Simple(pressedHandler->dragText());
 | |
| 		}
 | |
| 		return TextForMimeData();
 | |
| 	}();
 | |
| 	if (auto mimeData = TextUtilities::MimeDataFromText(selectedText)) {
 | |
| 		auto drag = new QDrag(window());
 | |
| 		drag->setMimeData(mimeData.release());
 | |
| 		drag->exec(Qt::CopyAction);
 | |
| 
 | |
| 		// We don't receive mouseReleaseEvent when drag is finished.
 | |
| 		ClickHandler::unpressed();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::clickHandlerActiveChanged(const ClickHandlerPtr &action, bool active) {
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void FlatLabel::clickHandlerPressedChanged(const ClickHandlerPtr &action, bool active) {
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| CrossFadeAnimation::Data FlatLabel::crossFadeData(
 | |
| 		style::color bg,
 | |
| 		QPoint basePosition) {
 | |
| 	auto result = CrossFadeAnimation::Data();
 | |
| 	result.full = GrabWidgetToImage(this, QRect(), bg->c);
 | |
| 	const auto textWidth = width() - _st.margin.left() - _st.margin.right();
 | |
| 	_text.countLineWidths(textWidth, &result.lineWidths, _breakEverywhere);
 | |
| 	result.lineHeight = _st.style.font->height;
 | |
| 	const auto addedHeight = (_st.style.lineHeight - result.lineHeight);
 | |
| 	if (addedHeight > 0) {
 | |
| 		result.lineAddTop = addedHeight / 2;
 | |
| 		result.lineHeight += addedHeight;
 | |
| 	}
 | |
| 	result.position = basePosition + pos();
 | |
| 	result.align = _st.align;
 | |
| 	result.font = _st.style.font;
 | |
| 	result.margin = _st.margin;
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| std::unique_ptr<CrossFadeAnimation> FlatLabel::CrossFade(
 | |
| 		not_null<FlatLabel*> from,
 | |
| 		not_null<FlatLabel*> to,
 | |
| 		style::color bg,
 | |
| 		QPoint fromPosition,
 | |
| 		QPoint toPosition) {
 | |
| 	return std::make_unique<CrossFadeAnimation>(
 | |
| 		bg,
 | |
| 		from->crossFadeData(bg, fromPosition),
 | |
| 		to->crossFadeData(bg, toPosition));
 | |
| }
 | |
| 
 | |
| Text::StateResult FlatLabel::dragActionUpdate() {
 | |
| 	auto m = mapFromGlobal(_lastMousePos);
 | |
| 	auto state = getTextState(m);
 | |
| 	updateHover(state);
 | |
| 
 | |
| 	if (_dragAction == PrepareDrag && (m - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
 | |
| 		_dragAction = Dragging;
 | |
| 		InvokeQueued(this, [=] { executeDrag(); });
 | |
| 	}
 | |
| 
 | |
| 	return state;
 | |
| }
 | |
| 
 | |
| void FlatLabel::updateHover(const Text::StateResult &state) {
 | |
| 	bool lnkChanged = ClickHandler::setActive(state.link, this);
 | |
| 
 | |
| 	if (!_selectable) {
 | |
| 		refreshCursor(state.uponSymbol);
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	Qt::CursorShape cur = style::cur_default;
 | |
| 	if (_dragAction == NoDrag) {
 | |
| 		if (state.link) {
 | |
| 			cur = style::cur_pointer;
 | |
| 		} else if (state.uponSymbol) {
 | |
| 			cur = style::cur_text;
 | |
| 		}
 | |
| 	} else {
 | |
| 		if (_dragAction == Selecting) {
 | |
| 			uint16 second = state.symbol;
 | |
| 			if (state.afterSymbol && _selectionType == TextSelectType::Letters) {
 | |
| 				++second;
 | |
| 			}
 | |
| 			auto selection = _text.adjustSelection({ qMin(second, _dragSymbol), qMax(second, _dragSymbol) }, _selectionType);
 | |
| 			if (_selection != selection) {
 | |
| 				_selection = selection;
 | |
| 				_savedSelection = { 0, 0 };
 | |
| 				setFocus();
 | |
| 				update();
 | |
| 			}
 | |
| 		} else if (_dragAction == Dragging) {
 | |
| 		}
 | |
| 
 | |
| 		if (ClickHandler::getPressed()) {
 | |
| 			cur = style::cur_pointer;
 | |
| 		} else if (_dragAction == Selecting) {
 | |
| 			cur = style::cur_text;
 | |
| 		}
 | |
| 	}
 | |
| 	if (_dragAction == Selecting) {
 | |
| 		//		checkSelectingScroll();
 | |
| 	} else {
 | |
| 		//		noSelectingScroll();
 | |
| 	}
 | |
| 
 | |
| 	if (_dragAction == NoDrag && (lnkChanged || cur != _cursor)) {
 | |
| 		setCursor(_cursor = cur);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::refreshCursor(bool uponSymbol) {
 | |
| 	if (_dragAction != NoDrag) {
 | |
| 		return;
 | |
| 	}
 | |
| 	bool needTextCursor = _selectable && uponSymbol;
 | |
| 	style::cursor newCursor = needTextCursor ? style::cur_text : style::cur_default;
 | |
| 	if (ClickHandler::getActive()) {
 | |
| 		newCursor = style::cur_pointer;
 | |
| 	}
 | |
| 	if (newCursor != _cursor) {
 | |
| 		_cursor = newCursor;
 | |
| 		setCursor(_cursor);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| Text::StateResult FlatLabel::getTextState(const QPoint &m) const {
 | |
| 	Text::StateRequestElided request;
 | |
| 	request.align = _st.align;
 | |
| 	if (_selectable) {
 | |
| 		request.flags |= Text::StateRequest::Flag::LookupSymbol;
 | |
| 	}
 | |
| 	int textWidth = width() - _st.margin.left() - _st.margin.right();
 | |
| 
 | |
| 	Text::StateResult state;
 | |
| 	bool heightExceeded = _st.maxHeight && (_st.maxHeight < _fullTextHeight || textWidth < _text.maxWidth());
 | |
| 	bool renderElided = _breakEverywhere || heightExceeded;
 | |
| 	if (renderElided) {
 | |
| 		auto lineHeight = qMax(_st.style.lineHeight, _st.style.font->height);
 | |
| 		auto lines = _st.maxHeight ? qMax(_st.maxHeight / lineHeight, 1) : ((height() / lineHeight) + 2);
 | |
| 		request.lines = lines;
 | |
| 		if (_breakEverywhere) {
 | |
| 			request.flags |= Text::StateRequest::Flag::BreakEverywhere;
 | |
| 		}
 | |
| 		state = _text.getStateElided(m - QPoint(_st.margin.left(), _st.margin.top()), textWidth, request);
 | |
| 	} else {
 | |
| 		state = _text.getState(m - QPoint(_st.margin.left(), _st.margin.top()), textWidth, request);
 | |
| 	}
 | |
| 
 | |
| 	return state;
 | |
| }
 | |
| 
 | |
| void FlatLabel::setOpacity(float64 o) {
 | |
| 	_opacity = o;
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void FlatLabel::setTextColorOverride(std::optional<QColor> color) {
 | |
| 	_textColorOverride = color;
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void FlatLabel::paintEvent(QPaintEvent *e) {
 | |
| 	Painter p(this);
 | |
| 	p.setOpacity(_opacity);
 | |
| 	if (_textColorOverride) {
 | |
| 		p.setPen(*_textColorOverride);
 | |
| 	} else {
 | |
| 		p.setPen(_st.textFg);
 | |
| 	}
 | |
| 	p.setTextPalette(_st.palette);
 | |
| 	const auto textWidth = _textWidth
 | |
| 		? _textWidth
 | |
| 		: (width() - _st.margin.left() - _st.margin.right());
 | |
| 	const auto textLeft = _textWidth
 | |
| 		? ((_st.align & Qt::AlignLeft)
 | |
| 			? _st.margin.left()
 | |
| 			: (_st.align & Qt::AlignHCenter)
 | |
| 			? ((width() - _textWidth) / 2)
 | |
| 			: (width() - _st.margin.right() - _textWidth))
 | |
| 		: _st.margin.left();
 | |
| 	auto selection = _selection.empty() ? (_contextMenu ? _savedSelection : _selection) : _selection;
 | |
| 	bool heightExceeded = _st.maxHeight && (_st.maxHeight < _fullTextHeight || textWidth < _text.maxWidth());
 | |
| 	bool renderElided = _breakEverywhere || heightExceeded;
 | |
| 	if (renderElided) {
 | |
| 		auto lineHeight = qMax(_st.style.lineHeight, _st.style.font->height);
 | |
| 		auto lines = _st.maxHeight ? qMax(_st.maxHeight / lineHeight, 1) : ((height() / lineHeight) + 2);
 | |
| 		_text.drawElided(p, textLeft, _st.margin.top(), textWidth, lines, _st.align, e->rect().y(), e->rect().bottom(), 0, _breakEverywhere, selection);
 | |
| 	} else {
 | |
| 		_text.draw(p, textLeft, _st.margin.top(), textWidth, _st.align, e->rect().y(), e->rect().bottom(), selection);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| int DividerLabel::naturalWidth() const {
 | |
| 	return -1;
 | |
| }
 | |
| 
 | |
| void DividerLabel::resizeEvent(QResizeEvent *e) {
 | |
| 	_background->lower();
 | |
| 	_background->setGeometry(rect());
 | |
| 	return PaddingWrap::resizeEvent(e);
 | |
| }
 | |
| 
 | |
| } // namespace Ui
 | 
