Merge lp:~renatofilho/address-book-app/release-2014-07-02 into lp:address-book-app

Proposed by Renato Araujo Oliveira Filho
Status: Merged
Approved by: Bill Filler
Approved revision: 205
Merged at revision: 224
Proposed branch: lp:~renatofilho/address-book-app/release-2014-07-02
Merge into: lp:address-book-app
Diff against target: 1768 lines (+1109/-260)
23 files modified
README (+26/-0)
debian/control (+10/-2)
po/address-book-app.pot (+17/-13)
src/app/addressbookapp.cpp (+1/-1)
src/imports/CMakeLists.txt (+2/-0)
src/imports/Common/ContactDetailBase.qml (+7/-0)
src/imports/ContactList/ContactListPage.qml (+1/-15)
src/imports/ContactView/ContactView.qml (+11/-13)
src/imports/Ubuntu/Contacts/CMakeLists.txt (+31/-0)
src/imports/Ubuntu/Contacts/ContactDelegate.qml (+7/-7)
src/imports/Ubuntu/Contacts/ContactListView.qml (+4/-3)
src/imports/Ubuntu/Contacts/ContactSimpleListView.qml (+39/-6)
src/imports/Ubuntu/Contacts/ListItemWithActions.qml (+292/-0)
src/imports/Ubuntu/Contacts/MostCalledModel.qml (+27/-110)
src/imports/Ubuntu/Contacts/mostcalledproxymodel.cpp (+302/-0)
src/imports/Ubuntu/Contacts/mostcalledproxymodel.h (+95/-0)
src/imports/Ubuntu/Contacts/plugin.cpp (+33/-0)
src/imports/Ubuntu/Contacts/plugin.h (+34/-0)
src/imports/Ubuntu/Contacts/qmldir (+3/-0)
tests/CMakeLists.txt (+2/-0)
tests/autopilot/address_book_app/tests/test_add_contact.py (+0/-90)
tests/qml/CMakeLists.txt (+22/-0)
tests/qml/tst_ContactEditor.qml (+143/-0)
To merge this branch: bzr merge lp:~renatofilho/address-book-app/release-2014-07-02
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Needs Fixing
Ubuntu Phablet Team Pending
Review via email: mp+225418@code.launchpad.net

Commit message

* Implemented MostCalledContactsModel in C++ as a proxy model for HistoryEventModel;
* Enabled share button again.
* Fixed build warning: comparison with string literal results in unspecified behavior;
* Implemented swipe to delete.

To post a comment you must log in.
Revision history for this message
Renato Araujo Oliveira Filho (renatofilho) wrote :

Are there any related MPs required for this MP to build/function as expected? NO

Is your branch in sync with latest trunk? YES

Did you perform an exploratory manual test run of your code change and any related functionality on device or emulator? YES

Did you successfully run all tests found in your component's Test Plan on device or emulator? YES

If you changed the UI, was the change specified/approved by design? YES

If you changed the packaging (debian), did you add a core-dev as a reviewer to this MP? NO PACKAGE CHANGE

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
205. By Renato Araujo Oliveira Filho

Staging merged.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
206. By Renato Araujo Oliveira Filho

Does not install unit tests dependency for [arm64, powerpc and ppc64el]

Some packages necessary to run the unit test is not available for all architectures.

207. By Renato Araujo Oliveira Filho

Check for "qmltestrunner" and "xvfb-run" installation before run qml tests.

208. By Renato Araujo Oliveira Filho

