Merge lp:~uriboni/messaging-app/stickers into lp:messaging-app

Proposed by Ugo Riboni
Status: Merged
Merged at revision: 491
Proposed branch: lp:~uriboni/messaging-app/stickers
Merge into: lp:messaging-app
Diff against target: 1937 lines (+1685/-33)
22 files modified
.bzrignore (+37/-0)
debian/control (+1/-0)
debian/messaging-app.install (+1/-0)
src/CMakeLists.txt (+3/-1)
src/messagingapplication.cpp (+17/-0)
src/qml/CMakeLists.txt (+1/-0)
src/qml/Messages.qml (+55/-2)
src/qml/Stickers/CMakeLists.txt (+4/-0)
src/qml/Stickers/HistoryButton.qml (+35/-0)
src/qml/Stickers/StickerDelegate.qml (+32/-0)
src/qml/Stickers/StickerPackDelegate.qml (+53/-0)
src/qml/Stickers/StickerPackModel.qml (+27/-0)
src/qml/Stickers/StickerPacksModel.qml (+25/-0)
src/qml/Stickers/StickersPicker.qml (+112/-0)
src/qml/assets/face-smile-big-symbolic-2.svg (+182/-0)
src/qml/assets/history.svg (+173/-0)
src/qml/assets/input-keyboard-symbolic.svg (+221/-0)
src/stickers-history-model.cpp (+307/-0)
src/stickers-history-model.h (+91/-0)
tests/qml/CMakeLists.txt (+42/-30)
tests/qml/tst_QmlTests.cpp (+78/-0)
tests/qml/tst_StickersHistoryModel.qml (+188/-0)
To merge this branch: bzr merge lp:~uriboni/messaging-app/stickers
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Needs Fixing
Ubuntu Phablet Team Pending
Review via email: mp+277446@code.launchpad.net

Commit message

Prototype implementation for stickers support.

Description of the change

Prototype implementation for stickers support.
At the moment only the sticker picker component is implemented (including the most frequently used stickers panel).
Actually sending the sticker chosen by the user is not implemented by this branch yet.

It is also unclear what stickers will be preinstalled, so at the moment the app reads stickers from the user data directory: ~/.local/share/com.ubuntu.messaging-app/MessagingApp/stickers

Each subdirectory is considered a "sticker pack" and the pack can contain any number of png, webm or gif files, preferrably with transparency.

Test sticke packs: http://people.canonical.com/~bfiller/stickers.tgz

To post a comment you must log in.
lp:~uriboni/messaging-app/stickers updated
475. By Ugo Riboni

Remove debug code

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

FAILED: Continuous integration, rev:475
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~uriboni/messaging-app/stickers/+merge/277446/+edit-commit-message

http://jenkins.qa.ubuntu.com/job/messaging-app-ci/700/
Executed test runs:
    FAILURE: http://jenkins.qa.ubuntu.com/job/generic-deb-autopilot-vivid-touch/5144/console
    FAILURE: http://jenkins.qa.ubuntu.com/job/messaging-app-vivid-i386-ci/213/console
    FAILURE: http://jenkins.qa.ubuntu.com/job/generic-mediumtests-builder-vivid-armhf/5158/console

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/messaging-app-ci/700/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~uriboni/messaging-app/stickers updated
476. By Ugo Riboni

Add stickers directory to cmake

477. By Ugo Riboni

The model is now a chronological list of the most recently used stickers

478. By Ugo Riboni

Add a limit property to the model to set the maximum size of the history

479. By Ugo Riboni

Fix .bzrignore to properly ignore test binaries, and remove mistakely added test binary

480. By Ugo Riboni

Add stickers qml dir to the list of installed files in debian

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~uriboni/messaging-app/stickers updated
481. By Ugo Riboni

Fix interactions between OSK, stickers picker and the rest of the UI

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~uriboni/messaging-app/stickers updated
482. By Ugo Riboni

