diff --git a/desktop/OmniMenu.qml b/desktop/OmniMenu.qml index 5074eff..234b5b1 100644 --- a/desktop/OmniMenu.qml +++ b/desktop/OmniMenu.qml @@ -36,6 +36,10 @@ Item { // Named `navbar` to avoid colliding with the existing `nav: []` // category-row array further down. property var navbar: null + // Expose can host the palette's search state and result list inside its + // own layer-shell surface. In that mode the normal fullscreen panel and + // global shortcuts stay dormant; the owning surface handles focus. + property bool embedded: false readonly property color paper: theme.paper readonly property color ink: theme.ink @@ -140,6 +144,7 @@ Item { function collapseTile() { root.expandedTile = null; } Bookmarks { id: bookmarks } + readonly property alias omniBookmarks: bookmarks // ---------- Quick tiles ---------- // Split into a *static* base array (the Repeater's model) and a @@ -249,6 +254,7 @@ Item { active: root.procMode && !root.tldrMode && !root.llmMode selectedItem: root.filteredItems[root.selectedIndex] || null } + readonly property alias omniProcesses: processes readonly property alias procItems: processes.items readonly property alias procRunning: processes.running readonly property alias procPreviewText: processes.previewText @@ -258,6 +264,7 @@ Item { id: themes active: root.themeMode && !root.tldrMode && !root.llmMode } + readonly property alias omniThemes: themes readonly property alias themeItems: themes.items readonly property alias themeLoaded: themes.loaded @@ -282,6 +289,7 @@ Item { active: root.llmMode mode: root.cmdMode ? "command" : "chat" } + readonly property alias omniOllamaChat: ollamaChat readonly property alias chatItems: ollamaChat.items readonly property alias chatRunning: ollamaChat.running readonly property alias chatPreview: ollamaChat.previewText @@ -387,6 +395,7 @@ Item { // ||, &&, redirects) work alongside plain // argv-style commands without a special case Process { id: runner; running: false } + property var externalList: null function activate(item) { if (!item) return; if (item.isCategory) { @@ -680,7 +689,8 @@ Item { next = wrap ? ((next % n) + n) % n : Math.max(0, Math.min(n - 1, next)); root.selectedIndex = next; - resultListInstance.list.positionViewAtIndex(next, ListView.Contain); + const view = root.externalList || resultListInstance.list; + if (view) view.positionViewAtIndex(next, ListView.Contain); } // Grid-aware step for Quick mode. `delta` may exceed ±1 (arrow Up/Down @@ -724,13 +734,13 @@ Item { // bind = ALT, SPACE, global, quickshell:palette-quick GlobalShortcut { appid: "quickshell" - name: "palette-toggle" + name: root.embedded ? "palette-toggle-embedded" : "palette-toggle" description: "Toggle omni-menu palette" onPressed: root.toggle() } GlobalShortcut { appid: "quickshell" - name: "palette-quick" + name: root.embedded ? "palette-quick-embedded" : "palette-quick" description: "Open omni-menu pivoted to Quick" onPressed: { root.open(); root.categoryFilter = "Quick"; } } @@ -738,13 +748,14 @@ Item { // ---------- Panel ---------- PanelWindow { id: panel - visible: root.visible_ || reveal > 0.001 + visible: !root.embedded && (root.visible_ || reveal > 0.001) color: "transparent" anchors { top: true; bottom: true; left: true; right: true } exclusionMode: ExclusionMode.Ignore WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "omni-menu" - WlrLayershell.keyboardFocus: root.visible_ ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: !root.embedded && root.visible_ + ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None property real reveal: root.visible_ ? 1 : 0 // Open and close are both instant: SUPER+SPACE paints the palette on diff --git a/desktop/omni/qmldir b/desktop/omni/qmldir new file mode 100644 index 0000000..3eba29b --- /dev/null +++ b/desktop/omni/qmldir @@ -0,0 +1,7 @@ +module desktop.omni +Footer 1.0 Footer.qml +HeaderBar 1.0 HeaderBar.qml +PreviewPane 1.0 PreviewPane.qml +QuickContainer 1.0 QuickContainer.qml +ResultList 1.0 ResultList.qml +SearchInput 1.0 SearchInput.qml diff --git a/desktop/qmldir b/desktop/qmldir new file mode 100644 index 0000000..09ee86c --- /dev/null +++ b/desktop/qmldir @@ -0,0 +1,50 @@ +module desktop +AppScan 1.0 AppScan.qml +AetherPopup 1.0 AetherPopup.qml +Bar 1.0 Bar.qml +BarHacker 1.0 BarHacker.qml +BarWhiterose 1.0 BarWhiterose.qml +Bloom 1.0 Bloom.qml +Bookmarks 1.0 Bookmarks.qml +CalendarChevron 1.0 CalendarChevron.qml +CalendarPopup 1.0 CalendarPopup.qml +CardWindow 1.0 CardWindow.qml +DisplayChip 1.0 DisplayChip.qml +DisplayPopup 1.0 DisplayPopup.qml +DisplaySlider 1.0 DisplaySlider.qml +FileSearch 1.0 FileSearch.qml +GhSearch 1.0 GhSearch.qml +HackerStat 1.0 HackerStat.qml +Module 1.0 Module.qml +NavbarApps 1.0 NavbarApps.qml +Navbar 1.0 Navbar.qml +OllamaChat 1.0 OllamaChat.qml +OmniMenu 1.0 OmniMenu.qml +Processes 1.0 Processes.qml +QuickAetherBody 1.0 QuickAetherBody.qml +QuickAudioBody 1.0 QuickAudioBody.qml +QuickBatteryBody 1.0 QuickBatteryBody.qml +QuickBluetoothBody 1.0 QuickBluetoothBody.qml +QuickButton 1.0 QuickButton.qml +QuickCalendarBody 1.0 QuickCalendarBody.qml +QuickCpuBody 1.0 QuickCpuBody.qml +QuickDisplayBody 1.0 QuickDisplayBody.qml +QuickPowerBody 1.0 QuickPowerBody.qml +QuickScreenshotsBody 1.0 QuickScreenshotsBody.qml +QuickSlider 1.0 QuickSlider.qml +QuickVideosBody 1.0 QuickVideosBody.qml +QuickWeatherBody 1.0 QuickWeatherBody.qml +QuickWifiBody 1.0 QuickWifiBody.qml +ScreenshotsPopup 1.0 ScreenshotsPopup.qml +Separator 1.0 Separator.qml +SystemPopup 1.0 SystemPopup.qml +Theme 1.0 Theme.qml +Themes 1.0 Themes.qml +TldrSearch 1.0 TldrSearch.qml +Tuis 1.0 Tuis.qml +TooltipOverlay 1.0 TooltipOverlay.qml +VideosPopup 1.0 VideosPopup.qml +WallhavenSource 1.0 WallhavenSource.qml +WeatherPopup 1.0 WeatherPopup.qml +WhiteRoseCell 1.0 WhiteRoseCell.qml +Workspace 1.0 Workspace.qml diff --git a/expose/README.md b/expose/README.md index e1cac49..c82dec9 100644 --- a/expose/README.md +++ b/expose/README.md @@ -1,6 +1,6 @@ # expose -Mission-Control-style workspace overview with real per-workspace thumbnails. Triggered by IPC. Click a tile to jump, click a window inside the tile to focus it directly. +Mission-Control-style workspace overview with real per-workspace thumbnails and an inline omni search dock. Triggered by IPC. Click a tile to jump, click a window inside the tile to focus it directly. ``` ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ @@ -31,7 +31,7 @@ Wayland forbids a layer-shell client from screencapping arbitrary surfaces. So t 1. Tails Hyprland's `.socket2.sock` event stream. 2. On any `workspace`, `workspacev2`, `focusedmon`, `openwindow`, `closewindow`, `movewindow`, `fullscreen`, or `changefloatingmode` event, restarts a 400ms debounce. -3. When the debounce fires, runs `grim -o -s 0.35` for every monitor's currently active workspace and writes the result to `~/.cache/quickshell/expose/ws-.png` (~50KB). +3. When the debounce fires, runs `grim -o -s 1` for every monitor's currently active workspace and writes a full-resolution result to `~/.cache/quickshell/expose/ws-.png`. A workspace shows as a real screenshot once you've visited it at least once during this session. Never-visited workspaces show a fallback schematic view (faint indigo rectangles labeled with their window class). The active-window outline overlay works in both modes. @@ -58,11 +58,13 @@ qs -c expose ipc call expose capture # force a fresh capture of all visible wo | Key | Action | | --- | --- | -| arrows / hjkl | move selection | -| Tab / Shift+Tab | next / previous | -| 1-9 | jump directly to that workspace | -| Enter, Space | jump to selected workspace | -| Esc, q | dismiss | +| arrows / hjkl | move workspace selection | +| Tab / Shift+Tab | next / previous workspace | +| any printable key | open inline omni search | +| arrows / Tab / Shift+Tab | move through search results while typing | +| 1-9 | jump directly to that workspace when search is idle | +| Enter | jump to the selected workspace or run the selected search result | +| Esc | clear the search, then dismiss | Click on the dim backdrop also dismisses. Hovering a tile selects it. @@ -84,4 +86,4 @@ If `grim` is missing the module still works, it just falls back to the schematic ## Theming -Reads `~/.config/omarchy/current/theme/colors.toml` for `background`, `foreground`, `accent`, `color1`, `color7`, `color8`. Re-themes live on `omarchy theme set`. Serif for the kanji markers and overview title, JetBrainsMono for window class labels and badges. +Reads `~/.config/omarchy/current/theme/colors.toml` for `background`, `foreground`, `accent`, `color1`, `color7`, `color8`. Re-themes live on `omarchy theme set`. Serif for the workspace markers; JetBrainsMono for window class labels, badges, and the inline omni search. diff --git a/expose/WorkspaceTile.qml b/expose/WorkspaceTile.qml index cd1e01b..70680b6 100644 --- a/expose/WorkspaceTile.qml +++ b/expose/WorkspaceTile.qml @@ -33,8 +33,12 @@ Item { property int wsIndex: 0 // ---- derived ---- - readonly property real monW: mon && mon.width ? mon.width : 1920 - readonly property real monH: mon && mon.height ? mon.height : 1080 + // Hyprland reports monitor dimensions in physical pixels, while client + // geometry and grim -s 1 screenshots use logical pixels. + readonly property real monW: mon && mon.width + ? mon.width / (mon.scale || 1) : 1920 + readonly property real monH: mon && mon.height + ? mon.height / (mon.scale || 1) : 1080 readonly property color stripeColor: focused ? tile.shell.seal : (current ? tile.shell.indigo : Qt.rgba(tile.shell.sumi.r, tile.shell.sumi.g, tile.shell.sumi.b, 0.45)) diff --git a/expose/shell.qml b/expose/shell.qml index 526a5a5..c0c90de 100644 --- a/expose/shell.qml +++ b/expose/shell.qml @@ -11,7 +11,7 @@ import Quickshell.Io // workspace / window change (debounced 400ms), shells out to `grim` to grab // each monitor's currently-visible workspace into: // -// ~/.cache/quickshell/expose/ws-.png (~900x500, ~50KB) +// ~/.cache/quickshell/expose/ws-.png (logical monitor resolution) // // Workspaces visited at least once during the session show as real pixels; // never-visited workspaces fall back to a schematic class-name view. The @@ -81,15 +81,68 @@ ShellRoot { property var availableThumbs: ({}) // { wsId: true } for ws-N.png files that exist property real revealPhase: 0 // 0..1, drives the stagger reveal of tiles + // The expose surface hosts the omni search state directly. The desktop + // palette is loaded as a component so this config can share its search + // implementation without creating a second layer-shell window. + readonly property string desktopConfigDir: Quickshell.env("HOME") + "/.config/quickshell/desktop" + property var omniSearch: omniLoader.item + + Loader { + id: omniThemeLoader + source: root.desktopConfigDir + "/Theme.qml" + active: true + } + Loader { id: omniLoader; active: false } + + function loadOmni() { + if (omniThemeLoader.item && !omniLoader.item) { + omniLoader.setSource(root.desktopConfigDir + "/OmniMenu.qml", { + theme: omniThemeLoader.item, + embedded: true + }); + omniLoader.active = true; + } + } + + Connections { + target: omniThemeLoader + function onLoaded() { root.loadOmni(); } + } + Connections { + target: omniLoader + function onLoaded() { + const o = omniLoader.item; + omniInputLoader.setSource(root.desktopConfigDir + "/omni/SearchInput.qml", { omni: o }); + omniResultLoader.setSource(root.desktopConfigDir + "/omni/ResultList.qml", { + omni: o, + bookmarks: o.omniBookmarks, + processes: o.omniProcesses, + themes: o.omniThemes, + ollamaChat: o.omniOllamaChat + }); + if (root.open) o.open(); + } + } + Connections { + target: omniResultLoader + function onLoaded() { + if (root.omniSearch && omniResultLoader.item) + root.omniSearch.externalList = omniResultLoader.item.list; + } + } + Component.onCompleted: root.loadOmni() + function hasThumb(wsId) { return root.availableThumbs[wsId] === true; } onOpenChanged: { if (root.open) { revealPhase = 0; revealAnim.restart(); + if (omniSearch) omniSearch.open(); } else { revealAnim.stop(); revealPhase = 0; + if (omniSearch) omniSearch.close(); } } NumberAnimation { @@ -189,8 +242,34 @@ ShellRoot { Process { id: runner; running: false } function dispatch(args) { runner.command = ["hyprctl"].concat(args); runner.running = false; runner.running = true; } - function jumpWorkspace(wsId) { root.open = false; root.dispatch(["dispatch", "workspace", String(wsId)]); } - function focusWindow(addr) { root.open = false; root.dispatch(["dispatch", "focuswindow", "address:" + addr]); } + function jumpWorkspace(wsId) { + root.open = false; + root.dispatch(["dispatch", "hl.dsp.focus({ workspace = " + String(wsId) + " })"]); + } + function focusWindow(addr) { + let targetWs = null; + for (let i = 0; i < root.clients.length; i++) { + const c = root.clients[i]; + if (c.address === addr && c.workspace) { + targetWs = c.workspace.id; + break; + } + } + + root.open = false; + if (targetWs !== null && targetWs > 0) { + // Focus the target workspace before focusing its window. Doing + // both in one batch avoids the focus request being ignored while the + // target workspace is inactive. + root.dispatch([ + "--batch", + "dispatch hl.dsp.focus({ workspace = " + String(targetWs) + " })" + + " ; dispatch hl.dsp.focus({ window = \"address:" + addr + "\" })", + ]); + } else { + root.dispatch(["dispatch", "hl.dsp.focus({ window = \"address:" + addr + "\" })"]); + } + } function jumpSelected() { const ws = root.workspaces[root.selectedIndex]; if (ws) root.jumpWorkspace(ws.id); } function clientsInWorkspace(wsId) { @@ -234,7 +313,7 @@ ShellRoot { + " if not wid or wid<=0: continue\n" + " dst=f\"{out}/ws-{wid}.png\"\n" + " tmp=dst+\".tmp\"\n" - + " r=subprocess.run([\"grim\",\"-o\",m[\"name\"],\"-s\",\"0.35\",tmp],\n" + + " r=subprocess.run([\"grim\",\"-o\",m[\"name\"],\"-s\",\"1\",tmp],\n" + " stderr=subprocess.DEVNULL)\n" + " if r.returncode==0 and os.path.exists(tmp):\n" + " os.replace(tmp,dst)\n" @@ -378,11 +457,72 @@ ShellRoot { focus: root.open Keys.onPressed: function(e) { if (!root.open) return; - if (e.key === Qt.Key_Escape || e.key === Qt.Key_Q) { - root.open = false; e.accepted = true; return; - } + const omni = root.omniSearch; + const query = omni ? omni.query : ""; const n = root.workspaces.length; if (n === 0) return; + + // Expose owns the keyboard focus. While the omni query is + // active, arrows and Enter operate on its inline results; + // with an empty query they continue to switch workspaces. + if (e.key === Qt.Key_Escape) { + if (query.length > 0) { + if (omni) { + omni.query = ""; + omni.selectedIndex = 0; + } + } else { + root.open = false; + } + e.accepted = true; return; + } + if (query.length > 0 && omni) { + if (e.key === Qt.Key_Right || e.key === Qt.Key_Down + || (e.key === Qt.Key_Tab && !(e.modifiers & Qt.ShiftModifier))) { + omni.moveSelection(1, true); e.accepted = true; return; + } + if (e.key === Qt.Key_Left || e.key === Qt.Key_Up + || e.key === Qt.Key_Backtab + || (e.key === Qt.Key_Tab && (e.modifiers & Qt.ShiftModifier))) { + omni.moveSelection(-1, true); e.accepted = true; return; + } + if (e.key === Qt.Key_PageDown) { + omni.moveSelection(8, false); e.accepted = true; return; + } + if (e.key === Qt.Key_PageUp) { + omni.moveSelection(-8, false); e.accepted = true; return; + } + if (e.key === Qt.Key_Backspace) { + omni.query = query.slice(0, -1); + omni.selectedIndex = 0; + e.accepted = true; return; + } + if (e.key === Qt.Key_Return || e.key === Qt.Key_Enter) { + const item = omni.filteredItems[omni.selectedIndex]; + if (item) omni.activate(item); + e.accepted = true; return; + } + } + + // Direct workspace numbers remain available when the + // search bar is idle, matching the original expose flow. + if (query.length === 0 && e.key >= Qt.Key_1 && e.key <= Qt.Key_9) { + const i = e.key - Qt.Key_1; + if (i < n) { root.selectedIndex = i; root.jumpSelected(); } + e.accepted = true; return; + } + + // Typing anywhere on the overview pivots into omni search, + // just like a desktop overview. This comes before the + // hjkl aliases so those letters remain searchable. + if (e.text && e.text.length === 1 + && e.text.charCodeAt(0) >= 32 && e.text.charCodeAt(0) !== 127) { + if (!omni) return; + omni.query += e.text; + omni.selectedIndex = 0; + e.accepted = true; return; + } + if (e.key === Qt.Key_Right || e.key === Qt.Key_L || (e.key === Qt.Key_Tab && !(e.modifiers & Qt.ShiftModifier))) { root.selectedIndex = (root.selectedIndex + 1) % n; e.accepted = true; return; @@ -399,97 +539,9 @@ ShellRoot { } if (e.key === Qt.Key_Home) { root.selectedIndex = 0; e.accepted = true; return; } if (e.key === Qt.Key_End) { root.selectedIndex = n - 1; e.accepted = true; return; } - if (e.key === Qt.Key_Return || e.key === Qt.Key_Enter || e.key === Qt.Key_Space) { + if (e.key === Qt.Key_Return || e.key === Qt.Key_Enter) { root.jumpSelected(); e.accepted = true; return; } - if (e.key >= Qt.Key_1 && e.key <= Qt.Key_9) { - const i = e.key - Qt.Key_1; - if (i < n) { root.selectedIndex = i; root.jumpSelected(); } - e.accepted = true; return; - } - } - } - - // ---- title block ---- - // 概観 (gaikan) — "overview" — set large in serif, with a - // brush-stroke divider beneath punctuated by a vermilion hanko - // dot, then a tiny romaji caption with the workspace count. - Item { - id: titleBlock - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: 44 - width: Math.min(520, parent.width - 200) - height: 92 - opacity: root.open ? 1 : 0 - Behavior on opacity { NumberAnimation { duration: 260 } } - - Text { - id: titleKanji - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - text: "概観" - color: root.ink - opacity: 0.92 - font.family: root.serif - font.pixelSize: 32 - font.letterSpacing: 14 - font.weight: Font.Light - } - - // Brush-stroke divider: two short hairlines tapered toward a - // central vermilion dot. Reads like a sumi-e flick from each - // side meeting at a hanko stamp. - Item { - id: divider - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: titleKanji.bottom - anchors.topMargin: 14 - width: 280; height: 8 - - Rectangle { - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - width: (parent.width - dot.width) / 2 - 10 - height: 1 - color: root.sumi - opacity: 0.42 - } - Rectangle { - id: dot - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - width: 6; height: 6; radius: 3 - color: root.seal - rotation: -8 - SequentialAnimation on opacity { - loops: Animation.Infinite - NumberAnimation { from: 0.85; to: 1.0; duration: 1400; easing.type: Easing.InOutSine } - NumberAnimation { from: 1.0; to: 0.85; duration: 1400; easing.type: Easing.InOutSine } - } - } - Rectangle { - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - width: (parent.width - dot.width) / 2 - 10 - height: 1 - color: root.sumi - opacity: 0.42 - } - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: divider.bottom - anchors.topMargin: 12 - text: "exposé · " + root.kanji(root.workspaces.length) + " · " - + root.workspaces.length + (root.workspaces.length === 1 ? " workspace" : " workspaces") - color: root.sumi - opacity: 0.65 - font.family: root.serif - font.pixelSize: 11 - font.italic: true - font.letterSpacing: 4 } } @@ -497,8 +549,8 @@ ShellRoot { Item { id: grid anchors.fill: parent - anchors.topMargin: 178 - anchors.bottomMargin: 108 + anchors.topMargin: 76 + anchors.bottomMargin: 176 anchors.leftMargin: 96 anchors.rightMargin: 96 @@ -565,13 +617,105 @@ ShellRoot { } } + // ---- inline omni search ---- + // The launcher shares the expose surface: idle state is a small + // bottom-centered pill, while typed queries grow a result stack + // upward without opening another layer-shell window. + Connections { + target: root.omniSearch + function onVisible_Changed() { + if (root.open && root.omniSearch && !root.omniSearch.visible_) + root.open = false; + } + } + Item { + id: omniDock + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 52 + width: Math.min(720, parent.width - 160) + height: 54 + (root.omniSearch && root.omniSearch.query.length > 0 + ? Math.min(350, Math.max(76, root.omniSearch.filteredItems.length * 38 + 16)) + 12 + : 0) + z: 20 + visible: root.open + opacity: root.open ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } + + Rectangle { + id: searchPill + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 54 + radius: 16 + color: Qt.rgba(root.paper.r, root.paper.g, root.paper.b, 0.97) + border.color: root.omniSearch && root.omniSearch.query.length > 0 ? root.seal + : Qt.rgba(root.ink.r, root.ink.g, root.ink.b, 0.28) + border.width: root.omniSearch && root.omniSearch.query.length > 0 ? 2 : 1 + Behavior on border.color { ColorAnimation { duration: 120 } } + Behavior on border.width { NumberAnimation { duration: 120 } } + + Loader { + id: omniInputLoader + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 20 + anchors.rightMargin: 132 + anchors.verticalCenter: parent.verticalCenter + height: 34 + } + + Text { + anchors.right: parent.right + anchors.rightMargin: 20 + anchors.verticalCenter: parent.verticalCenter + text: root.omniSearch && root.omniSearch.query.length > 0 + ? (root.omniSearch.filteredItems.length + " results") + : "OMNI · TYPE TO SEARCH" + color: root.sumi + opacity: 0.78 + font.family: root.mono + font.pixelSize: 10 + font.letterSpacing: 1 + } + } + + Item { + id: omniResults + visible: root.omniSearch && root.omniSearch.query.length > 0 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: searchPill.top + anchors.bottomMargin: 12 + height: visible + ? Math.min(350, Math.max(76, root.omniSearch.filteredItems.length * 38 + 16)) + : 0 + z: 21 + + Rectangle { + anchors.fill: parent + radius: 14 + color: Qt.rgba(root.paper.r, root.paper.g, root.paper.b, 0.98) + border.color: Qt.rgba(root.ink.r, root.ink.g, root.ink.b, 0.24) + border.width: 1 + } + + Loader { + id: omniResultLoader + anchors.fill: parent + anchors.margins: 8 + } + } + } + // ---- footer ---- Item { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.bottomMargin: 40 - height: 28 + anchors.bottomMargin: 16 + height: 24 opacity: root.open ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 260 } }