Start implementing rich-text editor
This commit is contained in:
committed by
James Graham
parent
1f723d1fdf
commit
9cbe9f7280
@@ -8,6 +8,7 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
||||
QML_FILES
|
||||
AttachDialog.qml
|
||||
ChatBar.qml
|
||||
RichEditBar.qml
|
||||
CompletionMenu.qml
|
||||
EmojiDelegate.qml
|
||||
EmojiGrid.qml
|
||||
@@ -17,4 +18,9 @@ ecm_add_qml_module(Chatbar GENERATE_PLUGIN_SOURCE
|
||||
EmojiTonesPicker.qml
|
||||
ImageEditorPage.qml
|
||||
VoiceMessageDialog.qml
|
||||
ImageDialog.qml
|
||||
LinkDialog.qml
|
||||
LocationChooser.qml
|
||||
NewPollDialog.qml
|
||||
TableDialog.qml
|
||||
)
|
||||
|
||||
@@ -70,117 +70,6 @@ QQC2.Control {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The list of actions in the ChatBar.
|
||||
*
|
||||
* Each of these will be visualised in the ChatBar so new actions can be added
|
||||
* by appending to this list.
|
||||
*/
|
||||
property list<BusyAction> actions: [
|
||||
BusyAction {
|
||||
id: attachmentAction
|
||||
|
||||
isBusy: root.currentRoom && root.currentRoom.hasFileUploading
|
||||
|
||||
// Matrix does not allow sending attachments in replies
|
||||
visible: _private.chatBarCache.replyId.length === 0 && _private.chatBarCache.attachmentPath.length === 0
|
||||
icon.name: "mail-attachment"
|
||||
text: i18nc("@action:button", "Attach an image or file")
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
if (Clipboard.hasImage) {
|
||||
let dialog = attachDialog.createObject(root.QQC2.Overlay.overlay) as AttachDialog;
|
||||
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
|
||||
dialog.open();
|
||||
} else {
|
||||
let dialog = openFileDialog.createObject(root.QQC2.Overlay.overlay) as OpenFileDialog;
|
||||
dialog.chosen.connect(path => _private.chatBarCache.attachmentPath = path);
|
||||
dialog.open();
|
||||
}
|
||||
}
|
||||
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
id: emojiAction
|
||||
|
||||
isBusy: false
|
||||
|
||||
visible: !Kirigami.Settings.isMobile
|
||||
icon.name: "smiley"
|
||||
text: i18nc("@action:button", "Emojis & Stickers")
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
checkable: true
|
||||
|
||||
onTriggered: {
|
||||
if (emojiDialog.visible) {
|
||||
emojiDialog.close();
|
||||
} else {
|
||||
emojiDialog.open();
|
||||
}
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
id: mapButton
|
||||
icon.name: "mark-location-symbolic"
|
||||
isBusy: false
|
||||
text: i18nc("@action:button", "Send a Location")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
(locationChooser.createObject(QQC2.Overlay.overlay, {
|
||||
room: root.currentRoom
|
||||
}) as LocationChooser).open();
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
id: pollButton
|
||||
icon.name: "amarok_playcount"
|
||||
isBusy: false
|
||||
text: i18nc("@action:button", "Create a Poll")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
|
||||
onTriggered: {
|
||||
(newPollDialog.createObject(QQC2.Overlay.overlay, {
|
||||
room: root.currentRoom
|
||||
}) as NewPollDialog).open();
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
icon.name: "microphone"
|
||||
isBusy: false
|
||||
text: i18nc("@action:button", "Send a Voice Message")
|
||||
displayHint: QQC2.AbstractButton.IconOnly
|
||||
onTriggered: {
|
||||
let dialog = voiceMessageDialog.createObject(root, {
|
||||
room: root.currentRoom
|
||||
}) as VoiceMessageDialog;
|
||||
dialog.open();
|
||||
}
|
||||
tooltip: text
|
||||
},
|
||||
BusyAction {
|
||||
id: sendAction
|
||||
|
||||
isBusy: false
|
||||
|
||||
icon.name: "document-send"
|
||||
text: i18nc("@action:button", "Send message")
|
||||
displayHint: Kirigami.DisplayHint.IconOnly
|
||||
checkable: true
|
||||
|
||||
onTriggered: {
|
||||
_private.postMessage();
|
||||
}
|
||||
|
||||
tooltip: text
|
||||
}
|
||||
]
|
||||
|
||||
spacing: 0
|
||||
|
||||
Kirigami.Theme.colorSet: Kirigami.Theme.View
|
||||
@@ -274,9 +163,7 @@ QQC2.Control {
|
||||
placeholderText: root.currentRoom.usesEncryption ? i18nc("@placeholder", "Send an encrypted message…") : root.currentRoom.mainCache.attachmentPath.length > 0 ? i18nc("@placeholder", "Set an attachment caption…") : i18nc("@placeholder", "Send a message…")
|
||||
verticalAlignment: TextEdit.AlignVCenter
|
||||
wrapMode: TextEdit.Wrap
|
||||
// This has to stay PlainText or else formatting starts breaking in strange ways
|
||||
textFormat: TextEdit.PlainText
|
||||
font.pointSize: Kirigami.Theme.defaultFont.pointSize * NeoChatConfig.fontScale
|
||||
persistentSelection: true
|
||||
|
||||
Accessible.description: placeholderText
|
||||
|
||||
@@ -411,6 +298,18 @@ QQC2.Control {
|
||||
}
|
||||
}
|
||||
}
|
||||
RichEditBar {
|
||||
id: richEditBar
|
||||
Layout.maximumWidth: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
|
||||
Layout.margins: Kirigami.Units.largeSpacing
|
||||
Layout.alignment:Qt.AlignHCenter
|
||||
maxAvailableWidth: chatBarSizeHelper.availableWidth - Kirigami.Units.largeSpacing * 2
|
||||
|
||||
room: root.currentRoom
|
||||
documentHandler: documentHandler
|
||||
|
||||
onRequestPostMessage: _private.postMessage()
|
||||
}
|
||||
}
|
||||
LibNeoChat.DelegateSizeHelper {
|
||||
id: chatBarSizeHelper
|
||||
@@ -468,11 +367,13 @@ QQC2.Control {
|
||||
QtObject {
|
||||
id: _private
|
||||
property ChatBarCache chatBarCache
|
||||
onChatBarCacheChanged: {
|
||||
richEditBar.chatBarCache = chatBarCache
|
||||
}
|
||||
|
||||
function postMessage() {
|
||||
_private.chatBarCache.postMessage();
|
||||
repeatTimer.stop();
|
||||
root.currentRoom.markAllMessagesAsRead();
|
||||
textField.clear();
|
||||
}
|
||||
|
||||
@@ -534,38 +435,6 @@ QQC2.Control {
|
||||
room: root.currentRoom
|
||||
}
|
||||
|
||||
Component {
|
||||
id: openFileDialog
|
||||
|
||||
OpenFileDialog {
|
||||
parentWindow: Window.window
|
||||
currentFolder: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: attachDialog
|
||||
|
||||
AttachDialog {
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: locationChooser
|
||||
LocationChooser {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: newPollDialog
|
||||
NewPollDialog {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: voiceMessageDialog
|
||||
VoiceMessageDialog {}
|
||||
}
|
||||
|
||||
CompletionMenu {
|
||||
id: completionMenu
|
||||
chatDocumentHandler: documentHandler
|
||||
@@ -582,32 +451,4 @@ QQC2.Control {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EmojiDialog {
|
||||
id: emojiDialog
|
||||
|
||||
x: root.width - width
|
||||
y: -implicitHeight
|
||||
|
||||
modal: false
|
||||
includeCustom: true
|
||||
closeOnChosen: false
|
||||
|
||||
currentRoom: root.currentRoom
|
||||
|
||||
onChosen: emoji => root.insertText(emoji)
|
||||
onClosed: if (emojiAction.checked) {
|
||||
emojiAction.checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
function insertText(text) {
|
||||
let initialCursorPosition = textField.cursorPosition;
|
||||
textField.text = textField.text.substr(0, initialCursorPosition) + text + textField.text.substr(initialCursorPosition);
|
||||
textField.cursorPosition = initialCursorPosition + text.length;
|
||||
}
|
||||
|
||||
component BusyAction : Kirigami.Action {
|
||||
required property bool isBusy
|
||||
}
|
||||
}
|
||||
|
||||
55
src/chatbar/ImageDialog.qml
Normal file
55
src/chatbar/ImageDialog.qml
Normal file
@@ -0,0 +1,55 @@
|
||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtCore
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
FormCard.FormCardDialog {
|
||||
id: root
|
||||
|
||||
readonly property alias imagePath: imageField.path
|
||||
|
||||
title: i18nc("@title:window", "Insert Image")
|
||||
standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
|
||||
title: i18nc("@title:window", "Select an image")
|
||||
currentFolder: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
fileMode: FileDialog.OpenFile
|
||||
nameFilters: [i18n("Image files (*.jpg *.jpeg *.png *.svg *.webp)"), i18n("All files (*)")]
|
||||
onAccepted: imageField.path = selectedFile
|
||||
}
|
||||
|
||||
FormCard.FormButtonDelegate {
|
||||
id: imageField
|
||||
|
||||
property url path
|
||||
|
||||
text: i18nc("@label:textbox", "Image Location:")
|
||||
description: path.toString().length > 0 ? path.toString().split('/').slice(-1)[0] : ''
|
||||
|
||||
onClicked: fileDialog.open()
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: imageField.path.toString().length > 0
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 200
|
||||
Layout.topMargin: Kirigami.Units.largeSpacing
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: imageField.path
|
||||
fillMode: Image.PreserveAspectFit
|
||||
horizontalAlignment: Image.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/chatbar/LinkDialog.qml
Normal file
33
src/chatbar/LinkDialog.qml
Normal file
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
|
||||
FormCard.FormCardDialog {
|
||||
id: root
|
||||
|
||||
property alias linkText: linkTextField.text
|
||||
property alias linkUrl: linkUrlField.text
|
||||
|
||||
title: i18nc("@title:window", "Insert Link")
|
||||
standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: linkTextField
|
||||
|
||||
label: i18nc("@label:textbox", "Link Text:")
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: linkUrlField
|
||||
|
||||
label: i18nc("@label:textbox", "Link URL:")
|
||||
}
|
||||
}
|
||||
100
src/chatbar/LocationChooser.qml
Normal file
100
src/chatbar/LocationChooser.qml
Normal file
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de>
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import QtQuick
|
||||
import QtLocation
|
||||
import QtPositioning
|
||||
|
||||
import org.kde.kirigamiaddons.labs.components as Components
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Components.AbstractMaximizeComponent {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
property var location
|
||||
|
||||
title: i18n("Choose a Location")
|
||||
|
||||
actions: [
|
||||
Kirigami.Action {
|
||||
icon.name: "document-send"
|
||||
text: i18n("Send this location")
|
||||
onTriggered: {
|
||||
root.room.sendLocation(root.location.latitude, root.location.longitude, "");
|
||||
root.close();
|
||||
}
|
||||
enabled: !!root.location
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:intoolbar Re-center the map onto the set location", "Re-Center")
|
||||
icon.name: "snap-bounding-box-center-symbolic"
|
||||
onTriggered: mapView.map.fitViewportToMapItems([mapView.locationMapItem])
|
||||
enabled: root.location !== undefined
|
||||
},
|
||||
Kirigami.Action {
|
||||
text: i18nc("@action:intoolbar Determine the device's location", "Locate")
|
||||
icon.name: "mark-location-symbolic"
|
||||
enabled: positionSource.valid
|
||||
onTriggered: positionSource.update()
|
||||
}
|
||||
]
|
||||
|
||||
onOpened: forceActiveFocus()
|
||||
|
||||
PositionSource {
|
||||
id: positionSource
|
||||
|
||||
active: false
|
||||
|
||||
onPositionChanged: {
|
||||
const coord = position.coordinate;
|
||||
mapView.gpsMapItem.latitude = coord.latitude;
|
||||
mapView.gpsMapItem.longitude = coord.longitude;
|
||||
|
||||
mapView.map.addMapItem(mapView.gpsMapItem);
|
||||
mapView.map.fitViewportToMapItems([mapView.gpsMapItem])
|
||||
}
|
||||
}
|
||||
|
||||
content: MapView {
|
||||
id: mapView
|
||||
map.plugin: OsmLocationPlugin.plugin
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.location = mapView.map.toCoordinate(Qt.point(mouseX, mouseY), false);
|
||||
mapView.map.addMapItem(mapView.locationMapItem);
|
||||
}
|
||||
}
|
||||
|
||||
readonly property LocationMapItem locationMapItem: LocationMapItem {
|
||||
latitude: root.location.latitude
|
||||
longitude: root.location.longitude
|
||||
isLive: false
|
||||
heading: NaN
|
||||
asset: ""
|
||||
author: null
|
||||
}
|
||||
|
||||
readonly property LocationMapItem gpsMapItem: LocationMapItem {
|
||||
latitude: 0.0
|
||||
longitude: 0.0
|
||||
isLive: true
|
||||
heading: NaN
|
||||
asset: ""
|
||||
author: null
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: mapView.map
|
||||
function onCopyrightLinkActivated(link: string) {
|
||||
Qt.openUrlExternally(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
151
src/chatbar/NewPollDialog.qml
Normal file
151
src/chatbar/NewPollDialog.qml
Normal file
@@ -0,0 +1,151 @@
|
||||
// SPDX-FileCopyrightText: 2025 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import org.kde.kirigamiaddons.delegates as Delegates
|
||||
|
||||
import org.kde.neochat
|
||||
|
||||
Kirigami.Dialog {
|
||||
id: root
|
||||
|
||||
required property NeoChatRoom room
|
||||
|
||||
standardButtons: Kirigami.Dialog.Cancel
|
||||
|
||||
customFooterActions: [
|
||||
Kirigami.Action {
|
||||
enabled: optionModel.allValuesSet && questionTextField.text.length > 0
|
||||
text: i18nc("@action:button", "Send")
|
||||
icon.name: "document-send"
|
||||
onTriggered: {
|
||||
root.room.postPoll(pollTypeCombo.currentValue, questionTextField.text, optionModel.values())
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24)
|
||||
title: i18nc("@title: create new poll in the room", "Create Poll")
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
spacing: 0
|
||||
|
||||
FormCard.FormComboBoxDelegate {
|
||||
id: pollTypeCombo
|
||||
|
||||
text: i18nc("@label", "Poll type:")
|
||||
currentIndex: 0
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
model: [
|
||||
{ value: PollKind.Disclosed, text: i18nc("@item:inlistbox", "Open poll") },
|
||||
{ value: PollKind.Undisclosed, text: i18nc("@item:inlistbox", "Closed poll") }
|
||||
]
|
||||
}
|
||||
FormCard.FormTextDelegate {
|
||||
verticalPadding: 0
|
||||
text: pollTypeCombo.currentValue == 0 ? i18nc("@info", "Voters can see the result as soon as they have voted") : i18nc("@info", "Results are revealed only after the poll has closed")
|
||||
}
|
||||
FormCard.FormTextFieldDelegate {
|
||||
id: questionTextField
|
||||
label: i18nc("@label", "Question:")
|
||||
}
|
||||
Repeater {
|
||||
id: optionRepeater
|
||||
|
||||
model: ListModel {
|
||||
id: optionModel
|
||||
|
||||
readonly property bool allValuesSet: {
|
||||
for (let i = 0; i < optionModel.rowCount(); i++) {
|
||||
if (optionModel.get(i).optionText.length <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ListElement {
|
||||
optionText: ""
|
||||
}
|
||||
ListElement {
|
||||
optionText: ""
|
||||
}
|
||||
|
||||
function values() {
|
||||
let textValues = []
|
||||
for(let i = 0; i < optionModel.rowCount(); i++) {
|
||||
textValues.push(optionModel.get(i).optionText);
|
||||
}
|
||||
return textValues;
|
||||
}
|
||||
}
|
||||
delegate: FormCard.AbstractFormDelegate {
|
||||
id: optionDelegate
|
||||
|
||||
required property int index
|
||||
required property string optionText
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
QQC2.Label {
|
||||
id: optionLabel
|
||||
|
||||
Layout.fillWidth: true
|
||||
text: i18nc("As in first answer option to the poll", "Option %1:", optionDelegate.index + 1)
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap
|
||||
Accessible.ignored: true
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
QQC2.TextField {
|
||||
id: textField
|
||||
Layout.fillWidth: true
|
||||
Accessible.name: optionLabel.text
|
||||
onTextChanged: {
|
||||
optionModel.set(optionDelegate.index, {optionText: text})
|
||||
optionModel.allValuesSetChanged()
|
||||
}
|
||||
placeholderText: i18nc("@placeholder", "Enter option")
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
text: i18nc("@action:button", "Remove option")
|
||||
icon.name: "edit-delete-remove"
|
||||
onClicked: optionModel.remove(optionDelegate.index)
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
QQC2.ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: null
|
||||
}
|
||||
}
|
||||
Delegates.RoundedItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
|
||||
horizontalPadding: Kirigami.Units.largeSpacing * 2
|
||||
leftInset: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||
rightInset: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
|
||||
|
||||
highlighted: true
|
||||
|
||||
icon.name: "list-add"
|
||||
text: i18nc("@action:button", "Add option")
|
||||
|
||||
onClicked: optionModel.append({optionText: ""})
|
||||
}
|
||||
}
|
||||
}
|
||||
564
src/chatbar/RichEditBar.qml
Normal file
564
src/chatbar/RichEditBar.qml
Normal file
@@ -0,0 +1,564 @@
|
||||
// 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 QtQuick
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
|
||||
import org.kde.kirigami as Kirigami
|
||||
|
||||
import org.kde.neochat.libneochat as LibNeoChat
|
||||
|
||||
QQC2.ToolBar {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* @brief The current room that user is viewing.
|
||||
*/
|
||||
required property LibNeoChat.NeoChatRoom room
|
||||
|
||||
property LibNeoChat.ChatBarCache chatBarCache
|
||||
|
||||
required property LibNeoChat.ChatDocumentHandler documentHandler
|
||||
|
||||
required property real maxAvailableWidth
|
||||
|
||||
readonly property real uncompressedImplicitWidth: textFormatRow.implicitWidth +
|
||||
listRow.implicitWidth +
|
||||
styleButton.implicitWidth +
|
||||
emojiButton.implicitWidth +
|
||||
linkButton.implicitWidth +
|
||||
sendRow.implicitWidth +
|
||||
sendButton.implicitWidth +
|
||||
buttonRow.spacing * 9 +
|
||||
3
|
||||
|
||||
readonly property real listCompressedImplicitWidth: textFormatRow.implicitWidth +
|
||||
compressedListButton.implicitWidth +
|
||||
styleButton.implicitWidth +
|
||||
emojiButton.implicitWidth +
|
||||
linkButton.implicitWidth +
|
||||
sendRow.implicitWidth +
|
||||
sendButton.implicitWidth +
|
||||
buttonRow.spacing * 9 +
|
||||
3
|
||||
|
||||
readonly property real textFormatCompressedImplicitWidth: compressedTextFormatButton.implicitWidth +
|
||||
compressedListButton.implicitWidth +
|
||||
styleButton.implicitWidth +
|
||||
emojiButton.implicitWidth +
|
||||
linkButton.implicitWidth +
|
||||
sendRow.implicitWidth +
|
||||
sendButton.implicitWidth +
|
||||
buttonRow.spacing * 9 +
|
||||
3
|
||||
|
||||
signal requestPostMessage
|
||||
|
||||
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"
|
||||
text: i18nc("@action:button", "Bold")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.documentHandler.bold
|
||||
onClicked: root.documentHandler.bold = checked;
|
||||
|
||||
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"
|
||||
text: i18nc("@action:button", "Italic")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.documentHandler.italic
|
||||
onClicked: root.documentHandler.italic = checked;
|
||||
|
||||
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"
|
||||
text: i18nc("@action:button", "Underline")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.documentHandler.underline
|
||||
onClicked: root.documentHandler.underline = checked;
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-text-strikethrough"
|
||||
text: i18nc("@action:button", "Strikethrough")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.documentHandler.strikethrough
|
||||
onClicked: root.documentHandler.strikethrough = checked;
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: compressedTextFormatButton
|
||||
visible: root.maxAvailableWidth < root.listCompressedImplicitWidth
|
||||
icon.name: "dialog-text-and-font"
|
||||
text: i18nc("@action:button", "Format Text")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: compressedTextFormatMenu.visible
|
||||
onClicked: {
|
||||
compressedTextFormatMenu.open()
|
||||
}
|
||||
|
||||
QQC2.Menu {
|
||||
id: compressedTextFormatMenu
|
||||
y: -implicitHeight
|
||||
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-text-bold"
|
||||
text: i18nc("@action:button", "Bold")
|
||||
checkable: true
|
||||
checked: root.documentHandler.bold
|
||||
onTriggered: root.documentHandler.bold = checked;
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-text-italic"
|
||||
text: i18nc("@action:button", "Italic")
|
||||
checkable: true
|
||||
checked: root.documentHandler.italic
|
||||
onTriggered: root.documentHandler.italic = checked;
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-text-underline"
|
||||
text: i18nc("@action:button", "Underline")
|
||||
checkable: true
|
||||
checked: root.documentHandler.underline
|
||||
onTriggered: root.documentHandler.underline = checked;
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-text-strikethrough"
|
||||
text: i18nc("@action:button", "Strikethrough")
|
||||
checkable: true
|
||||
checked: root.documentHandler.strikethrough
|
||||
onTriggered: root.documentHandler.strikethrough = checked;
|
||||
}
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
Kirigami.Separator {
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: 0
|
||||
}
|
||||
RowLayout {
|
||||
id: listRow
|
||||
visible: root.maxAvailableWidth > root.uncompressedImplicitWidth
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-list-unordered"
|
||||
text: i18nc("@action:button", "Unordered List")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.documentHandler.currentListStyle === 1
|
||||
onClicked: {
|
||||
root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 1 ? 0 : 1)
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
icon.name: "format-list-ordered"
|
||||
text: i18nc("@action:button", "Ordered List")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: root.documentHandler.currentListStyle === 4
|
||||
onClicked: root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 4 ? 0 : 4)
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: indentAction
|
||||
icon.name: "format-indent-more"
|
||||
text: i18nc("@action:button", "Increase List Level")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
root.documentHandler.indentListMore();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: dedentAction
|
||||
icon.name: "format-indent-less"
|
||||
text: i18nc("@action:button", "Decrease List Level")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
onClicked: {
|
||||
root.documentHandler.indentListLess();
|
||||
}
|
||||
|
||||
QQC2.ToolTip.text: text
|
||||
QQC2.ToolTip.visible: hovered
|
||||
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||
}
|
||||
}
|
||||
QQC2.ToolButton {
|
||||
id: compressedListButton
|
||||
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.documentHandler.setListStyle(root.documentHandler.currentListStyle === 1 ? 0 : 1);
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-list-ordered"
|
||||
text: i18nc("@action:button", "Ordered List")
|
||||
onTriggered: root.documentHandler.setListStyle(root.documentHandler.currentListStyle === 4 ? 0 : 4);
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-indent-more"
|
||||
text: i18nc("@action:button", "Increase List Level")
|
||||
onTriggered: root.documentHandler.indentListMore();
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
icon.name: "format-indent-less"
|
||||
text: i18nc("@action:button", "Decrease List Level")
|
||||
onTriggered: root.documentHandler.indentListLess();
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
display: QQC2.AbstractButton.IconOnly
|
||||
checkable: true
|
||||
checked: styleMenu.visible
|
||||
onClicked: {
|
||||
styleMenu.open()
|
||||
}
|
||||
|
||||
QQC2.Menu {
|
||||
id: styleMenu
|
||||
y: -implicitHeight
|
||||
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("@item:inmenu no heading", "Paragraph")
|
||||
onTriggered: root.documentHandler.setHeadingLevel(0);
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("@item:inmenu heading level 1 (largest)", "Heading 1")
|
||||
onTriggered: root.documentHandler.setHeadingLevel(1);
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("@item:inmenu heading level 2", "Heading 2")
|
||||
onTriggered: root.documentHandler.setHeadingLevel(2);
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("@item:inmenu heading level 3", "Heading 3")
|
||||
onTriggered: root.documentHandler.setHeadingLevel(3);
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("@item:inmenu heading level 4", "Heading 4")
|
||||
onTriggered: root.documentHandler.setHeadingLevel(4);
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("@item:inmenu heading level 5", "Heading 5")
|
||||
onTriggered: root.documentHandler.setHeadingLevel(5);
|
||||
}
|
||||
QQC2.MenuItem {
|
||||
text: i18nc("@item:inmenu heading level 6 (smallest)", "Heading 6")
|
||||
onTriggered: root.documentHandler.setHeadingLevel(6);
|
||||
}
|
||||
}
|
||||
|
||||
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.documentHandler.currentLinkText(),
|
||||
linkUrl: root.documentHandler.currentLinkUrl()
|
||||
})
|
||||
dialog.onAccepted.connect(() => { documentHandler.updateLink(dialog.linkUrl, dialog.linkText) });
|
||||
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: {
|
||||
let dialog = (LibNeoChat.Clipboard.hasImage ? attachDialog : openFileDialog).createObject(QQC2.Overlay.overlay);
|
||||
dialog.chosen.connect(path => root.chatBarCache.attachmentPath = 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
|
||||
checkable: true
|
||||
|
||||
onClicked: root.requestPostMessage()
|
||||
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 {
|
||||
id: linkDialog
|
||||
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 {
|
||||
x: root.width - width
|
||||
y: -implicitHeight
|
||||
|
||||
modal: false
|
||||
includeCustom: true
|
||||
closeOnChosen: false
|
||||
|
||||
currentRoom: root.room
|
||||
|
||||
onChosen: emoji => {
|
||||
root.documentHandler.insertText(emoji);
|
||||
close();
|
||||
}
|
||||
onClosed: if (emojiButton.checked) {
|
||||
emojiButton.checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: locationChooser
|
||||
LocationChooser {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: newPollDialog
|
||||
NewPollDialog {}
|
||||
}
|
||||
}
|
||||
32
src/chatbar/TableDialog.qml
Normal file
32
src/chatbar/TableDialog.qml
Normal file
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: 2024 Carl Schwan <carl@carlschwan.eu>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
import QtQuick
|
||||
import QtCore
|
||||
import org.kde.kirigami as Kirigami
|
||||
import org.kde.kirigamiaddons.formcard as FormCard
|
||||
import QtQuick.Controls as QQC2
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
FormCard.FormCardDialog {
|
||||
id: root
|
||||
|
||||
readonly property alias rows: rowsSpinBox.value
|
||||
readonly property alias cols: colsSpinBox.value
|
||||
|
||||
title: i18nc("@title:window", "Insert Table")
|
||||
standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel
|
||||
|
||||
FormCard.FormSpinBoxDelegate {
|
||||
id: rowsSpinBox
|
||||
label: i18nc("@label:textbox", "Number of Rows:")
|
||||
}
|
||||
|
||||
FormCard.FormDelegateSeparator {}
|
||||
|
||||
FormCard.FormSpinBoxDelegate {
|
||||
id: colsSpinBox
|
||||
label: i18nc("@label:textbox", "Number of Columns:")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user