Merge branch 'GSoC2017-evaluation-final'

This commit is contained in:
Gioacchino Mazzurco 2017-08-18 01:37:39 +02:00
commit 32f43b999e
1431 changed files with 6666 additions and 445 deletions

View file

@ -0,0 +1,159 @@
/*
* RetroShare Android QML App
* Copyright (C) 2017 Gioacchino Mazzurco <gio@eigenlab.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.7
import "../" //Needed for Chat Cache
Item
{
id: compRoot
property string gxs_id
property bool onlyCached: false
signal clicked ()
height: 130
width: height
////////////// The following should be considered privates /////////////////////
property bool has_avatar: false
property bool default_image: false
property int avatarAttemptCnt: 0
property string noGxsImage: "/icons/retroshare.png"
function getDetails()
{
console.log("getDetails() ", compRoot.gxs_id )
++compRoot.avatarAttemptCnt
if (gxs_id)
{
default_image = false
var hasAvatarCached = hasAvatar (ChatCache.contactsCache.getIdentityAvatar(gxs_id))
if ( !hasAvatarCached )
{
rsApi.request(
"/identity/get_identity_details",
JSON.stringify({ gxs_id: compRoot.gxs_id }),
function(par)
{
var jData = JSON.parse(par.response).data
saveDetails(jData)
if(!compRoot.has_avatar &&
compRoot.avatarAttemptCnt < 3) getDetails()
})
}
else
{
setImage(ChatCache.contactsCache.getIdentityDetails(gxs_id))
}
}
else
{
has_avatar = true
default_image = true
contactAvatar.source = noGxsImage
}
}
function saveDetails(data)
{
ChatCache.contactsCache.setIdentityDetails(data)
setImage(data)
}
function setImage (data)
{
compRoot.has_avatar = hasAvatar (data.avatar)
if(compRoot.has_avatar)
{
contactAvatar.source =
"data:image/png;base64," + data.avatar
}
}
function hasAvatar (avatar)
{
return avatar.length > 0
}
function showDetails()
{
console.log("showDetails() ", gxs_id)
if (stackView.currentItem.objectName != "contactDetails")
{
stackView.push(
"qrc:/ContactDetails.qml",
{md: ChatCache.contactsCache.getContactFromGxsId(gxs_id)})
}
}
function refresh()
{
ChatCache.contactsCache.delIdentityAvatar(gxs_id)
compRoot.avatarAttemptCnt = 0
getDetails()
}
Component.onCompleted: startComponent ()
onVisibleChanged: startComponent ()
function startComponent ()
{
if (onlyCached && hasAvatar (ChatCache.contactsCache.getIdentityAvatar(gxs_id) ) )
{
console.log("load cached avatar")
setImage(ChatCache.contactsCache.getIdentityDetails(gxs_id))
}
else if (!onlyCached)
{
if(visible && (!has_avatar || default_image ) ) getDetails()
}
}
Image
{
id: contactAvatar
anchors.fill: parent
visible: compRoot.has_avatar
fillMode: Image.PreserveAspectFit
}
Faces
{
visible: !compRoot.has_avatar
hash: compRoot.gxs_id
anchors.fill: parent
iconSize: parent.height
}
MouseArea
{
anchors.fill: parent
onClicked:
{
compRoot.clicked()
showDetails()
}
}
}

View file

@ -0,0 +1,28 @@
import QtQuick 2.0
Item {
id: root
signal clicked
signal pressed
signal released
property var imgUrl: ""
property alias fillMode: image.fillMode
Image {
id: image
anchors.fill: parent
source: imgUrl
height: parent.height
width: parent.width
}
MouseArea {
anchors.fill: root
onClicked: { root.clicked() }
onPressed: { root.pressed() }
onReleased: { root.released() }
}
}

View file

@ -0,0 +1,130 @@
import QtQuick 2.0
import QtQuick.Controls 2.0
Item
{
id: button
property alias text: innerText.text;
property alias buttonTextPixelSize: innerText.font.pixelSize
property alias innerAnchors: innerElements.anchors;
property alias rectangleButton: rectangleButton;
property var iconUrl
property int iconHeight: 20
property color color: "lightsteelblue"
property color hoverColor: color
property color pressColor: color
property int fontSize
property int borderWidth
property int borderRadius: 3
property int innerMargin: 10
height: innerText.height + innerMargin + innerMargin
width: innerText.width + innerMargin + innerMargin + icon.width + icon.width
scale: state === "Pressed" ? 0.96 : 1.0
onEnabledChanged: state = ""
signal clicked
Rectangle
{
id: rectangleButton
anchors.fill: parent
radius: borderRadius
color: button.enabled ? button.color : "lavender"
border.width: borderWidth
border.color: "black"
ToolButton {
id: innerElements
Image
{
id: icon
source: (iconUrl)? iconUrl: ""
height: (iconUrl)? iconHeight: 0
width: (iconUrl)? iconHeight: 0
fillMode: Image.PreserveAspectFit
visible: (iconUrl)? true: false
anchors.left: innerElements.left
anchors.margins:(iconUrl)? innerMargin : 0
anchors.verticalCenter: parent.verticalCenter
sourceSize.height: height
}
Text
{
anchors.margins: innerMargin
id: innerText
font.pointSize: fontSize
anchors.left: icon.right
anchors.verticalCenter: parent.verticalCenter
color: button.enabled ? "black" : "grey"
}
}
}
states: [
State
{
name: "Hovering"
PropertyChanges
{
target: rectangleButton
color: hoverColor
}
},
State
{
name: "Pressed"
PropertyChanges
{
target: rectangleButton
color: pressColor
}
}
]
transitions: [
Transition
{
from: ""; to: "Hovering"
ColorAnimation { duration: 200 }
},
Transition
{
from: "*"; to: "Pressed"
ColorAnimation { duration: 10 }
}
]
MouseArea
{
hoverEnabled: true
anchors.fill: button
onEntered: { button.state='Hovering'}
onExited: { button.state=''}
onClicked: { button.clicked();}
onPressed: { button.state="Pressed" }
onReleased:
{
if (containsMouse)
button.state="Hovering";
else
button.state="";
}
}
Behavior on scale
{
NumberAnimation
{
duration: 100
easing.type: Easing.InOutQuad
}
}
}

View file

@ -0,0 +1,72 @@
/*
* RetroShare Android QML App
* Copyright (C) 2017 Gioacchino Mazzurco <gio@eigenlab.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.7
Rectangle
{
property string hash
width: height
property int childHeight : height/2
color: "white"
Image
{
source: "qrc:/icons/edit-image-face-detect.svg"
anchors.centerIn: parent
height: parent.height
width: parent.width
sourceSize.height: height
sourceSize.width: width
fillMode: Image.PreserveAspectFit
}
Rectangle
{
color: '#' + hash.substring(1, 9)
height: parent.childHeight
width: height
anchors.top: parent.top
anchors.left: parent.left
}
Rectangle
{
color: '#' + hash.substring(9, 17)
height: parent.childHeight
width: height
anchors.top: parent.top
anchors.right: parent.right
}
Rectangle
{
color: '#' + hash.substring(17, 25)
height: parent.childHeight
width: height
anchors.bottom: parent.bottom
anchors.left: parent.left
}
Rectangle
{
color: '#' + hash.slice(-8)
height: parent.childHeight
width: height
anchors.bottom: parent.bottom
anchors.right: parent.right
}
}

View file

@ -0,0 +1,60 @@
import QtQuick 2.7
import QtQuick.Dialogs 1.2
import "../URI.js" as UriJs
Item
{
id: compRoot
property var resultFile
FileDialog
{
id: fileDialog
title: "Please choose a file"
folder: shortcuts.pictures
nameFilters: [ "Image files (*.png *.jpg)"]
visible: false
selectMultiple: false
onAccepted: {
console.log("You chose: " + fileDialog.fileUrl)
resultFile = fileDialog.fileUrl
}
onRejected: {
console.log("Canceled")
}
}
function open()
{
if (Qt.platform.os === "android")
{
console.log("ImagePicker Android platform detected")
mainWindow.addUriHandler("file", androidResult)
androidImagePicker.openPicker()
}
else
{
fileDialog.visible = true
}
}
function androidResult (uri)
{
console.log("QML Android image uri found" , uri)
resultFile = normalizeUriToFilePath (uri)
mainWindow.delUriHandler("media", androidResult)
}
function normalizeUriToFilePath (uriStr)
{
var uri = new UriJs.URI(uriStr)
var hPath = uri.path()
return "file:///"+hPath
}
}

View file

@ -0,0 +1,202 @@
import QtQuick 2.7
import Qt.labs.settings 1.0
import "../" // Needed by ChatCache (where stores generated faces)
Item
{
id: faces
property string hash
property var facesCache: ChatCache.facesCache
Image
{
id: imageAvatar
width: height
height: iconSize
visible: true
}
Canvas
{
id: canvasAvatar
width: height
height: canvasSizes
visible: false
renderStrategy: Canvas.Threaded;
renderTarget: Canvas.Image;
property var images
property var callback
onPaint:
{
var ctx = getContext("2d");
if (images)
{
for (y = 0 ; y< nPieces ; y++)
{
ctx.drawImage(images[y], 0, 0, iconSize, iconSize )
}
}
}
onPainted:
{
if (callback)
{
var data = toDataURL('image/png')
callback(data)
}
}
}
Component.onCompleted:
{
createFromHex(hash)
}
property var facesPath: "/icons/faces/"
property var iconSize: 32
property var canvasSizes: iconSize > 32 ? 64 : 32;
property var nPieces: 6
property var pieces: []
// Number of image files corresponding to heach part:
// [background, face, hair|head, mouth, clothes, eye ]
property var total:
({
female: [5, 4, 33, 17, 59, 53],
male: [5, 4, 36, 26, 65, 32]
})
function src (gender, piece, random)
{
var head = gender === 'female' ? 'head' : 'hair';
var pieces = ['background', 'face', head, 'mouth', 'clothes', 'eye'];
var num = random % total[gender][piece] + 1;
return facesPath+gender+canvasSizes+'/'+pieces[piece]+num+'.png';
}
function calcDataFromFingerprint(dataHex)
{
var females = total.female.reduce(function(previousValue, currentValue)
{
return previousValue * currentValue;
});
var males = total.male.reduce(function(previousValue, currentValue)
{
return previousValue * currentValue;
});
var ret = [];
var data = parseInt(dataHex, 16) % (females+males);
if (data - females < 0)
{
var i = 0;
ret = ['female'];
while (data > 0 || i < total.female.length)
{
ret.push(data % total.female[i]);
data = parseInt(data / total.female[i]);
i++;
}
}
else
{
data = data - females;
var i = 0;
ret = ['male'];
while (data > 0 || i < total.male.length)
{
ret.push(data % total.male[i]);
data = parseInt(data / total.male[i]);
i++;
}
}
return ret;
}
function generateImage(data, callback)
{
var gender = data[0];
var onloads = [];
for (var i=0; i<nPieces; i++)
{
var url = src(gender, i, data[i+1])
onloads.push(url)
canvasAvatar.loadImage(url)
}
canvasAvatar.images = onloads
canvasAvatar.callback = callback
canvasAvatar.requestPaint()
}
// Create the identicon
function createFromHex(dataHex)
{
var iconId = [dataHex, iconSize];
var update = function(data)
{
// This conditions are for solve a bug on an Lg S3.
// On this device the toDataURL() is incompleted.
// So for see the complete avatar at least at first execution we'll show the canvas,
// instead of the image component.
// See issue: https://gitlab.com/angesoc/RetroShare/issues/37
if (facesCache.iconCache[iconId])
{
imageAvatar.source = data
imageAvatar.visible = true
canvasAvatar.visible = false
canvasAvatar.height = 0
imageAvatar.height = iconSize
}
else
{
canvasAvatar.visible = true
imageAvatar.visible = false
canvasAvatar.height = iconSize
imageAvatar.height = 0
}
facesCache.iconCache[iconId] = data;
}
if (facesCache.iconCache.hasOwnProperty(iconId))
{
update(facesCache.iconCache[iconId])
}
else if(facesCache.callbackCache.hasOwnProperty(iconId))
{
facesCache.callbackCache[iconId].push(update)
}
else
{
var onImageGenerated = function(data)
{
facesCache.callbackCache[iconId].forEach(function(callback)
{
callback(data);
})
}
facesCache.callbackCache[iconId] = [update];
if (dataHex)
{
generateImage(calcDataFromFingerprint(dataHex), onImageGenerated);
}
}
}
}

View file

@ -0,0 +1,283 @@
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import "../URI.js" as UriJs
import "../"
import "../components"
Drawer
{
property var styles: StyleSideBar
id: drawer
height: parent.height
width: Math.min(parent.width, parent.height) / 3 * styles.width
dragMargin: 10
Rectangle
{
width: parent.width
height: parent.height
ListView
{
id: listView
currentIndex: -1
anchors.fill:parent
clip: true
snapMode: ListView.SnapToItem
headerPositioning: ListView.OverlayHeader
header:Rectangle
{
id:header
property var styles: StyleSideBar.header
width: parent.width
height: colorHash.height + nickText.height + gxsText.height + 40
color: styles.color
AvatarOrColorHash
{
id: colorHash
gxs_id: (ChatCache.contactsCache.own)?ChatCache.contactsCache.own.gxs_id : ""
height: styles.avatarHeight
anchors.margins: styles.avatarMargins
anchors.horizontalCenter: header.horizontalCenter
anchors.top: header.top
onClicked: drawer.close()
}
Text
{
id: nickText
text: (ChatCache.contactsCache.own)?ChatCache.contactsCache.own.name : "Retroshare"
height: contentHeight
anchors.top: colorHash.bottom
anchors.left: header.left
anchors.right: header.right
anchors.leftMargin: 10
anchors.rightMargin: 10
horizontalAlignment:Text.AlignHCenter
wrapMode: Text.Wrap
color: styles.textColor
font.bold: true
font.pixelSize: styles.textNickSize
}
Text
{
id: gxsText
text: (ChatCache.contactsCache.own)?ChatCache.contactsCache.own.gxs_id : ""
// height: contentHeight
wrapMode: Text.WrapAnywhere
width: header.width
anchors.top: nickText.bottom
anchors.left: header.left
anchors.right: header.right
anchors.leftMargin: 10
anchors.rightMargin: 10
horizontalAlignment:Text.AlignHCenter
color: styles.textColor
font.pixelSize: styles.textGxsidSize
}
}
delegate: Item
{
property var styles: StyleSideBar.item
id: menuItem
width: parent.width
height: styles.height
Connections
{
target: mainWindow
onCoreReadyChanged:
{
if (model.showOnCoreReady)
{
setVisible(mainWindow.coreReady)
}
}
}
ButtonText
{
text: model.title
width: parent.width
height: parent.height
color: menuItem.styles.defaultColor
hoverColor: menuItem.styles.hoverColor
innerAnchors.left: rectangleButton.left
innerAnchors.verticalCenter: rectangleButton.verticalCenter
iconUrl: (model.icon)? model.icon : undefined
innerMargin: 20
buttonTextPixelSize: menuItem.styles.pixelSize
borderRadius: 0
}
MouseArea
{
property var lastItem
id: itemArea
width: parent.width
height: parent.height
onClicked:
{
if (listView.currentIndex != index || stackView.currentItem != lastItem)
{
listView.currentIndex = index
menuList.actions[model.title]();
lastItem = stackView.currentItem
// titleLabel.text = model.title
// stackView.replace(model.source)
}
drawer.close()
}
}
visible: (model.showOnCoreReady)? setVisible(mainWindow.coreReady) : true
Component.onCompleted:
{
if (model.showOnOsAndroid && !Q_OS_ANDROID)
{
menuItem.visible = false
menuItem.height = 0
}
}
function setVisible(b)
{
menuItem.visible = b
if (!b)
{
menuItem.height = 0
}
else
{
menuItem.height = styles.height
}
}
}
model: ListModel
{
id: menuList
property var actions :
{
"Contacts": function()
{
stackView.push("qrc:/Contacts.qml" )
},
"Trusted Nodes": function()
{
stackView.push("qrc:/TrustedNodesView.qml");
},
"Paste Link": function()
{
UriJs.URI.withinString(
ClipboardWrapper.getFromClipBoard(),
handleIntentUri);
},
"Share identity": function()
{
rsApi.request(
"/peers/self/certificate/", "",
function(par)
{
var radix = JSON.parse(par.response).data.cert_string
var name = mainWindow.user_name
var encodedName = UriJs.URI.encode(name)
var nodeUrl = (
"retroshare://certificate?" +
"name=" + encodedName +
"&radix=" + UriJs.URI.encode(radix) +
"&location=" + encodedName )
ClipboardWrapper.postToClipBoard(nodeUrl)
linkCopiedPopup.itemName = name
linkCopiedPopup.open()
platformGW.shareUrl(nodeUrl);
})
},
"Terminate Core": function()
{
rsApi.request("/control/shutdown");
}
}
ListElement
{
title: "Contacts"
showOnCoreReady: true
icon: "/icons/search.svg"
}
ListElement
{
title: "Trusted Nodes"
showOnCoreReady: true
icon: "/icons/netgraph.svg"
}
ListElement
{
title: "Paste Link"
showOnCoreReady: true
icon: "/icons/add.svg"
}
ListElement
{
title: "Share identity"
showOnCoreReady: true
icon: "/icons/share.svg"
}
ListElement
{
title: "Terminate Core"
showOnOsAndroid: false
icon: "/icons/exit.svg"
}
}
ScrollIndicator.vertical: ScrollIndicator { }
}
Rectangle
{
property var styles: StyleSideBar.footer
width: parent.width
anchors.bottom: parent.bottom
height: Label.contentHeight
color: styles.color
Label
{
horizontalAlignment: Text.AlignRight
anchors.bottom: parent.bottom
anchors.right: parent.right
text: parent.styles.text
color: parent.styles.textColor
anchors.rightMargin: parent.styles.margins
anchors.bottomMargin: parent.styles.margins
}
}
}
}

View file

@ -0,0 +1,38 @@
import QtQuick 2.0
import QtQuick.Controls 2.0
Item
{
height: innerText.implicitHeight
property int iconHeight: 25
property alias iconUrl: icon.source
property alias innerText: innerText.text
Image
{
id: icon
height: iconHeight
width: height
fillMode: Image.PreserveAspectFit
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
}
Text
{
id: innerText
wrapMode: Text.WrapAnywhere
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.left: icon.right
anchors.leftMargin: 5
}
}

View file

@ -0,0 +1,56 @@
import QtQuick 2.7
import QtQuick.Controls.Styles 1.2
Rectangle {
id: emojiButton
property var fontName
Text {
id: emojiText
color: "gray"
text: qsTr(eCatText)
font.pixelSize: emojiButton.width - 8
anchors.centerIn: parent
font.family: fontName
}
state: "RELEASED"
states: [
State {
name: "PRESSED"
PropertyChanges {
target: emojiText
font.pixelSize: emojiButton.width - 10
}
},
State {
name: "RELEASED"
PropertyChanges {
target: emojiText
font.pixelSize: emojiButton.width - 8
}
}
]
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
emojiText.color = "black"
}
onExited: {
emojiText.color = "gray"
}
onPressedChanged: {
emojiButton.state = emojiButton.state == "PRESSED" ? "RELEASED" : "PRESSED"
}
onClicked: {
Qt.emojiClickedHandler(emojiText.text)
}
}
}

View file

@ -0,0 +1,82 @@
import QtQuick 2.7
import QtQuick.Controls.Styles 1.2
Rectangle {
id: emojiCategoryButton
property string categoryName
property var fontName
function completedHandler() {
categoryName = eCatName
//initialize
if (parent.currSelEmojiButton === undefined) {
clickedHandler()
}
}
function pressedHandler() {
if (state != "SELECTED") {
state = state == "PRESSED" ? "RELEASED" : "PRESSED"
}
}
function clickedHandler() {
if (parent.currSelEmojiButton !== undefined) {
parent.currSelEmojiButton.state = "RELEASED"
}
parent.currSelEmojiButton = emojiCategoryButton
state = "SELECTED"
Qt.emojiCategoryChangedHandler(emojiCategoryButton.categoryName)
}
Text {
id: emojiText
color: "gray"
text: qsTr(eCatText)
font.pixelSize: emojiCategoryButton.width - 8
anchors.centerIn: parent
font.family: fontName
}
state: "RELEASED"
states: [
State {
name: "PRESSED"
PropertyChanges {
target: emojiText
font.pixelSize: emojiCategoryButton.width - 10
}
},
State {
name: "RELEASED"
PropertyChanges {
target: emojiText
font.pixelSize: emojiCategoryButton.width - 8
}
},
State {
name: "SELECTED"
PropertyChanges {
target: emojiCategoryButton
color: "#ADD6FF"
}
}
]
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: emojiText.color = "black"
onExited: emojiText.color = "gray"
onPressedChanged: pressedHandler()
onClicked: clickedHandler()
}
Component.onCompleted: completedHandler()
}

View file

@ -0,0 +1,130 @@
import QtQuick 2.7
import QtQuick.Controls 2.0
import "emoji.js" as EmojiJSON
Rectangle {
id: emojiPicker
property EmojiCategoryButton currSelEmojiButton
property variant emojiParsedJson
property int buttonWidth: 40
property TextArea textArea
property var rootFontName: theme.emojiFontName
//displays all Emoji of one categroy by modifying the ListModel of emojiGrid
function categoryChangedHandler (newCategoryName){
emojiByCategory.clear()
for (var i = 0; i < emojiParsedJson.emoji_by_category[newCategoryName].length; i++) {
var elem = emojiParsedJson.emoji_by_category[newCategoryName][i]
emojiByCategory.append({eCatName: newCategoryName, eCatText: elem})
}
}
//adds the clicked Emoji (and one ' ' if the previous character isn't an Emoji) to textArea
function emojiClickedHandler(selectedEmoji) {
var strAppnd = ""
var plainText = textArea.getText(0, textArea.length)
if (plainText.length > 0) {
var lastChar = plainText[plainText.length-1]
if ((lastChar !== ' ') && (lastChar.charCodeAt(0) < 255)) {
strAppnd = " "
}
}
strAppnd += selectedEmoji
textArea.insert(textArea.cursorPosition, strAppnd)
}
//parses JSON, publishes button handlers and inits textArea
function completedHandler() {
// emojiParsedJson = JSON.parse(EmojiJSON.emoji_json)
emojiParsedJson = EmojiJSON.emoji_json
for (var i = 0; i < emojiParsedJson.emoji_categories.length; i++) {
var elem = emojiParsedJson.emoji_categories[i]
emojiCategoryButtons.append({eCatName: elem.name, eCatText: elem.emoji_unified})
}
Qt.emojiCategoryChangedHandler = categoryChangedHandler
Qt.emojiClickedHandler = emojiClickedHandler
textArea.cursorPosition = textArea.length
textArea.Keys.pressed.connect(keyPressedHandler)
}
//checks if the previous character is an Emoji and adds a ' ' if that's the case
//this is necessary, because Emoji use a bigger font-size, and that font-size is kept using without a ' '
function keyPressedHandler(event) {
var testStr = textArea.getText(textArea.length-2, textArea.length)
var ptrn = new RegExp("[\uD800-\uDBFF][\uDC00-\uDFFF]")
if ((event.key !== Qt.Key_Backspace) && (ptrn.test(testStr))) {
textArea.text += " "
textArea.cursorPosition = textArea.length
}
}
//all emoji of one category
ListModel {
id: emojiByCategory
}
GridView {
id: emojiGrid
width: parent.width
anchors.fill: parent
anchors.bottomMargin: buttonWidth
cellWidth: buttonWidth; cellHeight: buttonWidth
model: emojiByCategory
delegate: EmojiButton {
width: buttonWidth
height: buttonWidth
color: emojiPicker.color
fontName: rootFontName
}
}
//seperator
Rectangle {
color: emojiPicker.color
anchors.bottom: parent.bottom
width: parent.width
height: buttonWidth
}
Rectangle {
color: "black"
anchors.bottom: parent.bottom
anchors.bottomMargin: buttonWidth
width: parent.width
height: 1
}
//emoji category selector
ListView {
width: parent.width
anchors.bottom: parent.bottom
anchors.bottomMargin: buttonWidth
orientation: ListView.Horizontal
model: emojiCategoryButtons
delegate: EmojiCategoryButton {
width: buttonWidth
height: buttonWidth
color: emojiPicker.color
fontName: rootFontName
}
}
ListModel {
id: emojiCategoryButtons
}
Component.onCompleted:
{
completedHandler()
}
}

File diff suppressed because it is too large Load diff