Extract clock and tray bar widgets
This commit is contained in:
+2
-495
@@ -1,8 +1,6 @@
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
@@ -54,7 +52,6 @@ Item {
|
||||
Behavior on foreground { ColorAnimation { duration: 420; easing.type: Easing.InOutCubic } }
|
||||
Behavior on background { ColorAnimation { duration: 420; easing.type: Easing.InOutCubic } }
|
||||
Behavior on urgent { ColorAnimation { duration: 420; easing.type: Easing.InOutCubic } }
|
||||
property bool clockAlt: false
|
||||
property var tooltipTarget: null
|
||||
property var pendingTooltipTarget: null
|
||||
property string tooltipText: ""
|
||||
@@ -214,14 +211,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function builtinModuleComponent(name) {
|
||||
switch (String(name)) {
|
||||
case "clock": return clockModuleComponent
|
||||
case "tray": return trayModuleComponent
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
function expandPath(path) {
|
||||
var value = String(path || "")
|
||||
if (value === "") return ""
|
||||
@@ -322,48 +311,6 @@ Item {
|
||||
clearTooltip()
|
||||
}
|
||||
|
||||
function trayIconSource(icon) {
|
||||
var value = String(icon || "")
|
||||
var marker = "?path="
|
||||
var markerIndex = value.indexOf(marker)
|
||||
if (markerIndex === -1) return value
|
||||
|
||||
var name = value.substring(0, markerIndex).split("/").pop()
|
||||
var iconPath = value.substring(markerIndex + marker.length).split("&")[0]
|
||||
return Util.fileUrl(iconPath + "/hicolor/16x16/status/" + name + ".png")
|
||||
}
|
||||
|
||||
function trayTooltip(item) {
|
||||
return item.tooltipTitle || item.title || item.id || ""
|
||||
}
|
||||
|
||||
function clockEntry() {
|
||||
var serial = barConfigSerial
|
||||
var sections = ["left", "center", "right"]
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var entries = layoutEntries(sections[i])
|
||||
var idx = entryIndex(entries, "clock")
|
||||
if (idx !== -1) return entries[idx]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function clockText() {
|
||||
var entry = clockEntry()
|
||||
if (clockAlt)
|
||||
return Qt.formatDateTime(systemClock.date, moduleString(entry, "formatAlt", "dd MMMM 'W'ww yyyy"))
|
||||
|
||||
if (vertical)
|
||||
return Qt.formatDateTime(systemClock.date, moduleString(entry, "verticalFormat", "HH\n—\nmm"))
|
||||
|
||||
return Qt.formatDateTime(systemClock.date, moduleString(entry, "format", "dddd HH:mm"))
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: systemClock
|
||||
precision: SystemClock.Minutes
|
||||
}
|
||||
|
||||
Process { id: launcher }
|
||||
|
||||
Timer {
|
||||
@@ -543,8 +490,6 @@ Item {
|
||||
}
|
||||
|
||||
Component { id: emptyModuleComponent; Item { implicitWidth: 0; implicitHeight: 0; visible: false } }
|
||||
Component { id: clockModuleComponent; ClockModule {} }
|
||||
Component { id: trayModuleComponent; TrayModule {} }
|
||||
|
||||
function findCenterAnchorEntry() {
|
||||
var entries = root.layoutEntries("center")
|
||||
@@ -717,13 +662,12 @@ Item {
|
||||
readonly property string moduleName: root.entryId(entry)
|
||||
readonly property var moduleSettings: root.entrySettings(entry)
|
||||
readonly property string customType: root.customModuleType(entry)
|
||||
readonly property var builtinComponent: customType ? null : root.builtinModuleComponent(moduleName)
|
||||
// Re-evaluate when the registry mutates (Component reference changes,
|
||||
// plugin enabled/disabled, etc.). Reading the `widgets` property creates
|
||||
// the binding dependency — the wrapped function call alone wouldn't.
|
||||
readonly property var registryComponent: {
|
||||
var w = root.barWidgetRegistry.widgets
|
||||
if (customType || builtinComponent) return null
|
||||
if (customType) return null
|
||||
var registryName = root.canonicalWidgetId(moduleName)
|
||||
return w[registryName] ? w[registryName].component : null
|
||||
}
|
||||
@@ -744,7 +688,7 @@ Item {
|
||||
Loader {
|
||||
id: componentLoader
|
||||
active: !slot.qmlCustom && !slot.registered
|
||||
sourceComponent: slot.builtinComponent || (slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent)
|
||||
sourceComponent: slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent
|
||||
anchors.fill: parent
|
||||
onLoaded: {
|
||||
slot.injectProps()
|
||||
@@ -913,441 +857,4 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
component ClockModule: ModuleButton {
|
||||
text: root.clockText()
|
||||
horizontalMargin: 8.75
|
||||
verticalPadding: 8.75
|
||||
onPressed: function(button) {
|
||||
if (button === Qt.RightButton) root.run("omarchy-launch-floating-terminal-with-presentation omarchy-tz-select")
|
||||
else root.clockAlt = !root.clockAlt
|
||||
}
|
||||
}
|
||||
|
||||
component TrayModule: Item {
|
||||
id: trayRoot
|
||||
|
||||
property bool expanded: false
|
||||
property bool managePopupOpen: false
|
||||
function close() { managePopupOpen = false }
|
||||
|
||||
// Re-resolve the tray's own entry settings whenever the bar layout reloads.
|
||||
readonly property var trayEntry: {
|
||||
var serial = root.barConfigSerial
|
||||
var sections = ["left", "center", "right"]
|
||||
for (var s = 0; s < sections.length; s++) {
|
||||
var arr = root.layoutEntries(sections[s])
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
if (root.entryId(arr[i]) === "tray") return arr[i]
|
||||
}
|
||||
}
|
||||
return ({})
|
||||
}
|
||||
readonly property var pinnedIds: Array.isArray(trayEntry.pinned) ? trayEntry.pinned : []
|
||||
readonly property var hiddenIds: Array.isArray(trayEntry.hidden) ? trayEntry.hidden : []
|
||||
|
||||
function classifyItem(item) {
|
||||
var iid = String(item.id || "")
|
||||
if (hiddenIds.indexOf(iid) !== -1) return "hidden"
|
||||
if (pinnedIds.indexOf(iid) !== -1) return "pinned"
|
||||
return "drawer"
|
||||
}
|
||||
|
||||
function bucket(category) {
|
||||
var values = SystemTray.items.values
|
||||
var result = []
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var item = values[i]
|
||||
if (item.status === Status.Passive) continue
|
||||
if (category === "all") { result.push(item); continue }
|
||||
if (classifyItem(item) === category) result.push(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
readonly property var pinnedItems: bucket("pinned")
|
||||
readonly property var drawerItems: bucket("drawer")
|
||||
readonly property var allItems: bucket("all")
|
||||
readonly property int drawerCount: drawerItems.length
|
||||
readonly property int drawerExtent: drawerCount > 0 ? drawerCount * 16 + (drawerCount - 1) * 17 : 0
|
||||
// Match Waybar's group/tray-expander drawer transition-duration.
|
||||
readonly property int animationDuration: 600
|
||||
property real revealProgress: expanded ? 1 : 0
|
||||
readonly property real revealExtent: drawerExtent * revealProgress
|
||||
|
||||
Behavior on revealProgress {
|
||||
NumberAnimation { duration: trayRoot.animationDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
function persistTrayState(pinned, hidden) {
|
||||
if (!root.shell || typeof root.shell.updateEntryInline !== "function") return
|
||||
root.shell.updateEntryInline("tray", { id: "tray", pinned: pinned, hidden: hidden })
|
||||
}
|
||||
|
||||
function togglePin(iid) {
|
||||
var p = pinnedIds.slice(), h = hiddenIds.slice()
|
||||
var idx = p.indexOf(iid)
|
||||
if (idx !== -1) p.splice(idx, 1)
|
||||
else {
|
||||
p.push(iid)
|
||||
var hi = h.indexOf(iid); if (hi !== -1) h.splice(hi, 1)
|
||||
}
|
||||
persistTrayState(p, h)
|
||||
}
|
||||
|
||||
function toggleHide(iid) {
|
||||
var p = pinnedIds.slice(), h = hiddenIds.slice()
|
||||
var idx = h.indexOf(iid)
|
||||
if (idx !== -1) h.splice(idx, 1)
|
||||
else {
|
||||
h.push(iid)
|
||||
var pi = p.indexOf(iid); if (pi !== -1) p.splice(pi, 1)
|
||||
}
|
||||
persistTrayState(p, h)
|
||||
}
|
||||
|
||||
visible: pinnedItems.length > 0 || drawerCount > 0
|
||||
clip: false
|
||||
implicitWidth: root.vertical ? root.barSize : trayContent.implicitWidth
|
||||
implicitHeight: root.vertical ? trayContent.implicitHeight : root.barSize
|
||||
|
||||
Loader {
|
||||
id: trayContent
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.vertical ? verticalTray : horizontalTray
|
||||
}
|
||||
|
||||
Component {
|
||||
id: horizontalTray
|
||||
|
||||
Item {
|
||||
id: horizontalTrayRoot
|
||||
|
||||
readonly property int pinnedWidth: pinnedRow.implicitWidth
|
||||
readonly property int drawerBlockWidth: trayRoot.allItems.length > 0 ? expandIcon.implicitWidth + trayRoot.drawerExtent : 0
|
||||
|
||||
implicitWidth: pinnedWidth + drawerBlockWidth
|
||||
implicitHeight: root.barSize
|
||||
|
||||
// Mask out the empty area the collapsed drawer reserves for its slide-in,
|
||||
// so hovering it doesn't trigger expand and clicks pass through.
|
||||
containmentMask: QtObject {
|
||||
function contains(point: point): bool {
|
||||
if (point.y < 0 || point.y > horizontalTrayRoot.height) return false
|
||||
// Drawer reveals leftward; chevron sits at the right end when collapsed
|
||||
// and slides left as it opens. The visible region starts at the chevron.
|
||||
var chevronX = trayRoot.drawerExtent - trayRoot.revealExtent
|
||||
if (point.x >= chevronX && point.x <= horizontalTrayRoot.drawerBlockWidth) return true
|
||||
// Pinned items, placed to the right of the drawer block.
|
||||
var pinnedStart = horizontalTrayRoot.drawerBlockWidth
|
||||
return point.x >= pinnedStart && point.x <= horizontalTrayRoot.implicitWidth
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: drawerArea
|
||||
x: 0
|
||||
width: horizontalTrayRoot.drawerBlockWidth
|
||||
height: root.barSize
|
||||
visible: trayRoot.allItems.length > 0
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: trayRoot.expanded = hovered
|
||||
}
|
||||
|
||||
ModuleButton {
|
||||
id: expandIcon
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
x: trayRoot.drawerExtent - trayRoot.revealExtent
|
||||
text: ""
|
||||
horizontalMargin: 9
|
||||
verticalPadding: 6
|
||||
onPressed: function(button) {
|
||||
if (button === Qt.RightButton) trayRoot.managePopupOpen = !trayRoot.managePopupOpen
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: trayClip
|
||||
x: expandIcon.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: trayRoot.drawerExtent
|
||||
height: root.barSize
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: trayIcons
|
||||
x: trayRoot.drawerExtent - trayRoot.revealExtent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(17)
|
||||
layer.enabled: true
|
||||
|
||||
Repeater {
|
||||
model: trayRoot.drawerItems
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: pinnedRow
|
||||
x: drawerArea.x + horizontalTrayRoot.drawerBlockWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(17)
|
||||
leftPadding: trayRoot.pinnedItems.length > 0 && trayRoot.allItems.length > 0 ? Style.space(6) : 0
|
||||
Repeater {
|
||||
model: trayRoot.pinnedItems
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: verticalTray
|
||||
|
||||
Item {
|
||||
id: verticalTrayRoot
|
||||
|
||||
readonly property int pinnedHeight: pinnedCol.implicitHeight
|
||||
readonly property int drawerBlockHeight: trayRoot.allItems.length > 0 ? expandIcon.implicitHeight + trayRoot.drawerExtent : 0
|
||||
|
||||
implicitWidth: root.barSize
|
||||
implicitHeight: pinnedHeight + drawerBlockHeight
|
||||
|
||||
containmentMask: QtObject {
|
||||
function contains(point: point): bool {
|
||||
if (point.x < 0 || point.x > verticalTrayRoot.width) return false
|
||||
var chevronY = trayRoot.drawerExtent - trayRoot.revealExtent
|
||||
if (point.y >= chevronY && point.y <= verticalTrayRoot.drawerBlockHeight) return true
|
||||
var pinnedStart = verticalTrayRoot.drawerBlockHeight
|
||||
return point.y >= pinnedStart && point.y <= verticalTrayRoot.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: drawerArea
|
||||
y: 0
|
||||
width: root.barSize
|
||||
height: verticalTrayRoot.drawerBlockHeight
|
||||
visible: trayRoot.allItems.length > 0
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: trayRoot.expanded = hovered
|
||||
}
|
||||
|
||||
ModuleButton {
|
||||
id: expandIcon
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
y: trayRoot.drawerExtent - trayRoot.revealExtent
|
||||
text: ""
|
||||
textRotation: 90
|
||||
horizontalMargin: 9
|
||||
verticalPadding: 6
|
||||
onPressed: function(button) {
|
||||
if (button === Qt.RightButton) trayRoot.managePopupOpen = !trayRoot.managePopupOpen
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: trayClip
|
||||
y: expandIcon.height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: root.barSize
|
||||
height: trayRoot.drawerExtent
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: trayIcons
|
||||
y: trayRoot.drawerExtent - trayRoot.revealExtent
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Style.space(17)
|
||||
layer.enabled: true
|
||||
|
||||
Repeater {
|
||||
model: trayRoot.drawerItems
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: pinnedCol
|
||||
y: drawerArea.y + verticalTrayRoot.drawerBlockHeight
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Style.space(17)
|
||||
topPadding: trayRoot.pinnedItems.length > 0 && trayRoot.allItems.length > 0 ? Style.space(6) : 0
|
||||
Repeater {
|
||||
model: trayRoot.pinnedItems
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopupCard {
|
||||
id: managePopup
|
||||
anchorItem: trayRoot
|
||||
owner: trayRoot
|
||||
bar: root
|
||||
open: trayRoot.managePopupOpen
|
||||
contentWidth: managePopup.fittedContentWidth(Style.space(300))
|
||||
contentHeight: managePopup.fittedContentHeight(manageColumn.implicitHeight)
|
||||
|
||||
Column {
|
||||
id: manageColumn
|
||||
anchors.fill: parent
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
text: "Tray icons"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Pinned icons stay visible. Hidden icons never show."
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: trayRoot.allItems.length === 0
|
||||
text: "No tray items reporting."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
font.italic: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: trayRoot.allItems
|
||||
delegate: Item {
|
||||
id: rowRoot
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: manageColumn.width
|
||||
implicitHeight: 28
|
||||
|
||||
readonly property string itemId: String(modelData.id || "")
|
||||
readonly property string displayName: {
|
||||
var t = String(modelData.title || "").trim()
|
||||
if (t) return t
|
||||
var tt = String(modelData.tooltipTitle || "").trim()
|
||||
if (tt) return tt
|
||||
var id = String(modelData.id || "")
|
||||
var slash = id.lastIndexOf("/")
|
||||
return slash !== -1 ? id.substring(slash + 1) : (id || "Unknown")
|
||||
}
|
||||
readonly property bool isPinned: trayRoot.pinnedIds.indexOf(itemId) !== -1
|
||||
readonly property bool isHidden: trayRoot.hiddenIds.indexOf(itemId) !== -1
|
||||
|
||||
IconImage {
|
||||
id: rowIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
implicitSize: 16
|
||||
width: 16
|
||||
height: 16
|
||||
source: root.trayIconSource(rowRoot.modelData.icon)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: rowIcon.right
|
||||
anchors.leftMargin: Style.space(10)
|
||||
anchors.right: rowHideBtn.left
|
||||
anchors.rightMargin: Style.space(8)
|
||||
text: rowRoot.displayName
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Button {
|
||||
id: rowPinBtn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
iconText: ""
|
||||
text: rowRoot.isPinned ? "Unpin" : "Pin"
|
||||
foreground: root.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 3
|
||||
iconSize: Style.font.bodySmall
|
||||
fontSize: Style.font.bodySmall
|
||||
onClicked: trayRoot.togglePin(rowRoot.itemId)
|
||||
}
|
||||
|
||||
Button {
|
||||
id: rowHideBtn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: rowPinBtn.left
|
||||
anchors.rightMargin: Style.space(6)
|
||||
iconText: ""
|
||||
text: rowRoot.isHidden ? "Show" : "Hide"
|
||||
foreground: root.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 3
|
||||
iconSize: Style.font.bodySmall
|
||||
fontSize: Style.font.bodySmall
|
||||
onClicked: trayRoot.toggleHide(rowRoot.itemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component TrayItem: Item {
|
||||
id: trayItemRoot
|
||||
|
||||
required property var modelData
|
||||
|
||||
visible: modelData.status !== Status.Passive
|
||||
implicitWidth: visible ? 16 : 0
|
||||
implicitHeight: visible ? 16 : 0
|
||||
|
||||
IconImage {
|
||||
anchors.centerIn: parent
|
||||
implicitSize: 12
|
||||
width: 12
|
||||
height: 12
|
||||
source: root.trayIconSource(trayItemRoot.modelData.icon)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: root.showTooltip(trayItemRoot, root.trayTooltip(modelData))
|
||||
onExited: root.hideTooltip(trayItemRoot)
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.RightButton && trayItemRoot.modelData.hasMenu) {
|
||||
var point = trayItemRoot.QsWindow.contentItem.mapFromItem(trayItemRoot, mouse.x, mouse.y)
|
||||
trayItemRoot.modelData.display(trayItemRoot.QsWindow.window, point.x, point.y)
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
trayItemRoot.modelData.secondaryActivate()
|
||||
} else {
|
||||
trayItemRoot.modelData.activate()
|
||||
}
|
||||
}
|
||||
onWheel: function(wheel) {
|
||||
trayItemRoot.modelData.scroll(wheel.angleDelta.y, false)
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool tooltipHovered: visible && opacity > 0 && mouseArea.containsMouse
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ QtObject {
|
||||
"networkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Network" },
|
||||
"powerPanel": { displayName: "Power", description: "Battery, power profile, and system stats", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Power" },
|
||||
"bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Bluetooth" },
|
||||
"clock": { displayName: "Clock", description: "Date / time text", category: "Time", allowMultiple: false },
|
||||
"daytime": { displayName: "Daytime", description: "Day/time label with date tooltip", category: "Time", allowMultiple: false, settingsForm: "daytimeSettings" },
|
||||
"indicators": { displayName: "Indicators", description: "Manual state indicators", category: "Status", allowMultiple: false },
|
||||
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false },
|
||||
"systemUpdate": { displayName: "System update", description: "Indicates available system updates", category: "System", allowMultiple: false },
|
||||
"systemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false },
|
||||
"tray": { displayName: "System tray", description: "Status notifier items", category: "Status", allowMultiple: false },
|
||||
"weather": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false, settingsForm: "weatherSettings" },
|
||||
"microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false },
|
||||
"activeWindow": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false },
|
||||
|
||||
@@ -56,12 +56,14 @@ Example `shell.json` (bar subtree only shown):
|
||||
|---|---|---|
|
||||
| `omarchy` | Omarchy menu launcher | left = menu · right = terminal |
|
||||
| `workspaces` | Hyprland workspace switcher | left = focus workspace |
|
||||
| `clock` | Date/time label | left = alternate format · right = timezone selector |
|
||||
| `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup |
|
||||
| `daytime` | Day/time label with date tooltip | hover = date |
|
||||
| `indicators` | Manual state indicators | left = indicator action |
|
||||
| `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND |
|
||||
| `systemUpdate` | Available update indicator | left = update |
|
||||
| `systemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal |
|
||||
| `tray` | System tray | hover = reveal drawer · right on chevron = manage |
|
||||
| `weather` | Weather icon + popup with forecast | left = popup · right = full notification |
|
||||
| `microphone` | Mic icon + scroll volume | left = mute toggle · middle = audio panel · scroll = source volume |
|
||||
|
||||
@@ -75,10 +77,6 @@ Example `shell.json` (bar subtree only shown):
|
||||
| `panels.bluetooth` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI |
|
||||
| `panels.monitor` | Brightness and laptop display controls | left = popup |
|
||||
|
||||
### Built-in base modules (in `Bar.qml`)
|
||||
|
||||
`clock`, `tray`.
|
||||
|
||||
The `indicators` widget loads individual bar indicators from `indicators/`, ordered by its `items` array in `shell.json`. Rich panels such as `powerPanel`, `networkPanel`, and `audioPanel` live in `../panels/` above.
|
||||
|
||||
## Orientation
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
|
||||
BarWidget {
|
||||
id: root
|
||||
moduleName: "clock"
|
||||
|
||||
property bool alt: false
|
||||
|
||||
function setting(name, fallback) {
|
||||
var value = settings ? settings[name] : undefined
|
||||
return value === undefined || value === null ? fallback : value
|
||||
}
|
||||
|
||||
function label() {
|
||||
if (alt) return Qt.formatDateTime(clock.date, String(setting("formatAlt", "dd MMMM 'W'ww yyyy")))
|
||||
if (bar && bar.vertical) return Qt.formatDateTime(clock.date, String(setting("verticalFormat", "HH\n—\nmm")))
|
||||
return Qt.formatDateTime(clock.date, String(setting("format", "dddd HH:mm")))
|
||||
}
|
||||
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Minutes
|
||||
}
|
||||
|
||||
WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
bar: root.bar
|
||||
text: root.label()
|
||||
horizontalMargin: 8.75
|
||||
verticalPadding: 8.75
|
||||
onPressed: function(button) {
|
||||
if (!root.bar) return
|
||||
if (button === Qt.RightButton) root.bar.run("omarchy-launch-floating-terminal-with-presentation omarchy-tz-select")
|
||||
else root.alt = !root.alt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
|
||||
BarWidget {
|
||||
id: root
|
||||
moduleName: "tray"
|
||||
|
||||
property bool expanded: false
|
||||
property bool managePopupOpen: false
|
||||
readonly property bool vertical: bar ? bar.vertical : false
|
||||
readonly property int barSize: bar ? bar.barSize : Style.bar.sizeHorizontal
|
||||
readonly property color foreground: bar ? bar.foreground : Color.foreground
|
||||
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
||||
readonly property var pinnedIds: Array.isArray(settings.pinned) ? settings.pinned : []
|
||||
readonly property var hiddenIds: Array.isArray(settings.hidden) ? settings.hidden : []
|
||||
readonly property var pinnedItems: bucket("pinned")
|
||||
readonly property var drawerItems: bucket("drawer")
|
||||
readonly property var allItems: bucket("all")
|
||||
readonly property int drawerCount: drawerItems.length
|
||||
readonly property int drawerExtent: drawerCount > 0 ? drawerCount * 16 + (drawerCount - 1) * 17 : 0
|
||||
// Match Waybar's group/tray-expander drawer transition-duration.
|
||||
readonly property int animationDuration: 600
|
||||
property real revealProgress: expanded ? 1 : 0
|
||||
readonly property real revealExtent: drawerExtent * revealProgress
|
||||
|
||||
function close() {
|
||||
managePopupOpen = false
|
||||
}
|
||||
|
||||
function trayIconSource(icon) {
|
||||
var value = String(icon || "")
|
||||
var marker = "?path="
|
||||
var markerIndex = value.indexOf(marker)
|
||||
if (markerIndex === -1) return value
|
||||
|
||||
var name = value.substring(0, markerIndex).split("/").pop()
|
||||
var iconPath = value.substring(markerIndex + marker.length).split("&")[0]
|
||||
return Util.fileUrl(iconPath + "/hicolor/16x16/status/" + name + ".png")
|
||||
}
|
||||
|
||||
function trayTooltip(item) {
|
||||
return item.tooltipTitle || item.title || item.id || ""
|
||||
}
|
||||
|
||||
function classifyItem(item) {
|
||||
var iid = String(item.id || "")
|
||||
if (hiddenIds.indexOf(iid) !== -1) return "hidden"
|
||||
if (pinnedIds.indexOf(iid) !== -1) return "pinned"
|
||||
return "drawer"
|
||||
}
|
||||
|
||||
function bucket(category) {
|
||||
var values = SystemTray.items.values
|
||||
var result = []
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var item = values[i]
|
||||
if (item.status === Status.Passive) continue
|
||||
if (category === "all") {
|
||||
result.push(item)
|
||||
continue
|
||||
}
|
||||
if (classifyItem(item) === category) result.push(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function persistTrayState(pinned, hidden) {
|
||||
if (!root.bar || !root.bar.shell || typeof root.bar.shell.updateEntryInline !== "function") return
|
||||
root.bar.shell.updateEntryInline("tray", { id: "tray", pinned: pinned, hidden: hidden })
|
||||
}
|
||||
|
||||
function togglePin(iid) {
|
||||
var p = pinnedIds.slice(), h = hiddenIds.slice()
|
||||
var idx = p.indexOf(iid)
|
||||
if (idx !== -1) p.splice(idx, 1)
|
||||
else {
|
||||
p.push(iid)
|
||||
var hi = h.indexOf(iid)
|
||||
if (hi !== -1) h.splice(hi, 1)
|
||||
}
|
||||
persistTrayState(p, h)
|
||||
}
|
||||
|
||||
function toggleHide(iid) {
|
||||
var p = pinnedIds.slice(), h = hiddenIds.slice()
|
||||
var idx = h.indexOf(iid)
|
||||
if (idx !== -1) h.splice(idx, 1)
|
||||
else {
|
||||
h.push(iid)
|
||||
var pi = p.indexOf(iid)
|
||||
if (pi !== -1) p.splice(pi, 1)
|
||||
}
|
||||
persistTrayState(p, h)
|
||||
}
|
||||
|
||||
visible: pinnedItems.length > 0 || drawerCount > 0
|
||||
clip: false
|
||||
implicitWidth: root.vertical ? root.barSize : trayContent.implicitWidth
|
||||
implicitHeight: root.vertical ? trayContent.implicitHeight : root.barSize
|
||||
|
||||
Behavior on revealProgress {
|
||||
NumberAnimation { duration: root.animationDuration; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: trayContent
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.vertical ? verticalTray : horizontalTray
|
||||
}
|
||||
|
||||
Component {
|
||||
id: horizontalTray
|
||||
|
||||
Item {
|
||||
id: horizontalTrayRoot
|
||||
|
||||
readonly property int pinnedWidth: pinnedRow.implicitWidth
|
||||
readonly property int drawerBlockWidth: root.allItems.length > 0 ? expandIcon.implicitWidth + root.drawerExtent : 0
|
||||
|
||||
implicitWidth: pinnedWidth + drawerBlockWidth
|
||||
implicitHeight: root.barSize
|
||||
|
||||
// Mask out the empty area the collapsed drawer reserves for its slide-in,
|
||||
// so hovering it doesn't trigger expand and clicks pass through.
|
||||
containmentMask: QtObject {
|
||||
function contains(point: point): bool {
|
||||
if (point.y < 0 || point.y > horizontalTrayRoot.height) return false
|
||||
// Drawer reveals leftward; chevron sits at the right end when collapsed
|
||||
// and slides left as it opens. The visible region starts at the chevron.
|
||||
var chevronX = root.drawerExtent - root.revealExtent
|
||||
if (point.x >= chevronX && point.x <= horizontalTrayRoot.drawerBlockWidth) return true
|
||||
// Pinned items, placed to the right of the drawer block.
|
||||
var pinnedStart = horizontalTrayRoot.drawerBlockWidth
|
||||
return point.x >= pinnedStart && point.x <= horizontalTrayRoot.implicitWidth
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: drawerArea
|
||||
x: 0
|
||||
width: horizontalTrayRoot.drawerBlockWidth
|
||||
height: root.barSize
|
||||
visible: root.allItems.length > 0
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: root.expanded = hovered
|
||||
}
|
||||
|
||||
WidgetButton {
|
||||
id: expandIcon
|
||||
bar: root.bar
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
x: root.drawerExtent - root.revealExtent
|
||||
text: "\uf053"
|
||||
horizontalMargin: 9
|
||||
verticalPadding: 6
|
||||
onPressed: function(button) {
|
||||
if (button === Qt.RightButton) root.managePopupOpen = !root.managePopupOpen
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: trayClip
|
||||
x: expandIcon.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: root.drawerExtent
|
||||
height: root.barSize
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: trayIcons
|
||||
x: root.drawerExtent - root.revealExtent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(17)
|
||||
layer.enabled: true
|
||||
|
||||
Repeater {
|
||||
model: root.drawerItems
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: pinnedRow
|
||||
x: drawerArea.x + horizontalTrayRoot.drawerBlockWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(17)
|
||||
leftPadding: root.pinnedItems.length > 0 && root.allItems.length > 0 ? Style.space(6) : 0
|
||||
Repeater {
|
||||
model: root.pinnedItems
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: verticalTray
|
||||
|
||||
Item {
|
||||
id: verticalTrayRoot
|
||||
|
||||
readonly property int pinnedHeight: pinnedCol.implicitHeight
|
||||
readonly property int drawerBlockHeight: root.allItems.length > 0 ? expandIcon.implicitHeight + root.drawerExtent : 0
|
||||
|
||||
implicitWidth: root.barSize
|
||||
implicitHeight: pinnedHeight + drawerBlockHeight
|
||||
|
||||
containmentMask: QtObject {
|
||||
function contains(point: point): bool {
|
||||
if (point.x < 0 || point.x > verticalTrayRoot.width) return false
|
||||
var chevronY = root.drawerExtent - root.revealExtent
|
||||
if (point.y >= chevronY && point.y <= verticalTrayRoot.drawerBlockHeight) return true
|
||||
var pinnedStart = verticalTrayRoot.drawerBlockHeight
|
||||
return point.y >= pinnedStart && point.y <= verticalTrayRoot.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: drawerArea
|
||||
y: 0
|
||||
width: root.barSize
|
||||
height: verticalTrayRoot.drawerBlockHeight
|
||||
visible: root.allItems.length > 0
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: root.expanded = hovered
|
||||
}
|
||||
|
||||
WidgetButton {
|
||||
id: expandIcon
|
||||
bar: root.bar
|
||||
width: implicitWidth
|
||||
height: implicitHeight
|
||||
y: root.drawerExtent - root.revealExtent
|
||||
text: "\uf053"
|
||||
textRotation: 90
|
||||
horizontalMargin: 9
|
||||
verticalPadding: 6
|
||||
onPressed: function(button) {
|
||||
if (button === Qt.RightButton) root.managePopupOpen = !root.managePopupOpen
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: trayClip
|
||||
y: expandIcon.height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: root.barSize
|
||||
height: root.drawerExtent
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: trayIcons
|
||||
y: root.drawerExtent - root.revealExtent
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Style.space(17)
|
||||
layer.enabled: true
|
||||
|
||||
Repeater {
|
||||
model: root.drawerItems
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: pinnedCol
|
||||
y: drawerArea.y + verticalTrayRoot.drawerBlockHeight
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Style.space(17)
|
||||
topPadding: root.pinnedItems.length > 0 && root.allItems.length > 0 ? Style.space(6) : 0
|
||||
Repeater {
|
||||
model: root.pinnedItems
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopupCard {
|
||||
id: managePopup
|
||||
anchorItem: root
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.managePopupOpen
|
||||
contentWidth: managePopup.fittedContentWidth(Style.space(300))
|
||||
contentHeight: managePopup.fittedContentHeight(manageColumn.implicitHeight)
|
||||
|
||||
Column {
|
||||
id: manageColumn
|
||||
anchors.fill: parent
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
text: "Tray icons"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Pinned icons stay visible. Hidden icons never show."
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.allItems.length === 0
|
||||
text: "No tray items reporting."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
font.italic: true
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.allItems
|
||||
delegate: Item {
|
||||
id: rowRoot
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: manageColumn.width
|
||||
implicitHeight: 28
|
||||
|
||||
readonly property string itemId: String(modelData.id || "")
|
||||
readonly property string displayName: {
|
||||
var t = String(modelData.title || "").trim()
|
||||
if (t) return t
|
||||
var tt = String(modelData.tooltipTitle || "").trim()
|
||||
if (tt) return tt
|
||||
var id = String(modelData.id || "")
|
||||
var slash = id.lastIndexOf("/")
|
||||
return slash !== -1 ? id.substring(slash + 1) : (id || "Unknown")
|
||||
}
|
||||
readonly property bool isPinned: root.pinnedIds.indexOf(itemId) !== -1
|
||||
readonly property bool isHidden: root.hiddenIds.indexOf(itemId) !== -1
|
||||
|
||||
IconImage {
|
||||
id: rowIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
implicitSize: 16
|
||||
width: 16
|
||||
height: 16
|
||||
source: root.trayIconSource(rowRoot.modelData.icon)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: rowIcon.right
|
||||
anchors.leftMargin: Style.space(10)
|
||||
anchors.right: rowHideBtn.left
|
||||
anchors.rightMargin: Style.space(8)
|
||||
text: rowRoot.displayName
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Button {
|
||||
id: rowPinBtn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
iconText: "\uf08d"
|
||||
text: rowRoot.isPinned ? "Unpin" : "Pin"
|
||||
foreground: root.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 3
|
||||
iconSize: Style.font.bodySmall
|
||||
fontSize: Style.font.bodySmall
|
||||
onClicked: root.togglePin(rowRoot.itemId)
|
||||
}
|
||||
|
||||
Button {
|
||||
id: rowHideBtn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: rowPinBtn.left
|
||||
anchors.rightMargin: Style.space(6)
|
||||
iconText: "\uf06e"
|
||||
text: rowRoot.isHidden ? "Show" : "Hide"
|
||||
foreground: root.foreground
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 3
|
||||
iconSize: Style.font.bodySmall
|
||||
fontSize: Style.font.bodySmall
|
||||
onClicked: root.toggleHide(rowRoot.itemId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component TrayItem: Item {
|
||||
id: trayItemRoot
|
||||
|
||||
required property var modelData
|
||||
|
||||
visible: modelData.status !== Status.Passive
|
||||
implicitWidth: visible ? 16 : 0
|
||||
implicitHeight: visible ? 16 : 0
|
||||
|
||||
IconImage {
|
||||
anchors.centerIn: parent
|
||||
implicitSize: 12
|
||||
width: 12
|
||||
height: 12
|
||||
source: root.trayIconSource(trayItemRoot.modelData.icon)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: if (root.bar) root.bar.showTooltip(trayItemRoot, root.trayTooltip(modelData))
|
||||
onExited: if (root.bar) root.bar.hideTooltip(trayItemRoot)
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.RightButton && trayItemRoot.modelData.hasMenu) {
|
||||
var point = trayItemRoot.QsWindow.contentItem.mapFromItem(trayItemRoot, mouse.x, mouse.y)
|
||||
trayItemRoot.modelData.display(trayItemRoot.QsWindow.window, point.x, point.y)
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
trayItemRoot.modelData.secondaryActivate()
|
||||
} else {
|
||||
trayItemRoot.modelData.activate()
|
||||
}
|
||||
}
|
||||
onWheel: function(wheel) {
|
||||
trayItemRoot.modelData.scroll(wheel.angleDelta.y, false)
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool tooltipHovered: visible && opacity > 0 && mouseArea.containsMouse
|
||||
}
|
||||
}
|
||||
@@ -290,11 +290,6 @@ Item {
|
||||
}
|
||||
|
||||
// ---------------- widget catalog -----------------------------------------
|
||||
readonly property var builtinWidgetMeta: ({
|
||||
"clock": { name: "Clock", description: "Date / time text", category: "Time" },
|
||||
"tray": { name: "System tray", description: "Status notifier items", category: "Status" }
|
||||
})
|
||||
|
||||
property int catalogRevision: 0
|
||||
onBarWidgetRegistryChanged: {
|
||||
catalogRevision++
|
||||
@@ -323,7 +318,6 @@ Item {
|
||||
var canonicalKey = canonicalWidgetId(key)
|
||||
if (root.barWidgetRegistry && root.barWidgetRegistry.has(canonicalKey))
|
||||
return root.barWidgetRegistry.metadataFor(canonicalKey) || {}
|
||||
if (builtinWidgetMeta[canonicalKey]) return builtinWidgetMeta[canonicalKey]
|
||||
|
||||
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[key] : null
|
||||
if (manifest) {
|
||||
@@ -388,7 +382,6 @@ Item {
|
||||
ids[pid] = true
|
||||
}
|
||||
}
|
||||
for (var key in builtinWidgetMeta) ids[key] = true
|
||||
return Object.keys(ids)
|
||||
}
|
||||
|
||||
@@ -411,7 +404,7 @@ Item {
|
||||
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
|
||||
var manifestIsBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1
|
||||
var isBarWidget = !!(meta && meta.source !== "plugin") || manifestIsBarWidget
|
||||
if (!isBarWidget && !builtinWidgetMeta[id]) continue
|
||||
if (!isBarWidget) continue
|
||||
|
||||
var inSection = sectionArray(section)
|
||||
var existsHere = false
|
||||
|
||||
Reference in New Issue
Block a user