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