Update the look of the chatbar to be floating with the rich text controls on top and send buttons inline

This commit is contained in:
James Graham
2026-01-17 15:46:00 +00:00
parent 79de8a792c
commit 6b318ec754
25 changed files with 945 additions and 806 deletions

View File

@@ -361,7 +361,6 @@ Kirigami.Page {
id: chatBar
width: parent.width
currentRoom: root.currentRoom
connection: root.currentRoom.connection as NeoChatConnection
// Creating a reply (or doing anything in the chat bar) can change the height, but this isn't picked up on the root's onHeightChanged.
onHeightChanged: root.resetViewSettling()

View File

@@ -9,6 +9,7 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
AttachDialog.qml
ChatBar.qml
RichEditBar.qml
SendBar.qml
CompletionMenu.qml
EmojiDelegate.qml
EmojiGrid.qml
@@ -17,6 +18,7 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
EmojiDialog.qml
EmojiTonesPicker.qml
StylePicker.qml
StyleDelegate.qml
ImageEditorPage.qml
VoiceMessageDialog.qml
ImageDialog.qml
@@ -24,6 +26,7 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
LocationChooser.qml
NewPollDialog.qml
TableDialog.qml
StyleButton.qml
SOURCES
chatbuttonhelper.cpp
styledelegatehelper.cpp

View File

@@ -25,20 +25,14 @@ import org.kde.neochat.libneochat as LibNeoChat
*
* @sa ChatBar
*/
QQC2.Control {
Item {
id: root
/**
* @brief The current room that user is viewing.
*/
required property NeoChatRoom currentRoom
required property NeoChatConnection connection
onActiveFocusChanged: chatContentView.itemAt(contentModel.index(contentModel.focusRow, 0)).forceActiveFocus()
required property LibNeoChat.NeoChatRoom currentRoom
onCurrentRoomChanged: {
_private.chatBarCache = currentRoom.mainCache
if (ShareHandler.text.length > 0 && ShareHandler.room === root.currentRoom.id) {
contentModel.focusedTextItem.
textField.text = ShareHandler.text;
@@ -47,37 +41,7 @@ QQC2.Control {
}
}
Connections {
target: contentModel.keyHelper
function onUnhandledUp(isCompleting: bool): void {
if (!isCompleting) {
return;
}
completionMenu.decrementIndex();
}
function onUnhandledDown(isCompleting: bool): void {
if (!isCompleting) {
return;
}
completionMenu.incrementIndex();
}
function onUnhandledTab(isCompleting: bool): void {
if (!isCompleting) {
return;
}
completionMenu.completeCurrent();
}
function onUnhandledReturn(isCompleting: bool): void {
if (!isCompleting) {
return;
}
completionMenu.completeCurrent();
}
}
onActiveFocusChanged: chatContentView.itemAt(contentModel.index(contentModel.focusRow, 0)).forceActiveFocus()
Connections {
target: ShareHandler
@@ -100,64 +64,111 @@ QQC2.Control {
}
}
spacing: 0
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Message.room: root.currentRoom
Message.contentModel: contentModel
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Separator {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
}
}
implicitHeight: chatBar.implicitHeight + Kirigami.Units.largeSpacing
height: Math.max(Math.min(chatScrollView.contentHeight + bottomPadding + topPadding, Kirigami.Units.gridUnit * 10), Kirigami.Units.gridUnit * 5)
leftPadding: rightPadding
rightPadding: (root.width - chatBarSizeHelper.availableWidth) / 2 + Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
QQC2.Control {
id: chatBar
contentItem: ColumnLayout {
QQC2.ScrollView {
id: chatScrollView
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
anchors.top: root.top
anchors.horizontalCenter: root.horizontalCenter
clip: true
spacing: 0
ColumnLayout {
width: chatScrollView.width
spacing: Kirigami.Units.smallSpacing
width: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
Repeater {
id: chatContentView
model: ChatBarMessageContentModel {
id: contentModel
type: ChatBarType.Room
room: root.currentRoom
contentItem: ColumnLayout {
RichEditBar {
id: richEditBar
visible: NeoChatConfig.sendMessageWith === 1
maxAvailableWidth: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
room: root.currentRoom
contentModel: chatContentView.model
onClicked: contentModel.refocusCurrentComponent()
}
Kirigami.Separator {
Layout.fillWidth: true
visible: NeoChatConfig.sendMessageWith === 1
}
RowLayout {
spacing: 0
QQC2.ScrollView {
id: chatScrollView
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
clip: true
ColumnLayout {
width: chatScrollView.width
spacing: Kirigami.Units.smallSpacing
Repeater {
id: chatContentView
model: ChatBarMessageContentModel {
id: contentModel
type: ChatBarType.Room
room: root.currentRoom
sendMessageWithEnter: NeoChatConfig.sendMessageWith === 0
}
delegate: MessageComponentChooser {}
}
}
delegate: MessageComponentChooser {}
}
SendBar {
room: root.currentRoom
contentModel: chatContentView.model
}
}
}
RichEditBar {
id: richEditBar
Layout.alignment: Qt.AlignCenter
maxAvailableWidth: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
room: root.currentRoom
contentModel: chatContentView.model
background: Kirigami.ShadowedRectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
onClicked: contentModel.refocusCurrentComponent()
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
border {
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
width: 1
}
}
}
MouseArea {
id: hoverArea
anchors {
top: chatModeButton.top
left: root.left
right: root.right
bottom: chatBar.top
}
propagateComposedEvents: true
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
QQC2.Button {
id: chatModeButton
anchors {
bottom: chatBar.top
bottomMargin: Kirigami.Units.smallSpacing
horizontalCenter: root.horizontalCenter
}
visible: hoverArea.containsMouse || hovered || chatBar.hovered
width: Kirigami.Units.iconSizes.enormous
height: Kirigami.Units.iconSizes.smallMedium
icon.name: NeoChatConfig.sendMessageWith === 0 ? "arrow-up" : "arrow-down"
onClicked: NeoChatConfig.sendMessageWith = NeoChatConfig.sendMessageWith === 0 ? 1 : 0
}
LibNeoChat.DelegateSizeHelper {
id: chatBarSizeHelper
@@ -171,35 +182,23 @@ QQC2.Control {
QtObject {
id: _private
property ChatBarCache chatBarCache
onChatBarCacheChanged: {
richEditBar.chatBarCache = chatBarCache
}
function pasteImage() {
let localPath = Clipboard.saveImage();
if (localPath.length === 0) {
return false;
}
_private.chatBarCache.attachmentPath = localPath;
return true;
}
}
property LibNeoChat.CompletionModel completionModel: LibNeoChat.CompletionModel {
room: root.currentRoom
type: LibNeoChat.ChatBarType.Room
textItem: contentModel.focusedTextItem
roomListModel: RoomManager.roomListModel
userListModel: RoomManager.userListModel
CompletionMenu {
id: completionMenu
room: root.currentRoom
type: LibNeoChat.ChatBarType.Room
textItem: contentModel.focusedTextItem
onIsCompletingChanged: {
if (!isCompleting) {
return;
}
x: 1
y: -height
width: parent.width - 1
Behavior on height {
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
let dialog = Qt.createComponent('org.kde.neochat.chatbar', 'CompletionMenu').createObject(contentModel.focusedTextItem.textItem, {
model: _private.completionModel,
keyHelper: contentModel.keyHelper
}).open();
}
}
}

View File

@@ -11,33 +11,55 @@ import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
import org.kde.neochat.libneochat as LibNeoChat
QQC2.Popup {
id: root
/**
* @brief The current room that user is viewing.
*/
required property LibNeoChat.NeoChatRoom room
property alias model: completions.model
/**
* @brief The chatbar type
*/
required property int type
required property LibNeoChat.ChatKeyHelper keyHelper
/**
* @brief The chatbar type
*/
required property LibNeoChat.ChatTextItemHelper textItem
Connections {
target: keyHelper
visible: completions.count > 0
function onUnhandledUp(isCompleting: bool): void {
if (!isCompleting) {
return;
}
root.decrementIndex();
}
onVisibleChanged: if (visible) {
root.open();
function onUnhandledDown(isCompleting: bool): void {
if (!isCompleting) {
return;
}
root.incrementIndex();
}
function onUnhandledTab(isCompleting: bool): void {
if (!isCompleting) {
return;
}
root.completeCurrent();
}
function onUnhandledReturn(isCompleting: bool): void {
if (!isCompleting) {
return;
}
root.completeCurrent();
}
function onCloseCompletion(): void {
root.close();
root.model.ignoreCurrentCompletion();
}
}
x: model.textItem.textItem.cursorRectangle.x - Kirigami.Units.largeSpacing
y: model.textItem.textItem.cursorRectangle.y - implicitHeight - Kirigami.Units.smallSpacing
function incrementIndex() {
completions.incrementCurrentIndex();
}
@@ -47,11 +69,11 @@ QQC2.Popup {
}
function complete(text: string, hRef: string) {
completionModel.insertCompletion(text, hRef);
model.insertCompletion(text, hRef);
}
function completeCurrent() {
completionModel.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef);
model.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef);
}
leftPadding: 0
@@ -61,70 +83,57 @@ QQC2.Popup {
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
contentItem: ColumnLayout {
spacing: 0
Kirigami.Separator {
Layout.fillWidth: true
}
QQC2.ScrollView {
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
Layout.maximumHeight: Kirigami.Units.gridUnit * 10
contentItem: QQC2.ScrollView {
contentWidth: Kirigami.Units.gridUnit * 20
background: Rectangle {
color: Kirigami.Theme.backgroundColor
}
ListView {
id: completions
currentIndex: 0
keyNavigationWraps: true
highlightMoveDuration: 100
onCountChanged: currentIndex = 0
delegate: Delegates.RoundedItemDelegate {
id: completionDelegate
ListView {
id: completions
required property int index
required property string displayName
required property string subtitle
required property string iconName
required property string replacedText
required property url hRef
model: LibNeoChat.CompletionModel {
id: completionModel
room: root.room
type: root.type
textItem: root.textItem
roomListModel: RoomManager.roomListModel
userListModel: RoomManager.userListModel
}
currentIndex: 0
keyNavigationWraps: true
highlightMoveDuration: 100
onCountChanged: currentIndex = 0
delegate: Delegates.RoundedItemDelegate {
id: completionDelegate
text: displayName
required property int index
required property string displayName
required property string subtitle
required property string iconName
required property string replacedText
required property url hRef
text: displayName
contentItem: RowLayout {
KirigamiComponents.Avatar {
visible: completionDelegate.iconName !== "invalid"
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
name: completionDelegate.text
}
Delegates.SubtitleContentItem {
itemDelegate: completionDelegate
labelItem.textFormat: Text.PlainText
labelItem.clip: true // Intentional to limit insane Unicode in display names
subtitle: completionDelegate.subtitle ?? ""
subtitleItem.textFormat: Text.PlainText
}
contentItem: RowLayout {
KirigamiComponents.Avatar {
visible: completionDelegate.iconName !== "invalid"
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
name: completionDelegate.text
}
Delegates.SubtitleContentItem {
itemDelegate: completionDelegate
labelItem.textFormat: Text.PlainText
labelItem.clip: true // Intentional to limit insane Unicode in display names
subtitle: completionDelegate.subtitle ?? ""
subtitleItem.textFormat: Text.PlainText
}
onClicked: completionModel.insertCompletion(replacedText, hRef)
}
onClicked: root.model.insertCompletion(replacedText, hRef)
}
}
}
background: Rectangle {
background: Kirigami.ShadowedRectangle {
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.View
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
border {
width: 1
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
}
}
}

View File

@@ -11,7 +11,7 @@ import org.kde.kirigami as Kirigami
import org.kde.neochat.libneochat as LibNeoChat
import org.kde.neochat.messagecontent as MessageContent
QQC2.ToolBar {
RowLayout {
id: root
/**
@@ -19,41 +19,39 @@ QQC2.ToolBar {
*/
required property LibNeoChat.NeoChatRoom room
property LibNeoChat.ChatBarCache chatBarCache
required property MessageContent.ChatBarMessageContentModel contentModel
required property real maxAvailableWidth
readonly property real uncompressedImplicitWidth: textFormatRow.implicitWidth +
readonly property real uncompressedImplicitWidth: boldButton.implicitWidth +
italicButton.implicitWidth +
extraTextFormatRow.implicitWidth +
listRow.implicitWidth +
styleButton.implicitWidth +
emojiButton.implicitWidth +
linkButton.implicitWidth +
sendRow.implicitWidth +
sendButton.implicitWidth +
buttonRow.spacing * 9 +
3
root.spacing * 7 +
Kirigami.Units.gridUnit
readonly property real listCompressedImplicitWidth: textFormatRow.implicitWidth +
readonly property real listCompressedImplicitWidth: boldButton.implicitWidth +
italicButton.implicitWidth +
extraTextFormatRow.implicitWidth +
compressedListButton.implicitWidth +
styleButton.implicitWidth +
styleButton.uncompressedWidth +
emojiButton.implicitWidth +
linkButton.implicitWidth +
sendRow.implicitWidth +
sendButton.implicitWidth +
buttonRow.spacing * 9 +
3
root.spacing * 7 +
Kirigami.Units.gridUnit
readonly property real textFormatCompressedImplicitWidth: compressedTextFormatButton.implicitWidth +
compressedListButton.implicitWidth +
styleButton.implicitWidth +
emojiButton.implicitWidth +
linkButton.implicitWidth +
sendRow.implicitWidth +
sendButton.implicitWidth +
buttonRow.spacing * 9 +
3
readonly property real extraTextCompressedImplicitWidth: boldButton.implicitWidth +
italicButton.implicitWidth +
compressedExtraTextFormatButton.implicitWidth +
compressedListButton.implicitWidth +
styleButton.uncompressedWidth +
emojiButton.implicitWidth +
linkButton.implicitWidth +
root.spacing * 7 +
Kirigami.Units.gridUnit
readonly property ChatButtonHelper chatButtonHelper: ChatButtonHelper {
textItem: contentModel.focusedTextItem
@@ -61,128 +59,111 @@ QQC2.ToolBar {
signal clicked
RowLayout {
id: buttonRow
RowLayout {
id: textFormatRow
visible: root.maxAvailableWidth > root.listCompressedImplicitWidth
QQC2.ToolButton {
id: boldButton
Shortcut {
sequence: "Ctrl+B"
onActivated: boldButton.clicked()
}
icon.name: "format-text-bold"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Bold")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.bold
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Bold);
root.clicked()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: italicButton
Shortcut {
sequence: "Ctrl+I"
onActivated: italicButton.clicked()
}
icon.name: "format-text-italic"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Italic")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.italic
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Italic);
root.clicked()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: underlineButton
Shortcut {
sequence: "Ctrl+U"
onActivated: underlineButton.clicked()
}
icon.name: "format-text-underline"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Underline")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.underline
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Underline);
root.clicked();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
icon.name: "format-text-strikethrough"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Strikethrough")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.strikethrough
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Strikethrough);
root.clicked()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: boldButton
Shortcut {
sequence: "Ctrl+B"
onActivated: boldButton.clicked()
}
icon.name: "format-text-bold"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Bold")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.bold
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Bold);
root.clicked()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: italicButton
Shortcut {
sequence: "Ctrl+I"
onActivated: italicButton.clicked()
}
icon.name: "format-text-italic"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Italic")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.italic
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Italic);
root.clicked()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
RowLayout {
id: extraTextFormatRow
visible: root.maxAvailableWidth > root.listCompressedImplicitWidth
QQC2.ToolButton {
id: compressedTextFormatButton
visible: root.maxAvailableWidth < root.listCompressedImplicitWidth
icon.name: "dialog-text-and-font"
id: underlineButton
Shortcut {
sequence: "Ctrl+U"
onActivated: underlineButton.clicked()
}
icon.name: "format-text-underline"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Format Text")
text: i18nc("@action:button", "Underline")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: compressedTextFormatMenu.visible
checked: root.chatButtonHelper.underline
onClicked: {
compressedTextFormatMenu.open()
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Underline);
root.clicked();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
icon.name: "format-text-strikethrough"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Strikethrough")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.strikethrough
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Strikethrough);
root.clicked()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
QQC2.ToolButton {
id: compressedExtraTextFormatButton
visible: root.maxAvailableWidth < root.listCompressedImplicitWidth
icon.name: "dialog-text-and-font"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Format Text")
display: QQC2.AbstractButton.IconOnly
checkable: true
onClicked: {
let dialog = compressedTextFormatMenu.createObject(compressedExtraTextFormatButton)
dialog.onClosed.connect(() => {
compressedExtraTextFormatButton.checked = false;
});
dialog.open();
compressedExtraTextFormatButton.checked = true;
}
Component {
id: compressedTextFormatMenu
QQC2.Menu {
id: compressedTextFormatMenu
y: -implicitHeight
QQC2.MenuItem {
icon.name: "format-text-bold"
text: i18nc("@action:button", "Bold")
checkable: true
checked: root.chatButtonHelper.bold
onTriggered: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Bold);
root.clicked();
}
}
QQC2.MenuItem {
icon.name: "format-text-italic"
text: i18nc("@action:button", "Italic")
checkable: true
checked: root.chatButtonHelper.italic
onTriggered: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.Italic);
root.clicked();
}
}
QQC2.MenuItem {
icon.name: "format-text-underline"
text: i18nc("@action:button", "Underline")
@@ -204,370 +185,205 @@ QQC2.ToolBar {
}
}
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
StyleButton {
id: styleButton
Layout.minimumWidth: compressed ? -1 : Kirigami.Units.gridUnit * 8 + Kirigami.Units.largeSpacing * 2
icon.name: "typewriter"
text: i18nc("@action:button", "Text Style")
style: root.chatButtonHelper.currentStyle
compressed: root.maxAvailableWidth < root.extraTextCompressedImplicitWidth
enabled: root.chatButtonHelper.styleFormatEnabled
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: styleMenu.visible
onClicked: {
if (styleMenu.visible) {
styleMenu.close();
return;
}
open = true;
styleMenu.open();
}
StylePicker {
id: styleMenu
chatContentModel: root.contentModel
chatButtonHelper: root.chatButtonHelper
onClosed: {
root.clicked()
styleButton.open = false;
}
}
}
QQC2.ToolButton {
id: emojiButton
property bool isBusy: false
visible: !Kirigami.Settings.isMobile
icon.name: "smiley"
text: i18n("Emojis & Stickers")
display: QQC2.AbstractButton.IconOnly
checkable: true
onClicked: {
let dialog = emojiDialog.createObject(root).open();
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
}
QQC2.ToolButton {
id: linkButton
icon.name: "insert-link-symbolic"
text: i18nc("@action:button", "Insert link")
display: QQC2.AbstractButton.IconOnly
onClicked: {
let dialog = linkDialog.createObject(QQC2.Overlay.overlay, {
linkText: root.chatButtonHelper.currentLinkText,
linkUrl: root.chatButtonHelper.currentLinkUrl
})
dialog.onAccepted.connect(() => {
root.chatButtonHelper.updateLink(dialog.linkUrl, dialog.linkText)
root.clicked();
});
dialog.open();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
RowLayout {
id: listRow
visible: root.maxAvailableWidth > root.uncompressedImplicitWidth
QQC2.ToolButton {
icon.name: "format-list-unordered"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Unordered List")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.unorderedList
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList);
root.clicked();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
Kirigami.Separator {
Layout.fillHeight: true
Layout.margins: 0
QQC2.ToolButton {
icon.name: "format-list-ordered"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Ordered List")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.orderedlist
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList);
root.clicked();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
RowLayout {
id: listRow
visible: root.maxAvailableWidth > root.uncompressedImplicitWidth
QQC2.ToolButton {
QQC2.ToolButton {
id: indentAction
icon.name: "format-indent-more"
enabled: chatButtonHelper.richFormatEnabled && root.chatButtonHelper.canIndentListMore
text: i18nc("@action:button", "Increase List Level")
display: QQC2.AbstractButton.IconOnly
onClicked: {
root.chatButtonHelper.indentListMore();
root.clicked();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: dedentAction
icon.name: "format-indent-less"
enabled: chatButtonHelper.richFormatEnabled && root.chatButtonHelper.canIndentListLess
text: i18nc("@action:button", "Decrease List Level")
display: QQC2.AbstractButton.IconOnly
onClicked: {
root.chatButtonHelper.indentListLess();
root.clicked();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
QQC2.ToolButton {
id: compressedListButton
enabled: chatButtonHelper.richFormatEnabled
visible: root.maxAvailableWidth < root.uncompressedImplicitWidth
icon.name: "format-list-unordered"
text: i18nc("@action:button", "List Style")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: compressedListMenu.visible
onClicked: {
compressedListMenu.open()
}
QQC2.Menu {
id: compressedListMenu
y: -implicitHeight
QQC2.MenuItem {
icon.name: "format-list-unordered"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Unordered List")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.unorderedList
onClicked: {
onTriggered: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList);
root.clicked();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
QQC2.MenuItem {
icon.name: "format-list-ordered"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Ordered List")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: root.chatButtonHelper.orderedlist
onClicked: {
onTriggered: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList);
root.clicked();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: indentAction
QQC2.MenuItem {
icon.name: "format-indent-more"
enabled: chatButtonHelper.richFormatEnabled && root.chatButtonHelper.canIndentListMore
text: i18nc("@action:button", "Increase List Level")
display: QQC2.AbstractButton.IconOnly
onClicked: {
enabled: root.chatButtonHelper.canIndentListMore
onTriggered: {
root.chatButtonHelper.indentListMore();
root.clicked();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: dedentAction
QQC2.MenuItem {
icon.name: "format-indent-less"
enabled: chatButtonHelper.richFormatEnabled && root.chatButtonHelper.canIndentListLess
text: i18nc("@action:button", "Decrease List Level")
display: QQC2.AbstractButton.IconOnly
onClicked: {
enabled: root.chatButtonHelper.canIndentListLess
onTriggered: {
root.chatButtonHelper.indentListLess();
root.clicked();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
QQC2.ToolButton {
id: compressedListButton
enabled: chatButtonHelper.richFormatEnabled
visible: root.maxAvailableWidth < root.uncompressedImplicitWidth
icon.name: "format-list-unordered"
text: i18nc("@action:button", "List Style")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: compressedListMenu.visible
onClicked: {
compressedListMenu.open()
}
QQC2.Menu {
id: compressedListMenu
y: -implicitHeight
QQC2.MenuItem {
icon.name: "format-list-unordered"
text: i18nc("@action:button", "Unordered List")
onTriggered: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList);
root.clicked();
}
}
QQC2.MenuItem {
icon.name: "format-list-ordered"
text: i18nc("@action:button", "Ordered List")
onTriggered: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList);
root.clicked();
}
}
QQC2.MenuItem {
icon.name: "format-indent-more"
text: i18nc("@action:button", "Increase List Level")
enabled: root.chatButtonHelper.canIndentListMore
onTriggered: {
root.chatButtonHelper.indentListMore();
root.clicked();
}
}
QQC2.MenuItem {
icon.name: "format-indent-less"
text: i18nc("@action:button", "Decrease List Level")
enabled: root.chatButtonHelper.canIndentListLess
onTriggered: {
root.chatButtonHelper.indentListLess();
root.clicked();
}
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: styleButton
icon.name: "typewriter"
text: i18nc("@action:button", "Text Style")
enabled: root.chatButtonHelper.styleFormatEnabled
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: styleMenu.visible
onClicked: {
if (styleMenu.visible) {
styleMenu.close();
return;
}
styleMenu.open()
}
StylePicker {
id: styleMenu
chatContentModel: root.contentModel
chatButtonHelper: root.chatButtonHelper
onClosed: root.clicked()
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
Kirigami.Separator {
Layout.fillHeight: true
Layout.margins: 0
}
QQC2.ToolButton {
id: emojiButton
property bool isBusy: false
visible: !Kirigami.Settings.isMobile
icon.name: "smiley"
text: i18n("Emojis & Stickers")
display: QQC2.AbstractButton.IconOnly
checkable: true
onClicked: {
let dialog = emojiDialog.createObject(root).open();
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
}
QQC2.ToolButton {
id: linkButton
icon.name: "insert-link-symbolic"
text: i18nc("@action:button", "Insert link")
display: QQC2.AbstractButton.IconOnly
onClicked: {
let dialog = linkDialog.createObject(QQC2.Overlay.overlay, {
linkText: root.chatButtonHelper.currentLinkText,
linkUrl: root.chatButtonHelper.currentLinkUrl
})
dialog.onAccepted.connect(() => {
root.chatButtonHelper.updateLink(dialog.linkUrl, dialog.linkText)
root.clicked();
});
dialog.open();
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
Kirigami.Separator {
Layout.fillHeight: true
Layout.margins: 0
}
RowLayout {
id: sendRow
visible: root.maxAvailableWidth > root.textFormatCompressedImplicitWidth
QQC2.ToolButton {
id: attachmentButton
property bool isBusy: root.room && root.room.hasFileUploading
visible: root.chatBarCache.attachmentPath.length === 0
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
display: QQC2.AbstractButton.IconOnly
onClicked: {
if (!root.contentModel.hasRichFormatting) {
fileDialog();
return;
}
let warningDialog = Qt.createComponent('org.kde.kirigami', 'PromptDialog').createObject(QQC2.Overlay.overlay, {
dialogType: Kirigami.PromptDialog.Warning,
title: i18n("Attach an image or file?"),
subtitle: i18n("Attachments can only have plain text captions, all rich formatting will be removed"),
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
});
warningDialog.onAccepted.connect(() => {
attachmentButton.fileDialog();
});
warningDialog.open();
}
function fileDialog(): void {
let dialog = (LibNeoChat.Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
dialog.chosen.connect(path => root.contentModel.addAttachment(path));
dialog.open();
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
}
QQC2.ToolButton {
id: mapButton
icon.name: "globe"
property bool isBusy: false
text: i18n("Send a Location")
display: QQC2.AbstractButton.IconOnly
onClicked: {
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {
room: root.room
}).open();
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
}
QQC2.ToolButton {
id: pollButton
icon.name: "amarok_playcount"
property bool isBusy: false
text: i18nc("@action:button", "Create a Poll")
display: QQC2.AbstractButton.IconOnly
onClicked: {
newPollDialog.createObject(QQC2.Overlay.overlay, {
room: root.room
}).open();
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
}
}
QQC2.ToolButton {
id: compressedSendButton
visible: root.maxAvailableWidth < root.textFormatCompressedImplicitWidth
icon.name: "overflow-menu"
text: i18nc("@action:button", "Send Other")
display: QQC2.AbstractButton.IconOnly
checkable: true
checked: compressedSendMenu.visible
onClicked: {
compressedSendMenu.open()
}
QQC2.Menu {
id: compressedSendMenu
y: -implicitHeight
QQC2.MenuItem {
visible: root.chatBarCache.attachmentPath.length === 0
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
onTriggered: {
let dialog = (LibNeoChat.Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = path);
dialog.open();
}
}
QQC2.MenuItem {
icon.name: "globe"
text: i18n("Send a Location")
onTriggered: {
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {
room: root.room
}).open();
}
}
QQC2.MenuItem {
icon.name: "amarok_playcount"
text: i18nc("@action:button", "Create a Poll")
onTriggered: {
newPollDialog.createObject(QQC2.Overlay.overlay, {
room: root.room
}).open();
}
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.ToolButton {
id: sendButton
property bool isBusy: false
icon.name: "document-send"
text: i18n("Send message")
display: QQC2.AbstractButton.IconOnly
onClicked: root.contentModel.postMessage();
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
}
}
background: Kirigami.ShadowedRectangle {
color: Kirigami.Theme.backgroundColor
radius: 5
shadow {
size: 15
yOffset: 3
color: Qt.rgba(0, 0, 0, 0.2)
}
border {
color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
width: 1
}
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Window
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
Component {
@@ -575,21 +391,6 @@ QQC2.ToolBar {
LinkDialog {}
}
Component {
id: attachDialog
AttachDialog {
anchors.centerIn: parent
}
}
Component {
id: openFileDialog
LibNeoChat.OpenFileDialog {
parentWindow: Window.window
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
}
}
Component {
id: emojiDialog
EmojiDialog {

131
src/chatbar/SendBar.qml Normal file
View File

@@ -0,0 +1,131 @@
// 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
import QtCore
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
RowLayout {
id: root
/**
* @brief The current room that user is viewing.
*/
required property LibNeoChat.NeoChatRoom room
property LibNeoChat.ChatBarCache chatBarCache
required property MessageContent.ChatBarMessageContentModel contentModel
Kirigami.Separator {
Layout.fillHeight: true
}
QQC2.ToolButton {
id: attachmentButton
property bool isBusy: root.room && root.room.hasFileUploading
visible: root.chatBarCache.attachmentPath.length === 0
icon.name: "mail-attachment"
text: i18n("Attach an image or file")
display: QQC2.AbstractButton.IconOnly
onClicked: {
if (!root.contentModel.hasRichFormatting) {
if (LibNeoChat.Clipboard.hasImage) {
attachDialog();
} else {
fileDialog();
}
return;
}
let warningDialog = Qt.createComponent('org.kde.kirigami', 'PromptDialog').createObject(QQC2.Overlay.overlay, {
dialogType: Kirigami.PromptDialog.Warning,
title: i18n("Attach an image or file?"),
subtitle: i18n("Attachments can only have plain text captions, all rich formatting will be removed"),
standardButtons: Kirigami.Dialog.Ok | Kirigami.Dialog.Cancel
});
warningDialog.onAccepted.connect(() => {
if (LibNeoChat.Clipboard.hasImage) {
attachmentButton.attachDialog();
} else {
attachmentButton.fileDialog();
}
});
warningDialog.open();
}
function attachDialog(): void {
let dialog = Qt.createComponent('org.kde.neochat.chatbar', 'AttachDialog').createObject(QQC2.Overlay.overlay) as AttachDialog;
dialog.anchors.centerIn = QQC2.Overlay.overlay;
dialog.chosen.connect(path => root.contentModel.addAttachment(path));
dialog.open();
}
function fileDialog(): void {
let dialog = Qt.createComponent('org.kde.neochat.libneochat', 'OpenFileDialog').createObject(QQC2.Overlay.overlay, {
parentWindow: Window.window,
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
});
dialog.chosen.connect(path => root.contentModel.addAttachment(path));
dialog.open();
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
}
QQC2.ToolButton {
id: mapButton
icon.name: "globe"
property bool isBusy: false
text: i18n("Send a Location")
display: QQC2.AbstractButton.IconOnly
onClicked: {
locationChooser.createObject(QQC2.ApplicationWindow.overlay, {
room: root.room
}).open();
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
}
QQC2.ToolButton {
id: pollButton
icon.name: "amarok_playcount"
property bool isBusy: false
text: i18nc("@action:button", "Create a Poll")
display: QQC2.AbstractButton.IconOnly
onClicked: {
newPollDialog.createObject(QQC2.Overlay.overlay, {
room: root.room
}).open();
}
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
}
QQC2.ToolButton {
id: sendButton
property bool isBusy: false
icon.name: "document-send"
text: i18n("Send message")
display: QQC2.AbstractButton.IconOnly
onClicked: root.contentModel.postMessage();
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
QQC2.ToolTip.text: text
}
}

View File

@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: 2026 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
QQC2.AbstractButton {
id: root
required property int style
property bool open: false
property bool compressed: false
readonly property real uncompressedWidth: styleDelegate.implicitWidth + arrowIcon.implicitWidth + 1 + contentRow.spacing * 2 + padding * 2
padding: Kirigami.Units.smallSpacing
icon {
width: Kirigami.Units.iconSizes.smallMedium
height: Kirigami.Units.iconSizes.smallMedium
}
contentItem: RowLayout {
id: contentRow
StyleDelegate {
id: styleDelegate
Layout.fillWidth: true
visible: !root.compressed
style: root.style
sizeText: false
onPressed: root.clicked()
}
Kirigami.Icon {
id: styleIcon
visible: root.compressed
source: root.icon.name
implicitWidth: root.icon.width
implicitHeight: root.icon.height
}
Kirigami.Separator {
Layout.fillHeight: true
}
Kirigami.Icon {
id: arrowIcon
source: root.open ? "arrow-down" : "arrow-up"
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
}
}
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
radius: Kirigami.Units.cornerRadius
border {
width: root.hovered || root.open ? 1 : 0
color: Kirigami.Theme.highlightColor
}
}
}

View File

@@ -0,0 +1,73 @@
// SPDX-FileCopyrightText: 2026 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
QQC2.TextArea {
id: root
required property int style
property bool highlight: false
property bool sizeText: true
leftPadding: lineRow.visible ? lineRow.width + lineRow.anchors.leftMargin + Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing
verticalAlignment: Text.AlignVCenter
readOnly: true
selectByMouse: false
RowLayout {
id: lineRow
anchors {
top: root.top
bottom: root.bottom
left: root.left
leftMargin: Kirigami.Units.smallSpacing
}
visible: root.style === LibNeoChat.RichFormat.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: root
}
background: Rectangle {
color: Kirigami.Theme.backgroundColor
Kirigami.Theme.colorSet: root.style === LibNeoChat.RichFormat.Quote ? Kirigami.Theme.Window : Kirigami.Theme.View
Kirigami.Theme.inherit: false
radius: Kirigami.Units.cornerRadius
border {
width: 1
color: root.highlight ?
Kirigami.Theme.highlightColor :
Kirigami.ColorUtils.linearInterpolation(
Kirigami.Theme.backgroundColor,
Kirigami.Theme.textColor,
Kirigami.Theme.frameContrast
)
}
}
}

View File

@@ -19,6 +19,7 @@ QQC2.Popup {
required property ChatButtonHelper chatButtonHelper
y: -implicitHeight
padding: Kirigami.Units.largeSpacing
contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing
@@ -26,24 +27,21 @@ QQC2.Popup {
Repeater {
model: 9
delegate: QQC2.TextArea {
delegate: StyleDelegate {
id: styleDelegate
required property int index
Layout.fillWidth: true
Layout.minimumWidth: Kirigami.Units.gridUnit * 7
Layout.minimumWidth: Kirigami.Units.gridUnit * 8
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.RichFormat.Paragraph || styleDelegate.index === LibNeoChat.RichFormat.Quote
readOnly: true
selectByMouse: false
style: index
highlight: root.chatButtonHelper.currentStyle === index || hovered
onPressed: (event) => {
if (styleDelegate.index === LibNeoChat.RichFormat.Paragraph ||
styleDelegate.index === LibNeoChat.RichFormat.Code ||
styleDelegate.index === LibNeoChat.RichFormat.Quote
if (index === LibNeoChat.RichFormat.Paragraph ||
index === LibNeoChat.RichFormat.Code ||
index === LibNeoChat.RichFormat.Quote
) {
root.chatContentModel.insertStyleAtCursor(styleDelegate.index);
} else {
@@ -51,71 +49,19 @@ QQC2.Popup {
}
root.close();
}
RowLayout {
id: lineRow
anchors {
top: styleDelegate.top
bottom: styleDelegate.bottom
left: styleDelegate.left
leftMargin: Kirigami.Units.smallSpacing
}
visible: styleDelegate.index === LibNeoChat.RichFormat.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.RichFormat.Quote ? Kirigami.Theme.Window : Kirigami.Theme.View
Kirigami.Theme.inherit: false
radius: Kirigami.Units.cornerRadius
border {
width: 1
color: styleDelegate.hovered || (root.chatButtonHelper.currentStyle === styleDelegate.index) ?
Kirigami.Theme.highlightColor :
Kirigami.ColorUtils.linearInterpolation(
Kirigami.Theme.backgroundColor,
Kirigami.Theme.textColor,
Kirigami.Theme.frameContrast
)
}
}
}
}
}
background: Kirigami.ShadowedRectangle {
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.View
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

@@ -28,6 +28,8 @@ void StyleDelegateHelper::setTextItem(QQuickItem *textItem)
m_textItem = textItem;
if (m_textItem) {
connect(m_textItem, SIGNAL(styleChanged()), this, SLOT(formatDocument()));
connect(m_textItem, SIGNAL(styleChanged()), this, SLOT(formatDocument()));
if (document()) {
formatDocument();
}
@@ -59,10 +61,11 @@ void StyleDelegateHelper::formatDocument()
cursor.beginEditBlock();
cursor.select(QTextCursor::Document);
cursor.removeSelectedText();
const auto style = static_cast<RichFormat::Format>(m_textItem->property("index").toInt());
const auto style = static_cast<RichFormat::Format>(m_textItem->property("style").toInt());
const auto string = RichFormat::styleString(style);
const int headingLevel = style <= 6 ? style : 0;
const auto sizeText = static_cast<RichFormat::Format>(m_textItem->property("sizeText").toBool());
const int headingLevel = style <= 6 && sizeText ? 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;

View File

@@ -32,5 +32,6 @@ private:
QPointer<QQuickItem> m_textItem;
QTextDocument *document() const;
private Q_SLOTS:
void formatDocument();
};

View File

@@ -6,6 +6,7 @@
#include "chattextitemhelper.h"
#include "clipboard.h"
#include "neochatroom.h"
#include <qnamespace.h>
ChatKeyHelper::ChatKeyHelper(QObject *parent)
: QObject(parent)
@@ -29,7 +30,10 @@ bool ChatKeyHelper::handleKey(Qt::Key key, Qt::KeyboardModifiers modifiers)
return backspace();
case Qt::Key_Enter:
case Qt::Key_Return:
return insertReturn();
return insertReturn(modifiers);
case Qt::Key_Escape:
case Qt::Key_Cancel:
return cancel();
default:
return false;
}
@@ -155,16 +159,28 @@ bool ChatKeyHelper::backspace()
return false;
}
bool ChatKeyHelper::insertReturn()
bool ChatKeyHelper::insertReturn(Qt::KeyboardModifiers modifiers)
{
if (!textItem) {
return false;
}
if (textItem->isCompleting) {
bool shiftPressed = modifiers.testFlag(Qt::ShiftModifier);
if (shiftPressed && !sendMessageWithEnter) {
Q_EMIT unhandledReturn(false);
return true;
}
if (!shiftPressed && textItem->isCompleting) {
Q_EMIT unhandledReturn(true);
return true;
}
if (!shiftPressed && sendMessageWithEnter) {
Q_EMIT unhandledReturn(false);
return true;
}
QTextCursor cursor = textItem->textCursor();
if (cursor.isNull()) {
return false;
@@ -173,6 +189,18 @@ bool ChatKeyHelper::insertReturn()
return true;
}
bool ChatKeyHelper::cancel()
{
if (!textItem) {
return false;
}
if (textItem->isCompleting) {
Q_EMIT closeCompletion();
return true;
}
return false;
}
bool ChatKeyHelper::pasteImage()
{
if (!textItem) {

View File

@@ -45,6 +45,15 @@ public:
*/
Q_INVOKABLE bool handleKey(Qt::Key key, Qt::KeyboardModifiers modifiers);
/**
* @brief Whether the enter/return should send message.
*
* If false, return/enter adds a new line.
*
* shift + return/enter does the opposite to return/enter.
*/
bool sendMessageWithEnter = true;
Q_SIGNALS:
/**
* @brief There is an unhandled up key press.
@@ -98,6 +107,14 @@ Q_SIGNALS:
*/
void unhandledReturn(bool isCompleting);
/**
* @brief The completion dialog should be closed if open.
*
* Current trigger conditions:
* - Return clicked when a completion has been started.
*/
void closeCompletion();
/**
* @brief An image has been pasted.
*/
@@ -116,7 +133,9 @@ private:
bool backspace();
bool insertReturn();
bool insertReturn(Qt::KeyboardModifiers modifiers);
bool cancel();
bool pasteImage();
};

View File

@@ -342,6 +342,14 @@ std::optional<int> ChatTextItemHelper::cursorPosition() const
return m_textItem->property("cursorPosition").toInt();
}
QRect ChatTextItemHelper::cursorRectangle() const
{
if (!m_textItem) {
return {};
}
return m_textItem->property("cursorRectangle").toRect();
}
int ChatTextItemHelper::selectionStart() const
{
if (!m_textItem) {

View File

@@ -172,6 +172,11 @@ public:
*/
std::optional<int> cursorPosition() const;
/**
* @brief Return the rectangle where the cursor of the underlying text item is rendered.
*/
QRect cursorRectangle() const;
/**
* @brief Set the cursor position of the underlying text item to the given value.
*/

View File

@@ -7,27 +7,29 @@
#include <QTextCharFormat>
#include <QTextCursor>
#include <KLocalizedString>
QString RichFormat::styleString(Format format)
{
switch (format) {
case Paragraph:
return u"Paragraph"_s;
return i18nc("As in the default paragraph text style in the chat bar", "Paragraph Style");
case Heading1:
return u"Heading 1"_s;
return i18nc("As in heading level 1 text style in the chat bar", "Heading 1");
case Heading2:
return u"Heading 2"_s;
return i18nc("As in heading level 2 text style in the chat bar", "Heading 2");
case Heading3:
return u"Heading 3"_s;
return i18nc("As in heading level 3 text style in the chat bar", "Heading 3");
case Heading4:
return u"Heading 4"_s;
return i18nc("As in heading level 4 text style in the chat bar", "Heading 4");
case Heading5:
return u"Heading 5"_s;
return i18nc("As in heading level 5 text style in the chat bar", "Heading 5");
case Heading6:
return u"Heading 6"_s;
return i18nc("As in heading level 6 text style in the chat bar", "Heading 6");
case Code:
return u"Code"_s;
return i18nc("As in code text style in the chat bar", "Code");
case Quote:
return u"\"Quote\""_s;
return i18nc("As in quote text style in the chat bar", "\"Quote\"");
default:
return {};
}

View File

@@ -77,6 +77,21 @@ void CompletionModel::setTextItem(ChatTextItemHelper *textItem)
Q_EMIT textItemChanged();
}
bool CompletionModel::isCompleting() const
{
if (!m_textItem) {
return false;
}
return m_textItem->isCompleting;
}
void CompletionModel::ignoreCurrentCompletion()
{
m_ignoreCurrentCompletion = true;
m_textItem->isCompleting = false;
Q_EMIT isCompletingChanged();
}
void CompletionModel::updateTextStart()
{
auto cursor = m_textItem->textCursor();
@@ -193,6 +208,15 @@ void CompletionModel::updateCompletion()
if (cursor.isNull()) {
return;
}
if (m_ignoreCurrentCompletion) {
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
if (cursor.selectedText() == u' ') {
m_ignoreCurrentCompletion = false;
}
return;
}
cursor.setPosition(m_textStart);
while (!cursor.selectedText().endsWith(u' ') && !cursor.atBlockEnd()) {
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
@@ -242,6 +266,7 @@ void CompletionModel::updateCompletion()
endResetModel();
m_textItem->isCompleting = rowCount() > 0;
Q_EMIT isCompletingChanged();
}
CompletionModel::AutoCompletionType CompletionModel::autoCompletionType() const

View File

@@ -62,6 +62,11 @@ class CompletionModel : public QAbstractListModel
*/
Q_PROPERTY(UserListModel *userListModel READ userListModel WRITE setUserListModel NOTIFY userListModelChanged)
/**
* @brief The UserListModel to be used for room completions.
*/
Q_PROPERTY(bool isCompleting READ isCompleting NOTIFY isCompletingChanged)
public:
/**
* @brief Defines the different types of completion available.
@@ -98,6 +103,10 @@ public:
ChatTextItemHelper *textItem() const;
void setTextItem(ChatTextItemHelper *textItem);
bool isCompleting() const;
Q_INVOKABLE void ignoreCurrentCompletion();
/**
* @brief Get the given role value at the given index.
*
@@ -137,12 +146,14 @@ Q_SIGNALS:
void autoCompletionTypeChanged();
void roomListModelChanged();
void userListModelChanged();
void isCompletingChanged();
private:
QPointer<NeoChatRoom> m_room;
ChatBarType::Type m_type = ChatBarType::None;
QPointer<ChatTextItemHelper> m_textItem;
bool m_ignoreCurrentCompletion = false;
int m_textStart = 0;
void updateTextStart();

View File

@@ -110,14 +110,10 @@ QQC2.Control {
height: implicitHeight
y: -height - 5
z: 10
<<<<<<< HEAD
chatDocumentHandler: documentHandler
=======
room: root.Message.room
type: root.chatBarCache.isEditing ? ChatBarType.Edit : ChatBarType.Thread
// textItem: textArea
>>>>>>> c7858a151 (Move the remaining functionality of ChatDocumentHandler to ChatTextItemHelper or split into own objects)
margins: 0
Behavior on height {
NumberAnimation {

View File

@@ -60,21 +60,6 @@ RowLayout {
}
}
}
QQC2.Button {
id: cancelButton
anchors.top: root.top
anchors.right: root.right
visible: root.editable
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onClicked: root.Message.room.mainCache.replyId = ""
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
HoverHandler {
cursorShape: Qt.PointingHandCursor
}
@@ -86,5 +71,22 @@ RowLayout {
id: _private
// The space available for the component after taking away the border
readonly property real availableContentWidth: root.Message.maxContentWidth - verticalBorder.implicitWidth - root.spacing
readonly property QQC2.Button cancelButton: QQC2.Button {
id: cancelButton
parent: root
anchors.top: root.top
anchors.right: root.right
visible: root.editable
display: QQC2.AbstractButton.IconOnly
text: i18nc("@action:button", "Cancel reply")
icon.name: "dialog-close"
onClicked: root.Message.room.mainCache.replyId = ""
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
}

View File

@@ -174,6 +174,11 @@ void ChatBarMessageContentModel::connectKeyHelper()
insertComponentAtCursor(MessageComponentType::Text);
}
});
connect(m_keyHelper, &ChatKeyHelper::unhandledReturn, this, [this](bool isCompleting) {
if (!isCompleting) {
postMessage();
}
});
connect(m_keyHelper, &ChatKeyHelper::imagePasted, this, [this](const QString &filePath) {
m_room->cacheForType(m_type)->setAttachmentPath(filePath);
});
@@ -448,6 +453,21 @@ void ChatBarMessageContentModel::removeAttachment()
}
}
bool ChatBarMessageContentModel::sendMessageWithEnter() const
{
return m_sendMessageWithEnter;
}
void ChatBarMessageContentModel::setSendMessageWithEnter(bool sendMessageWithEnter)
{
if (sendMessageWithEnter == m_sendMessageWithEnter) {
return;
}
m_sendMessageWithEnter = sendMessageWithEnter;
m_keyHelper->sendMessageWithEnter = sendMessageWithEnter;
Q_EMIT sendMessageWithEnterChanged();
}
ChatBarMessageContentModel::ComponentIt ChatBarMessageContentModel::removeComponent(ComponentIt it)
{
if (it == m_components.end()) {

View File

@@ -66,6 +66,11 @@ class ChatBarMessageContentModel : public MessageContentModel
*/
Q_PROPERTY(bool hasRichFormatting READ hasRichFormatting NOTIFY hasRichFormattingChanged)
/**
* @brief The UserListModel to be used for room completions.
*/
Q_PROPERTY(bool sendMessageWithEnter READ sendMessageWithEnter WRITE setSendMessageWithEnter NOTIFY sendMessageWithEnterChanged)
public:
explicit ChatBarMessageContentModel(QObject *parent = nullptr);
@@ -89,12 +94,16 @@ public:
Q_INVOKABLE void removeAttachment();
bool sendMessageWithEnter() const;
void setSendMessageWithEnter(bool sendMessageWithEnter);
Q_INVOKABLE void postMessage();
Q_SIGNALS:
void typeChanged();
void focusRowChanged();
void hasRichFormattingChanged();
void sendMessageWithEnterChanged();
private:
ChatBarType::Type m_type = ChatBarType::None;
@@ -125,5 +134,7 @@ private:
void updateCache() const;
QString messageText() const;
bool m_sendMessageWithEnter = true;
void clearModel();
};

View File

@@ -264,32 +264,6 @@ FormCard.FormCardPage {
title: i18nc("Chat Editor", "Editor")
}
FormCard.FormCard {
FormCard.FormRadioDelegate {
text: i18nc("@option:radio", "Send messages with Enter")
checked: NeoChatConfig.sendMessageWith === 0
visible: !Kirigami.Settings.isMobile
enabled: !NeoChatConfig.isSendMessageWithImmutable
onToggled: {
NeoChatConfig.sendMessageWith = 0
NeoChatConfig.save()
}
}
FormCard.FormRadioDelegate {
id: sendWithEnterRadio
text: i18nc("@option:radio", "Send messages with Ctrl+Enter")
checked: NeoChatConfig.sendMessageWith === 1
visible: !Kirigami.Settings.isMobile
enabled: !NeoChatConfig.isSendMessageWithImmutable
onToggled: {
NeoChatConfig.sendMessageWith = 1
NeoChatConfig.save()
}
}
FormCard.FormDelegateSeparator {
visible: !Kirigami.Settings.isMobile
above: sendWithEnterRadio
below: quickEditCheckbox
}
FormCard.FormCheckDelegate {
id: quickEditCheckbox
text: i18n("Use s/text/replacement syntax to edit your last message")

View File

@@ -335,9 +335,13 @@ QQC2.ScrollView {
RowLayout {
id: typingPaneContainer
visible: _private.room && _private.room.otherMembersTyping.length > 0
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors {
left: parent.left
leftMargin: Kirigami.Units.largeSpacing
right: parent.right
bottom: parent.bottom
bottomMargin: Kirigami.Units.smallSpacing
}
height: visible ? typingPane.implicitHeight : 0
z: 2
Behavior on height {

View File

@@ -16,28 +16,19 @@ Loader {
property string labelText: ""
active: visible
sourceComponent: QQC2.Pane {
sourceComponent: QQC2.Control {
id: typingPane
leftPadding: Kirigami.Units.largeSpacing
rightPadding: Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.largeSpacing
FontMetrics {
id: fontMetrics
}
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
contentItem: RowLayout {
spacing: typingPane.spacing
Row {
id: dotRow
property int duration: 400
spacing: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.largeSpacing
Repeater {
model: 3
delegate: Rectangle {
@@ -113,14 +104,16 @@ Loader {
}
}
leftInset: !mirrored ? 0 : -(background as Rectangle).radius
rightInset: mirrored ? 0 : -(background as Rectangle).radius
bottomInset: -(background as Rectangle).radius
background: Rectangle {
background: Kirigami.ShadowedRectangle {
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor
border.color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.2)
border.width: 1
border {
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast)
width: 1
}
}
}
}