1/*
2 * Copyright 2013 Canonical Ltd.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation; version 3.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 * GNU Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 */
16
17import QtQuick 2.4
18import QtQuick.LocalStorage 2.0
19
20import MaliitKeyboard 2.0
21
22import keys 1.0
23import "emoji.js" as Emoji
24
25KeyPad {
26    anchors.fill: parent
27
28    content: c1
29    symbols: "languages/Keyboard_symbols.qml"
30
31    Component.onCompleted: {
32        panel.switchBack = true;
33    }
34
35    QtObject {
36        id: internal
37        property int offset: 0
38        property bool loading: true
39        property int maxRecent: (c1.numberOfRows - 1) * c1.maxNrOfKeys
40        property int oldVisibleIndex: -1
41        property var recentEmoji: []
42        property var chars
43        property var db
44
45        Component.onCompleted: {
46            db = LocalStorage.openDatabaseSync("Emoji", "1.0", "Storage for emoji keyboard layout", 1000000);
47
48            db.transaction(
49                function(tx) {
50                    // Create the database if it doesn't already exist
51                    tx.executeSql('CREATE TABLE IF NOT EXISTS Recent(emoji VARCHAR(16), time TIMESTAMP DEFAULT CURRENT_TIMESTAMP)');
52                    tx.executeSql('CREATE TABLE IF NOT EXISTS State(contentX INTEGER, visibleIndex INTEGER)');
53
54                    var rs = tx.executeSql('SELECT emoji FROM Recent ORDER BY time ASC');
55                    for (var i = 0; i < rs.rows.length; i++) {
56                        recentEmoji.push(rs.rows.item(i).emoji);
57                    }
58                    for (var i = 0; i < recentEmoji.length % (c1.numberOfRows - 1); i++) {
59                        recentEmoji.push("");
60                    }
61                    chars = recentEmoji.concat(Emoji.emoji);
62
63                    for (var i = 0; i < chars.length; i++) {
64                        c1.model.append({char: chars[i]});
65                    }
66
67                    rs = tx.executeSql('SELECT contentX, visibleIndex FROM State');
68                    if (rs.rows.length > 0) {
69                        internal.oldVisibleIndex = rs.rows.item(0).visibleIndex;
70                        c1.contentX = rs.rows.item(0).contentX;
71                    } else {
72                        tx.executeSql('INSERT INTO State VALUES(0, 0)');
73                        // Start on the smiley page
74                        c1.positionViewAtIndex(recentEmoji.length, GridView.Beginning)
75                        updatePositionDb();
76                    }
77                }
78            );
79        }
80
81        function jumpTo(position) {
82            c1.positionViewAtIndex(position, GridView.Beginning);
83            c1.startingPosition = false;
84            internal.updatePositionDb();
85        }
86
87        function updatePositionDb() {
88            db.transaction(
89                function(tx) {
90                    tx.executeSql('UPDATE State SET contentX=?, visibleIndex=?', [c1.contentX, c1.midVisibleIndex]);
91                }
92            );
93        }
94
95        function updateRecent(emoji) {
96            internal.loading = false;
97            // Hide the magnifier before we reposition the key
98            magnifier.shown = false;
99            magnifier.currentlyAssignedKey = null;
100            var originalLength = recentEmoji.length;
101            var position = recentEmoji.indexOf(emoji);
102            c1.positionBeforeInsertion = c1.contentX;
103            // If this emoji is already in the recent list we leave it alone
104            if (position != -1) {
105                return;
106            }
107
108            // If the list is full remove the last emoji before inserting
109            if (recentEmoji.length >= maxRecent || recentEmoji[recentEmoji.length - 1] == "") {
110                recentEmoji.splice(recentEmoji.length - 1, 1);
111            }
112
113            recentEmoji.unshift(emoji);
114
115            // Always append a column at a time
116            for (var i = 0; i < recentEmoji.length % (c1.numberOfRows - 1); i++) {
117                recentEmoji.push("");
118            }
119
120            // We then update the char properties in the model
121            for (var i = 0; i < recentEmoji.length; i++) {
122                if (i >= originalLength) {
123                    c1.model.insert(i, {"char" : recentEmoji[i]});
124                } else {
125                    c1.model.setProperty(i, "char", recentEmoji[i]);
126                }
127            }
128
129            db.transaction(
130                function(tx) {
131                    tx.executeSql('DELETE FROM Recent WHERE emoji = ?', emoji);
132                    tx.executeSql('INSERT INTO Recent(emoji) VALUES(?)', emoji);
133                    var rs = tx.executeSql('SELECT COUNT(emoji) as totalRecent FROM Recent');
134                    if (rs.rows.item(0).totalRecent > maxRecent) {
135                        tx.executeSql('DELETE FROM Recent ORDER BY time ASC LIMIT ?', rs.rows.item(0).totalRecent - maxRecent);
136                    }
137                }
138            );
139        }
140    }
141
142    GridView {
143        id: c1
144        objectName: "emojiGrid"
145        property int midVisibleIndex: indexAt(contentX + (width / 2), 0) == -1 ? internal.oldVisibleIndex : indexAt(contentX + (width / 2), 0);
146        property int numberOfRows: 5
147        property int maxNrOfKeys: fullScreenItem.tablet ? 12 : 10
148        property int oldWidth: 0
149        property int positionBeforeInsertion: 0
150        property bool startingPosition: true
151        anchors.top: parent.top
152        anchors.bottom: categories.top
153        anchors.left: parent.left
154        anchors.right: parent.right
155        model: ListModel { }
156        flow: GridView.FlowTopToBottom
157        flickDeceleration: Device.gu(500)
158        snapMode: GridView.SnapToRow
159        cellWidth: fullScreenItem.landscape ? panel.keyWidth * 0.7 : panel.keyWidth
160        cellHeight: panel.keyHeight
161        cacheBuffer: Device.gu(30)
162        onContentXChanged: {
163            magnifier.shown = false;
164            magnifier.currentlyAssignedKey = null;
165        }
166        onContentWidthChanged: {
167            // Shift view to compensate for new emoji being added
168            // But only if the view has actually moved (GridView's
169            // behaviour is inconsistent depending on how far away
170            // from the insertion point we are in the model)
171            if (!internal.loading && (positionBeforeInsertion != contentX || startingPosition)) {
172                contentX += contentWidth - oldWidth;
173            }
174            oldWidth = contentWidth;
175        }
176        onMovementEnded: {
177            internal.updatePositionDb();
178            startingPosition = false;
179        }
180
181        Component {
182            id: charDelegate
183            CharKey {
184                property var emoji: null
185                visible: label != ""
186                label: emoji != null ? emoji.char : ""
187                shifted: label
188                normalColor: Theme.backgroundColor
189                borderColor: normalColor
190                pressedColor: Theme.backgroundColor
191                fontSize: Device.gu(2.5)
192                fontFamily: "Noto Color Emoji"
193                onKeySent: {
194                    internal.updateRecent(key);
195                }
196            }
197        }
198
199        delegate: Loader {
200            // Don't load asynchronously if the user is flicking through the
201            // grid, otherwise loading looks messy
202            asynchronous: !c1.movingHorizontally
203            sourceComponent: charDelegate
204            onLoaded: {
205                item.emoji = model;
206            }
207        }
208
209     }
210
211     Row {
212        id: categories
213        anchors.bottom: parent.bottom
214        anchors.left: parent.left
215        anchors.right: parent.right
216        height: panel.keyHeight
217
218        spacing: fullScreenItem.tablet ? panel.keyWidth / 5 : 0
219
220        LanguageKey {
221            id: languageMenuButton
222            normalColor: Theme.backgroundColor
223            borderColor: normalColor
224            pressedColor: Theme.backgroundColor
225        }
226
227        CategoryKey {
228            id: recentCat
229            label: "⏱"
230            highlight: (c1.midVisibleIndex < internal.recentEmoji.length && c1.midVisibleIndex > 0)
231                       || (c1.contentX == 0 && internal.recentEmoji.length > 0)
232            onPressed: {
233                Feedback.startPressEffect();
234                internal.jumpTo(0);
235            }
236        }
237
238        CategoryKey {
239            label: "��"
240            highlight: (c1.midVisibleIndex >= internal.recentEmoji.length
241                        && c1.midVisibleIndex < 540 + internal.recentEmoji.length
242                        && !recentCat.highlight)
243                       || c1.midVisibleIndex == -1
244            onPressed: {
245                Feedback.startPressEffect();
246                internal.jumpTo(internal.recentEmoji.length);
247                if (internal.recentEmoji.length < internal.maxRecent) {
248                    c1.startingPosition = true;
249                }
250            }
251        }
252
253        CategoryKey {
254            label: "��"
255            highlight: c1.midVisibleIndex >= 540 + internal.recentEmoji.length && c1.midVisibleIndex < 701 + internal.recentEmoji.length
256            onPressed: {
257                Feedback.startPressEffect();
258                internal.jumpTo(540 + internal.recentEmoji.length);
259            }
260        }
261
262        CategoryKey {
263            label: "��"
264            highlight: c1.midVisibleIndex >= 701 + internal.recentEmoji.length && c1.midVisibleIndex < 786 + internal.recentEmoji.length
265            onPressed: {
266                Feedback.startPressEffect();
267                internal.jumpTo(701 + internal.recentEmoji.length);
268            }
269        }
270
271        CategoryKey {
272            label: "��"
273            highlight: c1.midVisibleIndex >= 786 + internal.recentEmoji.length && c1.midVisibleIndex < 931 + internal.recentEmoji.length
274            onPressed: {
275                Feedback.startPressEffect();
276                internal.jumpTo(786 + internal.recentEmoji.length);
277            }
278        }
279
280        CategoryKey {
281            label: "��"
282             highlight: c1.midVisibleIndex >= 931 + internal.recentEmoji.length && c1.midVisibleIndex < 1050 + internal.recentEmoji.length
283            onPressed: {
284                Feedback.startPressEffect();
285                internal.jumpTo(931 + internal.recentEmoji.length);
286            }
287        }
288
289        CategoryKey {
290            label: "��"
291            highlight: c1.midVisibleIndex >= 1050 + internal.recentEmoji.length && c1.midVisibleIndex < 1229 + internal.recentEmoji.length
292            onPressed: {
293                Feedback.startPressEffect();
294                internal.jumpTo(1050 + internal.recentEmoji.length);
295            }
296        }
297
298        CategoryKey {
299            label: "❤"
300            highlight: c1.midVisibleIndex >= 1229 + internal.recentEmoji.length && c1.midVisibleIndex < 1512 + internal.recentEmoji.length
301            onPressed: {
302                Feedback.startPressEffect();
303                internal.jumpTo(1229 + internal.recentEmoji.length);
304            }
305        }
306
307        CategoryKey {
308            label: "��"
309            highlight: c1.midVisibleIndex >= 1512 + internal.recentEmoji.length
310            onPressed: {
311                Feedback.startPressEffect();
312                internal.jumpTo(1512 + internal.recentEmoji.length);
313            }
314        }
315
316        BackspaceKey {
317            padding: 0
318            normalColor: Theme.backgroundColor
319            borderColor: normalColor
320            pressedColor: Theme.backgroundColor
321        }
322    }
323}
324