Skip to content

Commit 1c925a0

Browse files
fix(markdown): re-render preview on font change, fix hard breaks
- Font settings were read once at startup; connecting to fontNameChanged/fontSizeChanged signals ensures the preview re-renders whenever the user changes the editor font. - Storing m_lastSourceText avoids re-parsing from the editor when only style needs to change. - Single bare \n was treated as a soft break by CommonMark, collapsing agent output lines into one paragraph; injecting two trailing spaces converts them to hard breaks without touching fences or existing hard breaks. - dockForTab now reads the QDockWidget pointer from tabData instead of matching by title, which was ambiguous when multiple docks shared the same windowTitle. - AiAgentDock gets a 500px minimum width so it is usable when first docked.
1 parent 5ce8d06 commit 1c925a0

7 files changed

Lines changed: 85 additions & 11 deletions

src/MarkdownPreviewWidget.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "MarkdownPreviewWidget.h"
22
#include "NotepadNextApplication.h"
3+
#include "ApplicationSettings.h"
34

45
#include <QVBoxLayout>
56
#include <QImage>
@@ -29,6 +30,10 @@ MarkdownPreviewWidget::MarkdownPreviewWidget(NotepadNextApplication *app, QWidge
2930

3031
m_palette = app->palette();
3132
m_isDark = app->isEffectiveThemeDark();
33+
34+
auto *settings = app->getSettings();
35+
connect(settings, &ApplicationSettings::fontNameChanged, this, &MarkdownPreviewWidget::onFontChanged);
36+
connect(settings, &ApplicationSettings::fontSizeChanged, this, &MarkdownPreviewWidget::onFontChanged);
3237
}
3338

3439
void MarkdownPreviewWidget::setContent(const QString &text, const QString &basePath)
@@ -69,6 +74,10 @@ MarkdownRenderRequest MarkdownPreviewWidget::buildRequest(const QString &text)
6974
req.isDark = m_isDark;
7075
req.basePath = m_basePath;
7176