Used cmake variable with full path for XVFB_RUN_BIN

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== renamed file 'README.click' => 'README'
2--- README.click 2014-06-07 13:07:42 +0000
3+++ README 2014-07-04 14:53:15 +0000
4@@ -1,3 +1,29 @@
5+Building for desktop
6+====================
7+
8+mkdir build
9+cd build
10+cmake ..
11+make
12+
13+Run on desktop
14+==============
15+
16+cd build
17+./src/app/address-book-app
18+
19+Run the QML tests
20+=================
21+
22+cd build
23+make test or ctest
24+
25+Run the Autopilot tests
26+=======================
27+
28+cd build
29+make autopilot
30+
31 Building for click
32 ==================
33
34
35=== modified file 'debian/control'
36--- debian/control 2014-06-24 21:57:09 +0000
37+++ debian/control 2014-07-04 14:53:15 +0000
38@@ -9,9 +9,18 @@
39 libgles2-mesa-dev,
40 python3,
41 pkg-config,
42+ qml-module-qttest [i386 amd64 armhf],
43+ qtdeclarative5-dev-tools [i386 amd64 armhf],
44+ qtdeclarative5-qtcontacts-plugin [i386 amd64 armhf],
45+ qtdeclarative5-ubuntu-content0.1 [i386 amd64 armhf],
46+ qtdeclarative5-ubuntu-keyboard-extensions0.1 [i386 amd64 armhf],
47+ qtdeclarative5-ubuntu-telephony-phonenumber0.1 [i386 amd64 armhf],
48+ qtdeclarative5-ubuntu-ui-toolkit-plugin [i386 amd64 armhf],
49 qt5-default,
50 qtbase5-dev,
51 qtdeclarative5-dev,
52+ qtpim5-dev,
53+ xvfb [i386 amd64 armhf],
54 Standards-Version: 3.9.5
55 Homepage: https://launchpad.net/address-book-app
56 # If you aren't a member of ~phablet-team but need to upload packaging changes,
57@@ -32,7 +41,6 @@
58 qtdeclarative5-ubuntu-history0.1,
59 qtdeclarative5-ubuntu-keyboard-extensions0.1,
60 qtdeclarative5-ubuntu-telephony-phonenumber0.1,
61- qtdeclarative5-ubuntu-telephony0.1,
62 ${misc:Depends},
63 ${shlibs:Depends},
64 Description: Address Book application
65@@ -76,4 +84,4 @@
66 address-book-app (>= ${binary:Version}),
67 ubuntu-mobile-icons,
68 Description: Test package for address-book-app
69- Autopilot tests for the address-book-app package
70+ Autopilot tests for the address-book-app package
71
72=== modified file 'po/address-book-app.pot'
73--- po/address-book-app.pot 2014-06-27 12:54:55 +0000
74+++ po/address-book-app.pot 2014-07-04 14:53:15 +0000
75@@ -8,7 +8,7 @@
76 msgstr ""
77 "Project-Id-Version: address-book-app\n"
78 "Report-Msgid-Bugs-To: \n"
79-"POT-Creation-Date: 2014-06-27 09:54-0300\n"
80+"POT-Creation-Date: 2014-06-30 15:01-0300\n"
81 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
82 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
83 "Language-Team: LANGUAGE <LL@li.org>\n"
84@@ -55,12 +55,12 @@
85
86 #: src/imports/ContactEdit/AddFieldDialog.qml:133
87 #: src/imports/ContactEdit/ContactEditor.qml:372
88-#: src/imports/ContactList/ContactListPage.qml:324
89+#: src/imports/ContactList/ContactListPage.qml:314
90 #: src/imports/Ubuntu/Contacts/DialogButtons.qml:37
91 msgid "Cancel"
92 msgstr ""
93
94-#: src/imports/ContactList/ContactListPage.qml:252
95+#: src/imports/ContactList/ContactListPage.qml:242
96 msgid "Cancel selection"
97 msgstr ""
98
99@@ -86,8 +86,8 @@
100 msgstr ""
101
102 #: src/imports/ContactEdit/ContactEditor.qml:337
103-#: src/imports/ContactList/ContactListPage.qml:148
104-#: src/imports/ContactList/ContactListPage.qml:277
105+#: src/imports/ContactList/ContactListPage.qml:146
106+#: src/imports/ContactList/ContactListPage.qml:267
107 msgid "Delete"
108 msgstr ""
109
110@@ -96,7 +96,7 @@
111 msgstr ""
112
113 #: src/imports/ContactEdit/ContactEditor.qml:141
114-#: src/imports/ContactView/ContactView.qml:219
115+#: src/imports/ContactView/ContactView.qml:217
116 msgid "Edit"
117 msgstr ""
118
119@@ -173,7 +173,7 @@
120 msgid "Loading"
121 msgstr ""
122
123-#: src/imports/ContactList/ContactListPage.qml:242
124+#: src/imports/ContactList/ContactListPage.qml:232
125 msgid "Loading..."
126 msgstr ""
127
128@@ -246,15 +246,15 @@
129 msgid "Save"
130 msgstr ""
131
132-#: src/imports/ContactList/ContactListPage.qml:302
133+#: src/imports/ContactList/ContactListPage.qml:292
134 msgid "Search"
135 msgstr ""
136
137-#: src/imports/ContactList/ContactListPage.qml:277
138+#: src/imports/ContactList/ContactListPage.qml:267
139 msgid "Select"
140 msgstr ""
141
142-#: src/imports/ContactList/ContactListPage.qml:261
143+#: src/imports/ContactList/ContactListPage.qml:251
144 msgid "Select All"
145 msgstr ""
146
147@@ -266,6 +266,10 @@
148 msgid "Select a field"
149 msgstr ""
150
151+#: src/imports/ContactView/ContactView.qml:206
152+msgid "Share"
153+msgstr ""
154+
155 #: src/imports/Ubuntu/Contacts/ContactDetailOnlineAccountTypeModel.qml:67
156 msgid "Skype"
157 msgstr ""
158@@ -279,15 +283,15 @@
159 msgid "Street"
160 msgstr ""
161
162-#: src/imports/ContactList/ContactListPage.qml:293
163+#: src/imports/ContactList/ContactListPage.qml:283
164 msgid "Sync"
165 msgstr ""
166
167-#: src/imports/ContactList/ContactListPage.qml:293
168+#: src/imports/ContactList/ContactListPage.qml:283
169 msgid "Syncing"
170 msgstr ""
171
172-#: src/imports/ContactList/ContactListPage.qml:242
173+#: src/imports/ContactList/ContactListPage.qml:232
174 msgid "Syncing..."
175 msgstr ""
176
177
178=== modified file 'src/app/addressbookapp.cpp'
179--- src/app/addressbookapp.cpp 2014-06-26 15:46:45 +0000
180+++ src/app/addressbookapp.cpp 2014-07-04 14:53:15 +0000
181@@ -74,7 +74,7 @@
182 QString appPath = QCoreApplication::applicationDirPath();
183 if (appPath.startsWith(ADDRESS_BOOK_DEV_BINDIR)) {
184 return QString(ADDRESS_BOOK_APP_DEV_DATADIR) + suffix;
185- } else if (QT_EXTRA_IMPORTS_DIR != ""){
186+ } else if (!QStringLiteral(QT_EXTRA_IMPORTS_DIR).isEmpty()) {
187 return QString(QT_EXTRA_IMPORTS_DIR) + suffix;
188 } else {
189 return "";
190
191=== modified file 'src/imports/CMakeLists.txt'
192--- src/imports/CMakeLists.txt 2014-06-11 21:25:08 +0000
193+++ src/imports/CMakeLists.txt 2014-07-04 14:53:15 +0000
194@@ -1,3 +1,5 @@
195+project(imports)
196+
197 set(ADDRESS_BOOK_APP_QMLS
198 MainWindow.qml
199 )
200
201=== modified file 'src/imports/Common/ContactDetailBase.qml'
202--- src/imports/Common/ContactDetailBase.qml 2014-06-12 18:54:18 +0000
203+++ src/imports/Common/ContactDetailBase.qml 2014-07-04 14:53:15 +0000
204@@ -60,6 +60,12 @@
205 imMap[QtContacts.OnlineAccount.Protocol] = "imProtocol"
206 imMap[QtContacts.OnlineAccount.Capabilities] = "imCaps"
207
208+ // organization
209+ var organizationMap = {}
210+ organizationMap[QtContacts.Organization.Name] = 'orgName'
211+ organizationMap[QtContacts.Organization.Role] = 'orgRole'
212+ organizationMap[QtContacts.Organization.Title] = 'orgTitle'
213+
214 // SyncTarget
215 var syncTargetMap = {}
216 syncTargetMap[QtContacts.SyncTarget.SyncTarget] = "syncTarget"
217@@ -71,6 +77,7 @@
218 detailMap[QtContacts.ContactDetail.Email] = emailMap
219 detailMap[QtContacts.ContactDetail.Address] = addressMap
220 detailMap[QtContacts.ContactDetail.OnlineAccount] = imMap
221+ detailMap[QtContacts.ContactDetail.Organization] = organizationMap
222 detailMap[QtContacts.ContactDetail.SyncTarget] = syncTargetMap
223
224 // detail name
225
226=== modified file 'src/imports/ContactList/ContactListPage.qml'
227--- src/imports/ContactList/ContactListPage.qml 2014-06-27 12:54:15 +0000
228+++ src/imports/ContactList/ContactListPage.qml 2014-07-04 14:53:15 +0000
229@@ -141,20 +141,10 @@
230 multipleSelection: !pickMode ||
231 mainPage.pickMultipleContacts || (contactExporter.active && contactExporter.isMultiple)
232
233- anchors.fill: parent
234-
235 leftSideAction: Action {
236 iconName: "delete"
237 text: i18n.tr("Delete")
238- onTriggered: {
239- value.makeDisappear()
240- }
241- }
242-
243- onContactDisappeared: {
244- if (contact) {
245- contactModel.removeContact(contact.contactId)
246- }
247+ onTriggered: value.remove()
248 }
249
250 onCountChanged: {
251@@ -168,10 +158,6 @@
252 mainPage.onlineAccountsMessageDialog = null
253 application.unsetFirstRun()
254 }
255-
256- if (mainPage.searching) {
257- contactList.positionViewAtBeginning()
258- }
259 }
260
261 onAddContactClicked: mainPage.createContactWithPhoneNumber(label)
262
263=== modified file 'src/imports/ContactView/ContactView.qml'
264--- src/imports/ContactView/ContactView.qml 2014-06-13 19:54:01 +0000
265+++ src/imports/ContactView/ContactView.qml 2014-07-04 14:53:15 +0000
266@@ -200,19 +200,17 @@
267 }
268 }
269 }
270- // FIXME: Having more than 3 options in the header causes a bug that make difficult to reach the component behind it.
271- // Enable it again when the bug #1329557 get fix
272-// ToolbarButton {
273-// action: Action {
274-// objectName: "share"
275-// text: i18n.tr("Share")
276-// iconName: "share"
277-// onTriggered: {
278-// pageStack.push(Qt.resolvedUrl("../ContactShare/ContactSharePage.qml"),
279-// { contactModel: root.model, contact: root.contact})
280-// }
281-// }
282-// }
283+ ToolbarButton {
284+ action: Action {
285+ objectName: "share"
286+ text: i18n.tr("Share")
287+ iconName: "share"
288+ onTriggered: {
289+ pageStack.push(Qt.resolvedUrl("../ContactShare/ContactSharePage.qml"),
290+ { contactModel: root.model, contact: root.contact})
291+ }
292+ }
293+ }
294 ToolbarButton {
295 action: Action {
296 objectName: "edit"
297
298=== modified file 'src/imports/Ubuntu/Contacts/CMakeLists.txt'
299--- src/imports/Ubuntu/Contacts/CMakeLists.txt 2014-06-18 18:19:09 +0000
300+++ src/imports/Ubuntu/Contacts/CMakeLists.txt 2014-07-04 14:53:15 +0000
301@@ -1,3 +1,5 @@
302+set(CONTACT_COMPONENTS_PLUGIN "ubuntu-contacts-qml")
303+
304 set(CONTACT_COMPONENTS_QMLS
305 ContactList.js
306 ContactAvatar.qml
307@@ -12,15 +14,44 @@
308 DialogButtons.qml
309 FastScroll.qml
310 FastScroll.js
311+ ListItemWithActions.qml
312 MostCalledModel.qml
313 MultipleSelectionListView.qml
314 MultipleSelectionVisualModel.qml
315 qmldir
316 )
317
318+set(CONTACT_COMPONENTS_SRC
319+ mostcalledproxymodel.h
320+ mostcalledproxymodel.cpp
321+ plugin.h
322+ plugin.cpp
323+)
324+
325+add_library(${CONTACT_COMPONENTS_PLUGIN} MODULE
326+ ${CONTACT_COMPONENTS_SRC}
327+)
328+
329+qt5_use_modules(${CONTACT_COMPONENTS_PLUGIN} Core Contacts Qml Quick)
330+
331 # make the files visible on qtcreator
332 add_custom_target(contact_components_QmlFiles ALL SOURCES ${CONTACT_COMPONENTS_QMLS})
333
334 if(INSTALL_COMPONENTS)
335 install(FILES ${CONTACT_COMPONENTS_QMLS} DESTINATION ${QMLPLUGIN_INSTALL_PREFIX})
336+ install(TARGETS ${CONTACT_COMPONENTS_PLUGIN} DESTINATION ${QMLPLUGIN_INSTALL_PREFIX})
337+endif()
338+
339+
340+#copy qml files to build dir to make it possible to run without install
341+
342+add_custom_target(copy_qml)
343+foreach(QML_FILE ${CONTACT_COMPONENTS_QMLS})
344+ add_custom_command(TARGET copy_qml PRE_BUILD
345+ COMMAND ${CMAKE_COMMAND} -E
346+ copy ${CMAKE_CURRENT_SOURCE_DIR}/${QML_FILE} ${CMAKE_CURRENT_BINARY_DIR}/)
347+endforeach()
348+
349+if (NOT ${CMAKE_CURRENT_BINARY_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR})
350+ add_dependencies(${CONTACT_COMPONENTS_PLUGIN} copy_qml)
351 endif()
352
353=== modified file 'src/imports/Ubuntu/Contacts/ContactDelegate.qml'
354--- src/imports/Ubuntu/Contacts/ContactDelegate.qml 2014-06-23 18:31:56 +0000
355+++ src/imports/Ubuntu/Contacts/ContactDelegate.qml 2014-07-04 14:53:15 +0000
356@@ -20,7 +20,7 @@
357 import Ubuntu.Components.ListItems 0.1 as ListItem
358 import "Contacts.js" as ContactsJS
359
360-Item {
361+ListItemWithActions {
362 id: root
363
364 property bool showAvatar: true
365@@ -32,6 +32,7 @@
366 property variant titleFields: [ Name.FirstName, Name.LastName ]
367 property bool detailsShown: false
368 property int loaderOpacity: 0.0
369+ property var _contact: contact
370
371 signal clicked(int index, QtObject contact)
372 signal pressAndHold(int index, QtObject contact)
373@@ -47,6 +48,10 @@
374 height: delegate.height
375 implicitHeight: delegate.height + (pickerLoader.item ? pickerLoader.item.height : 0)
376 width: parent ? parent.width : 0
377+ color: Theme.palette.normal.background
378+
379+ onItemClicked: root.clicked(index, contact)
380+ onItemPressAndHold: root.pressAndHold(index, contact)
381
382 Item {
383 id: delegate
384@@ -67,12 +72,6 @@
385 visible: root.selected || root.detailsShown
386 }
387
388- MouseArea {
389- anchors.fill: parent
390- onClicked: root.clicked(index, contact)
391- onPressAndHold: root.pressAndHold(index, contact)
392- }
393-
394 ContactAvatar {
395 id: avatar
396
397@@ -181,6 +180,7 @@
398 clip: true
399 height: root.implicitHeight
400 loaderOpacity: 1.0
401+ locked: true
402 // FIXME: Setting detailsShown to true on expanded state cause the property to change to false and true during the state transition, and that
403 // causes the loader to load twice
404 //detailsShown: true
405
406=== modified file 'src/imports/Ubuntu/Contacts/ContactListView.qml'
407--- src/imports/Ubuntu/Contacts/ContactListView.qml 2014-07-01 18:48:39 +0000
408+++ src/imports/Ubuntu/Contacts/ContactListView.qml 2014-07-04 14:53:15 +0000
409@@ -441,14 +441,14 @@
410 model: MostCalledModel {
411 id: calledModel
412
413- readonly property bool visible: view.favouritesIsSelected
414+ readonly property bool visible: view.favouritesIsSelected
415
416 onVisibleChanged: {
417+ // update the model every time that it became visible
418 if (visible) {
419- filterEntries()
420+ model.update()
421 }
422 }
423- maxCount: 20
424 onInfoRequested: root.infoRequested(contact)
425 onDetailClicked: root.detailClicked(contact, detail, action)
426 onAddContactClicked: root.addContactClicked(label)
427@@ -549,6 +549,7 @@
428 repeat: false
429 interval: 300
430 onTriggered: {
431+ view.positionViewAtBeginning()
432 var needUpdate = false
433 if (root.filterTerm === "") { // if the search criteria is empty clear the list before show all contacts
434 if (contactTermFilter.value !== "") {
435
436=== modified file 'src/imports/Ubuntu/Contacts/ContactSimpleListView.qml'
437--- src/imports/Ubuntu/Contacts/ContactSimpleListView.qml 2014-06-19 15:38:16 +0000
438+++ src/imports/Ubuntu/Contacts/ContactSimpleListView.qml 2014-07-04 14:53:15 +0000
439@@ -268,6 +268,13 @@
440 listDelegate: ContactDelegate {
441 id: contactDelegate
442
443+ property var removalAnimation
444+
445+ function remove()
446+ {
447+ removalAnimation.start()
448+ }
449+
450 width: parent.width
451 selected: contactListView.multiSelectionEnabled && contactListView.isSelected(contactDelegate)
452 defaultAvatarUrl: contactListView.defaultAvatarImageUrl
453@@ -275,17 +282,43 @@
454 titleFields: contactListView.titleFields
455 isCurrentItem: ListView.isCurrentItem
456
457+ // actions
458+ leftSideAction: contactListView.leftSideAction
459+ rightSideActions: contactListView.rightSideActions
460+
461 onDetailClicked: contactListView.detailClicked(contact, detail, action)
462 onInfoRequested: contactListView._fetchContact(index, contact)
463
464 // collapse the item before remove it, to avoid crash
465- ListView.onRemove: SequentialAnimation {
466+ ListView.onRemove: ScriptAction {
467+ script: {
468+ if (contactDelegate.state !== "") {
469+ contactListView.currentIndex = -1
470+ }
471+ }
472+ }
473+
474+ // used by swipe to delete
475+ removalAnimation: SequentialAnimation {
476+ alwaysRunToEnd: true
477+
478+ PropertyAction {
479+ target: contactDelegate
480+ property: "ListView.delayRemove"
481+ value: true
482+ }
483+ UbuntuNumberAnimation {
484+ target: contactDelegate
485+ property: "height"
486+ to: 1
487+ }
488+ PropertyAction {
489+ target: contactDelegate
490+ property: "ListView.delayRemove"
491+ value: false
492+ }
493 ScriptAction {
494- script: {
495- if (contactDelegate.state !== "") {
496- contactListView.currentIndex = -1
497- }
498- }
499+ script: contactListView.listModel.removeContact(contact.contactId)
500 }
501 }
502
503
504=== added file 'src/imports/Ubuntu/Contacts/ListItemWithActions.qml'
505--- src/imports/Ubuntu/Contacts/ListItemWithActions.qml 1970-01-01 00:00:00 +0000
506+++ src/imports/Ubuntu/Contacts/ListItemWithActions.qml 2014-07-04 14:53:15 +0000
507@@ -0,0 +1,292 @@
508+/*
509+ * Copyright (C) 2012-2014 Canonical, Ltd.
510+ *
511+ * This program is free software; you can redistribute it and/or modify
512+ * it under the terms of the GNU General Public License as published by
513+ * the Free Software Foundation; version 3.
514+ *
515+ * This program is distributed in the hope that it will be useful,
516+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
517+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
518+ * GNU General Public License for more details.
519+ *
520+ * You should have received a copy of the GNU General Public License
521+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
522+ */
523+
524+import QtQuick 2.2
525+import Ubuntu.Components 0.1
526+
527+Item {
528+ id: root
529+
530+ property Action leftSideAction: null
531+ property list<Action> rightSideActions
532+ property double defaultHeight: units.gu(8)
533+ property bool locked: false
534+ property Action activeAction: null
535+ property var activeItem: null
536+ property bool triggerActionOnMouseRelease: false
537+ property alias color: main.color
538+ default property alias contents: main.children
539+
540+ readonly property double actionWidth: units.gu(5)
541+ readonly property double threshold: 0.4
542+ readonly property string swipeState: main.x == 0 ? "Normal" : main.x > 0 ? "LeftToRight" : "RightToLeft"
543+
544+ signal itemClicked(var mouse)
545+ signal itemPressAndHold(var mouse)
546+
547+ function returnToBoundsRTL()
548+ {
549+ var xOffset = Math.abs(main.x)
550+ var actionFullWidth = actionWidth + units.gu(1)
551+
552+ if (xOffset < actionFullWidth) {
553+ main.x = 0
554+ } else if (xOffset > (actionFullWidth * rightActionsRepeater.count)) {
555+ main.x = - (actionFullWidth * rightActionsRepeater.count)
556+ } else {
557+ for (var i = rightActionsRepeater.count; i >= 2; i--) {
558+ if (xOffset >= (actionFullWidth * i)) {
559+ main.x = -(actionWidth * i)
560+ return
561+ }
562+ }
563+ main.x = -actionWidth
564+ }
565+ }
566+
567+ function returnToBoundsLTR()
568+ {
569+ var finalX = leftActionView.width
570+ if (main.x > (finalX * root.threshold))
571+ main.x = finalX
572+ else
573+ main.x = 0
574+ }
575+
576+ function returnToBounds()
577+ {
578+ if (main.x < 0) {
579+ returnToBoundsRTL()
580+ } else if (main.x > 0) {
581+ returnToBoundsLTR()
582+ }
583+ }
584+
585+ function contains(item, point)
586+ {
587+ return (point.x >= item.x) && (point.x <= (item.x + item.width)) && (point.y >= item.y) && (point.y <= (item.y + item.height));
588+ }
589+
590+ function getActionAt(point)
591+ {
592+ if (contains(leftActionView, point)) {
593+ return leftSideAction
594+ } else if (contains(rightActionsView, point)) {
595+ var newPoint = root.mapToItem(rightActionsView, point.x, point.y)
596+ for (var i = 0; i < rightActionsRepeater.count; i++) {
597+ var child = rightActionsRepeater.itemAt(i)
598+ if (contains(child, newPoint)) {
599+ return rightSideActions[i]
600+ }
601+ }
602+ }
603+ return null
604+ }
605+
606+ function updateActiveAction()
607+ {
608+ var xOffset = Math.abs(main.x)
609+ if (main.x < 0) {
610+ for (var i = rightActionsRepeater.count - 1; i >= 0; i--) {
611+ var child = rightActionsRepeater.itemAt(i)
612+ var childOffset = rightActionsView.width - child.x
613+ if (xOffset <= childOffset) {
614+ root.activeItem = child
615+ root.activeAction = root.rightSideActions[i]
616+ return
617+ }
618+ }
619+ } else {
620+ root.activeAction = null
621+ }
622+ }
623+
624+ function resetSwipe()
625+ {
626+ main.x = 0
627+ }
628+
629+ height: defaultHeight
630+ clip: height !== defaultHeight
631+
632+ Rectangle {
633+ id: leftActionView
634+
635+ anchors {
636+ top: parent.top
637+ bottom: parent.bottom
638+ left: parent.left
639+ }
640+ width: height
641+ visible: leftSideAction
642+ color: "red"
643+
644+ Icon {
645+ anchors.centerIn: parent
646+ name: leftSideAction ? leftSideAction.iconName : ""
647+ color: Theme.palette.selected.field
648+ height: units.gu(3)
649+ width: units.gu(3)
650+ }
651+ }
652+
653+ Item {
654+ id: rightActionsView
655+
656+ anchors {
657+ top: main.top
658+ right: parent.right
659+ bottom: main.bottom
660+ }
661+ width: rightActionsRepeater.count * (root.actionWidth + units.gu(1))
662+ Row {
663+ anchors.fill: parent
664+ spacing: units.gu(1)
665+ Repeater {
666+ id: rightActionsRepeater
667+
668+ model: rightSideActions
669+ Item {
670+ property alias image: img
671+
672+ anchors {
673+ top: parent.top
674+ bottom: parent.bottom
675+ }
676+ width: root.actionWidth
677+
678+ Icon {
679+ id: img
680+
681+ anchors.centerIn: parent
682+ width: units.gu(3)
683+ height: units.gu(3)
684+ name: iconName
685+ color: root.activeAction === modelData || !root.triggerActionOnMouseRelease ? UbuntuColors.lightAubergine : Theme.palette.selected.background
686+ }
687+ }
688+ }
689+ }
690+ }
691+
692+
693+ Rectangle {
694+ id: main
695+
696+ anchors {
697+ top: parent.top
698+ bottom: parent.bottom
699+ }
700+
701+ width: parent.width
702+ Behavior on x {
703+ UbuntuNumberAnimation {
704+ easing.type: Easing.OutElastic
705+ duration: UbuntuAnimation.SlowDuration
706+ }
707+ }
708+ }
709+
710+ SequentialAnimation {
711+ id: triggerAction
712+
713+ property var currentItem: root.activeItem ? root.activeItem.image : null
714+
715+ running: false
716+ ParallelAnimation {
717+ UbuntuNumberAnimation {
718+ target: triggerAction.currentItem
719+ property: "opacity"
720+ from: 1.0
721+ to: 0.0
722+ duration: UbuntuAnimation.SlowDuration
723+ easing {type: Easing.InOutBack; }
724+ }
725+ UbuntuNumberAnimation {
726+ target: triggerAction.currentItem
727+ properties: "width, height"
728+ from: units.gu(3)
729+ to: root.actionWidth
730+ duration: UbuntuAnimation.SlowDuration
731+ easing {type: Easing.InOutBack; }
732+ }
733+ }
734+ ScriptAction {
735+ script: {
736+ root.activeAction.triggered(root)
737+ }
738+ }
739+ PropertyAction {
740+ target: triggerAction.currentItem
741+ properties: "width, height"
742+ value: units.gu(3)
743+ }
744+ PropertyAction {
745+ target: triggerAction.currentItem
746+ properties: "opacity"
747+ value: 1.0
748+ }
749+ UbuntuNumberAnimation {
750+ target: main
751+ property: "x"
752+ to: 0
753+ easing.type: Easing.OutElastic
754+ duration: UbuntuAnimation.SlowDuration
755+ }
756+ }
757+
758+ MouseArea {
759+ id: mouseArea
760+
761+ property bool locked: root.locked || ((root.leftSideAction === null) && (root.rightSideActions.count === 0))
762+
763+ anchors.fill: parent
764+ drag {
765+ target: locked ? null : main
766+ axis: Drag.XAxis
767+ minimumX: -rightActionsView.width
768+ maximumX: leftActionView.visible ? leftActionView.width : 0
769+ }
770+
771+ onReleased: {
772+ if (root.triggerActionOnMouseRelease && root.activeAction) {
773+ triggerAction.start()
774+ } else {
775+ root.returnToBounds()
776+ }
777+ }
778+ onClicked: {
779+ if (main.x === 0) {
780+ root.itemClicked(mouse)
781+ return
782+ }
783+
784+ var action = getActionAt(Qt.point(mouse.x, mouse.y))
785+ if (action) {
786+ action.triggered(root)
787+ }
788+ root.resetSwipe()
789+ }
790+
791+ onPositionChanged: updateActiveAction()
792+ onPressAndHold: {
793+ if (main.x === 0) {
794+ root.itemPressAndHold(mouse)
795+ }
796+ }
797+ z: -1
798+ }
799+}
800
801=== modified file 'src/imports/Ubuntu/Contacts/MostCalledModel.qml'
802--- src/imports/Ubuntu/Contacts/MostCalledModel.qml 2014-07-01 18:48:39 +0000
803+++ src/imports/Ubuntu/Contacts/MostCalledModel.qml 2014-07-04 14:53:15 +0000
804@@ -18,14 +18,12 @@
805 import QtQuick 2.2
806 import QtContacts 5.0
807 import Ubuntu.History 0.1
808-import Ubuntu.Telephony 0.1
809+import Ubuntu.Contacts 0.1
810
811 VisualDataModel {
812 id: root
813
814- property int maxCount: 10
815 property var contactModel: null
816- property var historyModel
817 property int currentIndex: -1
818
819 signal clicked(int index, QtObject contact)
820@@ -33,115 +31,39 @@
821 signal infoRequested(int index, QtObject contact)
822 signal addContactClicked(string label)
823
824- function filterEntries()
825- {
826- var contacts = {}
827- var interval = new Date()
828- var secs = (interval.getTime() - 2592000000) // one month ago
829- interval.setTime(secs)
830-
831- var totalCount = 0
832- var i = 0;
833- while(true) {
834- var event = historyModel.getItem(i)
835- if (!event) {
836- break
837- }
838-
839- if (event.timestamp < interval) {
840- break
841- }
842-
843- var participants = event.participants
844- for (var p=0; p < participants.length; p++) {
845- var phoneNumber = participants[p]
846- if (phoneNumber) {
847- if (contacts[phoneNumber] === undefined) {
848- contacts[phoneNumber] = 1
849- } else {
850- var count = contacts[phoneNumber]
851- contacts[phoneNumber] = count + 1
852- }
853- totalCount += 1
854- }
855- }
856- i++
857- }
858-
859- listModel.clear()
860- if (totalCount == 0) {
861- return
862- }
863-
864- // sort phones most called first
865- var mostCalledFirst = []
866- for (var key in contacts) {
867- mostCalledFirst.push([key, contacts[key]]);
868- }
869-
870- mostCalledFirst.sort(function(a, b) {
871- a = a[1];
872- b = b[1];
873-
874- return a < b ? -1 : (a > b ? 1 : 0);
875- });
876-
877- contacts = {}
878- for (var i = 0; i < mostCalledFirst.length; i++) {
879- var key = mostCalledFirst[i][0];
880- var value = mostCalledFirst[i][1];
881- contacts[key] = value
882- }
883-
884- // get the avarage frequency
885- var average = totalCount / mostCalledFirst.length
886-
887- for (var phone in contacts) {
888- if (contacts[phone] >= average) {
889- listModel.insert(0, {"participant": phone})
890- if (listModel.count >= root.maxCount) {
891- return;
892- }
893- }
894- }
895- }
896-
897- model: ListModel {
898- id: listModel
899- }
900-
901- historyModel: HistoryEventModel {
902-
903- function getItem(row) {
904- while ((row >= count) && (canFetchMore())) {
905- fetchMore()
906- }
907- return get(row)
908- }
909-
910- type: HistoryThreadModel.EventTypeVoice
911- sort: HistorySort {
912- sortField: "timestamp"
913- sortOrder: HistorySort.DescendingOrder
914- }
915- }
916-
917+
918+ model: MostCalledContactsModel {
919+ startInterval: new Date((new Date().getTime() - 2592000000)) // one month ago
920+ sourceModel: HistoryEventModel {
921+ type: HistoryThreadModel.EventTypeVoice
922+ sort: HistorySort {
923+ sortField: "timestamp"
924+ sortOrder: HistorySort.DescendingOrder
925+ }
926+ filter: HistoryFilter {
927+ filterProperty: "senderId"
928+ filterValue: "self"
929+ matchFlags: HistoryFilter.MatchCaseSensitive
930+ }
931+ }
932+ }
933
934 delegate: ContactDelegate {
935 id: contactDelegate
936
937 readonly property alias contact: contactFetch.contact
938+ property var contents
939
940 onDetailClicked: root.detailClicked(contact, detail, action)
941 onInfoRequested: root.infoRequested(index, contact)
942 onAddContactClicked: root.addContactClicked(label)
943
944 defaultAvatarUrl: "image://theme/contacts"
945- defaultTitle: participant
946- width: parent.width
947+ width: parent ? parent.width : 0
948 titleDetail: ContactDetail.DisplayLabel
949 titleFields: [ DisplayLabel.Label ]
950 isCurrentItem: root.currentIndex === index
951+ locked: true
952
953 // collapse the item before remove it, to avoid crash
954 ListView.onRemove: SequentialAnimation {
955@@ -166,17 +88,12 @@
956 }
957 }
958
959- ContactWatcher {
960- id: contactWatcher
961-
962- phoneNumber: participant
963- onContactIdChanged: contactFetch.fetchContact(contactId)
964- }
965-
966- ContactFetch {
967- id: contactFetch
968-
969- model: contactsModel
970- }
971+ // delegate does not support more than one child
972+ contents: ContactFetch {
973+ id: contactFetch
974+ model: contactsModel
975+ }
976+
977+ Component.onCompleted: contactFetch.fetchContact(contactId)
978 }
979 }
980
981=== added file 'src/imports/Ubuntu/Contacts/mostcalledproxymodel.cpp'
982--- src/imports/Ubuntu/Contacts/mostcalledproxymodel.cpp 1970-01-01 00:00:00 +0000
983+++ src/imports/Ubuntu/Contacts/mostcalledproxymodel.cpp 2014-07-04 14:53:15 +0000
984@@ -0,0 +1,302 @@
985+/*
986+ * Copyright (C) 2012-2013 Canonical, Ltd.
987+ *
988+ * This program is free software; you can redistribute it and/or modify
989+ * it under the terms of the GNU General Public License as published by
990+ * the Free Software Foundation; version 3.
991+ *
992+ * This program is distributed in the hope that it will be useful,
993+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
994+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
995+ * GNU General Public License for more details.
996+ *
997+ * You should have received a copy of the GNU General Public License
998+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
999+ */
1000+
1001+#include "mostcalledproxymodel.h"
1002+
1003+#include <QtContacts/QContactManager>
1004+#include <QtContacts/QContactFilter>
1005+#include <QtContacts/QContactPhoneNumber>
1006+
1007+#include <QDebug>
1008+
1009+using namespace QtContacts;
1010+
1011+bool mostCalledContactsModelDataLessThan(const MostCalledContactsModelData &d1, const MostCalledContactsModelData &d2)
1012+{
1013+ return d1.callCount < d2.callCount;
1014+}
1015+
1016+MostCalledContactsModel::MostCalledContactsModel(QObject *parent)
1017+ : QAbstractListModel(parent),
1018+ m_sourceModel(0),
1019+ m_manager(new QContactManager("galera")),
1020+ m_maxCount(20),
1021+ m_average(0),
1022+ m_outdated(true),
1023+ m_reloadingModel(false)
1024+{
1025+ connect(this, SIGNAL(sourceModelChanged(QAbstractItemModel*)), SLOT(markAsOutdated()));
1026+ connect(this, SIGNAL(maxCountChanged(uint)), SLOT(markAsOutdated()));
1027+ connect(this, SIGNAL(startIntervalChanged(QDateTime)), SLOT(markAsOutdated()));
1028+ connect(this, SIGNAL(modelReset()), SIGNAL(outdatedChange(bool)));
1029+}
1030+
1031+MostCalledContactsModel::~MostCalledContactsModel()
1032+{
1033+}
1034+
1035+QVariant MostCalledContactsModel::data(const QModelIndex &index, int role) const
1036+{
1037+ if (!index.isValid()) {
1038+ return QVariant();
1039+ }
1040+
1041+ int row = index.row();
1042+ if ((row >= 0) && (row < m_data.size())) {
1043+ switch (role)
1044+ {
1045+ case MostCalledContactsModel::ContactIdRole:
1046+ return m_data[row].contactId;
1047+ case MostCalledContactsModel::PhoneNumberRole:
1048+ return m_data[row].phoneNumber;
1049+ case MostCalledContactsModel::CallCountRole:
1050+ return m_data[row].callCount;
1051+ default:
1052+ return QVariant();
1053+ }
1054+ }
1055+ return QVariant();
1056+}
1057+
1058+QHash<int, QByteArray> MostCalledContactsModel::roleNames() const
1059+{
1060+ static QHash<int, QByteArray> roles;
1061+ if (roles.isEmpty()) {
1062+ roles.insert(MostCalledContactsModel::ContactIdRole, "contactId");
1063+ roles.insert(MostCalledContactsModel::PhoneNumberRole, "phoneNumber");
1064+ roles.insert(MostCalledContactsModel::CallCountRole, "callCount");
1065+ }
1066+ return roles;
1067+}
1068+
1069+int MostCalledContactsModel::rowCount(const QModelIndex &) const
1070+{
1071+ return m_data.size();
1072+}
1073+
1074+QAbstractItemModel *MostCalledContactsModel::sourceModel() const
1075+{
1076+ return m_sourceModel;
1077+}
1078+
1079+void MostCalledContactsModel::setSourceModel(QAbstractItemModel *model)
1080+{
1081+ if (m_sourceModel != model) {
1082+ if (m_sourceModel) {
1083+ disconnect(m_sourceModel);
1084+ }
1085+
1086+ m_sourceModel = model;
1087+ connect(m_sourceModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)), SLOT(markAsOutdated()));
1088+ connect(m_sourceModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(markAsOutdated()));
1089+ connect(m_sourceModel, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(markAsOutdated()));
1090+ connect(m_sourceModel, SIGNAL(modelReset()), SLOT(markAsOutdated()));
1091+
1092+ Q_EMIT sourceModelChanged(m_sourceModel);
1093+ }
1094+}
1095+
1096+
1097+uint MostCalledContactsModel::maxCount() const
1098+{
1099+ return m_maxCount;
1100+}
1101+
1102+void MostCalledContactsModel::setMaxCount(uint value)
1103+{
1104+ if (m_maxCount != value) {
1105+ m_maxCount = value;
1106+ Q_EMIT maxCountChanged(m_maxCount);
1107+ }
1108+}
1109+
1110+int MostCalledContactsModel::callAverage() const
1111+{
1112+ return m_average;
1113+}
1114+
1115+QDateTime MostCalledContactsModel::startInterval() const
1116+{
1117+ return m_startInterval;
1118+}
1119+
1120+void MostCalledContactsModel::setStartInterval(const QDateTime &value)
1121+{
1122+ if (m_startInterval != value) {
1123+ m_startInterval = value;
1124+ Q_EMIT startIntervalChanged(m_startInterval);
1125+ }
1126+}
1127+
1128+QVariant MostCalledContactsModel::getSourceData(int row, int role)
1129+{
1130+ QAbstractItemModel *source = sourceModel();
1131+ if (!source) {
1132+ return QVariant();
1133+ }
1134+
1135+ while ((source->rowCount() <= row) && (source->canFetchMore(QModelIndex()))) {
1136+ source->fetchMore(QModelIndex());
1137+ }
1138+
1139+ if (source->rowCount() < row) {
1140+ return QVariant();
1141+ }
1142+
1143+ QModelIndex sourceIndex = source->index(row, 0);
1144+ return source->data(sourceIndex, role);
1145+}
1146+
1147+QString MostCalledContactsModel::fetchContactId(const QString &phoneNumber)
1148+{
1149+ QContactFilter filter(QContactPhoneNumber::match(phoneNumber));
1150+ QContactFetchHint hint;
1151+ hint.setDetailTypesHint(QList<QContactDetail::DetailType>() << QContactDetail::TypeGuid);
1152+ QList<QContact> contacts = m_manager->contacts(filter, QList<QContactSortOrder>() , hint);
1153+ if (contacts.isEmpty()) {
1154+ return QString();
1155+ }
1156+ return contacts[0].id().toString();
1157+}
1158+
1159+void MostCalledContactsModel::update()
1160+{
1161+ // skip update if not necessary
1162+ if (!m_outdated || m_reloadingModel) {
1163+ return;
1164+ }
1165+
1166+ Q_EMIT beginResetModel();
1167+
1168+ m_reloadingModel = true;
1169+ m_outdated = false;
1170+ m_data.clear();
1171+ m_average = 0;
1172+
1173+ if (m_maxCount <= 0) {
1174+ qWarning() << "update model requested with invalid maxCount";
1175+ Q_EMIT endResetModel();
1176+ m_reloadingModel = false;
1177+ return;
1178+ }
1179+
1180+ if (!m_startInterval.isValid()) {
1181+ qWarning() << "Update model requested with invalid startInterval";
1182+ Q_EMIT endResetModel();
1183+ m_reloadingModel = false;
1184+ return;
1185+ }
1186+
1187+ QAbstractItemModel *source = sourceModel();
1188+ if (!source) {
1189+ qWarning() << "Update model requested with null source model";
1190+ m_outdated = false;
1191+ Q_EMIT endResetModel();
1192+ m_reloadingModel = false;
1193+ return;
1194+ }
1195+
1196+ QHash<int, QByteArray> roles = source->roleNames();
1197+ int participantsRole = roles.key("participants", -1);
1198+ int timestampRole = roles.key("timestamp", -1);
1199+ int row = 0;
1200+
1201+ Q_ASSERT(participantsRole != -1);
1202+ Q_ASSERT(timestampRole != -1);
1203+
1204+ QMap<QString, QString> phoneToContactCache;
1205+ QMap<QString, MostCalledContactsModelData > contactsData;
1206+
1207+ // get all call in the interval
1208+ int totalCalls = 0;
1209+ while(true) {
1210+ QVariant date = getSourceData(row, timestampRole);
1211+
1212+ // end of source model
1213+ if (date.isNull()) {
1214+ break;
1215+ }
1216+
1217+ // exit if date is out of interval
1218+ if (date.toDateTime() < m_startInterval) {
1219+ break;
1220+ }
1221+
1222+ QVariant participants = getSourceData(row, participantsRole);
1223+ if (participants.isValid()) {
1224+ Q_FOREACH(const QString phone, participants.toStringList()) {
1225+ QString contactId;
1226+ if (phoneToContactCache.contains(phone)) {
1227+ contactId = phoneToContactCache.value(phone);
1228+ } else {
1229+ contactId = fetchContactId(phone);
1230+ }
1231+
1232+ // skip uknown contacts
1233+ if (contactId.isEmpty()) {
1234+ continue;
1235+ }
1236+
1237+ if (contactsData.contains(contactId)) {
1238+ MostCalledContactsModelData &data = contactsData[contactId];
1239+ data.callCount++;
1240+ } else {
1241+ MostCalledContactsModelData data;
1242+ data.contactId = contactId;
1243+ data.phoneNumber = phone;
1244+ data.callCount = 1;
1245+ contactsData.insert(contactId, data);
1246+ }
1247+ totalCalls++;
1248+ }
1249+ }
1250+ row++;
1251+ }
1252+
1253+ if (!contactsData.isEmpty()) {
1254+ // sort by callCount
1255+ QList<MostCalledContactsModelData> data = contactsData.values();
1256+ qSort(data.begin(), data.end(), mostCalledContactsModelDataLessThan);
1257+
1258+ // average
1259+ m_average = totalCalls / contactsData.size();
1260+
1261+ Q_FOREACH(const MostCalledContactsModelData &d, data) {
1262+ if (d.callCount >= m_average) {
1263+ m_data << d;
1264+ }
1265+ if ((uint) m_data.size() > m_maxCount) {
1266+ break;
1267+ }
1268+ }
1269+ }
1270+
1271+ Q_EMIT endResetModel();
1272+ m_reloadingModel = false;
1273+}
1274+
1275+void MostCalledContactsModel::markAsOutdated()
1276+{
1277+ // skip if model is being reloaded
1278+ if (m_reloadingModel) {
1279+ return;
1280+ }
1281+
1282+ if (!m_outdated) {
1283+ m_outdated = true;
1284+ Q_EMIT outdatedChange(m_outdated);
1285+ }
1286+}
1287
1288=== added file 'src/imports/Ubuntu/Contacts/mostcalledproxymodel.h'
1289--- src/imports/Ubuntu/Contacts/mostcalledproxymodel.h 1970-01-01 00:00:00 +0000
1290+++ src/imports/Ubuntu/Contacts/mostcalledproxymodel.h 2014-07-04 14:53:15 +0000
1291@@ -0,0 +1,95 @@
1292+/*
1293+ * Copyright (C) 2012-2013 Canonical, Ltd.
1294+ *
1295+ * This program is free software; you can redistribute it and/or modify
1296+ * it under the terms of the GNU General Public License as published by
1297+ * the Free Software Foundation; version 3.
1298+ *
1299+ * This program is distributed in the hope that it will be useful,
1300+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1301+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1302+ * GNU General Public License for more details.
1303+ *
1304+ * You should have received a copy of the GNU General Public License
1305+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1306+ */
1307+
1308+#ifndef __MOSTCALLEDCONTACTSMODEL_H__
1309+#define __MOSTCALLEDCONTACTSMODEL_H__
1310+
1311+#include <QtCore/QAbstractListModel>
1312+#include <QtCore/QScopedPointer>
1313+#include <QtCore/QDateTime>
1314+
1315+#include <QtContacts/QContactManager>
1316+
1317+struct MostCalledContactsModelData
1318+{
1319+ QString contactId;
1320+ QString phoneNumber;
1321+ int callCount;
1322+};
1323+
1324+class MostCalledContactsModel : public QAbstractListModel
1325+{
1326+ Q_OBJECT
1327+ Q_PROPERTY(QAbstractItemModel* sourceModel READ sourceModel WRITE setSourceModel NOTIFY sourceModelChanged)
1328+ Q_PROPERTY(uint maxCount READ maxCount WRITE setMaxCount NOTIFY maxCountChanged)
1329+ Q_PROPERTY(int callAverage READ callAverage NOTIFY callAverageChanged)
1330+ Q_PROPERTY(QDateTime startInterval READ startInterval WRITE setStartInterval NOTIFY startIntervalChanged)
1331+ Q_PROPERTY(bool outdated READ outdated NOTIFY outdatedChange)
1332+
1333+public:
1334+ enum Role {
1335+ ContactIdRole = 0,
1336+ PhoneNumberRole,
1337+ CallCountRole
1338+ };
1339+
1340+ MostCalledContactsModel(QObject *parent=0);
1341+ ~MostCalledContactsModel();
1342+
1343+ QVariant data(const QModelIndex &index, int role) const;
1344+ QHash<int, QByteArray> roleNames() const;
1345+ int rowCount(const QModelIndex&) const;
1346+
1347+ QAbstractItemModel *sourceModel() const;
1348+ void setSourceModel(QAbstractItemModel *model);
1349+
1350+ uint maxCount() const;
1351+ void setMaxCount(uint value);
1352+
1353+ int callAverage() const;
1354+ bool outdated() const;
1355+
1356+ QDateTime startInterval() const;
1357+ void setStartInterval(const QDateTime &value);
1358+
1359+ Q_INVOKABLE void update();
1360+
1361+Q_SIGNALS:
1362+ void maxCountChanged(uint value);
1363+ void callAverageChanged(int value);
1364+ void startIntervalChanged(const QDateTime &value);
1365+ void sourceModelChanged(QAbstractItemModel *value);
1366+ void outdatedChange(bool value);
1367+
1368+private Q_SLOTS:
1369+ void markAsOutdated();
1370+
1371+private:
1372+ QAbstractItemModel *m_sourceModel;
1373+ QScopedPointer<QtContacts::QContactManager> m_manager;
1374+ QList<MostCalledContactsModelData> m_data;
1375+ uint m_maxCount;
1376+ int m_average;
1377+ QDateTime m_startInterval;
1378+ bool m_outdated;
1379+ bool m_reloadingModel;
1380+
1381+ QString fetchContactId(const QString &phoneNumber);
1382+ QVariant getSourceData(int row, int role);
1383+};
1384+
1385+
1386+#endif
1387
1388=== added file 'src/imports/Ubuntu/Contacts/plugin.cpp'
1389--- src/imports/Ubuntu/Contacts/plugin.cpp 1970-01-01 00:00:00 +0000
1390+++ src/imports/Ubuntu/Contacts/plugin.cpp 2014-07-04 14:53:15 +0000
1391@@ -0,0 +1,33 @@
1392+/*
1393+ * Copyright (C) 2012-2013 Canonical, Ltd.
1394+ *
1395+ * This program is free software; you can redistribute it and/or modify
1396+ * it under the terms of the GNU General Public License as published by
1397+ * the Free Software Foundation; version 3.
1398+ *
1399+ * This program is distributed in the hope that it will be useful,
1400+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1401+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1402+ * GNU General Public License for more details.
1403+ *
1404+ * You should have received a copy of the GNU General Public License
1405+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1406+ */
1407+
1408+#include "plugin.h"
1409+#include "mostcalledproxymodel.h"
1410+
1411+#include <QQmlEngine>
1412+#include <qqml.h>
1413+
1414+void UbuntuContacts::initializeEngine(QQmlEngine *engine, const char *uri)
1415+{
1416+ Q_UNUSED(engine);
1417+ Q_UNUSED(uri);
1418+}
1419+
1420+void UbuntuContacts::registerTypes(const char *uri)
1421+{
1422+ // @uri Ubuntu.Contacts
1423+ qmlRegisterType<MostCalledContactsModel>(uri, 0, 1, "MostCalledContactsModel");
1424+}
1425
1426=== added file 'src/imports/Ubuntu/Contacts/plugin.h'
1427--- src/imports/Ubuntu/Contacts/plugin.h 1970-01-01 00:00:00 +0000
1428+++ src/imports/Ubuntu/Contacts/plugin.h 2014-07-04 14:53:15 +0000
1429@@ -0,0 +1,34 @@
1430+/*
1431+ * Copyright (C) 2012-2013 Canonical, Ltd.
1432+ *
1433+ * This program is free software; you can redistribute it and/or modify
1434+ * it under the terms of the GNU General Public License as published by
1435+ * the Free Software Foundation; version 3.
1436+ *
1437+ * This program is distributed in the hope that it will be useful,
1438+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1439+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1440+ * GNU General Public License for more details.
1441+ *
1442+ * You should have received a copy of the GNU General Public License
1443+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1444+ */
1445+
1446+
1447+#ifndef _UBUNTU_CONTACTS_H_
1448+#define _UBUNTU_CONTACTS_H_
1449+
1450+#include <QQmlContext>
1451+#include <QQmlExtensionPlugin>
1452+
1453+class UbuntuContacts : public QQmlExtensionPlugin
1454+{
1455+ Q_OBJECT
1456+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
1457+
1458+public:
1459+ void initializeEngine(QQmlEngine *engine, const char *uri);
1460+ void registerTypes(const char *uri);
1461+};
1462+
1463+#endif //_UBUNTU_CONTACTS_H_
1464
1465=== modified file 'src/imports/Ubuntu/Contacts/qmldir'
1466--- src/imports/Ubuntu/Contacts/qmldir 2014-06-18 18:19:09 +0000
1467+++ src/imports/Ubuntu/Contacts/qmldir 2014-07-04 14:53:15 +0000
1468@@ -1,10 +1,13 @@
1469 module Ubuntu.Contacts
1470
1471+plugin ubuntu-contacts-qml
1472+
1473 ContactAvatar 0.1 ContactAvatar.qml
1474 ContactDetailOnlineAccountTypeModel 0.1 ContactDetailOnlineAccountTypeModel.qml
1475 ContactDetailPhoneNumberTypeModel 0.1 ContactDetailPhoneNumberTypeModel.qml
1476 ContactFetch 0.1 ContactFetch.qml
1477 ContactListView 0.1 ContactListView.qml
1478+ListItemWithActions 0.1 ListItemWithActions.qml
1479 MultipleSelectionListView 0.1 MultipleSelectionListView.qml
1480
1481 internal ContactSimpleListView ContactSimpleListView.qml
1482
1483=== modified file 'tests/CMakeLists.txt'
1484--- tests/CMakeLists.txt 2013-12-12 15:55:57 +0000
1485+++ tests/CMakeLists.txt 2014-07-04 14:53:15 +0000
1486@@ -1,3 +1,5 @@
1487+add_subdirectory(qml)
1488+
1489 if(ENABLE_AUTOPILOT)
1490 add_subdirectory(autopilot)
1491 add_subdirectory(data)
1492
1493=== modified file 'tests/autopilot/address_book_app/tests/test_add_contact.py'
1494--- tests/autopilot/address_book_app/tests/test_add_contact.py 2014-06-17 00:20:22 +0000
1495+++ tests/autopilot/address_book_app/tests/test_add_contact.py 2014-07-04 14:53:15 +0000
1496@@ -49,96 +49,6 @@
1497 list_view = self.app.main_window.get_contact_list_view()
1498 self.assertThat(list_view.count, Eventually(Equals(0)))
1499
1500- def test_add_contact_without_name(self):
1501- # execute add new contact
1502- contact_editor = self.app.main_window.go_to_add_contact()
1503-
1504- # Try to save a empty contact
1505- acceptButton = self.app.main_window.get_button("save")
1506-
1507- # Save button must be disabled
1508- self.assertThat(acceptButton.enabled, Eventually(Equals(False)))
1509-
1510- contact_editor.fill_form(data.Contact(first_name='Fulano'))
1511-
1512- # Save button must be enabled
1513- self.assertThat(acceptButton.enabled, Eventually(Equals(True)))
1514-
1515- contact_editor.fill_form(data.Contact(first_name=''))
1516-
1517- # Save button must be disabled
1518- self.assertThat(acceptButton.enabled, Eventually(Equals(False)))
1519-
1520- contact_editor.fill_form(data.Contact(last_name='de Tal'))
1521-
1522- # Save button must be enabled
1523- self.assertThat(acceptButton.enabled, Eventually(Equals(True)))
1524-
1525- # clear lastName field
1526- contact_editor.fill_form(data.Contact(last_name=''))
1527-
1528- # Save button must be disabled
1529- self.assertThat(acceptButton.enabled, Eventually(Equals(False)))
1530-
1531- # Clicking on disabled button should do nothing
1532- self.pointing_device.click_object(acceptButton)
1533-
1534- # Check if the contact editor still visbile
1535- list_page = self.app.main_window.get_contact_list_page()
1536-
1537- self.assertThat(list_page.visible, Eventually(Equals(False)))
1538- self.assertThat(contact_editor.visible, Eventually(Equals(True)))
1539-
1540- # Check if the contact list still empty
1541- list_view = self.app.main_window.get_contact_list_view()
1542- self.assertThat(list_view.count, Eventually(Equals(0)))
1543-
1544- def test_add_contact_with_full_name(self):
1545- test_contact = data.Contact(first_name='Fulano', last_name='de Tal')
1546-
1547- # execute add new contact
1548- contact_editor = self.app.main_window.go_to_add_contact()
1549- contact_editor.fill_form(test_contact)
1550-
1551- # Save contact
1552- self.app.main_window.save()
1553-
1554- # Check if the contact list is visible again
1555- list_page = self.app.main_window.get_contact_list_page()
1556- self.assertThat(list_page.visible, Eventually(Equals(True)))
1557-
1558- # Check if contact was added
1559- list_view = self.app.main_window.get_contact_list_view()
1560- self.assertThat(list_view.count, Eventually(Equals(1)))
1561-
1562- def test_add_contact_with_first_name(self):
1563- test_contact = data.Contact(first_name='Fulano')
1564-
1565- # execute add new contact
1566- contact_editor = self.app.main_window.go_to_add_contact()
1567- contact_editor.fill_form(test_contact)
1568-
1569- # Save contact
1570- self.app.main_window.save()
1571-
1572- # Check if contact was added
1573- list_view = self.app.main_window.get_contact_list_view()
1574- self.assertThat(list_view.count, Eventually(Equals(1)))
1575-
1576- def test_add_contact_with_last_name(self):
1577- test_contact = data.Contact(last_name='de Tal')
1578-
1579- # execute add new contact
1580- contact_editor = self.app.main_window.go_to_add_contact()
1581- contact_editor.fill_form(test_contact)
1582-
1583- # Save contact
1584- self.app.main_window.save()
1585-
1586- # Check if contact was added
1587- list_view = self.app.main_window.get_contact_list_view()
1588- self.assertThat(list_view.count, Eventually(Equals(1)))
1589-
1590 def test_add_contact_with_name_and_phone(self):
1591 test_contact = data.Contact(
1592 first_name='Fulano', last_name='de Tal',
1593
1594=== added directory 'tests/qml'
1595=== added file 'tests/qml/CMakeLists.txt'
1596--- tests/qml/CMakeLists.txt 1970-01-01 00:00:00 +0000
1597+++ tests/qml/CMakeLists.txt 2014-07-04 14:53:15 +0000
1598@@ -0,0 +1,22 @@
1599+find_program(QMLTESTRUNNER_BIN
1600+ NAMES qmltestrunner
1601+ PATHS /usr/lib/*/qt5/bin
1602+ NO_DEFAULT_PATH
1603+)
1604+
1605+find_program(XVFB_RUN_BIN
1606+ NAMES xvfb-run
1607+)
1608+
1609+if(QMLTESTRUNNER_BIN AND XVFB_RUN_BIN)
1610+ add_test(NAME qmltestrunner
1611+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
1612+ COMMAND ${XVFB_RUN_BIN} -a -s "-screen 0 1024x768x24" ${QMLTESTRUNNER_BIN} -import ${imports_BINARY_DIR} -input ${CMAKE_SOURCE_DIR}/tests/qml
1613+ )
1614+else()
1615+ if (NOT QMLTESTRUNNER_BIN)
1616+ message(WARNING "Qml tests disabled: qmltestrunner not found")
1617+ else()
1618+ message(WARNING "Qml tests disabled: xvfb-run not found")
1619+ endif()
1620+endif()
1621
1622=== added file 'tests/qml/tst_ContactEditor.qml'
1623--- tests/qml/tst_ContactEditor.qml 1970-01-01 00:00:00 +0000
1624+++ tests/qml/tst_ContactEditor.qml 2014-07-04 14:53:15 +0000
1625@@ -0,0 +1,143 @@
1626+/*
1627+ * Copyright (C) 2014 Canonical, Ltd.
1628+ *
1629+ * This program is free software; you can redistribute it and/or modify
1630+ * it under the terms of the GNU General Public License as published by
1631+ * the Free Software Foundation; version 3.
1632+ *
1633+ * This program is distributed in the hope that it will be useful,
1634+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1635+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1636+ * GNU General Public License for more details.
1637+ *
1638+ * You should have received a copy of the GNU General Public License
1639+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1640+ */
1641+
1642+import QtQuick 2.2
1643+import QtTest 1.0
1644+import Ubuntu.Components 0.1
1645+import Ubuntu.Test 0.1
1646+import Ubuntu.Contacts 0.1
1647+
1648+import '../../src/imports/ContactEdit'
1649+
1650+Item {
1651+
1652+ id: root
1653+
1654+ width: units.gu(40)
1655+ height: units.gu(80)
1656+
1657+ MainView {
1658+ id: mainView
1659+ anchors.fill: parent
1660+ useDeprecatedToolbar: false
1661+
1662+ ListModel {
1663+ // dummy data model.
1664+ id: dummyDataModel
1665+
1666+ signal contactsFetched
1667+ }
1668+
1669+ ContactEditor {
1670+ id: contactEditor
1671+ anchors.fill: parent
1672+ model: dummyDataModel
1673+ contact: createEmptyContact('')
1674+ }
1675+ }
1676+
1677+ function createEmptyContact(phoneNumber) {
1678+ var details = [
1679+ {detail: 'PhoneNumber', field: 'number', value: phoneNumber},
1680+ {detail: 'EmailAddress', field: 'emailAddress', value: ''},
1681+ {detail: 'OnlineAccount', field: 'accountUri', value: ''},
1682+ {detail: 'Address', field: 'street', value: ''},
1683+ {detail: 'Name', field: 'firstName', value: ''},
1684+ {detail: 'Organization', field: 'name', value: ''}
1685+ ];
1686+
1687+ var newContact = Qt.createQmlObject(
1688+ 'import QtContacts 5.0; Contact{ }', mainView);
1689+ var detailSourceTemplate = 'import QtContacts 5.0; %1{ %2: "%3" }';
1690+ for (var i=0; i < details.length; i++) {
1691+ var detailMetaData = details[i];
1692+ var template = detailSourceTemplate.arg(detailMetaData.detail).arg(
1693+ detailMetaData.field).arg(detailMetaData.value);
1694+ var newDetail = Qt.createQmlObject(template, mainView);
1695+ newContact.addDetail(newDetail);
1696+ }
1697+ return newContact;
1698+ }
1699+
1700+ UbuntuTestCase {
1701+ id: contactEditorTestCase
1702+ name: 'contactEditorTestCase'
1703+
1704+ when: windowShown
1705+
1706+ function init() {
1707+ waitForRendering(contactEditor);
1708+ var saveButton = findChild(root, 'save_header_button');
1709+ compare(saveButton.enabled, false);
1710+ }
1711+
1712+ function cleanup() {
1713+ var textFields = getRequiredTextFields().concat(
1714+ getOptionalTextFields());
1715+ textFields.forEach(clearTextField);
1716+ }
1717+
1718+ function getRequiredTextFields() {
1719+ return ['firstName', 'lastName', 'phoneNumber_0'];
1720+ }
1721+
1722+ function getOptionalTextFields() {
1723+ return [
1724+ 'emailAddress_0', 'imUri_0', 'streetAddress_0',
1725+ 'localityAddress_0', 'regionAddress_0', 'postcodeAddress_0',
1726+ 'countryAddress_0', 'orgName_0', 'orgRole_0', 'orgTitle_0'
1727+ ];
1728+ }
1729+
1730+ function clearTextField(value, index, array) {
1731+ var textField = findChild(root, value);
1732+ textField.text = '';
1733+ }
1734+
1735+ function test_fillRequiredFieldsMustEnableSaveButton_data() {
1736+ var textFields = getRequiredTextFields();
1737+ return objectNamesArrayToDataScenarios(textFields);
1738+ }
1739+
1740+ function objectNamesArrayToDataScenarios(objectNamesArray) {
1741+ var data = [];
1742+ for (var index = 0; index < objectNamesArray.length; index++) {
1743+ var objectName = objectNamesArray[index];
1744+ data.push({tag: objectName, objectName: objectName});
1745+ }
1746+ return data;
1747+ }
1748+
1749+ function test_fillRequiredFieldsMustEnableSaveButton(data) {
1750+ var textField = findChild(root, data.objectName);
1751+ textField.text = 'test'
1752+ var saveButton = findChild(root, 'save_header_button');
1753+ tryCompare(saveButton, 'enabled', true);
1754+ }
1755+
1756+ function test_fillOptionalFieldsMustNotEnableSaveButton_data() {
1757+ var textFields = getOptionalTextFields();
1758+ return objectNamesArrayToDataScenarios(textFields)
1759+ }
1760+
1761+ function test_fillOptionalFieldsMustNotEnableSaveButton(data) {
1762+ var textField = findChild(root, data.objectName);
1763+ textField.text = 'test'
1764+ var saveButton = findChild(root, 'save_header_button');
1765+ tryCompare(saveButton, 'enabled', false);
1766+ }
1767+ }
1768+}

Subscribers

People subscribed via source and target branches