* Play matching theme background intros at boot * Add Tokyo Night sunset lake boot intro * Stage Tokyo Night boot intros on update * Add user controls for background boot intros * Optimize Tokyo Night boot intro videos * Add Tokyo Night cityscape boot intro * Add Ristretto boot intro videos * Add Solitude boot intro videos * Add White boot intro videos * Add Vantablack boot intro videos * Add Miasma nature boot intro * Add Matte Black boot intro videos * Add Lupine wave boot intro videos * Add Last Horizon blink boot intro * Add Everforest tree tops boot intro * Add Ethereal cosmic boot intro * Add four Retro 82 boot intros * Add Tokyo Night Quattro boot intro * Add Catppuccin Latte color fade boot intro * Add five Gruvbox boot intros * Expand Retro 82 boot intro coverage * Complete Ethereal boot intros * Complete Retro 82 boot intros * Stage boot intros for the active theme * Add Catppuccin boot intros * Complete Tokyo Night boot intros * Expand Lupine boot intros * Add Osaka Jade mountain boot intro * Prime boot intros before revealing wallpaper * Complete Osaka Jade boot intros * Keep kernel migration test valid after timestamp reuse * Shorten packaged boot intros to six seconds * Revert "Shorten packaged boot intros to six seconds" This reverts commit 5530403aa9e789b977a1a1a77ea7acf37a9e3598. * Document five to seven second intro target * Ease Matte Black intros into six seconds * Play boot intros through OWE instead of a shell player The merged desktop video path removes BackgroundVideo.qml and makes BackgroundMedia image-only. This keeps the intro feature by resolving the matching intro as before and playing it with 'owe intro', which owns the decode and the reveal. OWE 0.2.3 adds that command. The shell holds the theme color while resolving and cancels with 'owe intro-stop'. * Make boot intro startup atomic * Retry boot intro through login window * Document required OWE intro protocol * Add Last Horizon video boot intros * Play boot intros with released OWE Use `owe intro` instead of the unreleased intro-prepare/intro-commit protocol from omacom/owe#5. The boot is recorded before playback, and OWE's intro_result status decides whether a failed intro never started (release the boot for retry) or was interrupted (keep it consumed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Replace Matte Black boot intros with shorter cuts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hide the still behind black until the boot intro starts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Shorten the Everforest boot intro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document that boot intros should open dark Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refine the Osaka Jade boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Skip compositor fades on the background layers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Shorten the Catppuccin Latte boot intro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Shorten the Catppuccin boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Shorten the White boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Shorten the Vantablack boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Shorten the Gruvbox boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Shorten the Retro 82 boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Shorten the Solitude boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Rework the Ristretto boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Shorten the Tokyo Night boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Complete the Lupine boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Complete the Last Horizon boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Complete the Miasma boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add the Nord boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add the Rose Pine boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add the Flexoki Light boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add the Hackerman boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add the Kanagawa boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add the Lumon boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fade in the Last Horizon intros from black Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Animate the Catppuccin boot intros from their wallpapers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF * Animate the third White boot intro from its wallpaper Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF * Animate the Flexoki Light, Lumon and Rose Pine plants intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF * Shorten the Ethereal boot intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF * Animate the remaining placeholder intros from their wallpapers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF * Replace Catppuccin intros with themotion scenes Totoro moonrise, waves swelling from a line, and the blue eye tracing itself open, rendered by https://github.com/yamz8/themotion (scenes/catppuccin/*). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KAPp37DLjmKQy8GZr3vFvF * Re-encode Catppuccin intros with exact final frames Same motion; the settled frames are encoded at a fixed high quality so the handoff lands on the wallpaper (themotion 0c5e86c). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Replace Lumon intros with themotion scenes United in severance draws its lines and globe with a small pen-tip light; Opinions equally wakes inside the picture's own screen and types out the mark and tagline. Both keep the wallpaper's proportions, so the handoff holds its shape. Made with https://github.com/yamz8/themotion (scenes/lumon/*). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Land the Last Horizon intros exactly on their wallpapers Same footage and timing; the last 0.4 s now blends into the wallpaper as the shell draws it (libwebp decode, BT.709 limited range), with the settled frames at high quality. Re-finished with themotion fit (09f156e). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add Nord video boot intro for Night Hawks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Replace Rose Pine intros with themotion scenes Funky shapes pop in on a jelly spring and the doodles draw on (replacing the reversed stock clip), the dot map spins in from a swelling dot, and the plants spring up before the OMARCHY letters drop in. Made with https://github.com/yamz8/themotion (scenes/rose-pine/*). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Replace Hackerman intros with themotion scenes The synthwave terrain unfolds as the sun rises and the valley glow flares into the floor; the geometric network bursts out from a triangle at its core and locks into place. Made with https://github.com/yamz8/themotion (scenes/hackerman/*). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Replace White 3-white with a themotion scene and land 1-2 exactly 3-white folds and slides its paper sheets into place along their diagonal (themotion scenes/white/3-white). 1-white and 2-white keep their footage and timing; their last 0.4 s now blend into the wallpaper as the shell draws it (themotion fit). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Replace Ristretto 0-1 with themotion scenes and land 2-3 exactly 0-launch lifts its stripes off and bends them into their curves, and 1-color-curves fans its colour bands in along their curves (themotion scenes/ristretto/*). 2-coffee-beans and 3-industrial-moon keep their footage and timing; their last 0.4 s now blend into the wallpaper as the shell draws it (themotion fit). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Replace Vantablack intros: new layer scenes, shared dot-hands cut 0-dot-hands uses the Matte Black cut, since both themes ship the same wallpaper. 1-twisted-stairs keeps its footage and lands exactly on the wallpaper (themotion fit). 2-layers-deep opens its depth front to back and 3-layers-stacked lays its papercut stack down sheet by sheet (themotion scenes/vantablack/*), replacing footage that opened on unrelated shapes and a plain fade-in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop the shockwave ring from the Totoro intro The moon now opens from its seed of light with only its halo; the thin ring racing outward read as clutter (themotion 6970484). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop the shockwave ring from the blue-eye intro The eye closes with its pulse and halo only (themotion, same change as Totoro's). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Land the earlier footage intros exactly on their wallpapers Everforest, Osaka Jade, Catppuccin Latte and two Matte Black intros were finished before the exact-ending tooling and ended 0.9-4 levels off their wallpapers (up to 134 on small spots). Their footage and timing are unchanged; the last 0.4 s now blend into the wallpaper as the shell draws it (themotion fit). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fade two Matte Black intros up from black ship-at-sea and dark-waters opened at brightness 17-20, so they popped in after the black cover. They now fade up over 0.3 s; the cuts and their exact endings are otherwise unchanged (themotion fit --fade-in). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop the Flexoki Light intro for now Its orb scene isn't good enough yet, so Flexoki Light starts on its still until a better intro is ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit bfe60d6c0a7a0983509a2dc2b295693bec2ceeb8) * Skip boot intros while animations are off A VM starts with animations off, and there Hyprland renders on the CPU, so a video intro costs the most. The launcher now consumes the boot without playing when Hyprland reports animations:enabled false, and the shell's cover check treats that, like the intros toggle, as nothing to wait for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add Miasma video boot intro for Crowned Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep boot intro startup outside the background service OWE destroys and recreates the background plugin during playback. Keep the launcher and black cover in the persistent shell host, consume the boot before waiting briefly for OWE, and start on the video's first frame. The startup check runs with a video background too, so enabling the still background service later cannot play an intro over an established desktop. Co-Authored-By: Codex Medium <codex@openai.com> * Release the boot marker lock before waiting for OWE The marker settles startup once it is written. Keeping the lock through readiness and playback makes a fresh shell wait for an older launcher before it can discover that the boot was consumed, leaving the still covered during that wait. * Keep the boot intro cover above native and cloned backgrounds Cloned backgrounds receive a scoped shell interface without the startup coordinator. Keep the input-transparent cover in the persistent host so every background stays hidden until OWE has a frame, without widening the plugin interface. Co-Authored-By: Codex Medium <codex@openai.com> * Map the startup cover only for an unsettled boot The cover must know the boot was already consumed before its first frame, so restarting the shell during playback leaves OWE visible. Snapshot the marker without watching subsequent writes: the launcher consumes this boot before the intro starts, while the cover remains needed until the first decoded frame. * Exercise automatic boot cover release in the clone test The fixture hid the cover itself before the fake launcher exited, so a broken exit handler still passed. Let the launcher finish before checking the actual background pixels and leave cover release to the service. Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com> * Finish the Miasma, Kanagawa, Ethereal and Nord intros Start each intro on its scene instead of fading in from black, since OWE now starts intros on their first frame. - Miasma Crowned: ejuro's intro, shortened to 6.7 s and opening on the lit hand - Nord Night Hawks: ejuro's intro - Kanagawa: rendered in the wallpaper's own shape, so the handoff matches on 16:10 and 3:2 screens too - Ethereal, Nord and Miasma Crowned: 1080p endings that blend into the exact wallpaper, so the handoff no longer sharpens visibly Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Finish the Tokyo Night intros Start each intro on its scene instead of fading in from black, since OWE now starts intros on their first frame. - Winding Road: a new starry night to sunset with no added clouds or sun, ending on the wallpaper - Quattro: a new take where the car lands in the wallpaper's exact pose, colour-matched to the wallpaper - Oma Cityscape: drop the second, larger moon that faded out at the end - Quattro, Swirl Buck, Omakub and Oma Cityscape: 1080p endings that blend into the exact wallpaper, so the handoff no longer sharpens visibly Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Pair background intros by filename and play them on theme switches Keep matching clips in backgrounds/intros so the selected image determines playback without registration, overrides, or checksum sidecars. Retain one global toggle and play automatically at boot and on a different theme selection. Launch theme playback after releasing the selection lock. Check the staged theme directory identity so an older launcher cannot play a newer selection when filenames are reused. Skipped or failed playback immediately hands the selected still to the shell, including new pixels behind an unchanged path. Synchronize the clone screenshot fixture with capture instead of a fixed launcher lifetime, while still requiring the real exit handler to release the cover. Co-Authored-By: Codex Medium <codex@openai.com> * Settle pending background colors when applying the current theme An intro applies its theme immediately while a previous still-image reveal may still hold an older palette. Replace and consume that pending palette through the background's existing handoff methods so its reveal or fallback timer cannot restore old colors. Reusing those methods also works with existing background clones. Publish the fake renderer's PID before announcing playback readiness, so the lock test cannot read an unwritten PID file. Regressions reproduce both failures on 7b238cf1 and pass with the fixes, including a deliberate scheduling delay at PID publication. Co-Authored-By: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com> * Use the existing animations menu toggle for background intros Keep the separate intro command available without another Style menu control. Disabled animations already suppress playback at boot and on theme changes. Use remembered backgrounds with matching clips when testing suppressed theme intros, and require the normal still transition so the checks cannot pass merely because no clip was selected. Co-Authored-By: Codex Medium <codex@openai.com> * Finish the Lupine intros Start each intro on its scene instead of fading in from black, since OWE now starts intros on their first frame. - Cherry Blossom White: a new take that pulls back from fresh leaves to the whole tree, easing to a stop on the wallpaper - Pastel Clouds: drop the unrelated starry opening and its dip to black - Elegant Blue Wave: drop the empty white opening - Cherry Blossom Bokeh, Pastel Clouds, Elegant Blue Wave and Cherry Blossom White: 1080p endings that blend into the exact wallpaper Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JBE1V6gjrqn2aXCNNGhrUY * Synchronize theme intro and bar crossfades * Start theme intro fades from a cached first frame * Open theme intros on moving scenes * Start theme crossfades promptly * Prepare theme intros before switching * Finish the Catppuccin intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JBE1V6gjrqn2aXCNNGhrUY * Keep theme intro handoffs covered * Reload Hyprland after theme intro playback * Finish the Rose Pine intros Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JBE1V6gjrqn2aXCNNGhrUY * Restore the wallpaper early on each login * Keep the startup wallpaper cover transparent * Wait for readiness in the intro lifecycle test * Consume login intros for updates outside Hyprland * Fade login directly into the ready desktop intro * Sharpen Quattro, Swirl Buck and Oma Cityscape Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep compositor backgrounds black during login * Keep the shell visible through compositor shutdown * Bound the login cover when startup media fails * Encode the 1440p intros at H.264 level 5.1 At 2560x1440 the previous encodes used 16 reference frames, which puts them at level 6.0. Intel hardware decoding plays those with stray grey blocks. Four reference frames keep them at level 5.1, like the other intros. Swirl Buck's stag now changes colour as one silhouette instead of leaving a shrinking black disc on its head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hide the startup cursor until the desktop fades in * Restore Swirl Buck's original stag The stag now changes colour exactly as in the original clip again; the previous edit made it jump from black to purple in one frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Initialize the startup cursor before the first compositor frame * Render the Hackerman intros at 1440p Both open on their first motion instead of fading in from black. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Render the Lumon intros at 1440p Both open on their first motion instead of fading in from black. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sharpen the Lupine intros Upscaled from the original 720p clips and exported at 1440p with a gentler encode, keeping the same frames, timing and endings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Resolve login intros before the wallpaper daemon starts * Synchronize cold-start captures with the fixture phases * Sharpen the rest of the Tokyo Night intros Winding Road and Sunset Lake are regenerated natively at 1440p from their wallpapers, since their original clips were 360p and 720p. Omakub and Oma are redrawn from their wallpapers with the same motion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Bjarne Oeverli <1419214+bjarneo@users.noreply.github.com> Co-authored-by: Erik Johansson <172146456+ejuro@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Omarchy Bot <omarchybot@users.noreply.github.com> Co-authored-by: Codex Medium <codex@openai.com> Co-authored-by: Greptile <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: David Heinemeier Hansson <david@hey.com>
2107 lines
70 KiB
QML
2107 lines
70 KiB
QML
import Quickshell
|
|
import Quickshell.Hyprland
|
|
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.
|
|
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",
|
|
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
|
|
// 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: 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: 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
|
|
|
|
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 || "")
|
|
|
|
// 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)
|
|
}
|
|
|
|
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
|
|
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.
|
|
visible: !remapGuard.remapping
|
|
exclusionMode: root.barHidden ? ExclusionMode.Ignore : ExclusionMode.Auto
|
|
|
|
ScreenMoveRemap {
|
|
id: remapGuard
|
|
window: barWindow
|
|
}
|
|
|
|
margins {
|
|
top: root.barHidden && root.position === "top" ? -root.barSize : 0
|
|
bottom: root.barHidden && root.position === "bottom" ? -root.barSize : 0
|
|
left: root.barHidden && root.position === "left" ? -root.barSize : 0
|
|
right: root.barHidden && root.position === "right" ? -root.barSize : 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
|
|
}
|
|
|
|
implicitWidth: root.vertical ? root.barSize : 0
|
|
implicitHeight: root.vertical ? 0 : root.barSize
|
|
color: root.transparent ? "transparent" : 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: Color.tooltip.background
|
|
borderSpec: Border.surfaceSpec("tooltip", "border", Color.tooltip.border, 1)
|
|
radius: Style.cornerRadius
|
|
|
|
Text {
|
|
id: tooltipLabel
|
|
textFormat: Text.PlainText
|
|
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
|
|
}
|
|
}
|
|
|
|
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: 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() {
|
|
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 }
|
|
|
|
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.toggleTransparency()
|
|
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: 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)
|
|
}
|
|
}
|
|
}
|