/* 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 "iv/markdown/iv_markdown_prepare_native_richtext.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 #include #include #include #include #include #include #include #include #include #include #include #include namespace Iv::Markdown { namespace { void EnsureBlockquotePaintCache( std::unique_ptr &cache, const style::color &color) { if (cache) { return; } cache = std::make_unique(); 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 &cache, const style::color &color) { if (cache) { return; } cache = std::make_unique(); 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, }; } [[nodiscard]] std::vector HighlightColors( not_null palette) { auto result = Ui::SyntaxHighlightColors(palette); const auto &fg = palette->lightButtonFg(); const auto &bg = palette->lightButtonBgOver(); result.push_back({ &fg->p, &fg->p, &bg->b, &bg->b }); Ensures(result.size() == kNativeIvLinkSpecialColorIndex); return result; } } // namespace MarkdownDocumentWidget::MarkdownDocumentWidget(QWidget *parent) : Ui::RpWidget(parent) , _highlightColors(HighlightColors(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() { if (_article && (_article->mediaBlockHost() == this)) { _article->setTextRepaintCallbacks(nullptr, nullptr); _article->setMediaBlockHost(nullptr); } } void MarkdownDocumentWidget::setLinkActivationCallback( std::function callback) { _activateLink = std::move(callback); } void MarkdownDocumentWidget::setMediaActivationCallback( std::function callback) { _activateMedia = std::move(callback); } void MarkdownDocumentWidget::setClickHandlerContext( QVariant context, std::shared_ptr contextRef) { _clickHandlerContext = std::move(context); _clickHandlerContextRef = std::move(contextRef); } void MarkdownDocumentWidget::setArticle( std::shared_ptr article) { ClickHandler::clearActive(this); applyCursor(style::cur_default); if (_article && (_article->mediaBlockHost() == this)) { _article->setTextRepaintCallbacks(nullptr, nullptr); _article->setMediaBlockHost(nullptr); } _article = std::move(article); if (_article) { const auto weak = QPointer(this); _article->setTextRepaintCallbacks( [=] { if (weak) { weak->requestRepaint(QRect()); } }, [=](QRect articleRect) { if (weak) { weak->requestRepaint(articleRect); } }); _article->setMediaBlockHost(this); } _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 = HighlightColors(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::maxWidth() const { return _article ? std::max(int(std::ceil(_article->maxWidth() * zoomScale())), 1) : 1; } 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::requestRepaint(QRect articleRect) { crl::on_main(this, [=] { if (!_article) { return; } if (articleRect.isEmpty()) { update(); } else { update(articleRectToWidget(articleRect)); } }); } void MarkdownDocumentWidget::requestRelayout(QRect articleRect) { crl::on_main(this, [=] { if (!_article) { return; } _article->invalidateLayout(); const auto previousHeight = height(); 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(); _lastRelayoutMs = int(timer.elapsed()); const auto newHeight = std::max(int(std::ceil(articleHeight * scale)), 1); if (previousHeight != newHeight) { resize(width(), newHeight); update(); } else if (articleRect.isEmpty()) { update(); } else { update(articleRectToWidget(articleRect)); } updateHoverAtCursor(); }); } void MarkdownDocumentWidget::setPlaceholderLoading( PreparedPlaceholderBlockId id) { if (_article) { _article->setPlaceholderLoading(id); } } void MarkdownDocumentWidget::clearPlaceholderLoading( PreparedPlaceholderBlockId id) { if (_article) { _article->clearPlaceholderLoading(id); } } void MarkdownDocumentWidget::clearAllPlaceholderLoading() { if (_article) { _article->clearAllPlaceholderLoading(); } } void MarkdownDocumentWidget::addPlaceholderRipple( PreparedPlaceholderBlockId id, QPoint point) { if (_article) { _article->addPlaceholderRipple(id, point); } } void MarkdownDocumentWidget::stopPlaceholderRipple( PreparedPlaceholderBlockId id) { if (_article) { _article->stopPlaceholderRipple(id); } } 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( 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::InstantViewPage: 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) { const auto weak = base::make_weak(this); dragActionFinish(e->pos(), e->button()); if (weak && !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) { stopPressedPlaceholderRipple(); 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(); if (!context.show) { return clickHandlerContext(); } auto sanitized = context; sanitized.sessionWindow = base::weak_ptr(); return QVariant::fromValue(sanitized); } void MarkdownDocumentWidget::showToast(const QString &text) const { const auto context = clickHandlerContext().value(); 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; } QRect MarkdownDocumentWidget::articleRectToWidget(QRect articleRect) const { if (articleRect.isEmpty()) { return rect(); } const auto scale = zoomScale(); const auto left = int(std::floor(articleRect.x() * scale)); const auto top = int(std::floor(articleRect.y() * scale)); const auto right = int(std::ceil( (articleRect.x() + articleRect.width()) * scale)); const auto bottom = int(std::ceil( (articleRect.y() + articleRect.height()) * scale)); return QRect( left, top, std::max(right - left, 1), std::max(bottom - top, 1)); } 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(); }); }, .repaintRect = [=](QRect articleRect) { crl::on_main(this, [=] { if (articleRect.isEmpty()) { update(); } else { update(articleRectToWidget(articleRect)); } }); }, }; } void MarkdownDocumentWidget::stopPressedPlaceholderRipple() { if (_pressedPlaceholderId) { stopPlaceholderRipple(_pressedPlaceholderId); _pressedPlaceholderId = {}; } } void MarkdownDocumentWidget::dragActionStart( QPoint point, Qt::MouseButton button) { stopPressedPlaceholderRipple(); const auto state = hitTest( point, Ui::Text::StateRequest::Flag::LookupLink | Ui::Text::StateRequest::Flag::LookupSymbol); updateHover(state); if (button != Qt::LeftButton) { return; } if (state.mediaActivation.kind == MediaActivationKind::Embed && state.mediaActivation.placeholderId) { _pressedPlaceholderId = state.mediaActivation.placeholderId; addPlaceholderRipple( state.mediaActivation.placeholderId, state.placeholderLocalPoint); } _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); stopPressedPlaceholderRipple(); 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( activated); const auto pre = dynamic_cast( activated.get()); if (monospace || pre) { clickHandlerContext = viewerToastClickHandlerContext(); } if (monospace) { const auto context = clickHandlerContext.value(); if (context.show) { const auto handled = Ui::Integration::Instance().copyPreOnClick( clickHandlerContext); static_cast(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