Add missing debian dependecy

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2015-11-16 14:15:20 +0000
4@@ -0,0 +1,37 @@
5+*.qmlproject.user
6+CMakeCache.txt
7+CMakeFiles/
8+CMakeLists.txt.user
9+cmake_install.cmake
10+cmake_uninstall.cmake
11+Makefile
12+install_manifest.txt
13+CTestTestfile.cmake
14+
15+*.cbp
16+*.moc
17+moc_*.cpp
18+*_automoc.cpp
19+
20+Testing
21+RE:tests/qml/tst_\w+Tests$
22+po/*.gmo
23+po/src
24+
25+config.h
26+src/messaging-app
27+src/messaging-app.desktop*
28+
29+obj-*
30+debian/usr.bin.webbrowser-app
31+debian/files
32+debian/tmp/
33+debian/qtdeclarative5-ubuntu-ui-extras-browser-plugin/
34+debian/qtdeclarative5-ubuntu-web-plugin/
35+debian/qtdeclarative5-ubuntu-web-plugin-doc/
36+debian/messaging-app/
37+debian/messaging-app-autopilot/
38+debian/*.debhelper
39+debian/*.debhelper.log
40+debian/*.substvars
41+debian/stamp-*
42
43=== modified file 'debian/control'
44--- debian/control 2015-09-11 14:25:57 +0000
45+++ debian/control 2015-11-16 14:15:20 +0000
46@@ -23,6 +23,7 @@
47 qtdeclarative5-ubuntu-addressbook0.1,
48 qtdeclarative5-qtcontacts-plugin,
49 qml-module-qt-labs-settings,
50+ qtdeclarative5-folderlistmodel-plugin,
51 qtpim5-dev,
52 xvfb,
53 Standards-Version: 3.9.4
54
55=== modified file 'debian/messaging-app.install'
56--- debian/messaging-app.install 2014-08-11 22:22:59 +0000
57+++ debian/messaging-app.install 2015-11-16 14:15:20 +0000
58@@ -8,4 +8,5 @@
59 usr/share/messaging-app/3rd_party
60 usr/share/messaging-app/MMS/*.qml
61 usr/share/messaging-app/Dialogs/*.qml
62+usr/share/messaging-app/Stickers/*.qml
63 usr/bin/*messaging-app*
64
65=== modified file 'src/CMakeLists.txt'
66--- src/CMakeLists.txt 2015-02-24 13:33:31 +0000
67+++ src/CMakeLists.txt 2015-11-16 14:15:20 +0000
68@@ -2,17 +2,19 @@
69
70 set(messaging_app_HDRS
71 messagingapplication.h
72+ stickers-history-model.h
73 )
74
75 set(messaging_app_SRCS
76 messagingapplication.cpp
77 main.cpp
78+ stickers-history-model.cpp
79 )
80
81 add_executable(${MESSAGING_APP}
82 ${messaging_app_SRCS}
83 )
84-qt5_use_modules(${MESSAGING_APP} Core DBus Gui Qml Quick Versit)
85+qt5_use_modules(${MESSAGING_APP} Core DBus Gui Qml Quick Versit Sql)
86
87 include_directories(
88 ${CMAKE_CURRENT_BINARY_DIR}
89
90=== modified file 'src/messagingapplication.cpp'
91--- src/messagingapplication.cpp 2015-07-07 16:31:26 +0000
92+++ src/messagingapplication.cpp 2015-11-16 14:15:20 +0000
93@@ -17,6 +17,7 @@
94 */
95
96 #include "messagingapplication.h"
97+#include "stickers-history-model.h"
98
99 #include <libnotify/notify.h>
100
101@@ -24,6 +25,7 @@
102 #include <QUrl>
103 #include <QUrlQuery>
104 #include <QDebug>
105+#include <QDir>
106 #include <QStringList>
107 #include <QQuickItem>
108 #include <QQmlComponent>
109@@ -36,6 +38,7 @@
110 #include "config.h"
111 #include <QQmlEngine>
112 #include <QMimeDatabase>
113+#include <QStandardPaths>
114 #include <QVersitReader>
115
116 using namespace QtVersit;
117@@ -71,6 +74,13 @@
118 setApplicationName("MessagingApp");
119 }
120
121+static QObject* StickersHistoryModel_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
122+{
123+ Q_UNUSED(engine);
124+ Q_UNUSED(scriptEngine);
125+ return new StickersHistoryModel();
126+}
127+
128 bool MessagingApplication::setup()
129 {
130 installIconPath();
131@@ -149,6 +159,13 @@
132 m_view->rootContext()->setContextProperty("QTCONTACTS_MANAGER_OVERRIDE", contactsBackend);
133 }
134
135+ QDir dataLocation(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
136+ m_view->rootContext()->setContextProperty("dataLocation", dataLocation.absolutePath());
137+ dataLocation.mkpath("stickers");
138+ const char* uri = "messagingapp.private";
139+ qmlRegisterSingletonType<StickersHistoryModel>(uri, 0, 1, "StickersHistoryModel", StickersHistoryModel_singleton_factory);
140+
141+
142 // used by autopilot tests to load vcards during tests
143 QByteArray testData = qgetenv("QTCONTACTS_PRELOAD_VCARD");
144 m_view->rootContext()->setContextProperty("QTCONTACTS_PRELOAD_VCARD", testData);
145
146=== modified file 'src/qml/CMakeLists.txt'
147--- src/qml/CMakeLists.txt 2014-08-11 22:22:59 +0000
148+++ src/qml/CMakeLists.txt 2015-11-16 14:15:20 +0000
149@@ -17,3 +17,4 @@
150
151 add_subdirectory(MMS)
152 add_subdirectory(Dialogs)
153+add_subdirectory(Stickers)
154
155=== modified file 'src/qml/Messages.qml'
156--- src/qml/Messages.qml 2015-11-12 17:33:30 +0000
157+++ src/qml/Messages.qml 2015-11-16 14:15:20 +0000
158@@ -26,6 +26,7 @@
159 import Ubuntu.Telephony 0.1
160 import Ubuntu.Contacts 0.1
161
162+import "Stickers"
163 import "dateUtils.js" as DateUtils
164
165 Page {
166@@ -783,9 +784,26 @@
167 }
168 }
169
170+ StickersPicker {
171+ id: stickersPicker
172+ anchors.left: parent.left
173+ anchors.right: parent.right
174+ anchors.bottom: parent.bottom
175+ height: keyboard.maximumHeight
176+ visible: false
177+
178+ onStickerSelected: console.log(">>>>>>>>>>>> send:", path)
179+
180+ Connections {
181+ target: Qt.inputMethod
182+ onVisibleChanged: if (Qt.inputMethod.visible) stickersPicker.visible = false
183+ }
184+ }
185+
186 Item {
187 id: bottomPanel
188- anchors.bottom: isSearching ? parent.bottom : keyboard.top
189+ anchors.bottom: isSearching ? parent.bottom :
190+ (Qt.inputMethod.visible ? keyboard.top : stickersPicker.top)
191 anchors.left: parent.left
192 anchors.right: parent.right
193 height: selectionMode || (participants.length > 0 && !contactWatcher.interactive) ? 0 : textEntry.height + units.gu(2)
194@@ -826,6 +844,32 @@
195 }
196 }
197
198+ Icon {
199+ id: stickersButton
200+ objectName: "stickersButton"
201+ anchors.left: attachButton.right
202+ anchors.leftMargin: units.gu(2)
203+ anchors.verticalCenter: sendButton.verticalCenter
204+ height: units.gu(3)
205+ width: units.gu(3)
206+ color: "gray"
207+ visible: messageTextArea.activeFocus || stickersPicker.visible
208+ source: Qt.resolvedUrl("./assets/" + (Qt.inputMethod.visible ?
209+ "face-smile-big-symbolic-2.svg" :
210+ "input-keyboard-symbolic.svg"))
211+ MouseArea {
212+ anchors.fill: parent
213+ onClicked: {
214+ if (Qt.inputMethod.visible) {
215+ messageTextArea.focus = false
216+ stickersPicker.visible = true
217+ } else {
218+ messageTextArea.forceActiveFocus()
219+ }
220+ }
221+ }
222+ }
223+
224 StyledItem {
225 id: textEntry
226 property alias text: messageTextArea.text
227@@ -834,7 +878,7 @@
228 style: Theme.createStyleComponent("TextAreaStyle.qml", textEntry)
229 anchors.bottomMargin: units.gu(1)
230 anchors.bottom: parent.bottom
231- anchors.left: attachButton.right
232+ anchors.left: stickersButton.visible ? stickersButton.right : attachButton.right
233 anchors.leftMargin: units.gu(1)
234 anchors.right: sendButton.left
235 anchors.rightMargin: units.gu(1)
236@@ -1045,6 +1089,7 @@
237 font.pixelSize: FontUtils.sizeToPixels("medium")
238 color: "#5d5d5d"
239 text: messages.text
240+ onActiveFocusChanged: if (activeFocus) stickersPicker.visible = false
241 }
242
243 /*InverseMouseArea {
244@@ -1125,6 +1170,14 @@
245
246 KeyboardRectangle {
247 id: keyboard
248+
249+ // Workaround for not being able to know the keyboard rectangle size
250+ // before it has shown at least once: keep matching its height until it
251+ // has reached its maximum.
252+ property int maximumHeight: 0
253+ onHeightChanged: {
254+ if (height > maximumHeight) maximumHeight = height
255+ }
256 }
257
258 MessageInfoDialog {
259
260=== added directory 'src/qml/Stickers'
261=== added file 'src/qml/Stickers/CMakeLists.txt'
262--- src/qml/Stickers/CMakeLists.txt 1970-01-01 00:00:00 +0000
263+++ src/qml/Stickers/CMakeLists.txt 2015-11-16 14:15:20 +0000
264@@ -0,0 +1,4 @@
265+file(GLOB STICKERS_QML_FILES *.qml)
266+
267+add_custom_target(messaging_app_Stickers_QMlFiles ALL SOURCES ${STICKERS_QML_FILES})
268+install(FILES ${STICKERS_QML_FILES} DESTINATION ${MESSAGING_APP_DIR}/Stickers)
269
270=== added file 'src/qml/Stickers/HistoryButton.qml'
271--- src/qml/Stickers/HistoryButton.qml 1970-01-01 00:00:00 +0000
272+++ src/qml/Stickers/HistoryButton.qml 2015-11-16 14:15:20 +0000
273@@ -0,0 +1,35 @@
274+/*
275+ * Copyright 2015 Canonical Ltd.
276+ *
277+ * This file is part of messaging-app.
278+ *
279+ * messaging-app is free software; you can redistribute it and/or modify
280+ * it under the terms of the GNU General Public License as published by
281+ * the Free Software Foundation; version 3.
282+ *
283+ * messaging-app is distributed in the hope that it will be useful,
284+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
285+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
286+ * GNU General Public License for more details.
287+ *
288+ * You should have received a copy of the GNU General Public License
289+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
290+ */
291+
292+import QtQuick 2.3
293+import Ubuntu.Components 1.3
294+
295+AbstractButton {
296+ property bool selected
297+
298+ Rectangle {
299+ anchors.fill: parent
300+ color: selected ? "#f5f5f5" : "transparent"
301+ }
302+
303+ Icon {
304+ name: "history"
305+ anchors.fill: parent
306+ anchors.margins: units.gu(1.5)
307+ }
308+}
309
310=== added file 'src/qml/Stickers/StickerDelegate.qml'
311--- src/qml/Stickers/StickerDelegate.qml 1970-01-01 00:00:00 +0000
312+++ src/qml/Stickers/StickerDelegate.qml 2015-11-16 14:15:20 +0000
313@@ -0,0 +1,32 @@
314+/*
315+ * Copyright 2015 Canonical Ltd.
316+ *
317+ * This file is part of messaging-app.
318+ *
319+ * messaging-app is free software; you can redistribute it and/or modify
320+ * it under the terms of the GNU General Public License as published by
321+ * the Free Software Foundation; version 3.
322+ *
323+ * messaging-app is distributed in the hope that it will be useful,
324+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
325+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
326+ * GNU General Public License for more details.
327+ *
328+ * You should have received a copy of the GNU General Public License
329+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
330+ */
331+
332+import QtQuick 2.3
333+import Ubuntu.Components 1.3
334+
335+AbstractButton {
336+ property alias stickerSource: image.source
337+
338+ Image {
339+ id: image
340+ anchors.fill: parent
341+ anchors.margins: units.gu(0.5)
342+ fillMode: Image.PreserveAspectFit
343+ smooth: true
344+ }
345+}
346
347=== added file 'src/qml/Stickers/StickerPackDelegate.qml'
348--- src/qml/Stickers/StickerPackDelegate.qml 1970-01-01 00:00:00 +0000
349+++ src/qml/Stickers/StickerPackDelegate.qml 2015-11-16 14:15:20 +0000
350@@ -0,0 +1,53 @@
351+/*
352+ * Copyright 2015 Canonical Ltd.
353+ *
354+ * This file is part of messaging-app.
355+ *
356+ * messaging-app is free software; you can redistribute it and/or modify
357+ * it under the terms of the GNU General Public License as published by
358+ * the Free Software Foundation; version 3.
359+ *
360+ * messaging-app is distributed in the hope that it will be useful,
361+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
362+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
363+ * GNU General Public License for more details.
364+ *
365+ * You should have received a copy of the GNU General Public License
366+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
367+ */
368+
369+import QtQuick 2.3
370+import Qt.labs.folderlistmodel 2.1
371+import Ubuntu.Components 1.3
372+
373+AbstractButton {
374+ property alias path: stickers.folder
375+ property string name
376+ property bool selected
377+
378+ Rectangle {
379+ anchors.fill: parent
380+ color: selected ? "#f5f5f5" : "transparent"
381+ }
382+
383+ Icon {
384+ anchors.fill: parent
385+ visible: stickers.count === 0
386+ name: "cancel"
387+ }
388+
389+ Image {
390+ visible: stickers.count > 0
391+ anchors.fill: parent
392+ anchors.margins: units.gu(0.5)
393+ fillMode: Image.PreserveAspectFit
394+ smooth: true
395+ source: visible ? stickers.get(0, "filePath") : ""
396+ }
397+
398+ FolderListModel {
399+ id: stickers
400+ showDirs: false
401+ nameFilters: ["*.png", "*.webm", "*.gif"]
402+ }
403+}
404
405=== added file 'src/qml/Stickers/StickerPackModel.qml'
406--- src/qml/Stickers/StickerPackModel.qml 1970-01-01 00:00:00 +0000
407+++ src/qml/Stickers/StickerPackModel.qml 2015-11-16 14:15:20 +0000
408@@ -0,0 +1,27 @@
409+/*
410+ * Copyright 2015 Canonical Ltd.
411+ *
412+ * This file is part of messaging-app.
413+ *
414+ * messaging-app is free software; you can redistribute it and/or modify
415+ * it under the terms of the GNU General Public License as published by
416+ * the Free Software Foundation; version 3.
417+ *
418+ * messaging-app is distributed in the hope that it will be useful,
419+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
420+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
421+ * GNU General Public License for more details.
422+ *
423+ * You should have received a copy of the GNU General Public License
424+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
425+ */
426+
427+import QtQuick 2.3
428+import Qt.labs.folderlistmodel 2.1
429+
430+FolderListModel {
431+ property string packName
432+ folder: "%1/stickers/%2".arg(dataLocation).arg(packName)
433+ showDirs: false
434+ nameFilters: ["*.png", "*.webm", "*.gif"]
435+}
436
437=== added file 'src/qml/Stickers/StickerPacksModel.qml'
438--- src/qml/Stickers/StickerPacksModel.qml 1970-01-01 00:00:00 +0000
439+++ src/qml/Stickers/StickerPacksModel.qml 2015-11-16 14:15:20 +0000
440@@ -0,0 +1,25 @@
441+/*
442+ * Copyright 2015 Canonical Ltd.
443+ *
444+ * This file is part of messaging-app.
445+ *
446+ * messaging-app is free software; you can redistribute it and/or modify
447+ * it under the terms of the GNU General Public License as published by
448+ * the Free Software Foundation; version 3.
449+ *
450+ * messaging-app is distributed in the hope that it will be useful,
451+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
452+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
453+ * GNU General Public License for more details.
454+ *
455+ * You should have received a copy of the GNU General Public License
456+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
457+ */
458+
459+import QtQuick 2.3
460+import Qt.labs.folderlistmodel 2.1
461+
462+FolderListModel {
463+ folder: dataLocation + "/stickers/"
464+ showFiles: false
465+}
466
467=== added file 'src/qml/Stickers/StickersPicker.qml'
468--- src/qml/Stickers/StickersPicker.qml 1970-01-01 00:00:00 +0000
469+++ src/qml/Stickers/StickersPicker.qml 2015-11-16 14:15:20 +0000
470@@ -0,0 +1,112 @@
471+/*
472+ * Copyright 2015 Canonical Ltd.
473+ *
474+ * This file is part of messaging-app.
475+ *
476+ * messaging-app is free software; you can redistribute it and/or modify
477+ * it under the terms of the GNU General Public License as published by
478+ * the Free Software Foundation; version 3.
479+ *
480+ * messaging-app is distributed in the hope that it will be useful,
481+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
482+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
483+ * GNU General Public License for more details.
484+ *
485+ * You should have received a copy of the GNU General Public License
486+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
487+ */
488+
489+import QtQuick 2.3
490+import Ubuntu.Components 1.3
491+import messagingapp.private 0.1
492+
493+FocusScope {
494+ id: pickerRoot
495+ signal stickerSelected(string path)
496+
497+ Component.onCompleted: {
498+ StickersHistoryModel.databasePath = dataLocation + "/stickers/stickers.sqlite"
499+ StickersHistoryModel.limit = 10
500+ }
501+
502+ ListView {
503+ id: setsList
504+ model: StickerPacksModel {}
505+ orientation: ListView.Horizontal
506+ anchors.left: parent.left
507+ anchors.right: parent.right
508+ anchors.top: parent.top
509+ height: units.gu(6)
510+
511+ header: HistoryButton {
512+ height: units.gu(6)
513+ width: height
514+
515+ onTriggered: stickersGrid.model.packName = ""
516+ selected: stickersGrid.model.packName === ""
517+ }
518+ delegate: StickerPackDelegate {
519+ anchors.top: parent.top
520+ anchors.bottom: parent.bottom
521+ width: units.gu(6)
522+
523+ path: filePath
524+ onTriggered: stickersGrid.model.packName = fileName
525+ selected: stickersGrid.model.packName === fileName
526+ }
527+ }
528+
529+ Rectangle {
530+ anchors.fill: stickersGrid
531+ color: "#f5f5f5"
532+ }
533+
534+ GridView {
535+ id: stickersGrid
536+ anchors.left: parent.left
537+ anchors.right: parent.right
538+ anchors.top: setsList.bottom
539+ anchors.bottom: parent.bottom
540+ clip: true
541+ cellWidth: units.gu(10)
542+ cellHeight: units.gu(10)
543+ visible: stickersGrid.model.packName.length > 0
544+
545+ model: StickerPackModel { }
546+ delegate: StickerDelegate {
547+ stickerSource: filePath
548+ width: stickersGrid.cellWidth
549+ height: stickersGrid.cellHeight
550+
551+ onTriggered: {
552+ StickersHistoryModel.add("%1/%2".arg(stickersGrid.model.packName).arg(fileName))
553+ pickerRoot.stickerSelected(stickerSource)
554+ }
555+ }
556+ }
557+
558+ GridView {
559+ id: historyGrid
560+ anchors.left: parent.left
561+ anchors.right: parent.right
562+ anchors.top: setsList.bottom
563+ anchors.bottom: parent.bottom
564+ clip: true
565+ cellWidth: units.gu(10)
566+ cellHeight: units.gu(10)
567+ visible: stickersGrid.model.packName.length === 0
568+
569+ model: StickersHistoryModel
570+
571+ delegate: StickerDelegate {
572+ stickerSource: "%1/stickers/%2".arg(dataLocation).arg(sticker)
573+ width: stickersGrid.cellWidth
574+ height: stickersGrid.cellHeight
575+
576+ onTriggered: {
577+ StickersHistoryModel.add(sticker)
578+ pickerRoot.stickerSelected(stickerSource)
579+ }
580+ }
581+ }
582+}
583
584=== added file 'src/qml/assets/face-smile-big-symbolic-2.svg'
585--- src/qml/assets/face-smile-big-symbolic-2.svg 1970-01-01 00:00:00 +0000
586+++ src/qml/assets/face-smile-big-symbolic-2.svg 2015-11-16 14:15:20 +0000
587@@ -0,0 +1,182 @@
588+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
589+<!-- Created with Inkscape (http://www.inkscape.org/) -->
590+
591+<svg
592+ xmlns:dc="http://purl.org/dc/elements/1.1/"
593+ xmlns:cc="http://creativecommons.org/ns#"
594+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
595+ xmlns:svg="http://www.w3.org/2000/svg"
596+ xmlns="http://www.w3.org/2000/svg"
597+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
598+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
599+ width="96"
600+ height="96"
601+ id="svg4874"
602+ version="1.1"
603+ inkscape:version="0.91+devel r"
604+ viewBox="0 0 96 96.000001"
605+ sodipodi:docname="face-smile-big-symbolic.svg">
606+ <defs
607+ id="defs4876" />
608+ <sodipodi:namedview
609+ id="base"
610+ pagecolor="#ffffff"
611+ bordercolor="#666666"
612+ borderopacity="1.0"
613+ inkscape:pageopacity="0.0"
614+ inkscape:pageshadow="2"
615+ inkscape:zoom="2.8774396"
616+ inkscape:cx="-93.086241"
617+ inkscape:cy="47.64648"
618+ inkscape:document-units="px"
619+ inkscape:current-layer="g4780"
620+ showgrid="true"
621+ showborder="true"
622+ fit-margin-top="0"
623+ fit-margin-left="0"
624+ fit-margin-right="0"
625+ fit-margin-bottom="0"
626+ inkscape:snap-bbox="true"
627+ inkscape:bbox-paths="true"
628+ inkscape:bbox-nodes="true"
629+ inkscape:snap-bbox-edge-midpoints="true"
630+ inkscape:snap-bbox-midpoints="true"
631+ inkscape:object-paths="true"
632+ inkscape:snap-intersection-paths="true"
633+ inkscape:object-nodes="true"
634+ inkscape:snap-smooth-nodes="true"
635+ inkscape:snap-midpoints="true"
636+ inkscape:snap-object-midpoints="true"
637+ inkscape:snap-center="true"
638+ showguides="true"
639+ inkscape:guide-bbox="true">
640+ <inkscape:grid
641+ type="xygrid"
642+ id="grid5451"
643+ empspacing="8" />
644+ <sodipodi:guide
645+ orientation="1,0"
646+ position="8,-8.0000001"
647+ id="guide4063" />
648+ <sodipodi:guide
649+ orientation="1,0"
650+ position="4,-8.0000001"
651+ id="guide4065" />
652+ <sodipodi:guide
653+ orientation="0,1"
654+ position="-8,88.000001"
655+ id="guide4067" />
656+ <sodipodi:guide
657+ orientation="0,1"
658+ position="-8,92.000001"
659+ id="guide4069" />
660+ <sodipodi:guide
661+ orientation="0,1"
662+ position="104,4"
663+ id="guide4071" />
664+ <sodipodi:guide
665+ orientation="0,1"
666+ position="-5,8.0000001"
667+ id="guide4073" />
668+ <sodipodi:guide
669+ orientation="1,0"
670+ position="92,-8.0000001"
671+ id="guide4075" />
672+ <sodipodi:guide
673+ orientation="1,0"
674+ position="88,-8.0000001"
675+ id="guide4077" />
676+ <sodipodi:guide
677+ orientation="0,1"
678+ position="-8,84.000001"
679+ id="guide4074" />
680+ <sodipodi:guide
681+ orientation="1,0"
682+ position="12,-8.0000001"
683+ id="guide4076" />
684+ <sodipodi:guide
685+ orientation="0,1"
686+ position="-5,12"
687+ id="guide4078" />
688+ <sodipodi:guide
689+ orientation="1,0"
690+ position="84,-9.0000001"
691+ id="guide4080" />
692+ <sodipodi:guide
693+ position="48,-8.0000001"
694+ orientation="1,0"
695+ id="guide4170" />
696+ <sodipodi:guide
697+ position="-8,48"
698+ orientation="0,1"
699+ id="guide4172" />
700+ </sodipodi:namedview>
701+ <metadata
702+ id="metadata4879">
703+ <rdf:RDF>
704+ <cc:Work
705+ rdf:about="">
706+ <dc:format>image/svg+xml</dc:format>
707+ <dc:type
708+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
709+ <dc:title></dc:title>
710+ </cc:Work>
711+ </rdf:RDF>
712+ </metadata>
713+ <g
714+ inkscape:label="Layer 1"
715+ inkscape:groupmode="layer"
716+ id="layer1"
717+ transform="translate(67.857146,-78.50504)">
718+ <g
719+ transform="matrix(0,-1,-1,0,373.50506,516.50504)"
720+ id="g4845"
721+ style="display:inline">
722+ <g
723+ inkscape:export-ydpi="90"
724+ inkscape:export-xdpi="90"
725+ inkscape:export-filename="next01.png"
726+ transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
727+ id="g4778"
728+ inkscape:label="Layer 1">
729+ <g
730+ transform="matrix(-1,0,0,1,575.99999,611)"
731+ id="g4780"
732+ style="display:inline">
733+ <rect
734+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
735+ id="rect4782"
736+ width="96.037987"
737+ height="96"
738+ x="-438.00244"
739+ y="345.36221"
740+ transform="scale(-1,1)" />
741+ <path
742+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
743+ d="m 432,393.36133 c 0,23.17236 -18.83538,42 -42.01562,42 -23.18025,0 -42.01563,-18.82764 -42.01563,-42 0,-23.17236 18.83538,-42 42.01563,-42 23.18024,0 42.01562,18.82764 42.01562,42 z m -4.00195,0 c 0,-21.00932 -16.99476,-37.99805 -38.01367,-37.99805 -21.01892,0 -38.01563,16.98873 -38.01563,37.99805 0,21.00931 16.99671,37.99804 38.01563,37.99805 21.01891,0 38.01367,-16.98874 38.01367,-37.99805 z"
744+ id="path4116"
745+ inkscape:connector-curvature="0" />
746+ <path
747+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:64.01265717;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
748+ d="m 404.99002,409.36281 1.50057,-8.00059 -13.50534,0 0,8.00059 z"
749+ id="rect4175"
750+ inkscape:connector-curvature="0"
751+ sodipodi:nodetypes="ccccc" />
752+ <path
753+ sodipodi:nodetypes="ccccc"
754+ inkscape:connector-curvature="0"
755+ id="path4178"
756+ d="m 404.99002,385.36222 1.50057,-7.99941 -13.50534,0 0,7.99941 z"
757+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:64.01265717;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
758+ <path
759+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu, Normal';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#808080;fill-opacity:1;stroke:none;stroke-width:1.00019777px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
760+ d="m 383.98169,414.37529 c -1.5268,-0.21155 -2.98748,-0.52576 -4.37873,-0.94375 -2.81692,-0.88674 -5.23617,-2.19676 -7.25967,-3.92985 -2.0236,-1.77325 -3.59002,-3.99099 -4.70116,-6.65098 -1.11104,-2.70036 -1.66756,-5.86393 -1.66756,-9.49118 0,-3.62725 0.55652,-6.77166 1.66756,-9.43164 1.11114,-2.70027 2.67756,-4.91499 4.70116,-6.64799 2.0235,-1.77334 4.44275,-3.0835 7.25967,-3.92986 1.39125,-0.4379 2.85193,-0.76678 4.37873,-0.98841 l 0,42.01366 z"
761+ id="path4190"
762+ inkscape:connector-curvature="0"
763+ inkscape:transform-center-x="0.0068360001"
764+ inkscape:transform-center-y="-9.0000001" />
765+ </g>
766+ </g>
767+ </g>
768+ </g>
769+</svg>
770
771=== added file 'src/qml/assets/history.svg'
772--- src/qml/assets/history.svg 1970-01-01 00:00:00 +0000
773+++ src/qml/assets/history.svg 2015-11-16 14:15:20 +0000
774@@ -0,0 +1,173 @@
775+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
776+<!-- Created with Inkscape (http://www.inkscape.org/) -->
777+
778+<svg
779+ xmlns:dc="http://purl.org/dc/elements/1.1/"
780+ xmlns:cc="http://creativecommons.org/ns#"
781+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
782+ xmlns:svg="http://www.w3.org/2000/svg"
783+ xmlns="http://www.w3.org/2000/svg"
784+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
785+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
786+ width="96"
787+ height="96"
788+ id="svg4874"
789+ version="1.1"
790+ inkscape:version="0.91+devel r"
791+ viewBox="0 0 96 96.000001"
792+ sodipodi:docname="history.svg">
793+ <defs
794+ id="defs4876" />
795+ <sodipodi:namedview
796+ id="base"
797+ pagecolor="#ffffff"
798+ bordercolor="#666666"
799+ borderopacity="1.0"
800+ inkscape:pageopacity="0.0"
801+ inkscape:pageshadow="2"
802+ inkscape:zoom="8.7812488"
803+ inkscape:cx="12.412809"
804+ inkscape:cy="56.142342"
805+ inkscape:document-units="px"
806+ inkscape:current-layer="g4780"
807+ showgrid="true"
808+ showborder="true"
809+ fit-margin-top="0"
810+ fit-margin-left="0"
811+ fit-margin-right="0"
812+ fit-margin-bottom="0"
813+ inkscape:snap-bbox="true"
814+ inkscape:bbox-paths="true"
815+ inkscape:bbox-nodes="true"
816+ inkscape:snap-bbox-edge-midpoints="true"
817+ inkscape:snap-bbox-midpoints="true"
818+ inkscape:object-paths="true"
819+ inkscape:snap-intersection-paths="true"
820+ inkscape:object-nodes="true"
821+ inkscape:snap-smooth-nodes="true"
822+ inkscape:snap-midpoints="true"
823+ inkscape:snap-object-midpoints="true"
824+ inkscape:snap-center="true"
825+ showguides="true"
826+ inkscape:guide-bbox="true">
827+ <inkscape:grid
828+ type="xygrid"
829+ id="grid5451"
830+ empspacing="8" />
831+ <sodipodi:guide
832+ orientation="1,0"
833+ position="8,-8.0000001"
834+ id="guide4063" />
835+ <sodipodi:guide
836+ orientation="1,0"
837+ position="4,-8.0000001"
838+ id="guide4065" />
839+ <sodipodi:guide
840+ orientation="0,1"
841+ position="-8,88.000001"
842+ id="guide4067" />
843+ <sodipodi:guide
844+ orientation="0,1"
845+ position="-8,92.000001"
846+ id="guide4069" />
847+ <sodipodi:guide
848+ orientation="0,1"
849+ position="104,4"
850+ id="guide4071" />
851+ <sodipodi:guide
852+ orientation="0,1"
853+ position="-5,8.0000001"
854+ id="guide4073" />
855+ <sodipodi:guide
856+ orientation="1,0"
857+ position="92,-8.0000001"
858+ id="guide4075" />
859+ <sodipodi:guide
860+ orientation="1,0"
861+ position="88,-8.0000001"
862+ id="guide4077" />
863+ <sodipodi:guide
864+ orientation="0,1"
865+ position="-8,84.000001"
866+ id="guide4074" />
867+ <sodipodi:guide
868+ orientation="1,0"
869+ position="12,-8.0000001"
870+ id="guide4076" />
871+ <sodipodi:guide
872+ orientation="0,1"
873+ position="-5,12"
874+ id="guide4078" />
875+ <sodipodi:guide
876+ orientation="1,0"
877+ position="84,-9.0000001"
878+ id="guide4080" />
879+ <sodipodi:guide
880+ position="48,-8.0000001"
881+ orientation="1,0"
882+ id="guide4170" />
883+ <sodipodi:guide
884+ position="-8,48"
885+ orientation="0,1"
886+ id="guide4172" />
887+ </sodipodi:namedview>
888+ <metadata
889+ id="metadata4879">
890+ <rdf:RDF>
891+ <cc:Work
892+ rdf:about="">
893+ <dc:format>image/svg+xml</dc:format>
894+ <dc:type
895+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
896+ <dc:title></dc:title>
897+ </cc:Work>
898+ </rdf:RDF>
899+ </metadata>
900+ <g
901+ inkscape:label="Layer 1"
902+ inkscape:groupmode="layer"
903+ id="layer1"
904+ transform="translate(67.857146,-78.50504)">
905+ <g
906+ transform="matrix(0,-1,-1,0,373.50506,516.50504)"
907+ id="g4845"
908+ style="display:inline">
909+ <g
910+ inkscape:export-ydpi="90"
911+ inkscape:export-xdpi="90"
912+ inkscape:export-filename="next01.png"
913+ transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
914+ id="g4778"
915+ inkscape:label="Layer 1">
916+ <g
917+ transform="matrix(-1,0,0,1,575.99999,611)"
918+ id="g4780"
919+ style="display:inline">
920+ <rect
921+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
922+ id="rect4782"
923+ width="96.037987"
924+ height="96"
925+ x="-438.00244"
926+ y="345.36221"
927+ transform="scale(-1,1)" />
928+ <path
929+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
930+ d="m 420.87891,421.82617 c -13.02519,14.12639 -34.00962,17.61704 -50.90821,8.4668 -16.89858,-9.15024 -25.43667,-28.62704 -20.71484,-47.25 4.72183,-18.62296 21.51014,-31.68164 40.72852,-31.68164 l 0,4.00195 c -17.40844,0 -32.5749,11.79684 -36.85157,28.66406 -4.27667,16.86723 3.43743,34.45783 18.74414,42.7461 15.30671,8.28827 34.26279,5.13698 46.06055,-7.65821 l 2.94141,2.71094 z"
931+ id="path4116"
932+ inkscape:connector-curvature="0" />
933+ <path
934+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:8.00158199, 8.00158199;stroke-dashoffset:1.60031641;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
935+ d="m 431.99414,394.00977 -0.0137,0.42968 -3.99805,-0.13281 0.0137,-0.42187 0,-0.008 -0.006,-1.35937 0,-0.0254 -0.0527,-1.31446 -0.002,-0.0254 -0.10352,-1.36328 -0.002,-0.01 -0.16016,-1.41016 -0.20312,-1.34961 -0.004,-0.0215 -0.041,-0.21875 3.93164,-0.74414 0.0449,0.24023 0.0117,0.0723 0.2207,1.45703 0.01,0.0723 0.16602,1.45899 0.008,0.0742 0.11133,1.46093 0.006,0.0723 0.0586,1.46094 0,0.0723 0.006,1.45899 z m -1.11914,9.00195 -0.0156,0.0703 -0.36133,1.4082 -0.0195,0.0684 -0.41211,1.39453 -0.0215,0.0703 -0.46289,1.3789 -0.0234,0.0703 -0.51367,1.36328 -0.0273,0.0684 -0.5625,1.3457 -0.0293,0.0664 -0.17969,0.38867 -3.63281,-1.67774 0.15039,-0.32226 0.0137,-0.0312 0.54493,-1.30274 0.004,-0.0117 0.48242,-1.28516 0.004,-0.0117 0.42773,-1.27734 0.40039,-1.35938 0.3418,-1.33789 0.0469,-0.2168 3.9082,0.85352 z m -5.52344,-23.56055 -0.25,-0.63476 -0.0117,-0.0312 -0.54492,-1.24805 -0.0156,-0.0312 -0.60352,-1.25586 -0.0137,-0.0254 -0.68359,-1.29297 -0.002,-0.004 -0.6914,-1.19531 -0.0176,-0.0293 -0.64453,-1.02343 3.38672,-2.12891 0.68359,1.08789 0.0391,0.0625 0.74414,1.28906 0.0371,0.0664 0.69922,1.32227 0.0332,0.0664 0.6543,1.35547 0.0312,0.0684 0.60547,1.38477 0.0273,0.0703 0.26172,0.66601 z m -1.52929,38.8086 -0.0449,0.0605 -0.90625,1.18164 -0.0469,0.0566 -0.95313,1.15235 -0.0488,0.0566 -1.00196,1.12304 -0.0527,0.0547 -1.04883,1.08789 -2.88086,-2.77539 1.02344,-1.06054 0.92773,-1.03711 0.0234,-0.0274 0.88477,-1.0664 0.0215,-0.0274 0.86719,-1.13086 0.0176,-0.0234 0.1875,-0.26563 3.26171,2.31445 z m -6.66993,-51.46875 -0.68554,-0.68946 -0.0215,-0.0215 -0.99219,-0.92774 -0.0195,-0.0176 -0.98828,-0.85938 -0.0117,-0.01 -1.11132,-0.89648 -0.008,-0.008 -1.04687,-0.78516 -0.0176,-0.0117 -0.94922,-0.66015 2.28125,-3.28711 0.9668,0.67187 0.0586,0.043 1.16406,0.87109 0.0566,0.0449 1.13086,0.91406 0.0566,0.0469 1.09766,0.95508 0.0547,0.0488 1.06445,0.99609 0.0527,0.0508 0.70507,0.71093 z m -12.49609,-8.47852 -0.86523,-0.35547 -0.0176,-0.006 -1.32813,-0.49023 -0.0254,-0.01 -1.29101,-0.41992 -1.30469,-0.375 -0.0156,-0.004 -1.39843,-0.3457 -0.0156,-0.004 -1.02734,-0.21289 0.81445,-3.91601 1.10156,0.22851 0.0703,0.0156 1.43164,0.35351 0.0703,0.0195 1.41211,0.40429 0.0703,0.0215 1.39258,0.45508 0.0703,0.0254 1.3711,0.5039 0.0684,0.0274 0.9336,0.38281 z"
936+ id="ellipse4178"
937+ inkscape:connector-curvature="0" />
938+ <path
939+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#808080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.00079107;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.5999999;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
940+ d="m 387.98242,367.36133 0,26.83008 20.5957,20.58593 2.82813,-2.83007 -19.42187,-19.41407 0,-25.17187 -4.00196,0 z"
941+ id="path4180"
942+ inkscape:connector-curvature="0" />
943+ </g>
944+ </g>
945+ </g>
946+ </g>
947+</svg>
948
949=== added file 'src/qml/assets/input-keyboard-symbolic.svg'
950--- src/qml/assets/input-keyboard-symbolic.svg 1970-01-01 00:00:00 +0000
951+++ src/qml/assets/input-keyboard-symbolic.svg 2015-11-16 14:15:20 +0000
952@@ -0,0 +1,221 @@
953+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
954+<!-- Created with Inkscape (http://www.inkscape.org/) -->
955+
956+<svg
957+ xmlns:dc="http://purl.org/dc/elements/1.1/"
958+ xmlns:cc="http://creativecommons.org/ns#"
959+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
960+ xmlns:svg="http://www.w3.org/2000/svg"
961+ xmlns="http://www.w3.org/2000/svg"
962+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
963+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
964+ width="96"
965+ height="96"
966+ id="svg4874"
967+ version="1.1"
968+ inkscape:version="0.91+devel r"
969+ viewBox="0 0 96 96.000001"
970+ sodipodi:docname="input-keyboard-symbolic.svg">
971+ <defs
972+ id="defs4876" />
973+ <sodipodi:namedview
974+ id="base"
975+ pagecolor="#ffffff"
976+ bordercolor="#666666"
977+ borderopacity="1.0"
978+ inkscape:pageopacity="0.0"
979+ inkscape:pageshadow="2"
980+ inkscape:zoom="7.0249992"
981+ inkscape:cx="12.362986"
982+ inkscape:cy="54.10675"
983+ inkscape:document-units="px"
984+ inkscape:current-layer="g4780"
985+ showgrid="true"
986+ showborder="true"
987+ fit-margin-top="0"
988+ fit-margin-left="0"
989+ fit-margin-right="0"
990+ fit-margin-bottom="0"
991+ inkscape:snap-bbox="true"
992+ inkscape:bbox-paths="true"
993+ inkscape:bbox-nodes="true"
994+ inkscape:snap-bbox-edge-midpoints="true"
995+ inkscape:snap-bbox-midpoints="true"
996+ inkscape:object-paths="true"
997+ inkscape:snap-intersection-paths="true"
998+ inkscape:object-nodes="true"
999+ inkscape:snap-smooth-nodes="true"
1000+ inkscape:snap-midpoints="true"
1001+ inkscape:snap-object-midpoints="true"
1002+ inkscape:snap-center="true"
1003+ showguides="true"
1004+ inkscape:guide-bbox="true"
1005+ inkscape:snap-global="true">
1006+ <inkscape:grid
1007+ type="xygrid"
1008+ id="grid5451"
1009+ empspacing="8" />
1010+ <sodipodi:guide
1011+ orientation="1,0"
1012+ position="8,-8.0000001"
1013+ id="guide4063" />
1014+ <sodipodi:guide
1015+ orientation="1,0"
1016+ position="4,-8.0000001"
1017+ id="guide4065" />
1018+ <sodipodi:guide
1019+ orientation="0,1"
1020+ position="-8,88.000001"
1021+ id="guide4067" />
1022+ <sodipodi:guide
1023+ orientation="0,1"
1024+ position="-8,92.000001"
1025+ id="guide4069" />
1026+ <sodipodi:guide
1027+ orientation="0,1"
1028+ position="104,4"
1029+ id="guide4071" />
1030+ <sodipodi:guide
1031+ orientation="0,1"
1032+ position="-5,8.0000001"
1033+ id="guide4073" />
1034+ <sodipodi:guide
1035+ orientation="1,0"
1036+ position="88,-8.0000001"
1037+ id="guide4077" />
1038+ <sodipodi:guide
1039+ orientation="0,1"
1040+ position="-8,84.000001"
1041+ id="guide4074" />
1042+ <sodipodi:guide
1043+ orientation="1,0"
1044+ position="12,-8.0000001"
1045+ id="guide4076" />
1046+ <sodipodi:guide
1047+ orientation="1,0"
1048+ position="84,-8.0000001"
1049+ id="guide4080" />
1050+ <sodipodi:guide
1051+ position="48,-8.0000001"
1052+ orientation="1,0"
1053+ id="guide4170" />
1054+ <sodipodi:guide
1055+ position="-8,48"
1056+ orientation="0,1"
1057+ id="guide4172" />
1058+ <sodipodi:guide
1059+ position="92,-8.0000001"
1060+ orientation="1,0"
1061+ id="guide4760" />
1062+ </sodipodi:namedview>
1063+ <metadata
1064+ id="metadata4879">
1065+ <rdf:RDF>
1066+ <cc:Work
1067+ rdf:about="">
1068+ <dc:format>image/svg+xml</dc:format>
1069+ <dc:type
1070+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
1071+ <dc:title></dc:title>
1072+ </cc:Work>
1073+ </rdf:RDF>
1074+ </metadata>
1075+ <g
1076+ inkscape:label="Layer 1"
1077+ inkscape:groupmode="layer"
1078+ id="layer1"
1079+ transform="translate(67.857146,-78.50504)">
1080+ <g
1081+ transform="matrix(0,-1,-1,0,373.50506,516.50504)"
1082+ id="g4845"
1083+ style="display:inline">
1084+ <g
1085+ inkscape:export-ydpi="90"
1086+ inkscape:export-xdpi="90"
1087+ inkscape:export-filename="next01.png"
1088+ transform="matrix(-0.9996045,0,0,1,575.94296,-611.00001)"
1089+ id="g4778"
1090+ inkscape:label="Layer 1">
1091+ <g
1092+ transform="matrix(-1,0,0,1,575.99999,611)"
1093+ id="g4780"
1094+ style="display:inline">
1095+ <rect
1096+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:4;marker:none;enable-background:accumulate"
1097+ id="rect4782"
1098+ width="96.037987"
1099+ height="96"
1100+ x="-438.00244"
1101+ y="345.36221"
1102+ transform="scale(-1,1)" />
1103+ <path
1104+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
1105+ d="m 407.99059,425.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1106+ id="path4321"
1107+ inkscape:connector-curvature="0" />
1108+ <path
1109+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
1110+ d="m 379.97952,411.36222 0,-36 -8.00318,0 0,36 8.00318,0 z"
1111+ id="path4301"
1112+ inkscape:connector-curvature="0" />
1113+ <path
1114+ sodipodi:nodetypes="ccccccccc"
1115+ inkscape:connector-curvature="0"
1116+ id="path4297"
1117+ d="m 371.97588,349.36261 c -10.00396,0 -9.8873,3.91306 -10.00396,14 l 0,59.99961 c 0.11666,10.08694 0,14 10.00396,14 l 36.01466,0 c 10.00396,0 9.8873,-3.91306 10.00396,-14 l 0,-59.99961 c -0.11666,-10.08694 0,-14 -10.00396,-14 z"
1118+ style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#808080;stroke-width:4.00079155;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
1119+ <path
1120+ inkscape:connector-curvature="0"
1121+ id="path4165"
1122+ d="m 407.99059,411.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1123+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
1124+ <path
1125+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
1126+ d="m 407.99059,397.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1127+ id="path4167"
1128+ inkscape:connector-curvature="0" />
1129+ <path
1130+ inkscape:connector-curvature="0"
1131+ id="path4169"
1132+ d="m 407.99059,383.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1133+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
1134+ <path
1135+ inkscape:connector-curvature="0"
1136+ id="path4171"
1137+ d="m 393.98503,418.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1138+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
1139+ <path
1140+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
1141+ d="m 393.98503,404.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1142+ id="path4173"
1143+ inkscape:connector-curvature="0" />
1144+ <path
1145+ inkscape:connector-curvature="0"
1146+ id="path4175"
1147+ d="m 393.98503,390.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1148+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
1149+ <path
1150+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
1151+ d="m 393.98503,376.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1152+ id="path4177"
1153+ inkscape:connector-curvature="0" />
1154+ <path
1155+ inkscape:connector-curvature="0"
1156+ id="path4179"
1157+ d="m 379.97951,425.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1158+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
1159+ <path
1160+ inkscape:connector-curvature="0"
1161+ id="path4181"
1162+ d="m 379.97947,369.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1163+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none" />
1164+ <path
1165+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:15px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;display:inline;fill:#808080;fill-opacity:1;stroke:none"
1166+ d="m 407.99059,369.36222 0,-8.00001 -8.00317,0 0,8.00001 8.00317,0 z"
1167+ id="path4185"
1168+ inkscape:connector-curvature="0" />
1169+ </g>
1170+ </g>
1171+ </g>
1172+ </g>
1173+</svg>
1174
1175=== added file 'src/stickers-history-model.cpp'
1176--- src/stickers-history-model.cpp 1970-01-01 00:00:00 +0000
1177+++ src/stickers-history-model.cpp 2015-11-16 14:15:20 +0000
1178@@ -0,0 +1,307 @@
1179+/*
1180+ * Copyright 2015 Canonical Ltd.
1181+ *
1182+ * This file is part of messaging-app.
1183+ *
1184+ * messaging-app is free software; you can redistribute it and/or modify
1185+ * it under the terms of the GNU General Public License as published by
1186+ * the Free Software Foundation; version 3.
1187+ *
1188+ * messaging-app is distributed in the hope that it will be useful,
1189+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1190+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1191+ * GNU General Public License for more details.
1192+ *
1193+ * You should have received a copy of the GNU General Public License
1194+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1195+ */
1196+
1197+#include "stickers-history-model.h"
1198+
1199+// Qt
1200+#include <QtCore/QDebug>
1201+#include <QtCore/QMutexLocker>
1202+#include <QtSql/QSqlQuery>
1203+#include <QtSql/QSqlError>
1204+
1205+#define CONNECTION_NAME "messaging-app-stickers-history"
1206+
1207+/*!
1208+ \class StickersHistoryModel
1209+ \brief List model that stores information about most recently used stickers
1210+
1211+ StickersHistoryModel is a list model that stores information about the most
1212+ recently used stickers.
1213+ Each sticker is simply identified by the sticker pack name plus the name of
1214+ the sticker image file itself.
1215+ Stickers are ordered by the time of last use, with the most recent first.
1216+ By default the model stores a rolling list of the 10 most recently used
1217+ stickers, though this number can be changed by setting the /a limit
1218+*/
1219+StickersHistoryModel::StickersHistoryModel(QObject* parent)
1220+ : QAbstractListModel(parent),
1221+ m_limit(10)
1222+{
1223+ m_database = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), CONNECTION_NAME);
1224+}
1225+
1226+StickersHistoryModel::~StickersHistoryModel()
1227+{
1228+ m_database.close();
1229+ m_database = QSqlDatabase();
1230+ QSqlDatabase::removeDatabase(CONNECTION_NAME);
1231+}
1232+
1233+void StickersHistoryModel::resetDatabase(const QString& databaseName)
1234+{
1235+ beginResetModel();
1236+ m_entries.clear();
1237+ m_database.close();
1238+ m_database.setDatabaseName(databaseName);
1239+ m_database.open();
1240+ createOrAlterDatabaseSchema();
1241+ endResetModel();
1242+ populateFromDatabase();
1243+}
1244+
1245+void StickersHistoryModel::createOrAlterDatabaseSchema()
1246+{
1247+ QMutexLocker ml(&m_dbMutex);
1248+ QSqlQuery query(m_database);
1249+ QString statement = QLatin1String("CREATE TABLE IF NOT EXISTS history "
1250+ "(sticker VARCHAR, mostRecentUse DATETIME);");
1251+ query.prepare(statement);
1252+ if (!query.exec()) {
1253+ qWarning() << "Query failed" << query.lastError();
1254+ }
1255+}
1256+
1257+void StickersHistoryModel::populateFromDatabase()
1258+{
1259+ QSqlQuery query(m_database);
1260+ QString statement = QLatin1String("SELECT sticker, mostRecentUse "
1261+ "FROM history ORDER BY mostRecentUse DESC;");
1262+ query.prepare(statement);
1263+ if (!query.exec()) {
1264+ qWarning() << "Query failed" << query.lastError();
1265+ }
1266+
1267+ int count = 0;
1268+ while (query.next()) {
1269+ HistoryEntry entry;
1270+ entry.sticker = query.value(0).toString();
1271+ entry.mostRecentUse = QDateTime::fromTime_t(query.value(1).toInt());
1272+ beginInsertRows(QModelIndex(), count, count);
1273+ m_entries.append(entry);
1274+ endInsertRows();
1275+ ++count;
1276+ }
1277+}
1278+
1279+QHash<int, QByteArray> StickersHistoryModel::roleNames() const
1280+{
1281+ static QHash<int, QByteArray> roles;
1282+ if (roles.isEmpty()) {
1283+ roles[Sticker] = "sticker";
1284+ roles[MostRecentUse] = "mostRecentUse";
1285+ }
1286+ return roles;
1287+}
1288+
1289+int StickersHistoryModel::rowCount(const QModelIndex& parent) const
1290+{
1291+ Q_UNUSED(parent);
1292+ return m_entries.count();
1293+}
1294+
1295+QVariant StickersHistoryModel::data(const QModelIndex& index, int role) const
1296+{
1297+ if (!index.isValid()) {
1298+ return QVariant();
1299+ }
1300+ const HistoryEntry& entry = m_entries.at(index.row());
1301+ switch (role) {
1302+ case Sticker:
1303+ return entry.sticker;
1304+ case MostRecentUse:
1305+ return entry.mostRecentUse;
1306+ default:
1307+ return QVariant();
1308+ }
1309+}
1310+
1311+const QString StickersHistoryModel::databasePath() const
1312+{
1313+ return m_database.databaseName();
1314+}
1315+
1316+int StickersHistoryModel::limit() const
1317+{
1318+ return m_limit;
1319+}
1320+
1321+void StickersHistoryModel::setLimit(int limit)
1322+{
1323+ if (limit != m_limit) {
1324+ m_limit = limit;
1325+ Q_EMIT limitChanged();
1326+ removeExcessRows();
1327+ }
1328+}
1329+
1330+void StickersHistoryModel::setDatabasePath(const QString& path)
1331+{
1332+ if (path != databasePath()) {
1333+ if (path.isEmpty()) {
1334+ resetDatabase(":memory:");
1335+ } else {
1336+ resetDatabase(path);
1337+ }
1338+ Q_EMIT databasePathChanged();
1339+ }
1340+}
1341+
1342+int StickersHistoryModel::getEntryIndex(const QString& sticker) const
1343+{
1344+ for (int i = 0; i < m_entries.count(); ++i) {
1345+ if (m_entries.at(i).sticker == sticker) {
1346+ return i;
1347+ }
1348+ }
1349+ return -1;
1350+}
1351+
1352+/*!
1353+ Add an entry to the model.
1354+
1355+ If an entry for the same sticker already exists, it is updated and placed
1356+ first in the model. Otherwise a new entry is created and added at the
1357+ begining of the model.
1358+ If the new row count exceeds the limit, excess rows are purged.
1359+*/
1360+void StickersHistoryModel::add(const QString& sticker)
1361+{
1362+ if (sticker.isEmpty()) {
1363+ return;
1364+ }
1365+ QDateTime now = QDateTime::currentDateTime();
1366+ int index = getEntryIndex(sticker);
1367+
1368+ if (index == -1) {
1369+ HistoryEntry entry;
1370+ entry.sticker = sticker;
1371+ entry.mostRecentUse = now;
1372+ beginInsertRows(QModelIndex(), 0, 0);
1373+ m_entries.prepend(entry);
1374+ endInsertRows();
1375+ insertNewEntryInDatabase(entry);
1376+ Q_EMIT rowCountChanged();
1377+ removeExcessRows();
1378+ } else {
1379+ HistoryEntry entry;
1380+ if (index > 0) {
1381+ beginMoveRows(QModelIndex(), index, index, QModelIndex(), 0);
1382+ }
1383+ entry = m_entries.takeAt(index);
1384+ entry.mostRecentUse = now;
1385+ m_entries.prepend(entry);
1386+ if (index > 0) {
1387+ endMoveRows();
1388+ }
1389+ QVector<int> roles;
1390+ roles << MostRecentUse;
1391+ Q_EMIT dataChanged(this->index(0), this->index(0), roles);
1392+ updateExistingEntryInDatabase(m_entries.first());
1393+ }
1394+}
1395+
1396+void StickersHistoryModel::removeExcessRows()
1397+{
1398+ if (m_limit < rowCount()) {
1399+ beginRemoveRows(QModelIndex(), m_limit, rowCount() - 1);
1400+ for (int i = rowCount() - 1; i >= m_limit; i--) {
1401+ HistoryEntry item = m_entries.takeAt(i);
1402+ removeEntryFromDatabase(item.sticker);
1403+ }
1404+ endRemoveRows();
1405+ Q_EMIT rowCountChanged();
1406+ }
1407+}
1408+
1409+void StickersHistoryModel::insertNewEntryInDatabase(const HistoryEntry& entry)
1410+{
1411+ QMutexLocker ml(&m_dbMutex);
1412+ QSqlQuery query(m_database);
1413+ static QString statement = QLatin1String("INSERT INTO history (sticker, mostRecentUse) "
1414+ "VALUES (?, ?);");
1415+ query.prepare(statement);
1416+ query.addBindValue(entry.sticker);
1417+ query.addBindValue(entry.mostRecentUse.toTime_t());
1418+
1419+ if (!query.exec()) {
1420+ qWarning() << "Query failed" << query.lastError();
1421+ }
1422+}
1423+
1424+void StickersHistoryModel::updateExistingEntryInDatabase(const HistoryEntry& entry)
1425+{
1426+ QMutexLocker ml(&m_dbMutex);
1427+ QSqlQuery query(m_database);
1428+ static QString statement = QLatin1String("UPDATE history SET mostRecentUse=?"
1429+ " WHERE sticker=?;");
1430+ query.prepare(statement);
1431+ query.addBindValue(entry.mostRecentUse.toTime_t());
1432+ query.addBindValue(entry.sticker);
1433+ if (!query.exec()) {
1434+ qWarning() << "Query failed" << query.lastError();
1435+ }
1436+}
1437+
1438+void StickersHistoryModel::removeEntryFromDatabase(const QString& sticker)
1439+{
1440+ QMutexLocker ml(&m_dbMutex);
1441+ QSqlQuery query(m_database);
1442+ static QString statement = QLatin1String("DELETE FROM history WHERE sticker=?;");
1443+ query.prepare(statement);
1444+ query.addBindValue(sticker);
1445+ if (!query.exec()) {
1446+ qWarning() << "Query failed" << query.lastError();
1447+ }
1448+}
1449+
1450+void StickersHistoryModel::clearAll()
1451+{
1452+ if (!m_entries.isEmpty()) {
1453+ beginResetModel();
1454+ m_entries.clear();
1455+ endResetModel();
1456+ clearDatabase();
1457+ Q_EMIT rowCountChanged();
1458+ }
1459+}
1460+
1461+void StickersHistoryModel::clearDatabase()
1462+{
1463+ QMutexLocker ml(&m_dbMutex);
1464+ QSqlQuery query(m_database);
1465+ QString statement = QLatin1String("DELETE FROM history;");
1466+ query.prepare(statement);
1467+ if (!query.exec()) {
1468+ qWarning() << "Query failed" << query.lastError();
1469+ }
1470+}
1471+
1472+QVariantMap StickersHistoryModel::get(int i) const
1473+{
1474+ QVariantMap item;
1475+ QHash<int, QByteArray> roles = roleNames();
1476+
1477+ QModelIndex modelIndex = index(i, 0);
1478+ if (modelIndex.isValid()) {
1479+ Q_FOREACH(int role, roles.keys()) {
1480+ QString roleName = QString::fromUtf8(roles.value(role));
1481+ item.insert(roleName, data(modelIndex, role));
1482+ }
1483+ }
1484+ return item;
1485+}
1486
1487=== added file 'src/stickers-history-model.h'
1488--- src/stickers-history-model.h 1970-01-01 00:00:00 +0000
1489+++ src/stickers-history-model.h 2015-11-16 14:15:20 +0000
1490@@ -0,0 +1,91 @@
1491+/*
1492+ * Copyright 2015 Canonical Ltd.
1493+ *
1494+ * This file is part of messaging-app.
1495+ *
1496+ * messaging-app is free software; you can redistribute it and/or modify
1497+ * it under the terms of the GNU General Public License as published by
1498+ * the Free Software Foundation; version 3.
1499+ *
1500+ * messaging-app is distributed in the hope that it will be useful,
1501+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1502+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1503+ * GNU General Public License for more details.
1504+ *
1505+ * You should have received a copy of the GNU General Public License
1506+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1507+ */
1508+
1509+#ifndef __STICKERS_HISTORY_MODEL_H__
1510+#define __STICKERS_HISTORY_MODEL_H__
1511+
1512+// Qt
1513+#include <QtCore/QAbstractListModel>
1514+#include <QtCore/QDateTime>
1515+#include <QtCore/QList>
1516+#include <QtCore/QMutex>
1517+#include <QtCore/QString>
1518+#include <QtSql/QSqlDatabase>
1519+
1520+class StickersHistoryModel : public QAbstractListModel
1521+{
1522+ Q_OBJECT
1523+
1524+ Q_PROPERTY(QString databasePath READ databasePath WRITE setDatabasePath NOTIFY databasePathChanged)
1525+ Q_PROPERTY(int limit READ limit WRITE setLimit NOTIFY limitChanged)
1526+ Q_PROPERTY(int count READ rowCount NOTIFY rowCountChanged)
1527+
1528+ Q_ENUMS(Roles)
1529+
1530+public:
1531+ StickersHistoryModel(QObject* parent=0);
1532+ ~StickersHistoryModel();
1533+
1534+ enum Roles {
1535+ Sticker = Qt::UserRole + 1,
1536+ MostRecentUse
1537+ };
1538+
1539+ // reimplemented from QAbstractListModel
1540+ QHash<int, QByteArray> roleNames() const;
1541+ int rowCount(const QModelIndex& parent=QModelIndex()) const;
1542+ QVariant data(const QModelIndex& index, int role) const;
1543+
1544+ const QString databasePath() const;
1545+ void setDatabasePath(const QString& path);
1546+ int limit() const;
1547+ void setLimit(int limit);
1548+
1549+ Q_INVOKABLE void add(const QString& sticker);
1550+ Q_INVOKABLE void clearAll();
1551+ Q_INVOKABLE QVariantMap get(int index) const;
1552+
1553+Q_SIGNALS:
1554+ void databasePathChanged() const;
1555+ void rowCountChanged() const;
1556+ void limitChanged() const;
1557+
1558+protected:
1559+ struct HistoryEntry {
1560+ QString sticker;
1561+ QDateTime mostRecentUse;
1562+ };
1563+ QList<HistoryEntry> m_entries;
1564+ int getEntryIndex(const QString& sticker) const;
1565+ void updateExistingEntryInDatabase(const HistoryEntry& entry);
1566+
1567+private:
1568+ QMutex m_dbMutex;
1569+ QSqlDatabase m_database;
1570+ int m_limit;
1571+
1572+ void resetDatabase(const QString& databaseName);
1573+ void createOrAlterDatabaseSchema();
1574+ void populateFromDatabase();
1575+ void insertNewEntryInDatabase(const HistoryEntry& entry);
1576+ void removeEntryFromDatabase(const QString& sticker);
1577+ void clearDatabase();
1578+ void removeExcessRows();
1579+};
1580+
1581+#endif // __STICKERS_HISTORY_MODEL_H__
1582
1583=== modified file 'tests/qml/CMakeLists.txt'
1584--- tests/qml/CMakeLists.txt 2015-08-13 18:34:55 +0000
1585+++ tests/qml/CMakeLists.txt 2015-11-16 14:15:20 +0000
1586@@ -1,33 +1,45 @@
1587-find_program(QMLTESTRUNNER_BIN
1588- NAMES qmltestrunner
1589- PATHS /usr/lib/*/qt5/bin
1590- NO_DEFAULT_PATH
1591-)
1592-
1593-find_program(XVFB_RUN_BIN
1594- NAMES xvfb-run
1595-)
1596-
1597-macro(DECLARE_QML_TEST TST_NAME TST_QML_FILE)
1598- add_test(NAME ${TST_NAME}
1599- WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
1600- COMMAND ${XVFB_RUN_BIN} -a -s "-screen 0 1024x768x24" ${QMLTESTRUNNER_BIN} -import ${qml_BINARY_DIR} -input ${CMAKE_CURRENT_SOURCE_DIR}/${TST_QML_FILE}
1601- )
1602-endmacro()
1603-
1604-if(QMLTESTRUNNER_BIN AND XVFB_RUN_BIN)
1605- declare_qml_test("message_bubble" tst_MessageBubble.qml)
1606- declare_qml_test("messages_view" tst_MessagesView.qml)
1607+find_package(Qt5Core REQUIRED)
1608+find_package(Qt5Gui REQUIRED)
1609+find_package(Qt5Network REQUIRED)
1610+find_package(Qt5Qml REQUIRED)
1611+find_package(Qt5Quick REQUIRED)
1612+find_package(Qt5QuickTest REQUIRED)
1613+find_package(Qt5Sql REQUIRED)
1614+
1615+set(XVFB_COMMAND)
1616+find_program(XVFBRUN xvfb-run)
1617+if(XVFBRUN)
1618+ set(XVFB_COMMAND ${XVFBRUN} -s "-screen 0 1024x768x24" -a)
1619 else()
1620- if (NOT QMLTESTRUNNER_BIN)
1621- message(WARNING "Qml tests disabled: qmltestrunner not found")
1622- else()
1623- message(WARNING "Qml tests disabled: xvfb-run not found")
1624- endif()
1625+ message(WARNING "Cannot find xvfb-run.")
1626 endif()
1627
1628-set(QML_TST_FILES
1629- tst_MessageBubble.qml
1630- tst_MessagesView.qml
1631-)
1632-add_custom_target(tst_QmlFiles ALL SOURCES ${QML_TST_FILES})
1633+set(CMAKE_AUTOMOC ON)
1634+
1635+set(TEST tst_QmlTests)
1636+set(SOURCES
1637+ ${messaging-app_SOURCE_DIR}/src/stickers-history-model.cpp
1638+ tst_QmlTests.cpp
1639+)
1640+add_executable(${TEST} ${SOURCES})
1641+include_directories(
1642+ ${CMAKE_CURRENT_BINARY_DIR}
1643+ ${CMAKE_CURRENT_SOURCE_DIR}
1644+ ${messaging-app_SOURCE_DIR}/src/
1645+)
1646+target_link_libraries(${TEST}
1647+ Qt5::Core
1648+ Qt5::Gui
1649+ Qt5::Network
1650+ Qt5::Qml
1651+ Qt5::Quick
1652+ Qt5::QuickTest
1653+ Qt5::Sql
1654+)
1655+add_test(${TEST} ${XVFB_COMMAND} ${CMAKE_CURRENT_BINARY_DIR}/${TEST}
1656+ -input ${CMAKE_CURRENT_SOURCE_DIR}
1657+ -import ${CMAKE_BINARY_DIR}/src)
1658+
1659+# make qml files visible in QtCreator
1660+file(GLOB_RECURSE NON_COMPILED_FILES *.qml)
1661+add_custom_target(NON_COMPILED_TARGET ALL SOURCES ${NON_COMPILED_FILES})
1662
1663=== added file 'tests/qml/tst_QmlTests.cpp'
1664--- tests/qml/tst_QmlTests.cpp 1970-01-01 00:00:00 +0000
1665+++ tests/qml/tst_QmlTests.cpp 2015-11-16 14:15:20 +0000
1666@@ -0,0 +1,78 @@
1667+/*
1668+ * Copyright 2015 Canonical Ltd.
1669+ *
1670+ * This file is part of messaging-app.
1671+ *
1672+ * webbrowser-app is free software; you can redistribute it and/or modify
1673+ * it under the terms of the GNU General Public License as published by
1674+ * the Free Software Foundation; version 3.
1675+ *
1676+ * webbrowser-app is distributed in the hope that it will be useful,
1677+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1678+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1679+ * GNU General Public License for more details.
1680+ *
1681+ * You should have received a copy of the GNU General Public License
1682+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1683+ */
1684+
1685+// Qt
1686+#include <QtCore/QDir>
1687+#include <QtCore/QTemporaryDir>
1688+#include <QtCore/QObject>
1689+#include <QtCore/QString>
1690+#include <QtCore/QTemporaryDir>
1691+#include <QtQml/QtQml>
1692+#include <QtQuickTest/QtQuickTest>
1693+
1694+// local
1695+#include "stickers-history-model.h"
1696+
1697+static QObject* StickersHistoryModel_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
1698+{
1699+ Q_UNUSED(engine);
1700+ Q_UNUSED(scriptEngine);
1701+ return new StickersHistoryModel();
1702+}
1703+
1704+class TestContext : public QObject
1705+{
1706+ Q_OBJECT
1707+
1708+ Q_PROPERTY(QString testDir READ testDir CONSTANT)
1709+
1710+public:
1711+ explicit TestContext(QObject* parent=0)
1712+ : QObject(parent)
1713+ {
1714+ QDir dir(m_temporary.path());
1715+ dir.mkpath("stickers");
1716+ }
1717+
1718+ QString testDir() const
1719+ {
1720+ return m_temporary.path();
1721+ }
1722+
1723+ QTemporaryDir m_temporary;
1724+};
1725+
1726+static QObject* TestContext_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
1727+{
1728+ Q_UNUSED(engine);
1729+ Q_UNUSED(scriptEngine);
1730+ return new TestContext();
1731+}
1732+
1733+int main(int argc, char** argv)
1734+{
1735+ const char* uri = "messagingapp.private";
1736+ qmlRegisterSingletonType<StickersHistoryModel>(uri, 0, 1, "StickersHistoryModel", StickersHistoryModel_singleton_factory);
1737+
1738+ const char* testUri = "messagingapptest.private";
1739+ qmlRegisterSingletonType<TestContext>(testUri, 0, 1, "TestContext", TestContext_singleton_factory);
1740+
1741+ return quick_test_main(argc, argv, "QmlTests", 0);
1742+}
1743+
1744+#include "tst_QmlTests.moc"
1745
1746=== added file 'tests/qml/tst_StickersHistoryModel.qml'
1747--- tests/qml/tst_StickersHistoryModel.qml 1970-01-01 00:00:00 +0000
1748+++ tests/qml/tst_StickersHistoryModel.qml 2015-11-16 14:15:20 +0000
1749@@ -0,0 +1,188 @@
1750+/*
1751+ * Copyright 2015 Canonical Ltd.
1752+ *
1753+ * This file is part of messaging-app.
1754+ *
1755+ * messaging-app is free software; you can redistribute it and/or modify
1756+ * it under the terms of the GNU General Public License as published by
1757+ * the Free Software Foundation; version 3.
1758+ *
1759+ * messaging-app is distributed in the hope that it will be useful,
1760+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1761+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1762+ * GNU General Public License for more details.
1763+ *
1764+ * You should have received a copy of the GNU General Public License
1765+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1766+ */
1767+
1768+import QtQuick 2.2
1769+import QtTest 1.0
1770+import Ubuntu.Components 1.3
1771+import Ubuntu.Test 0.1
1772+import messagingapp.private 0.1
1773+import messagingapptest.private 0.1
1774+
1775+Item {
1776+ id: root
1777+ width: 1
1778+ height: 1
1779+
1780+ property var model: StickersHistoryModel
1781+
1782+ SignalSpy {
1783+ id: countSpy
1784+ target: model
1785+ signalName: "rowCountChanged"
1786+ }
1787+
1788+ SignalSpy {
1789+ id: rowsInsertedSpy
1790+ target: model
1791+ signalName: "rowsInserted"
1792+ }
1793+
1794+ SignalSpy {
1795+ id: rowsMovedSpy
1796+ target: model
1797+ signalName: "rowsMoved"
1798+ }
1799+
1800+ SignalSpy {
1801+ id: dataChangedSpy
1802+ target: model
1803+ signalName: "dataChanged"
1804+ }
1805+
1806+ SignalSpy {
1807+ id: rowsRemovedSpy
1808+ target: model
1809+ signalName: "rowsRemoved"
1810+ }
1811+
1812+ UbuntuTestCase {
1813+ id: test
1814+ name: 'stickersHistoryModelTestCase'
1815+ when: windowShown
1816+
1817+ function init() {
1818+ model.databasePath = ":memory:"
1819+ model.limit = 10
1820+ }
1821+
1822+ function cleanup() {
1823+ model.databasePath = ""
1824+ countSpy.clear()
1825+ rowsInsertedSpy.clear()
1826+ dataChangedSpy.clear()
1827+ rowsMovedSpy.clear()
1828+ rowsRemovedSpy.clear()
1829+ }
1830+
1831+ function test_initiallyEmpty() {
1832+ compare(model.count, 0)
1833+ }
1834+
1835+ function test_addingEmptyOrInvalidDoesNothing() {
1836+ model.add("")
1837+ compare(model.count, 0)
1838+ model.add(null)
1839+ compare(model.count, 0)
1840+ model.add(undefined)
1841+ compare(model.count, 0)
1842+ }
1843+
1844+ function test_add_new() {
1845+ model.add("foo")
1846+ compare(model.count, 1)
1847+ compare(countSpy.count, 1)
1848+ compare(rowsInsertedSpy.count, 1)
1849+ compare(rowsInsertedSpy.signalArguments[0][1], 0) // first
1850+ compare(rowsInsertedSpy.signalArguments[0][2], 0) // last
1851+
1852+ model.add("bar")
1853+ compare(model.count, 2)
1854+ compare(countSpy.count, 2)
1855+ compare(rowsInsertedSpy.count, 2)
1856+ compare(rowsInsertedSpy.signalArguments[0][1], 0) // first
1857+ compare(rowsInsertedSpy.signalArguments[0][2], 0) // last
1858+ }
1859+
1860+ function test_signals() {
1861+ model.limit = 2
1862+
1863+ model.add("a")
1864+ compare(model.count, 1)
1865+ compare(countSpy.count, 1)
1866+ compare(rowsInsertedSpy.count, 1)
1867+ compare(rowsInsertedSpy.signalArguments[0][1], 0) // first
1868+ compare(rowsInsertedSpy.signalArguments[0][2], 0) // last
1869+
1870+ model.add("a")
1871+ compare(dataChangedSpy.count, 1)
1872+
1873+ model.add("b")
1874+ compare(model.count, 2)
1875+ compare(countSpy.count, 2)
1876+ compare(rowsInsertedSpy.count, 2)
1877+ compare(rowsInsertedSpy.signalArguments[0][1], 0) // first
1878+ compare(rowsInsertedSpy.signalArguments[0][2], 0) // last
1879+
1880+ model.add("a")
1881+ compare(rowsMovedSpy.count, 1)
1882+ compare(rowsMovedSpy.signalArguments[0][1], 1) // from first
1883+ compare(rowsMovedSpy.signalArguments[0][2], 1) // from last
1884+ compare(rowsMovedSpy.signalArguments[0][4], 0) // to
1885+ compare(dataChangedSpy.count, 2)
1886+
1887+ model.add("c")
1888+ compare(model.count, 2)
1889+ compare(rowsRemovedSpy.count, 1)
1890+ compare(rowsRemovedSpy.signalArguments[0][1], 2) // from
1891+ compare(rowsRemovedSpy.signalArguments[0][2], 2) // to
1892+ }
1893+
1894+ function test_get_and_order() {
1895+ model.add("a")
1896+ // datetimes have only millisecond precision, and we want to prevent
1897+ // the two entries to have the same timestamp
1898+ wait(100)
1899+ model.add("b")
1900+
1901+ var a = model.get(1)
1902+ verify(a !== null)
1903+ compare(a.sticker, "a")
1904+
1905+ var b = model.get(0)
1906+ verify(b !== null)
1907+ compare(b.sticker, "b")
1908+
1909+ verify(b.mostRecentUse.toISOString() > a.mostRecentUse.toISOString())
1910+
1911+ wait(100)
1912+ model.add("a")
1913+
1914+ a = model.get(0)
1915+ verify(a !== null)
1916+ compare(a.sticker, "a")
1917+
1918+ b = model.get(1)
1919+ verify(b !== null)
1920+ compare(b.sticker, "b")
1921+
1922+ verify(a.mostRecentUse.toISOString() > b.mostRecentUse.toISOString())
1923+ }
1924+
1925+ function test_limit_change() {
1926+ model.add("d")
1927+ model.add("c")
1928+ model.add("b")
1929+ model.add("a")
1930+ model.limit = 2
1931+
1932+ compare(model.count, 2)
1933+ compare(model.get(0).sticker, "a")
1934+ compare(model.get(1).sticker, "b")
1935+ }
1936+ }
1937+}

Subscribers

People subscribed via source and target branches