From 11bf741554516cad0fb063b5689b321728e152a4 Mon Sep 17 00:00:00 2001 From: James Graham Date: Mon, 27 Oct 2025 18:10:13 +0000 Subject: [PATCH] Improve the style picker --- src/chatbar/CMakeLists.txt | 12 ++ src/chatbar/RichEditBar.qml | 79 +----------- src/chatbar/StylePicker.qml | 121 ++++++++++++++++++ src/chatbar/styledelegatehelper.cpp | 88 +++++++++++++ src/chatbar/styledelegatehelper.h | 36 ++++++ src/libneochat/CMakeLists.txt | 1 + src/libneochat/chatdocumenthandler.cpp | 7 +- src/libneochat/chatdocumenthandler.h | 26 +--- src/libneochat/enums/textstyle.h | 74 +++++++++++ .../models/chatbarmessagecontentmodel.cpp | 20 ++- .../models/chatbarmessagecontentmodel.h | 3 + 11 files changed, 369 insertions(+), 98 deletions(-) create mode 100644 src/chatbar/StylePicker.qml create mode 100644 src/chatbar/styledelegatehelper.cpp create mode 100644 src/chatbar/styledelegatehelper.h create mode 100644 src/libneochat/enums/textstyle.h diff --git a/src/chatbar/CMakeLists.txt b/src/chatbar/CMakeLists.txt index 0595bb2b0..132a4fd7e 100644 --- a/src/chatbar/CMakeLists.txt +++ b/src/chatbar/CMakeLists.txt @@ -16,6 +16,7 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE EmojiPicker.qml EmojiDialog.qml EmojiTonesPicker.qml + StylePicker.qml ImageEditorPage.qml VoiceMessageDialog.qml ImageDialog.qml @@ -23,4 +24,15 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE LocationChooser.qml NewPollDialog.qml TableDialog.qml + SOURCES + styledelegatehelper.cpp +) + +target_include_directories(Chatbar PRIVATE ${CMAKE_BINARY_DIR}) +target_link_libraries(Chatbar PRIVATE + Qt::Core + Qt::Quick + Qt::QuickControls2 + KF6::Kirigami + LibNeoChat ) diff --git a/src/chatbar/RichEditBar.qml b/src/chatbar/RichEditBar.qml index 32f27ef4f..6236fbd32 100644 --- a/src/chatbar/RichEditBar.qml +++ b/src/chatbar/RichEditBar.qml @@ -37,7 +37,6 @@ QQC2.ToolBar { readonly property real uncompressedImplicitWidth: textFormatRow.implicitWidth + listRow.implicitWidth + styleButton.implicitWidth + - codeButton.implicitWidth + emojiButton.implicitWidth + linkButton.implicitWidth + sendRow.implicitWidth + @@ -48,7 +47,6 @@ QQC2.ToolBar { readonly property real listCompressedImplicitWidth: textFormatRow.implicitWidth + compressedListButton.implicitWidth + styleButton.implicitWidth + - codeButton.implicitWidth + emojiButton.implicitWidth + linkButton.implicitWidth + sendRow.implicitWidth + @@ -59,7 +57,6 @@ QQC2.ToolBar { readonly property real textFormatCompressedImplicitWidth: compressedTextFormatButton.implicitWidth + compressedListButton.implicitWidth + styleButton.implicitWidth + - codeButton.implicitWidth + emojiButton.implicitWidth + linkButton.implicitWidth + sendRow.implicitWidth + @@ -348,86 +345,24 @@ QQC2.ToolBar { checkable: true checked: styleMenu.visible onClicked: { + if (styleMenu.visible) { + styleMenu.close(); + return; + } styleMenu.open() } - QQC2.Menu { + StylePicker { id: styleMenu - y: -implicitHeight + chatContentModel: root.contentModel - QQC2.MenuItem { - text: i18nc("@item:inmenu no heading", "Paragraph") - onTriggered: root.contentModel.insertComponentAtCursor(LibNeoChat.MessageComponentType.Text); - } - QQC2.MenuItem { - text: i18nc("@item:inmenu heading level 1 (largest)", "Heading 1") - onTriggered: { - root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading1; - root.clicked(); - } - } - QQC2.MenuItem { - text: i18nc("@item:inmenu heading level 2", "Heading 2") - onTriggered: { - root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading2; - root.clicked(); - } - } - QQC2.MenuItem { - text: i18nc("@item:inmenu heading level 3", "Heading 3") - onTriggered: { - root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading3; - root.clicked(); - } - } - QQC2.MenuItem { - text: i18nc("@item:inmenu heading level 4", "Heading 4") - onTriggered: { - root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading4; - root.clicked(); - } - } - QQC2.MenuItem { - text: i18nc("@item:inmenu heading level 5", "Heading 5") - onTriggered: { - root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading5; - root.clicked(); - } - } - QQC2.MenuItem { - text: i18nc("@item:inmenu heading level 6 (smallest)", "Heading 6") - onTriggered: { - root.focusedDocumentHandler.style = LibNeoChat.ChatDocumentHandler.Heading6; - root.clicked(); - } - } - QQC2.MenuItem { - text: i18nc("@item:inmenu", "Quote") - onTriggered: { - root.contentModel.insertComponentAtCursor(LibNeoChat.MessageComponentType.Quote); - root.clicked(); - } - } + onClosed: root.clicked() } QQC2.ToolTip.text: text QQC2.ToolTip.visible: hovered QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay } - QQC2.ToolButton { - id: codeButton - icon.name: "format-text-code" - text: i18n("Code") - display: QQC2.AbstractButton.IconOnly - - onClicked: { - root.contentModel.insertComponentAtCursor(LibNeoChat.MessageComponentType.Code); - root.clicked(); - } - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - QQC2.ToolTip.text: text - } Kirigami.Separator { Layout.fillHeight: true Layout.margins: 0 diff --git a/src/chatbar/StylePicker.qml b/src/chatbar/StylePicker.qml new file mode 100644 index 000000000..8a7386443 --- /dev/null +++ b/src/chatbar/StylePicker.qml @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +import org.kde.neochat.libneochat as LibNeoChat +import org.kde.neochat.messagecontent as MessageContent + +QQC2.Popup { + id: root + + required property MessageContent.ChatBarMessageContentModel chatContentModel + readonly property LibNeoChat.ChatDocumentHandler focusedDocumentHandler: chatContentModel.focusedDocumentHandler + + y: -implicitHeight + + contentItem: ColumnLayout { + spacing: Kirigami.Units.smallSpacing + + Repeater { + model: 9 + + delegate: QQC2.TextArea { + id: styleDelegate + required property int index + + Layout.fillWidth: true + Layout.minimumWidth: Kirigami.Units.gridUnit * 7 + Layout.minimumHeight: Kirigami.Units.gridUnit * 2 + leftPadding: lineRow.visible ? lineRow.width + lineRow.anchors.leftMargin + Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing + verticalAlignment: Text.AlignVCenter + + enabled: root.chatContentModel.focusType !== LibNeoChat.MessageComponentType.Code || styleDelegate.index === LibNeoChat.TextStyle.Paragraph || styleDelegate.index === LibNeoChat.TextStyle.Quote + readOnly: true + selectByMouse: false + + onPressed: (event) => { + if (styleDelegate.index === LibNeoChat.TextStyle.Paragraph || + styleDelegate.index === LibNeoChat.TextStyle.Code || + styleDelegate.index === LibNeoChat.TextStyle.Quote + ) { + root.chatContentModel.insertStyleAtCursor(styleDelegate.index); + } else { + root.focusedDocumentHandler.style = styleDelegate.index; + } + root.close(); + } + + RowLayout { + id: lineRow + anchors { + top: styleDelegate.top + bottom: styleDelegate.bottom + left: styleDelegate.left + leftMargin: Kirigami.Units.smallSpacing + } + + visible: styleDelegate.index === LibNeoChat.TextStyle.Code + + QQC2.Label { + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + text: "1" + color: Kirigami.Theme.disabledTextColor + + font.family: "monospace" + } + Kirigami.Separator { + Layout.fillHeight: true + } + } + + StyleDelegateHelper { + textItem: styleDelegate + } + + background: Rectangle { + color: Kirigami.Theme.backgroundColor + Kirigami.Theme.colorSet: styleDelegate.index === LibNeoChat.TextStyle.Quote ? Kirigami.Theme.Window : Kirigami.Theme.View + Kirigami.Theme.inherit: false + radius: Kirigami.Units.cornerRadius + border { + width: 1 + color: styleDelegate.hovered || (root.focusedDocumentHandler?.style ?? false) === styleDelegate.index ? + Kirigami.Theme.highlightColor : + Kirigami.ColorUtils.linearInterpolation( + Kirigami.Theme.backgroundColor, + Kirigami.Theme.textColor, + Kirigami.Theme.frameContrast + ) + } + } + } + } + } + + background: Kirigami.ShadowedRectangle { + radius: Kirigami.Units.cornerRadius + color: Kirigami.Theme.backgroundColor + + border { + width: 1 + color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast) + } + + shadow { + size: Kirigami.Units.gridUnit + yOffset: 0 + color: Qt.rgba(0, 0, 0, 0.2) + } + + Kirigami.Theme.inherit: false + Kirigami.Theme.colorSet: Kirigami.Theme.View + } +} diff --git a/src/chatbar/styledelegatehelper.cpp b/src/chatbar/styledelegatehelper.cpp new file mode 100644 index 000000000..92a784c67 --- /dev/null +++ b/src/chatbar/styledelegatehelper.cpp @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "styledelegatehelper.h" + +#include +#include +#include + +#include "enums/textstyle.h" + +StyleDelegateHelper::StyleDelegateHelper(QObject *parent) + : QObject(parent) +{ +} + +QQuickItem *StyleDelegateHelper::textItem() const +{ + return m_textItem; +} + +void StyleDelegateHelper::setTextItem(QQuickItem *textItem) +{ + if (textItem == m_textItem) { + return; + } + + m_textItem = textItem; + + if (m_textItem) { + if (document()) { + formatDocument(); + } + } + + Q_EMIT textItemChanged(); +} + +QTextDocument *StyleDelegateHelper::document() const +{ + if (!m_textItem) { + return nullptr; + } + const auto quickDocument = qvariant_cast(m_textItem->property("textDocument")); + return quickDocument ? quickDocument->textDocument() : nullptr; +} + +void StyleDelegateHelper::formatDocument() +{ + if (!document()) { + return; + } + + auto cursor = QTextCursor(document()); + if (cursor.isNull()) { + return; + } + + cursor.beginEditBlock(); + cursor.select(QTextCursor::Document); + cursor.removeSelectedText(); + const auto style = static_cast(m_textItem->property("index").toInt()); + const auto string = TextStyle::styleString(style); + + const int headingLevel = style <= 6 ? style : 0; + // Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and + // level=2 look the same + const int sizeAdjustment = headingLevel > 0 ? 5 - headingLevel : 0; + + QTextBlockFormat blkfmt; + blkfmt.setHeadingLevel(headingLevel); + cursor.mergeBlockFormat(blkfmt); + + QTextCharFormat chrfmt; + chrfmt.setFontWeight(headingLevel > 0 ? QFont::Bold : QFont::Normal); + chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment / 2); + if (style == TextStyle::Code) { + chrfmt.setFontFamilies({u"monospace"_s}); + } else if (style == TextStyle::Quote) { + chrfmt.setFontItalic(true); + } + + cursor.mergeBlockCharFormat(chrfmt); + cursor.insertText(string); + cursor.endEditBlock(); +} + +#include "moc_styledelegatehelper.cpp" diff --git a/src/chatbar/styledelegatehelper.h b/src/chatbar/styledelegatehelper.h new file mode 100644 index 000000000..acd5f534d --- /dev/null +++ b/src/chatbar/styledelegatehelper.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include +#include + +class QTextDocument; + +class StyleDelegateHelper : public QObject +{ + Q_OBJECT + QML_ELEMENT + + /** + * @brief The QML text Item the ChatDocumentHandler is handling. + */ + Q_PROPERTY(QQuickItem *textItem READ textItem WRITE setTextItem NOTIFY textItemChanged) + +public: + explicit StyleDelegateHelper(QObject *parent = nullptr); + + QQuickItem *textItem() const; + void setTextItem(QQuickItem *textItem); + +Q_SIGNALS: + void textItemChanged(); + +private: + QPointer m_textItem; + QTextDocument *document() const; + + void formatDocument(); +}; diff --git a/src/libneochat/CMakeLists.txt b/src/libneochat/CMakeLists.txt index 8258f8f0b..2107ee3b5 100644 --- a/src/libneochat/CMakeLists.txt +++ b/src/libneochat/CMakeLists.txt @@ -33,6 +33,7 @@ target_sources(LibNeoChat PRIVATE enums/pushrule.h enums/roomsortparameter.cpp enums/roomsortorder.h + enums/textstyle.h enums/timelinemarkreadcondition.h events/imagepackevent.cpp events/pollevent.cpp diff --git a/src/libneochat/chatdocumenthandler.cpp b/src/libneochat/chatdocumenthandler.cpp index 882ed6f9b..94b6862ee 100644 --- a/src/libneochat/chatdocumenthandler.cpp +++ b/src/libneochat/chatdocumenthandler.cpp @@ -29,6 +29,7 @@ #include "chatbartype.h" #include "chatdocumenthandler_logging.h" #include "eventhandler.h" +#include "textstyle.h" using namespace Qt::StringLiterals; @@ -1027,12 +1028,12 @@ int ChatDocumentHandler::currentListStyle() const return -textCursor().currentList()->format().style(); } -ChatDocumentHandler::Style ChatDocumentHandler::style() const +TextStyle::Style ChatDocumentHandler::style() const { - return static_cast