796 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			796 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| This file is part of Telegram Desktop,
 | |
| the official desktop version of Telegram messaging app, see https://telegram.org
 | |
| 
 | |
| Telegram Desktop is free software: you can redistribute it and/or modify
 | |
| it under the terms of the GNU General Public License as published by
 | |
| the Free Software Foundation, either version 3 of the License, or
 | |
| (at your option) any later version.
 | |
| 
 | |
| It is distributed in the hope that it will be useful,
 | |
| but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | |
| GNU General Public License for more details.
 | |
| 
 | |
| In addition, as a special exception, the copyright holders give permission
 | |
| to link the code of portions of this program with the OpenSSL library.
 | |
| 
 | |
| Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
 | |
| Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
 | |
| */
 | |
| #include "stdafx.h"
 | |
| #include "ui/widgets/labels.h"
 | |
| 
 | |
| #include "ui/widgets/popup_menu.h"
 | |
| #include "mainwindow.h"
 | |
| #include "lang.h"
 | |
| 
 | |
| namespace Ui {
 | |
| namespace {
 | |
| 
 | |
| TextParseOptions _labelOptions = {
 | |
| 	TextParseMultiline, // flags
 | |
| 	0, // maxw
 | |
| 	0, // maxh
 | |
| 	Qt::LayoutDirectionAuto, // dir
 | |
| };
 | |
| TextParseOptions _labelMarkedOptions = {
 | |
| 	TextParseMultiline | TextParseRichText | TextParseLinks | TextParseHashtags | TextParseMentions | TextParseBotCommands | TextParseMono, // flags
 | |
| 	0, // maxw
 | |
| 	0, // maxh
 | |
| 	Qt::LayoutDirectionAuto, // dir
 | |
| };
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| CrossFadeAnimation::CrossFadeAnimation(const style::color &bg) : _bg(bg) {
 | |
| }
 | |
| 
 | |
| void CrossFadeAnimation::addLine(Part was, Part now) {
 | |
| 	_lines.push_back(Line(std_::move(was), std_::move(now)));
 | |
| }
 | |
| 
 | |
| void CrossFadeAnimation::paintFrame(Painter &p, float64 positionReady, float64 alphaWas, float64 alphaNow) {
 | |
| 	if (_lines.isEmpty()) return;
 | |
| 
 | |
| 	for_const (auto &line, _lines) {
 | |
| 		paintLine(p, line, positionReady, alphaWas, alphaNow);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void CrossFadeAnimation::paintLine(Painter &p, const Line &line, float64 positionReady, float64 alphaWas, float64 alphaNow) {
 | |
| 	auto &snapshotWas = line.was.snapshot;
 | |
| 	auto &snapshotNow = line.now.snapshot;
 | |
| 	t_assert(!snapshotWas.isNull() || !snapshotNow.isNull());
 | |
| 
 | |
| 	auto positionWas = line.was.position;
 | |
| 	auto positionNow = line.now.position;
 | |
| 	auto left = anim::interpolate(positionWas.x(), positionNow.x(), positionReady);
 | |
| 	auto topDelta = (snapshotNow.height() / cIntRetinaFactor()) - (snapshotWas.height() / cIntRetinaFactor());
 | |
| 	auto widthDelta = (snapshotNow.width() / cIntRetinaFactor()) - (snapshotWas.width() / cIntRetinaFactor());
 | |
| 	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() / cIntRetinaFactor(), topDelta, _bg);
 | |
| 		}
 | |
| 	}
 | |
| 	if (widthDelta > 0) {
 | |
| 		p.fillRect(left + (snapshotWas.width() / cIntRetinaFactor()), topNow, widthDelta, snapshotNow.height() / cIntRetinaFactor(), _bg);
 | |
| 	}
 | |
| 
 | |
| 	p.setOpacity(alphaNow);
 | |
| 	if (!snapshotNow.isNull()) {
 | |
| 		p.drawPixmap(left, topNow, snapshotNow);
 | |
| 		if (topDelta < 0) {
 | |
| 			p.fillRect(left, topNow + topDelta, snapshotNow.width() / cIntRetinaFactor(), -topDelta, _bg);
 | |
| 		}
 | |
| 	}
 | |
| 	if (widthDelta < 0) {
 | |
| 		p.fillRect(left + (snapshotNow.width() / cIntRetinaFactor()), topWas, -widthDelta, snapshotWas.height() / cIntRetinaFactor(), _bg);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| LabelSimple::LabelSimple(QWidget *parent, const style::LabelSimple &st, const QString &value) : TWidget(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, const style::TextStyle &tst) : TWidget(parent)
 | |
| , _text(st.width ? st.width : QFIXED_MAX)
 | |
| , _st(st)
 | |
| , _tst(tst)
 | |
| , _contextCopyText(lang(lng_context_copy_text)) {
 | |
| 	init();
 | |
| }
 | |
| 
 | |
| FlatLabel::FlatLabel(QWidget *parent, const QString &text, InitType initType, const style::FlatLabel &st, const style::TextStyle &tst) : TWidget(parent)
 | |
| , _text(st.width ? st.width : QFIXED_MAX)
 | |
| , _st(st)
 | |
| , _tst(tst)
 | |
| , _contextCopyText(lang(lng_context_copy_text)) {
 | |
| 	if (initType == InitType::Rich) {
 | |
| 		setRichText(text);
 | |
| 	} else {
 | |
| 		setText(text);
 | |
| 	}
 | |
| 	init();
 | |
| }
 | |
| 
 | |
| void FlatLabel::init() {
 | |
| 	_trippleClickTimer.setSingleShot(true);
 | |
| 
 | |
| 	_touchSelectTimer.setSingleShot(true);
 | |
| 	connect(&_touchSelectTimer, SIGNAL(timeout()), this, SLOT(onTouchSelect()));
 | |
| }
 | |
| 
 | |
| template <typename SetCallback>
 | |
| void FlatLabel::setTextByCallback(SetCallback callback) {
 | |
| 	textstyleSet(&_tst);
 | |
| 	callback();
 | |
| 	refreshSize();
 | |
| 	textstyleRestore();
 | |
| 	setMouseTracking(_selectable || _text.hasLinks());
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void FlatLabel::setText(const QString &text) {
 | |
| 	setTextByCallback([this, &text]() {
 | |
| 		_text.setText(_st.font, text, _labelOptions);
 | |
| 	});
 | |
| }
 | |
| 
 | |
| void FlatLabel::setRichText(const QString &text) {
 | |
| 	setTextByCallback([this, &text]() {
 | |
| 		_text.setRichText(_st.font, text, _labelOptions);
 | |
| 	});
 | |
| }
 | |
| 
 | |
| void FlatLabel::setMarkedText(const TextWithEntities &textWithEntities) {
 | |
| 	setTextByCallback([this, &textWithEntities]() {
 | |
| 		_text.setMarkedText(_st.font, textWithEntities, _labelMarkedOptions);
 | |
| 	});
 | |
| }
 | |
| 
 | |
| 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::setExpandLinksMode(ExpandLinksMode mode) {
 | |
| 	_contextExpandLinksMode = mode;
 | |
| }
 | |
| 
 | |
| void FlatLabel::setBreakEverywhere(bool breakEverywhere) {
 | |
| 	_breakEverywhere = breakEverywhere;
 | |
| }
 | |
| 
 | |
| int FlatLabel::resizeGetHeight(int newWidth) {
 | |
| 	_allowedWidth = newWidth;
 | |
| 	textstyleSet(&_tst);
 | |
| 	int textWidth = countTextWidth();
 | |
| 	int textHeight = countTextHeight(textWidth);
 | |
| 	textstyleRestore();
 | |
| 	return _st.margin.top() + textHeight + _st.margin.bottom();
 | |
| }
 | |
| 
 | |
| int FlatLabel::naturalWidth() const {
 | |
| 	return _text.maxWidth();
 | |
| }
 | |
| 
 | |
| int FlatLabel::countTextWidth() const {
 | |
| 	return _allowedWidth ? (_allowedWidth - _st.margin.left() - _st.margin.right()) : (_st.width ? _st.width : _text.maxWidth());
 | |
| }
 | |
| 
 | |
| int FlatLabel::countTextHeight(int textWidth) {
 | |
| 	_fullTextHeight = _text.countHeight(textWidth);
 | |
| 	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::setClickHandlerHook(ClickHandlerHook &&hook) {
 | |
| 	_clickHandlerHook = std_::move(hook);
 | |
| }
 | |
| 
 | |
| 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 = App::wnd()->inactivePress();
 | |
| 	if (_dragWasInactive) App::wnd()->inactivePress(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.start(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();
 | |
| 
 | |
| 	ClickHandlerPtr activated = ClickHandler::unpressed();
 | |
| 	if (_dragAction == Dragging) {
 | |
| 		activated.clear();
 | |
| 	} else if (_dragAction == PrepareDrag) {
 | |
| 		_selection = { 0, 0 };
 | |
| 		_savedSelection = { 0, 0 };
 | |
| 		update();
 | |
| 	}
 | |
| 	_dragAction = NoDrag;
 | |
| 	_selectionType = TextSelectType::Letters;
 | |
| 
 | |
| 	if (activated) {
 | |
| 		if (!_clickHandlerHook || _clickHandlerHook(activated, button)) {
 | |
| 			App::activateClickHandler(activated, button);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| #if defined Q_OS_LINUX32 || defined Q_OS_LINUX64
 | |
| 	if (!_selection.empty()) {
 | |
| 		QApplication::clipboard()->setText(_text.originalText(_selection, _contextExpandLinksMode), QClipboard::Selection);
 | |
| 	}
 | |
| #endif // Q_OS_LINUX32 || Q_OS_LINUX64
 | |
| 
 | |
| 	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.start(QApplication::doubleClickInterval());
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::enterEvent(QEvent *e) {
 | |
| 	_lastMousePos = QCursor::pos();
 | |
| 	dragActionUpdate();
 | |
| }
 | |
| 
 | |
| void FlatLabel::leaveEvent(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()) {
 | |
| 			onCopySelectedText();
 | |
| 			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()) {
 | |
| 			QApplication::clipboard()->setText(_text.originalText(selection, _contextExpandLinksMode), QClipboard::FindBuffer);
 | |
| 		}
 | |
| #endif // Q_OS_MAC
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::contextMenuEvent(QContextMenuEvent *e) {
 | |
| 	if (!_selectable) return;
 | |
| 
 | |
| 	showContextMenu(e, ContextMenuReason::FromEvent);
 | |
| }
 | |
| 
 | |
| bool FlatLabel::event(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 QWidget::event(e);
 | |
| }
 | |
| 
 | |
| void FlatLabel::touchEvent(QTouchEvent *e) {
 | |
| 	const Qt::TouchPointStates &states(e->touchPointStates());
 | |
| 	if (e->type() == QEvent::TouchCancel) { // cancel
 | |
| 		if (!_touchInProgress) return;
 | |
| 		_touchInProgress = false;
 | |
| 		_touchSelectTimer.stop();
 | |
| 		_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.start(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;
 | |
| 	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);
 | |
| 	}
 | |
| 	_touchSelectTimer.stop();
 | |
| 	_touchSelect = false;
 | |
| 	break;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::showContextMenu(QContextMenuEvent *e, ContextMenuReason reason) {
 | |
| 	if (_contextMenu) {
 | |
| 		_contextMenu->deleteLater();
 | |
| 		_contextMenu = nullptr;
 | |
| 	}
 | |
| 
 | |
| 	if (e->reason() == QContextMenuEvent::Mouse) {
 | |
| 		_lastMousePos = e->globalPos();
 | |
| 	} else {
 | |
| 		_lastMousePos = QCursor::pos();
 | |
| 	}
 | |
| 	auto state = dragActionUpdate();
 | |
| 
 | |
| 	bool hasSelection = !_selection.empty();
 | |
| 	bool uponSelection = state.uponSymbol && (state.symbol >= _selection.from) && (state.symbol < _selection.to);
 | |
| 	bool fullSelection = _text.isFullSelection(_selection);
 | |
| 	if (reason == ContextMenuReason::FromTouch && hasSelection && !uponSelection) {
 | |
| 		uponSelection = hasSelection;
 | |
| 	}
 | |
| 
 | |
| 	_contextMenu = new Ui::PopupMenu();
 | |
| 
 | |
| 	_contextMenuClickHandler = ClickHandler::getActive();
 | |
| 
 | |
| 	if (fullSelection && !_contextCopyText.isEmpty()) {
 | |
| 		_contextMenu->addAction(_contextCopyText, this, SLOT(onCopyContextText()))->setEnabled(true);
 | |
| 	} else if (uponSelection && !fullSelection) {
 | |
| 		_contextMenu->addAction(lang(lng_context_copy_selected), this, SLOT(onCopySelectedText()))->setEnabled(true);
 | |
| 	} else if (!hasSelection && !_contextCopyText.isEmpty()) {
 | |
| 		_contextMenu->addAction(_contextCopyText, this, SLOT(onCopyContextText()))->setEnabled(true);
 | |
| 	}
 | |
| 
 | |
| 	QString linkCopyToClipboardText = _contextMenuClickHandler ? _contextMenuClickHandler->copyToClipboardContextItemText() : QString();
 | |
| 	if (!linkCopyToClipboardText.isEmpty()) {
 | |
| 		_contextMenu->addAction(linkCopyToClipboardText, this, SLOT(onCopyContextUrl()))->setEnabled(true);
 | |
| 	}
 | |
| 
 | |
| 	if (_contextMenu->actions().isEmpty()) {
 | |
| 		delete _contextMenu;
 | |
| 		_contextMenu = nullptr;
 | |
| 	} else {
 | |
| 		connect(_contextMenu, SIGNAL(destroyed(QObject*)), this, SLOT(onContextMenuDestroy(QObject*)));
 | |
| 		_contextMenu->popup(e->globalPos());
 | |
| 		e->accept();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::onCopySelectedText() {
 | |
| 	auto selection = _selection.empty() ? (_contextMenu ? _savedSelection : _selection) : _selection;
 | |
| 	if (!selection.empty()) {
 | |
| 		QApplication::clipboard()->setText(_text.originalText(selection, _contextExpandLinksMode));
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::onCopyContextText() {
 | |
| 	QApplication::clipboard()->setText(_text.originalText({ 0, 0xFFFF }, _contextExpandLinksMode));
 | |
| }
 | |
| 
 | |
| void FlatLabel::onCopyContextUrl() {
 | |
| 	if (_contextMenuClickHandler) {
 | |
| 		_contextMenuClickHandler->copyToClipboard();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::onTouchSelect() {
 | |
| 	_touchSelect = true;
 | |
| 	dragActionStart(_touchPos, Qt::LeftButton);
 | |
| }
 | |
| 
 | |
| void FlatLabel::onContextMenuDestroy(QObject *obj) {
 | |
| 	if (obj == _contextMenu) {
 | |
| 		_contextMenu = nullptr;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void FlatLabel::onExecuteDrag() {
 | |
| 	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;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	ClickHandlerPtr pressedHandler = ClickHandler::getPressed();
 | |
| 	QString selectedText;
 | |
| 	if (uponSelected) {
 | |
| 		selectedText = _text.originalText(_selection, ExpandLinksAll);
 | |
| 	} else if (pressedHandler) {
 | |
| 		selectedText = pressedHandler->dragText();
 | |
| 	}
 | |
| 	if (!selectedText.isEmpty()) {
 | |
| 		auto mimeData = new QMimeData();
 | |
| 		mimeData->setText(selectedText);
 | |
| 		auto drag = new QDrag(App::wnd());
 | |
| 		drag->setMimeData(mimeData);
 | |
| 		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();
 | |
| }
 | |
| 
 | |
| std_::unique_ptr<CrossFadeAnimation> FlatLabel::CrossFade(FlatLabel *from, FlatLabel *to, const style::color &bg, QPoint fromPosition, QPoint toPosition) {
 | |
| 	auto result = std_::make_unique<CrossFadeAnimation>(bg);
 | |
| 
 | |
| 	struct Data {
 | |
| 		QImage full;
 | |
| 		QVector<int> lineWidths;
 | |
| 		int lineHeight = 0;
 | |
| 		int lineAddTop = 0;
 | |
| 	};
 | |
| 	auto prepareData = [&bg](FlatLabel *label) {
 | |
| 		auto result = Data();
 | |
| 		result.full = QImage(label->size() * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied);
 | |
| 		result.full.setDevicePixelRatio(cRetinaFactor());
 | |
| 		result.full.fill(bg->c);
 | |
| 		Painter(&result.full).drawImage(0, 0, myGrabImage(label));
 | |
| 		auto textWidth = label->width() - label->_st.margin.left() - label->_st.margin.right();
 | |
| 		label->_text.countLineWidths(textWidth, &result.lineWidths);
 | |
| 		result.lineHeight = label->_st.font->height;
 | |
| 		auto addedHeight = (label->_tst.lineHeight - result.lineHeight);
 | |
| 		if (addedHeight > 0) {
 | |
| 			result.lineAddTop = addedHeight / 2;
 | |
| 			result.lineHeight += addedHeight;
 | |
| 		}
 | |
| 		return std_::move(result);
 | |
| 	};
 | |
| 	auto was = prepareData(from);
 | |
| 	auto now = prepareData(to);
 | |
| 
 | |
| 	auto maxLines = qMax(was.lineWidths.size(), now.lineWidths.size());
 | |
| 	auto fillDataTill = [maxLines](Data &data) {
 | |
| 		for (auto i = data.lineWidths.size(); i != maxLines; ++i) {
 | |
| 			data.lineWidths.push_back(-1);
 | |
| 		}
 | |
| 	};
 | |
| 	fillDataTill(was);
 | |
| 	fillDataTill(now);
 | |
| 	auto preparePart = [](FlatLabel *label, QPoint position, Data &data, int index, Data &other) {
 | |
| 		auto result = CrossFadeAnimation::Part();
 | |
| 		auto lineWidth = data.lineWidths[index];
 | |
| 		if (lineWidth < 0) {
 | |
| 			lineWidth = other.lineWidths[index];
 | |
| 		}
 | |
| 		auto fullWidth = data.full.width() / cIntRetinaFactor();
 | |
| 		auto top = index * data.lineHeight + data.lineAddTop;
 | |
| 		auto left = 0;
 | |
| 		if (label->_st.align & Qt::AlignHCenter) {
 | |
| 			left += (fullWidth - lineWidth) / 2;
 | |
| 		} else if (label->_st.align & Qt::AlignRight) {
 | |
| 			left += (fullWidth - lineWidth);
 | |
| 		}
 | |
| 		auto snapshotRect = data.full.rect().intersected(QRect(left * cIntRetinaFactor(), top * cIntRetinaFactor(), lineWidth * cIntRetinaFactor(), label->_st.font->height * cIntRetinaFactor()));
 | |
| 		if (!snapshotRect.isEmpty()) {
 | |
| 			result.snapshot = App::pixmapFromImageInPlace(data.full.copy(snapshotRect));
 | |
| 			result.snapshot.setDevicePixelRatio(cRetinaFactor());
 | |
| 		}
 | |
| 		auto positionBase = position + label->pos();
 | |
| 		result.position = positionBase + QPoint(label->_st.margin.left() + left, label->_st.margin.top() + top);
 | |
| 		return std_::move(result);
 | |
| 	};
 | |
| 	for (int i = 0; i != maxLines; ++i) {
 | |
| 		result->addLine(preparePart(from, fromPosition, was, i, now), preparePart(to, toPosition, now, i, was));
 | |
| 	}
 | |
| 
 | |
| 	return std_::move(result);
 | |
| }
 | |
| 
 | |
| Text::StateResult FlatLabel::dragActionUpdate() {
 | |
| 	auto m = mapFromGlobal(_lastMousePos);
 | |
| 	auto state = getTextState(m);
 | |
| 	updateHover(state);
 | |
| 
 | |
| 	if (_dragAction == PrepareDrag && (m - _dragStartPosition).manhattanLength() >= QApplication::startDragDistance()) {
 | |
| 		_dragAction = Dragging;
 | |
| 		QTimer::singleShot(1, this, SLOT(onExecuteDrag()));
 | |
| 	}
 | |
| 
 | |
| 	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();
 | |
| 
 | |
| 	textstyleSet(&_tst);
 | |
| 	Text::StateResult state;
 | |
| 	bool heightExceeded = _st.maxHeight && (_st.maxHeight < _fullTextHeight || textWidth < _text.maxWidth());
 | |
| 	bool renderElided = _breakEverywhere || heightExceeded;
 | |
| 	if (renderElided) {
 | |
| 		auto lineHeight = qMax(_tst.lineHeight, _st.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.x() - _st.margin.left(), m.y() - _st.margin.top(), textWidth, request);
 | |
| 	} else {
 | |
| 		state = _text.getState(m.x() - _st.margin.left(), m.y() - _st.margin.top(), textWidth, request);
 | |
| 	}
 | |
| 	textstyleRestore();
 | |
| 
 | |
| 	return state;
 | |
| }
 | |
| 
 | |
| void FlatLabel::setOpacity(float64 o) {
 | |
| 	_opacity = o;
 | |
| 	update();
 | |
| }
 | |
| 
 | |
| void FlatLabel::paintEvent(QPaintEvent *e) {
 | |
| 	Painter p(this);
 | |
| 	p.setOpacity(_opacity);
 | |
| 	p.setPen(_st.textFg);
 | |
| 	textstyleSet(&_tst);
 | |
| 	int textWidth = width() - _st.margin.left() - _st.margin.right();
 | |
| 	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(_tst.lineHeight, _st.font->height);
 | |
| 		auto lines = _st.maxHeight ? qMax(_st.maxHeight / lineHeight, 1) : ((height() / lineHeight) + 2);
 | |
| 		_text.drawElided(p, _st.margin.left(), _st.margin.top(), textWidth, lines, _st.align, e->rect().y(), e->rect().bottom(), 0, _breakEverywhere, selection);
 | |
| 	} else {
 | |
| 		_text.draw(p, _st.margin.left(), _st.margin.top(), textWidth, _st.align, e->rect().y(), e->rect().bottom(), selection);
 | |
| 	}
 | |
| 	textstyleRestore();
 | |
| }
 | |
| 
 | |
| } // namespace Ui
 | 
