diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 22129bb9d..eeea72eb9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -288,6 +288,7 @@ qt_add_qml_module(neochat URI org.kde.neochat NO_PLUGIN qml/ConfirmUrlDialog.qml qml/AccountSwitchDialog.qml qml/ConfirmLeaveDialog.qml + qml/CodeMaximizeComponent.qml RESOURCES qml/confetti.png qml/glowdot.png diff --git a/src/qml/CodeMaximizeComponent.qml b/src/qml/CodeMaximizeComponent.qml new file mode 100644 index 000000000..9991e496d --- /dev/null +++ b/src/qml/CodeMaximizeComponent.qml @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigamiaddons.labs.components as Components +import org.kde.kirigami as Kirigami +import org.kde.syntaxhighlighting + +import org.kde.neochat + +Components.AbstractMaximizeComponent { + id: root + + /** + * @brief The message author. + * + * This should consist of the following: + * - id - The matrix ID of the author. + * - isLocalUser - Whether the author is the local user. + * - avatarSource - The mxc URL for the author's avatar in the current room. + * - avatarMediaId - The media ID of the author's avatar. + * - avatarUrl - The mxc URL for the author's avatar. + * - displayName - The display name of the author. + * - display - The name of the author. + * - color - The color for the author. + * - object - The Quotient::User object for the author. + * + * @sa Quotient::User + */ + property var author + + /** + * @brief The timestamp of the message. + */ + property var time + + /** + * @brief The code text to show. + */ + property string codeText + + /** + * @brief The code language, if any. + */ + property string language + + actions: [ + Kirigami.Action { + text: i18nc("@action", "Copy to clipboard") + icon.name: "edit-copy" + onTriggered: Clipboard.saveText(root.codeText) + } + ] + + leading: RowLayout { + Components.Avatar { + id: userAvatar + implicitWidth: Kirigami.Units.iconSizes.medium + implicitHeight: Kirigami.Units.iconSizes.medium + + name: root.author.name ?? root.author.displayName + source: root.author.avatarSource + color: root.author.color + } + ColumnLayout { + spacing: 0 + QQC2.Label { + id: userLabel + + text: root.author.name ?? root.author.displayName + color: root.author.color + font.weight: Font.Bold + elide: Text.ElideRight + } + QQC2.Label { + id: dateTimeLabel + text: root.time.toLocaleString(Qt.locale(), Locale.ShortFormat) + color: Kirigami.Theme.disabledTextColor + elide: Text.ElideRight + } + } + } + + content: QQC2.ScrollView { + id: codeScrollView + contentWidth: root.width + + // HACK: Hide unnecessary horizontal scrollbar (https://bugreports.qt.io/browse/QTBUG-83890) + QQC2.ScrollBar.horizontal.policy: QQC2.ScrollBar.AlwaysOff + + QQC2.TextArea { + id: codeText + topPadding: Kirigami.Units.smallSpacing + bottomPadding: Kirigami.Units.smallSpacing + leftPadding: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing * 2 + + text: root.codeText + readOnly: true + textFormat: TextEdit.PlainText + wrapMode: TextEdit.Wrap + color: Kirigami.Theme.textColor + + font.family: "monospace" + + Kirigami.SpellCheck.enabled: false + + onWidthChanged: lineModel.resetModel() + onHeightChanged: lineModel.resetModel() + + SyntaxHighlighter { + property string definitionName: Repository.definitionForName(root.language).name + textEdit: definitionName == "None" ? null : codeText + definition: definitionName + } + ColumnLayout { + id: lineNumberColumn + anchors { + top: codeText.top + topMargin: codeText.topPadding + 1 + left: codeText.left + leftMargin: Kirigami.Units.smallSpacing + } + spacing: 0 + Repeater { + id: repeater + model: LineModel { + id: lineModel + document: codeText.textDocument + } + delegate: QQC2.Label { + id: label + required property int index + required property int docLineHeight + Layout.fillWidth: true + Layout.preferredHeight: docLineHeight + horizontalAlignment: Text.AlignRight + text: index + 1 + color: Kirigami.Theme.disabledTextColor + + font.family: "monospace" + } + } + } + Kirigami.Separator { + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + leftMargin: lineNumberColumn.width + lineNumberColumn.anchors.leftMargin + Kirigami.Units.smallSpacing + } + } + TapHandler { + acceptedButtons: Qt.LeftButton + onTapped: root.close() + } + + background: null + } + + background: Rectangle { + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + color: Kirigami.Theme.backgroundColor + } + } +} diff --git a/src/qml/RoomPage.qml b/src/qml/RoomPage.qml index e625d6af3..bc164578a 100644 --- a/src/qml/RoomPage.qml +++ b/src/qml/RoomPage.qml @@ -298,6 +298,15 @@ Kirigami.Page { }); popup.open(); } + + function onShowMaximizedCode(author, time, codeText, language) { + let popup = Qt.createComponent('org.kde.neochat', 'CodeMaximizeComponent.qml').createObject(QQC2.Overlay.overlay, { + author: author, + time: time, + codeText: codeText, + language: language + }).open(); + } } Component { diff --git a/src/roommanager.cpp b/src/roommanager.cpp index d7039e5e4..2fba40a9f 100644 --- a/src/roommanager.cpp +++ b/src/roommanager.cpp @@ -158,6 +158,14 @@ void RoomManager::maximizeMedia(int index) Q_EMIT showMaximizedMedia(index); } +void RoomManager::maximizeCode(const QVariantMap &author, const QDateTime &time, const QString &codeText, const QString &language) +{ + if (codeText.isEmpty()) { + return; + } + Q_EMIT showMaximizedCode(author, time, codeText, language); +} + void RoomManager::requestFullScreenClose() { Q_EMIT closeFullScreen(); diff --git a/src/roommanager.h b/src/roommanager.h index 1be1f7304..fa66f767f 100644 --- a/src/roommanager.h +++ b/src/roommanager.h @@ -203,6 +203,8 @@ public: */ Q_INVOKABLE void maximizeMedia(int index); + Q_INVOKABLE void maximizeCode(const QVariantMap &author, const QDateTime &time, const QString &codeText, const QString &language); + /** * @brief Request that any full screen overlay currently open closes. */ @@ -264,6 +266,11 @@ Q_SIGNALS: */ void showMaximizedMedia(int index); + /** + * @brief Request a block of code is shown maximized. + */ + void showMaximizedCode(const QVariantMap &author, const QDateTime &time, const QString &codeText, const QString &language); + /** * @brief Request that any full screen overlay closes. */ diff --git a/src/timeline/Bubble.qml b/src/timeline/Bubble.qml index eb7cafbb5..e8e092c78 100644 --- a/src/timeline/Bubble.qml +++ b/src/timeline/Bubble.qml @@ -154,6 +154,7 @@ QQC2.Control { delegate: MessageComponentChooser { room: root.room index: root.index + time: root.time actionsHandler: root.actionsHandler timeline: root.timeline maxContentWidth: root.maxContentWidth diff --git a/src/timeline/CodeComponent.qml b/src/timeline/CodeComponent.qml index 721d1da69..81a3b09b3 100644 --- a/src/timeline/CodeComponent.qml +++ b/src/timeline/CodeComponent.qml @@ -13,6 +13,29 @@ import org.kde.neochat QQC2.Control { id: root + /** + * @brief The message author. + * + * This should consist of the following: + * - id - The matrix ID of the author. + * - isLocalUser - Whether the author is the local user. + * - avatarSource - The mxc URL for the author's avatar in the current room. + * - avatarMediaId - The media ID of the author's avatar. + * - avatarUrl - The mxc URL for the author's avatar. + * - displayName - The display name of the author. + * - display - The name of the author. + * - color - The color for the author. + * - object - The Quotient::User object for the author. + * + * @sa Quotient::User + */ + required property var author + + /** + * @brief The timestamp of the message. + */ + required property var time + /** * @brief The display text of the message. */ @@ -113,6 +136,7 @@ QQC2.Control { TapHandler { acceptedButtons: Qt.LeftButton + onTapped: RoomManager.maximizeCode(root.author, root.time, root.display, root.componentAttributes.class) onLongPressed: root.showMessageMenu() } diff --git a/src/timeline/MessageComponentChooser.qml b/src/timeline/MessageComponentChooser.qml index 09e9bd4c1..36e209885 100644 --- a/src/timeline/MessageComponentChooser.qml +++ b/src/timeline/MessageComponentChooser.qml @@ -22,6 +22,11 @@ DelegateChooser { */ required property var index + /** + * @brief The timestamp of the message. + */ + required property var time + /** * @brief The ActionsHandler object to use. * @@ -89,6 +94,7 @@ DelegateChooser { DelegateChoice { roleValue: MessageComponentType.Code delegate: CodeComponent { + time: root.time maxContentWidth: root.maxContentWidth onSelectedTextChanged: selectedText => { root.selectedTextChanged(selectedText);