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 id: chatBar
width: parent.width width: parent.width
currentRoom: root.currentRoom 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. // 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() onHeightChanged: root.resetViewSettling()

View File

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

View File

@@ -25,20 +25,14 @@ import org.kde.neochat.libneochat as LibNeoChat
* *
* @sa ChatBar * @sa ChatBar
*/ */
QQC2.Control { Item {
id: root id: root
/** /**
* @brief The current room that user is viewing. * @brief The current room that user is viewing.
*/ */
required property NeoChatRoom currentRoom required property LibNeoChat.NeoChatRoom currentRoom
required property NeoChatConnection connection
onActiveFocusChanged: chatContentView.itemAt(contentModel.index(contentModel.focusRow, 0)).forceActiveFocus()
onCurrentRoomChanged: { onCurrentRoomChanged: {
_private.chatBarCache = currentRoom.mainCache
if (ShareHandler.text.length > 0 && ShareHandler.room === root.currentRoom.id) { if (ShareHandler.text.length > 0 && ShareHandler.room === root.currentRoom.id) {
contentModel.focusedTextItem. contentModel.focusedTextItem.
textField.text = ShareHandler.text; textField.text = ShareHandler.text;
@@ -47,37 +41,7 @@ QQC2.Control {
} }
} }
Connections { onActiveFocusChanged: chatContentView.itemAt(contentModel.index(contentModel.focusRow, 0)).forceActiveFocus()
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();
}
}
Connections { Connections {
target: ShareHandler 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.room: root.currentRoom
Message.contentModel: contentModel Message.contentModel: contentModel
background: Rectangle { implicitHeight: chatBar.implicitHeight + Kirigami.Units.largeSpacing
color: Kirigami.Theme.backgroundColor
Kirigami.Separator {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
}
}
height: Math.max(Math.min(chatScrollView.contentHeight + bottomPadding + topPadding, Kirigami.Units.gridUnit * 10), Kirigami.Units.gridUnit * 5) QQC2.Control {
leftPadding: rightPadding id: chatBar
rightPadding: (root.width - chatBarSizeHelper.availableWidth) / 2 + Kirigami.Units.largeSpacing
topPadding: Kirigami.Units.smallSpacing
bottomPadding: Kirigami.Units.smallSpacing
contentItem: ColumnLayout { anchors.top: root.top
QQC2.ScrollView { anchors.horizontalCenter: root.horizontalCenter
id: chatScrollView
Layout.fillWidth: true
Layout.maximumHeight: Kirigami.Units.gridUnit * 8
clip: true spacing: 0
ColumnLayout { width: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
width: chatScrollView.width topPadding: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing bottomPadding: Kirigami.Units.smallSpacing
Repeater { contentItem: ColumnLayout {
id: chatContentView RichEditBar {
model: ChatBarMessageContentModel { id: richEditBar
id: contentModel visible: NeoChatConfig.sendMessageWith === 1
type: ChatBarType.Room maxAvailableWidth: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
room: root.currentRoom
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 background: Kirigami.ShadowedRectangle {
contentModel: chatContentView.model 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 { LibNeoChat.DelegateSizeHelper {
id: chatBarSizeHelper id: chatBarSizeHelper
@@ -171,35 +182,23 @@ QQC2.Control {
QtObject { QtObject {
id: _private id: _private
property ChatBarCache chatBarCache
onChatBarCacheChanged: {
richEditBar.chatBarCache = chatBarCache
}
function pasteImage() { property LibNeoChat.CompletionModel completionModel: LibNeoChat.CompletionModel {
let localPath = Clipboard.saveImage(); room: root.currentRoom
if (localPath.length === 0) { type: LibNeoChat.ChatBarType.Room
return false; textItem: contentModel.focusedTextItem
} roomListModel: RoomManager.roomListModel
_private.chatBarCache.attachmentPath = localPath; userListModel: RoomManager.userListModel
return true;
}
}
CompletionMenu { onIsCompletingChanged: {
id: completionMenu if (!isCompleting) {
room: root.currentRoom return;
type: LibNeoChat.ChatBarType.Room }
textItem: contentModel.focusedTextItem
x: 1 let dialog = Qt.createComponent('org.kde.neochat.chatbar', 'CompletionMenu').createObject(contentModel.focusedTextItem.textItem, {
y: -height model: _private.completionModel,
width: parent.width - 1 keyHelper: contentModel.keyHelper
Behavior on height { }).open();
NumberAnimation {
property: "height"
duration: Kirigami.Units.shortDuration
easing.type: Easing.OutCubic
} }
} }
} }

View File

@@ -11,33 +11,55 @@ import org.kde.kirigami as Kirigami
import org.kde.kirigamiaddons.delegates as Delegates import org.kde.kirigamiaddons.delegates as Delegates
import org.kde.kirigamiaddons.labs.components as KirigamiComponents import org.kde.kirigamiaddons.labs.components as KirigamiComponents
import org.kde.neochat
import org.kde.neochat.libneochat as LibNeoChat import org.kde.neochat.libneochat as LibNeoChat
QQC2.Popup { QQC2.Popup {
id: root id: root
/** property alias model: completions.model
* @brief The current room that user is viewing.
*/
required property LibNeoChat.NeoChatRoom room
/** required property LibNeoChat.ChatKeyHelper keyHelper
* @brief The chatbar type
*/
required property int type
/** Connections {
* @brief The chatbar type target: keyHelper
*/
required property LibNeoChat.ChatTextItemHelper textItem
visible: completions.count > 0 function onUnhandledUp(isCompleting: bool): void {
if (!isCompleting) {
return;
}
root.decrementIndex();
}
onVisibleChanged: if (visible) { function onUnhandledDown(isCompleting: bool): void {
root.open(); 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() { function incrementIndex() {
completions.incrementCurrentIndex(); completions.incrementCurrentIndex();
} }
@@ -47,11 +69,11 @@ QQC2.Popup {
} }
function complete(text: string, hRef: string) { function complete(text: string, hRef: string) {
completionModel.insertCompletion(text, hRef); model.insertCompletion(text, hRef);
} }
function completeCurrent() { function completeCurrent() {
completionModel.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef); model.insertCompletion(completions.currentItem.replacedText, completions.currentItem.hRef);
} }
leftPadding: 0 leftPadding: 0
@@ -61,70 +83,57 @@ QQC2.Popup {
implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10) implicitHeight: Math.min(completions.contentHeight, Kirigami.Units.gridUnit * 10)
contentItem: ColumnLayout { contentItem: QQC2.ScrollView {
spacing: 0 contentWidth: Kirigami.Units.gridUnit * 20
Kirigami.Separator {
Layout.fillWidth: true
}
QQC2.ScrollView {
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
Layout.maximumHeight: Kirigami.Units.gridUnit * 10
background: Rectangle { ListView {
color: Kirigami.Theme.backgroundColor id: completions
} currentIndex: 0
keyNavigationWraps: true
highlightMoveDuration: 100
onCountChanged: currentIndex = 0
delegate: Delegates.RoundedItemDelegate {
id: completionDelegate
ListView { required property int index
id: completions required property string displayName
required property string subtitle
required property string iconName
required property string replacedText
required property url hRef
model: LibNeoChat.CompletionModel { text: displayName
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
required property int index contentItem: RowLayout {
required property string displayName KirigamiComponents.Avatar {
required property string subtitle visible: completionDelegate.iconName !== "invalid"
required property string iconName Layout.preferredWidth: Kirigami.Units.iconSizes.medium
required property string replacedText Layout.preferredHeight: Kirigami.Units.iconSizes.medium
required property url hRef source: completionDelegate.iconName === "invalid" ? "" : completionDelegate.iconName
name: completionDelegate.text
text: displayName }
Delegates.SubtitleContentItem {
contentItem: RowLayout { itemDelegate: completionDelegate
KirigamiComponents.Avatar { labelItem.textFormat: Text.PlainText
visible: completionDelegate.iconName !== "invalid" labelItem.clip: true // Intentional to limit insane Unicode in display names
Layout.preferredWidth: Kirigami.Units.iconSizes.medium subtitle: completionDelegate.subtitle ?? ""
Layout.preferredHeight: Kirigami.Units.iconSizes.medium subtitleItem.textFormat: Text.PlainText
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 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.libneochat as LibNeoChat
import org.kde.neochat.messagecontent as MessageContent import org.kde.neochat.messagecontent as MessageContent
QQC2.ToolBar { RowLayout {
id: root id: root
/** /**
@@ -19,41 +19,39 @@ QQC2.ToolBar {
*/ */
required property LibNeoChat.NeoChatRoom room required property LibNeoChat.NeoChatRoom room
property LibNeoChat.ChatBarCache chatBarCache
required property MessageContent.ChatBarMessageContentModel contentModel required property MessageContent.ChatBarMessageContentModel contentModel
required property real maxAvailableWidth required property real maxAvailableWidth
readonly property real uncompressedImplicitWidth: textFormatRow.implicitWidth + readonly property real uncompressedImplicitWidth: boldButton.implicitWidth +
italicButton.implicitWidth +
extraTextFormatRow.implicitWidth +
listRow.implicitWidth + listRow.implicitWidth +
styleButton.implicitWidth + styleButton.implicitWidth +
emojiButton.implicitWidth + emojiButton.implicitWidth +
linkButton.implicitWidth + linkButton.implicitWidth +
sendRow.implicitWidth + root.spacing * 7 +
sendButton.implicitWidth + Kirigami.Units.gridUnit
buttonRow.spacing * 9 +
3
readonly property real listCompressedImplicitWidth: textFormatRow.implicitWidth + readonly property real listCompressedImplicitWidth: boldButton.implicitWidth +
italicButton.implicitWidth +
extraTextFormatRow.implicitWidth +
compressedListButton.implicitWidth + compressedListButton.implicitWidth +
styleButton.implicitWidth + styleButton.uncompressedWidth +
emojiButton.implicitWidth + emojiButton.implicitWidth +
linkButton.implicitWidth + linkButton.implicitWidth +
sendRow.implicitWidth + root.spacing * 7 +
sendButton.implicitWidth + Kirigami.Units.gridUnit
buttonRow.spacing * 9 +
3
readonly property real textFormatCompressedImplicitWidth: compressedTextFormatButton.implicitWidth + readonly property real extraTextCompressedImplicitWidth: boldButton.implicitWidth +
compressedListButton.implicitWidth + italicButton.implicitWidth +
styleButton.implicitWidth + compressedExtraTextFormatButton.implicitWidth +
emojiButton.implicitWidth + compressedListButton.implicitWidth +
linkButton.implicitWidth + styleButton.uncompressedWidth +
sendRow.implicitWidth + emojiButton.implicitWidth +
sendButton.implicitWidth + linkButton.implicitWidth +
buttonRow.spacing * 9 + root.spacing * 7 +
3 Kirigami.Units.gridUnit
readonly property ChatButtonHelper chatButtonHelper: ChatButtonHelper { readonly property ChatButtonHelper chatButtonHelper: ChatButtonHelper {
textItem: contentModel.focusedTextItem textItem: contentModel.focusedTextItem
@@ -61,128 +59,111 @@ QQC2.ToolBar {
signal clicked signal clicked
RowLayout { QQC2.ToolButton {
id: buttonRow id: boldButton
RowLayout { Shortcut {
id: textFormatRow sequence: "Ctrl+B"
visible: root.maxAvailableWidth > root.listCompressedImplicitWidth onActivated: boldButton.clicked()
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
}
} }
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 { QQC2.ToolButton {
id: compressedTextFormatButton id: underlineButton
visible: root.maxAvailableWidth < root.listCompressedImplicitWidth Shortcut {
icon.name: "dialog-text-and-font" sequence: "Ctrl+U"
onActivated: underlineButton.clicked()
}
icon.name: "format-text-underline"
enabled: chatButtonHelper.richFormatEnabled enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Format Text") text: i18nc("@action:button", "Underline")
display: QQC2.AbstractButton.IconOnly display: QQC2.AbstractButton.IconOnly
checkable: true checkable: true
checked: compressedTextFormatMenu.visible checked: root.chatButtonHelper.underline
onClicked: { 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 { QQC2.Menu {
id: compressedTextFormatMenu
y: -implicitHeight 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 { QQC2.MenuItem {
icon.name: "format-text-underline" icon.name: "format-text-underline"
text: i18nc("@action:button", "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.text: text
QQC2.ToolTip.visible: hovered QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
} }
Kirigami.Separator { QQC2.ToolButton {
Layout.fillHeight: true icon.name: "format-list-ordered"
Layout.margins: 0 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 { QQC2.ToolButton {
id: listRow id: indentAction
visible: root.maxAvailableWidth > root.uncompressedImplicitWidth icon.name: "format-indent-more"
QQC2.ToolButton { 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" icon.name: "format-list-unordered"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Unordered List") text: i18nc("@action:button", "Unordered List")
display: QQC2.AbstractButton.IconOnly onTriggered: {
checkable: true
checked: root.chatButtonHelper.unorderedList
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList); root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.UnorderedList);
root.clicked(); 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" icon.name: "format-list-ordered"
enabled: chatButtonHelper.richFormatEnabled
text: i18nc("@action:button", "Ordered List") text: i18nc("@action:button", "Ordered List")
display: QQC2.AbstractButton.IconOnly onTriggered: {
checkable: true
checked: root.chatButtonHelper.orderedlist
onClicked: {
root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList); root.chatButtonHelper.setFormat(LibNeoChat.RichFormat.OrderedList);
root.clicked(); root.clicked();
} }
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
} }
QQC2.ToolButton { QQC2.MenuItem {
id: indentAction
icon.name: "format-indent-more" icon.name: "format-indent-more"
enabled: chatButtonHelper.richFormatEnabled && root.chatButtonHelper.canIndentListMore
text: i18nc("@action:button", "Increase List Level") text: i18nc("@action:button", "Increase List Level")
display: QQC2.AbstractButton.IconOnly enabled: root.chatButtonHelper.canIndentListMore
onClicked: { onTriggered: {
root.chatButtonHelper.indentListMore(); root.chatButtonHelper.indentListMore();
root.clicked(); root.clicked();
} }
QQC2.ToolTip.text: text
QQC2.ToolTip.visible: hovered
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
} }
QQC2.ToolButton { QQC2.MenuItem {
id: dedentAction
icon.name: "format-indent-less" icon.name: "format-indent-less"
enabled: chatButtonHelper.richFormatEnabled && root.chatButtonHelper.canIndentListLess
text: i18nc("@action:button", "Decrease List Level") text: i18nc("@action:button", "Decrease List Level")
display: QQC2.AbstractButton.IconOnly enabled: root.chatButtonHelper.canIndentListLess
onClicked: { onTriggered: {
root.chatButtonHelper.indentListLess(); root.chatButtonHelper.indentListLess();
root.clicked(); 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 { QQC2.ToolTip.text: text
id: compressedListMenu QQC2.ToolTip.visible: hovered
y: -implicitHeight QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
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
} }
Component { Component {
@@ -575,21 +391,6 @@ QQC2.ToolBar {
LinkDialog {} LinkDialog {}
} }
Component {
id: attachDialog
AttachDialog {
anchors.centerIn: parent
}
}
Component {
id: openFileDialog
LibNeoChat.OpenFileDialog {
parentWindow: Window.window
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
}
}
Component { Component {
id: emojiDialog id: emojiDialog
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 required property ChatButtonHelper chatButtonHelper
y: -implicitHeight y: -implicitHeight
padding: Kirigami.Units.largeSpacing
contentItem: ColumnLayout { contentItem: ColumnLayout {
spacing: Kirigami.Units.smallSpacing spacing: Kirigami.Units.smallSpacing
@@ -26,24 +27,21 @@ QQC2.Popup {
Repeater { Repeater {
model: 9 model: 9
delegate: QQC2.TextArea { delegate: StyleDelegate {
id: styleDelegate id: styleDelegate
required property int index required property int index
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: Kirigami.Units.gridUnit * 7 Layout.minimumWidth: Kirigami.Units.gridUnit * 8
Layout.minimumHeight: Kirigami.Units.gridUnit * 2 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 style: index
readOnly: true highlight: root.chatButtonHelper.currentStyle === index || hovered
selectByMouse: false
onPressed: (event) => { onPressed: (event) => {
if (styleDelegate.index === LibNeoChat.RichFormat.Paragraph || if (index === LibNeoChat.RichFormat.Paragraph ||
styleDelegate.index === LibNeoChat.RichFormat.Code || index === LibNeoChat.RichFormat.Code ||
styleDelegate.index === LibNeoChat.RichFormat.Quote index === LibNeoChat.RichFormat.Quote
) { ) {
root.chatContentModel.insertStyleAtCursor(styleDelegate.index); root.chatContentModel.insertStyleAtCursor(styleDelegate.index);
} else { } else {
@@ -51,71 +49,19 @@ QQC2.Popup {
} }
root.close(); 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 { background: Kirigami.ShadowedRectangle {
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.View
radius: Kirigami.Units.cornerRadius radius: Kirigami.Units.cornerRadius
color: Kirigami.Theme.backgroundColor color: Kirigami.Theme.backgroundColor
border { border {
width: 1 width: 1
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, Kirigami.Theme.frameContrast) 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; m_textItem = textItem;
if (m_textItem) { if (m_textItem) {
connect(m_textItem, SIGNAL(styleChanged()), this, SLOT(formatDocument()));
connect(m_textItem, SIGNAL(styleChanged()), this, SLOT(formatDocument()));
if (document()) { if (document()) {
formatDocument(); formatDocument();
} }
@@ -59,10 +61,11 @@ void StyleDelegateHelper::formatDocument()
cursor.beginEditBlock(); cursor.beginEditBlock();
cursor.select(QTextCursor::Document); cursor.select(QTextCursor::Document);
cursor.removeSelectedText(); 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 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 // Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and
// level=2 look the same // level=2 look the same
const int sizeAdjustment = headingLevel > 0 ? 5 - headingLevel : 0; const int sizeAdjustment = headingLevel > 0 ? 5 - headingLevel : 0;

View File

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

View File

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

View File

@@ -45,6 +45,15 @@ public:
*/ */
Q_INVOKABLE bool handleKey(Qt::Key key, Qt::KeyboardModifiers modifiers); 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: Q_SIGNALS:
/** /**
* @brief There is an unhandled up key press. * @brief There is an unhandled up key press.
@@ -98,6 +107,14 @@ Q_SIGNALS:
*/ */
void unhandledReturn(bool isCompleting); 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. * @brief An image has been pasted.
*/ */
@@ -116,7 +133,9 @@ private:
bool backspace(); bool backspace();
bool insertReturn(); bool insertReturn(Qt::KeyboardModifiers modifiers);
bool cancel();
bool pasteImage(); bool pasteImage();
}; };

View File

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

View File

@@ -172,6 +172,11 @@ public:
*/ */
std::optional<int> cursorPosition() const; 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. * @brief Set the cursor position of the underlying text item to the given value.
*/ */

View File

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

View File

@@ -77,6 +77,21 @@ void CompletionModel::setTextItem(ChatTextItemHelper *textItem)
Q_EMIT textItemChanged(); 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() void CompletionModel::updateTextStart()
{ {
auto cursor = m_textItem->textCursor(); auto cursor = m_textItem->textCursor();
@@ -193,6 +208,15 @@ void CompletionModel::updateCompletion()
if (cursor.isNull()) { if (cursor.isNull()) {
return; return;
} }
if (m_ignoreCurrentCompletion) {
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
if (cursor.selectedText() == u' ') {
m_ignoreCurrentCompletion = false;
}
return;
}
cursor.setPosition(m_textStart); cursor.setPosition(m_textStart);
while (!cursor.selectedText().endsWith(u' ') && !cursor.atBlockEnd()) { while (!cursor.selectedText().endsWith(u' ') && !cursor.atBlockEnd()) {
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
@@ -242,6 +266,7 @@ void CompletionModel::updateCompletion()
endResetModel(); endResetModel();
m_textItem->isCompleting = rowCount() > 0; m_textItem->isCompleting = rowCount() > 0;
Q_EMIT isCompletingChanged();
} }
CompletionModel::AutoCompletionType CompletionModel::autoCompletionType() const 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) 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: public:
/** /**
* @brief Defines the different types of completion available. * @brief Defines the different types of completion available.
@@ -98,6 +103,10 @@ public:
ChatTextItemHelper *textItem() const; ChatTextItemHelper *textItem() const;
void setTextItem(ChatTextItemHelper *textItem); void setTextItem(ChatTextItemHelper *textItem);
bool isCompleting() const;
Q_INVOKABLE void ignoreCurrentCompletion();
/** /**
* @brief Get the given role value at the given index. * @brief Get the given role value at the given index.
* *
@@ -137,12 +146,14 @@ Q_SIGNALS:
void autoCompletionTypeChanged(); void autoCompletionTypeChanged();
void roomListModelChanged(); void roomListModelChanged();
void userListModelChanged(); void userListModelChanged();
void isCompletingChanged();
private: private:
QPointer<NeoChatRoom> m_room; QPointer<NeoChatRoom> m_room;
ChatBarType::Type m_type = ChatBarType::None; ChatBarType::Type m_type = ChatBarType::None;
QPointer<ChatTextItemHelper> m_textItem; QPointer<ChatTextItemHelper> m_textItem;
bool m_ignoreCurrentCompletion = false;
int m_textStart = 0; int m_textStart = 0;
void updateTextStart(); void updateTextStart();

View File

@@ -110,14 +110,10 @@ QQC2.Control {
height: implicitHeight height: implicitHeight
y: -height - 5 y: -height - 5
z: 10 z: 10
<<<<<<< HEAD
chatDocumentHandler: documentHandler
=======
room: root.Message.room room: root.Message.room
type: root.chatBarCache.isEditing ? ChatBarType.Edit : ChatBarType.Thread type: root.chatBarCache.isEditing ? ChatBarType.Edit : ChatBarType.Thread
// textItem: textArea // textItem: textArea
>>>>>>> c7858a151 (Move the remaining functionality of ChatDocumentHandler to ChatTextItemHelper or split into own objects)
margins: 0 margins: 0
Behavior on height { Behavior on height {
NumberAnimation { 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 { HoverHandler {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
} }
@@ -86,5 +71,22 @@ RowLayout {
id: _private id: _private
// The space available for the component after taking away the border // The space available for the component after taking away the border
readonly property real availableContentWidth: root.Message.maxContentWidth - verticalBorder.implicitWidth - root.spacing 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); 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) { connect(m_keyHelper, &ChatKeyHelper::imagePasted, this, [this](const QString &filePath) {
m_room->cacheForType(m_type)->setAttachmentPath(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) ChatBarMessageContentModel::ComponentIt ChatBarMessageContentModel::removeComponent(ComponentIt it)
{ {
if (it == m_components.end()) { if (it == m_components.end()) {

View File

@@ -66,6 +66,11 @@ class ChatBarMessageContentModel : public MessageContentModel
*/ */
Q_PROPERTY(bool hasRichFormatting READ hasRichFormatting NOTIFY hasRichFormattingChanged) 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: public:
explicit ChatBarMessageContentModel(QObject *parent = nullptr); explicit ChatBarMessageContentModel(QObject *parent = nullptr);
@@ -89,12 +94,16 @@ public:
Q_INVOKABLE void removeAttachment(); Q_INVOKABLE void removeAttachment();
bool sendMessageWithEnter() const;
void setSendMessageWithEnter(bool sendMessageWithEnter);
Q_INVOKABLE void postMessage(); Q_INVOKABLE void postMessage();
Q_SIGNALS: Q_SIGNALS:
void typeChanged(); void typeChanged();
void focusRowChanged(); void focusRowChanged();
void hasRichFormattingChanged(); void hasRichFormattingChanged();
void sendMessageWithEnterChanged();
private: private:
ChatBarType::Type m_type = ChatBarType::None; ChatBarType::Type m_type = ChatBarType::None;
@@ -125,5 +134,7 @@ private:
void updateCache() const; void updateCache() const;
QString messageText() const; QString messageText() const;
bool m_sendMessageWithEnter = true;
void clearModel(); void clearModel();
}; };

View File

@@ -264,32 +264,6 @@ FormCard.FormCardPage {
title: i18nc("Chat Editor", "Editor") title: i18nc("Chat Editor", "Editor")
} }
FormCard.FormCard { 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 { FormCard.FormCheckDelegate {
id: quickEditCheckbox id: quickEditCheckbox
text: i18n("Use s/text/replacement syntax to edit your last message") text: i18n("Use s/text/replacement syntax to edit your last message")

View File

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

View File

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