77+
auto *settings = m_app->getSettings();
78+
req.fontFamily = settings->fontName();
79+
req.fontSize = settings->fontSize();
80+
7281
QSet<QString> labels = MarkdownRenderer::scanFenceLabels(text);
7382
for (const QString &label : labels) {
7483
QString normalized = MarkdownRenderer::normalizeFenceLabel(label);
@@ -89,6 +98,7 @@ MarkdownRenderRequest MarkdownPreviewWidget::buildRequest(const QString &text)
8998

9099
void MarkdownPreviewWidget::renderAsync(const QString &text)
91100
{
101+
m_lastSourceText = text;
92102
int gen = m_renderGeneration.fetch_add(1, std::memory_order_relaxed) + 1;
93103
MarkdownRenderRequest req = buildRequest(text);
94104

@@ -116,3 +126,9 @@ void MarkdownPreviewWidget::applyHtml(const QString &html)
116126
m_browser->setHtml(html);
117127
m_browser->verticalScrollBar()->setValue(scrollPos);
118128
}
129+
130+
void MarkdownPreviewWidget::onFontChanged()
131+
{
132+
if (m_cachedHtml.isEmpty()) return;
133+
renderAsync(m_lastSourceText);
134+
}

src/MarkdownPreviewWidget.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class MarkdownPreviewWidget : public PreviewContentWidget
3232
void renderAsync(const QString &text);
3333
void applyHtml(const QString &html);
3434
MarkdownRenderRequest buildRequest(const QString &text);
35+
void onFontChanged();
3536

3637
NotepadNextApplication *m_app;
3738
QTextBrowser *m_browser;
@@ -40,6 +41,7 @@ class MarkdownPreviewWidget : public PreviewContentWidget
4041
QPalette m_palette;
4142
bool m_isDark = false;
4243
QString m_cachedHtml;
44+
QString m_lastSourceText;
4345
std::atomic<int> m_renderGeneration{0};
4446
QFutureWatcher<MarkdownRenderResult> *m_watcher = nullptr;
4547

src/MarkdownRenderer.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -288,17 +288,26 @@ QString MarkdownRenderer::highlightCodeBlock(const QByteArray &code, const QStri
288288

289289
// PLACEHOLDER_RENDER
290290

291-
QString MarkdownRenderer::buildStyleBlock(const QPalette &palette, bool isDark)
291+
QString MarkdownRenderer::buildStyleBlock(const QPalette &palette, bool isDark,
292+
const QString &fontFamily, int fontSize)
292293
{
293294
QString bg = palette.color(QPalette::Base).name();
294295
QString fg = palette.color(QPalette::Text).name();
295296
QString link = palette.color(QPalette::Link).name();
296297
QString mid = palette.color(QPalette::Mid).name();
297298
QString altBase = palette.color(QPalette::AlternateBase).name();
298299

300+
QString fontCss;
301+
if (!fontFamily.isEmpty())
302+
fontCss += QStringLiteral("font-family:'%1',system-ui,sans-serif;").arg(fontFamily);
303+
else
304+
fontCss += QStringLiteral("font-family:system-ui,sans-serif;");
305+
if (fontSize > 0)
306+
fontCss += QStringLiteral("font-size:%1pt;").arg(fontSize);
307+
299308
return QStringLiteral(
300309
"<style>"
301-
"body{background:%1;color:%2;font-family:system-ui,sans-serif;padding:16px;line-height:1.6;}"
310+
"body{background:%1;color:%2;%6padding:16px;line-height:1.6;}"
302311
"a{color:%3;}"
303312
"code{background:%4;padding:2px 4px;border-radius:3px;font-family:monospace;}"
304313
"pre{background:%4;padding:12px;border-radius:4px;overflow-x:auto;}"
@@ -310,7 +319,7 @@ QString MarkdownRenderer::buildStyleBlock(const QPalette &palette, bool isDark)
310319
"hr{border:none;border-top:1px solid %5;}"
311320
"img{max-width:100%%;}"
312321
"</style>"
313-
).arg(bg, fg, link, altBase, mid);
322+
).arg(bg, fg, link, altBase, mid, fontCss);
314323
}
315324

316325
namespace {
@@ -365,7 +374,7 @@ MarkdownRenderResult MarkdownRenderer::render(const MarkdownRenderRequest &reque
365374

366375
ctx.html.reserve(ctx.sourceUtf8.size() * 2);
367376
ctx.html += QStringLiteral("<!DOCTYPE html><html><head>");
368-
ctx.html += buildStyleBlock(request.palette, request.isDark);
377+
ctx.html += buildStyleBlock(request.palette, request.isDark, request.fontFamily, request.fontSize);
369378
ctx.html += QStringLiteral("</head><body>");
370379

371380
// Two-pass approach:

src/MarkdownRenderer.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ struct MarkdownRenderRequest {
1111
QPalette palette;
1212
bool isDark = false;
1313
QString basePath;
14+
QString fontFamily;
15+
int fontSize = 0;
1416
};
1517

1618
struct MarkdownRenderResult {
@@ -29,7 +31,8 @@ class MarkdownRenderer
2931
static constexpr size_t kMaxHtmlBytes = 512 * 1024;
3032

3133
private:
32-
static QString buildStyleBlock(const QPalette &palette, bool isDark);
34+
static QString buildStyleBlock(const QPalette &palette, bool isDark,
35+
const QString &fontFamily, int fontSize);
3336
static QString highlightCodeBlock(const QByteArray &code, const QString &lexerName, bool isDark);
3437
};
3538

src/docks/AiAgentDock.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ AiAgentDock::AiAgentDock(QString sessionId,
5353
{
5454
setAttribute(Qt::WA_DeleteOnClose, true);
5555
setObjectName(QStringLiteral("AiAgentDock_%1").arg(m_sessionId));
56+
setMinimumWidth(500);
5657
// Spec ("Default dock area"): dock is unrestricted — user may move it to
5758
// any side. defaultArea() is only consulted on first attach.
5859
setAllowedAreas(Qt::AllDockWidgetAreas);

src/widgets/AcpMessageWidget.cpp

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,40 @@ void normalizeBlockMargins(QTextDocument *doc)
8282
} while (cur.movePosition(QTextCursor::NextBlock));
8383
}
8484

85+
// Agent output uses bare \n for visual line breaks, but CommonMark treats a
86+
// single newline as a soft break (rendered as a space). Convert lone \n into
87+
// hard breaks (two trailing spaces before the newline) so they render visually.
88+
// Preserves code fences, blank-line paragraph separators, and lines that
89+
// already end with trailing spaces or a backslash hard break.
90+
QString ensureHardBreaks(const QString &md)
91+
{
92+
const QStringList lines = md.split(QLatin1Char('\n'));
93+
QString out;
94+
out.reserve(md.size() + lines.size() * 2);
95+
bool inFence = false;
96+
97+
for (int i = 0; i < lines.size(); ++i) {
98+
const QString &line = lines[i];
99+
100+
if (line.startsWith(QLatin1String("```")) || line.startsWith(QLatin1String("~~~"))) {
101+
inFence = !inFence;
102+
}
103+
104+
out += line;
105+
106+
if (i < lines.size() - 1) {
107+
const bool nextIsBlank = (i + 1 < lines.size()) && lines[i + 1].trimmed().isEmpty();
108+
const bool alreadyHard = line.endsWith(QLatin1String(" "))
109+
|| line.endsWith(QLatin1Char('\\'));
110+
if (!inFence && !nextIsBlank && !line.trimmed().isEmpty() && !alreadyHard) {
111+
out += QLatin1String(" ");
112+
}
113+
out += QLatin1Char('\n');
114+
}
115+
}
116+
return out;
117+
}
118+
85119
} // namespace
86120

87121
AcpMessageWidget::AcpMessageWidget(QString role, QWidget *parent)
@@ -313,7 +347,7 @@ void AcpMessageWidget::rerender()
313347
// producing visible double borders. Re-emit the HTML with
314348
// cellspacing="0" so border-collapse actually collapses.
315349
QTextDocument tmp;
316-
tmp.setMarkdown(m_text);
350+
tmp.setMarkdown(ensureHardBreaks(m_text));
317351
QString html = tmp.toHtml();
318352
html.replace(QRegularExpression(QStringLiteral("<table([^>]*)>")),
319353
QStringLiteral("<table\\1 cellspacing=\"0\" cellpadding=\"8\">"));
@@ -332,7 +366,7 @@ void AcpMessageWidget::rerender()
332366
|| text.endsWith(QLatin1Char('\t')))) {
333367
text.chop(1);
334368
}
335-
m_browser->document()->setMarkdown(text);
369+
m_browser->document()->setMarkdown(ensureHardBreaks(text));
336370
normalizeBlockMargins(m_browser->document());
337371
} else {
338372
// Streamed chunks often end with "\n", which QTextDocument turns into

src/widgets/DockMiddleClickCloser.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <QStyle>
3030
#include <QStyleOptionDockWidget>
3131
#include <QTabBar>
32+
#include <QVariant>
3233

3334
namespace {
3435

@@ -85,14 +86,22 @@ Filter *sharedFilter()
8586
return instance.data();
8687
}
8788

88-
// Finds the QDockWidget that owns a given tab in Qt's internal dock tab bar.
89-
// When QDockWidgets are tabified, Qt creates a QTabBar whose tab labels match
90-
// the docks' windowTitle(). We walk up to the nearest QMainWindow and search
91-
// its docks for a title match.
9289
QDockWidget *dockForTab(QTabBar *tabBar, int index)
9390
{
9491
if (index < 0)
9592
return nullptr;
93+
94+
// Qt stores the QDockWidget pointer (as quintptr) in each tab's data.
95+
// This is the only unambiguous mapping when multiple docks share a title.
96+
const QVariant data = tabBar->tabData(index);
97+
if (data.isValid()) {
98+
auto *widget = reinterpret_cast<QWidget *>(qvariant_cast<quintptr>(data));
99+
if (auto *dock = qobject_cast<QDockWidget *>(widget))
100+
return dock;
101+
}
102+
103+
// Fallback: title match (ambiguous with duplicate titles, but covers the
104+
// case where tabData is not populated).
96105
const QString tabText = tabBar->tabText(index);
97106
if (tabText.isEmpty())
98107
return nullptr;

0 commit comments

Comments
 (0)