66#include < QImage>
77#include < QScrollBar>
88#include < QTextDocument>
9+ #include < QTextBlock>
10+ #include < QTextBlockFormat>
11+ #include < QAbstractTextDocumentLayout>
12+ #include < QTextFrame>
913#include < QtConcurrent>
1014#include < QKeyEvent>
15+ #include < QMouseEvent>
16+ #include < QToolButton>
17+ #include < QGuiApplication>
18+ #include < QClipboard>
1119
1220MarkdownPreviewWidget::MarkdownPreviewWidget (NotepadNextApplication *app, QWidget *parent)
1321 : PreviewContentWidget(parent)
@@ -20,8 +28,21 @@ MarkdownPreviewWidget::MarkdownPreviewWidget(NotepadNextApplication *app, QWidge
2028 m_browser->setOpenExternalLinks (true );
2129 m_browser->setFrameShape (QFrame::NoFrame);
2230 m_browser->setReadOnly (true );
31+ m_browser->viewport ()->setMouseTracking (true );
32+ m_browser->viewport ()->installEventFilter (this );
2333 layout->addWidget (m_browser);
2434
35+ m_copyBtn = new QToolButton (m_browser->viewport ());
36+ m_copyBtn->setText (QStringLiteral (" Copy" ));
37+ m_copyBtn->setAutoRaise (true );
38+ m_copyBtn->setCursor (Qt::PointingHandCursor);
39+ m_copyBtn->setStyleSheet (QStringLiteral (
40+ " QToolButton { background: palette(mid); border: none; padding: 2px 6px;"
41+ " border-radius: 3px; font-size: 11px; }"
42+ " QToolButton:hover { background: palette(dark); }" ));
43+ m_copyBtn->hide ();
44+ connect (m_copyBtn, &QToolButton::clicked, this , &MarkdownPreviewWidget::copyCurrentCodeBlock);
45+
2546 m_relayoutTimer.setSingleShot (true );
2647 m_relayoutTimer.setInterval (100 );
2748 connect (&m_relayoutTimer, &QTimer::timeout, this , [this ]() {
@@ -122,6 +143,8 @@ void MarkdownPreviewWidget::renderAsync(const QString &text)
122143void MarkdownPreviewWidget::applyHtml (const QString &html)
123144{
124145 m_cachedHtml = html;
146+ m_copyBtn->hide ();
147+ m_hoveredCodeBlock = QTextBlock ();
125148 int scrollPos = m_browser->verticalScrollBar ()->value ();
126149 m_browser->setHtml (html);
127150 m_browser->verticalScrollBar ()->setValue (scrollPos);
@@ -132,3 +155,86 @@ void MarkdownPreviewWidget::onFontChanged()
132155 if (m_cachedHtml.isEmpty ()) return ;
133156 renderAsync (m_lastSourceText);
134157}
158+
159+ bool MarkdownPreviewWidget::eventFilter (QObject *watched, QEvent *event)
160+ {
161+ if (watched == m_browser->viewport ()) {
162+ if (event->type () == QEvent::MouseMove) {
163+ auto *me = static_cast <QMouseEvent *>(event);
164+ updateCopyButtonPosition (me->pos ());
165+ } else if (event->type () == QEvent::Leave) {
166+ if (!m_copyBtn->underMouse ()) {
167+ m_copyBtn->hide ();
168+ m_hoveredCodeBlock = QTextBlock ();
169+ }
170+ }
171+ }
172+ return PreviewContentWidget::eventFilter (watched, event);
173+ }
174+
175+ void MarkdownPreviewWidget::updateCopyButtonPosition (const QPoint &mousePos)
176+ {
177+ QTextCursor cursor = m_browser->cursorForPosition (mousePos);
178+ QTextBlock block = cursor.block ();
179+
180+ if (!block.isValid () || !isCodeBlock (block)) {
181+ m_copyBtn->hide ();
182+ m_hoveredCodeBlock = QTextBlock ();
183+ return ;
184+ }
185+
186+ // Find the first block of this code group
187+ QTextBlock first = block;
188+ while (first.previous ().isValid () && isCodeBlock (first.previous ()))
189+ first = first.previous ();
190+
191+ if (m_hoveredCodeBlock == first && m_copyBtn->isVisible ())
192+ return ;
193+
194+ m_hoveredCodeBlock = first;
195+
196+ QRectF firstRect = m_browser->document ()->documentLayout ()->blockBoundingRect (first);
197+ int scrollY = m_browser->verticalScrollBar ()->value ();
198+
199+ QSize btnSize = m_copyBtn->sizeHint ();
200+ int x = m_browser->viewport ()->width () - btnSize.width () - 8 ;
201+ int y = static_cast <int >(firstRect.top ()) - scrollY + 4 ;
202+
203+ if (y < 0 ) y = 4 ;
204+
205+ m_copyBtn->move (x, y);
206+ m_copyBtn->show ();
207+ m_copyBtn->raise ();
208+ }
209+
210+ void MarkdownPreviewWidget::copyCurrentCodeBlock ()
211+ {
212+ if (!m_hoveredCodeBlock.isValid ()) return ;
213+ QString text = extractCodeBlockText (m_hoveredCodeBlock);
214+ if (!text.isEmpty ())
215+ QGuiApplication::clipboard ()->setText (text);
216+ }
217+
218+ QString MarkdownPreviewWidget::extractCodeBlockText (const QTextBlock &block) const
219+ {
220+ QString result;
221+ QTextBlock b = block;
222+ while (b.isValid () && isCodeBlock (b)) {
223+ if (!result.isEmpty ()) result += QLatin1Char (' \n ' );
224+ result += b.text ();
225+ b = b.next ();
226+ }
227+ return result;
228+ }
229+
230+ bool MarkdownPreviewWidget::isCodeBlock (const QTextBlock &block) const
231+ {
232+ if (!block.isValid ()) return false ;
233+ QTextBlockFormat fmt = block.blockFormat ();
234+ // QTextBrowser renders <pre> blocks with a non-default background from our CSS
235+ QBrush bg = fmt.background ();
236+ if (bg.style () == Qt::NoBrush) return false ;
237+ // Check that the background differs from the document's default (body) background
238+ QColor docBg = m_browser->document ()->rootFrame ()->frameFormat ().background ().color ();
239+ return bg.color () != docBg && bg.color ().isValid ();
240+ }
0 commit comments