Bar-widget panels (audio, bluetooth, network, power, monitor) used to be toggled via their own per-plugin IpcHandler targets. Quickshell resolves duplicate targets first-handler-wins, so after a plugin or bar reload the stale handler of the destroyed widget instance kept claiming the target and the hotkeys went dead. The shell root's IpcHandler lives outside the reload cycle, so summon, hide, and toggle now go through `omarchy-shell shell toggle <plugin>` and the shell routes to the live widget instance via the bar's slot registry. Panel plugins that are also panel/overlay/menu kinds keep using the panel loader path. Failures to find a live widget are logged so a widget missing from the bar layout stays diagnosable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1611 lines
49 KiB
QML
1611 lines
49 KiB
QML
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import qs.Commons
|
|
import qs.Ui
|
|
import "BarModel.js" as BarModel
|
|
|
|
Item {
|
|
id: root
|
|
|
|
// The omarchy-shell host injects omarchyPath from OMARCHY_PATH.
|
|
required property string omarchyPath
|
|
// Injected by the host shell so bar slots can resolve enabled widgets.
|
|
required property var barWidgetRegistry
|
|
// Injected by the host shell every time shell.json is reloaded. Holds the
|
|
// `bar:` subtree: position, centerAnchor, layout. The host owns file IO;
|
|
// the bar just renders whatever it's handed. The bar font follows the
|
|
// OS-level fontconfig monospace binding — it is not stored in shell.json.
|
|
required property var barConfig
|
|
// Injected by the host shell. Used for shell-wide actions such as opening
|
|
// settings and persisting inline widget state.
|
|
property var shell: null
|
|
// Manifest for the active bar option. Present for custom bars and useful for
|
|
// diagnostics; the built-in bar does not otherwise need it.
|
|
property var manifest: null
|
|
// Mirrors the on-disk `bar-off` flag so the user can hide the bar without
|
|
// killing the entire shell. Wired to BarPanel.visible below; updated by the
|
|
// FileView watcher further down.
|
|
property bool barHidden: false
|
|
property string home: Quickshell.env("HOME")
|
|
property string stateHome: home + "/.local/state"
|
|
property string omarchyConfigDir: home + "/.config/omarchy"
|
|
property var fallbackBarConfig: ({
|
|
position: "top",
|
|
transparent: false,
|
|
centerAnchor: "omarchy.clock",
|
|
layout: { left: [], center: [], right: [] }
|
|
})
|
|
property var layoutConfig: fallbackBarConfig.layout
|
|
property string centerAnchor: ""
|
|
property bool requestedTransparent: false
|
|
property bool useTransparentForeground: false
|
|
property bool transparent: false
|
|
property bool centerSectionHovered: false
|
|
property bool centerSectionRevealHeld: false
|
|
property bool centerHoverRevealSuppressed: false
|
|
property int barConfigSerial: 0
|
|
property string position: "top"
|
|
// Resolves through fontconfig at paint time (Style.font.family defaults
|
|
// to "monospace"), so changing the system font (via `omarchy-font-set`)
|
|
// updates the bar without a reload.
|
|
property string fontFamily: Style.font.family
|
|
// Bound to the central Color singleton so the bar tracks shell.toml's
|
|
// [bar] section. Property names kept for the rest of this file's bindings.
|
|
property color themeForeground: Color.bar.text
|
|
property color themeContrastForeground: Color.background
|
|
property color transparentForeground: Color.bar.text
|
|
property color foreground: themeForeground
|
|
property color barForeground: useTransparentForeground ? transparentForeground : themeForeground
|
|
property bool foregroundAnimationEnabled: true
|
|
property color background: Color.bar.background
|
|
property color urgent: Color.bar.active
|
|
|
|
Behavior on barForeground { enabled: root.foregroundAnimationEnabled; 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 var tooltipTarget: null
|
|
property var pendingTooltipTarget: null
|
|
property string tooltipText: ""
|
|
property string pendingTooltipText: ""
|
|
property bool tooltipShown: false
|
|
property int tooltipRequest: 0
|
|
property var activePopout: null
|
|
property var barDragSource: null
|
|
property var barDragTarget: null
|
|
property bool barDragAfter: false
|
|
property var barDragWindow: null
|
|
property var barDragScreen: null
|
|
property url barDragImageUrl: ""
|
|
property real barDragSceneX: 0
|
|
property real barDragSceneY: 0
|
|
property real barDragScreenX: 0
|
|
property real barDragScreenY: 0
|
|
property real barDragOffsetX: 0
|
|
property real barDragOffsetY: 0
|
|
property var configControls: []
|
|
property var clickTargets: []
|
|
property var moduleSlots: []
|
|
|
|
function registerClickTarget(target) {
|
|
if (!target || clickTargets.indexOf(target) !== -1) return
|
|
var next = clickTargets.slice()
|
|
next.push(target)
|
|
clickTargets = next
|
|
}
|
|
|
|
function unregisterClickTarget(target) {
|
|
var next = clickTargets.filter(function(item) { return item !== target })
|
|
clickTargets = next
|
|
}
|
|
|
|
function registerModuleSlot(slot) {
|
|
if (!slot || moduleSlots.indexOf(slot) !== -1) return
|
|
var next = moduleSlots.slice()
|
|
next.push(slot)
|
|
moduleSlots = next
|
|
}
|
|
|
|
function unregisterModuleSlot(slot) {
|
|
var next = moduleSlots.filter(function(item) { return item !== slot })
|
|
moduleSlots = next
|
|
}
|
|
|
|
function registerConfigControl(control) {
|
|
if (!control || configControls.indexOf(control) !== -1) return
|
|
var next = configControls.slice()
|
|
next.push(control)
|
|
configControls = next
|
|
}
|
|
|
|
function unregisterConfigControl(control) {
|
|
var next = configControls.filter(function(item) { return item !== control })
|
|
configControls = next
|
|
}
|
|
|
|
function debugBarGeometry() {
|
|
var out = []
|
|
for (var i = 0; i < moduleSlots.length; i++) {
|
|
var slot = moduleSlots[i]
|
|
if (!slot || !slot.activeItem) continue
|
|
var point = { x: slot.x, y: slot.y }
|
|
try {
|
|
point = slot.mapToItem(null, 0, 0)
|
|
} catch (e) {
|
|
}
|
|
out.push({
|
|
id: slot.moduleName,
|
|
section: slot.region,
|
|
x: Math.round(point.x),
|
|
y: Math.round(point.y),
|
|
width: Math.round(slot.width),
|
|
height: Math.round(slot.height),
|
|
visible: slot.visible === true && slot.width > 0 && slot.height > 0,
|
|
itemVisible: slot.activeItem.visible === true,
|
|
itemWidth: Math.round(slot.activeItem.implicitWidth || 0),
|
|
itemHeight: Math.round(slot.activeItem.implicitHeight || 0)
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
function targetWindow(target) {
|
|
return target && target.QsWindow ? target.QsWindow.window : null
|
|
}
|
|
|
|
function targetBelongsToWindow(target, window) {
|
|
return !!target && !!window && targetWindow(target) === window
|
|
}
|
|
|
|
function slotWindow(slot) {
|
|
if (!slot) return null
|
|
return targetWindow(slot.activeItem) || targetWindow(slot)
|
|
}
|
|
|
|
function sameWindow(left, right) {
|
|
if (!left || !right) return false
|
|
if (left === right) return true
|
|
return !!left.screen && !!right.screen && !!left.screen.name && !!right.screen.name && left.screen.name === right.screen.name
|
|
}
|
|
|
|
function targetTooltipHovered(target) {
|
|
return !!target && target.visible !== false && target.opacity !== 0 && target.tooltipHovered === true
|
|
}
|
|
|
|
function clearTooltip() {
|
|
tooltipTimer.stop()
|
|
pendingTooltipTarget = null
|
|
pendingTooltipText = ""
|
|
tooltipTarget = null
|
|
tooltipText = ""
|
|
tooltipShown = false
|
|
}
|
|
|
|
function clearBarDrag() {
|
|
barDragSource = null
|
|
barDragWindow = null
|
|
barDragScreen = null
|
|
barDragImageUrl = ""
|
|
barDragTarget = null
|
|
barDragAfter = false
|
|
barDragSceneX = 0
|
|
barDragSceneY = 0
|
|
barDragScreenX = 0
|
|
barDragScreenY = 0
|
|
barDragOffsetX = 0
|
|
barDragOffsetY = 0
|
|
}
|
|
|
|
function barDragScreenPoint(scenePoint) {
|
|
var x = scenePoint ? scenePoint.x : 0
|
|
var y = scenePoint ? scenePoint.y : 0
|
|
var window = barDragWindow
|
|
if (!window || !window.screen) return { x: x, y: y }
|
|
|
|
if (root.position === "bottom")
|
|
y += Math.max(0, window.screen.height - window.height)
|
|
else if (root.position === "right")
|
|
x += Math.max(0, window.screen.width - window.width)
|
|
|
|
return { x: x, y: y }
|
|
}
|
|
|
|
function captureBarDragGhost(slot) {
|
|
var item = slot && slot.activeItem ? slot.activeItem : null
|
|
barDragImageUrl = ""
|
|
if (!item || typeof item.grabToImage !== "function") return
|
|
|
|
var grabWidth = Math.max(1, Math.ceil(item.width || item.implicitWidth || slot.width || 1))
|
|
var grabHeight = Math.max(1, Math.ceil(item.height || item.implicitHeight || slot.height || 1))
|
|
item.grabToImage(function(result) {
|
|
if (root.barDragSource !== slot || !result || !result.url) return
|
|
root.barDragImageUrl = result.url
|
|
}, Qt.size(grabWidth, grabHeight))
|
|
}
|
|
|
|
function requestPopout(owner) {
|
|
if (activePopout === owner) return
|
|
if (activePopout) {
|
|
if ("closeForPopoutSwitch" in activePopout) activePopout.closeForPopoutSwitch()
|
|
else if ("close" in activePopout) activePopout.close()
|
|
}
|
|
activePopout = owner
|
|
}
|
|
|
|
function releasePopout(owner) {
|
|
if (activePopout === owner) activePopout = null
|
|
}
|
|
|
|
readonly property bool vertical: position === "left" || position === "right"
|
|
readonly property int barSize: vertical ? Style.bar.sizeVertical : Style.bar.sizeHorizontal
|
|
|
|
function normalizePosition(value) {
|
|
return BarModel.normalizePosition(value)
|
|
}
|
|
|
|
// Apply tray-pinning on top of the shared layout normalization so the
|
|
// bar host and scriptable config helpers can't drift on entry shape.
|
|
function normalizeLayout(layout) {
|
|
var normalized = Util.normalizeLayout(Util.isPlainObject(layout) ? layout : fallbackBarConfig.layout)
|
|
return {
|
|
left: pinTrayToInner(normalized.left, "left"),
|
|
center: pinTrayToInner(normalized.center, "center"),
|
|
right: pinTrayToInner(normalized.right, "right")
|
|
}
|
|
}
|
|
|
|
// The tray drawer reveals inward (away from the bar edge). Place it at the
|
|
// section's inner edge: start of the right section, end of the left/center
|
|
// sections. The drawer's reserved space then sits next to the bar center,
|
|
// not stranded mid-section.
|
|
function pinTrayToInner(entries, section) {
|
|
return BarModel.pinTrayToInner(entries, section)
|
|
}
|
|
|
|
function applyBarConfig() {
|
|
var config = Util.isPlainObject(barConfig) ? barConfig : fallbackBarConfig
|
|
|
|
position = normalizePosition(config.position)
|
|
setRequestedTransparency(config.transparent === true)
|
|
centerAnchor = Util.canonicalWidgetId(config.centerAnchor || "")
|
|
layoutConfig = normalizeLayout(config.layout)
|
|
barConfigSerial++
|
|
}
|
|
|
|
onBarConfigChanged: applyBarConfig()
|
|
|
|
function layoutEntries(region) {
|
|
var serial = barConfigSerial
|
|
var entries = layoutConfig ? layoutConfig[region] : null
|
|
return Array.isArray(entries) ? entries : []
|
|
}
|
|
|
|
function panelNavigationSlots(region) {
|
|
var entries = layoutEntries(region)
|
|
var slots = []
|
|
for (var i = 0; i < entries.length; i++) {
|
|
var id = entryId(entries[i])
|
|
for (var j = 0; j < moduleSlots.length; j++) {
|
|
var slot = moduleSlots[j]
|
|
if (!slot || slot.region !== region || slot.moduleName !== id) continue
|
|
var item = slot.activeItem
|
|
if (!item || item.visible !== true || slot.visible !== true || slot.width <= 0 || slot.height <= 0) continue
|
|
if (typeof item.open !== "function" || typeof item.close !== "function" || item.opened === undefined) continue
|
|
slots.push(slot)
|
|
break
|
|
}
|
|
}
|
|
return slots
|
|
}
|
|
|
|
function switchPanelFrom(owner, direction) {
|
|
if (!owner) return false
|
|
|
|
var currentSlot = null
|
|
for (var i = 0; i < moduleSlots.length; i++) {
|
|
var slot = moduleSlots[i]
|
|
if (slot && slot.activeItem === owner) {
|
|
currentSlot = slot
|
|
break
|
|
}
|
|
}
|
|
if (!currentSlot) return false
|
|
|
|
var slots = panelNavigationSlots(currentSlot.region)
|
|
if (slots.length < 2) return false
|
|
|
|
var currentIndex = -1
|
|
for (var j = 0; j < slots.length; j++) {
|
|
if (slots[j] === currentSlot) {
|
|
currentIndex = j
|
|
break
|
|
}
|
|
}
|
|
if (currentIndex < 0) return false
|
|
|
|
var step = direction < 0 ? -1 : 1
|
|
var nextSlot = slots[(currentIndex + step + slots.length) % slots.length]
|
|
if (!nextSlot || !nextSlot.activeItem || nextSlot.activeItem === owner) return false
|
|
|
|
nextSlot.activeItem.open()
|
|
return true
|
|
}
|
|
|
|
// Resolve the live bar-widget instance for a plugin id (e.g. "omarchy.bluetooth").
|
|
// Only widgets that expose popup open/close methods count; plain indicators
|
|
// (clock, workspaces, tray) return null. Used by shell.summon/toggle so
|
|
// panel hotkeys route through the bar instead of a per-target IpcHandler
|
|
// that goes stale when the bar reloads its widget instances.
|
|
function findPanelWidget(pluginId) {
|
|
var id = String(pluginId || "")
|
|
if (!id) return null
|
|
for (var i = 0; i < moduleSlots.length; i++) {
|
|
var slot = moduleSlots[i]
|
|
if (!slot || !slot.activeItem) continue
|
|
if (slot.moduleName !== id) continue
|
|
var item = slot.activeItem
|
|
if (typeof item.open !== "function" || typeof item.close !== "function" || item.opened === undefined) continue
|
|
return item
|
|
}
|
|
return null
|
|
}
|
|
|
|
function summonBarWidget(pluginId) {
|
|
var item = findPanelWidget(pluginId)
|
|
if (!item || typeof item.open !== "function") return false
|
|
item.open()
|
|
return true
|
|
}
|
|
|
|
function hideBarWidget(pluginId) {
|
|
var item = findPanelWidget(pluginId)
|
|
if (!item || typeof item.close !== "function") return false
|
|
item.close()
|
|
return true
|
|
}
|
|
|
|
function isBarWidgetOpen(pluginId) {
|
|
var item = findPanelWidget(pluginId)
|
|
return !!item && item.opened === true
|
|
}
|
|
|
|
function entrySettings(entry) {
|
|
return BarModel.entrySettings(entry)
|
|
}
|
|
|
|
function entryId(entry) {
|
|
return BarModel.entryId(entry)
|
|
}
|
|
|
|
function moduleString(entry, key, fallback) {
|
|
return BarModel.moduleString(entry, key, fallback)
|
|
}
|
|
|
|
function entryIndex(entries, name) {
|
|
return BarModel.entryIndex(entries, name)
|
|
}
|
|
|
|
function entriesBefore(entries, name) {
|
|
return BarModel.entriesBefore(entries, name)
|
|
}
|
|
|
|
function entriesAfter(entries, name) {
|
|
return BarModel.entriesAfter(entries, name)
|
|
}
|
|
|
|
function canonicalWidgetId(name) {
|
|
return Util.canonicalWidgetId(name)
|
|
}
|
|
|
|
function expandPath(path) {
|
|
return BarModel.expandPath(path, home)
|
|
}
|
|
|
|
function customModuleSafeName(name) {
|
|
return BarModel.customModuleSafeName(name)
|
|
}
|
|
|
|
function customModuleType(entry) {
|
|
return BarModel.customModuleType(entry)
|
|
}
|
|
|
|
function customModuleSource(entry) {
|
|
var source = BarModel.customModulePath(entry, home, omarchyConfigDir)
|
|
return source ? Util.fileUrl(source) : ""
|
|
}
|
|
|
|
Component.onCompleted: applyBarConfig()
|
|
|
|
function setCenterSectionHovered(hovered) {
|
|
centerSectionHovered = hovered
|
|
if (hovered) {
|
|
centerSectionRevealTimer.stop()
|
|
centerSectionRevealHeld = true
|
|
} else {
|
|
centerSectionRevealTimer.restart()
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: centerSectionRevealTimer
|
|
interval: 120
|
|
onTriggered: root.centerSectionRevealHeld = root.centerSectionHovered
|
|
}
|
|
|
|
function run(command) {
|
|
if (!command) return
|
|
|
|
launcher.command = Util.hyprExecCommand(command)
|
|
launcher.startDetached()
|
|
}
|
|
|
|
function openConfigPanel() {
|
|
for (var i = 0; i < configControls.length; i++) {
|
|
var control = configControls[i]
|
|
if (!control || control.visible !== true || typeof control.openPanel !== "function") continue
|
|
control.openPanel()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
function toggleTransparency() {
|
|
var nextTransparent = !(root.requestedTransparent === true)
|
|
if (root.shell && typeof root.shell.mutateShellConfig === "function") {
|
|
root.shell.mutateShellConfig(function(config) {
|
|
if (!Util.isPlainObject(config.bar)) config.bar = {}
|
|
config.bar.transparent = nextTransparent
|
|
})
|
|
} else {
|
|
root.setRequestedTransparency(nextTransparent)
|
|
}
|
|
}
|
|
|
|
function rawLayoutSection(config, region) {
|
|
if (!Util.isPlainObject(config.bar)) config.bar = {}
|
|
if (!Util.isPlainObject(config.bar.layout)) config.bar.layout = {}
|
|
if (!Array.isArray(config.bar.layout[region])) config.bar.layout[region] = []
|
|
|
|
return config.bar.layout[region]
|
|
}
|
|
|
|
function rawEntryIndex(entries, name) {
|
|
for (var i = 0; i < entries.length; i++) {
|
|
if (root.entryId(entries[i]) === name) return i
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
function moveModuleInConfig(config, fromRegion, fromName, toRegion, beforeName) {
|
|
var fromEntries = rawLayoutSection(config, fromRegion)
|
|
var toEntries = rawLayoutSection(config, toRegion)
|
|
var fromIndex = rawEntryIndex(fromEntries, fromName)
|
|
if (fromIndex < 0) return false
|
|
|
|
var toIndex = beforeName ? rawEntryIndex(toEntries, beforeName) : toEntries.length
|
|
if (toIndex < 0) toIndex = toEntries.length
|
|
|
|
if (fromRegion === toRegion && fromIndex === toIndex) return false
|
|
|
|
var movedEntry = fromEntries[fromIndex]
|
|
fromEntries.splice(fromIndex, 1)
|
|
|
|
if (fromRegion === toRegion && fromIndex < toIndex) toIndex -= 1
|
|
if (toIndex < 0) toIndex = 0
|
|
if (toIndex > toEntries.length) toIndex = toEntries.length
|
|
if (fromRegion === toRegion && fromIndex === toIndex) {
|
|
fromEntries.splice(fromIndex, 0, movedEntry)
|
|
return false
|
|
}
|
|
|
|
toEntries.splice(toIndex, 0, movedEntry)
|
|
return true
|
|
}
|
|
|
|
function dropBarModule(source, toRegion, beforeName) {
|
|
if (!source || !source.region || !source.moduleName || !toRegion) return false
|
|
if (source.region === toRegion && source.moduleName === beforeName) return false
|
|
if (!root.shell || typeof root.shell.mutateShellConfig !== "function") return false
|
|
|
|
var changed = false
|
|
root.shell.mutateShellConfig(function(config) {
|
|
changed = moveModuleInConfig(config, source.region, source.moduleName, toRegion, beforeName)
|
|
})
|
|
return changed
|
|
}
|
|
|
|
function moduleDropAtScene(scenePoint, sourceSlot) {
|
|
var sourceWindow = root.slotWindow(sourceSlot) || root.barDragWindow
|
|
for (var i = 0; i < moduleSlots.length; i++) {
|
|
var slot = moduleSlots[i]
|
|
if (!slot || slot === sourceSlot || !slot.visible || slot.width <= 0 || slot.height <= 0) continue
|
|
if (sourceWindow && !root.sameWindow(root.slotWindow(slot), sourceWindow)) continue
|
|
|
|
var slotPoint = { x: slot.x, y: slot.y }
|
|
try {
|
|
slotPoint = slot.mapToItem(null, 0, 0)
|
|
} catch (e) {
|
|
}
|
|
|
|
if (scenePoint.x >= slotPoint.x && scenePoint.x <= slotPoint.x + slot.width &&
|
|
scenePoint.y >= slotPoint.y && scenePoint.y <= slotPoint.y + slot.height) {
|
|
return {
|
|
slot: slot,
|
|
after: root.vertical ? scenePoint.y > slotPoint.y + slot.height / 2 : scenePoint.x > slotPoint.x + slot.width / 2
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function visibleModuleSlot(region, name, sourceSlot) {
|
|
var sourceWindow = root.slotWindow(sourceSlot) || root.barDragWindow
|
|
for (var i = 0; i < moduleSlots.length; i++) {
|
|
var slot = moduleSlots[i]
|
|
if (!slot || slot === sourceSlot || slot.region !== region || slot.moduleName !== name ||
|
|
!slot.visible || slot.width <= 0 || slot.height <= 0) continue
|
|
if (sourceWindow && !root.sameWindow(root.slotWindow(slot), sourceWindow)) continue
|
|
return slot
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function nextVisibleModuleName(region, afterName, sourceSlot) {
|
|
var entries = layoutEntries(region)
|
|
var found = false
|
|
for (var i = 0; i < entries.length; i++) {
|
|
var name = entryId(entries[i])
|
|
if (!found) {
|
|
found = name === afterName
|
|
continue
|
|
}
|
|
|
|
if (visibleModuleSlot(region, name, sourceSlot)) return name
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
function dropBarModuleAtTarget(sourceSlot, targetSlot, afterTarget) {
|
|
if (!sourceSlot || !targetSlot) return false
|
|
|
|
var beforeName = afterTarget ? nextVisibleModuleName(targetSlot.region, targetSlot.moduleName, sourceSlot) : targetSlot.moduleName
|
|
return dropBarModule(sourceSlot, targetSlot.region, beforeName)
|
|
}
|
|
|
|
function moduleTargetClickable(target) {
|
|
return target
|
|
&& target.visible !== false
|
|
&& target.opacity !== 0
|
|
&& target.interactive !== false
|
|
&& target.pressable !== false
|
|
&& target.concealed !== true
|
|
&& typeof target.triggerPress === "function"
|
|
}
|
|
|
|
function moduleClickTargetAt(slot, localX, localY) {
|
|
for (var i = clickTargets.length - 1; i >= 0; i--) {
|
|
var target = clickTargets[i]
|
|
if (!moduleTargetClickable(target)) continue
|
|
|
|
var targetPoint = { x: localX, y: localY }
|
|
try {
|
|
targetPoint = slot.mapToItem(target, localX, localY)
|
|
} catch (e) {
|
|
continue
|
|
}
|
|
|
|
if (targetPoint.x >= 0 && targetPoint.x <= target.width &&
|
|
targetPoint.y >= 0 && targetPoint.y <= target.height) {
|
|
return target
|
|
}
|
|
}
|
|
|
|
if (moduleTargetClickable(slot.activeItem)) return slot.activeItem
|
|
return null
|
|
}
|
|
|
|
function pressModuleClickTarget(slot, button, localX, localY) {
|
|
var target = moduleClickTargetAt(slot, localX, localY)
|
|
if (!target) return false
|
|
|
|
target.triggerPress(button)
|
|
return true
|
|
}
|
|
|
|
function colorHex(colorValue) {
|
|
var c = colorValue
|
|
if (typeof c === "string") c = Qt.color(c)
|
|
function hexChannel(value) {
|
|
var s = Math.round(Util.clamp(value, 0, 1) * 255).toString(16)
|
|
return s.length < 2 ? "0" + s : s
|
|
}
|
|
return "#" + hexChannel(c.r) + hexChannel(c.g) + hexChannel(c.b)
|
|
}
|
|
|
|
function setRequestedTransparency(value) {
|
|
var nextTransparent = value === true
|
|
requestedTransparent = nextTransparent
|
|
if (!nextTransparent) {
|
|
foregroundAnimationEnabled = false
|
|
useTransparentForeground = false
|
|
transparent = false
|
|
transparentForeground = themeForeground
|
|
restoreForegroundAnimation()
|
|
return
|
|
}
|
|
scheduleTransparentForegroundRefresh()
|
|
}
|
|
|
|
function restoreForegroundAnimation() {
|
|
Qt.callLater(function() {
|
|
Qt.callLater(function() { root.foregroundAnimationEnabled = true })
|
|
})
|
|
}
|
|
|
|
function scheduleTransparentForegroundRefresh() {
|
|
if (!requestedTransparent) {
|
|
transparentForeground = themeForeground
|
|
return
|
|
}
|
|
transparentForegroundTimer.restart()
|
|
}
|
|
|
|
function refreshTransparentForeground() {
|
|
if (!requestedTransparent || transparentForegroundProc.running) return
|
|
|
|
transparentForegroundProc.command = [
|
|
"omarchy-shell-bar-text-color",
|
|
root.position,
|
|
String(root.barSize),
|
|
colorHex(root.themeForeground),
|
|
colorHex(root.themeContrastForeground)
|
|
]
|
|
transparentForegroundProc.running = true
|
|
}
|
|
|
|
onRequestedTransparentChanged: scheduleTransparentForegroundRefresh()
|
|
onPositionChanged: scheduleTransparentForegroundRefresh()
|
|
onThemeForegroundChanged: scheduleTransparentForegroundRefresh()
|
|
onThemeContrastForegroundChanged: scheduleTransparentForegroundRefresh()
|
|
|
|
Timer {
|
|
id: transparentForegroundTimer
|
|
interval: 120
|
|
repeat: false
|
|
onTriggered: root.refreshTransparentForeground()
|
|
}
|
|
|
|
Process {
|
|
id: transparentForegroundProc
|
|
stdout: SplitParser {
|
|
onRead: function(line) {
|
|
var value = String(line || "").trim()
|
|
if (!/^#[0-9A-Fa-f]{6}$/.test(value)) return
|
|
|
|
root.foregroundAnimationEnabled = false
|
|
root.transparentForeground = value
|
|
if (root.requestedTransparent) {
|
|
root.useTransparentForeground = true
|
|
root.transparent = true
|
|
}
|
|
root.restoreForegroundAnimation()
|
|
}
|
|
}
|
|
}
|
|
|
|
FileView {
|
|
path: root.stateHome + "/omarchy/current"
|
|
watchChanges: true
|
|
printErrors: false
|
|
onFileChanged: root.scheduleTransparentForegroundRefresh()
|
|
}
|
|
|
|
function runProcess(process) {
|
|
if (!process.running)
|
|
process.running = true
|
|
}
|
|
|
|
function showTooltip(target, text) {
|
|
clearTooltip()
|
|
|
|
if (!targetTooltipHovered(target) || !text) {
|
|
tooltipRequest += 1
|
|
return
|
|
}
|
|
|
|
var request = tooltipRequest + 1
|
|
tooltipRequest = request
|
|
pendingTooltipTarget = target
|
|
pendingTooltipText = text
|
|
|
|
Qt.callLater(function() {
|
|
if (request !== tooltipRequest) return
|
|
if (!targetTooltipHovered(pendingTooltipTarget)) {
|
|
clearTooltip()
|
|
return
|
|
}
|
|
tooltipTarget = pendingTooltipTarget
|
|
tooltipText = pendingTooltipText
|
|
pendingTooltipTarget = null
|
|
pendingTooltipText = ""
|
|
tooltipTimer.restart()
|
|
})
|
|
}
|
|
|
|
function hideTooltip(target) {
|
|
if (tooltipTarget !== target && pendingTooltipTarget !== target) return
|
|
|
|
tooltipRequest += 1
|
|
clearTooltip()
|
|
}
|
|
|
|
Process { id: launcher }
|
|
|
|
Timer {
|
|
id: tooltipTimer
|
|
interval: 400
|
|
onTriggered: {
|
|
if (root.targetTooltipHovered(root.tooltipTarget)) root.tooltipShown = true
|
|
else root.clearTooltip()
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
interval: 100
|
|
running: root.tooltipShown
|
|
repeat: true
|
|
onTriggered: if (!root.targetTooltipHovered(root.tooltipTarget)) root.hideTooltip(root.tooltipTarget)
|
|
}
|
|
|
|
// Presence of the `bar-off` flag = bar hidden. Watching the parent toggles
|
|
// directory because FileView can't observe a file that doesn't exist yet,
|
|
// and the flag is created/removed by `omarchy-toggle-bar`.
|
|
Process {
|
|
id: barHiddenProbe
|
|
running: true
|
|
command: ["bash", "-lc", "[[ -f $HOME/.local/state/omarchy/toggles/bar-off ]] && echo yes || echo no"]
|
|
stdout: SplitParser { onRead: function(line) { root.barHidden = String(line).trim() === "yes" } }
|
|
}
|
|
FileView {
|
|
path: root.home + "/.local/state/omarchy/toggles"
|
|
watchChanges: true
|
|
printErrors: false
|
|
onFileChanged: barHiddenProbe.running = true
|
|
}
|
|
|
|
Variants {
|
|
model: Quickshell.screens
|
|
|
|
delegate: Component {
|
|
BarPanel {
|
|
required property var modelData
|
|
|
|
screen: modelData
|
|
}
|
|
}
|
|
}
|
|
|
|
Variants {
|
|
model: Quickshell.screens
|
|
|
|
delegate: Component {
|
|
DragGhostPanel {
|
|
required property var modelData
|
|
|
|
screen: modelData
|
|
ghostScreen: modelData
|
|
}
|
|
}
|
|
}
|
|
|
|
component BarPanel: PanelWindow {
|
|
id: barWindow
|
|
|
|
visible: !root.barHidden
|
|
|
|
anchors {
|
|
top: root.position === "top" || root.vertical
|
|
bottom: root.position === "bottom" || root.vertical
|
|
left: root.position === "left" || !root.vertical
|
|
right: root.position === "right" || !root.vertical
|
|
}
|
|
|
|
implicitWidth: root.vertical ? root.barSize : 0
|
|
implicitHeight: root.vertical ? 0 : root.barSize
|
|
color: root.transparent ? "transparent" : root.background
|
|
WlrLayershell.namespace: "omarchy-bar"
|
|
WlrLayershell.layer: WlrLayer.Top
|
|
|
|
Loader {
|
|
anchors.fill: parent
|
|
sourceComponent: root.vertical ? verticalBar : horizontalBar
|
|
}
|
|
|
|
PopupWindow {
|
|
id: tooltipWindow
|
|
|
|
visible: root.tooltipShown && root.tooltipTarget !== null && root.tooltipText !== "" && root.targetBelongsToWindow(root.tooltipTarget, barWindow)
|
|
color: "transparent"
|
|
implicitWidth: Math.ceil(tooltipBubble.implicitWidth)
|
|
implicitHeight: Math.ceil(tooltipBubble.implicitHeight)
|
|
|
|
anchor {
|
|
id: tooltipAnchor
|
|
window: barWindow
|
|
adjustment: PopupAdjustment.Slide
|
|
edges: Edges.Top | Edges.Left
|
|
gravity: Edges.Bottom | Edges.Right
|
|
rect.width: 1
|
|
rect.height: 1
|
|
|
|
onAnchoring: {
|
|
var target = root.tooltipTarget
|
|
if (!root.targetBelongsToWindow(target, barWindow)) return
|
|
|
|
var popupWidth = tooltipWindow.implicitWidth
|
|
var popupHeight = tooltipWindow.implicitHeight
|
|
var localX = target.width / 2 - popupWidth / 2
|
|
var localY = target.height + 6
|
|
|
|
if (root.position === "bottom") {
|
|
localY = -popupHeight - 6
|
|
} else if (root.position === "left") {
|
|
localX = target.width + 6
|
|
localY = target.height / 2 - popupHeight / 2
|
|
} else if (root.position === "right") {
|
|
localX = -popupWidth - 6
|
|
localY = target.height / 2 - popupHeight / 2
|
|
}
|
|
|
|
var point = barWindow.contentItem.mapFromItem(target, localX, localY)
|
|
tooltipAnchor.rect.x = Math.round(point.x)
|
|
tooltipAnchor.rect.y = Math.round(point.y)
|
|
}
|
|
}
|
|
|
|
BorderSurface {
|
|
id: tooltipBubble
|
|
implicitWidth: tooltipLabel.implicitWidth + 20
|
|
implicitHeight: tooltipLabel.implicitHeight + 14
|
|
color: Color.tooltip.background
|
|
borderSpec: Border.surfaceSpec("tooltip", "border", Color.tooltip.border, 1)
|
|
radius: Style.cornerRadius
|
|
|
|
Text {
|
|
id: tooltipLabel
|
|
anchors.centerIn: parent
|
|
text: root.tooltipText
|
|
color: Color.tooltip.text
|
|
font.family: root.fontFamily
|
|
font.pixelSize: Style.font.body
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: horizontalBar
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
|
|
CenterModules { anchors.fill: parent }
|
|
|
|
LeftModules {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Style.space(8)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
RightModules {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Style.space(8)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: verticalBar
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
|
|
CenterModules { anchors.fill: parent }
|
|
|
|
LeftModules {
|
|
anchors.top: parent.top
|
|
anchors.topMargin: Style.space(8)
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
|
|
RightModules {
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: Style.space(8)
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component { id: emptyModuleComponent; Item { implicitWidth: 0; implicitHeight: 0; visible: false } }
|
|
|
|
component DragGhostPanel: PanelWindow {
|
|
id: ghostWindow
|
|
|
|
required property var ghostScreen
|
|
readonly property bool screenMatches: root.barDragScreen === ghostScreen ||
|
|
(root.barDragScreen && ghostScreen && root.barDragScreen.name && ghostScreen.name && root.barDragScreen.name === ghostScreen.name)
|
|
readonly property bool active: root.barDragSource && root.barDragScreen && screenMatches
|
|
readonly property var sourceItem: root.barDragSource ? root.barDragSource.activeItem : null
|
|
readonly property int ghostPadding: Style.space(1)
|
|
readonly property int ghostWidth: sourceItem ? Math.max(1, Math.ceil(sourceItem.width)) : 1
|
|
readonly property int ghostHeight: sourceItem ? Math.max(1, Math.ceil(sourceItem.height)) : 1
|
|
|
|
visible: active && sourceItem !== null
|
|
color: "transparent"
|
|
exclusionMode: ExclusionMode.Ignore
|
|
WlrLayershell.namespace: "omarchy-bar-drag-ghost"
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
|
|
anchors {
|
|
top: true
|
|
bottom: true
|
|
left: true
|
|
right: true
|
|
}
|
|
|
|
// Visual-only drag feedback. Keep the input region empty so the ghost can
|
|
// sit under the cursor without stealing the MouseArea's active pointer grab.
|
|
mask: Region {}
|
|
|
|
Item {
|
|
visible: ghostWindow.visible
|
|
x: Math.round(root.barDragScreenX - root.barDragOffsetX - ghostWindow.ghostPadding)
|
|
y: Math.round(root.barDragScreenY - root.barDragOffsetY - ghostWindow.ghostPadding)
|
|
width: ghostWindow.ghostWidth + ghostWindow.ghostPadding * 2
|
|
height: ghostWindow.ghostHeight + ghostWindow.ghostPadding * 2
|
|
|
|
BorderSurface {
|
|
anchors.fill: parent
|
|
color: root.transparent ? "transparent" : root.background
|
|
borderSpec: Border.flat(root.barForeground, 1)
|
|
radius: Math.min(Style.cornerRadius, height / 2)
|
|
opacity: root.transparent ? 0.45 : 0.94
|
|
}
|
|
|
|
Image {
|
|
anchors.fill: parent
|
|
anchors.margins: ghostWindow.ghostPadding
|
|
source: root.barDragImageUrl
|
|
fillMode: Image.Stretch
|
|
smooth: true
|
|
opacity: 0.84
|
|
}
|
|
}
|
|
}
|
|
|
|
function findCenterAnchorEntry() {
|
|
var entries = root.layoutEntries("center")
|
|
var idx = root.entryIndex(entries, root.centerAnchor)
|
|
return idx === -1 ? null : entries[idx]
|
|
}
|
|
|
|
component LeftModules: ModuleList {
|
|
entries: root.layoutEntries("left")
|
|
region: "left"
|
|
}
|
|
|
|
component RightModules: ModuleList {
|
|
entries: root.layoutEntries("right")
|
|
region: "right"
|
|
}
|
|
|
|
component CenterModules: Item {
|
|
id: centerRoot
|
|
|
|
property var entries: root.layoutEntries("center")
|
|
readonly property bool hasAnchor: root.entryIndex(entries, root.centerAnchor) !== -1
|
|
readonly property var anchorEntry: root.findCenterAnchorEntry()
|
|
|
|
Loader {
|
|
anchors.fill: parent
|
|
sourceComponent: root.vertical ? verticalCenterModules : horizontalCenterModules
|
|
}
|
|
|
|
Component {
|
|
id: horizontalCenterModules
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
|
|
CenterGestureArea { anchors.fill: parent }
|
|
|
|
HoverHandler {
|
|
onHoveredChanged: root.setCenterSectionHovered(hovered)
|
|
}
|
|
|
|
ModuleList {
|
|
visible: !centerRoot.hasAnchor
|
|
entries: centerRoot.entries
|
|
region: "center"
|
|
anchors.centerIn: parent
|
|
}
|
|
|
|
ModuleList {
|
|
visible: centerRoot.hasAnchor
|
|
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
|
|
region: "center"
|
|
anchors.right: centerConfigControl.visible ? centerConfigControl.left : centerAnchorModule.left
|
|
anchors.verticalCenter: centerAnchorModule.verticalCenter
|
|
}
|
|
|
|
ModuleSlot {
|
|
id: centerAnchorModule
|
|
visible: centerRoot.hasAnchor
|
|
entry: centerRoot.anchorEntry
|
|
region: "center"
|
|
anchors.centerIn: parent
|
|
}
|
|
|
|
BarConfigControl {
|
|
id: centerConfigControl
|
|
|
|
visible: centerRoot.hasAnchor && centerAnchorModule.moduleName === "omarchy.clock"
|
|
clockHovered: centerAnchorModule.hovered
|
|
centerHovered: root.centerSectionRevealHeld && !root.centerHoverRevealSuppressed
|
|
anchors.right: centerAnchorModule.left
|
|
anchors.verticalCenter: centerAnchorModule.verticalCenter
|
|
}
|
|
|
|
ModuleList {
|
|
visible: centerRoot.hasAnchor
|
|
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
|
|
region: "center"
|
|
anchors.left: centerAnchorModule.right
|
|
anchors.verticalCenter: centerAnchorModule.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: verticalCenterModules
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
|
|
CenterGestureArea { anchors.fill: parent }
|
|
|
|
HoverHandler {
|
|
onHoveredChanged: root.setCenterSectionHovered(hovered)
|
|
}
|
|
|
|
ModuleList {
|
|
visible: !centerRoot.hasAnchor
|
|
entries: centerRoot.entries
|
|
region: "center"
|
|
anchors.centerIn: parent
|
|
}
|
|
|
|
ModuleList {
|
|
visible: centerRoot.hasAnchor
|
|
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
|
|
region: "center"
|
|
anchors.bottom: centerConfigControl.visible ? centerConfigControl.top : centerAnchorModule.top
|
|
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
|
}
|
|
|
|
ModuleSlot {
|
|
id: centerAnchorModule
|
|
visible: centerRoot.hasAnchor
|
|
entry: centerRoot.anchorEntry
|
|
region: "center"
|
|
anchors.centerIn: parent
|
|
}
|
|
|
|
BarConfigControl {
|
|
id: centerConfigControl
|
|
|
|
visible: centerRoot.hasAnchor && centerAnchorModule.moduleName === "omarchy.clock"
|
|
clockHovered: centerAnchorModule.hovered
|
|
centerHovered: root.centerSectionRevealHeld && !root.centerHoverRevealSuppressed
|
|
anchors.bottom: centerAnchorModule.top
|
|
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
|
}
|
|
|
|
ModuleList {
|
|
visible: centerRoot.hasAnchor
|
|
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
|
|
region: "center"
|
|
anchors.top: centerAnchorModule.bottom
|
|
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component CenterGestureArea: MouseArea {
|
|
acceptedButtons: Qt.LeftButton
|
|
|
|
onDoubleClicked: function(mouse) {
|
|
if (mouse.button === Qt.LeftButton) {
|
|
root.toggleTransparency()
|
|
mouse.accepted = true
|
|
}
|
|
}
|
|
}
|
|
|
|
component BarConfigControl: Item {
|
|
id: configControl
|
|
|
|
property bool clockHovered: false
|
|
property bool centerHovered: false
|
|
property bool openWhenReady: false
|
|
|
|
readonly property var panelItem: configPanelLoader.item
|
|
readonly property bool panelOpen: panelItem ? panelItem.opened === true : false
|
|
readonly property bool revealed: visible && (clockHovered || centerHovered || controlHover.hovered || panelOpen)
|
|
|
|
implicitWidth: button.implicitWidth
|
|
implicitHeight: button.implicitHeight
|
|
width: implicitWidth
|
|
height: implicitHeight
|
|
z: 500
|
|
|
|
HoverHandler { id: controlHover }
|
|
|
|
Component.onCompleted: root.registerConfigControl(configControl)
|
|
Component.onDestruction: root.unregisterConfigControl(configControl)
|
|
|
|
function configurePanel(panel) {
|
|
if (!panel) return
|
|
panel.bar = root
|
|
panel.anchorItem = button
|
|
}
|
|
|
|
function openPanel() {
|
|
if (!panelItem) {
|
|
openWhenReady = true
|
|
return
|
|
}
|
|
panelItem.open()
|
|
}
|
|
|
|
function togglePanel() {
|
|
if (!panelItem) {
|
|
openPanel()
|
|
return
|
|
}
|
|
panelItem.toggle()
|
|
}
|
|
|
|
WidgetButton {
|
|
id: button
|
|
|
|
anchors.fill: parent
|
|
bar: root
|
|
text: ""
|
|
keepSpace: true
|
|
concealed: !configControl.revealed
|
|
dimmed: configControl.revealed && !controlHover.hovered && !configControl.panelOpen
|
|
interactive: configControl.revealed
|
|
horizontalMargin: 6.5
|
|
verticalPadding: 6
|
|
tooltipText: "Bar config"
|
|
onPressed: function(b) {
|
|
if (b === Qt.LeftButton) configControl.togglePanel()
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: configPanelLoader
|
|
|
|
active: true
|
|
source: Qt.resolvedUrl("BarConfigPanel.qml")
|
|
onLoaded: {
|
|
configControl.configurePanel(item)
|
|
if (configControl.openWhenReady) {
|
|
configControl.openWhenReady = false
|
|
item.open()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component ModuleList: Loader {
|
|
id: moduleListRoot
|
|
|
|
property var entries: []
|
|
property string region: ""
|
|
|
|
visible: entries.length > 0
|
|
sourceComponent: root.vertical ? verticalModuleList : horizontalModuleList
|
|
width: item ? item.implicitWidth : 0
|
|
height: item ? item.implicitHeight : 0
|
|
|
|
Component {
|
|
id: horizontalModuleList
|
|
|
|
Row {
|
|
spacing: 0
|
|
|
|
Repeater {
|
|
model: moduleListRoot.entries
|
|
|
|
ModuleSlot {
|
|
required property var modelData
|
|
entry: modelData
|
|
region: moduleListRoot.region
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: verticalModuleList
|
|
|
|
Column {
|
|
spacing: 0
|
|
|
|
Repeater {
|
|
model: moduleListRoot.entries
|
|
|
|
ModuleSlot {
|
|
required property var modelData
|
|
entry: modelData
|
|
region: moduleListRoot.region
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component ModuleSlot: Item {
|
|
id: slot
|
|
|
|
required property var entry
|
|
property string region: ""
|
|
readonly property string moduleName: root.entryId(entry)
|
|
readonly property var moduleSettings: root.entrySettings(entry)
|
|
readonly property string customType: root.customModuleType(entry)
|
|
// 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) return null
|
|
var registryName = root.canonicalWidgetId(moduleName)
|
|
return w[registryName] ? w[registryName].component : null
|
|
}
|
|
readonly property bool qmlCustom: customType === "qml"
|
|
readonly property bool commandCustom: customType === "command"
|
|
readonly property bool registered: registryComponent !== null
|
|
readonly property var activeItem: {
|
|
if (registered) return registryLoader.item
|
|
if (qmlCustom) return qmlLoader.item
|
|
return componentLoader.item
|
|
}
|
|
readonly property bool hovered: moduleHover.hovered
|
|
readonly property bool dragSource: root.barDragSource === slot
|
|
readonly property bool panelOpen: root.activePopout === slot.activeItem
|
|
readonly property real openIndicatorInlineOffset: {
|
|
var item = slot.activeItem
|
|
if (!item || !("openIndicatorInlineOffset" in item)) return 0
|
|
var offset = Number(item.openIndicatorInlineOffset)
|
|
return isFinite(offset) ? offset : 0
|
|
}
|
|
|
|
implicitWidth: activeItem && activeItem.visible ? (root.vertical ? root.barSize : activeItem.implicitWidth) : 0
|
|
implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0
|
|
width: implicitWidth
|
|
height: implicitHeight
|
|
z: modulePointer.dragging ? 100 : 0
|
|
|
|
Component.onCompleted: root.registerModuleSlot(slot)
|
|
Component.onDestruction: {
|
|
if (root.barDragSource === slot) root.clearBarDrag()
|
|
root.unregisterModuleSlot(slot)
|
|
}
|
|
|
|
HoverHandler { id: moduleHover }
|
|
|
|
BorderSurface {
|
|
visible: slot.dragSource
|
|
anchors.fill: parent
|
|
anchors.margins: Style.space(1)
|
|
color: root.transparent ? "transparent" : root.background
|
|
borderSpec: Border.flat(root.barForeground, 1)
|
|
radius: Math.min(Style.cornerRadius, height / 2)
|
|
opacity: root.transparent ? 0.22 : 0.32
|
|
}
|
|
|
|
Loader {
|
|
id: componentLoader
|
|
active: !slot.qmlCustom && !slot.registered
|
|
sourceComponent: slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent
|
|
anchors.fill: parent
|
|
opacity: slot.dragSource ? 0.22 : 1.0
|
|
onLoaded: {
|
|
slot.injectProps()
|
|
Qt.callLater(slot.injectProps)
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: registryLoader
|
|
active: slot.registered
|
|
sourceComponent: slot.registered ? slot.registryComponent : null
|
|
anchors.fill: parent
|
|
opacity: slot.dragSource ? 0.22 : 1.0
|
|
onLoaded: {
|
|
slot.injectProps()
|
|
Qt.callLater(slot.injectProps)
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: qmlLoader
|
|
active: slot.qmlCustom
|
|
source: slot.qmlCustom ? root.customModuleSource(slot.entry) : ""
|
|
anchors.fill: parent
|
|
opacity: slot.dragSource ? 0.22 : 1.0
|
|
onLoaded: {
|
|
slot.injectProps()
|
|
Qt.callLater(slot.injectProps)
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: openPanelIndicator
|
|
|
|
readonly property int inset: Style.space(2)
|
|
|
|
visible: opacity > 0
|
|
opacity: slot.panelOpen && !slot.dragSource ? 0.9 : 0
|
|
color: Color.accent
|
|
radius: Math.min(width, height) / 2
|
|
width: root.vertical ? Style.space(2) : Math.max(Style.space(10), Math.round(parent.width * 0.55))
|
|
height: root.vertical ? Math.max(Style.space(10), Math.round(parent.height * 0.55)) : Style.space(2)
|
|
x: root.vertical
|
|
? (root.position === "left" ? parent.width - width - inset : inset)
|
|
: (slot.openIndicatorInlineOffset === 0 ? Math.round((parent.width - width) / 2) : (parent.width - width) / 2 + slot.openIndicatorInlineOffset)
|
|
y: root.vertical
|
|
? (slot.openIndicatorInlineOffset === 0 ? Math.round((parent.height - height) / 2) : (parent.height - height) / 2 + slot.openIndicatorInlineOffset)
|
|
: (root.position === "top" ? parent.height - height - inset : inset)
|
|
z: 50
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: 120; easing.type: Easing.OutCubic }
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
visible: !root.vertical && root.barDragTarget === slot && !root.barDragAfter
|
|
anchors {
|
|
left: parent.left
|
|
top: parent.top
|
|
bottom: parent.bottom
|
|
}
|
|
width: 2
|
|
color: root.barForeground
|
|
opacity: 0.9
|
|
}
|
|
|
|
Rectangle {
|
|
visible: !root.vertical && root.barDragTarget === slot && root.barDragAfter
|
|
anchors {
|
|
right: parent.right
|
|
top: parent.top
|
|
bottom: parent.bottom
|
|
}
|
|
width: 2
|
|
color: root.barForeground
|
|
opacity: 0.9
|
|
}
|
|
|
|
Rectangle {
|
|
visible: root.vertical && root.barDragTarget === slot && !root.barDragAfter
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
top: parent.top
|
|
}
|
|
height: 2
|
|
color: root.barForeground
|
|
opacity: 0.9
|
|
}
|
|
|
|
Rectangle {
|
|
visible: root.vertical && root.barDragTarget === slot && root.barDragAfter
|
|
anchors {
|
|
left: parent.left
|
|
right: parent.right
|
|
bottom: parent.bottom
|
|
}
|
|
height: 2
|
|
color: root.barForeground
|
|
opacity: 0.9
|
|
}
|
|
|
|
MouseArea {
|
|
id: modulePointer
|
|
|
|
property bool dragging: false
|
|
property bool suppressClick: false
|
|
property real pressedX: 0
|
|
property real pressedY: 0
|
|
readonly property bool canReorder: root.shell && typeof root.shell.mutateShellConfig === "function"
|
|
readonly property real dragThreshold: Style.space(4)
|
|
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton
|
|
enabled: slot.visible && slot.width > 0 && slot.height > 0
|
|
propagateComposedEvents: true
|
|
cursorShape: root.moduleClickTargetAt(slot, mouseX, mouseY) ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
// Do not assign drag.target here: ModuleSlot is owned by Row/Column
|
|
// positioners, and mutating slot.x/slot.y can leave stale offsets that
|
|
// make neighboring modules overlap after a small aborted drag.
|
|
|
|
onPressed: function(mouse) {
|
|
dragging = false
|
|
suppressClick = false
|
|
pressedX = mouse.x
|
|
pressedY = mouse.y
|
|
root.clearBarDrag()
|
|
}
|
|
|
|
onPositionChanged: function(mouse) {
|
|
if (!canReorder || !(mouse.buttons & Qt.LeftButton)) return
|
|
|
|
var distance = Math.abs(mouse.x - pressedX) + Math.abs(mouse.y - pressedY)
|
|
if (distance >= dragThreshold) {
|
|
if (!dragging) {
|
|
root.barDragWindow = root.targetWindow(slot.activeItem) || root.targetWindow(slot)
|
|
root.barDragScreen = root.barDragWindow ? root.barDragWindow.screen : null
|
|
root.barDragOffsetX = pressedX
|
|
root.barDragOffsetY = pressedY
|
|
root.captureBarDragGhost(slot)
|
|
root.barDragSource = slot
|
|
}
|
|
dragging = true
|
|
root.hideTooltip(slot.activeItem)
|
|
}
|
|
|
|
if (dragging) {
|
|
var scenePoint = slot.mapToItem(null, mouse.x, mouse.y)
|
|
var screenPoint = root.barDragScreenPoint(scenePoint)
|
|
root.barDragSceneX = scenePoint.x
|
|
root.barDragSceneY = scenePoint.y
|
|
root.barDragScreenX = screenPoint.x
|
|
root.barDragScreenY = screenPoint.y
|
|
|
|
var drop = root.moduleDropAtScene(scenePoint, slot)
|
|
root.barDragTarget = drop ? drop.slot : null
|
|
root.barDragAfter = drop ? drop.after : false
|
|
}
|
|
}
|
|
|
|
onReleased: function(mouse) {
|
|
var wasDragging = dragging
|
|
var targetSlot = root.barDragTarget
|
|
var afterTarget = root.barDragAfter
|
|
|
|
if (wasDragging) suppressClick = true
|
|
|
|
dragging = false
|
|
root.clearBarDrag()
|
|
|
|
if (wasDragging && targetSlot) {
|
|
root.dropBarModuleAtTarget(slot, targetSlot, afterTarget)
|
|
mouse.accepted = true
|
|
} else if (!wasDragging) {
|
|
mouse.accepted = false
|
|
}
|
|
}
|
|
|
|
onCanceled: {
|
|
dragging = false
|
|
suppressClick = false
|
|
root.clearBarDrag()
|
|
}
|
|
|
|
onClicked: function(mouse) {
|
|
if (suppressClick) {
|
|
suppressClick = false
|
|
mouse.accepted = true
|
|
return
|
|
}
|
|
|
|
if (!root.pressModuleClickTarget(slot, mouse.button, mouse.x, mouse.y)) mouse.accepted = false
|
|
}
|
|
}
|
|
|
|
onActiveItemChanged: Qt.callLater(injectProps)
|
|
onModuleSettingsChanged: injectProps()
|
|
|
|
function injectProps() {
|
|
var target = activeItem
|
|
if (!target) return
|
|
if ("bar" in target) target.bar = root
|
|
if ("moduleName" in target) target.moduleName = moduleName
|
|
if ("settings" in target) target.settings = moduleSettings
|
|
}
|
|
|
|
Component {
|
|
id: customCommandModuleComponent
|
|
CustomCommandModule { entry: slot.entry }
|
|
}
|
|
}
|
|
|
|
component CustomCommandModule: WidgetButton {
|
|
id: customRoot
|
|
|
|
required property var entry
|
|
readonly property string moduleName: root.entryId(entry)
|
|
readonly property var settings: root.entrySettings(entry)
|
|
property string outputText: ""
|
|
property string outputTooltip: ""
|
|
property bool outputActive: false
|
|
|
|
function setting(name, fallback) {
|
|
var value = settings ? settings[name] : undefined
|
|
return value === undefined || value === null ? fallback : value
|
|
}
|
|
|
|
function update(raw) {
|
|
var data = Util.parseModuleJson(raw)
|
|
var klass = data.class || data.alt || ""
|
|
|
|
outputText = data.text || String(raw || "").trim()
|
|
outputTooltip = data.tooltip || String(setting("tooltip", ""))
|
|
outputActive = klass === "active" || (Array.isArray(klass) && klass.indexOf("active") !== -1)
|
|
}
|
|
|
|
bar: root
|
|
text: outputText || String(setting("text", ""))
|
|
tooltipText: outputTooltip || String(setting("tooltip", ""))
|
|
active: outputActive
|
|
keepSpace: setting("keepSpace", false) === true
|
|
horizontalMargin: Number(setting("horizontalMargin", 7.5))
|
|
verticalPadding: Number(setting("verticalPadding", 6))
|
|
fontSize: Number(setting("fontSize", 12))
|
|
|
|
onPressed: function(button) {
|
|
var command = ""
|
|
if (button === Qt.RightButton)
|
|
command = String(setting("onRightClick", ""))
|
|
else if (button === Qt.MiddleButton)
|
|
command = String(setting("onMiddleClick", ""))
|
|
else
|
|
command = String(setting("onClick", ""))
|
|
|
|
if (command) root.run(command)
|
|
}
|
|
|
|
Process {
|
|
id: customProc
|
|
command: ["bash", "-lc", String(customRoot.setting("exec", ""))]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: customRoot.update(text)
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
interval: Math.max(1, Number(customRoot.setting("interval", 5))) * 1000
|
|
running: String(customRoot.setting("exec", "")) !== ""
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: root.runProcess(customProc)
|
|
}
|
|
}
|
|
}
|