diff --git a/src/app/qml/RoomPage.qml b/src/app/qml/RoomPage.qml index 51e7ea49f..b0da6e149 100644 --- a/src/app/qml/RoomPage.qml +++ b/src/app/qml/RoomPage.qml @@ -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() diff --git a/src/chatbar/CMakeLists.txt b/src/chatbar/CMakeLists.txt index a2d3525fc..1a4a85b6f 100644 --- a/src/chatbar/CMakeLists.txt +++ b/src/chatbar/CMakeLists.txt @@ -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 diff --git a/src/chatbar/ChatBar.qml b/src/chatbar/ChatBar.qml index f62fe3f85..bdf9dcbf7 100644 --- a/src/chatbar/ChatBar.qml +++ b/src/chatbar/ChatBar.qml @@ -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(); } } } diff --git a/src/chatbar/CompletionMenu.qml b/src/chatbar/CompletionMenu.qml index 4f8b1ebe0..97c3ada7e 100644 --- a/src/chatbar/CompletionMenu.qml +++ b/src/chatbar/CompletionMenu.qml @@ -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) + } } } diff --git a/src/chatbar/RichEditBar.qml b/src/chatbar/RichEditBar.qml index 90452b17b..5af34a639 100644 --- a/src/chatbar/RichEditBar.qml +++ b/src/chatbar/RichEditBar.qml @@ -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 { diff --git a/src/chatbar/SendBar.qml b/src/chatbar/SendBar.qml new file mode 100644 index 000000000..b19359f6f --- /dev/null +++ b/src/chatbar/SendBar.qml @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// 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 + } +} diff --git a/src/chatbar/StyleButton.qml b/src/chatbar/StyleButton.qml new file mode 100644 index 000000000..517838b95 --- /dev/null +++ b/src/chatbar/StyleButton.qml @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2026 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +import org.kde.neochat.libneochat as LibNeoChat + +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 + } + } +} diff --git a/src/chatbar/StyleDelegate.qml b/src/chatbar/StyleDelegate.qml new file mode 100644 index 000000000..3635176fd --- /dev/null +++ b/src/chatbar/StyleDelegate.qml @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2026 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +import org.kde.neochat.libneochat as LibNeoChat + +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 + ) + } + } +} diff --git a/src/chatbar/StylePicker.qml b/src/chatbar/StylePicker.qml index 3781b3ef2..5285a665c 100644 --- a/src/chatbar/StylePicker.qml +++ b/src/chatbar/StylePicker.qml @@ -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 } } diff --git a/src/chatbar/styledelegatehelper.cpp b/src/chatbar/styledelegatehelper.cpp index c794d0f0b..c6dd06e98 100644 --- a/src/chatbar/styledelegatehelper.cpp +++ b/src/chatbar/styledelegatehelper.cpp @@ -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(m_textItem->property("index").toInt()); + const auto style = static_cast(m_textItem->property("style").toInt()); const auto string = RichFormat::styleString(style); - const int headingLevel = style <= 6 ? style : 0; + const auto sizeText = static_cast(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; diff --git a/src/chatbar/styledelegatehelper.h b/src/chatbar/styledelegatehelper.h index caf808c8b..87b7f545a 100644 --- a/src/chatbar/styledelegatehelper.h +++ b/src/chatbar/styledelegatehelper.h @@ -32,5 +32,6 @@ private: QPointer m_textItem; QTextDocument *document() const; +private Q_SLOTS: void formatDocument(); }; diff --git a/src/libneochat/chatkeyhelper.cpp b/src/libneochat/chatkeyhelper.cpp index a5fbcca27..0c3ea1df7 100644 --- a/src/libneochat/chatkeyhelper.cpp +++ b/src/libneochat/chatkeyhelper.cpp @@ -6,6 +6,7 @@ #include "chattextitemhelper.h" #include "clipboard.h" #include "neochatroom.h" +#include 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) { diff --git a/src/libneochat/chatkeyhelper.h b/src/libneochat/chatkeyhelper.h index 86a64b477..c1969e814 100644 --- a/src/libneochat/chatkeyhelper.h +++ b/src/libneochat/chatkeyhelper.h @@ -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(); }; diff --git a/src/libneochat/chattextitemhelper.cpp b/src/libneochat/chattextitemhelper.cpp index 8564ced18..286567c4f 100644 --- a/src/libneochat/chattextitemhelper.cpp +++ b/src/libneochat/chattextitemhelper.cpp @@ -342,6 +342,14 @@ std::optional 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) { diff --git a/src/libneochat/chattextitemhelper.h b/src/libneochat/chattextitemhelper.h index 4231afd6f..be38fbae6 100644 --- a/src/libneochat/chattextitemhelper.h +++ b/src/libneochat/chattextitemhelper.h @@ -172,6 +172,11 @@ public: */ std::optional 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. */ diff --git a/src/libneochat/enums/richformat.cpp b/src/libneochat/enums/richformat.cpp index efdb4f13d..c24d034fa 100644 --- a/src/libneochat/enums/richformat.cpp +++ b/src/libneochat/enums/richformat.cpp @@ -7,27 +7,29 @@ #include #include +#include + 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 {}; } diff --git a/src/libneochat/models/completionmodel.cpp b/src/libneochat/models/completionmodel.cpp index 7a9764dbd..ec88f07c8 100644 --- a/src/libneochat/models/completionmodel.cpp +++ b/src/libneochat/models/completionmodel.cpp @@ -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 diff --git a/src/libneochat/models/completionmodel.h b/src/libneochat/models/completionmodel.h index 01c0b2cfa..a5f9a0aac 100644 --- a/src/libneochat/models/completionmodel.h +++ b/src/libneochat/models/completionmodel.h @@ -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 m_room; ChatBarType::Type m_type = ChatBarType::None; QPointer m_textItem; + bool m_ignoreCurrentCompletion = false; int m_textStart = 0; void updateTextStart(); diff --git a/src/messagecontent/ChatBarComponent.qml b/src/messagecontent/ChatBarComponent.qml index c39bd68b1..9b6b03454 100644 --- a/src/messagecontent/ChatBarComponent.qml +++ b/src/messagecontent/ChatBarComponent.qml @@ -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 { diff --git a/src/messagecontent/ReplyComponent.qml b/src/messagecontent/ReplyComponent.qml index cfa9a1f3e..0bfd74789 100644 --- a/src/messagecontent/ReplyComponent.qml +++ b/src/messagecontent/ReplyComponent.qml @@ -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 + } } } diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.cpp b/src/messagecontent/models/chatbarmessagecontentmodel.cpp index b15923b45..9b04c6846 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.cpp +++ b/src/messagecontent/models/chatbarmessagecontentmodel.cpp @@ -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()) { diff --git a/src/messagecontent/models/chatbarmessagecontentmodel.h b/src/messagecontent/models/chatbarmessagecontentmodel.h index 632a12bcc..6b3cbab8a 100644 --- a/src/messagecontent/models/chatbarmessagecontentmodel.h +++ b/src/messagecontent/models/chatbarmessagecontentmodel.h @@ -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(); }; diff --git a/src/settings/NeoChatGeneralPage.qml b/src/settings/NeoChatGeneralPage.qml index 842f9d8d2..39d886bdb 100644 --- a/src/settings/NeoChatGeneralPage.qml +++ b/src/settings/NeoChatGeneralPage.qml @@ -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") diff --git a/src/timeline/TimelineView.qml b/src/timeline/TimelineView.qml index 43d64c814..be83d6b69 100644 --- a/src/timeline/TimelineView.qml +++ b/src/timeline/TimelineView.qml @@ -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 { diff --git a/src/timeline/TypingPane.qml b/src/timeline/TypingPane.qml index 305778887..c0129f808 100644 --- a/src/timeline/TypingPane.qml +++ b/src/timeline/TypingPane.qml @@ -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 + } } } }