Improve the style picker

This commit is contained in:
James Graham
2025-10-27 18:10:13 +00:00
parent c128450cf5
commit 11bf741554
11 changed files with 369 additions and 98 deletions

View File

@@ -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
)

View File

@@ -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

121
src/chatbar/StylePicker.qml Normal file
View File

@@ -0,0 +1,121 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// 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
}
}

View File

@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "styledelegatehelper.h"
#include <QQuickTextDocument>
#include <QTextCursor>
#include <QTextDocument>
#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<QQuickTextDocument *>(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<TextStyle::Style>(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"

View File

@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QQuickItem>
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<QQuickItem> m_textItem;
QTextDocument *document() const;
void formatDocument();
};

View File

@@ -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

View File

@@ -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<Style>(textCursor().blockFormat().headingLevel());
return static_cast<TextStyle::Style>(textCursor().blockFormat().headingLevel());
}
void ChatDocumentHandler::setStyle(ChatDocumentHandler::Style style)
void ChatDocumentHandler::setStyle(TextStyle::Style style)
{
const int headingLevel = style <= 6 ? style : 0;
// Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and

View File

@@ -12,6 +12,7 @@
#include "chatbarcache.h"
#include "enums/chatbartype.h"
#include "enums/textstyle.h"
#include "models/completionmodel.h"
#include "neochatroom.h"
#include "nestedlisthelper_p.h"
@@ -110,7 +111,7 @@ class ChatDocumentHandler : public QObject
Q_PROPERTY(bool underline READ underline WRITE setUnderline NOTIFY formatChanged)
Q_PROPERTY(bool strikethrough READ strikethrough WRITE setStrikethrough NOTIFY formatChanged)
Q_PROPERTY(ChatDocumentHandler::Style style READ style WRITE setStyle NOTIFY styleChanged)
Q_PROPERTY(TextStyle::Style style READ style WRITE setStyle NOTIFY styleChanged)
// Q_PROPERTY(bool canIndentList READ canIndentList NOTIFY cursorPositionChanged)
// Q_PROPERTY(bool canDedentList READ canDedentList NOTIFY cursorPositionChanged)
@@ -132,25 +133,6 @@ public:
End,
};
/**
* @brief Enum to define available styles.
*
* @note The Paragraph and Heading values are intentially fixed to match heading
* level values returned by QTextBlockFormat::headingLevel().
*
* @sa QTextBlockFormat::headingLevel()
*/
enum Style {
Paragraph = 0,
Heading1 = 1,
Heading2 = 2,
Heading3 = 3,
Heading4 = 4,
Heading5 = 5,
Heading6 = 6,
};
Q_ENUM(Style);
explicit ChatDocumentHandler(QObject *parent = nullptr);
ChatBarType::Type type() const;
@@ -218,8 +200,8 @@ public:
bool canDedentList() const;
int currentListStyle() const;
Style style() const;
void setStyle(Style style);
TextStyle::Style style() const;
void setStyle(TextStyle::Style style);
// bool list() const;
// void setList(bool list);

View File

@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#pragma once
#include <QObject>
#include <QQmlEngine>
using namespace Qt::StringLiterals;
/**
* @class TextStyle
*
* A class with the Style enum for available text styles.
*/
class TextStyle : public QObject
{
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("")
public:
/**
* @brief Enum to define available styles.
*
* @note The Paragraph and Heading values are intentially fixed to match heading
* level values returned by QTextBlockFormat::headingLevel().
*
* @sa QTextBlockFormat::headingLevel()
*/
enum Style {
Paragraph = 0,
Heading1 = 1,
Heading2 = 2,
Heading3 = 3,
Heading4 = 4,
Heading5 = 5,
Heading6 = 6,
Code = 7,
Quote = 8,
};
Q_ENUM(Style);
/**
* @brief Translate the Kind enum value to a human readable string.
*
* @sa Kind
*/
static QString styleString(Style style)
{
switch (style) {
case Style::Paragraph:
return u"Paragraph"_s;
case Style::Heading1:
return u"Heading 1"_s;
case Style::Heading2:
return u"Heading 2"_s;
case Style::Heading3:
return u"Heading 3"_s;
case Style::Heading4:
return u"Heading 4"_s;
case Style::Heading5:
return u"Heading 5"_s;
case Style::Heading6:
return u"Heading 6"_s;
case Style::Code:
return u"Code"_s;
case Style::Quote:
return u"\"Quote\""_s;
default:
return {};
}
};
};

View File

@@ -9,6 +9,7 @@
#include "chatdocumenthandler.h"
#include "enums/chatbartype.h"
#include "enums/messagecomponenttype.h"
#include "enums/textstyle.h"
#include "messagecontentmodel.h"
ChatBarMessageContentModel::ChatBarMessageContentModel(QObject *parent)
@@ -277,11 +278,28 @@ ChatBarMessageContentModel::insertComponent(int row, MessageComponentType::Type
return it;
}
void ChatBarMessageContentModel::insertStyleAtCursor(TextStyle::Style style)
{
switch (style) {
case TextStyle::Paragraph:
insertComponentAtCursor(MessageComponentType::Text);
return;
case TextStyle::Code:
insertComponentAtCursor(MessageComponentType::Code);
return;
case TextStyle::Quote:
insertComponentAtCursor(MessageComponentType::Quote);
return;
default:
return;
}
}
void ChatBarMessageContentModel::insertComponentAtCursor(MessageComponentType::Type type)
{
if (m_components[m_currentFocusComponent.row()].type == type) {
if (type == MessageComponentType::Text && focusedDocumentHandler()) {
focusedDocumentHandler()->setStyle(ChatDocumentHandler::Paragraph);
focusedDocumentHandler()->setStyle(TextStyle::Paragraph);
}
return;
}

View File

@@ -9,6 +9,7 @@
#include "chatdocumenthandler.h"
#include "enums/messagecomponenttype.h"
#include "enums/textstyle.h"
#include "messagecomponent.h"
#include "models/messagecontentmodel.h"
@@ -55,6 +56,8 @@ public:
Q_INVOKABLE void refocusCurrentComponent() const;
ChatDocumentHandler *focusedDocumentHandler() const;
Q_INVOKABLE void insertStyleAtCursor(TextStyle::Style style);
Q_INVOKABLE void insertComponentAtCursor(MessageComponentType::Type type);
Q_INVOKABLE void addAttachment(const QUrl &path);