diff options
author | Christian Mollekopf <chrigi_1@fastmail.fm> | 2017-08-16 23:27:51 -0600 |
---|---|---|
committer | Christian Mollekopf <chrigi_1@fastmail.fm> | 2017-08-17 20:02:43 -0600 |
commit | de4be82bf141f1eb5f57189bf251123e57996f28 (patch) | |
tree | 28701506e5e77af0120b25f52aed02f252cb12ac | |
parent | a855b61ace6572e19c305cdee7bd080c5f89eb51 (diff) | |
download | kube-de4be82bf141f1eb5f57189bf251123e57996f28.tar.gz kube-de4be82bf141f1eb5f57189bf251123e57996f28.zip |
Non listview based conversationview
The listview deals badly with non uniformly sized items.
We use the buffer hack to ensure all items are loaded so it works at
all, and setting the current index resulted in unpredictable scrolling.
With this new approach we manage everything ourselves in a Flickable,
and just always load all delegates (which we also did before, but with a
hack). As an optimization it should be possible to avoid loading some
delegates until they become visible.
Note that ConversationView is thightly coupled to ConversationListView
due to dependencies on some properties in the delegate. This could be
handled more elegantly with attached properties.
In any case, this seems to work much, much better.
-rw-r--r-- | framework/qml/ConversationListView.qml | 138 | ||||
-rw-r--r-- | framework/qml/ConversationView.qml | 82 | ||||
-rw-r--r-- | framework/qmldir | 1 |
3 files changed, 155 insertions, 66 deletions
diff --git a/framework/qml/ConversationListView.qml b/framework/qml/ConversationListView.qml new file mode 100644 index 00000000..fb91e14d --- /dev/null +++ b/framework/qml/ConversationListView.qml | |||
@@ -0,0 +1,138 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2016 Michael Bohlender, <michael.bohlender@kdemail.net> | ||
3 | * Copyright (C) 2017 Christian Mollekopf, <mollekopf@kolabsystems.com> | ||
4 | * | ||
5 | * This program is free software; you can redistribute it and/or modify | ||
6 | * it under the terms of the GNU General Public License as published by | ||
7 | * the Free Software Foundation; either version 2 of the License, or | ||
8 | * (at your option) any later version. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU General Public License along | ||
16 | * with this program; if not, write to the Free Software Foundation, Inc., | ||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | ||
18 | */ | ||
19 | |||
20 | import QtQuick 2.7 | ||
21 | import QtQuick.Controls 2 | ||
22 | import QtQuick.Layouts 1.1 | ||
23 | import org.kube.framework 1.0 as Kube | ||
24 | |||
25 | import QtQml 2.2 as QtQml | ||
26 | |||
27 | Flickable { | ||
28 | id: root | ||
29 | focus: true | ||
30 | property alias model: repeater.model | ||
31 | property alias delegate: repeater.delegate | ||
32 | property int currentIndex: -1 | ||
33 | |||
34 | property var currentItem: null | ||
35 | |||
36 | function setCurrentItem() { | ||
37 | if (currentItem) { | ||
38 | currentItem.isCurrentItem = false | ||
39 | } | ||
40 | if (currentIndex >= 0 && activeFocus) { | ||
41 | var item = repeater.itemAt(currentIndex) | ||
42 | if (item) { | ||
43 | item.isCurrentItem = true | ||
44 | currentItem = item | ||
45 | } | ||
46 | } else { | ||
47 | currentItem = null | ||
48 | } | ||
49 | } | ||
50 | |||
51 | onCurrentIndexChanged: { | ||
52 | setCurrentItem() | ||
53 | } | ||
54 | |||
55 | onActiveFocusChanged: { | ||
56 | setCurrentItem() | ||
57 | } | ||
58 | |||
59 | //Optimize for view quality | ||
60 | pixelAligned: true | ||
61 | |||
62 | contentWidth: width | ||
63 | contentHeight: col.height | ||
64 | |||
65 | function scrollToIndex(index) { | ||
66 | var item = repeater.itemAt(index) | ||
67 | var pos = item.y | ||
68 | var scrollToEndPos = (root.contentHeight - root.height) | ||
69 | //Avoid scrolling past the end | ||
70 | if (pos < scrollToEndPos) { | ||
71 | root.contentY = pos | ||
72 | } else { | ||
73 | root.contentY = scrollToEndPos | ||
74 | } | ||
75 | } | ||
76 | |||
77 | onContentHeightChanged: { | ||
78 | if (repeater.count) { | ||
79 | //Scroll to the last item | ||
80 | currentIndex = repeater.count - 1 | ||
81 | scrollToIndex(repeater.count - 1) | ||
82 | } | ||
83 | } | ||
84 | |||
85 | property real span : contentY + height | ||
86 | Column { | ||
87 | id: col | ||
88 | width: parent.width | ||
89 | spacing: 2 | ||
90 | Repeater { | ||
91 | id: repeater | ||
92 | onCountChanged: { | ||
93 | for (var i = 0; i < count; i++) { | ||
94 | itemAt(i).index = i | ||
95 | } | ||
96 | } | ||
97 | } | ||
98 | } | ||
99 | |||
100 | function incrementCurrentIndex() { | ||
101 | if (currentIndex < repeater.count - 1) { | ||
102 | currentIndex = currentIndex + 1 | ||
103 | } | ||
104 | } | ||
105 | |||
106 | function decrementCurrentIndex() { | ||
107 | if (currentIndex > 0) { | ||
108 | currentIndex = currentIndex - 1 | ||
109 | } | ||
110 | } | ||
111 | |||
112 | Keys.onDownPressed: { | ||
113 | incrementCurrentIndex() | ||
114 | scrollToIndex(currentIndex) | ||
115 | } | ||
116 | |||
117 | Keys.onUpPressed: { | ||
118 | decrementCurrentIndex() | ||
119 | scrollToIndex(currentIndex) | ||
120 | } | ||
121 | |||
122 | Kube.ScrollHelper { | ||
123 | id: scrollHelper | ||
124 | flickable: root | ||
125 | anchors.fill: parent | ||
126 | } | ||
127 | |||
128 | //Intercept all scroll events, | ||
129 | //necessary due to the webengineview | ||
130 | Kube.MouseProxy { | ||
131 | anchors.fill: parent | ||
132 | target: scrollHelper | ||
133 | forwardWheelEvents: true | ||
134 | } | ||
135 | |||
136 | ScrollBar.vertical: ScrollBar {} | ||
137 | |||
138 | } | ||
diff --git a/framework/qml/ConversationView.qml b/framework/qml/ConversationView.qml index 85707300..03ea3065 100644 --- a/framework/qml/ConversationView.qml +++ b/framework/qml/ConversationView.qml | |||
@@ -36,7 +36,6 @@ FocusScope { | |||
36 | filter: Kube.Messages.mailSelection | 36 | filter: Kube.Messages.mailSelection |
37 | onMessageReceived: { | 37 | onMessageReceived: { |
38 | root.mail = message.mail | 38 | root.mail = message.mail |
39 | listView.forceLayout() | ||
40 | } | 39 | } |
41 | } | 40 | } |
42 | 41 | ||
@@ -52,10 +51,9 @@ FocusScope { | |||
52 | anchors.fill: parent | 51 | anchors.fill: parent |
53 | color: Kube.Colors.backgroundColor | 52 | color: Kube.Colors.backgroundColor |
54 | 53 | ||
55 | Kube.ListView { | 54 | Kube.ConversationListView { |
56 | id: listView | 55 | id: listView |
57 | focus: true | 56 | focus: true |
58 | currentIndex: -1 | ||
59 | 57 | ||
60 | anchors { | 58 | anchors { |
61 | top: parent.top | 59 | top: parent.top |
@@ -65,69 +63,33 @@ FocusScope { | |||
65 | //Shrink the listview if the content doesn't fill the full height, so the email appears on top instead of on the bottom. | 63 | //Shrink the listview if the content doesn't fill the full height, so the email appears on top instead of on the bottom. |
66 | height: Math.min(contentHeight, parent.height) | 64 | height: Math.min(contentHeight, parent.height) |
67 | 65 | ||
68 | verticalLayoutDirection: ListView.BottomToTop | ||
69 | |||
70 | onActiveFocusChanged: { | ||
71 | if (activeFocus) { | ||
72 | if (currentIndex < 0) { | ||
73 | currentIndex = 0 | ||
74 | } | ||
75 | } else { | ||
76 | currentIndex = -1 | ||
77 | } | ||
78 | } | ||
79 | |||
80 | //Position view so the last email begins on top of the view | ||
81 | onContentHeightChanged: { | ||
82 | //FIXME This doesn't work quite correctly when we have headers and footers in the listview and the mail loads to slowly and only one item to show. | ||
83 | listView.positionViewAtIndex(currentIndex, ListView.End) | ||
84 | } | ||
85 | |||
86 | Keys.onDownPressed: { | ||
87 | decrementCurrentIndex() | ||
88 | positionViewAtIndex(listView.currentIndex, ListView.End) | ||
89 | } | ||
90 | |||
91 | Keys.onUpPressed: { | ||
92 | incrementCurrentIndex() | ||
93 | positionViewAtIndex(listView.currentIndex, ListView.End) | ||
94 | } | ||
95 | |||
96 | onCurrentIndexChanged: { | ||
97 | markAsReadTimer.restart(); | ||
98 | } | ||
99 | |||
100 | model: Kube.MailListModel { | 66 | model: Kube.MailListModel { |
101 | mail: root.mail | 67 | mail: root.mail |
102 | } | 68 | } |
103 | 69 | ||
104 | header: Item { | ||
105 | height: Kube.Units.gridUnit * 0.5 | ||
106 | width: parent.width | ||
107 | } | ||
108 | |||
109 | footer: Item { | ||
110 | height: Kube.Units.gridUnit | ||
111 | width: parent.width | ||
112 | } | ||
113 | |||
114 | delegate: FocusScope { | 70 | delegate: FocusScope { |
115 | id: delegateRoot | 71 | id: delegateRoot |
116 | 72 | ||
117 | property var currentData: model | 73 | property var currentData: model |
74 | property bool isCurrentItem: false | ||
75 | property int index: -1 | ||
118 | 76 | ||
119 | focus: false | 77 | focus: false |
120 | activeFocusOnTab: false | 78 | activeFocusOnTab: false |
121 | 79 | ||
122 | height: sheet.height + Kube.Units.gridUnit | 80 | height: sheet.height + Kube.Units.gridUnit |
123 | width: parent.width | 81 | width: listView.width |
124 | visible: !((root.hideTrash && model.trash) || (root.hideNonTrash && !model.trash)) | 82 | visible: !((root.hideTrash && model.trash) || (root.hideNonTrash && !model.trash)) |
125 | 83 | ||
126 | MouseArea { | 84 | MouseArea { |
127 | anchors.fill: parent | 85 | anchors.fill: parent |
128 | hoverEnabled: true | 86 | hoverEnabled: true |
129 | onEntered: delegateRoot.ListView.view.currentIndex = index | 87 | onEntered: listView.currentIndex = delegateRoot.index |
130 | onClicked: delegateRoot.ListView.view.currentIndex = index | 88 | onClicked: { |
89 | listView.currentIndex = delegateRoot.index | ||
90 | listView.forceActiveFocus(Qt.MouseFocusReason) | ||
91 | } | ||
92 | z: 1.0 | ||
131 | } | 93 | } |
132 | 94 | ||
133 | MailViewer { | 95 | MailViewer { |
@@ -147,19 +109,15 @@ FocusScope { | |||
147 | draft: model.draft | 109 | draft: model.draft |
148 | sent: model.sent | 110 | sent: model.sent |
149 | incomplete: model.incomplete | 111 | incomplete: model.incomplete |
150 | current: delegateRoot.ListView.isCurrentItem | 112 | current: delegateRoot.isCurrentItem |
151 | } | 113 | } |
152 | } | 114 | } |
153 | 115 | ||
154 | 116 | onCurrentItemChanged: { | |
155 | //Optimize for view quality | 117 | if (currentItem) { |
156 | pixelAligned: true | 118 | markAsReadTimer.restart() |
157 | 119 | } | |
158 | 120 | } | |
159 | //The cacheBuffer needs to be large enough to fit the whole thread. | ||
160 | //Otherwise the contentHeight will constantly increase and decrease, | ||
161 | //which will break lot's of things. | ||
162 | cacheBuffer: 100000 | ||
163 | 121 | ||
164 | Timer { | 122 | Timer { |
165 | id: markAsReadTimer | 123 | id: markAsReadTimer |
@@ -172,14 +130,6 @@ FocusScope { | |||
172 | } | 130 | } |
173 | } | 131 | } |
174 | } | 132 | } |
175 | |||
176 | //Intercept all scroll events, | ||
177 | //necessary due to the webengineview | ||
178 | Kube.MouseProxy { | ||
179 | anchors.fill: parent | ||
180 | target: listView.mouseProxy | ||
181 | forwardWheelEvents: true | ||
182 | } | ||
183 | } | 133 | } |
184 | } | 134 | } |
185 | } | 135 | } |
diff --git a/framework/qmldir b/framework/qmldir index 02a66bb6..7ce99e2a 100644 --- a/framework/qmldir +++ b/framework/qmldir | |||
@@ -1,6 +1,7 @@ | |||
1 | module org.kube.framework | 1 | module org.kube.framework |
2 | 2 | ||
3 | ConversationView 1.0 ConversationView.qml | 3 | ConversationView 1.0 ConversationView.qml |
4 | ConversationListView 1.0 ConversationListView.qml | ||
4 | FolderListView 1.0 FolderListView.qml | 5 | FolderListView 1.0 FolderListView.qml |
5 | MailListView 1.0 MailListView.qml | 6 | MailListView 1.0 MailListView.qml |
6 | InlineAccountSwitcher 1.0 InlineAccountSwitcher.qml | 7 | InlineAccountSwitcher 1.0 InlineAccountSwitcher.qml |