diff --git a/src/jamichatview/qml/chatpage.qml b/src/jamichatview/qml/chatpage.qml index 0da47285..a98fb446 100644 --- a/src/jamichatview/qml/chatpage.qml +++ b/src/jamichatview/qml/chatpage.qml @@ -1,266 +1,247 @@ /*************************************************************************** * 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 QtQuick.Layouts 1.0 import org.kde.kirigami 2.2 as Kirigami import QtGraphicalEffects 1.0 import org.kde.ringkde.jamitimeline 1.0 as JamiTimeline import org.kde.ringkde.jamitimelinebase 1.0 as JamiTimelineBase import org.kde.ringkde.jamichatview 1.0 as JamiChatView import org.kde.ringkde.jamicontactview 1.0 as JamiContactView import net.lvindustries.ringqtquick 1.0 as RingQtQuick import org.kde.playground.kquickitemviews 1.0 as KQuickItemViews Rectangle { id: timelinePage signal disableContactRequests() property bool showScrollbar: true property bool _sendRequestOverride: true property var currentContactMethod: null property var currentIndividual: null property var timelineModel: null property bool canSendTexts: currentIndividual ? currentIndividual.canSendTexts : false property bool sendRequest: _sendRequestOverride && ( sendRequestLoader.active && sendRequestLoader.item && sendRequestLoader.item.sendRequests ) onDisableContactRequests: { if (timelinePage.setContactMethod()) currentContactMethod.confirmationEnabled = false timelinePage._sendRequestOverride = send } Kirigami.Theme.colorSet: Kirigami.Theme.View function focusEdit() { chatBox.focusEdit() } function showNewContent() { chatView.moveTo(Qt.BottomEdge) } function setContactMethod() { if (currentIndividual && !currentContactMethod) { currentContactMethod = currentIndividual.preferredContactMethod( RingQtQuick.Media.TEXT ) if (!currentContactMethod) console.log("Cannot find a valid ContactMethod for", currentIndividual) } return currentContactMethod } onCurrentIndividualChanged: { currentContactMethod = null setContactMethod() } color: Kirigami.Theme.backgroundColor // Scroll to the search, unread messages, bookmark, etc RingQtQuick.TimelineIterator { id: iterator currentIndividual: timelinePage.currentIndividual firstVisibleIndex: chatView.topLeft lastVisibleIndex: chatView.bottomLeft onContentAdded: { lastVisibleIndex = chatView.indexAt(Qt.BottomEdge) timelinePage.showNewContent() } onProposeIndex: { if (poposedIndex == newestIndex) timelinePage.showNewContent() else chatView.contentY = chatView.itemRect(newestIndex).y } } - onTimelineModelChanged: { - if (!fixmeTimer.running) - chatView.model = timelineModel - } - // Add a blurry background ShaderEffectSource { id: effectSource visible: chatView.displayExtraTime sourceItem: chatView anchors.right: timelinePage.right anchors.top: timelinePage.top width: scrollbar.fullWidth + 15 height: chatView.height sourceRect: Qt.rect( blurryOverlay.x, blurryOverlay.y, blurryOverlay.width, blurryOverlay.height ) } ColumnLayout { anchors.fill: parent clip: true spacing: 0 Loader { id: sendRequestLoader height: active && item ? item.implicitHeight : 0 Layout.fillWidth: true active: chatBox.requireContactRequest Layout.minimumHeight: active && item ? item.implicitHeight : 0 Layout.maximumHeight: active && item ? item.implicitHeight : 0 sourceComponent: JamiContactView.SendRequest { width: sendRequestLoader.width } } RowLayout { id: chatScrollView Layout.fillHeight: true Layout.fillWidth: true Layout.bottomMargin: 0 property bool lock: false Item { Layout.fillHeight: true Layout.fillWidth: true // Buttons to navigate to relevant content JamiChatView.Navigation { timelineIterator: iterator anchors.rightMargin: scrollbar.hasContent ? blurryOverlay.width : 0 anchors.right: parent.right anchors.bottom: parent.bottom Behavior on anchors.rightMargin { NumberAnimation {duration: 100; easing.type: Easing.InQuad} } } JamiChatView.ChatView { id: chatView width: Math.min(600, timelinePage.width - 50) height: parent.height anchors.horizontalCenter: parent.horizontalCenter - model: null//FIXME timelinePage.timelineModel - forceTime: scrollbar.overlayVisible - - // Due to a race condition, wait a bit, it should be fixed elsewhere, - //FIXME but it would take much longer. - Timer { - id: fixmeTimer - repeat: false - running: true - interval: 33 - onTriggered: { - chatView.model = timelinePage.timelineModel - } - } } // It needs to be here due to z-index conflicts between // chatScrollView and timelinePage Item { id: blurryOverlay z: 2 opacity: chatView.displayExtraTime && scrollbar.hasContent ? 1 : 0 anchors.right: parent.right anchors.top: parent.top anchors.rightMargin: - 15 height: chatScrollView.height clip: true width: chatView.displayExtraTime ? scrollbar.fullWidth + 15 : 0 visible: opacity > 0 Behavior on opacity { NumberAnimation {duration: 300; easing.type: Easing.InQuad} } Repeater { anchors.fill: parent model: 5 FastBlur { anchors.fill: parent source: effectSource radius: 30 } } Rectangle { anchors.fill: parent color: Kirigami.Theme.backgroundColor opacity: 0.75 } } } JamiTimelineBase.Scrollbar { id: scrollbar z: 1000 bottomUp: true Layout.fillHeight: true Layout.preferredWidth: 10 display: chatView.moving || timelinePage.showScrollbar - model: timelinePage.timelineModel + model: chatView.model view: chatView forceOverlay: chatView.displayExtraTime } } Kirigami.Separator { Layout.fillWidth: true } JamiChatView.ChatBox { id: chatBox Layout.fillWidth: true visible: canSendTexts RingQtQuick.MessageBuilder {id: builder} requireContactRequest: currentContactMethod && currentContactMethod.confirmationStatus == RingQtQuick.ContactMethod.UNCONFIRMED && currentContactMethod.confirmationStatus != RingQtQuick.ContactMethod.DISABLED } } Connections { target: chatBox onSendMessage: { timelinePage.setContactMethod() if (currentContactMethod) { if (currentContactMethod.account && currentContactMethod.confirmationStatus == RingQtQuick.ContactMethod.UNCONFIRMED) currentContactMethod.sendContactRequest() builder.addPayload("text/plain", message) builder.sendWidth(currentContactMethod) } } } } diff --git a/src/jamichatview/qml/chatview.qml b/src/jamichatview/qml/chatview.qml index fccde560..2401bbb0 100644 --- a/src/jamichatview/qml/chatview.qml +++ b/src/jamichatview/qml/chatview.qml @@ -1,170 +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.Controls 2.0 import QtQuick.Layouts 1.0 import org.kde.kirigami 2.2 as Kirigami import org.kde.playground.kquickitemviews 1.0 as KQuickItemViews import net.lvindustries.ringqtquick 1.0 as RingQtQuick import net.lvindustries.ringqtquick.models 1.0 as RingQtModels import org.kde.ringkde.jamichatview 1.0 as JamiChatView import org.kde.ringkde.genericutils 1.0 as GenericUtils KQuickItemViews.HierarchyView { id: chatView clip: true property bool forceTime: false - property var treeHelper: _treeHelper property var bubbleBackground: blendColor() property var bubbleForeground: "" property var unreadBackground: "" property var unreadForeground: "" property alias slideshow: slideshow property bool displayExtraTimePrivate: moving || dragging || forceTime property bool displayExtraTime: false onDisplayExtraTimePrivateChanged: { if (displayExtraTimePrivate) displayExtraTime = true else dateTimer.running = true } Timer { id: dateTimer interval: 1500 repeat: false onTriggered: displayExtraTime = false } - GenericUtils.TreeHelper { - id: _treeHelper + model: RingQtQuick.TimelineFilter { + individual: mainPage.currentIndividual + showCalls: false + showEmptyGroups: true + showMessages: true + initDelay: 33 } function blendColor() { chatView.bubbleBackground = Qt.tint( Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor //base1 ) chatView.unreadBackground = Qt.tint( Kirigami.Theme.backgroundColor, "#99BB0000" ) chatView.bubbleForeground = Kirigami.Theme.highlightedTextColor chatView.unreadForeground = Kirigami.Theme.highlightedTextColor return chatView.bubbleBackground } JamiChatView.Slideshow { id: slideshow } // Display something when the chat is empty Text { color: Kirigami.Theme.textColor text: i18n("There is nothing yet, enter a message below or place a call using the buttons\nfound in the header") anchors.centerIn: parent visible: chatView.empty horizontalAlignment: Text.AlignHCenter } Component { id: messageDelegate Loader { id: chatLoader // Create a delegate for each type Component { id: sectionDelegate JamiChatView.TextMessageGroup { width: chatView.width } } Component { id: snapshotGroupDelegate JamiChatView.Snapshots { width: chatView.width onViewImage: { chatView.slideshow.active = true chatView.slideshow.model = model chatView.slideshow.source = path } } } Component { id: callDelegate JamiChatView.CallGroup { width: chatView.width } } Component { id: categoryDelegate JamiChatView.CategoryHeader { width: chatView.width } } Component { id: textDelegate JamiChatView.TextBubble { background: isRead ? chatView.bubbleBackground : chatView.unreadBackground foreground: isRead ? chatView.bubbleForeground : chatView.unreadForeground width: chatView.width onClicked: { chatView.treeHelper.setData(rootIndex, true, "isRead") } } } // Some elements don't have delegates because they are handled // by their parent delegates function selectDelegate() { if (nodeType == RingQtModels.IndividualTimelineModel.TIME_CATEGORY) return categoryDelegate if (nodeType == RingQtModels.IndividualTimelineModel.TEXT_MESSAGE) return textDelegate if (nodeType == RingQtModels.IndividualTimelineModel.SNAPSHOT_GROUP) return snapshotGroupDelegate if (nodeType == RingQtModels.IndividualTimelineModel.SECTION_DELIMITER) return sectionDelegate if ( nodeType == RingQtModels.IndividualTimelineModel.CALL_GROUP || nodeType == RingQtModels.IndividualTimelineModel.RECORDINGS ) return callDelegate } sourceComponent: selectDelegate() } } delegate: messageDelegate } diff --git a/src/jamichatview/qml/groupheader.qml b/src/jamichatview/qml/groupheader.qml index fd420ec8..9e3ac042 100644 --- a/src/jamichatview/qml/groupheader.qml +++ b/src/jamichatview/qml/groupheader.qml @@ -1,65 +1,65 @@ /*************************************************************************** * 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.0 import org.kde.kirigami 2.2 as Kirigami RowLayout { property string type: "text" opacity: chatView.displayExtraTime ? 1 : 0.5 Behavior on opacity { NumberAnimation {duration: 500} } Item { Layout.preferredWidth: 5 } function getIcon() { if (type == "text") - return "image://icon/dialog-messages" + return "dialog-messages" else - return "image://icon/call-start" + return "call-start" } Rectangle { height: 30 width: 30 radius: 99 border.width: 1 border.color: Kirigami.Theme.disabledTextColor color: "transparent" - Image { - asynchronous: true + Kirigami.Icon { anchors.margins: 6 anchors.fill: parent + color: Kirigami.Theme.disabledTextColor source: getIcon() } } Item { Layout.preferredWidth: 10 } Text { text: display color: Kirigami.Theme.textColor } } diff --git a/src/jamitimelinebase/multicall.cpp b/src/jamitimelinebase/multicall.cpp index 43458ced..69775108 100644 --- a/src/jamitimelinebase/multicall.cpp +++ b/src/jamitimelinebase/multicall.cpp @@ -1,237 +1,250 @@ /*************************************************************************** * 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 "multicall.h" #include +#include #include #include #include #include class MultiCallPrivate { public: enum class Mode { UNDEFINED , /*!< Initialization */ SINGLE_MISSED , /*!< Single missing call */ SINGLE_ENTRY , /*!< Direction and length */ MULTI_ICON , /*!< Single row of small icons */ SUMMARY , /*!< Too many calls to display */ } m_Mode {Mode::UNDEFINED}; QPersistentModelIndex m_Index; static bool init; static QPixmap* iconCache[2][2]; MultiCall* q_ptr; int getHeight() const; void updateMode(); void paintRow (QPainter *painter); void paintMissed (QPainter *painter); void paintSingle (QPainter *painter); void paintSummary(QPainter *painter); }; bool MultiCallPrivate::init = false; QPixmap* MultiCallPrivate::iconCache[2][2] = { { nullptr, nullptr }, { nullptr, nullptr } }; MultiCall::MultiCall(QQuickItem* parent) : QQuickPaintedItem(parent), d_ptr(new MultiCallPrivate) { d_ptr->q_ptr = this; if (!d_ptr->init) { d_ptr->init = true; d_ptr->iconCache[1][0] = new QPixmap(QIcon(":/sharedassets/phone_dark/missed_incoming.svg").pixmap(28, 28)); d_ptr->iconCache[1][1] = new QPixmap(QIcon(":/sharedassets/phone_dark/missed_outgoing.svg").pixmap(28, 28)); d_ptr->iconCache[0][0] = new QPixmap(QIcon(":/sharedassets/phone_dark/incoming.svg" ).pixmap(28, 28)); d_ptr->iconCache[0][1] = new QPixmap(QIcon(":/sharedassets/phone_dark/outgoing.svg" ).pixmap(28, 28)); } setHeight(1); //Otherwise it will be treated as dead code setImplicitHeight(1); //Otherwise it will be treated as dead code } MultiCall::~MultiCall() { delete d_ptr; } void MultiCall::setModelIndex(const QPersistentModelIndex& idx) { d_ptr->m_Index = idx; //TODO support HiDPI setHeight(d_ptr->getHeight()); setImplicitHeight(d_ptr->getHeight()); update(); } QPersistentModelIndex MultiCall::modelIndex() const { return d_ptr->m_Index; } bool MultiCall::skipChildren() const { return true; } int MultiCallPrivate::getHeight() const { const int w(q_ptr->width()); if ((!m_Index.isValid()) || (!w)) return 1; - const int rc = m_Index.model()->rowCount(m_Index); + const int rc = m_Index.data( + (int)IndividualTimelineModel::Role::CallCount + ).toInt(); return 32*((rc*32)/w + ((rc*32)%w ? 1 : 0)); } void MultiCallPrivate::updateMode() { const int count = m_Index.data( (int)IndividualTimelineModel::Role::CallCount ).toInt(); if (count == 1) { const auto cidx = m_Index.model()->index(0, 0, m_Index); if (const auto event = qvariant_cast(cidx.data((int)Ring::Role::Object))) { const bool isMissed = event->status () == Event::Status::X_MISSED; const bool isOutgoing = event->direction() == Event::Direction::INCOMING; if (isMissed && isOutgoing) { m_Mode = Mode::SINGLE_MISSED; return; } m_Mode = Mode::SINGLE_ENTRY; return; } } if (count <= 10) { m_Mode = Mode::MULTI_ICON; return; } m_Mode = Mode::SUMMARY; } void MultiCallPrivate::paintMissed(QPainter *painter) { // } void MultiCallPrivate::paintSingle(QPainter *painter) { // } void MultiCallPrivate::paintSummary(QPainter *painter) { // } void MultiCallPrivate::paintRow(QPainter *painter) { const int w(q_ptr->width()); if (w < 32 || q_ptr->height() < 32) return; if ((!m_Index.isValid()) || (!w)) return; // In case there's a resize - const int rc = m_Index.model()->rowCount(m_Index); + const int rc = std::min( + m_Index.data((int)IndividualTimelineModel::Role::CallCount).toInt(), 10 + ); + const int h = getHeight(); if (h > q_ptr->height() + 1 || h < q_ptr->height() - 1) { // setHeight(h + 200); // update(); return; } const int perRow = w/32; + // Handle the case where a QSortFilterProxyModel is being used + const QAbstractProxyModel* pm = qobject_cast(m_Index.model()); + + const QModelIndex pidx = pm ? pm->mapToSource(m_Index) : QModelIndex(m_Index); + + const QAbstractItemModel* m = m ? pm->sourceModel() : m_Index.model(); + q_ptr->setImplicitWidth(rc*32); for (int i = 0; i < rc; i++) { - const auto cidx = m_Index.model()->index(i, 0, m_Index); + const auto cidx = m->index(i, 0, pidx); if (const auto event = qvariant_cast(cidx.data((int)Ring::Role::Object))) { const int col = (i/perRow) * 32; const int row = (i%perRow) * 32; const bool isMissed = event->status () == Event::Status::X_MISSED; const bool isOutgoing = event->direction() == Event::Direction::OUTGOING; painter->drawPixmap( QPoint{row, col}, *iconCache[isMissed][isOutgoing] ); } } } void MultiCall::paint(QPainter *painter) { d_ptr->updateMode(); switch(d_ptr->m_Mode) { case MultiCallPrivate::Mode::UNDEFINED: break; case MultiCallPrivate::Mode::SINGLE_MISSED: d_ptr->paintMissed(painter); break; case MultiCallPrivate::Mode::SINGLE_ENTRY: d_ptr->paintSingle(painter); break; case MultiCallPrivate::Mode::MULTI_ICON: d_ptr->paintRow(painter); break; case MultiCallPrivate::Mode::SUMMARY: d_ptr->paintSummary(painter); break; } } int MultiCall::count() const { if (!d_ptr->m_Index.model()) return 0; return d_ptr->m_Index.model()->rowCount(d_ptr->m_Index); } void MultiCall::setCount(int c) { Q_UNUSED(c) setHeight(d_ptr->getHeight()); setImplicitHeight(height()); update(); }