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