Five plugins (menu, launcher, clipboard, emojis, polkit) each looked up OMARCHY_MENU_FONT with the same env-or-monospace fallback. Hoist the resolution into Style so plugins read one source of truth, and so the fallback follows the real fontconfig family Style already tracks instead of the literal string "monospace".
354 lines
11 KiB
QML
354 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()
|
|
var escEmoji = emoji.replace(/'/g, "'\\''")
|
|
Quickshell.execDetached(["bash", "-lc", "wl-copy '" + escEmoji + "'; sleep 0.15; wtype '" + escEmoji + "' 2>/dev/null || true"])
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|