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
|
||||
StatusChatInfo 1.0 StatusChatInfo.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
|
||||
StatusImageIdenticon 1.0 StatusImageIdenticon.qml
|
||||
StatusLetterIdenticon 1.0 StatusLetterIdenticon.qml
|
||||
|
|
Loading…
Reference in New Issue