diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4914d348..29ef2953 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@
- Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605)
- Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, `/lockprediction`, and `/completeprediction` commands. (#6583, #6612, #6632)
- Minor: Added support for BetterTTV Pro subscriber badges. (#6625)
+- Minor: Added Markdown support to user notes. (#6490)
- Bugfix: Moderation checks now include the lead moderator badge. (#6642)
- Bugfix: Fixed lead moderator badges not being filtered by the `Channel` badge setting. (#6665)
- Bugfix: Expose the "Extra extension IDs" setting on non-Windows systems too. (#6509)
diff --git a/resources/buttons/bold-darkMode.svg b/resources/buttons/bold-darkMode.svg
new file mode 100644
index 00000000..1ff9500a
--- /dev/null
+++ b/resources/buttons/bold-darkMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/bold-lightMode.svg b/resources/buttons/bold-lightMode.svg
new file mode 100644
index 00000000..83ba847b
--- /dev/null
+++ b/resources/buttons/bold-lightMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/bullet-list-darkMode.svg b/resources/buttons/bullet-list-darkMode.svg
new file mode 100644
index 00000000..932bf5d4
--- /dev/null
+++ b/resources/buttons/bullet-list-darkMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/bullet-list-lightMode.svg b/resources/buttons/bullet-list-lightMode.svg
new file mode 100644
index 00000000..119a0580
--- /dev/null
+++ b/resources/buttons/bullet-list-lightMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/heading-darkMode.svg b/resources/buttons/heading-darkMode.svg
new file mode 100644
index 00000000..90781ba7
--- /dev/null
+++ b/resources/buttons/heading-darkMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/heading-lightMode.svg b/resources/buttons/heading-lightMode.svg
new file mode 100644
index 00000000..41239a08
--- /dev/null
+++ b/resources/buttons/heading-lightMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/italic-darkMode.svg b/resources/buttons/italic-darkMode.svg
new file mode 100644
index 00000000..df98e8ae
--- /dev/null
+++ b/resources/buttons/italic-darkMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/italic-lightMode.svg b/resources/buttons/italic-lightMode.svg
new file mode 100644
index 00000000..3787d694
--- /dev/null
+++ b/resources/buttons/italic-lightMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/link-darkMode.svg b/resources/buttons/link-darkMode.svg
new file mode 100644
index 00000000..947bea1a
--- /dev/null
+++ b/resources/buttons/link-darkMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/link-lightMode.svg b/resources/buttons/link-lightMode.svg
new file mode 100644
index 00000000..566e17ef
--- /dev/null
+++ b/resources/buttons/link-lightMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/quote-darkMode.svg b/resources/buttons/quote-darkMode.svg
new file mode 100644
index 00000000..1d1df391
--- /dev/null
+++ b/resources/buttons/quote-darkMode.svg
@@ -0,0 +1 @@
+
diff --git a/resources/buttons/quote-lightMode.svg b/resources/buttons/quote-lightMode.svg
new file mode 100644
index 00000000..876dbb4c
--- /dev/null
+++ b/resources/buttons/quote-lightMode.svg
@@ -0,0 +1 @@
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4f6e88b1..df851816 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -616,6 +616,8 @@ set(SOURCE_FILES
widgets/FramelessEmbedWindow.hpp
widgets/Label.cpp
widgets/Label.hpp
+ widgets/MarkdownLabel.cpp
+ widgets/MarkdownLabel.hpp
widgets/Notebook.cpp
widgets/Notebook.hpp
widgets/OverlayWindow.cpp
diff --git a/src/widgets/Label.cpp b/src/widgets/Label.cpp
index 074dac53..0f618da6 100644
--- a/src/widgets/Label.cpp
+++ b/src/widgets/Label.cpp
@@ -178,6 +178,7 @@ void Label::updateSize()
auto yPadding =
this->currentPadding_.top() + this->currentPadding_.bottom();
+
auto height = metrics.height() + yPadding;
if (this->shouldElide_)
{
@@ -214,7 +215,7 @@ bool Label::updateElidedText(const QFontMetricsF &fontMetrics, qreal width)
QRectF Label::textRect() const
{
- return this->rect().toRectF().marginsRemoved(this->currentPadding_);
+ return QRectF(this->rect()).marginsRemoved(this->currentPadding_);
}
} // namespace chatterino
diff --git a/src/widgets/Label.hpp b/src/widgets/Label.hpp
index bf068e89..813d1f06 100644
--- a/src/widgets/Label.hpp
+++ b/src/widgets/Label.hpp
@@ -43,8 +43,7 @@ protected:
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
-private:
- void updateSize();
+ virtual void updateSize();
QRectF textRect() const;
/// Returns the current font style's font metric based on the current scale.
diff --git a/src/widgets/MarkdownLabel.cpp b/src/widgets/MarkdownLabel.cpp
new file mode 100644
index 00000000..73d22eb9
--- /dev/null
+++ b/src/widgets/MarkdownLabel.cpp
@@ -0,0 +1,188 @@
+#include "widgets/MarkdownLabel.hpp"
+
+#include "Application.hpp"
+#include "singletons/Theme.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace chatterino {
+
+MarkdownLabel::MarkdownLabel(BaseWidget *parent, QString text, FontStyle style)
+ : Label(parent, std::move(text), style)
+ , markdownDocument(new QTextDocument(this))
+{
+ if (!this->text_.isEmpty())
+ {
+ this->markdownDocument->setMarkdown(this->text_);
+ }
+
+ this->setMouseTracking(true);
+}
+
+void MarkdownLabel::setText(const QString &text)
+{
+ assert(this->markdownDocument != nullptr);
+
+ if (this->text_ != text)
+ {
+ this->text_ = text;
+ this->markdownDocument->setMarkdown(text);
+ this->updateSize();
+ this->update();
+ }
+}
+
+void MarkdownLabel::paintEvent(QPaintEvent * /*event*/)
+{
+ assert(this->markdownDocument != nullptr);
+
+ QPainter painter(this);
+
+ auto font =
+ getApp()->getFonts()->getFont(this->getFontStyle(), this->scale());
+
+ painter.setFont(font);
+
+ // draw text
+ QRectF textRect = this->textRect();
+
+ if (!this->text_.isEmpty())
+ {
+ QColor textColor =
+ this->theme ? this->theme->messages.textColors.regular : Qt::black;
+
+ this->markdownDocument->setTextWidth(textRect.width());
+ this->markdownDocument->setDefaultFont(font);
+ this->markdownDocument->setMarkdown(this->text_);
+
+ QPalette docPalette = this->palette();
+ docPalette.setColor(QPalette::Text, textColor);
+ docPalette.setColor(QPalette::WindowText, textColor);
+
+ painter.setPen(textColor);
+
+ painter.save();
+ painter.translate(textRect.topLeft());
+
+ // create a rendering context using our text color and document palette
+ QAbstractTextDocumentLayout::PaintContext paintContext;
+ paintContext.palette = docPalette;
+ paintContext.clip = QRectF(0, 0, textRect.width(), textRect.height());
+ this->markdownDocument->documentLayout()->draw(&painter, paintContext);
+
+ painter.restore();
+ }
+ else
+ {
+ // Fall back to the base Label rendering if no markdown document
+ Label::paintEvent(nullptr);
+ return;
+ }
+}
+
+void MarkdownLabel::mousePressEvent(QMouseEvent *event)
+{
+ assert(this->markdownDocument != nullptr);
+
+ if (event->button() == Qt::LeftButton)
+ {
+ QRectF textRect = this->textRect();
+ QPointF pos = event->pos() - textRect.topLeft();
+
+ QString anchor =
+ this->markdownDocument->documentLayout()->anchorAt(pos);
+ if (!anchor.isEmpty())
+ {
+ QUrl url(anchor);
+
+ // Validate the URL and add scheme if missing
+ if (!url.isValid())
+ {
+ return;
+ }
+
+ // If the URL doesn't have a scheme, assume it's http
+ if (url.scheme().isEmpty())
+ {
+ url.setScheme("http");
+ }
+
+ // Only open URLs with safe schemes
+ QString scheme = url.scheme().toLower();
+ if (scheme == "http" || scheme == "https" || scheme == "ftp" ||
+ scheme == "file" || scheme == "mailto")
+ {
+ QDesktopServices::openUrl(url);
+ }
+ return;
+ }
+ }
+
+ Label::mousePressEvent(event);
+}
+
+void MarkdownLabel::mouseMoveEvent(QMouseEvent *event)
+{
+ assert(this->markdownDocument != nullptr);
+
+ QRectF textRect = this->textRect();
+ QPointF pos = event->pos() - textRect.topLeft();
+
+ QString anchor = this->markdownDocument->documentLayout()->anchorAt(pos);
+ if (!anchor.isEmpty())
+ {
+ this->setCursor(Qt::PointingHandCursor);
+ }
+ else
+ {
+ this->setCursor(Qt::ArrowCursor);
+ }
+
+ Label::mouseMoveEvent(event);
+}
+
+void MarkdownLabel::updateSize()
+{
+ assert(this->markdownDocument != nullptr);
+
+ this->currentPadding_ = this->basePadding_.toMarginsF() * this->scale();
+
+ if (!this->text_.isEmpty())
+ {
+ this->markdownDocument->setDefaultFont(
+ getApp()->getFonts()->getFont(this->getFontStyle(), this->scale()));
+
+ this->markdownDocument->setMarkdown(this->text_);
+
+ // Use word wrap width if enabled, otherwise use a reasonable default
+ qreal testWidth = this->wordWrap_
+ ? 400.0 * this->scale()
+ : this->markdownDocument->idealWidth();
+ this->markdownDocument->setTextWidth(testWidth);
+
+ auto yPadding =
+ this->currentPadding_.top() + this->currentPadding_.bottom();
+
+ auto height = this->markdownDocument->size().height() + yPadding;
+ auto width = qMin(this->markdownDocument->idealWidth(), testWidth) +
+ this->currentPadding_.left() +
+ this->currentPadding_.right();
+
+ this->sizeHint_ = QSizeF(width, height).toSize();
+ this->minimumSizeHint_ = this->sizeHint_;
+
+ this->updateGeometry();
+ }
+ else
+ {
+ // Fall back to base Label size calculation
+ Label::updateSize();
+ }
+}
+
+} // namespace chatterino
diff --git a/src/widgets/MarkdownLabel.hpp b/src/widgets/MarkdownLabel.hpp
new file mode 100644
index 00000000..7530f4f7
--- /dev/null
+++ b/src/widgets/MarkdownLabel.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "widgets/Label.hpp"
+
+class QTextDocument;
+
+namespace chatterino {
+
+/// @brief A Label that supports rendering markdown text
+///
+/// MarkdownLabel inherits from Label and adds markdown rendering capabilities.
+/// It automatically handles markdown document creation and rendering.
+class MarkdownLabel : public Label
+{
+public:
+ explicit MarkdownLabel(BaseWidget *parent, QString text,
+ FontStyle style = FontStyle::UiMedium);
+
+ void setText(const QString &text);
+
+protected:
+ void paintEvent(QPaintEvent * /*event*/) override;
+ void mousePressEvent(QMouseEvent *event) override;
+ void mouseMoveEvent(QMouseEvent *event) override;
+
+private:
+ void updateSize() override;
+
+ QTextDocument *markdownDocument;
+};
+
+} // namespace chatterino
diff --git a/src/widgets/dialogs/EditUserNotesDialog.cpp b/src/widgets/dialogs/EditUserNotesDialog.cpp
index cfdcc6bd..bcbab065 100644
--- a/src/widgets/dialogs/EditUserNotesDialog.cpp
+++ b/src/widgets/dialogs/EditUserNotesDialog.cpp
@@ -2,8 +2,14 @@
#include "singletons/Theme.hpp"
#include "util/LayoutCreator.hpp"
+#include "widgets/buttons/SvgButton.hpp"
+#include "widgets/MarkdownLabel.hpp"
+#include
#include
+#include
+#include
+#include
#include
namespace chatterino {
@@ -17,12 +23,281 @@ EditUserNotesDialog::EditUserNotesDialog(QWidget *parent)
},
parent)
{
- this->setScaleIndependentSize(500, 350);
+ this->setScaleIndependentSize(700, 450);
auto layout = LayoutCreator(this->getLayoutContainer())
.setLayoutType();
- auto edit = layout.emplace().assign(&this->textEdit_);
+ auto headerLayout = layout.emplace();
+
+ auto *headingButton = headerLayout
+ .emplace(SvgButton::Src{
+ .dark = ":/buttons/heading-darkMode.svg",
+ .light = ":/buttons/heading-lightMode.svg",
+ })
+ .getElement();
+ QObject::connect(headingButton, &Button::leftClicked, this, [&] {
+ auto cursor = this->textEdit_->textCursor();
+ const auto line = cursor.block().text();
+ const auto pos = EditUserNotesDialog::currentWordPosition(cursor);
+ const auto selectedText = cursor.selectedText();
+
+ if (!selectedText.isEmpty() &&
+ EditUserNotesDialog::isHeading(line, pos))
+ {
+ cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 1);
+ cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 4);
+ cursor.removeSelectedText();
+
+ // restore selection
+ cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor,
+ static_cast(selectedText.length()));
+ }
+ else if (cursor.hasSelection())
+ {
+ cursor.insertText("### " + selectedText);
+
+ // restore selection
+ cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor,
+ static_cast(selectedText.length()));
+ }
+ else
+ {
+ cursor.insertText("### ");
+ }
+ this->textEdit_->setTextCursor(cursor);
+ this->textEdit_->setFocus();
+ });
+ headingButton->setToolTip("Insert a heading");
+
+ auto *boldButton = headerLayout
+ .emplace(SvgButton::Src{
+ .dark = ":/buttons/bold-darkMode.svg",
+ .light = ":/buttons/bold-lightMode.svg",
+ })
+ .getElement();
+ QObject::connect(boldButton, &Button::leftClicked, this, [&] {
+ auto cursor = this->textEdit_->textCursor();
+ const auto line = cursor.block().text();
+ const auto pos = cursor.columnNumber();
+ auto selectedText = cursor.selectedText();
+
+ if (!selectedText.isEmpty() && EditUserNotesDialog::isBold(line, pos))
+ {
+ cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor,
+ 1); // un-select
+ cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 2);
+ cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor,
+ static_cast(selectedText.length() + 4));
+ cursor.insertText(selectedText);
+
+ // restore selection
+ cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor,
+ static_cast(selectedText.length()));
+ }
+ else if (cursor.hasSelection())
+ {
+ auto appended = 0;
+ QChar newLine(QChar::ParagraphSeparator);
+
+ if (selectedText.back() == newLine)
+ {
+ selectedText.chop(1);
+ cursor.insertText("**" + selectedText + "**" + newLine);
+ appended = 3;
+ }
+ else
+ {
+ cursor.insertText("**" + selectedText + "**");
+ appended = 2;
+ }
+
+ // restore selection
+ cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor,
+ appended);
+ cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor,
+ static_cast(selectedText.length()));
+ }
+ else
+ {
+ cursor.insertText("****");
+ cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 2);
+ }
+ this->textEdit_->setTextCursor(cursor);
+ this->textEdit_->setFocus();
+ });
+ boldButton->setToolTip("Make selected text bold");
+
+ auto *italicButton = headerLayout
+ .emplace(SvgButton::Src{
+ .dark = ":/buttons/italic-darkMode.svg",
+ .light = ":/buttons/italic-lightMode.svg",
+ })
+ .getElement();
+ QObject::connect(italicButton, &Button::leftClicked, this, [&] {
+ auto cursor = this->textEdit_->textCursor();
+ const auto line = cursor.block().text();
+ const auto pos = cursor.columnNumber();
+ auto selectedText = cursor.selectedText();
+
+ if (!selectedText.isEmpty() && EditUserNotesDialog::isItalic(line, pos))
+ {
+ cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor,
+ 1); // un-select
+ cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, 1);
+ cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor,
+ static_cast(selectedText.length() + 2));
+ cursor.insertText(selectedText);
+
+ // restore selection
+ cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor,
+ static_cast(selectedText.length()));
+ }
+ else if (cursor.hasSelection())
+ {
+ auto appended = 0;
+ QChar newLine(QChar::ParagraphSeparator);
+
+ if (selectedText.back() == newLine)
+ {
+ selectedText.chop(1);
+ cursor.insertText("*" + selectedText + "*" + newLine);
+ appended = 2;
+ }
+ else
+ {
+ cursor.insertText("*" + selectedText + "*");
+ appended = 1;
+ }
+
+ // restore selection
+ cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor,
+ appended);
+ cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor,
+ static_cast(selectedText.length()));
+ }
+ else
+ {
+ cursor.insertText("**");
+ cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 1);
+ }
+ this->textEdit_->setTextCursor(cursor);
+ this->textEdit_->setFocus();
+ });
+ italicButton->setToolTip("Make selected text italic");
+
+ auto *quoteButton = headerLayout
+ .emplace(SvgButton::Src{
+ .dark = ":/buttons/quote-darkMode.svg",
+ .light = ":/buttons/quote-lightMode.svg",
+ })
+ .getElement();
+ QObject::connect(quoteButton, &Button::leftClicked, this, [&] {
+ auto cursor = this->textEdit_->textCursor();
+ if (cursor.hasSelection())
+ {
+ auto selectedText = cursor.selectedText();
+ cursor.insertText("> " + selectedText);
+ }
+ else
+ {
+ cursor.insertText("> ");
+ }
+ this->textEdit_->setTextCursor(cursor);
+ this->textEdit_->setFocus();
+ });
+ quoteButton->setToolTip("Insert a blockquote");
+
+ auto *linkButton = headerLayout
+ .emplace(SvgButton::Src{
+ .dark = ":/buttons/link-darkMode.svg",
+ .light = ":/buttons/link-lightMode.svg",
+ })
+ .getElement();
+ QObject::connect(linkButton, &Button::leftClicked, this, [&] {
+ auto cursor = this->textEdit_->textCursor();
+ if (cursor.hasSelection())
+ {
+ auto appended = 0;
+ auto selectedText = cursor.selectedText();
+ QChar newLine(QChar::ParagraphSeparator);
+
+ if (selectedText.back() == newLine)
+ {
+ selectedText.chop(1);
+ cursor.insertText("[" + selectedText + "](url)" + newLine);
+ appended = 2;
+ }
+ else
+ {
+ cursor.insertText("[" + selectedText + "](url)");
+ appended = 1;
+ }
+
+ // select "url" for easy replacement
+ cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor,
+ appended);
+ cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 3);
+ }
+ else
+ {
+ cursor.insertText("[](url)");
+ cursor.movePosition(QTextCursor::Left, QTextCursor::MoveAnchor, 1);
+ cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 3);
+ }
+ this->textEdit_->setTextCursor(cursor);
+ this->textEdit_->setFocus();
+ });
+ linkButton->setToolTip("Insert a hyperlink");
+
+ auto *listButton = headerLayout
+ .emplace(SvgButton::Src{
+ .dark = ":/buttons/bullet-list-darkMode.svg",
+ .light = ":/buttons/bullet-list-lightMode.svg",
+ })
+ .getElement();
+ QObject::connect(listButton, &Button::leftClicked, this, [&] {
+ auto cursor = this->textEdit_->textCursor();
+ if (cursor.hasSelection())
+ {
+ cursor.select(QTextCursor::LineUnderCursor);
+ const auto selectedText = cursor.selectedText();
+ cursor.insertText("- " + selectedText);
+ cursor.movePosition(QTextCursor::StartOfLine,
+ QTextCursor::KeepAnchor, 0);
+ }
+ else
+ {
+ const auto pos = cursor.columnNumber();
+ cursor.movePosition(QTextCursor::StartOfLine,
+ QTextCursor::MoveAnchor, 0);
+ cursor.insertText("- ");
+ cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor,
+ pos);
+ }
+ this->textEdit_->setTextCursor(cursor);
+ this->textEdit_->setFocus();
+ });
+ listButton->setToolTip("Insert a bullet list item");
+
+ headerLayout->addSpacing(175); // group markdown toolbar buttons
+
+ auto previewCheckBox =
+ headerLayout.emplace("Show Markdown Preview")
+ .assign(&this->previewCheckBox_);
+
+ auto splitter =
+ layout.emplace(Qt::Horizontal).assign(&this->splitter_);
+
+ auto edit = splitter.emplace().assign(&this->textEdit_);
+
+ auto preview = splitter.emplace(this, QString())
+ .assign(&this->previewLabel_);
+ preview->setWordWrap(true);
+ preview->setPadding(QMargins(10, 10, 10, 10));
+
+ this->splitter_->setSizes({350, 350});
+ this->previewLabel_->setVisible(false);
layout
.emplace(QDialogButtonBox::Ok |
@@ -36,6 +311,31 @@ EditUserNotesDialog::EditUserNotesDialog(QWidget *parent)
this->close();
});
+ // Connect preview toggle
+ QObject::connect(this->previewCheckBox_, &QCheckBox::toggled, this,
+ [this](bool checked) {
+ this->previewLabel_->setVisible(checked);
+ if (checked)
+ {
+ this->updatePreview();
+ this->splitter_->setSizes({350, 350});
+ this->textEdit_->setFocus();
+ }
+ else
+ {
+ this->splitter_->setSizes({700, 0});
+ this->textEdit_->setFocus();
+ }
+ });
+
+ // Connect text changes to preview update
+ QObject::connect(this->textEdit_, &QTextEdit::textChanged, this, [this] {
+ if (this->previewCheckBox_->isChecked())
+ {
+ this->updatePreview();
+ }
+ });
+
this->themeChangedEvent();
}
@@ -63,6 +363,8 @@ void EditUserNotesDialog::themeChangedEvent()
return;
}
+ assert(this->previewLabel_ != nullptr);
+
auto palette = this->palette();
palette.setColor(QPalette::Window,
@@ -76,6 +378,84 @@ void EditUserNotesDialog::themeChangedEvent()
{
this->textEdit_->setPalette(palette);
}
+
+ this->previewLabel_->setPalette(palette);
+}
+
+void EditUserNotesDialog::updatePreview()
+{
+ assert(this->textEdit_ != nullptr);
+ assert(this->previewLabel_ != nullptr);
+
+ QString text = this->textEdit_->toPlainText();
+ if (text.isEmpty())
+ {
+ this->previewLabel_->setText(
+ "*Preview will appear here when you type markdown text...*");
+ }
+ else
+ {
+ this->previewLabel_->setText(text);
+ }
+}
+
+int EditUserNotesDialog::currentWordPosition(const QTextCursor &cursor)
+{
+ QTextCursor temp = cursor;
+ temp.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
+
+ return temp.columnNumber();
+}
+
+bool EditUserNotesDialog::isBold(const QString &line, const int pos)
+{
+ static QRegularExpression pattern(R"((\*{2,})(.*?)(\1))");
+
+ QRegularExpressionMatchIterator iter = pattern.globalMatch(line);
+
+ while (iter.hasNext())
+ {
+ QRegularExpressionMatch match = iter.next();
+ const auto startIndex = match.capturedStart(2);
+ const auto endIndex = match.capturedEnd(2);
+
+ if (pos == startIndex || pos == endIndex)
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool EditUserNotesDialog::isItalic(const QString &line, const int pos)
+{
+ static QRegularExpression pattern(R"(\*(.*?)\*)");
+
+ QRegularExpressionMatchIterator iter = pattern.globalMatch(line);
+
+ while (iter.hasNext())
+ {
+ QRegularExpressionMatch match = iter.next();
+ const auto startIndex = match.capturedStart(1);
+ const auto endIndex = match.capturedEnd(1);
+
+ if (pos == startIndex || pos == endIndex)
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool EditUserNotesDialog::isHeading(const QString &line, const int pos)
+{
+ static QRegularExpression pattern(R"(###\s)");
+
+ QRegularExpressionMatch match = pattern.match(line, pos - 4);
+
+ return match.hasMatch() && pos == match.capturedEnd();
}
} // namespace chatterino
diff --git a/src/widgets/dialogs/EditUserNotesDialog.hpp b/src/widgets/dialogs/EditUserNotesDialog.hpp
index 877787b7..b7f15b94 100644
--- a/src/widgets/dialogs/EditUserNotesDialog.hpp
+++ b/src/widgets/dialogs/EditUserNotesDialog.hpp
@@ -3,10 +3,15 @@
#include "pajlada/signals/signal.hpp"
#include "widgets/BasePopup.hpp"
+class QCheckBox;
+class QSplitter;
+class QTextCursor;
class QTextEdit;
namespace chatterino {
+class MarkdownLabel;
+
class EditUserNotesDialog : public BasePopup
{
Q_OBJECT
@@ -24,7 +29,16 @@ protected:
void themeChangedEvent() override;
private:
+ void updatePreview();
+ static int currentWordPosition(const QTextCursor &cursor);
+ static bool isBold(const QString &line, const int pos);
+ static bool isItalic(const QString &line, const int pos);
+ static bool isHeading(const QString &line, const int pos);
+
QTextEdit *textEdit_{};
+ QCheckBox *previewCheckBox_{};
+ QSplitter *splitter_{};
+ MarkdownLabel *previewLabel_{};
};
} // namespace chatterino
diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp
index 3a695586..54c8ff66 100644
--- a/src/widgets/dialogs/UserInfoPopup.cpp
+++ b/src/widgets/dialogs/UserInfoPopup.cpp
@@ -36,6 +36,7 @@
#include "widgets/helper/LiveIndicator.hpp"
#include "widgets/helper/ScalingSpacerItem.hpp"
#include "widgets/Label.hpp"
+#include "widgets/MarkdownLabel.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/Scrollbar.hpp"
#include "widgets/splits/Split.hpp"
@@ -496,7 +497,8 @@ UserInfoPopup::UserInfoPopup(bool closeAutomatically, Split *split)
});
}
- auto notesPreview = layout.emplace