* Pick window transparency from Appearance > Window Transparency Transparent, the default, keeps today's windows. Opaque turns the windows Omarchy makes transparent solid. Glassy is the frosted glass Starship (#14726): its blur behind every see-through window and shell surface, and a see-through, blurred background in the theme's terminal files, since foot turns compositor blur down and a Hyprland opacity rule fades text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Leave window transparency to Appearance when animations are off Turning animations off no longer makes windows opaque. VM installs start with opaque windows instead, and a migration keeps them opaque for anyone who already has animations off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add a Glassy bar background, under Menu Bar > Bar Background Menu Bar > Bar Background picks Opaque Bar, Glassy Bar or Clear Bar, and Indicators becomes Bar Indicators to match. Glassy keeps the theme background faintly and Hyprland blurs what is behind it, with the same blur as Glassy windows. omarchy bar background opaque|glassy|none|cycle sets it and prints it when given nothing. omarchy bar transparent true|false|toggle stays for compatibility, as none and opaque. Double-clicking the bar cycles through the three backgrounds the same way, from clear to glassy to opaque. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Stop when a saved corner choice can't be removed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Make btop follow glassy windows --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2214 lines
75 KiB
QML
2214 lines
75 KiB
QML
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import qs.Commons
|
|
import qs.Commons as Commons
|
|
import qs.Ui
|
|
import "BarModel.js" as BarModel
|
|
|
|
Item {
|
|
id: root
|
|
|
|
// The omarchy-shell host injects omarchyPath from OMARCHY_PATH.
|
|
property string omarchyPath: Quickshell.env("OMARCHY_PATH")
|
|
// Injected by the host shell so bar slots can resolve enabled widgets.
|
|
property var barWidgetRegistry: fallbackBarWidgetRegistry
|
|
// Read-only registry view for third-party full bars; the built-in bar does
|
|
// not otherwise need it, but declaring it keeps clone construction atomic.
|
|
property var pluginRegistry: null
|
|
// 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.
|
|
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
|
|
QtObject {
|
|
id: fallbackBarWidgetRegistry
|
|
property var widgets: ({})
|
|
property int revision: 0
|
|
function metadataFor(id) { return null }
|
|
}
|
|
// Mirrors the on-disk `bar-off` flag so the user can hide the bar without
|
|
// killing the entire shell. Hidden panels stay mapped but park off-screen
|
|
// without an exclusion zone; 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",
|
|
background: "opaque",
|
|
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
|
|
// A see-through bar that Hyprland blurs behind (the bar-glassy toggle), so
|
|
// its theme background stays faintly. A transparent bar wins over it.
|
|
property bool glassy: false
|
|
// The look the blur behind the bar was last synced for. A background set by
|
|
// editing shell.json skips the CLI, so the bar syncs Hyprland's blur itself.
|
|
property string glassSyncedLook: ""
|
|
property bool centerSectionHovered: false
|
|
// One bar surface exists per monitor and each reports into this count, so a
|
|
// pointer crossing from one monitor's bar to another's stays counted however
|
|
// the enter and leave interleave. A single shared bool would be left false by
|
|
// whichever event landed last.
|
|
property int barHoverCount: 0
|
|
// True while the pointer is over any bar, widgets included.
|
|
readonly property bool barHovered: barHoverCount > 0
|
|
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: Commons.Color.bar.text
|
|
property color themeContrastForeground: Commons.Color.background
|
|
property color transparentForeground: Commons.Color.bar.text
|
|
property color foreground: themeForeground
|
|
property color barForeground: useTransparentForeground ? transparentForeground : themeForeground
|
|
property bool foregroundAnimationEnabled: true
|
|
property color background: Commons.Color.bar.background
|
|
readonly property color glassBackground: Qt.rgba(background.r, background.g, background.b, Math.min(background.a, 0.35))
|
|
property color urgent: Commons.Color.bar.active
|
|
|
|
Behavior on barForeground { enabled: root.foregroundAnimationEnabled; ColorAnimation { duration: Style.duration(420); easing.type: Easing.OutCubic } }
|
|
Behavior on background { ColorAnimation { duration: Style.duration(420); easing.type: Easing.OutCubic } }
|
|
Behavior on urgent { ColorAnimation { duration: Style.duration(420); easing.type: Easing.OutCubic } }
|
|
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 var barDragTargetGeometry: 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 bool barMoveActive: false
|
|
property string barMoveCandidate: ""
|
|
property var barMoveWindow: null
|
|
property var barMoveScreen: null
|
|
property var clickTargets: []
|
|
property var moduleSlots: []
|
|
property var pluginBarApis: ({})
|
|
// target -> { target, pluginId, clickTarget, popout }. Keyed by object so a
|
|
// registration edits one entry in place instead of copying and rescanning an
|
|
// array in QML; Qt's Map still finds a key by a native linear scan.
|
|
readonly property var pluginObjectOwners: new Map()
|
|
property bool pluginBarApiSyncQueued: false
|
|
|
|
Component {
|
|
id: pluginBarApiComponent
|
|
PluginBarApi { }
|
|
}
|
|
|
|
function publicLayoutConfig() {
|
|
return JSON.parse(JSON.stringify(root.layoutConfig || {}))
|
|
}
|
|
|
|
function bindPluginBarApi(api) {
|
|
if (!api) return
|
|
api.foreground = Qt.binding(function() { return root.foreground })
|
|
api.barForeground = Qt.binding(function() { return root.barForeground })
|
|
api.background = Qt.binding(function() { return root.background })
|
|
api.urgent = Qt.binding(function() { return root.urgent })
|
|
api.fontFamily = Qt.binding(function() { return root.fontFamily })
|
|
api.position = Qt.binding(function() { return root.position })
|
|
api.vertical = Qt.binding(function() { return root.vertical })
|
|
api.barSize = Qt.binding(function() { return root.barSize })
|
|
api.transparent = Qt.binding(function() { return root.transparent })
|
|
api.foregroundAnimationEnabled = Qt.binding(function() { return root.foregroundAnimationEnabled })
|
|
api.centerSectionRevealHeld = Qt.binding(function() { return root.centerSectionRevealHeld })
|
|
api._centerHoverRevealSuppressed = Qt.binding(function() { return root.centerHoverRevealSuppressed })
|
|
root.syncPluginBarApiObjects(api)
|
|
}
|
|
|
|
// Each api keeps its own detached copy of the layout. A flush serialises
|
|
// the layout once and passes the string in so it is not re-serialised for
|
|
// every plugin.
|
|
function syncPluginBarApiObjects(api, layoutSnapshot) {
|
|
if (!api) return
|
|
api.activePopout = root.pluginOwnsBarObject(api.pluginId, root.activePopout)
|
|
? root.activePopout : (root.activePopout ? api.foreignPopoutMarker : null)
|
|
api.clickTargets = root.pluginClickTargets(api.pluginId)
|
|
api.layoutConfig = layoutSnapshot !== undefined
|
|
? JSON.parse(layoutSnapshot) : root.publicLayoutConfig()
|
|
}
|
|
|
|
function pluginObjectRecord(target) {
|
|
return target ? (pluginObjectOwners.get(target) || null) : null
|
|
}
|
|
|
|
function markPluginObject(pluginId, target, role) {
|
|
var key = String(pluginId || "")
|
|
if (!key || !target) return false
|
|
var record = root.pluginObjectRecord(target)
|
|
if (record && record.pluginId !== key) return false
|
|
if (!record) {
|
|
record = { target: target, pluginId: key, clickTarget: false, popout: false }
|
|
pluginObjectOwners.set(target, record)
|
|
}
|
|
record[role] = true
|
|
return true
|
|
}
|
|
|
|
function unmarkPluginObject(pluginId, target, role) {
|
|
var key = String(pluginId || "")
|
|
var record = root.pluginObjectRecord(target)
|
|
if (!record || record.pluginId !== key) return
|
|
record[role] = false
|
|
if (!record.clickTarget && !record.popout) pluginObjectOwners.delete(target)
|
|
}
|
|
|
|
function pluginOwnsBarObject(pluginId, target) {
|
|
var record = target ? root.pluginObjectRecord(target) : null
|
|
return !!record && record.pluginId === String(pluginId || "")
|
|
}
|
|
|
|
function pluginClickTargets(pluginId) {
|
|
var out = []
|
|
for (var i = 0; i < root.clickTargets.length; i++) {
|
|
var target = root.clickTargets[i]
|
|
if (root.pluginOwnsBarObject(pluginId, target)) out.push(target)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Every click target registration, popout change and layout change used to
|
|
// resync every plugin api synchronously. Startup alone is hundreds of
|
|
// registrations, each walking every api and every target, so coalesce them
|
|
// into one resync per event-loop turn (as onModuleSlotsChanged already does
|
|
// for prunePluginBarApis). bindPluginBarApi still syncs a brand-new api
|
|
// directly so a widget never sees an empty api on its first read.
|
|
function schedulePluginBarApiSync() {
|
|
if (pluginBarApiSyncQueued) return
|
|
pluginBarApiSyncQueued = true
|
|
Qt.callLater(root.syncAllPluginBarApiObjects)
|
|
}
|
|
|
|
function syncAllPluginBarApiObjects() {
|
|
pluginBarApiSyncQueued = false
|
|
var layoutSnapshot = JSON.stringify(root.layoutConfig || {})
|
|
for (var id in pluginBarApis) {
|
|
var api = pluginBarApis[id]
|
|
if (api) root.syncPluginBarApiObjects(api, layoutSnapshot)
|
|
}
|
|
}
|
|
|
|
function registerPluginClickTarget(pluginId, target) {
|
|
if (!root.markPluginObject(pluginId, target, "clickTarget")) return
|
|
root.registerClickTarget(target)
|
|
}
|
|
|
|
function unregisterPluginClickTarget(pluginId, target) {
|
|
if (!root.pluginOwnsBarObject(pluginId, target)) return
|
|
root.unregisterClickTarget(target)
|
|
root.unmarkPluginObject(pluginId, target, "clickTarget")
|
|
}
|
|
|
|
// A plugin may read api.activePopout right after asking for its popout, so
|
|
// its own api is brought up to date inline; everyone else waits for the
|
|
// coalesced resync.
|
|
function requestPluginPopout(pluginId, owner) {
|
|
if (!root.markPluginObject(pluginId, owner, "popout")) return
|
|
root.requestPopout(owner)
|
|
root.syncPluginBarApiObjects(pluginBarApis[String(pluginId || "")])
|
|
}
|
|
|
|
function releasePluginPopout(pluginId, owner) {
|
|
if (!root.pluginOwnsBarObject(pluginId, owner)) return
|
|
root.releasePopout(owner)
|
|
root.unmarkPluginObject(pluginId, owner, "popout")
|
|
root.syncPluginBarApiObjects(pluginBarApis[String(pluginId || "")])
|
|
}
|
|
|
|
function pluginBarApiFor(pluginId, moduleName, registered) {
|
|
var key = String(pluginId || "")
|
|
if (!key) return null
|
|
|
|
var pluginShell = null
|
|
if (registered && root.shell && typeof root.shell.pluginShellForId === "function") {
|
|
// Only the trusted built-in bar receives ShellRoot and can request a
|
|
// service-capable facade for the widget it is instantiating.
|
|
pluginShell = root.shell.pluginShellForId(moduleName)
|
|
} else if (root.shell && typeof root.shell.pluginShellForBarEntry === "function") {
|
|
// Replacement bars receive a service-less entry facade. Giving an
|
|
// untrusted bar a generic facade factory would let it retrieve another
|
|
// third-party plugin's live service object.
|
|
pluginShell = root.shell.pluginShellForBarEntry(key, moduleName)
|
|
}
|
|
|
|
if (pluginBarApis[key]) {
|
|
pluginBarApis[key].shell = pluginShell
|
|
return pluginBarApis[key]
|
|
}
|
|
|
|
var api = pluginBarApiComponent.createObject(null, {
|
|
pluginId: key,
|
|
moduleName: String(moduleName || ""),
|
|
shell: pluginShell,
|
|
_showTooltip: function(target, text) { root.showTooltip(target, text) },
|
|
_hideTooltip: function(target) { root.hideTooltip(target) },
|
|
_registerClickTarget: function(target) { root.registerPluginClickTarget(key, target) },
|
|
_unregisterClickTarget: function(target) { root.unregisterPluginClickTarget(key, target) },
|
|
_requestPopout: function(owner) { root.requestPluginPopout(key, owner) },
|
|
_releasePopout: function(owner) { root.releasePluginPopout(key, owner) },
|
|
_switchPanelFrom: function(owner, direction) { return root.switchPanelFrom(owner, direction) },
|
|
_targetBelongsToWindow: function(target, window) { return root.targetBelongsToWindow(target, window) },
|
|
_moduleWidgets: function(requestedId) {
|
|
return String(requestedId || "") === String(moduleName || "")
|
|
? root.moduleWidgets(moduleName) : []
|
|
},
|
|
_run: function(command) { root.run(command) },
|
|
_setCenterHoverRevealSuppressed: function(value) {
|
|
root.centerHoverRevealSuppressed = !!value
|
|
}
|
|
})
|
|
if (!api) return null
|
|
root.bindPluginBarApi(api)
|
|
|
|
var next = ({})
|
|
for (var id in pluginBarApis) next[id] = pluginBarApis[id]
|
|
next[key] = api
|
|
pluginBarApis = next
|
|
return api
|
|
}
|
|
|
|
function pluginBarApiUsed(pluginId) {
|
|
for (var i = 0; i < moduleSlots.length; i++) {
|
|
var slot = moduleSlots[i]
|
|
if (slot && slot.pluginApiId === pluginId) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
function releasePluginObjects(pluginId) {
|
|
var owned = Array.from(pluginObjectOwners.values())
|
|
for (var i = 0; i < owned.length; i++) {
|
|
var record = owned[i]
|
|
if (!record || record.pluginId !== pluginId) continue
|
|
if (record.clickTarget) root.unregisterClickTarget(record.target)
|
|
if (record.popout && root.activePopout === record.target) root.releasePopout(record.target)
|
|
pluginObjectOwners.delete(record.target)
|
|
}
|
|
}
|
|
|
|
function prunePluginBarApis() {
|
|
var next = ({})
|
|
for (var id in pluginBarApis) {
|
|
var api = pluginBarApis[id]
|
|
if (root.pluginBarApiUsed(id)) {
|
|
next[id] = api
|
|
continue
|
|
}
|
|
root.releasePluginObjects(id)
|
|
if (api && typeof api.destroy === "function") api.destroy()
|
|
}
|
|
pluginBarApis = next
|
|
}
|
|
|
|
onActivePopoutChanged: schedulePluginBarApiSync()
|
|
onClickTargetsChanged: schedulePluginBarApiSync()
|
|
onLayoutConfigChanged: schedulePluginBarApiSync()
|
|
onModuleSlotsChanged: Qt.callLater(prunePluginBarApis)
|
|
|
|
Component.onDestruction: {
|
|
for (var id in pluginBarApis) {
|
|
root.releasePluginObjects(id)
|
|
if (pluginBarApis[id] && typeof pluginBarApis[id].destroy === "function")
|
|
pluginBarApis[id].destroy()
|
|
}
|
|
pluginBarApis = ({})
|
|
}
|
|
|
|
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 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
|
|
barDragTargetGeometry = null
|
|
barDragAfter = false
|
|
barDragSceneX = 0
|
|
barDragSceneY = 0
|
|
barDragScreenX = 0
|
|
barDragScreenY = 0
|
|
barDragOffsetX = 0
|
|
barDragOffsetY = 0
|
|
}
|
|
|
|
function windowScreenPoint(scenePoint, window) {
|
|
var x = scenePoint ? scenePoint.x : 0
|
|
var y = scenePoint ? scenePoint.y : 0
|
|
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 barDragScreenPoint(scenePoint) {
|
|
return windowScreenPoint(scenePoint, barDragWindow)
|
|
}
|
|
|
|
function dropMarkerRect(slot, after) {
|
|
if (!slot) return null
|
|
|
|
try {
|
|
var slotPoint = slot.mapToItem(null, 0, 0)
|
|
var screenPoint = barDragScreenPoint(slotPoint)
|
|
var thickness = Style.spacing.xs
|
|
if (vertical) {
|
|
return {
|
|
x: screenPoint.x,
|
|
y: screenPoint.y + (after ? slot.height : 0) - thickness / 2,
|
|
width: slot.width,
|
|
height: thickness
|
|
}
|
|
}
|
|
|
|
return {
|
|
x: screenPoint.x + (after ? slot.width : 0) - thickness / 2,
|
|
y: screenPoint.y,
|
|
width: thickness,
|
|
height: slot.height
|
|
}
|
|
} catch (e) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Split the screen along its diagonals (in normalized space, so widescreens
|
|
// don't bias toward left/right): whichever triangle holds the cursor names
|
|
// the candidate edge.
|
|
function nearestScreenEdge(point, screen) {
|
|
var nx = screen.width > 0 ? Util.clamp(point.x / screen.width, 0, 1) : 0.5
|
|
var ny = screen.height > 0 ? Util.clamp(point.y / screen.height, 0, 1) : 0.5
|
|
|
|
var edge = "top"
|
|
var best = ny
|
|
if (1 - ny < best) { edge = "bottom"; best = 1 - ny }
|
|
if (nx < best) { edge = "left"; best = nx }
|
|
if (1 - nx < best) { edge = "right"; best = 1 - nx }
|
|
return edge
|
|
}
|
|
|
|
function beginBarMove(window) {
|
|
barMoveWindow = window
|
|
barMoveScreen = window ? window.screen : null
|
|
barMoveCandidate = position
|
|
barMoveActive = true
|
|
}
|
|
|
|
function updateBarMove(screenPoint) {
|
|
if (!barMoveActive || !barMoveScreen) return
|
|
barMoveCandidate = nearestScreenEdge(screenPoint, barMoveScreen)
|
|
}
|
|
|
|
function clearBarMove() {
|
|
barMoveActive = false
|
|
barMoveCandidate = ""
|
|
barMoveWindow = null
|
|
barMoveScreen = null
|
|
}
|
|
|
|
function finishBarMove() {
|
|
var edge = barMoveCandidate
|
|
if (!barMoveActive || !edge || edge === position) {
|
|
clearBarMove()
|
|
return
|
|
}
|
|
|
|
clearBarMove()
|
|
setBarPosition(edge)
|
|
}
|
|
|
|
function setBarPosition(value) {
|
|
var next = normalizePosition(value)
|
|
if (root.shell && typeof root.shell.mutateShellConfig === "function") {
|
|
root.shell.mutateShellConfig(function(config) {
|
|
if (!Util.isPlainObject(config.bar)) config.bar = {}
|
|
config.bar.position = next
|
|
})
|
|
} else {
|
|
root.position = next
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
// Display cutouts (a camera notch) the platform's own package describes in
|
|
// the fixed platform root, which no environment variable moves. Most machines
|
|
// have none. It is read as the shell starts (blockLoading), so a bar surface
|
|
// knows before it maps whether its screen may have one. See
|
|
// BarModel.parseCutouts.
|
|
readonly property var displayCutouts: displayCutoutsFile.missing ? [] : BarModel.parseCutouts(displayCutoutsFile.text())
|
|
FileView {
|
|
id: displayCutoutsFile
|
|
|
|
// A file that went away describes nothing, whatever text() still holds.
|
|
property bool missing: false
|
|
|
|
path: "/usr/share/omarchy-platform/display-cutouts.json"
|
|
blockLoading: true
|
|
watchChanges: true
|
|
printErrors: false
|
|
onLoaded: missing = false
|
|
onLoadFailed: missing = true
|
|
onFileChanged: reload()
|
|
}
|
|
|
|
// The physical mode Hyprland reports for a screen, as hyprctl monitors does.
|
|
// Qt's whole-number devicePixelRatio can't rebuild it at a fractional scale.
|
|
function panelModeFor(screen) {
|
|
var monitor = screen ? Hyprland.monitorFor(screen) : null
|
|
return monitor ? ({
|
|
width: monitor.width,
|
|
height: monitor.height,
|
|
transform: monitor.lastIpcObject ? monitor.lastIpcObject.transform : 0
|
|
}) : null
|
|
}
|
|
|
|
// How thick the bar is on a screen. A top bar shorter than a panel's camera
|
|
// cutout leaves a sliver of every window peeking out beside the camera, so
|
|
// the cutout is this panel's minimum sensible top-bar height. An
|
|
// intentionally taller bar still wins.
|
|
function thicknessFor(screen) {
|
|
if (root.vertical || !screen) return root.barSize
|
|
return Math.max(root.barSize, BarModel.notchFloor(root.displayCutouts, root.position, screen.name, screen.width, screen.height, screen.devicePixelRatio, root.panelModeFor(screen), Style.bar.notchHeight))
|
|
}
|
|
|
|
// Each screen's bar thickness, by screen name: barSize, or more where a notch
|
|
// floor raises a top bar. Toasts and plugins clear the bar by it; barSize
|
|
// stays the configured size the widgets are drawn at.
|
|
readonly property var screenBarSizes: {
|
|
var sizes = ({})
|
|
var screens = Quickshell.screens
|
|
for (var i = 0; i < screens.length; i++) sizes[screens[i].name] = root.thicknessFor(screens[i])
|
|
return sizes
|
|
}
|
|
|
|
function barSizeFor(screenName) {
|
|
return BarModel.barSizeFor(root.screenBarSizes, screenName, root.barSize)
|
|
}
|
|
|
|
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)
|
|
// opaque, glassy or none; a config from before background says
|
|
// transparent instead.
|
|
var look = config.background || (config.transparent === true ? "none" : "opaque")
|
|
setRequestedTransparency(look === "none")
|
|
// A bar going clear waits for its text color before dropping its
|
|
// background, so a glassy one stays glassy until then rather than
|
|
// flashing opaque. Once clear, transparent wins over a stale glassy.
|
|
if (look !== "none") glassy = look === "glassy"
|
|
if (look !== glassSyncedLook) {
|
|
glassSyncedLook = look
|
|
Util.execDetached("omarchy-bar sync-glass")
|
|
}
|
|
centerAnchor = Util.canonicalWidgetId(config.centerAnchor || "")
|
|
|
|
// layoutEntries feeds plain JS arrays to the module Repeaters, and QML
|
|
// cannot diff those: reassigning layoutConfig rebuilds every widget on
|
|
// every monitor. When a shell.json write only changed inline widget
|
|
// settings, patch the live layout and running widgets in place instead.
|
|
var next = normalizeLayout(config.layout)
|
|
var delta = BarModel.inlineSettingsDelta(layoutConfig, next)
|
|
if (delta) {
|
|
applySettingsDelta(delta)
|
|
return
|
|
}
|
|
layoutConfig = next
|
|
barConfigSerial++
|
|
}
|
|
|
|
function applySettingsDelta(delta) {
|
|
for (var i = 0; i < delta.length; i++) {
|
|
var change = delta[i]
|
|
layoutConfig[change.region][change.index] = change.entry
|
|
var settings = entrySettings(change.entry)
|
|
for (var s = 0; s < moduleSlots.length; s++) {
|
|
var slot = moduleSlots[s]
|
|
if (!slot || slot.region !== change.region || slot.moduleName !== entryId(change.entry)) continue
|
|
var item = slot.activeItem
|
|
if (item && "settings" in item) item.settings = settings
|
|
}
|
|
}
|
|
}
|
|
|
|
onBarConfigChanged: applyBarConfig()
|
|
|
|
function layoutEntries(region) {
|
|
var serial = barConfigSerial
|
|
var entries = layoutConfig ? layoutConfig[region] : null
|
|
return Array.isArray(entries) ? entries : []
|
|
}
|
|
|
|
// Tab order for the panels in one bar region. Scoped to a single bar surface
|
|
// so tabbing walks the bar the open panel belongs to instead of hopping the
|
|
// panel to another monitor's copy of the same widget.
|
|
function panelNavigationSlots(region, window) {
|
|
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
|
|
if (window && !sameWindow(slotWindow(slot), window)) 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
|
|
}
|
|
|
|
// The Nth panel in a bar region, counted the way the bar reads: layout order,
|
|
// and only the panels actually on screen. A widget with no panel (the tray)
|
|
// and one that is hiding itself are passed over, so the number lands on the
|
|
// Nth panel icon the user can see rather than the Nth layout entry.
|
|
// One-based, because it exists for hotkeys; anything else lands on no slot.
|
|
//
|
|
// Counting any bar surface is enough: every monitor lays its bar out from the
|
|
// one layout, and summoning the id routes through pickPanelSlot, which opens
|
|
// the focused monitor's copy whichever surface was counted.
|
|
function panelWidgetIdAt(region, index) {
|
|
var slots = panelNavigationSlots(String(region || ""), null)
|
|
var slot = slots[Math.round(Number(index)) - 1]
|
|
return slot ? String(slot.moduleName || "") : ""
|
|
}
|
|
|
|
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, slotWindow(currentSlot))
|
|
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
|
|
}
|
|
|
|
// Every live instance of a widget id. A bar surface is built per monitor, so
|
|
// a widget that appears once in the layout is still live once per screen.
|
|
function moduleWidgets(pluginId) {
|
|
var id = String(pluginId || "")
|
|
var items = []
|
|
if (!id) return items
|
|
for (var i = 0; i < moduleSlots.length; i++) {
|
|
var slot = moduleSlots[i]
|
|
if (!slot || !slot.activeItem || slot.moduleName !== id) continue
|
|
items.push(slot.activeItem)
|
|
}
|
|
return items
|
|
}
|
|
|
|
function slotScreenName(slot) {
|
|
var window = slotWindow(slot)
|
|
return window && window.screen ? String(window.screen.name || "") : ""
|
|
}
|
|
|
|
// The output Hyprland has focused, which is where a keyboard-summoned panel
|
|
// belongs. Empty until Hyprland reports one, which leaves panel routing on
|
|
// its per-monitor fallback rather than guessing at an output.
|
|
function focusedScreenName() {
|
|
var monitor = Hyprland.focusedMonitor
|
|
return monitor ? String(monitor.name || "") : ""
|
|
}
|
|
|
|
// 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 IPC handler
|
|
// that only reaches whichever per-monitor instance claimed the target.
|
|
function findPanelWidget(pluginId) {
|
|
var id = String(pluginId || "")
|
|
if (!id) return null
|
|
var candidates = []
|
|
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
|
|
candidates.push({ slot: slot, screenName: slotScreenName(slot), opened: item.opened === true })
|
|
}
|
|
// One copy per monitor, plus a zero-size placeholder for anchored center
|
|
// modules. See BarModel.pickPanelSlot for which one a hotkey acts on.
|
|
var chosen = BarModel.pickPanelSlot(candidates, focusedScreenName())
|
|
return chosen ? chosen.activeItem : 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()
|
|
|
|
// Revealing the indicators widens their section, which can slide a neighbour
|
|
// under a stationary pointer. Collapsing on that un-hover would move it back
|
|
// out and re-open the peek, so hold until the pointer leaves the bar.
|
|
function setCenterSectionHovered(hovered) {
|
|
centerSectionHovered = hovered
|
|
if (hovered) {
|
|
centerSectionRevealTimer.stop()
|
|
centerSectionRevealHeld = true
|
|
} else {
|
|
centerSectionRevealTimer.restart()
|
|
}
|
|
}
|
|
|
|
function setBarHovered(hovered) {
|
|
barHoverCount = Math.max(0, barHoverCount + (hovered ? 1 : -1))
|
|
if (barHoverCount === 0) centerSectionRevealTimer.restart()
|
|
}
|
|
|
|
function setCenterHoverRevealSuppressed(value) {
|
|
centerHoverRevealSuppressed = !!value
|
|
}
|
|
|
|
Timer {
|
|
id: centerSectionRevealTimer
|
|
interval: 120
|
|
// Collapse only. Opening the peek is the center section's own gesture, done
|
|
// in setCenterSectionHovered, so a timer left pending by a pointer that dipped
|
|
// off the bar and came back cannot reveal indicators it never pointed at.
|
|
onTriggered: if (!root.centerSectionHovered && !root.barHovered) root.centerSectionRevealHeld = false
|
|
}
|
|
|
|
function run(command) {
|
|
if (!command) return
|
|
|
|
Util.execDetached(command)
|
|
}
|
|
|
|
// Steps the bar background from clear to glassy to opaque and around. Through
|
|
// omarchy-bar, which keeps Hyprland's blur behind a glassy bar in step.
|
|
function cycleBackground() {
|
|
Util.execDetached("omarchy-bar background cycle")
|
|
}
|
|
|
|
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
|
|
if (sourceWindow && sourceWindow.contentItem) {
|
|
var barPoint = sourceWindow.contentItem.mapFromItem(null, scenePoint.x, scenePoint.y)
|
|
if (barPoint.x < 0 || barPoint.x > sourceWindow.contentItem.width ||
|
|
barPoint.y < 0 || barPoint.y > sourceWindow.contentItem.height)
|
|
return null
|
|
}
|
|
|
|
var candidates = []
|
|
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) {
|
|
}
|
|
|
|
candidates.push({
|
|
slot: slot,
|
|
x: slotPoint.x,
|
|
y: slotPoint.y,
|
|
width: slot.width,
|
|
height: slot.height
|
|
})
|
|
}
|
|
|
|
return BarModel.nearestDropTarget(candidates, scenePoint, root.vertical)
|
|
}
|
|
|
|
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-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()
|
|
}
|
|
|
|
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", "-c", "[[ -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
|
|
}
|
|
|
|
// The directory watch can permanently stop delivering events after flag
|
|
// changes land in quick succession, stranding the bar off screen until the
|
|
// shell restarts. `omarchy-toggle-bar` nudges this after flipping the flag
|
|
// so the probe re-reads it even when the watch has gone quiet.
|
|
ShellIpc {
|
|
target: "omarchy.bar"
|
|
|
|
// Start rather than restart: a probe already in flight was launched by the
|
|
// directory watch after the flag flipped, so its answer is current, and
|
|
// killing it here can swallow the result entirely.
|
|
function syncHidden(): void {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
Variants {
|
|
model: Quickshell.screens
|
|
|
|
delegate: Component {
|
|
BarMoveGhostPanel {
|
|
required property var modelData
|
|
|
|
screen: modelData
|
|
ghostScreen: modelData
|
|
}
|
|
}
|
|
}
|
|
|
|
component BarPanel: PanelWindow {
|
|
id: barWindow
|
|
|
|
// Hiding parks the bar just past its screen edge instead of unmapping it.
|
|
// Unmapping frees the layer surface and the whole scene graph, so every
|
|
// reveal has to rebuild them — new surface, re-shaped glyphs, re-uploaded
|
|
// textures — which measures ~150ms against ~20ms to tear down. Parking
|
|
// keeps the surface alive, so showing is only a margin change.
|
|
//
|
|
// A top bar that can't tell its notch floor yet waits for Hyprland's mode
|
|
// before it maps (see BarModel.cutoutPending), so it maps at its floor
|
|
// instead of growing once the windows are laid out. The wait ends once, for
|
|
// good: when the mode settles it, or after two seconds without one.
|
|
visible: !remapGuard.remapping && (cutoutWaitOver || !cutoutPending)
|
|
exclusionMode: root.barHidden ? ExclusionMode.Ignore : ExclusionMode.Auto
|
|
|
|
ScreenMoveRemap {
|
|
id: remapGuard
|
|
window: barWindow
|
|
}
|
|
|
|
// Parked by its full thickness, which a notch floor may make more than
|
|
// barSize, so no strip of it stays on screen.
|
|
margins {
|
|
top: root.barHidden && root.position === "top" ? -barWindow.thickness : 0
|
|
bottom: root.barHidden && root.position === "bottom" ? -barWindow.thickness : 0
|
|
left: root.barHidden && root.position === "left" ? -barWindow.thickness : 0
|
|
right: root.barHidden && root.position === "right" ? -barWindow.thickness : 0
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
readonly property var panelMode: root.panelModeFor(screen)
|
|
|
|
readonly property bool centerBesideRight: BarModel.centerBesideRight(root.displayCutouts, root.position, screen.name, screen.width, screen.height, screen.devicePixelRatio, panelMode)
|
|
|
|
readonly property bool cutoutPending: BarModel.cutoutPending(root.displayCutouts, root.position, screen.name, screen.width, screen.height, screen.devicePixelRatio, panelMode)
|
|
property bool cutoutWaitOver: false
|
|
onCutoutPendingChanged: if (!cutoutPending) cutoutWaitOver = true
|
|
Component.onCompleted: if (!cutoutPending) cutoutWaitOver = true
|
|
|
|
Timer {
|
|
interval: 2000
|
|
running: barWindow.cutoutPending && !barWindow.cutoutWaitOver
|
|
onTriggered: barWindow.cutoutWaitOver = true
|
|
}
|
|
|
|
// The same thickness the bar publishes for its screen (screenBarSizes).
|
|
readonly property int thickness: root.thicknessFor(screen)
|
|
|
|
implicitWidth: root.vertical ? thickness : 0
|
|
implicitHeight: root.vertical ? 0 : thickness
|
|
color: root.transparent ? "transparent" : (root.glassy ? root.glassBackground : root.background)
|
|
surfaceFormat.opaque: false
|
|
WlrLayershell.namespace: "omarchy-bar"
|
|
WlrLayershell.layer: WlrLayer.Top
|
|
|
|
Loader {
|
|
anchors.fill: parent
|
|
sourceComponent: root.vertical ? verticalBar : horizontalBar
|
|
|
|
// A child of the loader, not a sibling of the sections: an ancestor stays
|
|
// hovered while the pointer is over a widget, where a sibling would lose
|
|
// hover to the section the pointer entered.
|
|
HoverHandler {
|
|
onHoveredChanged: root.setBarHovered(hovered)
|
|
// Unplugging a monitor destroys its bar without a leave event, which
|
|
// would strand this surface's tally and hold the peek open for good.
|
|
Component.onDestruction: if (hovered) root.setBarHovered(false)
|
|
}
|
|
}
|
|
|
|
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: Commons.Color.tooltip.background
|
|
borderSpec: Border.surfaceSpec("tooltip", "border", Commons.Color.tooltip.border, 1)
|
|
radius: Style.cornerRadius
|
|
|
|
Text {
|
|
id: tooltipLabel
|
|
textFormat: Text.PlainText
|
|
anchors.centerIn: parent
|
|
text: root.tooltipText
|
|
color: Commons.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
|
|
entries: barWindow.centerBesideRight ? [] : root.layoutEntries("center")
|
|
}
|
|
|
|
LeftModules {
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Style.space(8)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
RightModules {
|
|
id: rightModules
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Style.space(8)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
// Keeps the center region, so settings pushes, drag and drop, and panel
|
|
// routing still address it. The gap lets a drop at the seam land in
|
|
// the section the pointer is nearer to.
|
|
ModuleList {
|
|
entries: barWindow.centerBesideRight ? root.layoutEntries("center") : []
|
|
region: "center"
|
|
anchors.right: rightModules.left
|
|
anchors.rightMargin: Style.space(4)
|
|
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
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
readonly property var targetRect: root.barDragTargetGeometry
|
|
|
|
visible: ghostWindow.active && targetRect !== null
|
|
x: targetRect ? Math.round(targetRect.x) : 0
|
|
y: targetRect ? Math.round(targetRect.y) : 0
|
|
width: targetRect ? targetRect.width : 0
|
|
height: targetRect ? targetRect.height : 0
|
|
color: Commons.Color.accent
|
|
radius: Math.min(width, height) / 2
|
|
}
|
|
}
|
|
|
|
component BarMoveGhostPanel: PanelWindow {
|
|
id: moveGhostWindow
|
|
|
|
required property var ghostScreen
|
|
readonly property bool screenMatches: root.barMoveScreen === ghostScreen ||
|
|
(root.barMoveScreen && ghostScreen && root.barMoveScreen.name && ghostScreen.name && root.barMoveScreen.name === ghostScreen.name)
|
|
visible: root.barMoveActive && screenMatches
|
|
color: "transparent"
|
|
exclusionMode: ExclusionMode.Ignore
|
|
WlrLayershell.namespace: "omarchy-bar-move-ghost"
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
|
|
anchors {
|
|
top: true
|
|
bottom: true
|
|
left: true
|
|
right: true
|
|
}
|
|
|
|
// Visual-only preview of the candidate edge. Keep the input region empty
|
|
// so the overlay never steals the gesture area's active pointer grab.
|
|
mask: Region {}
|
|
|
|
// One fixed-geometry slab per edge, crossfaded on candidate changes.
|
|
// Resizing a single slab between edges repaints mid-transition and
|
|
// flickers; fading between static ones does not.
|
|
Repeater {
|
|
model: ["top", "bottom", "left", "right"]
|
|
|
|
BorderSurface {
|
|
id: edgeSlab
|
|
|
|
required property string modelData
|
|
readonly property bool edgeVertical: modelData === "left" || modelData === "right"
|
|
readonly property int edgeSize: edgeVertical ? Style.bar.sizeVertical : Style.bar.sizeHorizontal
|
|
|
|
x: modelData === "right" ? parent.width - edgeSize : 0
|
|
y: modelData === "bottom" ? parent.height - edgeSize : 0
|
|
width: edgeVertical ? edgeSize : parent.width
|
|
height: edgeVertical ? parent.height : edgeSize
|
|
color: root.transparent ? "transparent" : root.background
|
|
borderSpec: Border.flat(root.barForeground, 1)
|
|
visible: opacity > 0
|
|
opacity: root.barMoveCandidate === modelData ? (root.transparent ? 0.45 : 0.7) : 0
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: Style.duration(140); easing.type: Easing.OutCubic }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function findCenterAnchorEntry(entries) {
|
|
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(entries)
|
|
|
|
Loader {
|
|
anchors.fill: parent
|
|
sourceComponent: root.vertical ? verticalCenterModules : horizontalCenterModules
|
|
}
|
|
|
|
Component {
|
|
id: horizontalCenterModules
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
|
|
CenterGestureArea { anchors.fill: parent }
|
|
|
|
Item {
|
|
anchors.left: parent.left
|
|
anchors.right: centerRoot.hasAnchor ? centerAnchorModule.left : parent.right
|
|
anchors.top: parent.top
|
|
anchors.bottom: parent.bottom
|
|
|
|
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: centerAnchorModule.left
|
|
anchors.verticalCenter: centerAnchorModule.verticalCenter
|
|
}
|
|
|
|
ModuleSlot {
|
|
id: centerAnchorModule
|
|
visible: centerRoot.hasAnchor
|
|
entry: centerRoot.anchorEntry
|
|
region: "center"
|
|
anchors.centerIn: parent
|
|
}
|
|
|
|
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 }
|
|
|
|
Item {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.bottom: centerRoot.hasAnchor ? centerAnchorModule.top : parent.bottom
|
|
|
|
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: centerAnchorModule.top
|
|
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
|
}
|
|
|
|
ModuleSlot {
|
|
id: centerAnchorModule
|
|
visible: centerRoot.hasAnchor
|
|
entry: centerRoot.anchorEntry
|
|
region: "center"
|
|
anchors.centerIn: parent
|
|
}
|
|
|
|
ModuleList {
|
|
visible: centerRoot.hasAnchor
|
|
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
|
|
region: "center"
|
|
anchors.top: centerAnchorModule.bottom
|
|
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
component CenterGestureArea: MouseArea {
|
|
id: gestureArea
|
|
|
|
property bool dragging: false
|
|
property bool suppressClick: false
|
|
property real pressedX: 0
|
|
property real pressedY: 0
|
|
readonly property real dragThreshold: Style.space(4)
|
|
|
|
acceptedButtons: Qt.LeftButton
|
|
cursorShape: dragging ? Qt.ClosedHandCursor : Qt.ArrowCursor
|
|
pressAndHoldInterval: 200
|
|
|
|
function startDrag(x, y) {
|
|
if (dragging) return
|
|
dragging = true
|
|
root.beginBarMove(root.targetWindow(gestureArea))
|
|
var scenePoint = gestureArea.mapToItem(null, x, y)
|
|
root.updateBarMove(root.windowScreenPoint(scenePoint, root.barMoveWindow))
|
|
}
|
|
|
|
onPressed: function(mouse) {
|
|
dragging = false
|
|
suppressClick = false
|
|
pressedX = mouse.x
|
|
pressedY = mouse.y
|
|
}
|
|
|
|
onPressAndHold: function(mouse) {
|
|
// A widget above us propagates its composed press-and-hold down here without
|
|
// ever handing over the grab, so we'd get no release or cancel to end the move.
|
|
if (!gestureArea.pressed) return
|
|
startDrag(mouse.x, mouse.y)
|
|
}
|
|
|
|
onPositionChanged: function(mouse) {
|
|
if (!(mouse.buttons & Qt.LeftButton)) return
|
|
|
|
if (!dragging) {
|
|
var distance = Math.abs(mouse.x - pressedX) + Math.abs(mouse.y - pressedY)
|
|
if (distance < dragThreshold) return
|
|
startDrag(mouse.x, mouse.y)
|
|
return
|
|
}
|
|
|
|
var scenePoint = gestureArea.mapToItem(null, mouse.x, mouse.y)
|
|
root.updateBarMove(root.windowScreenPoint(scenePoint, root.barMoveWindow))
|
|
}
|
|
|
|
onReleased: function(mouse) {
|
|
if (!dragging) return
|
|
dragging = false
|
|
suppressClick = true
|
|
root.finishBarMove()
|
|
mouse.accepted = true
|
|
}
|
|
|
|
onCanceled: {
|
|
dragging = false
|
|
suppressClick = false
|
|
root.clearBarMove()
|
|
}
|
|
|
|
onClicked: function(mouse) {
|
|
if (suppressClick) {
|
|
suppressClick = false
|
|
mouse.accepted = true
|
|
}
|
|
}
|
|
|
|
onDoubleClicked: function(mouse) {
|
|
if (suppressClick) {
|
|
suppressClick = false
|
|
return
|
|
}
|
|
if (mouse.button === Qt.LeftButton) {
|
|
root.cycleBackground()
|
|
mouse.accepted = true
|
|
}
|
|
}
|
|
}
|
|
|
|
component ModuleList: Loader {
|
|
id: moduleListRoot
|
|
|
|
property var entries: []
|
|
property string region: ""
|
|
|
|
visible: entries.length > 0
|
|
// A hidden list must not build its modules. The center section declares
|
|
// both an anchored and an unanchored arrangement and shows whichever
|
|
// fits, so leaving the other one loaded mounts every center module
|
|
// twice — two IPC handlers registered for the same target, two clocks
|
|
// ticking, two of every timer and fetch behind them.
|
|
active: 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)
|
|
readonly property var registryMetadata: root.barWidgetRegistry.metadataFor(root.canonicalWidgetId(moduleName))
|
|
readonly property bool firstParty: registryMetadata && registryMetadata.firstParty === true
|
|
readonly property string pluginApiId: registered ? root.canonicalWidgetId(moduleName) : "bar-entry:" + 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) 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
|
|
// Modules bigger than the mark they want (a text label in a padded slot,
|
|
// a multi-line stack on a vertical bar) can say how long the open-panel
|
|
// dot should be along the bar, so it tracks what the module paints
|
|
// instead of a fraction of whatever slot it happens to fill.
|
|
readonly property real panelIndicatorExtent: {
|
|
var key = root.vertical ? "openPanelIndicatorHeight" : "openPanelIndicatorWidth"
|
|
var hint = activeItem && key in activeItem ? activeItem[key] : undefined
|
|
if (hint !== undefined && hint !== null && hint > 0) return Math.round(hint)
|
|
return Math.max(Style.space(10), Math.round((root.vertical ? slot.height : slot.width) * 0.55))
|
|
}
|
|
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: Commons.Color.accent
|
|
radius: Math.min(width, height) / 2
|
|
width: root.vertical ? Style.space(2) : slot.panelIndicatorExtent
|
|
height: root.vertical ? slot.panelIndicatorExtent : Style.space(2)
|
|
// The mark sits on the module's inner edge — the one facing the
|
|
// desktop — so it underlines a top bar, overlines a bottom one, and
|
|
// points inward from a left or right one. It reads as pointing at the
|
|
// panel that opens on that side.
|
|
// Snap toward the start of the slot, matching native glyph rendering.
|
|
x: root.vertical
|
|
? (root.position === "left" ? parent.width - width - inset : inset)
|
|
: Math.floor((parent.width - width) / 2)
|
|
y: root.vertical
|
|
? Math.floor((parent.height - height) / 2)
|
|
: (root.position === "top" ? parent.height - height - inset : inset)
|
|
z: 50
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: Style.duration(120); easing.type: Easing.OutCubic }
|
|
}
|
|
}
|
|
|
|
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
|
|
// Only the hovered slot needs the hit test; without the guard every
|
|
// slot on every monitor re-ran it on each click target change.
|
|
cursorShape: moduleHover.hovered && 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
|
|
root.barDragTargetGeometry = drop ? root.dropMarkerRect(drop.slot, drop.after) : null
|
|
}
|
|
}
|
|
|
|
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 = firstParty
|
|
? root : root.pluginBarApiFor(pluginApiId, moduleName, registered)
|
|
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)
|
|
}
|
|
}
|
|
}
|