uiux: introduce Emoji popup components for new chat input
This commit is contained in:
parent
dcc0a1d321
commit
961a370002
|
@ -0,0 +1,55 @@
|
||||||
|
import QtQuick 2.13
|
||||||
|
import QtGraphicalEffects 1.0
|
||||||
|
import "../../imports"
|
||||||
|
import "../../shared"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool active: false
|
||||||
|
property var changeCategory: function () {}
|
||||||
|
property url source: "../app/img/emojiCategories/recent.svg"
|
||||||
|
|
||||||
|
id: categoryButton
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
SVGImage {
|
||||||
|
width: 20
|
||||||
|
height: 20
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
source: categoryButton.source
|
||||||
|
|
||||||
|
ColorOverlay {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: parent
|
||||||
|
color: categoryButton.active ? Style.current.blue : Style.current.transparent
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: categoryButton.active
|
||||||
|
width: parent.width
|
||||||
|
height: 2
|
||||||
|
radius: 1
|
||||||
|
color: Style.current.blue
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: -Style.current.smallPadding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: function () {
|
||||||
|
categoryButton.changeCategory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*##^##
|
||||||
|
Designer {
|
||||||
|
D{i:0;formeditorColor:"#ffffff";height:440;width:360}
|
||||||
|
}
|
||||||
|
##^##*/
|
|
@ -0,0 +1,232 @@
|
||||||
|
import QtQuick 2.13
|
||||||
|
import QtQuick.Controls 2.13
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import QtGraphicalEffects 1.0
|
||||||
|
import "../../imports"
|
||||||
|
import "../../shared"
|
||||||
|
|
||||||
|
import "./emojiList.js" as EmojiJSON
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
property var emojiSelected: function () {}
|
||||||
|
property var categories: []
|
||||||
|
property string searchString: searchBox.text
|
||||||
|
|
||||||
|
id: popup
|
||||||
|
modal: false
|
||||||
|
width: 360
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Style.current.radius
|
||||||
|
color: Style.current.background
|
||||||
|
border.color: Style.current.border
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: DropShadow{
|
||||||
|
verticalOffset: 3
|
||||||
|
radius: 8
|
||||||
|
samples: 15
|
||||||
|
fast: true
|
||||||
|
cached: true
|
||||||
|
color: "#22000000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addEmoji(emoji) {
|
||||||
|
const MAX_EMOJI_NUMBER = 36
|
||||||
|
const extenstionIndex = emoji.filename.lastIndexOf('.');
|
||||||
|
let iconCodePoint = emoji.filename
|
||||||
|
if (extenstionIndex > -1) {
|
||||||
|
iconCodePoint = iconCodePoint.substring(0, extenstionIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the filename to get all the parts and then encode them from hex to utf8
|
||||||
|
const splitCodePoint = iconCodePoint.split('-')
|
||||||
|
let codePointParts = []
|
||||||
|
splitCodePoint.forEach(function (codePoint) {
|
||||||
|
codePointParts.push(`0x${codePoint}`)
|
||||||
|
})
|
||||||
|
const encodedIcon = String.fromCodePoint(...codePointParts);
|
||||||
|
|
||||||
|
// Add at the start of the list
|
||||||
|
let recentEmojis = appSettings.recentEmojis
|
||||||
|
recentEmojis.unshift(emoji)
|
||||||
|
// Remove duplicates
|
||||||
|
recentEmojis = recentEmojis.filter(function (e, index) {
|
||||||
|
return !recentEmojis.some(function (e2, index2) {
|
||||||
|
return index2 < index && e2.filename === e.filename
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (recentEmojis.length > MAX_EMOJI_NUMBER) {
|
||||||
|
// remove last one
|
||||||
|
recentEmojis.splice(MAX_EMOJI_NUMBER - 1)
|
||||||
|
}
|
||||||
|
emojiSectionsRepeater.itemAt(0).allEmojis = recentEmojis
|
||||||
|
appSettings.recentEmojis = recentEmojis
|
||||||
|
|
||||||
|
popup.emojiSelected(Emoji.parse(encodedIcon, "26x26") + ' ', true) // Adding a space because otherwise, some emojis would fuse since emoji is just a string
|
||||||
|
popup.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
var categoryNames = {"recent": 0}
|
||||||
|
var newCategories = [[]]
|
||||||
|
|
||||||
|
EmojiJSON.emoji_json.forEach(function (emoji) {
|
||||||
|
if (!categoryNames[emoji.category] && categoryNames[emoji.category] !== 0) {
|
||||||
|
categoryNames[emoji.category] = newCategories.length
|
||||||
|
newCategories.push([])
|
||||||
|
}
|
||||||
|
newCategories[categoryNames[emoji.category]].push(Object.assign({}, emoji, {filename: emoji.unicode + '.png'}))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (newCategories[categoryNames.recent].length === 0) {
|
||||||
|
newCategories[categoryNames.recent].push({
|
||||||
|
category: "recent",
|
||||||
|
empty: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
categories = newCategories
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: applicationWindow
|
||||||
|
onSettingsLoaded: {
|
||||||
|
// Add recent
|
||||||
|
if (!appSettings.recentEmojis || !appSettings.recentEmojis.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emojiSectionsRepeater.itemAt(0).allEmojis = appSettings.recentEmojis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
searchBox.forceActiveFocus(Qt.MouseFocusReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property int headerMargin: 8
|
||||||
|
|
||||||
|
id: emojiHeader
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: searchBox.height + emojiHeader.headerMargin
|
||||||
|
|
||||||
|
SearchBox {
|
||||||
|
id: searchBox
|
||||||
|
anchors.right: skinToneEmoji.left
|
||||||
|
anchors.rightMargin: emojiHeader.headerMargin
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: emojiHeader.headerMargin
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: emojiHeader.headerMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
SVGImage {
|
||||||
|
id: skinToneEmoji
|
||||||
|
width: 22
|
||||||
|
height: 22
|
||||||
|
anchors.verticalCenter: searchBox.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: emojiHeader.headerMargin
|
||||||
|
source: "../../../../imports/twemoji/26x26/1f590.png"
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: function () {
|
||||||
|
console.log('Change skin tone')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
property ScrollBar vScrollBar: ScrollBar.vertical
|
||||||
|
property var categrorySectionHeightRatios: []
|
||||||
|
property int activeCategory: 0
|
||||||
|
|
||||||
|
id: scrollView
|
||||||
|
topPadding: Style.current.smallPadding
|
||||||
|
leftPadding: Style.current.smallPadding
|
||||||
|
rightPadding: Style.current.smallPadding / 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.rightMargin: Style.current.smallPadding / 2
|
||||||
|
Layout.topMargin: Style.current.smallPadding
|
||||||
|
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||||
|
Layout.preferredHeight: 400 - Style.current.smallPadding - emojiHeader.height
|
||||||
|
clip: true
|
||||||
|
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||||
|
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||||
|
|
||||||
|
ScrollBar.vertical.onPositionChanged: function () {
|
||||||
|
if (vScrollBar.position < categrorySectionHeightRatios[scrollView.activeCategory - 1]) {
|
||||||
|
scrollView.activeCategory--
|
||||||
|
} else if (vScrollBar.position > categrorySectionHeightRatios[scrollView.activeCategory]) {
|
||||||
|
scrollView.activeCategory++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToCategory(category) {
|
||||||
|
if (category === 0) {
|
||||||
|
return vScrollBar.setPosition(0)
|
||||||
|
}
|
||||||
|
vScrollBar.setPosition(categrorySectionHeightRatios[category - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight: {
|
||||||
|
var totalHeight = 0
|
||||||
|
var categoryHeights = []
|
||||||
|
for (let i = 0; i < emojiSectionsRepeater.count; i++) {
|
||||||
|
totalHeight += emojiSectionsRepeater.itemAt(i).height + Style.current.padding
|
||||||
|
categoryHeights.push(totalHeight)
|
||||||
|
}
|
||||||
|
var ratios = []
|
||||||
|
categoryHeights.forEach(function (catHeight) {
|
||||||
|
ratios.push(catHeight / totalHeight)
|
||||||
|
})
|
||||||
|
|
||||||
|
categrorySectionHeightRatios = ratios
|
||||||
|
return totalHeight + Style.current.padding
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: emojiSectionsRepeater
|
||||||
|
model: popup.categories
|
||||||
|
|
||||||
|
StatusEmojiSection {
|
||||||
|
searchString: popup.searchString
|
||||||
|
addEmoji: popup.addEmoji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 40
|
||||||
|
leftPadding: Style.current.smallPadding / 2
|
||||||
|
rightPadding: Style.current.smallPadding / 2
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: EmojiJSON.emojiCategories
|
||||||
|
|
||||||
|
StatusEmojiCategoryButton {
|
||||||
|
source: `../../app/img/emojiCategories/${modelData}.svg`
|
||||||
|
active: index === scrollView.activeCategory
|
||||||
|
changeCategory: function () {
|
||||||
|
scrollView.activeCategory = index
|
||||||
|
scrollView.scrollToCategory(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*##^##
|
||||||
|
Designer {
|
||||||
|
D{i:0;formeditorColor:"#ffffff";height:440;width:360}
|
||||||
|
}
|
||||||
|
##^##*/
|
|
@ -0,0 +1,108 @@
|
||||||
|
import QtQuick 2.13
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import "../../imports"
|
||||||
|
import "../../shared"
|
||||||
|
|
||||||
|
|
||||||
|
Item {
|
||||||
|
property string searchString: ""
|
||||||
|
property string searchStringLowercase: searchString.toLowerCase()
|
||||||
|
property int imageWidth: 26
|
||||||
|
property int imageMargin: 4
|
||||||
|
property var emojis: []
|
||||||
|
property var allEmojis: modelData
|
||||||
|
property var addEmoji: function () {}
|
||||||
|
|
||||||
|
id: emojiSection
|
||||||
|
visible: emojis.length > 0 || !!(modelData && modelData.length && modelData[0].empty && searchString === "")
|
||||||
|
|
||||||
|
anchors.top: index === 0 ? parent.top : parent.children[index - 1].bottom
|
||||||
|
anchors.topMargin: index === 0 ? 0 : Style.current.padding
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
// childrenRect caused a binding loop here
|
||||||
|
height: this.visible ? emojiGrid.height + categoryText.height + noRecentText.height + Style.current.padding : 0
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: categoryText
|
||||||
|
text: modelData && modelData.length ? modelData[0].category.toUpperCase() : ""
|
||||||
|
color: Style.current.darkGrey
|
||||||
|
font.pixelSize: 13
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: noRecentText
|
||||||
|
visible: !!(allEmojis && allEmojis.length && allEmojis[0].empty)
|
||||||
|
//% "No recent emojis"
|
||||||
|
text: qsTrId("no-recent-emojis")
|
||||||
|
color: Style.current.darkGrey
|
||||||
|
font.pixelSize: 10
|
||||||
|
anchors.top: categoryText.bottom
|
||||||
|
anchors.topMargin: Style.current.smallPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchStringLowercaseChanged: {
|
||||||
|
if (emojiSection.searchStringLowercase === "") {
|
||||||
|
this.emojis = modelData
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.emojis = modelData.filter(function (emoji) {
|
||||||
|
return emoji.name.includes(emojiSection.searchStringLowercase) ||
|
||||||
|
emoji.shortname.includes(emojiSection.searchStringLowercase) ||
|
||||||
|
emoji.aliases.some(a => a.includes(emojiSection.searchStringLowercase))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onAllEmojisChanged: {
|
||||||
|
if (this.allEmojis[0].empty) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.emojis = this.allEmojis
|
||||||
|
}
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
id: emojiGrid
|
||||||
|
anchors.top: categoryText.bottom
|
||||||
|
anchors.topMargin: Style.current.smallPadding
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
visible: count > 0
|
||||||
|
cellWidth: emojiSection.imageWidth + emojiSection.imageMargin * 2
|
||||||
|
cellHeight: emojiSection.imageWidth + emojiSection.imageMargin * 2
|
||||||
|
model: emojiSection.emojis
|
||||||
|
focus: true
|
||||||
|
clip: true
|
||||||
|
interactive: false
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: emojiContainer
|
||||||
|
width: emojiGrid.cellWidth
|
||||||
|
height: emojiGrid.cellHeight
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: emojiSection.imageMargin
|
||||||
|
anchors.leftMargin: emojiSection.imageMargin
|
||||||
|
|
||||||
|
SVGImage {
|
||||||
|
width: emojiSection.imageWidth
|
||||||
|
height: emojiSection.imageWidth
|
||||||
|
source: "../../imports/twemoji/26x26/" + modelData.filename
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
emojiSection.addEmoji(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*##^##
|
||||||
|
Designer {
|
||||||
|
D{i:0;formeditorColor:"#ffffff";height:440;width:360}
|
||||||
|
}
|
||||||
|
##^##*/
|
File diff suppressed because it is too large
Load Diff
|
@ -3,6 +3,9 @@ StatusChatCommandButton 1.0 StatusChatCommandButton.qml
|
||||||
StatusChatCommandPopup 1.0 StatusChatCommandPopup.qml
|
StatusChatCommandPopup 1.0 StatusChatCommandPopup.qml
|
||||||
StatusChatInfo 1.0 StatusChatInfo.qml
|
StatusChatInfo 1.0 StatusChatInfo.qml
|
||||||
StatusChatInfoButton 1.0 StatusChatInfoButton.qml
|
StatusChatInfoButton 1.0 StatusChatInfoButton.qml
|
||||||
|
StatusEmojiCategoryButton 1.0 StatusEmojiCategoryButton.qml
|
||||||
|
StatusEmojiPopup 1.0 StatusEmojiPopup.qml
|
||||||
|
StatusEmojiSection 1.0 StatusEmojiSection.qml
|
||||||
StatusIconButton 1.0 StatusIconButton.qml
|
StatusIconButton 1.0 StatusIconButton.qml
|
||||||
StatusImageIdenticon 1.0 StatusImageIdenticon.qml
|
StatusImageIdenticon 1.0 StatusImageIdenticon.qml
|
||||||
StatusLetterIdenticon 1.0 StatusLetterIdenticon.qml
|
StatusLetterIdenticon 1.0 StatusLetterIdenticon.qml
|
||||||
|
|
Loading…
Reference in New Issue