diff --git a/src/EmojiModel.h b/src/EmojiModel.h index 22315b5..9e0222a 100644 --- a/src/EmojiModel.h +++ b/src/EmojiModel.h @@ -1,101 +1,101 @@ #ifndef EMOJIMODEL_H #define EMOJIMODEL_H #include #include #include #include class Emoji { Q_GADGET Q_PROPERTY(const QString &unicode READ unicode CONSTANT) Q_PROPERTY(const QString &shortName READ shortName CONSTANT) Q_PROPERTY(Emoji::Group group READ group CONSTANT) public: - enum class Group { + enum Group { Invalid = -1, Favorites, People, Nature, Food, Activity, Travel, Objects, Symbols, Flags }; Q_ENUM(Group) Emoji(const QString& u = {}, const QString& s = {}, Emoji::Group g = Emoji::Group::Invalid) : m_unicode(u) , m_shortName(s) , m_group(g) { } inline const QString &unicode() const { return m_unicode; } inline const QString &shortName() const { return m_shortName; } inline Emoji::Group group() const { return m_group; } private: QString m_unicode; QString m_shortName; Emoji::Group m_group; }; class EmojiModel : public QAbstractListModel { Q_OBJECT public: enum class Roles { Unicode = Qt::UserRole, ShortName, Group, Emoji, }; using QAbstractListModel::QAbstractListModel; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QHash roleNames() const override; }; class EmojiProxyModel : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(Emoji::Group group READ group WRITE setGroup NOTIFY groupChanged) Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged) public: explicit EmojiProxyModel(QObject *parent = nullptr); ~EmojiProxyModel() override; Emoji::Group group() const; void setGroup(Emoji::Group group); QString filter() const; void setFilter(const QString &filter); public slots: void addFavoriteEmoji(int proxyRow); signals: void groupChanged(); void filterChanged(); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; private: Emoji::Group m_group = Emoji::Group::Invalid; QSet m_favoriteEmojis; }; Q_DECLARE_METATYPE(Emoji) #endif // EMOJIMODEL_H diff --git a/src/qml/ChatPage.qml b/src/qml/ChatPage.qml index 81946c5..3d618aa 100644 --- a/src/qml/ChatPage.qml +++ b/src/qml/ChatPage.qml @@ -1,361 +1,361 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Kaidan. If not, see . */ import QtQuick 2.6 import QtQuick.Controls 2.0 as Controls import QtQuick.Layouts 1.3 import org.kde.kirigami 2.2 as Kirigami import QtGraphicalEffects 1.0 import im.kaidan.kaidan 1.0 import EmojiModel 0.1 import "elements" Kirigami.ScrollablePage { property string chatName property bool isWritingSpoiler property string messageToCorrect title: chatName keyboardNavigationEnabled: true actions.contextualActions: [ Kirigami.Action { visible: !isWritingSpoiler iconSource: "password-show-off" text: qsTr("Send a spoiler message") onTriggered: isWritingSpoiler = true } ] SendMediaSheet { id: sendMediaSheet } FileChooser { id: fileChooser title: qsTr("Select a file") onAccepted: { sendMediaSheet.jid = kaidan.messageModel.chatPartner sendMediaSheet.fileUrl = fileUrl sendMediaSheet.open() } } function openFileDialog(filterName, filter) { fileChooser.filterName = filterName fileChooser.filter = filter fileChooser.open() mediaDrawer.close() } Kirigami.OverlayDrawer { id: mediaDrawer edge: Qt.BottomEdge height: Kirigami.Units.gridUnit * 8 contentItem: RowLayout { id: content Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true IconButton { buttonText: qsTr("Image") iconSource: "image-jpeg" onClicked: openFileDialog("Images", "*.jpg *.jpeg *.png *.gif") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Video") iconSource: "video-mp4" onClicked: openFileDialog("Videos", "*.mp4 *.mkv *.avi *.webm") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Audio") iconSource: "audio-mp3" onClicked: openFileDialog("Audio files", "*.mp3 *.wav *.flac *.ogg *.m4a *.mka") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Document") iconSource: "x-office-document" onClicked: openFileDialog("Documents", "*.doc *.docx *.odt") Layout.alignment: Qt.AlignHCenter } IconButton { buttonText: qsTr("Other file") iconSource: "text-x-plain" onClicked: openFileDialog("All files", "*") Layout.alignment: Qt.AlignHCenter } } } background: Image { id: bgimage source: kaidan.utils.getResourcePath("images/chat.png") anchors.fill: parent fillMode: Image.Tile horizontalAlignment: Image.AlignLeft verticalAlignment: Image.AlignTop } // Chat ListView { verticalLayoutDirection: ListView.BottomToTop spacing: Kirigami.Units.smallSpacing * 2 // connect the database model: kaidan.messageModel delegate: ChatMessage { msgId: model.id sender: model.sender sentByMe: model.sentByMe messageBody: model.body dateTime: new Date(model.timestamp) isRead: model.isDelivered name: chatName mediaType: model.mediaType mediaGetUrl: model.mediaUrl mediaLocation: model.mediaLocation edited: model.isEdited isSpoiler: model.isSpoiler isShowingSpoiler: false spoilerHint: model.spoilerHint onMessageEditRequested: { messageToCorrect = id messageField.text = body messageField.state = "edit" } } } // Message Writing footer: Controls.Pane { id: sendingArea layer.enabled: sendingArea.enabled layer.effect: DropShadow { verticalOffset: 1 color: Kirigami.Theme.disabledTextColor samples: 20 spread: 0.3 cached: true // element is static } padding: 0 wheelEnabled: true background: Rectangle { color: Kirigami.Theme.backgroundColor } RowLayout { anchors.fill: parent Layout.preferredHeight: Kirigami.Units.gridUnit * 3 Controls.ToolButton { id: attachButton visible: kaidan.uploadServiceFound Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: "document-send-symbolic" isMask: true smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: { if (Kirigami.Settings.isMobile) mediaDrawer.open() else openFileDialog("All files", "(*)") } } ColumnLayout { Layout.minimumHeight: messageField.height + Kirigami.Units.smallSpacing * 2 Layout.fillWidth: true spacing: 0 RowLayout { visible: isWritingSpoiler Controls.TextArea { id: spoilerHintField Layout.fillWidth: true placeholderText: qsTr("Spoiler hint") wrapMode: Controls.TextArea.Wrap selectByMouse: true background: Item {} } Controls.ToolButton { Layout.preferredWidth: Kirigami.Units.gridUnit * 1.5 Layout.preferredHeight: Kirigami.Units.gridUnit * 1.5 padding: 0 Kirigami.Icon { source: "tab-close" smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 1.5 height: width } onClicked: { isWritingSpoiler = false spoilerHintField.text = "" } } } Kirigami.Separator { visible: isWritingSpoiler Layout.fillWidth: true } Controls.TextArea { id: messageField Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter placeholderText: qsTr("Compose message") wrapMode: Controls.TextArea.Wrap selectByMouse: true background: Item {} state: "compose" states: [ State { name: "compose" }, State { name: "edit" } ] Keys.onReturnPressed: { if (event.key === Qt.Key_Return) { if (event.modifiers & Qt.ControlModifier) { messageField.append("") } else { sendButton.onClicked() event.accepted = true } } } } } EmojiPicker { x: -width + parent.width y: -height - 16 width: Kirigami.Units.gridUnit * 20 height: Kirigami.Units.gridUnit * 15 id: emojiPicker model: EmojiProxyModel { - group: Emoji.Group.People + group: Emoji.People sourceModel: EmojiModel {} } textArea: messageField } Controls.ToolButton { id: emojiPickerButton Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: "preferences-desktop-emoticons" enabled: sendButton.enabled isMask: false smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.open() } Controls.ToolButton { id: sendButton Layout.preferredWidth: Kirigami.Units.gridUnit * 3 Layout.preferredHeight: Kirigami.Units.gridUnit * 3 padding: 0 Kirigami.Icon { source: { if (messageField.state == "compose") return "document-send" else if (messageField.state == "edit") return "edit-symbolic" } enabled: sendButton.enabled isMask: true smooth: true anchors.centerIn: parent width: Kirigami.Units.gridUnit * 2 height: width } onClicked: { // don't send empty messages if (!messageField.text.length) { return } // disable the button to prevent sending // the same message several times sendButton.enabled = false // send the message if (messageField.state == "compose") { kaidan.sendMessage( kaidan.messageModel.chatPartner, messageField.text, isWritingSpoiler, spoilerHintField.text ) } else if (messageField.state == "edit") { kaidan.correctMessage( kaidan.messageModel.chatPartner, messageToCorrect, messageField.text ) } // clean up the text fields messageField.text = "" messageField.state = "compose" spoilerHintField.text = "" isWritingSpoiler = false // reenable the button sendButton.enabled = true } } } } } diff --git a/src/qml/elements/EmojiPicker.qml b/src/qml/elements/EmojiPicker.qml index cf1756b..d69a7ae 100644 --- a/src/qml/elements/EmojiPicker.qml +++ b/src/qml/elements/EmojiPicker.qml @@ -1,191 +1,200 @@ /* * Kaidan - A user-friendly XMPP client for every device! * * Copyright (C) 2016-2019 Kaidan developers and contributors * (see the LICENSE file for a full list of copyright authors) * * Kaidan is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * In addition, as a special exception, the author of Kaidan gives * permission to link the code of its release with the OpenSSL * project's "OpenSSL" library (or with modified versions of it that * use the same license as the "OpenSSL" library), and distribute the * linked executables. You must obey the GNU General Public License in * all respects for all of the code used other than "OpenSSL". If you * modify this file, you may extend this exception to your version of * the file, but you are not obligated to do so. If you do not wish to * do so, delete this exception statement from your version. * * Kaidan is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Kaidan. If not, see . */ import QtQuick 2.9 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import org.kde.kirigami 2.0 as Kirigami import EmojiModel 0.1 Popup { id: root property TextArea textArea property alias model: view.model ColumnLayout { anchors.fill: parent GridView { id: view Layout.fillWidth: true Layout.fillHeight: true cellWidth: Kirigami.Units.gridUnit * 2.5 cellHeight: Kirigami.Units.gridUnit * 2.5 boundsBehavior: Flickable.DragOverBounds clip: true delegate: ItemDelegate { width: Kirigami.Units.gridUnit * 2 height: Kirigami.Units.gridUnit * 2 contentItem: Text { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pointSize: 20 text: model.unicode } hoverEnabled: true ToolTip.text: model.shortName ToolTip.visible: hovered onClicked: { GridView.view.model.addFavoriteEmoji(model.index); textArea.insert(textArea.cursorPosition, model.unicode) } } ScrollBar.vertical: ScrollBar {} } Rectangle { Layout.fillWidth: true Layout.preferredHeight: 2 color: Kirigami.Theme.highlightColor } Row { Repeater { model: ListModel { - ListElement { label: "🔖"; group: Emoji.Group.Favorites } - ListElement { label: "😏"; group: Emoji.Group.People } - ListElement { label: "🌲"; group: Emoji.Group.Nature } - ListElement { label: "🍛"; group: Emoji.Group.Food } - ListElement { label: "🚁"; group: Emoji.Group.Activity } - ListElement { label: "🚅"; group: Emoji.Group.Travel } - ListElement { label: "💡"; group: Emoji.Group.Objects } - ListElement { label: "🔣"; group: Emoji.Group.Symbols } - ListElement { label: "🏁"; group: Emoji.Group.Flags } - ListElement { label: "🔍"; group: Emoji.Group.Invalid } + ListElement { label: "🔖"; group: Emoji.Favorites } + ListElement { label: "😏"; group: Emoji.People } + ListElement { label: "🌲"; group: Emoji.Nature } + ListElement { label: "🍛"; group: Emoji.Food } + ListElement { label: "🚁"; group: Emoji.Activity } + ListElement { label: "🚅"; group: Emoji.Travel } + ListElement { label: "💡"; group: Emoji.Objects } + ListElement { label: "🔣"; group: Emoji.Symbols } + ListElement { label: "🏁"; group: Emoji.Flags } + ListElement { label: "🔍"; group: Emoji.Invalid } } delegate: ItemDelegate { width: Kirigami.Units.gridUnit * 1.85 height: Kirigami.Units.gridUnit * 1.85 contentItem: Text { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pointSize: 20 text: model.label } hoverEnabled: true ToolTip.text: { switch (model.group) { - case Emoji.Group.Favorites: + case Emoji.Favorites: return qsTr('Favorites'); - case Emoji.Group.People: + case Emoji.People: return qsTr('People'); - case Emoji.Group.Nature: + case Emoji.Nature: return qsTr('Nature'); - case Emoji.Group.Food: + case Emoji.Food: return qsTr('Food'); - case Emoji.Group.Activity: + case Emoji.Activity: return qsTr('Activity'); - case Emoji.Group.Travel: + case Emoji.Travel: return qsTr('Travel'); - case Emoji.Group.Objects: + case Emoji.Objects: return qsTr('Objects'); - case Emoji.Group.Symbols: + case Emoji.Symbols: return qsTr('Symbols'); - case Emoji.Group.Flags: + case Emoji.Flags: return qsTr('Flags'); - case Emoji.Group.Invalid: + case Emoji.Invalid: return qsTr('Search'); } } ToolTip.visible: hovered highlighted: root.model.group === model.group onClicked: root.model.group = model.group } } } TextField { id: searchField Timer { id: searchTimer interval: 500 onTriggered: root.model.filter = searchField.text } Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter - visible: root.model.group === Emoji.Group.Invalid + visible: root.model.group === Emoji.Invalid placeholderText: qsTr("Search emoji") selectByMouse: true background: Item {} rightPadding: clearButton.width ToolButton { id: clearButton visible: searchField.text !== '' - icon.name: 'edit-clear' focusPolicy: Qt.NoFocus anchors { verticalCenter: parent.verticalCenter right: parent.right } + Kirigami.Icon { + source: 'edit-clear' + height: 24 + width: height + + anchors { + centerIn: parent + } + } + onClicked: searchField.clear() } onTextChanged: searchTimer.restart() onVisibleChanged: if (visible) forceActiveFocus() } } }