Files
AyuGramDesktop/Telegram/SourceFiles/iv/markdown/iv_markdown_view_widget.cpp
T
2026-05-19 12:49:08 +04:00

914 lines
25 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "iv/markdown/iv_markdown_view_widget.h"
#include "base/weak_ptr.h"
#include "core/click_handler_types.h"
#include "core/credits_amount.h"
#include "core/file_utilities.h"
#include "iv/markdown/iv_markdown_article_text.h"
#include "lang/lang_keys.h"
#include "spellcheck/spellcheck_highlight_syntax.h"
#include "ui/chat/chat_style.h"
#include "ui/layers/show.h"
#include "ui/text/text_extended_data.h"
#include "ui/widgets/popup_menu.h"
#include "ui/color_contrast.h"
#include "ui/integration.h"
#include "styles/palette.h"
#include "styles/style_chat.h"
#include "styles/style_iv.h"
#include "styles/style_layers.h"
#include "styles/style_menu_icons.h"
#include <QtCore/QElapsedTimer>
#include <QtGui/QClipboard>
#include <QtGui/QContextMenuEvent>
#include <QtGui/QCursor>
#include <QtGui/QGuiApplication>
#include <QtGui/QKeyEvent>
#include <QtGui/QKeySequence>
#include <QtGui/QMouseEvent>
#include <QtWidgets/QApplication>
#include <algorithm>
#include <cmath>
#include <utility>
namespace Iv::Markdown {
namespace {
void EnsureBlockquotePaintCache(
std::unique_ptr<Ui::Text::QuotePaintCache> &cache,
const style::color &color) {
if (cache) {
return;
}
cache = std::make_unique<Ui::Text::QuotePaintCache>();
cache->bg = color->c;
cache->bg.setAlpha(Ui::kDefaultBgOpacity * 255);
cache->outlines[0] = color->c;
cache->outlines[0].setAlpha(Ui::kDefaultOutline1Opacity * 255);
cache->outlines[1] = cache->outlines[2] = QColor(0, 0, 0, 0);
cache->header = color->c;
cache->header.setAlpha(Ui::kDefaultOutline2Opacity * 255);
cache->icon = color->c;
cache->icon.setAlpha(Ui::kDefaultOutline3Opacity * 255);
}
[[nodiscard]] bool UseDarkPrePaintBackground() {
const auto withBg = [](const QColor &color) {
return Ui::CountContrast(st::windowBg->c, color);
};
return withBg({ 0, 0, 0 }) < withBg({ 255, 255, 255 });
}
void EnsurePrePaintCache(
std::unique_ptr<Ui::Text::QuotePaintCache> &cache,
const style::color &color) {
if (cache) {
return;
}
cache = std::make_unique<Ui::Text::QuotePaintCache>();
if (UseDarkPrePaintBackground()) {
cache->bg = QColor(0, 0, 0, 192);
} else {
cache->bg = color->c;
cache->bg.setAlpha(Ui::kDefaultBgOpacity * 255);
}
cache->outlines[0] = color->c;
cache->outlines[0].setAlpha(Ui::kDefaultOutline1Opacity * 255);
cache->outlines[1] = cache->outlines[2] = QColor(0, 0, 0, 0);
cache->header = color->c;
cache->header.setAlpha(Ui::kDefaultOutline2Opacity * 255);
cache->icon = color->c;
cache->icon.setAlpha(Ui::kDefaultOutline3Opacity * 255);
}
[[nodiscard]] int CompareSelectionPositions(
MarkdownArticleSelectionPosition a,
MarkdownArticleSelectionPosition b) {
if (a.segment != b.segment) {
return (a.segment < b.segment) ? -1 : 1;
}
if (a.offset != b.offset) {
return (a.offset < b.offset) ? -1 : 1;
}
return 0;
}
[[nodiscard]] MarkdownArticleSelection NormalizeSelection(
MarkdownArticleSelection selection) {
if (selection.empty()) {
return {};
}
if (CompareSelectionPositions(selection.from, selection.to) > 0) {
std::swap(selection.from, selection.to);
}
return selection;
}
[[nodiscard]] MarkdownArticleSelectionEndpoint MakeSelectionEndpoint(
const MarkdownArticleHitTestResult &result) {
return {
.segment = result.segmentIndex,
.direct = result.direct,
};
}
} // namespace
MarkdownDocumentWidget::MarkdownDocumentWidget(QWidget *parent)
: Ui::RpWidget(parent)
, _highlightColors(Ui::SyntaxHighlightColors(style::main_palette::get())) {
setMouseTracking(true);
setFocusPolicy(Qt::StrongFocus);
Spellchecker::HighlightReady(
) | rpl::on_next([=](Spellchecker::HighlightProcessId processId) {
if (_article && _article->highlightProcessDone(processId)) {
update();
}
}, _highlightReadyLifetime);
}
MarkdownDocumentWidget::~MarkdownDocumentWidget() = default;
void MarkdownDocumentWidget::setLinkActivationCallback(
std::function<void(const PreparedLink &, Qt::MouseButton)> callback) {
_activateLink = std::move(callback);
}
void MarkdownDocumentWidget::setMediaActivationCallback(
std::function<bool(const MediaActivation &, Qt::MouseButton)> callback) {
_activateMedia = std::move(callback);
}
void MarkdownDocumentWidget::setClickHandlerContext(
QVariant context,
std::shared_ptr<QVariant> contextRef) {
_clickHandlerContext = std::move(context);
_clickHandlerContextRef = std::move(contextRef);
}
void MarkdownDocumentWidget::setArticle(
std::shared_ptr<MarkdownArticle> article) {
ClickHandler::clearActive(this);
applyCursor(style::cur_default);
_article = std::move(article);
_lastRelayoutMs = 0;
resetTextPaintCaches();
resetSelection();
forceRelayoutCurrentWidth();
}
void MarkdownDocumentWidget::setZoom(int value) {
value = (value > 0) ? value : 100;
if (_zoom == value) {
return;
}
_zoom = value;
clearSelection();
forceRelayoutCurrentWidth();
}
void MarkdownDocumentWidget::refreshPalette() {
ClickHandler::clearActive(this);
applyCursor(style::cur_default);
_highlightColors = Ui::SyntaxHighlightColors(style::main_palette::get());
resetTextPaintCaches();
if (_article) {
_article->invalidatePaletteCache();
}
update();
}
void MarkdownDocumentWidget::invalidateRasterCache() {
ClickHandler::clearActive(this);
applyCursor(style::cur_default);
if (_article) {
_article->invalidateRasterCache();
}
relayoutCurrentWidth(false);
update();
}
int MarkdownDocumentWidget::anchorTop(const QString &anchorId) const {
const auto top = _article ? _article->anchorTop(anchorId) : -1;
if (top < 0) {
return -1;
}
return int(std::floor(top * zoomScale()));
}
bool MarkdownDocumentWidget::toggleDetails(const QString &anchorId) {
if (!_article || !_article->toggleDetails(anchorId)) {
return false;
}
clearSelection();
forceRelayoutCurrentWidth();
updateHoverAtCursor();
return true;
}
int MarkdownDocumentWidget::lastRelayoutMs() const {
return _lastRelayoutMs;
}
int MarkdownDocumentWidget::resizeGetHeight(int newWidth) {
ClickHandler::clearActive(this);
applyCursor(style::cur_default);
clearSelection();
if (!_article) {
return 1;
}
const auto scale = zoomScale();
const auto layoutWidth = std::max(int(std::floor(newWidth / scale)), 1);
auto timer = QElapsedTimer();
timer.start();
const auto layoutHeight = _article->resizeGetHeight(layoutWidth);
syncArticleVisibleTopBottom();
_lastRelayoutMs = int(timer.elapsed());
return std::max(int(std::ceil(layoutHeight * scale)), 1);
}
void MarkdownDocumentWidget::paintEvent(QPaintEvent *e) {
if (!_article) {
return;
}
auto p = Painter(this);
p.setTextPalette(st::inTextPalette);
const auto caches = textPaintCaches();
const auto scale = zoomScale();
if (scale == 1.) {
_article->paint(
p,
e->rect(),
caches,
_selection,
&_selectionEndpoints);
return;
}
const auto clip = QRect(
int(std::floor(e->rect().x() / scale)),
int(std::floor(e->rect().y() / scale)),
int(std::ceil(e->rect().width() / scale)) + 1,
int(std::ceil(e->rect().height() / scale)) + 1);
p.save();
p.scale(scale, scale);
_article->paint(
p,
clip,
caches,
_selection,
&_selectionEndpoints);
p.restore();
}
void MarkdownDocumentWidget::visibleTopBottomUpdated(
int visibleTop,
int visibleBottom) {
_visibleRange = Ui::VisibleRange{
.top = visibleTop,
.bottom = visibleBottom,
};
syncArticleVisibleTopBottom();
}
void MarkdownDocumentWidget::keyPressEvent(QKeyEvent *e) {
if (e == QKeySequence::Copy && !selectionForCopy().empty()) {
copySelectedText();
return;
}
Ui::RpWidget::keyPressEvent(e);
}
void MarkdownDocumentWidget::contextMenuEvent(QContextMenuEvent *e) {
const auto globalPoint = (e->reason() == QContextMenuEvent::Mouse)
? e->globalPos()
: QCursor::pos();
const auto localPoint = (e->reason() == QContextMenuEvent::Mouse)
? e->pos()
: mapFromGlobal(globalPoint);
const auto state = hitTest(
localPoint,
Ui::Text::StateRequest::Flag::LookupLink
| Ui::Text::StateRequest::Flag::LookupSymbol);
const auto selection = selectionForCopy();
const auto uponSelection = !selection.empty()
&& ((e->reason() != QContextMenuEvent::Mouse)
|| selectionContains(selection, state));
const auto contextText = uponSelection
? TextForMimeData()
: (_article ? _article->textForContext(state) : TextForMimeData());
const auto link = state.preparedLink;
_contextMenu = base::make_unique_q<Ui::PopupMenu>(
this,
st::popupMenuWithIcons);
if (uponSelection) {
_contextMenu->addAction(
Ui::Integration::Instance().phraseContextCopySelected(),
[=] { copySelectedText(); },
&st::menuIconCopy);
} else if (!contextText.empty()) {
_contextMenu->addAction(
tr::lng_context_copy_text(tr::now),
[text = contextText, this] {
TextUtilities::SetClipboardText(text);
showToast(tr::lng_text_copied(tr::now));
},
&st::menuIconCopy);
}
if (link) {
const auto handler = CreatePreparedLinkHandler(*link);
const auto copyText = handler ? handler->copyToClipboardText() : QString();
const auto copyLabel = handler
? handler->copyToClipboardContextItemText()
: QString();
if (!copyText.isEmpty() && !copyLabel.isEmpty()) {
_contextMenu->addAction(
copyLabel,
[text = copyText] {
QGuiApplication::clipboard()->setText(text);
},
&st::menuIconCopy);
}
switch (link->kind) {
case PreparedLinkKind::RejectedRelative:
case PreparedLinkKind::ToggleDetails:
break;
case PreparedLinkKind::External:
case PreparedLinkKind::Anchor:
case PreparedLinkKind::Footnote:
case PreparedLinkKind::FootnoteBacklink:
case PreparedLinkKind::LocalFile:
_contextMenu->addAction(
tr::lng_open_link(tr::now),
[=, prepared = *link] {
if (_activateLink) {
_activateLink(prepared, Qt::LeftButton);
}
},
&st::menuIconAddress);
break;
}
}
if (_contextMenu->empty()) {
_contextMenu = nullptr;
return;
}
_contextMenu->popup(globalPoint);
e->accept();
}
void MarkdownDocumentWidget::mouseMoveEvent(QMouseEvent *e) {
dragActionUpdate(e->pos());
}
void MarkdownDocumentWidget::mousePressEvent(QMouseEvent *e) {
if (e->button() == Qt::LeftButton) {
dragActionStart(e->pos(), e->button());
return;
}
updateHover(hitTest(
e->pos(),
Ui::Text::StateRequest::Flag::LookupLink
| Ui::Text::StateRequest::Flag::LookupSymbol));
if (e->button() == Qt::MiddleButton) {
ClickHandler::pressed();
}
}
void MarkdownDocumentWidget::mouseReleaseEvent(QMouseEvent *e) {
dragActionFinish(e->pos(), e->button());
if (!rect().contains(e->pos())) {
ClickHandler::clearActive(this);
applyCursor(style::cur_default);
}
}
void MarkdownDocumentWidget::mouseDoubleClickEvent(QMouseEvent *e) {
dragActionStart(e->pos(), e->button());
if (_dragAction != Selecting || _selectionType != TextSelectType::Letters) {
return;
}
const auto state = hitTest(
e->pos(),
Ui::Text::StateRequest::Flag::LookupLink
| Ui::Text::StateRequest::Flag::LookupSymbol);
if (!_article
|| !_article->segmentIsText(state.segmentIndex)
|| !state.direct
|| !state.state.uponSymbol) {
return;
}
_dragSegment = state.segmentIndex;
_dragSymbol = selectionOffsetFromHit(state);
_selectionType = TextSelectType::Words;
_selection = selectionFromHit(state);
_savedSelection = {};
_selectionEndpoints = {
.from = MakeSelectionEndpoint(state),
.to = MakeSelectionEndpoint(state),
};
_savedSelectionEndpoints = {};
if (_selection.from.segment == _dragSegment
&& _selection.to.segment == _dragSegment) {
_dragExpandedSelection = TextSelection(
uint16(_selection.from.offset),
uint16(_selection.to.offset));
}
setFocus();
updateHover(state);
update();
}
void MarkdownDocumentWidget::focusOutEvent(QFocusEvent *e) {
if (!_selection.empty()) {
_savedSelection = _selection;
_savedSelectionEndpoints = _selectionEndpoints;
_selection = {};
_selectionEndpoints = {};
update();
}
ClickHandler::clearActive(this);
applyCursor(style::cur_default);
Ui::RpWidget::focusOutEvent(e);
}
void MarkdownDocumentWidget::focusInEvent(QFocusEvent *e) {
if (!_savedSelection.empty()) {
_selection = _savedSelection;
_selectionEndpoints = _savedSelectionEndpoints;
_savedSelection = {};
_savedSelectionEndpoints = {};
update();
}
Ui::RpWidget::focusInEvent(e);
}
void MarkdownDocumentWidget::leaveEventHook(QEvent *e) {
ClickHandler::clearActive(this);
applyCursor((_dragAction == Selecting)
? style::cur_text
: style::cur_default);
Ui::RpWidget::leaveEventHook(e);
}
void MarkdownDocumentWidget::clickHandlerActiveChanged(
const ClickHandlerPtr &,
bool) {
update();
}
void MarkdownDocumentWidget::clickHandlerPressedChanged(
const ClickHandlerPtr &,
bool) {
update();
}
ClickHandlerPtr MarkdownDocumentWidget::linkAt(QPoint point) const {
return hitTest(
point,
Ui::Text::StateRequest::Flag::LookupLink
| Ui::Text::StateRequest::Flag::LookupSymbol).state.link;
}
MarkdownArticleHitTestResult MarkdownDocumentWidget::hitTest(
QPoint point,
Ui::Text::StateRequest::Flags flags) const {
if (!_article) {
return {};
}
const auto scale = zoomScale();
if (scale != 1.) {
point = QPoint(
int(std::floor(point.x() / scale)),
int(std::floor(point.y() / scale)));
}
return _article->hitTest(point, flags);
}
MarkdownArticleSelection MarkdownDocumentWidget::selectionForCopy() const {
return !_selection.empty()
? _selection
: _contextMenu
? _savedSelection
: MarkdownArticleSelection();
}
MarkdownArticleSelectionEndpoints MarkdownDocumentWidget::selectionEndpointsForCopy() const {
return !_selection.empty()
? _selectionEndpoints
: _contextMenu
? _savedSelectionEndpoints
: MarkdownArticleSelectionEndpoints();
}
bool MarkdownDocumentWidget::selectionContains(
MarkdownArticleSelection selection,
const MarkdownArticleHitTestResult &result) const {
const auto endpoints = selectionEndpointsForCopy();
return _article
? _article->selectionContains(
selection,
&endpoints,
result)
: false;
}
int MarkdownDocumentWidget::selectionOffsetFromHit(
const MarkdownArticleHitTestResult &result) const {
return _article
? _article->selectionOffsetFromHit(result, _selectionType)
: 0;
}
MarkdownArticleSelection MarkdownDocumentWidget::selectionFromHit(
const MarkdownArticleHitTestResult &result) const {
if (!_article || _dragSegment < 0 || !result.valid()) {
return {};
}
auto first = _dragSymbol;
auto second = selectionOffsetFromHit(result);
if (_selectionType != TextSelectType::Letters
&& !_dragExpandedSelection.empty()
&& result.segmentIndex != _dragSegment) {
first = (CompareSelectionPositions(
MarkdownArticleSelectionPosition{ result.segmentIndex, second },
MarkdownArticleSelectionPosition{ _dragSegment, _dragSymbol }) < 0)
? _dragExpandedSelection.to
: _dragExpandedSelection.from;
}
if (result.segmentIndex == _dragSegment
&& _article->segmentIsText(_dragSegment)) {
const auto adjusted = _article->adjustSelection(
_dragSegment,
TextSelection(
uint16(std::min(first, second)),
uint16(std::max(first, second))),
_selectionType);
return {
{ _dragSegment, adjusted.from },
{ _dragSegment, adjusted.to },
};
}
return NormalizeSelection({
{ _dragSegment, first },
{ result.segmentIndex, second },
});
}
TextForMimeData MarkdownDocumentWidget::getSelectedText() const {
const auto endpoints = selectionEndpointsForCopy();
return _article
? _article->textForSelection(
selectionForCopy(),
&endpoints)
: TextForMimeData();
}
QVariant MarkdownDocumentWidget::clickHandlerContext() const {
return _clickHandlerContextRef
? *_clickHandlerContextRef
: _clickHandlerContext;
}
QVariant MarkdownDocumentWidget::viewerToastClickHandlerContext() const {
const auto context = clickHandlerContext().value<ClickHandlerContext>();
if (!context.show) {
return clickHandlerContext();
}
auto sanitized = context;
sanitized.sessionWindow = base::weak_ptr<Window::SessionController>();
return QVariant::fromValue(sanitized);
}
void MarkdownDocumentWidget::showToast(const QString &text) const {
const auto context = clickHandlerContext().value<ClickHandlerContext>();
if (context.show) {
context.show->showToast(text);
}
}
void MarkdownDocumentWidget::copySelectedText() {
if (const auto text = getSelectedText(); !text.empty()) {
TextUtilities::SetClipboardText(text);
showToast(tr::lng_text_copied(tr::now));
}
}
void MarkdownDocumentWidget::syncArticleVisibleTopBottom() {
if (!_article) {
return;
}
const auto scale = zoomScale();
_article->setVisibleTopBottom(
int(std::floor(_visibleRange.top / scale)),
int(std::ceil(_visibleRange.bottom / scale)));
}
void MarkdownDocumentWidget::relayoutCurrentWidth(bool clearSelection) {
if (clearSelection) {
this->clearSelection();
}
if (!_article) {
_lastRelayoutMs = 0;
return;
}
const auto scale = zoomScale();
const auto layoutWidth = std::max(int(std::floor(width() / scale)), 1);
auto timer = QElapsedTimer();
timer.start();
const auto articleHeight = _article->resizeGetHeight(layoutWidth);
syncArticleVisibleTopBottom();
(void)articleHeight;
_lastRelayoutMs = int(timer.elapsed());
}
void MarkdownDocumentWidget::forceRelayoutCurrentWidth() {
resizeToWidth(width());
update();
}
void MarkdownDocumentWidget::updateHover(
const MarkdownArticleHitTestResult &state) {
const auto changed = ClickHandler::setActive(state.state.link, this);
auto cursor = style::cur_default;
if (_dragAction == NoDrag) {
if (state.state.link
|| (state.preparedLink
&& state.preparedLink->kind == PreparedLinkKind::ToggleDetails)
|| state.mediaActivation.kind != MediaActivationKind::None) {
cursor = style::cur_pointer;
} else if (state.direct) {
cursor = style::cur_text;
}
} else {
if (_dragAction == Selecting) {
const auto selection = selectionFromHit(state);
const auto endpoints = MarkdownArticleSelectionEndpoints{
.from = _selectionEndpoints.from.valid()
? _selectionEndpoints.from
: MarkdownArticleSelectionEndpoint{ _dragSegment, false },
.to = MakeSelectionEndpoint(state),
};
const auto endpointsChanged
= (_selectionEndpoints.from.segment != endpoints.from.segment)
|| (_selectionEndpoints.from.direct != endpoints.from.direct)
|| (_selectionEndpoints.to.segment != endpoints.to.segment)
|| (_selectionEndpoints.to.direct != endpoints.to.direct);
if (_selection != selection || endpointsChanged) {
_selection = selection;
_selectionEndpoints = endpoints;
_savedSelection = {};
_savedSelectionEndpoints = {};
setFocus();
update();
} else {
_selectionEndpoints = endpoints;
}
cursor = style::cur_text;
} else if (ClickHandler::getPressed()) {
cursor = style::cur_pointer;
}
}
if (changed || cursor != _cursor) {
applyCursor(cursor);
}
}
void MarkdownDocumentWidget::updateHoverAtCursor() {
const auto point = mapFromGlobal(QCursor::pos());
if (rect().contains(point)) {
updateHover(hitTest(
point,
Ui::Text::StateRequest::Flag::LookupLink
| Ui::Text::StateRequest::Flag::LookupSymbol));
} else {
ClickHandler::clearActive(this);
applyCursor(style::cur_default);
}
}
void MarkdownDocumentWidget::resetSelection() {
_selection = {};
_savedSelection = {};
_selectionEndpoints = {};
_savedSelectionEndpoints = {};
_selectionType = TextSelectType::Letters;
_dragAction = NoDrag;
_dragStartPosition = QPoint();
_dragSegment = -1;
_dragSymbol = 0;
_dragExpandedSelection = {};
_selectionClickPreparedLink = std::nullopt;
_dragStartHadSelection = false;
}
void MarkdownDocumentWidget::clearSelection() {
const auto hadSelection = !_selection.empty()
|| !_savedSelection.empty()
|| (_dragAction != NoDrag);
resetSelection();
if (hadSelection) {
update();
}
}
void MarkdownDocumentWidget::resetTextPaintCaches() {
_prePaintCache = nullptr;
_blockquotePaintCache = nullptr;
}
Ui::Text::QuotePaintCache *MarkdownDocumentWidget::ensurePrePaintCache() {
EnsurePrePaintCache(_prePaintCache, st::inTextPalette.monoFg);
return _prePaintCache.get();
}
Ui::Text::QuotePaintCache *MarkdownDocumentWidget::ensureBlockquotePaintCache() {
EnsureBlockquotePaintCache(
_blockquotePaintCache,
st::defaultMarkdown.quotePaintColors.blockquote);
return _blockquotePaintCache.get();
}
MarkdownArticlePaintCaches MarkdownDocumentWidget::textPaintCaches() {
return {
.pre = ensurePrePaintCache(),
.blockquote = ensureBlockquotePaintCache(),
.colors = _highlightColors,
.repaint = [=] {
crl::on_main(this, [=] {
update();
});
},
};
}
void MarkdownDocumentWidget::dragActionStart(
QPoint point,
Qt::MouseButton button) {
const auto state = hitTest(
point,
Ui::Text::StateRequest::Flag::LookupLink
| Ui::Text::StateRequest::Flag::LookupSymbol);
updateHover(state);
if (button != Qt::LeftButton) {
return;
}
_dragStartPosition = point;
_dragStartHadSelection = !selectionForCopy().empty();
_selectionClickPreparedLink = (state.preparedLink
&& state.preparedLink->kind == PreparedLinkKind::ToggleDetails)
? state.preparedLink
: std::nullopt;
ClickHandler::pressed();
_dragAction = NoDrag;
_dragExpandedSelection = {};
_dragSegment = -1;
_dragSymbol = 0;
if (ClickHandler::getPressed()) {
_dragStartPosition = point;
_dragAction = PrepareDrag;
return;
}
if (!state.valid()) {
clearSelection();
return;
}
_dragSegment = state.segmentIndex;
_dragSymbol = selectionOffsetFromHit(state);
_selection = {
{ _dragSegment, _dragSymbol },
{ _dragSegment, _dragSymbol },
};
_savedSelection = {};
_selectionEndpoints = {
.from = MakeSelectionEndpoint(state),
.to = MakeSelectionEndpoint(state),
};
_savedSelectionEndpoints = {};
_dragAction = Selecting;
update();
}
MarkdownArticleHitTestResult MarkdownDocumentWidget::dragActionUpdate(QPoint point) {
const auto state = hitTest(
point,
Ui::Text::StateRequest::Flag::LookupLink
| Ui::Text::StateRequest::Flag::LookupSymbol);
if (_dragAction == PrepareDrag
&& (point - _dragStartPosition).manhattanLength()
>= QApplication::startDragDistance()) {
_dragAction = Dragging;
}
updateHover(state);
return state;
}
MarkdownArticleHitTestResult MarkdownDocumentWidget::dragActionFinish(
QPoint point,
Qt::MouseButton button) {
const auto state = dragActionUpdate(point);
auto activated = ClickHandler::unpressed();
const auto dragStartHadSelection = _dragStartHadSelection;
const auto toggleFromDetailsClick = !dragStartHadSelection
&& _selection.empty()
&& _selectionClickPreparedLink
&& (point - _dragStartPosition).manhattanLength()
< QApplication::startDragDistance()
&& state.preparedLink
&& state.preparedLink->kind == PreparedLinkKind::ToggleDetails
&& state.preparedLink->target == _selectionClickPreparedLink->target;
if (_dragAction == Dragging
|| (_dragAction == Selecting && !_selection.empty())) {
activated = nullptr;
} else if (_dragAction == PrepareDrag && button != Qt::RightButton) {
clearSelection();
}
const auto preparedToggle = toggleFromDetailsClick
? state.preparedLink
: std::nullopt;
_dragStartHadSelection = false;
_dragAction = NoDrag;
_selectionType = TextSelectType::Letters;
_dragExpandedSelection = {};
updateHover(state);
if (activated
&& (button == Qt::LeftButton || button == Qt::MiddleButton)) {
if (state.mediaActivation.kind != MediaActivationKind::None
&& _activateMedia
&& _activateMedia(state.mediaActivation, button)) {
return state;
}
if (state.preparedLink && _activateLink) {
if (state.preparedLink->kind == PreparedLinkKind::ToggleDetails
&& dragStartHadSelection) {
return state;
}
_activateLink(*state.preparedLink, button);
} else {
auto clickHandlerContext = this->clickHandlerContext();
const auto monospace = std::dynamic_pointer_cast<MonospaceClickHandler>(
activated);
const auto pre = dynamic_cast<Ui::Text::PreClickHandler*>(
activated.get());
if (monospace || pre) {
clickHandlerContext = viewerToastClickHandlerContext();
}
if (monospace) {
const auto context = clickHandlerContext.value<ClickHandlerContext>();
if (context.show) {
const auto handled = Ui::Integration::Instance().copyPreOnClick(
clickHandlerContext);
static_cast<void>(handled);
}
}
auto context = ClickContext();
context.button = button;
context.other = std::move(clickHandlerContext);
ActivateClickHandler(window(), activated, context);
}
} else if (preparedToggle
&& (button == Qt::LeftButton || button == Qt::MiddleButton)
&& _activateLink) {
clearSelection();
_activateLink(*preparedToggle, button);
return state;
} else if ((button == Qt::LeftButton || button == Qt::MiddleButton)
&& state.mediaActivation.kind != MediaActivationKind::None
&& _activateMedia
&& _activateMedia(state.mediaActivation, button)) {
return state;
}
if (QGuiApplication::clipboard()->supportsSelection()
&& !_selection.empty()) {
if (const auto text = getSelectedText(); !text.empty()) {
TextUtilities::SetClipboardText(text, QClipboard::Selection);
}
}
return state;
}
void MarkdownDocumentWidget::applyCursor(style::cursor cursor) {
if (_cursor != cursor) {
_cursor = cursor;
setCursor(_cursor);
}
}
double MarkdownDocumentWidget::zoomScale() const {
return std::max(_zoom, 1) / 100.;
}
} // namespace Iv::Markdown