mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-05 20:28:25 +02:00
353 lines
11 KiB
QML
353 lines
11 KiB
QML
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import qs.Commons
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
|
property var shell: null
|
|
property var manifest: null
|
|
|
|
property bool opened: false
|
|
property string filterText: ""
|
|
property int selectedIndex: 0
|
|
property bool cursorActive: false
|
|
property var emojis: []
|
|
property var filteredEmojis: []
|
|
|
|
// Shares the [menu] surface tokens — themes that style the menu also
|
|
// style emojis. Selected-cell colors composed in the
|
|
// singleton so consumers drop them straight into Rectangle bindings.
|
|
property color background: Color.menu.background
|
|
property color foreground: Color.menu.text
|
|
property color border: Color.menu.border
|
|
property color scrim: Color.menu.scrim
|
|
property color selectedBackground: Color.menu.selectedBackground
|
|
property color selectedText: Color.menu.selectedText
|
|
property color selectedBorder: Color.menu.selectedBorder
|
|
readonly property int cornerRadius: Style.cornerRadius
|
|
property string fontFamily: Style.font.menuFamily
|
|
property int contentMargin: Style.spacing.panelPadding
|
|
property int headerHeight: Math.max(Style.space(34), Style.font.title + Style.spacing.controlPaddingY * 2)
|
|
property int contentSpacing: Style.spacing.md
|
|
property int cardWidth: Math.min(Style.space(400), panel.width - Style.gapsOut * 2)
|
|
property int cardHeight: Math.min(Style.space(500), panel.height - Style.gapsOut * 2)
|
|
|
|
property int cellWidth: Math.max(Style.space(44), Style.font.display + Style.spacing.md)
|
|
property int cellHeight: Math.max(Style.space(44), Style.font.display + Style.spacing.md)
|
|
property int columns: Math.floor((cardWidth - contentMargin * 2) / cellWidth)
|
|
|
|
function open(payloadJson) {
|
|
root.opened = true
|
|
root.filterText = ""
|
|
root.selectedIndex = 0
|
|
root.cursorActive = false
|
|
root.rebuildDisplay()
|
|
Qt.callLater(function() { keyCatcher.forceActiveFocus() })
|
|
}
|
|
|
|
function close() {
|
|
root.opened = false
|
|
}
|
|
|
|
function dismiss() {
|
|
root.opened = false
|
|
if (root.shell && typeof root.shell.hide === "function")
|
|
root.shell.hide((root.manifest && root.manifest.id) || "omarchy.emojis")
|
|
}
|
|
|
|
function toggle() {
|
|
if (root.opened) root.dismiss()
|
|
else root.open("{}")
|
|
}
|
|
|
|
function loadEmojis(raw) {
|
|
try {
|
|
var data = JSON.parse(raw)
|
|
root.emojis = data || []
|
|
} catch (e) {
|
|
console.warn("Failed to parse emojis.json:", e)
|
|
root.emojis = []
|
|
}
|
|
if (root.opened) root.rebuildDisplay()
|
|
}
|
|
|
|
function rebuildDisplay() {
|
|
var query = root.filterText.trim().toLowerCase()
|
|
var out = []
|
|
for (var i = 0; i < root.emojis.length; i++) {
|
|
var item = root.emojis[i]
|
|
if (!query || item.k.indexOf(query) >= 0) {
|
|
out.push(item)
|
|
if (out.length >= 1000) break // limit to keep it fast
|
|
}
|
|
}
|
|
root.filteredEmojis = out
|
|
|
|
displayModel.clear()
|
|
for (var j = 0; j < out.length; j++) {
|
|
displayModel.append({ emoji: out[j].e, index: j })
|
|
}
|
|
|
|
if (displayModel.count === 0) selectedIndex = 0
|
|
else if (selectedIndex >= displayModel.count) selectedIndex = displayModel.count - 1
|
|
else if (selectedIndex < 0) selectedIndex = 0
|
|
|
|
Qt.callLater(function() {
|
|
if (displayModel.count > 0) resultGrid.positionViewAtIndex(root.selectedIndex, GridView.Contain)
|
|
})
|
|
}
|
|
|
|
function select(delta) {
|
|
if (displayModel.count === 0) return
|
|
if (!cursorActive) {
|
|
cursorActive = true
|
|
selectedIndex = delta < 0 ? displayModel.count - 1 : 0
|
|
} else {
|
|
selectedIndex = (selectedIndex + delta + displayModel.count) % displayModel.count
|
|
}
|
|
resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain)
|
|
}
|
|
|
|
function selectRow(delta) {
|
|
if (displayModel.count === 0) return
|
|
if (!cursorActive) {
|
|
cursorActive = true
|
|
selectedIndex = delta < 0 ? displayModel.count - 1 : 0
|
|
resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain)
|
|
return
|
|
}
|
|
var newIndex = selectedIndex + delta * columns
|
|
if (newIndex < 0) newIndex = 0
|
|
if (newIndex >= displayModel.count) newIndex = displayModel.count - 1
|
|
selectedIndex = newIndex
|
|
resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain)
|
|
}
|
|
|
|
function selectPage(delta) {
|
|
if (displayModel.count === 0) return
|
|
if (!cursorActive) {
|
|
cursorActive = true
|
|
selectedIndex = delta < 0 ? displayModel.count - 1 : 0
|
|
resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain)
|
|
return
|
|
}
|
|
var visibleRows = Math.max(1, Math.floor(resultGrid.height / cellHeight))
|
|
var newIndex = selectedIndex + delta * columns * visibleRows
|
|
if (newIndex < 0) newIndex = 0
|
|
if (newIndex >= displayModel.count) newIndex = displayModel.count - 1
|
|
selectedIndex = newIndex
|
|
resultGrid.positionViewAtIndex(selectedIndex, GridView.Contain)
|
|
}
|
|
|
|
function setFilter(nextFilter) {
|
|
root.filterText = nextFilter
|
|
root.selectedIndex = 0
|
|
root.cursorActive = false
|
|
root.rebuildDisplay()
|
|
}
|
|
|
|
function activateIndex(index) {
|
|
if (index < 0 || index >= displayModel.count) return
|
|
var row = displayModel.get(index)
|
|
root.applySelected(row.emoji)
|
|
}
|
|
|
|
function applySelected(emoji) {
|
|
if (!emoji) return
|
|
root.dismiss()
|
|
Quickshell.execDetached([root.omarchyPath + "/bin/omarchy-clipboard-paste-text", emoji])
|
|
}
|
|
ListModel { id: displayModel }
|
|
|
|
FileView {
|
|
path: root.omarchyPath + "/shell/plugins/emojis/emojis.json"
|
|
onLoaded: root.loadEmojis(text())
|
|
}
|
|
PanelWindow {
|
|
id: panel
|
|
visible: root.opened
|
|
anchors { top: true; bottom: true; left: true; right: true }
|
|
color: "transparent"
|
|
WlrLayershell.namespace: "omarchy-emojis"
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
|
exclusionMode: ExclusionMode.Ignore
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: root.scrim
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: root.dismiss()
|
|
}
|
|
|
|
Rectangle {
|
|
id: card
|
|
width: root.cardWidth
|
|
height: root.cardHeight
|
|
radius: root.cornerRadius
|
|
anchors.centerIn: parent
|
|
color: root.background
|
|
border.color: root.border
|
|
border.width: Math.max(1, Style.space(2))
|
|
|
|
MouseArea { anchors.fill: parent; onClicked: {} }
|
|
|
|
Item {
|
|
id: keyCatcher
|
|
anchors.fill: parent
|
|
focus: true
|
|
|
|
Keys.priority: Keys.BeforeItem
|
|
Keys.onPressed: function(event) {
|
|
if (event.key === Qt.Key_Escape) {
|
|
if (root.filterText) root.setFilter("")
|
|
else root.dismiss()
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Backspace) {
|
|
if (root.filterText.length > 0) root.setFilter(root.filterText.slice(0, -1))
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Left) {
|
|
root.select(-1)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Right) {
|
|
root.select(1)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Up) {
|
|
root.selectRow(-1)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Down) {
|
|
root.selectRow(1)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_PageUp) {
|
|
root.selectPage(-1)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_PageDown) {
|
|
root.selectPage(1)
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
if (root.cursorActive) root.activateIndex(root.selectedIndex)
|
|
else if (displayModel.count > 0) root.cursorActive = true
|
|
event.accepted = true
|
|
} else if (event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127) {
|
|
root.setFilter(root.filterText + event.text)
|
|
event.accepted = true
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
anchors.margins: root.contentMargin
|
|
spacing: root.contentSpacing
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: root.headerHeight
|
|
radius: root.cornerRadius
|
|
color: "transparent"
|
|
border.width: 0
|
|
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.filterText || "Search emojis…"
|
|
color: root.foreground
|
|
opacity: root.filterText ? 1 : 0.58
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.heading
|
|
elide: Text.ElideRight
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: parent.height - root.headerHeight - root.contentSpacing
|
|
|
|
GridView {
|
|
id: resultGrid
|
|
anchors.fill: parent
|
|
model: displayModel
|
|
clip: true
|
|
cellWidth: root.cellWidth
|
|
cellHeight: root.cellHeight
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
delegate: Rectangle {
|
|
required property int index
|
|
required property string emoji
|
|
|
|
readonly property bool hasCursor: root.cursorActive && index === root.selectedIndex
|
|
|
|
width: root.cellWidth
|
|
height: root.cellHeight
|
|
radius: root.cornerRadius
|
|
color: hasCursor ? root.selectedBackground : "transparent"
|
|
border.color: hasCursor ? root.selectedBorder : "transparent"
|
|
border.width: (hasCursor && root.selectedBorder.a > 0) ? Style.hoverBorderWidth : 0
|
|
|
|
Text {
|
|
text: parent.emoji
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.display
|
|
anchors.centerIn: parent
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
|
|
MouseArea {
|
|
id: mouseArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onContainsMouseChanged: if (containsMouse) {
|
|
root.cursorActive = true
|
|
root.selectedIndex = index
|
|
}
|
|
onClicked: {
|
|
root.cursorActive = true
|
|
root.selectedIndex = index
|
|
root.activateIndex(index)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
anchors.centerIn: parent
|
|
spacing: Style.space(8)
|
|
visible: displayModel.count === 0
|
|
|
|
Text {
|
|
text: ""
|
|
color: root.selectedText
|
|
opacity: 0.8
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.displayLarge
|
|
horizontalAlignment: Text.AlignHCenter
|
|
width: parent.width
|
|
}
|
|
|
|
Text {
|
|
text: "No matches for “" + root.filterText + "”"
|
|
color: root.foreground
|
|
opacity: 0.7
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.title
|
|
horizontalAlignment: Text.AlignHCenter
|
|
width: parent.width
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|