diff --git a/src/jamichatview/bubble.cpp b/src/jamichatview/bubble.cpp index 9777ed0c..8e32f193 100644 --- a/src/jamichatview/bubble.cpp +++ b/src/jamichatview/bubble.cpp @@ -1,221 +1,221 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * Author : Emmanuel Lepage Vallee * * * * This program 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. * * * * This program 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 this program. If not, see . * **************************************************************************/ #include "bubble.h" #include #include #include #include #include #include class BubblePrivate final { public: int m_Align {Qt::AlignmentFlag::AlignLeft}; QColor m_Color; QString m_Text; - QFont m_Font {QStringLiteral("Noto Color Emoji")}; + QFont m_Font {}; QFont m_DateFont; QFontMetricsF m_FontMetrics{m_Font}; QFontMetricsF m_DateFontMetrics{m_DateFont}; qreal m_MaximumWidth {-1}; qreal m_SideMargins {30}; }; Bubble::Bubble(QQuickItem* parent) : QQuickPaintedItem(parent), d_ptr(new BubblePrivate) { d_ptr->m_Color = QGuiApplication::palette().base().color(); connect(this, &Bubble::windowChanged, this, &Bubble::slotWindowChanged); } Bubble::~Bubble() { delete d_ptr; } void Bubble::paint(QPainter *painter) { painter->setWorldMatrixEnabled(true); const qreal w(boundingRect().width()), h(boundingRect().height()); // arrow size width, height, radius, bottom padding const qreal aw(10), ah(0), r(10), p(10); // Point left or right if (d_ptr->m_Align == Qt::AlignmentFlag::AlignRight) { painter->scale(-1, 1); painter->translate(QPointF{-w, 0}); } QPainterPath path; path.moveTo(0, h-ah-p-r); path.lineTo(aw, h-ah-r); path.lineTo(aw, h-r); path.cubicTo( path.currentPosition(), {aw, h}, {aw+r, h} ); path.lineTo(w- r, h); path.cubicTo( path.currentPosition(), {w, h}, {w, h-r} ); path.lineTo(w, r); path.cubicTo( path.currentPosition(), {w, 0}, {w-r, 0} ); path.lineTo(aw+r, 0); path.cubicTo( path.currentPosition(), {aw, 0}, {aw, r} ); path.lineTo(aw, h-r-p-ah-r); path.lineTo(0, h-ah-p-r); painter->setPen({}); painter->setRenderHint(QPainter::Antialiasing, true); painter->setBrush(d_ptr->m_Color); painter->drawPath(path); } int Bubble::alignment() const { return d_ptr->m_Align; } void Bubble::setAlignment(int a) { d_ptr->m_Align = a; } QColor Bubble::color() const { return d_ptr->m_Color; } void Bubble::setColor(const QColor& c) { d_ptr->m_Color = c; update(); } qreal Bubble::maximumWidth() const { return d_ptr->m_MaximumWidth; } void Bubble::setMaximumWidth(qreal value) { d_ptr->m_MaximumWidth = value; slotWindowChanged(window()); update(); } qreal Bubble::sideMargins() const { return d_ptr->m_SideMargins; } void Bubble::setSideMargins(qreal m) { d_ptr->m_SideMargins = m; update(); emit changed(); } QString Bubble::text() const { return d_ptr->m_Text; } void Bubble::setText(const QString& c) { d_ptr->m_Text = c; slotWindowChanged(window()); update(); } QFont& Bubble::font() const { return d_ptr->m_Font; } void Bubble::setFont(const QFont& f) { d_ptr->m_Font = f; d_ptr->m_FontMetrics = QFontMetricsF(f); slotWindowChanged(nullptr); emit fontChanged(d_ptr->m_Font); update(); } QFont Bubble::dateFont() const { return d_ptr->m_DateFont; } static qreal ratio = 0; void Bubble::slotWindowChanged(QQuickWindow *w) { w = window(); static qreal arrow = 20; static qreal dateW = 0; // There is a race condition, the item are created before the window if (!ratio) { ratio = w ? w->effectiveDevicePixelRatio():0; //TODO use the message date and store the result dateW = d_ptr->m_DateFontMetrics.width( QDateTime::currentDateTime().toString() ); } // At first, don't limit the height const auto r = d_ptr->m_FontMetrics.boundingRect( QRectF {0, 0, d_ptr->m_MaximumWidth, 9999.0}, Qt::AlignLeft|Qt::TextWordWrap, d_ptr->m_Text ); // Prevent bubble larger than the screen const qreal mw = std::max(dateW*1.05, r.width())+arrow+2*d_ptr->m_SideMargins; setWidth(std::min(d_ptr->m_MaximumWidth, mw)); } void Bubble::setDateFont(const QFont& f) { d_ptr->m_DateFont = f; d_ptr->m_DateFontMetrics = QFontMetricsF(f); ratio = 0; update(); emit fontChanged(d_ptr->m_Font); } diff --git a/src/jamichatview/qml/chatbox.qml b/src/jamichatview/qml/chatbox.qml index 64ee72d8..0af55fa7 100644 --- a/src/jamichatview/qml/chatbox.qml +++ b/src/jamichatview/qml/chatbox.qml @@ -1,291 +1,289 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * Author : Emmanuel Lepage Vallee * * * * This program 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. * * * * This program 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 this program. If not, see . * **************************************************************************/ import QtQuick 2.7 import QtQuick.Controls 2.0 import org.kde.kirigami 2.2 as Kirigami import QtQuick.Layouts 1.0 Item { property bool requireContactRequest: false height: Math.max(messageTextArea.implicitHeight, emojis.optimalHeight) implicitHeight: height Behavior on height { NumberAnimation {duration: 200; easing.type: Easing.OutQuad} } function focusEdit() { // Forcing the focus shows the keyboard and hide the toolbar. This // isn't usable. It would be fine if the controls were usable, but it // isn't the case for for no this is disabled. if (!Kirigami.Settings.isMobile) messageTextArea.forceActiveFocus() } id: chatBox signal sendMessage(string message, string richMessage) Rectangle { id: emojiButton property bool checked: false opacity: 0 radius: 999 width: Kirigami.Settings.isMobile ? 50 : 30 height: Kirigami.Settings.isMobile ? 50 : 30 visible: opacity > 0 anchors.bottomMargin: -15 anchors.bottom: parent.top anchors.horizontalCenter: parent.horizontalCenter color: Kirigami.Theme.backgroundColor border.width: 2 border.color: Kirigami.Theme.disabledTextColor Text { anchors.fill: parent text: "😀" color: Kirigami.Theme.textColor horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - font.family: "Noto Color Emoji" font.pixelSize : Kirigami.Settings.isMobile ? 24 : 18 } MouseArea { anchors.fill: parent onClicked: { emojiButton.checked = !emojiButton.checked } } Behavior on opacity { NumberAnimation {duration: 100; easing.type: Easing.InQuad} } Behavior on anchors.bottomMargin { NumberAnimation {duration: 100; easing.type: Easing.InQuad} } states: [ State { name: "checked" when: emojiButton.checked PropertyChanges { target: emojiButton color: Kirigami.Theme.highlightColor } } ] } Loader { id: emojis visible: false anchors.fill: parent property real optimalHeight: item && visible ? item.implicitHeight : 0 /** * Only load once, then keep alive because otherwise it takes like 2 * seconds each time on mobile. */ active: (Kirigami.Settings.isMobile && active) || visible sourceComponent: Grid { id: grid height: parent.height anchors.centerIn: emojis spacing: 0 width: Math.ceil(emoji.count/rows) * maxWidth*1.2 rows: Math.ceil((emoji.count*maxWidth*1.2)/width) property real maxWidth: 0 Repeater { model: ListModel { id: emoji ListElement { symbol: "😀" } ListElement { symbol: "😁" } ListElement { symbol: "😂" } ListElement { symbol: "😃" } ListElement { symbol: "😄" } ListElement { symbol: "😅" } ListElement { symbol: "😆" } ListElement { symbol: "😇" } ListElement { symbol: "😈" } ListElement { symbol: "😉" } ListElement { symbol: "😊" } ListElement { symbol: "😋" } ListElement { symbol: "😌" } ListElement { symbol: "😍" } ListElement { symbol: "😎" } ListElement { symbol: "😏" } ListElement { symbol: "😐" } ListElement { symbol: "😑" } ListElement { symbol: "😒" } ListElement { symbol: "😓" } ListElement { symbol: "😔" } ListElement { symbol: "😕" } ListElement { symbol: "😖" } ListElement { symbol: "😗" } ListElement { symbol: "😘" } ListElement { symbol: "😙" } ListElement { symbol: "😚" } ListElement { symbol: "😛" } ListElement { symbol: "😜" } ListElement { symbol: "😝" } ListElement { symbol: "😞" } ListElement { symbol: "😟" } ListElement { symbol: "😠" } ListElement { symbol: "😡" } ListElement { symbol: "😢" } ListElement { symbol: "😣" } ListElement { symbol: "😤" } ListElement { symbol: "😥" } ListElement { symbol: "😦" } ListElement { symbol: "😧" } ListElement { symbol: "😨" } ListElement { symbol: "😩" } ListElement { symbol: "😪" } ListElement { symbol: "😫" } ListElement { symbol: "😬" } ListElement { symbol: "😭" } ListElement { symbol: "😮" } ListElement { symbol: "😯" } ListElement { symbol: "😰" } ListElement { symbol: "😱" } ListElement { symbol: "😲" } ListElement { symbol: "😳" } ListElement { symbol: "😴" } ListElement { symbol: "😵" } ListElement { symbol: "😶" } ListElement { symbol: "😷" } ListElement { symbol: "😸" } ListElement { symbol: "😹" } ListElement { symbol: "😺" } ListElement { symbol: "😻" } ListElement { symbol: "😼" } ListElement { symbol: "😽" } ListElement { symbol: "😾" } ListElement { symbol: "😿" } ListElement { symbol: "🙀" } ListElement { symbol: "🙁" } ListElement { symbol: "🙂" } ListElement { symbol: "🙃" } ListElement { symbol: "🙄" } ListElement { symbol: "🙅" } ListElement { symbol: "🙆" } ListElement { symbol: "🙇" } ListElement { symbol: "🙈" } ListElement { symbol: "🙉" } ListElement { symbol: "🙊" } ListElement { symbol: "🙋" } ListElement { symbol: "🙌" } ListElement { symbol: "🙍" } ListElement { symbol: "🙎" } ListElement { symbol: "🙏" } } MouseArea { width: 1.3*maxWidth height: 2*emojiTxt.contentHeight Text { id: emojiTxt anchors.fill: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.family: "Noto Color Emoji" color: Kirigami.Theme.textColor font.pixelSize : Kirigami.Settings.isMobile ? 24 : 18 text: symbol Component.onCompleted: maxWidth = Math.max(maxWidth, emojiTxt.contentWidth) } onClicked: { messageTextArea.insert(messageTextArea.length, symbol) emojiButton.checked = false } } } } } ColumnLayout { anchors.fill: parent RowLayout { id: textMessagePanel Layout.fillHeight: true Layout.fillWidth : true spacing: 0 TextArea { id: messageTextArea Layout.fillHeight: true Layout.fillWidth: true textFormat: TextEdit.RichText wrapMode: TextEdit.WordWrap - font.family: "Noto Color Emoji" - font.pixelSize : 18 + font.pointSize: Kirigami.Theme.defaultFont.pointSize*1.6 placeholderText: " "+i18n("Write a message and press enter...") Keys.onReturnPressed: { var rawText = getText(0, length) var richText = getFormattedText(0, length) sendMessage(rawText, richText) } Keys.onEscapePressed: { console.log("escape") focus = false } background: Rectangle { color: Kirigami.Theme.backgroundColor anchors.fill: parent } persistentSelection: true states: [ State { name: "focus" when: messageTextArea.cursorVisible || chatBox.state == "emoji" || emojis.visible == true PropertyChanges { target: emojiButton opacity: 1 anchors.bottomMargin: 0 } } ] } Kirigami.Separator { Layout.fillHeight: true } Button { text: i18n("Send") Layout.fillHeight: true onClicked: { var rawText = messageTextArea.getText(0, messageTextArea.length) var richText = messageTextArea.getFormattedText(0, messageTextArea.length) sendMessage(rawText, richText) } background: Rectangle { color: Kirigami.Theme.buttonBackgroundColor anchors.fill: parent } } } } StateGroup { id: chatStateGroup states: [ State { name: "text" when: !emojiButton.checked PropertyChanges { target: messageTextArea focus: true } }, State { name: "emoji" when: emojiButton.checked PropertyChanges { target: textMessagePanel visible: false } PropertyChanges { target: emojis visible: true } } ] } Connections { target: chatBox onSendMessage: { console.log(message) messageTextArea.text = "" } } } diff --git a/src/jamichatview/qml/textbubble.qml b/src/jamichatview/qml/textbubble.qml index 6be0a2e3..f9af9077 100644 --- a/src/jamichatview/qml/textbubble.qml +++ b/src/jamichatview/qml/textbubble.qml @@ -1,185 +1,173 @@ /*************************************************************************** * Copyright (C) 2017 by Bluesystems * * Author : Emmanuel Lepage Vallee * * * * This program 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. * * * * This program 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 this program. If not, see . * **************************************************************************/ import QtQuick 2.7 import QtQuick.Layouts 1.2 import org.kde.kirigami 2.2 as Kirigami import org.kde.ringkde.jamicontactview 1.0 as JamiContactView import org.kde.ringkde.jamichatview 1.0 as JamiChatView Item { id: chatMessage width: parent.width property color background property color foreground property var cm: contactMethod signal clicked() Behavior on background { ColorAnimation {duration: 300; easing.type: Easing.InQuad} } height: bubble.height + 10 function getFactor() { return (height > (chatMessage.width*0.5)) ? 0.9 : 0.7 } // Prevent a binding loop. the "current" height isn't required anyway onWidthChanged: { bubble.maximumWidth = chatMessage.width*getFactor() } RowLayout { anchors.fill: parent JamiContactView.ContactPhoto { width: 50 height: 50 visible: direction == 0 drawEmptyOutline: false tracked: false contactMethod: chatMessage.cm Layout.alignment: Qt.AlignBottom Layout.bottomMargin: 20 defaultColor: Kirigami.Theme.textColor } - Item { + MouseArea { Layout.fillWidth: true Layout.fillHeight: true JamiChatView.Bubble { id: bubble anchors.margins: 5 sideMargins: 30 anchors.right: direction == 1 ? parent.right : undefined anchors.left : direction == 1 ? undefined : parent.left font.pointSize: Kirigami.Theme.defaultFont.pointSize*1.2 - font.family: "Noto Color Emoji" dateFont: dateLabel.font z: 1 alignment: direction == 1 ? Text.AlignRight : Text.AlignLeft color: background text: display != undefined ? display : "N/A" height: Math.max(50, label.implicitHeight + dateLabel.implicitHeight + 5) Text { id: label width: parent.width anchors.leftMargin: bubble.sideMargins anchors.rightMargin: bubble.sideMargins anchors.topMargin: 5 anchors.bottomMargin: 5 anchors.verticalCenter: bubble.verticalCenter anchors.left: direction == 1 ? undefined : bubble.left anchors.right: direction == 1 ? bubble.right : undefined horizontalAlignment: direction == 1 ? Text.AlignRight : Text.AlignLeft font: bubble.font text: display != undefined ? display : "N/A" color: foreground wrapMode: Text.WordWrap transitions: Transition { AnchorAnimation {duration: 200; easing.type: Easing.OutQuad } } states: [ State { name: "" when: !chatView.displayExtraTime AnchorChanges { target: label anchors.verticalCenter: bubble.verticalCenter anchors.top: undefined } PropertyChanges { target: label anchors.topMargin: 5 anchors.bottomMargin: 5 } }, State { name: "showtime" when: chatView.displayExtraTime AnchorChanges { target: label anchors.verticalCenter: undefined anchors.top: bubble.top } PropertyChanges { target: label anchors.topMargin: 0 anchors.bottomMargin: 0 } } ] } Text { id: dateLabel anchors.bottom: parent.bottom anchors.left: direction == 1 ? parent.left : undefined anchors.right: direction == 0 ? parent.right : undefined anchors.bottomMargin: 4 anchors.leftMargin: direction == 1 ? 4 : undefined anchors.rightMargin: direction == 0 ? 4 : undefined text: formattedDate != undefined ? formattedDate : "N/A" color: Kirigami.Theme.highlightedTextColor opacity: chatView.displayExtraTime ? 0.75 : 0 Behavior on opacity { NumberAnimation {duration: 200} } } + } - MouseArea { - anchors.fill: parent - - /** - * On mobile, there is already multiple layers of touch - * capable controls and this confuses the input handling - * code too much. - */ - enabled: !Kirigami.Settings.isMobile - - onClicked: { - chatMessage.clicked() - } - } + onClicked: { + chatMessage.clicked() } } JamiContactView.ContactPhoto { width: 50 height: 50 visible: direction == 1 drawEmptyOutline: false tracked: false contactMethod: chatMessage.cm Layout.alignment: Qt.AlignBottom Layout.bottomMargin: 20 defaultColor: Kirigami.Theme.textColor } } }