status-desktop/ui/imports/shared/status/StatusEmojiPopup.qml

271 lines
9.4 KiB
QML

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import utils 1.0
import shared.controls 1.0
import SortFilterProxyModel 0.2
StatusDropdown {
id: root
required property StatusEmojiModel emojiModel
required property var settings
property alias searchString: searchBox.text
property string emojiSize: ""
signal emojiSelected(string emoji, bool atCu)
modal: false
width: 360
padding: 0
function addEmoji(hexcode) {
const extensionIndex = hexcode.lastIndexOf('.');
let iconCodePoint = hexcode
if (extensionIndex > -1) {
iconCodePoint = iconCodePoint.substring(0, extensionIndex)
}
const encodedIcon = StatusQUtils.Emoji.getEmojiCodepoint(iconCodePoint)
root.emojiModel.addRecentEmoji(hexcode)
// Adding a space because otherwise, some emojis would fuse since emoji is just a string
root.emojiSelected(StatusQUtils.Emoji.parse(encodedIcon, root.emojiSize || undefined) + ' ', true)
root.close()
}
Component.onCompleted: {
root.emojiModel.recentEmojis = settings.recentEmojis
}
onOpened: {
searchBox.input.edit.forceActiveFocus()
emojiGrid.positionViewAtBeginning()
}
onClosed: {
const recent = root.emojiModel.recentEmojis
if (recent.length)
settings.recentEmojis = recent
searchBox.text = ""
root.emojiSize = ""
skinToneEmoji.expandSkinColorOptions = false
}
QtObject {
id: d
readonly property string searchStringLowercase: searchBox.text.toLowerCase()
readonly property int headerMargin: 8
readonly property int imageWidth: 26
readonly property int imageMargin: 4
readonly property var filteredModel: SortFilterProxyModel {
sourceModel: root.emojiModel
filters: [
FastExpressionFilter {
expression: {
if (model.category === root.emojiModel.recentCategoryName) // don't show/duplicate recents when searching
return false
d.searchStringLowercase
return model.name.toLowerCase().includes(d.searchStringLowercase) ||
model.shortname.toLowerCase().includes(d.searchStringLowercase) ||
model.aliases.some(a => a.includes(d.searchStringLowercase)) ||
model.keywords.some(k => k.includes(d.searchStringLowercase))
}
expectedRoles: ["name", "shortname", "aliases", "keywords", "category"]
enabled: !!d.searchStringLowercase
},
AnyOf {
ValueFilter {
roleName: "skinColor"
value: ""
}
ValueFilter {
roleName: "skinColor"
value: root.emojiModel.baseSkinColorName
}
enabled: settings.skinColor === ""
},
AnyOf {
ValueFilter {
roleName: "skinColor"
value: ""
}
ValueFilter {
roleName: "skinColor"
value: settings.skinColor
}
enabled: settings.skinColor !== ""
}
]
sorters: RoleSorter {
roleName: "emoji_order"
}
}
}
contentItem: ColumnLayout {
spacing: 0
Item {
Layout.fillWidth: true
Layout.preferredHeight: searchBox.height + d.headerMargin
SearchBox {
input.edit.objectName: "StatusEmojiPopup_searchBox"
id: searchBox
anchors.right: skinToneEmoji.left
anchors.rightMargin: d.headerMargin
anchors.top: parent.top
anchors.topMargin: d.headerMargin
anchors.left: parent.left
anchors.leftMargin: d.headerMargin
minimumHeight: 36
maximumHeight: 36
input.topPadding: 0
input.bottomPadding: 0
}
Row {
id: skinToneEmoji
property bool expandSkinColorOptions: false
clip: true
width: expandSkinColorOptions ? (22 * skinColorEmojiRepeater.count) : 22
height: 22
opacity: expandSkinColorOptions ? 1.0 : 0.0
Behavior on width { NumberAnimation { duration: 400 } }
Behavior on opacity { NumberAnimation { duration: 200 } }
visible: (opacity > 0.1)
anchors.verticalCenter: searchBox.verticalCenter
anchors.right: parent.right
anchors.rightMargin: d.headerMargin
Repeater {
id: skinColorEmojiRepeater
model: ["1f590-1f3fb", "1f590-1f3fc", "1f590-1f3fd", "1f590-1f3fe", "1f590-1f3ff", "1f590"]
delegate: StatusEmoji {
width: 22
height: 22
emojiId: modelData
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
settings.skinColor = (index === 5) ? "" : modelData.split("-")[1];
skinToneEmoji.expandSkinColorOptions = false;
}
}
}
}
}
StatusEmoji {
width: 22
height: 22
anchors.verticalCenter: searchBox.verticalCenter
anchors.right: parent.right
anchors.rightMargin: d.headerMargin
visible: !skinToneEmoji.expandSkinColorOptions
emojiId: "1f590" + ((settings.skinColor !== "" && visible) ? ("-" + settings.skinColor) : "")
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
skinToneEmoji.expandSkinColorOptions = true;
}
}
}
}
StatusBaseText {
Layout.fillWidth: true
Layout.leftMargin: d.headerMargin
Layout.topMargin: 8
Layout.bottomMargin: 4
font.weight: Font.Medium
color: Theme.palette.secondaryText
font.pixelSize: Theme.additionalTextSize
text: d.searchStringLowercase ? (d.filteredModel.count ? qsTr("Search Results") : qsTr("No results found"))
: emojiGrid.currentCategory
font.capitalization: Font.AllUppercase
}
StatusGridView {
id: emojiGrid
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: d.headerMargin
readonly property string currentCategory: {
const item = emojiGrid.itemAt(contentX, contentY + (contentY !== originY ? cellHeight : 0)) // taking the 2nd row; 1st might be split between 2 categories
return !!item ? item.category : root.emojiModel.recentCategoryName
}
readonly property string currentCategoryIndex: root.emojiModel.categories.indexOf(currentCategory)
model: d.filteredModel
cellWidth: d.imageWidth + d.imageMargin * 2
cellHeight: d.imageWidth + d.imageMargin * 2
ScrollBar.vertical: StatusScrollBar {
policy: ScrollBar.AsNeeded
}
delegate: Item {
readonly property string category: model.category
width: emojiGrid.cellWidth
height: emojiGrid.cellHeight
StatusEmoji {
objectName: "statusEmoji_" + model.shortname.replace(/:/g, "")
anchors.centerIn: parent
width: d.imageWidth
height: d.imageWidth
emojiId: model.unicode
MouseArea {
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: root.addEmoji(model.unicode)
}
}
}
}
Row {
Layout.fillWidth: true
height: 40
spacing: 0
Repeater {
model: StatusQUtils.Emoji.emojiJSON.emojiCategories
StatusTabBarIconButton {
icon.name: modelData
highlighted: !!d.searchStringLowercase ? index === 0 : index == emojiGrid.currentCategoryIndex
onClicked: {
const offset = d.filteredModel.mapFromSource(root.emojiModel.getCategoryOffset(index))
emojiGrid.positionViewAtIndex(offset, GridView.Beginning)
}
}
}
}
}
}