aurora4b1d495
aurora: search settings (ctrl+f)
Android-style search in the settings app: Ctrl+F or the search pill, results replace the page, Enter/click opens the section, scrolls to the row and flashes it. SetIndex parses the pages (SectionLabel + Switch/Knob/Choice/Field/Timeout labels, hints, choice options) plus optional keywords on the rows; the last result filters the keybinds.
+626 −120
21 files changed
- M .config/quickshell/aurora/AudioPage.qml +4 −4
- M .config/quickshell/aurora/BatteryInfo.qml +1 −1
- M .config/quickshell/aurora/BtPage.qml +1 −1
- M .config/quickshell/aurora/Choice.qml +1 −0
- M .config/quickshell/aurora/Field.qml +1 −0
- M .config/quickshell/aurora/GpuSwitch.qml +1 −1
- M .config/quickshell/aurora/IconBtn.qml +1 −0
- M .config/quickshell/aurora/Knob.qml +1 −0
- M .config/quickshell/aurora/NetPage.qml +1 −1
- M .config/quickshell/aurora/SectionLabel.qml +1 −0
- M .config/quickshell/aurora/SetAbout.qml +3 −3
- M .config/quickshell/aurora/SetAppearance.qml +21 −14
- M .config/quickshell/aurora/SetDisplay.qml +7 −2
- A .config/quickshell/aurora/SetIndex.qml +170 −0
- M .config/quickshell/aurora/SetInput.qml +22 −20
- M .config/quickshell/aurora/SetKeys.qml +1 −0
- M .config/quickshell/aurora/SetPower.qml +15 −6
- M .config/quickshell/aurora/SetShell.qml +17 −16
- M .config/quickshell/aurora/SetWindows.qml +17 −13
- M .config/quickshell/aurora/SettingsApp.qml +339 −38
- M .config/quickshell/aurora/Switch.qml +1 −0
| @@ -44,7 +44,7 @@ Flickable { | |||
| 44 | 44 | width: flick.width | |
| 45 | 45 | spacing: 6 | |
| 46 | 46 | ||
| 47 | − | SectionLabel { text: "levels" } | |
| 47 | + | SectionLabel { text: "levels"; keywords: "volume mute microphone mic" } | |
| 48 | 48 | SliderRow { | |
| 49 | 49 | Layout.fillWidth: true | |
| 50 | 50 | Layout.topMargin: 4 | |
| @@ -64,7 +64,7 @@ Flickable { | |||
| 64 | 64 | onIconClicked: if (flick.inp) flick.inp.muted = !flick.inp.muted | |
| 65 | 65 | } | |
| 66 | 66 | ||
| 67 | − | SectionLabel { text: "output"; count: String(flick.sinks.length); Layout.topMargin: 14 } | |
| 67 | + | SectionLabel { text: "output"; keywords: "speakers headphones hdmi default device sink"; count: String(flick.sinks.length); Layout.topMargin: 14 } | |
| 68 | 68 | Repeater { | |
| 69 | 69 | model: flick.sinks | |
| 70 | 70 | delegate: ListRow { | |
| @@ -78,7 +78,7 @@ Flickable { | |||
| 78 | 78 | } | |
| 79 | 79 | } | |
| 80 | 80 | ||
| 81 | − | SectionLabel { text: "input"; count: String(flick.sources.length); Layout.topMargin: 14 } | |
| 81 | + | SectionLabel { text: "input"; keywords: "microphone mic default device source"; count: String(flick.sources.length); Layout.topMargin: 14 } | |
| 82 | 82 | Repeater { | |
| 83 | 83 | model: flick.sources | |
| 84 | 84 | delegate: ListRow { | |
| @@ -92,7 +92,7 @@ Flickable { | |||
| 92 | 92 | } | |
| 93 | 93 | } | |
| 94 | 94 | ||
| 95 | − | SectionLabel { text: "apps"; count: String(flick.streams.length); Layout.topMargin: 14 } | |
| 95 | + | SectionLabel { text: "apps"; keywords: "mixer per-app app volume streams"; count: String(flick.streams.length); Layout.topMargin: 14 } | |
| 96 | 96 | Lb { | |
| 97 | 97 | visible: flick.streams.length === 0 | |
| 98 | 98 | Layout.leftMargin: 6 | |
| @@ -26,7 +26,7 @@ ColumnLayout { | |||
| 26 | 26 | Layout.fillWidth: true | |
| 27 | 27 | spacing: 12 | |
| 28 | 28 | ||
| 29 | − | SectionLabel { text: "battery" } | |
| 29 | + | SectionLabel { text: "battery"; keywords: "charge level percentage health time left" } | |
| 30 | 30 | RowLayout { | |
| 31 | 31 | Layout.leftMargin: 6 | |
| 32 | 32 | spacing: 14 | |
| @@ -83,7 +83,7 @@ ColumnLayout { | |||
| 83 | 83 | } | |
| 84 | 84 | } | |
| 85 | 85 | ||
| 86 | − | SectionLabel { text: "devices"; count: page.adapter && page.adapter.enabled ? String(page.devices.length) : "" } | |
| 86 | + | SectionLabel { text: "devices"; keywords: "pair pairing connect forget headphones headset earbuds speaker controller"; count: page.adapter && page.adapter.enabled ? String(page.devices.length) : "" } | |
| 87 | 87 | ||
| 88 | 88 | Flickable { | |
| 89 | 89 | id: flick | |
| @@ -5,6 +5,7 @@ import QtQuick.Layouts | |||
| 5 | 5 | RowLayout { | |
| 6 | 6 | id: c | |
| 7 | 7 | property string label: "" | |
| 8 | + | property string keywords: "" // synonyms for the settings search (SetIndex reads them from the source) | |
| 8 | 9 | property var options: [] | |
| 9 | 10 | property var value | |
| 10 | 11 | signal picked(var v) | |
| @@ -6,6 +6,7 @@ import QtQuick.Layouts | |||
| 6 | 6 | RowLayout { | |
| 7 | 7 | id: f | |
| 8 | 8 | property string label: "" | |
| 9 | + | property string keywords: "" // synonyms for the settings search (SetIndex reads them from the source) | |
| 9 | 10 | property string value: "" | |
| 10 | 11 | property string placeholder: "" | |
| 11 | 12 | signal submitted(string v) | |
| @@ -16,7 +16,7 @@ ColumnLayout { | |||
| 16 | 16 | visible: Gpu.mode !== "" | |
| 17 | 17 | Layout.fillWidth: true | |
| 18 | 18 | spacing: 10 | |
| 19 | − | SectionLabel { text: "graphics · envycontrol" } | |
| 19 | + | SectionLabel { text: "graphics · envycontrol"; keywords: "gpu graphics mode nvidia hybrid integrated dgpu prime" } | |
| 20 | 20 | RowLayout { | |
| 21 | 21 | Layout.fillWidth: true | |
| 22 | 22 | spacing: 6 | |
| @@ -5,6 +5,7 @@ Rectangle { | |||
| 5 | 5 | id: b | |
| 6 | 6 | property string icon: "" | |
| 7 | 7 | property string label: "" | |
| 8 | + | property string keywords: "" // settings search: a button is indexed only with keywords (SetIndex reads the source) | |
| 8 | 9 | property bool active: false | |
| 9 | 10 | property int iconSize: 16 | |
| 10 | 11 | signal clicked() | |
| @@ -5,6 +5,7 @@ import QtQuick.Layouts | |||
| 5 | 5 | RowLayout { | |
| 6 | 6 | id: k | |
| 7 | 7 | property string label: "" | |
| 8 | + | property string keywords: "" // synonyms for the settings search (SetIndex reads them from the source) | |
| 8 | 9 | property real value: 0 | |
| 9 | 10 | property real from: 0 | |
| 10 | 11 | property real to: 1 | |
| @@ -17,7 +17,7 @@ ColumnLayout { | |||
| 17 | 17 | visible: Net.wiredDevs.length > 0 | |
| 18 | 18 | Layout.fillWidth: true | |
| 19 | 19 | spacing: 4 | |
| 20 | − | SectionLabel { text: "wired" } | |
| 20 | + | SectionLabel { text: "wired"; keywords: "ethernet lan cable usb tethering phone" } | |
| 21 | 21 | Repeater { | |
| 22 | 22 | model: Net.wiredDevs | |
| 23 | 23 | delegate: ListRow { | |
| @@ -5,6 +5,7 @@ import QtQuick.Layouts | |||
| 5 | 5 | RowLayout { | |
| 6 | 6 | id: r | |
| 7 | 7 | property string text: "" | |
| 8 | + | property string keywords: "" // synonyms for the settings search (SetIndex reads them from the source) | |
| 8 | 9 | property string count: "" | |
| 9 | 10 | property string action: "" | |
| 10 | 11 | signal actionClicked() | |
| @@ -72,7 +72,7 @@ Flickable { | |||
| 72 | 72 | } | |
| 73 | 73 | } | |
| 74 | 74 | ||
| 75 | − | SectionLabel { text: "system"; Layout.topMargin: 6 } | |
| 75 | + | SectionLabel { text: "system"; keywords: "kernel cpu gpu host hostname distro packages version uptime"; Layout.topMargin: 6 } | |
| 76 | 76 | Repeater { | |
| 77 | 77 | model: flick.info | |
| 78 | 78 | delegate: RowLayout { | |
| @@ -98,7 +98,7 @@ Flickable { | |||
| 98 | 98 | } | |
| 99 | 99 | } | |
| 100 | 100 | ||
| 101 | − | SectionLabel { text: "memory & storage"; Layout.topMargin: 6 } | |
| 101 | + | SectionLabel { text: "memory & storage"; keywords: "ram disk space drives usage"; Layout.topMargin: 6 } | |
| 102 | 102 | StatBar { | |
| 103 | 103 | label: "memory" | |
| 104 | 104 | frac: SysStats.memTotal > 0 ? SysStats.memUsed / SysStats.memTotal : 0 | |
| @@ -115,7 +115,7 @@ Flickable { | |||
| 115 | 115 | } | |
| 116 | 116 | } | |
| 117 | 117 | ||
| 118 | − | SectionLabel { text: "credits"; Layout.topMargin: 6 } | |
| 118 | + | SectionLabel { text: "credits"; keywords: "license glyph-rice fonts"; Layout.topMargin: 6 } | |
| 119 | 119 | Text { | |
| 120 | 120 | Layout.fillWidth: true | |
| 121 | 121 | Layout.leftMargin: 18 | |
| @@ -17,7 +17,7 @@ Flickable { | |||
| 17 | 17 | width: flick.width | |
| 18 | 18 | spacing: 10 | |
| 19 | 19 | ||
| 20 | − | SectionLabel { text: "wallpaper" } | |
| 20 | + | SectionLabel { text: "wallpaper"; keywords: "background image picture" } | |
| 21 | 21 | Text { | |
| 22 | 22 | Layout.fillWidth: true | |
| 23 | 23 | Layout.leftMargin: 18 | |
| @@ -34,12 +34,14 @@ Flickable { | |||
| 34 | 34 | Layout.fillWidth: true | |
| 35 | 35 | icon: Theme.iImage | |
| 36 | 36 | label: "open picker" | |
| 37 | + | keywords: "wallpaper picker choose background" | |
| 37 | 38 | onClicked: flick.overlayRequested("wall") | |
| 38 | 39 | } | |
| 39 | 40 | IconBtn { | |
| 40 | 41 | Layout.fillWidth: true | |
| 41 | 42 | icon: Theme.iShuffle | |
| 42 | 43 | label: "random from folder" | |
| 44 | + | keywords: "shuffle random wallpaper background" | |
| 43 | 45 | onClicked: Wall.random() | |
| 44 | 46 | } | |
| 45 | 47 | } | |
| @@ -64,29 +66,34 @@ Flickable { | |||
| 64 | 66 | } | |
| 65 | 67 | } | |
| 66 | 68 | ||
| 67 | − | SectionLabel { text: "wallpaper look" } | |
| 69 | + | SectionLabel { text: "wallpaper look"; keywords: "wallpaper style halftone" } | |
| 68 | 70 | Choice { | |
| 69 | 71 | label: "drawn as" | |
| 72 | + | keywords: "wallpaper style look" | |
| 70 | 73 | options: [{ value: "dots", label: "mono dots" }, { value: "colour", label: "colour dots" }, { value: "photo", label: "photo" }] | |
| 71 | 74 | value: Settings.wallLook | |
| 72 | 75 | onPicked: v => Settings.wallLook = v | |
| 73 | 76 | } | |
| 74 | − | Knob { label: "dot size"; from: 6; to: 24; value: Settings.wallPitch; unit: "px"; onMoved: v => Settings.wallPitch = v } | |
| 75 | − | Knob { label: "brightness"; from: 20; to: 100; value: Settings.wallBright; unit: "%"; onMoved: v => Settings.wallBright = v } | |
| 77 | + | Knob { label: "dot size"; keywords: "pitch grid"; from: 6; to: 24; value: Settings.wallPitch; unit: "px"; onMoved: v => Settings.wallPitch = v } | |
| 78 | + | Knob { label: "brightness"; keywords: "wallpaper dim dark"; from: 20; to: 100; value: Settings.wallBright; unit: "%"; onMoved: v => Settings.wallBright = v } | |
| 76 | 79 | Switch { label: "dot grid"; hint: "on the desktop when there is no wallpaper"; checked: Settings.dotGrid; onToggled: on => Settings.dotGrid = on } | |
| 77 | 80 | ||
| 78 | − | SectionLabel { text: "desktop" } | |
| 79 | − | Switch { label: "24-hour clock"; checked: Settings.clock24; onToggled: on => Settings.clock24 = on } | |
| 80 | − | Switch { label: "blinking colon"; checked: Settings.blink; onToggled: on => Settings.blink = on } | |
| 81 | − | Switch { label: "animations"; hint: "panels slide and fade, dots light up in waves"; checked: Settings.motion; onToggled: on => Settings.motion = on } | |
| 81 | + | SectionLabel { text: "desktop"; keywords: "home screen" } | |
| 82 | + | Switch { label: "24-hour clock"; keywords: "time format 12-hour am pm"; checked: Settings.clock24; onToggled: on => Settings.clock24 = on } | |
| 83 | + | Switch { label: "blinking colon"; keywords: "clock blink"; checked: Settings.blink; onToggled: on => Settings.blink = on } | |
| 84 | + | Switch { label: "animations"; keywords: "motion transitions reduce"; hint: "panels slide and fade, dots light up in waves"; checked: Settings.motion; onToggled: on => Settings.motion = on } | |
| 82 | 85 | Choice { | |
| 83 | 86 | label: "app icons" | |
| 87 | + | keywords: "dock icons" | |
| 84 | 88 | options: [{ value: "dots", label: "dots" }, { value: "mono", label: "mono" }, { value: "colour", label: "colour" }] | |
| 85 | 89 | value: Settings.appIcons | |
| 86 | 90 | onPicked: v => Settings.appIcons = v | |
| 87 | 91 | } | |
| 88 | 92 | ||
| 89 | − | SectionLabel { text: "colours" } | |
| 93 | + | SectionLabel { | |
| 94 | + | text: "colours" | |
| 95 | + | keywords: "colors theme scheme accent palette matugen material tonal-spot vibrant expressive fidelity content fruit-salad rainbow neutral monochrome" | |
| 96 | + | } | |
| 90 | 97 | Text { | |
| 91 | 98 | Layout.fillWidth: true | |
| 92 | 99 | Layout.leftMargin: 18 | |
| @@ -110,13 +117,13 @@ Flickable { | |||
| 110 | 117 | } | |
| 111 | 118 | } | |
| 112 | 119 | } | |
| 113 | − | Knob { label: "contrast"; from: -1; to: 1; decimals: 2; value: Settings.matuContrast; onMoved: v => Settings.matuContrast = v } | |
| 120 | + | Knob { label: "contrast"; keywords: "colours colors"; from: -1; to: 1; decimals: 2; value: Settings.matuContrast; onMoved: v => Settings.matuContrast = v } | |
| 114 | 121 | ||
| 115 | 122 | SectionLabel { text: "sizes" } | |
| 116 | − | Knob { label: "bar height"; from: 32; to: 56; value: Settings.barSize; onMoved: v => Settings.barSize = v } | |
| 123 | + | Knob { label: "bar height"; keywords: "top bar size"; from: 32; to: 56; value: Settings.barSize; onMoved: v => Settings.barSize = v } | |
| 117 | 124 | ||
| 118 | − | SectionLabel { text: "transparency" } | |
| 119 | − | Knob { label: "bar opacity"; from: 0.3; to: 1; decimals: 2; value: Settings.barAlpha; onMoved: v => Settings.barAlpha = v } | |
| 120 | − | Knob { label: "panel opacity"; from: 0.3; to: 1; decimals: 2; value: Settings.panelAlpha; onMoved: v => Settings.panelAlpha = v } | |
| 125 | + | SectionLabel { text: "transparency"; keywords: "transparent opacity alpha translucent" } | |
| 126 | + | Knob { label: "bar opacity"; keywords: "transparent"; from: 0.3; to: 1; decimals: 2; value: Settings.barAlpha; onMoved: v => Settings.barAlpha = v } | |
| 127 | + | Knob { label: "panel opacity"; keywords: "transparent control panel"; from: 0.3; to: 1; decimals: 2; value: Settings.panelAlpha; onMoved: v => Settings.panelAlpha = v } | |
| 121 | 128 | } | |
| 122 | 129 | } | |
| @@ -176,9 +176,10 @@ Flickable { | |||
| 176 | 176 | font.pixelSize: 14 | |
| 177 | 177 | } | |
| 178 | 178 | ||
| 179 | − | SectionLabel { text: "layout" } | |
| 179 | + | SectionLabel { text: "layout"; keywords: "monitors screens" } | |
| 180 | 180 | Choice { | |
| 181 | 181 | label: "screens" | |
| 182 | + | keywords: "monitors extend external laptop mirror" | |
| 182 | 183 | options: [{ value: "extend", label: "extend" }, { value: "laptop", label: "laptop only" }, | |
| 183 | 184 | { value: "external", label: "external only" }] | |
| 184 | 185 | value: flick.layout | |
| @@ -243,6 +244,7 @@ Flickable { | |||
| 243 | 244 | Choice { | |
| 244 | 245 | visible: !mon.m.disabled | |
| 245 | 246 | label: "scale" | |
| 247 | + | keywords: "hidpi dpi zoom size" | |
| 246 | 248 | options: [{ value: 1, label: "100%" }, { value: 1.25, label: "125%" }, { value: 1.5, label: "150%" }, | |
| 247 | 249 | { value: 1.75, label: "175%" }, { value: 2, label: "200%" }] | |
| 248 | 250 | value: Math.round(mon.m.scale * 100) / 100 | |
| @@ -251,6 +253,7 @@ Flickable { | |||
| 251 | 253 | Field { | |
| 252 | 254 | visible: !mon.m.disabled | |
| 253 | 255 | label: "position" | |
| 256 | + | keywords: "arrange placement monitor" | |
| 254 | 257 | placeholder: "1920x0" | |
| 255 | 258 | value: mon.m.x + "x" + mon.m.y | |
| 256 | 259 | onSubmitted: x => { | |
| @@ -261,10 +264,11 @@ Flickable { | |||
| 261 | 264 | } | |
| 262 | 265 | } | |
| 263 | 266 | ||
| 264 | − | SectionLabel { text: "brightness & colour" } | |
| 267 | + | SectionLabel { text: "brightness & colour"; keywords: "backlight color" } | |
| 265 | 268 | Knob { | |
| 266 | 269 | visible: Bright.available | |
| 267 | 270 | label: "laptop panel" | |
| 271 | + | keywords: "brightness backlight screen" | |
| 268 | 272 | unit: "%" | |
| 269 | 273 | from: 1 | |
| 270 | 274 | to: 100 | |
| @@ -274,6 +278,7 @@ Flickable { | |||
| 274 | 278 | Switch { | |
| 275 | 279 | visible: Toggles.hasNightLight | |
| 276 | 280 | label: "night light" | |
| 281 | + | keywords: "blue light warm redshift gammastep" | |
| 277 | 282 | hint: "gammastep's warm colours (its schedule comes from ~/.config/gammastep)" | |
| 278 | 283 | checked: Toggles.nightLight | |
| 279 | 284 | onToggled: on => Toggles.setNightLight(on) | |
| @@ -0,0 +1,170 @@ | |||
| 1 | + | pragma Singleton | |
| 2 | + | import QtQuick | |
| 3 | + | import Quickshell | |
| 4 | + | import Quickshell.Io | |
| 5 | + | ||
| 6 | + | // The settings app's sections and its search index. Like Android indexes each preference XML, the rows are parsed out | |
| 7 | + | // of the page files themselves: SectionLabel (a group) and Switch / Knob / Choice / Field / Timeout with a literal | |
| 8 | + | // `label:`, plus their `hint:`, `keywords:` (synonyms, read only here) and a Choice's option labels. IconBtn only with | |
| 9 | + | // `keywords:` (opt-in). Non-literal labels aren't indexed. `extras` covers rows without a label (wi-fi, bluetooth power). | |
| 10 | + | // Editing a page hot-reloads the shell, which re-parses it. | |
| 11 | + | Singleton { | |
| 12 | + | id: idx | |
| 13 | + | ||
| 14 | + | // files: parsed in this order (a page's composite rows like BatteryInfo / GpuSwitch count as the page) | |
| 15 | + | readonly property var sections: [ | |
| 16 | + | { id: "network", word: "network", caption: "network · wi-fi, wired, tethering", files: ["NetPage.qml"], | |
| 17 | + | keywords: "wifi internet ethernet" }, | |
| 18 | + | { id: "bluetooth", word: "bluetooth", caption: "bluetooth · devices, pairing", files: ["BtPage.qml"], keywords: "bt" }, | |
| 19 | + | { id: "sound", word: "sound", caption: "sound · levels, devices, apps", files: ["AudioPage.qml"], | |
| 20 | + | keywords: "audio volume speakers microphone" }, | |
| 21 | + | { id: "appearance", word: "look", caption: "look · wallpaper, home screen, colours, sizes", files: ["SetAppearance.qml"], | |
| 22 | + | keywords: "appearance theme style" }, | |
| 23 | + | { id: "shell", word: "shell", caption: "shell · home screen, dock, osd, notifications", files: ["SetShell.qml"], | |
| 24 | + | keywords: "aurora" }, | |
| 25 | + | { id: "windows", word: "windows", caption: "windows · hyprland gaps, borders, effects, layout", files: ["SetWindows.qml"], | |
| 26 | + | keywords: "hyprland tiling" }, | |
| 27 | + | { id: "input", word: "input", caption: "input · keyboard, mouse, touchpad", files: ["SetInput.qml"], keywords: "" }, | |
| 28 | + | { id: "keybinds", word: "keys", caption: "keys · hyprland binds", files: [], | |
| 29 | + | keywords: "keybinds shortcuts hotkeys bindings" }, | |
| 30 | + | { id: "display", word: "display", caption: "display · monitors, layout, night light", files: ["SetDisplay.qml"], | |
| 31 | + | keywords: "screen monitor resolution" }, | |
| 32 | + | { id: "power", word: "power", caption: "power · battery, profiles, idle", files: ["BatteryInfo.qml", "SetPower.qml", "GpuSwitch.qml"], | |
| 33 | + | keywords: "energy sleep" }, | |
| 34 | + | { id: "about", word: "about", caption: "about · this system", files: ["SetAbout.qml"], keywords: "info version" } | |
| 35 | + | ] | |
| 36 | + | ||
| 37 | + | // rows the parser can't see. group = the SectionLabel to land on ("" = the top of the section) | |
| 38 | + | readonly property var extras: [ | |
| 39 | + | { section: "network", group: "", label: "wi-fi on / off", hint: "rescan for networks", | |
| 40 | + | keywords: "wifi wireless wlan radio internet scan" }, | |
| 41 | + | { section: "network", group: "networks", label: "wi-fi networks", hint: "connect, password, forget", | |
| 42 | + | keywords: "wifi ssid connect password forget hotspot" }, | |
| 43 | + | { section: "bluetooth", group: "", label: "bluetooth on / off", hint: "search for devices", | |
| 44 | + | keywords: "bt adapter radio discover scan" } | |
| 45 | + | ] | |
| 46 | + | ||
| 47 | + | readonly property string dir: Quickshell.env("HOME") + "/.config/quickshell/aurora/" | |
| 48 | + | property var parsed: ({}) // file -> [{kind, group, label, hint, keywords, context}] | |
| 49 | + | ||
| 50 | + | readonly property var files: { | |
| 51 | + | var out = [] | |
| 52 | + | for (var i = 0; i < sections.length; i++) | |
| 53 | + | for (var j = 0; j < sections[i].files.length; j++) out.push(sections[i].files[j]) | |
| 54 | + | return out | |
| 55 | + | } | |
| 56 | + | Variants { | |
| 57 | + | model: idx.files | |
| 58 | + | FileView { | |
| 59 | + | required property string modelData | |
| 60 | + | path: idx.dir + modelData | |
| 61 | + | onLoaded: idx.parse(modelData, text()) | |
| 62 | + | } | |
| 63 | + | } | |
| 64 | + | ||
| 65 | + | // every entry: {kind: section|group|row, section, word, group, label, hint, order, l, k, c, h} | |
| 66 | + | // l/k/c/h = lowercased label / keywords (own synonyms, options) / context (section, group + its keywords) / hint, | |
| 67 | + | // for search() | |
| 68 | + | readonly property var entries: { | |
| 69 | + | var out = [] | |
| 70 | + | function add(s, e) { | |
| 71 | + | out.push({ kind: e.kind, section: s.id, word: s.word, group: e.group, label: e.label, hint: e.hint, | |
| 72 | + | order: out.length, l: norm(e.label), k: norm(e.keywords), | |
| 73 | + | c: norm(s.word + " " + s.id + " " + (e.context || "")), h: norm(e.hint) }) | |
| 74 | + | } | |
| 75 | + | for (var i = 0; i < sections.length; i++) { | |
| 76 | + | var s = sections[i] | |
| 77 | + | add(s, { kind: "section", group: "", label: s.word, hint: s.caption.replace(/^[^·]*·\s*/, ""), | |
| 78 | + | keywords: s.id + " " + s.keywords }) | |
| 79 | + | for (var j = 0; j < s.files.length; j++) { | |
| 80 | + | var p = parsed[s.files[j]] || [] | |
| 81 | + | for (var n = 0; n < p.length; n++) add(s, p[n]) | |
| 82 | + | } | |
| 83 | + | for (var x = 0; x < extras.length; x++) | |
| 84 | + | if (extras[x].section === s.id) | |
| 85 | + | add(s, { kind: "group", group: extras[x].group, | |
| 86 | + | label: extras[x].label, hint: extras[x].hint, keywords: extras[x].keywords }) | |
| 87 | + | } | |
| 88 | + | return out | |
| 89 | + | } | |
| 90 | + | ||
| 91 | + | // the text of `key: "literal"` at the block's own level, or null (expressions don't count) | |
| 92 | + | function lit(top, key) { | |
| 93 | + | var m = new RegExp("(?:^|[\\s;])" + key + ":\\s*\"((?:[^\"\\\\]|\\\\.)*)\"\\s*(?:;|\\n|$)").exec(top) | |
| 94 | + | return m ? m[1].replace(/\\(.)/g, "$1") : null | |
| 95 | + | } | |
| 96 | + | // from just after a `{`: the block's own text (nested {} [] left out) and the nested text, skipping strings/comments | |
| 97 | + | function block(t, i) { | |
| 98 | + | var depth = 1, top = "", inner = "", q = "" | |
| 99 | + | for (; i < t.length && depth > 0; i++) { | |
| 100 | + | var ch = t[i] | |
| 101 | + | if (q !== "") { | |
| 102 | + | if (depth === 1) top += ch; else inner += ch | |
| 103 | + | if (ch === "\\") { i++; if (depth === 1) top += t[i]; else inner += t[i] } | |
| 104 | + | else if (ch === q) q = "" | |
| 105 | + | continue | |
| 106 | + | } | |
| 107 | + | if (ch === "/" && t[i + 1] === "/") { var nl = t.indexOf("\n", i); i = (nl < 0 ? t.length : nl) - 1; continue } | |
| 108 | + | if (ch === '"' || ch === "'" || ch === "`") q = ch | |
| 109 | + | if (ch === "{" || ch === "[") { depth++; continue } | |
| 110 | + | if (ch === "}" || ch === "]") { depth--; continue } | |
| 111 | + | if (depth === 1) top += ch; else inner += ch | |
| 112 | + | } | |
| 113 | + | return { top: top, inner: inner } | |
| 114 | + | } | |
| 115 | + | function parse(file, t) { | |
| 116 | + | var out = [], group = "", groupKw = "", re = /\b(SectionLabel|Switch|Knob|Choice|Field|Timeout|IconBtn)\s*\{/g, m | |
| 117 | + | while ((m = re.exec(t)) !== null) { | |
| 118 | + | var b = block(t, re.lastIndex), kw = lit(b.top, "keywords") | |
| 119 | + | if (m[1] === "SectionLabel") { | |
| 120 | + | group = lit(b.top, "text") || "" | |
| 121 | + | groupKw = group !== "" ? kw || "" : "" | |
| 122 | + | if (group !== "") out.push({ kind: "group", group: group, label: group, hint: "", keywords: groupKw, context: "" }) | |
| 123 | + | continue | |
| 124 | + | } | |
| 125 | + | var label = lit(b.top, "label") | |
| 126 | + | if (!label || (m[1] === "IconBtn" && kw === null)) continue | |
| 127 | + | var opts = [] | |
| 128 | + | if (m[1] === "Choice") { | |
| 129 | + | var om, ore = /\blabel:\s*"([^"]*)"/g | |
| 130 | + | while ((om = ore.exec(b.inner)) !== null) opts.push(om[1]) | |
| 131 | + | } | |
| 132 | + | out.push({ kind: "row", group: group, label: label, hint: lit(b.top, "hint") || "", | |
| 133 | + | keywords: (kw || "") + " " + opts.join(" "), context: group + " " + groupKw }) | |
| 134 | + | } | |
| 135 | + | var p = Object.assign({}, parsed) | |
| 136 | + | p[file] = out | |
| 137 | + | parsed = p | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | // every word has to match the entry (its own text or its section/group); at least one its own text. | |
| 141 | + | // own: label prefix > a label word > keywords > a hint word > inside a label word. Best first, then page order. | |
| 142 | + | // A hyphenated word is indexed split and joined ("24-hour" -> "24 hour 24hour"), the query joins them ("wi-fi" = "wifi"). | |
| 143 | + | function norm(s) { | |
| 144 | + | s = s.toLowerCase() | |
| 145 | + | return " " + s.replace(/-/g, " ") + (s.indexOf("-") >= 0 ? " " + s.replace(/-/g, "") : "") | |
| 146 | + | } | |
| 147 | + | function search(q) { | |
| 148 | + | var words = q.toLowerCase().replace(/-/g, "").trim().split(/\s+/).filter(function (w) { return w !== "" }) | |
| 149 | + | if (!words.length) return [] | |
| 150 | + | var hits = [] | |
| 151 | + | for (var i = 0; i < entries.length; i++) { | |
| 152 | + | var e = entries[i], total = 0, own = false | |
| 153 | + | for (var j = 0; j < words.length; j++) { | |
| 154 | + | var w = words[j], at = e.l.indexOf(" " + w) | |
| 155 | + | var sc = Math.max(at === 0 ? 1000 : at > 0 ? 500 : e.l.indexOf(w) > 0 ? 60 : 0, | |
| 156 | + | e.k.indexOf(" " + w) >= 0 ? 250 : e.k.indexOf(w) >= 0 ? 150 | |
| 157 | + | : e.h.indexOf(" " + w) >= 0 ? 80 : 0) | |
| 158 | + | if (sc > 0) own = true | |
| 159 | + | else sc = e.c.indexOf(" " + w) >= 0 ? 200 : e.c.indexOf(w) >= 0 ? 100 : 0 | |
| 160 | + | if (sc <= 0) { total = 0; break } | |
| 161 | + | total += sc | |
| 162 | + | } | |
| 163 | + | if (total > 0 && own) hits.push({ e: e, sc: total }) | |
| 164 | + | } | |
| 165 | + | hits.sort(function (a, b) { return b.sc - a.sc || a.e.order - b.e.order }) | |
| 166 | + | var res = [] | |
| 167 | + | for (var n = 0; n < hits.length && n < 60; n++) res.push(hits[n].e) | |
| 168 | + | return res | |
| 169 | + | } | |
| 170 | + | } | |
| @@ -28,43 +28,45 @@ Flickable { | |||
| 28 | 28 | font.pixelSize: 14 | |
| 29 | 29 | } | |
| 30 | 30 | ||
| 31 | − | SectionLabel { text: "keyboard" } | |
| 32 | − | Field { label: "layouts"; placeholder: "us,de"; value: flick.v("input:kb_layout", ""); onSubmitted: x => HyprConf.set("input:kb_layout", x) } | |
| 33 | − | Field { label: "variants"; placeholder: "(none)"; value: flick.v("input:kb_variant", ""); onSubmitted: x => HyprConf.set("input:kb_variant", x) } | |
| 34 | − | Field { label: "xkb options"; placeholder: "grp:alt_shift_toggle"; value: flick.v("input:kb_options", ""); onSubmitted: x => HyprConf.set("input:kb_options", x) } | |
| 35 | − | Knob { label: "repeat rate"; unit: "/s"; from: 10; to: 100; value: flick.v("input:repeat_rate", 25); onMoved: x => HyprConf.set("input:repeat_rate", x) } | |
| 36 | − | Knob { label: "repeat delay"; unit: "ms"; from: 150; to: 1000; value: flick.v("input:repeat_delay", 600); onMoved: x => HyprConf.set("input:repeat_delay", Math.round(x / 10) * 10) } | |
| 31 | + | SectionLabel { text: "keyboard"; keywords: "xkb typing" } | |
| 32 | + | Field { label: "layouts"; keywords: "keyboard layout language kb_layout"; placeholder: "us,de"; value: flick.v("input:kb_layout", ""); onSubmitted: x => HyprConf.set("input:kb_layout", x) } | |
| 33 | + | Field { label: "variants"; keywords: "keyboard variant dvorak colemak"; placeholder: "(none)"; value: flick.v("input:kb_variant", ""); onSubmitted: x => HyprConf.set("input:kb_variant", x) } | |
| 34 | + | Field { label: "xkb options"; keywords: "caps lock compose kb_options"; placeholder: "grp:alt_shift_toggle"; value: flick.v("input:kb_options", ""); onSubmitted: x => HyprConf.set("input:kb_options", x) } | |
| 35 | + | Knob { label: "repeat rate"; keywords: "key repeat speed"; unit: "/s"; from: 10; to: 100; value: flick.v("input:repeat_rate", 25); onMoved: x => HyprConf.set("input:repeat_rate", x) } | |
| 36 | + | Knob { label: "repeat delay"; keywords: "key repeat"; unit: "ms"; from: 150; to: 1000; value: flick.v("input:repeat_delay", 600); onMoved: x => HyprConf.set("input:repeat_delay", Math.round(x / 10) * 10) } | |
| 37 | 37 | ||
| 38 | − | SectionLabel { text: "mouse" } | |
| 39 | − | Knob { label: "sensitivity"; from: -1; to: 1; decimals: 2; value: flick.v("input:sensitivity", 0); onMoved: x => HyprConf.set("input:sensitivity", x) } | |
| 38 | + | SectionLabel { text: "mouse"; keywords: "pointer" } | |
| 39 | + | Knob { label: "sensitivity"; keywords: "mouse speed pointer"; from: -1; to: 1; decimals: 2; value: flick.v("input:sensitivity", 0); onMoved: x => HyprConf.set("input:sensitivity", x) } | |
| 40 | 40 | Choice { | |
| 41 | 41 | label: "acceleration" | |
| 42 | + | keywords: "mouse accel" | |
| 42 | 43 | options: [{ value: "", label: "default" }, { value: "flat", label: "flat" }, { value: "adaptive", label: "adaptive" }] | |
| 43 | 44 | value: flick.v("input:accel_profile", "") | |
| 44 | 45 | onPicked: x => HyprConf.set("input:accel_profile", x) | |
| 45 | 46 | } | |
| 46 | 47 | Choice { | |
| 47 | 48 | label: "focus follows mouse" | |
| 49 | + | keywords: "focus hover" | |
| 48 | 50 | options: [{ value: 0, label: "off" }, { value: 1, label: "always" }, { value: 2, label: "loose" }, { value: 3, label: "hover only" }] | |
| 49 | 51 | value: flick.v("input:follow_mouse", -1) | |
| 50 | 52 | onPicked: x => HyprConf.set("input:follow_mouse", x) | |
| 51 | 53 | } | |
| 52 | − | Switch { label: "natural scroll"; checked: flick.v("input:natural_scroll", false); onToggled: on => HyprConf.set("input:natural_scroll", on) } | |
| 53 | − | Switch { label: "left handed"; checked: flick.v("input:left_handed", false); onToggled: on => HyprConf.set("input:left_handed", on) } | |
| 54 | + | Switch { label: "natural scroll"; keywords: "reverse scrolling"; checked: flick.v("input:natural_scroll", false); onToggled: on => HyprConf.set("input:natural_scroll", on) } | |
| 55 | + | Switch { label: "left handed"; keywords: "swap buttons"; checked: flick.v("input:left_handed", false); onToggled: on => HyprConf.set("input:left_handed", on) } | |
| 54 | 56 | ||
| 55 | − | SectionLabel { text: "touchpad" } | |
| 56 | − | Switch { label: "natural scroll"; checked: flick.v("input:touchpad:natural_scroll", false); onToggled: on => HyprConf.set("input:touchpad:natural_scroll", on) } | |
| 57 | − | Switch { label: "tap to click"; checked: flick.v("input:touchpad:tap_to_click", false); onToggled: on => HyprConf.set("input:touchpad:tap_to_click", on) } | |
| 58 | − | Switch { label: "disable while typing"; checked: flick.v("input:touchpad:disable_while_typing", false); onToggled: on => HyprConf.set("input:touchpad:disable_while_typing", on) } | |
| 59 | − | Knob { label: "scroll speed"; from: 0.1; to: 3; decimals: 2; value: flick.v("input:touchpad:scroll_factor", 1); onMoved: x => HyprConf.set("input:touchpad:scroll_factor", x) } | |
| 57 | + | SectionLabel { text: "touchpad"; keywords: "trackpad" } | |
| 58 | + | Switch { label: "natural scroll"; keywords: "reverse scrolling"; checked: flick.v("input:touchpad:natural_scroll", false); onToggled: on => HyprConf.set("input:touchpad:natural_scroll", on) } | |
| 59 | + | Switch { label: "tap to click"; keywords: "tap"; checked: flick.v("input:touchpad:tap_to_click", false); onToggled: on => HyprConf.set("input:touchpad:tap_to_click", on) } | |
| 60 | + | Switch { label: "disable while typing"; keywords: "palm rejection dwt"; checked: flick.v("input:touchpad:disable_while_typing", false); onToggled: on => HyprConf.set("input:touchpad:disable_while_typing", on) } | |
| 61 | + | Knob { label: "scroll speed"; keywords: "scroll factor"; from: 0.1; to: 3; decimals: 2; value: flick.v("input:touchpad:scroll_factor", 1); onMoved: x => HyprConf.set("input:touchpad:scroll_factor", x) } | |
| 60 | 62 | ||
| 61 | − | SectionLabel { text: "gestures" } | |
| 63 | + | SectionLabel { text: "gestures"; keywords: "swipe touchpad fingers" } | |
| 62 | 64 | Switch { label: "invert swipe"; hint: "reverse the 3-finger workspace swipe"; checked: flick.v("gestures:workspace_swipe_invert", true); onToggled: on => HyprConf.set("gestures:workspace_swipe_invert", on) } | |
| 63 | 65 | Knob { label: "swipe distance"; unit: "px"; from: 100; to: 1000; value: flick.v("gestures:workspace_swipe_distance", 300); onMoved: x => HyprConf.set("gestures:workspace_swipe_distance", Math.round(x / 10) * 10) } | |
| 64 | 66 | ||
| 65 | − | SectionLabel { text: "cursor" } | |
| 66 | − | Knob { label: "hide when idle"; unit: "s"; from: 0; to: 30; value: flick.v("cursor:inactive_timeout", 0); onMoved: x => HyprConf.set("cursor:inactive_timeout", x) } | |
| 67 | − | Switch { label: "hide while typing"; checked: flick.v("cursor:hide_on_key_press", false); onToggled: on => HyprConf.set("cursor:hide_on_key_press", on) } | |
| 67 | + | SectionLabel { text: "cursor"; keywords: "mouse pointer" } | |
| 68 | + | Knob { label: "hide when idle"; keywords: "cursor inactive timeout"; unit: "s"; from: 0; to: 30; value: flick.v("cursor:inactive_timeout", 0); onMoved: x => HyprConf.set("cursor:inactive_timeout", x) } | |
| 69 | + | Switch { label: "hide while typing"; keywords: "cursor"; checked: flick.v("cursor:hide_on_key_press", false); onToggled: on => HyprConf.set("cursor:hide_on_key_press", on) } | |
| 68 | 70 | ||
| 69 | 71 | Item { Layout.preferredHeight: 6 } | |
| 70 | 72 | RowLayout { | |
| @@ -78,7 +80,7 @@ Flickable { | |||
| 78 | 80 | font.family: Theme.ui | |
| 79 | 81 | font.pixelSize: 13 | |
| 80 | 82 | } | |
| 81 | − | IconBtn { icon: Theme.iRefresh; label: "reset to config"; onClicked: HyprConf.resetAll(["input:", "cursor:", "gestures:"]) } | |
| 83 | + | IconBtn { icon: Theme.iRefresh; label: "reset to config"; keywords: "reset defaults restore hyprland"; onClicked: HyprConf.resetAll(["input:", "cursor:", "gestures:"]) } | |
| 82 | 84 | } | |
| 83 | 85 | } | |
| 84 | 86 | } | |
| @@ -91,6 +91,7 @@ ColumnLayout { | |||
| 91 | 91 | id: search | |
| 92 | 92 | label: Theme.iSearch + " filter" | |
| 93 | 93 | placeholder: "key, command or group" | |
| 94 | + | value: page.query // the settings search sets it | |
| 94 | 95 | onEdited: v => page.query = v | |
| 95 | 96 | } | |
| 96 | 97 | ||
| @@ -25,6 +25,7 @@ Flickable { | |||
| 25 | 25 | component Timeout: RowLayout { | |
| 26 | 26 | id: t | |
| 27 | 27 | property string label: "" | |
| 28 | + | property string keywords: "" | |
| 28 | 29 | property string key: "" | |
| 29 | 30 | property int value: 0 | |
| 30 | 31 | property int max: 1800 | |
| @@ -68,10 +69,11 @@ Flickable { | |||
| 68 | 69 | ||
| 69 | 70 | BatteryInfo {} | |
| 70 | 71 | ||
| 71 | − | SectionLabel { text: "battery care"; Layout.topMargin: 6; visible: Battery.hasBat } | |
| 72 | + | SectionLabel { text: "battery care"; keywords: "battery health"; Layout.topMargin: 6; visible: Battery.hasBat } | |
| 72 | 73 | Choice { | |
| 73 | 74 | visible: Battery.supported | |
| 74 | 75 | label: "charge limit" | |
| 76 | + | keywords: "conservation 80% battery health longevity" | |
| 75 | 77 | options: [{ value: "off", label: "off" }, { value: "conservation", label: "conservation" }] | |
| 76 | 78 | value: Settings.chargeMode === "off" ? "off" : "conservation" | |
| 77 | 79 | onPicked: v => Settings.chargeMode = v | |
| @@ -91,6 +93,7 @@ Flickable { | |||
| 91 | 93 | Knob { | |
| 92 | 94 | visible: Battery.hasBat | |
| 93 | 95 | label: "warn at" | |
| 96 | + | keywords: "low battery warning" | |
| 94 | 97 | from: 5; to: 40; unit: "%" | |
| 95 | 98 | value: Settings.batLow | |
| 96 | 99 | onMoved: v => Settings.batLow = v | |
| @@ -98,6 +101,7 @@ Flickable { | |||
| 98 | 101 | Knob { | |
| 99 | 102 | visible: Battery.hasBat | |
| 100 | 103 | label: "critical at" | |
| 104 | + | keywords: "critical battery" | |
| 101 | 105 | from: 2; to: 15; unit: "%" | |
| 102 | 106 | value: Settings.batCritical | |
| 103 | 107 | onMoved: v => Settings.batCritical = v | |
| @@ -105,6 +109,7 @@ Flickable { | |||
| 105 | 109 | Choice { | |
| 106 | 110 | visible: Battery.hasBat | |
| 107 | 111 | label: "when critical" | |
| 112 | + | keywords: "low battery action" | |
| 108 | 113 | options: [{ value: "none", label: "warn" }, { value: "suspend", label: "suspend" }, | |
| 109 | 114 | { value: "poweroff", label: "power off" }] | |
| 110 | 115 | value: Settings.batCriticalAction | |
| @@ -115,6 +120,7 @@ Flickable { | |||
| 115 | 120 | Layout.topMargin: 6 | |
| 116 | 121 | visible: Battery.hasProfiles | |
| 117 | 122 | label: "power profile" | |
| 123 | + | keywords: "performance balanced saver power-profiles-daemon" | |
| 118 | 124 | options: flick.profiles.concat(Battery.hasBat ? [{ value: "auto", label: "auto" }] : []) | |
| 119 | 125 | value: Battery.profile | |
| 120 | 126 | onPicked: v => Battery.setProfile(v) | |
| @@ -122,6 +128,7 @@ Flickable { | |||
| 122 | 128 | Choice { | |
| 123 | 129 | visible: Battery.hasProfiles && Settings.powerAuto | |
| 124 | 130 | label: "on AC" | |
| 131 | + | keywords: "plugged in charger power profile" | |
| 125 | 132 | options: flick.profiles | |
| 126 | 133 | value: Settings.powerAc | |
| 127 | 134 | onPicked: v => Settings.powerAc = v | |
| @@ -129,19 +136,21 @@ Flickable { | |||
| 129 | 136 | Choice { | |
| 130 | 137 | visible: Battery.hasProfiles && Settings.powerAuto | |
| 131 | 138 | label: "on battery" | |
| 139 | + | keywords: "unplugged power profile" | |
| 132 | 140 | options: flick.profiles | |
| 133 | 141 | value: Settings.powerBat | |
| 134 | 142 | onPicked: v => Settings.powerBat = v | |
| 135 | 143 | } | |
| 136 | 144 | GpuSwitch { Layout.topMargin: 6 } | |
| 137 | 145 | ||
| 138 | − | SectionLabel { text: "when idle"; Layout.topMargin: 6 } | |
| 139 | − | Timeout { label: "dim the screen"; key: "idleDim"; value: Idle.dim } | |
| 140 | − | Timeout { label: "screen off"; key: "idleOff"; value: Idle.screenOff } | |
| 141 | − | Timeout { label: "lock"; key: "idleLock"; value: Idle.lock } | |
| 142 | − | Timeout { label: "suspend"; key: "idleSuspend"; value: Idle.suspend; max: 3600 } | |
| 146 | + | SectionLabel { text: "when idle"; keywords: "idle timeout sleep hypridle"; Layout.topMargin: 6 } | |
| 147 | + | Timeout { label: "dim the screen"; keywords: "idle dim timeout"; key: "idleDim"; value: Idle.dim } | |
| 148 | + | Timeout { label: "screen off"; keywords: "dpms display off idle timeout"; key: "idleOff"; value: Idle.screenOff } | |
| 149 | + | Timeout { label: "lock"; keywords: "lock screen hyprlock idle timeout"; key: "idleLock"; value: Idle.lock } | |
| 150 | + | Timeout { label: "suspend"; keywords: "sleep idle timeout"; key: "idleSuspend"; value: Idle.suspend; max: 3600 } | |
| 143 | 151 | Switch { | |
| 144 | 152 | label: "caffeine" | |
| 153 | + | keywords: "stay awake keep awake inhibit idle" | |
| 145 | 154 | hint: "keep the screen awake until turned off (not saved)" | |
| 146 | 155 | checked: Toggles.caffeine | |
| 147 | 156 | onToggled: on => Toggles.caffeine = on | |
| @@ -14,13 +14,13 @@ Flickable { | |||
| 14 | 14 | width: flick.width | |
| 15 | 15 | spacing: 8 | |
| 16 | 16 | ||
| 17 | − | SectionLabel { text: "desktop" } | |
| 18 | − | Switch { label: "clock"; hint: "Doto clock, date, stats and what's playing on the desktop"; checked: Settings.desktopClock; onToggled: on => Settings.desktopClock = on } | |
| 19 | − | Switch { label: "visualizer"; hint: "cava as columns of dots along the bottom while music plays"; checked: Settings.desktopViz; onToggled: on => Settings.desktopViz = on } | |
| 20 | − | Knob { label: "visualizer bars"; from: 16; to: 128; value: Settings.vizBars; onMoved: v => Settings.vizBars = v } | |
| 17 | + | SectionLabel { text: "desktop"; keywords: "home screen widgets" } | |
| 18 | + | Switch { label: "clock"; keywords: "desktop clock widget date"; hint: "Doto clock, date, stats and what's playing on the desktop"; checked: Settings.desktopClock; onToggled: on => Settings.desktopClock = on } | |
| 19 | + | Switch { label: "visualizer"; keywords: "cava spectrum music audio"; hint: "cava as columns of dots along the bottom while music plays"; checked: Settings.desktopViz; onToggled: on => Settings.desktopViz = on } | |
| 20 | + | Knob { label: "visualizer bars"; keywords: "cava spectrum"; from: 16; to: 128; value: Settings.vizBars; onMoved: v => Settings.vizBars = v } | |
| 21 | 21 | ||
| 22 | − | SectionLabel { text: "dock" } | |
| 23 | − | Switch { label: "dock"; hint: "autohides at the bottom edge"; checked: Settings.dock; onToggled: on => Settings.dock = on } | |
| 22 | + | SectionLabel { text: "dock"; keywords: "taskbar launcher pinned apps" } | |
| 23 | + | Switch { label: "dock"; keywords: "taskbar launcher"; hint: "autohides at the bottom edge"; checked: Settings.dock; onToggled: on => Settings.dock = on } | |
| 24 | 24 | Text { | |
| 25 | 25 | text: "pinned apps (pin more from the dock's right-click)" | |
| 26 | 26 | color: Theme.fgDim | |
| @@ -43,18 +43,19 @@ Flickable { | |||
| 43 | 43 | } | |
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | − | SectionLabel { text: "on-screen display" } | |
| 47 | − | Knob { label: "osd timeout"; unit: "ms"; from: 500; to: 4000; value: Settings.osdTimeout; onMoved: v => Settings.osdTimeout = Math.round(v / 100) * 100 } | |
| 46 | + | SectionLabel { text: "on-screen display"; keywords: "osd popup indicator" } | |
| 47 | + | Knob { label: "osd timeout"; keywords: "on-screen display duration volume brightness popup"; unit: "ms"; from: 500; to: 4000; value: Settings.osdTimeout; onMoved: v => Settings.osdTimeout = Math.round(v / 100) * 100 } | |
| 48 | 48 | ||
| 49 | 49 | SectionLabel { text: "network" } | |
| 50 | − | Switch { label: "captive portals"; hint: "open the login page in the browser when a network needs one"; checked: Settings.portalAutoOpen; onToggled: on => Settings.portalAutoOpen = on } | |
| 50 | + | Switch { label: "captive portals"; keywords: "hotel campus wifi login sign in hotspot"; hint: "open the login page in the browser when a network needs one"; checked: Settings.portalAutoOpen; onToggled: on => Settings.portalAutoOpen = on } | |
| 51 | 51 | ||
| 52 | 52 | SectionLabel { text: "bar" } | |
| 53 | − | Switch { label: "privacy indicators"; hint: "show in the bar when an app records the mic, uses the camera or shares the screen"; checked: Settings.privacyIndicators; onToggled: on => Settings.privacyIndicators = on } | |
| 53 | + | Switch { label: "privacy indicators"; keywords: "microphone mic camera webcam screen sharing recording"; hint: "show in the bar when an app records the mic, uses the camera or shares the screen"; checked: Settings.privacyIndicators; onToggled: on => Settings.privacyIndicators = on } | |
| 54 | 54 | ||
| 55 | − | SectionLabel { text: "weather" } | |
| 55 | + | SectionLabel { text: "weather"; keywords: "forecast temperature" } | |
| 56 | 56 | Field { | |
| 57 | 57 | label: "location" | |
| 58 | + | keywords: "city place weather" | |
| 58 | 59 | placeholder: "by IP · or a city · or lat,lon" | |
| 59 | 60 | value: Settings.weatherLoc | |
| 60 | 61 | onSubmitted: v => Settings.weatherLoc = v.trim() | |
| @@ -70,13 +71,13 @@ Flickable { | |||
| 70 | 71 | font.pixelSize: 13 | |
| 71 | 72 | } | |
| 72 | 73 | ||
| 73 | − | SectionLabel { text: "notifications" } | |
| 74 | − | Switch { label: "do not disturb"; hint: "hide popups and mute the sound (critical ones still show)"; checked: Notifs.dnd; onToggled: on => Notifs.dnd = on } | |
| 75 | − | Switch { label: "sound"; checked: Settings.notifSound; onToggled: on => Settings.notifSound = on } | |
| 74 | + | SectionLabel { text: "notifications"; keywords: "popups toasts alerts" } | |
| 75 | + | Switch { label: "do not disturb"; keywords: "dnd silence quiet focus"; hint: "hide popups and mute the sound (critical ones still show)"; checked: Notifs.dnd; onToggled: on => Notifs.dnd = on } | |
| 76 | + | Switch { label: "sound"; keywords: "notification chime"; checked: Settings.notifSound; onToggled: on => Settings.notifSound = on } | |
| 76 | 77 | RowLayout { | |
| 77 | 78 | Layout.fillWidth: true | |
| 78 | 79 | spacing: 8 | |
| 79 | − | Knob { label: "sound volume"; from: 0; to: 1; decimals: 2; value: Settings.notifVolume; onMoved: v => Settings.notifVolume = v } | |
| 80 | + | Knob { label: "sound volume"; keywords: "notification chime loudness"; from: 0; to: 1; decimals: 2; value: Settings.notifVolume; onMoved: v => Settings.notifVolume = v } | |
| 80 | 81 | IconBtn { icon: Theme.iPlay; implicitHeight: 26; onClicked: Notifs.playSound() } | |
| 81 | 82 | } | |
| 82 | 83 | ||
| @@ -92,7 +93,7 @@ Flickable { | |||
| 92 | 93 | font.family: Theme.ui | |
| 93 | 94 | font.pixelSize: 13 | |
| 94 | 95 | } | |
| 95 | − | IconBtn { icon: Theme.iRefresh; label: "reset shell settings"; onClicked: Settings.reset() } | |
| 96 | + | IconBtn { icon: Theme.iRefresh; label: "reset shell settings"; keywords: "reset defaults restore"; onClicked: Settings.reset() } | |
| 96 | 97 | } | |
| 97 | 98 | } | |
| 98 | 99 | } | |
| @@ -29,29 +29,30 @@ Flickable { | |||
| 29 | 29 | font.pixelSize: 14 | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | − | SectionLabel { text: "gaps & borders" } | |
| 33 | − | Knob { label: "inner gaps"; unit: "px"; from: 0; to: 30; value: flick.v("general:gaps_in", 0); onMoved: x => HyprConf.set("general:gaps_in", x) } | |
| 34 | − | Knob { label: "outer gaps"; unit: "px"; from: 0; to: 40; value: flick.v("general:gaps_out", 0); onMoved: x => HyprConf.set("general:gaps_out", x) } | |
| 35 | − | Knob { label: "border"; unit: "px"; from: 0; to: 8; value: flick.v("general:border_size", 0); onMoved: x => HyprConf.set("general:border_size", x) } | |
| 36 | − | Knob { label: "rounding"; unit: "px"; from: 0; to: 20; value: flick.v("decoration:rounding", 0); onMoved: x => HyprConf.set("decoration:rounding", x) } | |
| 32 | + | SectionLabel { text: "gaps & borders"; keywords: "spacing margins" } | |
| 33 | + | Knob { label: "inner gaps"; keywords: "spacing padding gaps_in"; unit: "px"; from: 0; to: 30; value: flick.v("general:gaps_in", 0); onMoved: x => HyprConf.set("general:gaps_in", x) } | |
| 34 | + | Knob { label: "outer gaps"; keywords: "spacing margin gaps_out"; unit: "px"; from: 0; to: 40; value: flick.v("general:gaps_out", 0); onMoved: x => HyprConf.set("general:gaps_out", x) } | |
| 35 | + | Knob { label: "border"; keywords: "border size thickness outline"; unit: "px"; from: 0; to: 8; value: flick.v("general:border_size", 0); onMoved: x => HyprConf.set("general:border_size", x) } | |
| 36 | + | Knob { label: "rounding"; keywords: "corners radius rounded"; unit: "px"; from: 0; to: 20; value: flick.v("decoration:rounding", 0); onMoved: x => HyprConf.set("decoration:rounding", x) } | |
| 37 | 37 | Switch { label: "resize on border"; hint: "drag window edges to resize"; checked: flick.v("general:resize_on_border", false); onToggled: on => HyprConf.set("general:resize_on_border", on) } | |
| 38 | 38 | ||
| 39 | − | SectionLabel { text: "opacity" } | |
| 40 | − | Knob { label: "focused"; from: 0.3; to: 1; decimals: 2; value: flick.v("decoration:active_opacity", 1); onMoved: x => HyprConf.set("decoration:active_opacity", x) } | |
| 41 | − | Knob { label: "unfocused"; from: 0.3; to: 1; decimals: 2; value: flick.v("decoration:inactive_opacity", 1); onMoved: x => HyprConf.set("decoration:inactive_opacity", x) } | |
| 42 | − | Switch { label: "dim unfocused"; checked: flick.v("decoration:dim_inactive", false); onToggled: on => HyprConf.set("decoration:dim_inactive", on) } | |
| 39 | + | SectionLabel { text: "opacity"; keywords: "transparency transparent alpha" } | |
| 40 | + | Knob { label: "focused"; keywords: "active window opacity"; from: 0.3; to: 1; decimals: 2; value: flick.v("decoration:active_opacity", 1); onMoved: x => HyprConf.set("decoration:active_opacity", x) } | |
| 41 | + | Knob { label: "unfocused"; keywords: "inactive window opacity"; from: 0.3; to: 1; decimals: 2; value: flick.v("decoration:inactive_opacity", 1); onMoved: x => HyprConf.set("decoration:inactive_opacity", x) } | |
| 42 | + | Switch { label: "dim unfocused"; keywords: "inactive windows darken"; checked: flick.v("decoration:dim_inactive", false); onToggled: on => HyprConf.set("decoration:dim_inactive", on) } | |
| 43 | 43 | ||
| 44 | 44 | SectionLabel { text: "effects" } | |
| 45 | − | Switch { label: "blur"; hint: "behind transparent windows and layers"; checked: flick.v("decoration:blur:enabled", false); onToggled: on => HyprConf.set("decoration:blur:enabled", on) } | |
| 45 | + | Switch { label: "blur"; keywords: "frosted glass"; hint: "behind transparent windows and layers"; checked: flick.v("decoration:blur:enabled", false); onToggled: on => HyprConf.set("decoration:blur:enabled", on) } | |
| 46 | 46 | Knob { label: "blur size"; from: 1; to: 15; value: flick.v("decoration:blur:size", 1); onMoved: x => HyprConf.set("decoration:blur:size", x) } | |
| 47 | 47 | Knob { label: "blur passes"; from: 1; to: 6; value: flick.v("decoration:blur:passes", 1); onMoved: x => HyprConf.set("decoration:blur:passes", x) } | |
| 48 | 48 | Switch { label: "blur x-ray"; hint: "floating windows blur the wallpaper, not what's under them"; checked: flick.v("decoration:blur:xray", false); onToggled: on => HyprConf.set("decoration:blur:xray", on) } | |
| 49 | − | Switch { label: "shadows"; checked: flick.v("decoration:shadow:enabled", false); onToggled: on => HyprConf.set("decoration:shadow:enabled", on) } | |
| 50 | − | Switch { label: "animations"; checked: flick.v("animations:enabled", false); onToggled: on => HyprConf.set("animations:enabled", on) } | |
| 49 | + | Switch { label: "shadows"; keywords: "drop shadow"; checked: flick.v("decoration:shadow:enabled", false); onToggled: on => HyprConf.set("decoration:shadow:enabled", on) } | |
| 50 | + | Switch { label: "animations"; keywords: "window motion transitions"; checked: flick.v("animations:enabled", false); onToggled: on => HyprConf.set("animations:enabled", on) } | |
| 51 | 51 | ||
| 52 | 52 | SectionLabel { text: "layout" } | |
| 53 | 53 | Choice { | |
| 54 | 54 | label: "tiling layout" | |
| 55 | + | keywords: "tiling mode" | |
| 55 | 56 | options: [{ value: "master", label: "master" }, { value: "dwindle", label: "dwindle" }] | |
| 56 | 57 | value: flick.v("general:layout", "") | |
| 57 | 58 | onPicked: x => HyprConf.set("general:layout", x) | |
| @@ -60,15 +61,17 @@ Flickable { | |||
| 60 | 61 | Layout.fillWidth: true | |
| 61 | 62 | visible: flick.v("general:layout", "") === "master" | |
| 62 | 63 | spacing: 8 | |
| 63 | − | Knob { label: "master width"; from: 0.1; to: 0.9; decimals: 2; value: flick.v("master:mfact", 0.55); onMoved: x => HyprConf.set("master:mfact", x) } | |
| 64 | + | Knob { label: "master width"; keywords: "mfact ratio split size"; from: 0.1; to: 0.9; decimals: 2; value: flick.v("master:mfact", 0.55); onMoved: x => HyprConf.set("master:mfact", x) } | |
| 64 | 65 | Choice { | |
| 65 | 66 | label: "new window" | |
| 67 | + | keywords: "placement" | |
| 66 | 68 | options: [{ value: "master", label: "master" }, { value: "slave", label: "stack" }, { value: "inherit", label: "inherit" }] | |
| 67 | 69 | value: flick.v("master:new_status", "") | |
| 68 | 70 | onPicked: x => HyprConf.set("master:new_status", x) | |
| 69 | 71 | } | |
| 70 | 72 | Choice { | |
| 71 | 73 | label: "master side" | |
| 74 | + | keywords: "orientation" | |
| 72 | 75 | options: [{ value: "left", label: "left" }, { value: "right", label: "right" }, { value: "top", label: "top" }, | |
| 73 | 76 | { value: "bottom", label: "bottom" }, { value: "center", label: "center" }] | |
| 74 | 77 | value: flick.v("master:orientation", "") | |
| @@ -109,6 +112,7 @@ Flickable { | |||
| 109 | 112 | IconBtn { | |
| 110 | 113 | icon: Theme.iRefresh | |
| 111 | 114 | label: "reset to config" | |
| 115 | + | keywords: "reset defaults restore hyprland" | |
| 112 | 116 | onClicked: HyprConf.resetAll(["general:", "decoration:", "animations:", "master:", "dwindle:", "misc:"]) | |
| 113 | 117 | } | |
| 114 | 118 | } | |
| @@ -1,10 +1,13 @@ | |||
| 1 | 1 | import QtQuick | |
| 2 | + | import QtQuick.Layouts | |
| 2 | 3 | import Quickshell | |
| 3 | 4 | ||
| 4 | 5 | // The settings app: a normal Hyprland window (class org.quickshell, title "aurora settings"), opened maximized by a | |
| 5 | 6 | // window rule. glyph's prefs layout: the sections as a column of Doto words (the selected one large with the live dot), | |
| 6 | 7 | // the page on the right under a caption, key hints at the bottom. One Loader, so only the shown section runs. | |
| 7 | 8 | // Tab / Shift+Tab, Up/Down (outside text fields) or Ctrl+J/K = sections, Esc closes. | |
| 9 | + | // Ctrl+F (or the search button) searches every setting (SetIndex): the results replace the page, Enter / click opens | |
| 10 | + | // the section, scrolls to the row and flashes it. | |
| 8 | 11 | // Opened with the `settings [section]` IPC. After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), prefs/Prefs.qml. | |
| 9 | 12 | FloatingWindow { | |
| 10 | 13 | id: win | |
| @@ -21,20 +24,10 @@ FloatingWindow { | |||
| 21 | 24 | color: Theme.bg | |
| 22 | 25 | // closed by the compositor (killactive): keep shell state in sync | |
| 23 | 26 | onVisibleChanged: if (!visible && opened) closeRequested() | |
| 27 | + | onOpenedChanged: if (!opened) stopSearch() | |
| 28 | + | onSectionChanged: if (searching) stopSearch() // switched from outside (IPC, Tab) | |
| 24 | 29 | ||
| 25 | − | readonly property var sections: [ | |
| 26 | − | { id: "network", word: "network", caption: "network · wi-fi, wired, tethering" }, | |
| 27 | − | { id: "bluetooth", word: "bluetooth", caption: "bluetooth · devices, pairing" }, | |
| 28 | − | { id: "sound", word: "sound", caption: "sound · levels, devices, apps" }, | |
| 29 | − | { id: "appearance", word: "look", caption: "look · wallpaper, home screen, colours, sizes" }, | |
| 30 | − | { id: "shell", word: "shell", caption: "shell · home screen, dock, osd, notifications" }, | |
| 31 | − | { id: "windows", word: "windows", caption: "windows · hyprland gaps, borders, effects, layout" }, | |
| 32 | − | { id: "input", word: "input", caption: "input · keyboard, mouse, touchpad" }, | |
| 33 | − | { id: "keybinds", word: "keys", caption: "keys · hyprland binds" }, | |
| 34 | − | { id: "display", word: "display", caption: "display · monitors, layout, night light" }, | |
| 35 | − | { id: "power", word: "power", caption: "power · battery, profiles, idle" }, | |
| 36 | − | { id: "about", word: "about", caption: "about · this system" } | |
| 37 | − | ] | |
| 30 | + | readonly property var sections: SetIndex.sections | |
| 38 | 31 | readonly property int index: { | |
| 39 | 32 | for (var i = 0; i < sections.length; i++) if (sections[i].id === section) return i | |
| 40 | 33 | return 0 | |
| @@ -43,19 +36,134 @@ FloatingWindow { | |||
| 43 | 36 | // typing in a field: arrows belong to it | |
| 44 | 37 | function inText() { var a = win.activeFocusItem; return a !== null && a.cursorPosition !== undefined } | |
| 45 | 38 | ||
| 39 | + | // ---- search ---- | |
| 40 | + | property bool searching: false | |
| 41 | + | property string query: "" | |
| 42 | + | property int pick: 0 | |
| 43 | + | // the matches, then always "keybinds matching ..." (SetKeys has its own filter) | |
| 44 | + | readonly property var results: { | |
| 45 | + | var q = query.trim() | |
| 46 | + | if (!searching || q === "") return [] | |
| 47 | + | return SetIndex.search(q).concat([{ kind: "keys", section: "keybinds", word: "keys", group: "", | |
| 48 | + | label: "keybinds matching “" + q + "”", hint: "filter the hyprland binds", query: q }]) | |
| 49 | + | } | |
| 50 | + | readonly property var hitSections: { | |
| 51 | + | var h = {} | |
| 52 | + | for (var i = 0; i < results.length; i++) if (results[i].kind !== "keys") h[results[i].section] = true | |
| 53 | + | return h | |
| 54 | + | } | |
| 55 | + | onPickChanged: list.positionViewAtIndex(pick, ListView.Contain) | |
| 56 | + | ||
| 57 | + | function startSearch() { | |
| 58 | + | if (searching) searchInput.selectAll() | |
| 59 | + | searching = true | |
| 60 | + | searchInput.forceActiveFocus() | |
| 61 | + | } | |
| 62 | + | function stopSearch() { | |
| 63 | + | if (!searching) return | |
| 64 | + | searching = false | |
| 65 | + | searchInput.text = "" | |
| 66 | + | root.forceActiveFocus() | |
| 67 | + | } | |
| 68 | + | function crumb(e) { | |
| 69 | + | if (e.kind === "section") return "section" | |
| 70 | + | return e.word + (e.kind === "row" && e.group !== "" ? " · " + e.group : "") | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // ---- landing on a result: open its section, find the row, scroll to it, flash it ---- | |
| 74 | + | property var landing: null | |
| 75 | + | property int landTries: 0 | |
| 76 | + | property Item hlTarget: null // the flashed row (nulled when the page goes away) | |
| 77 | + | property Item hlFlick: null // the Flickable it scrolls in, if any | |
| 78 | + | function go(e) { | |
| 79 | + | stopSearch() | |
| 80 | + | hlTarget = null | |
| 81 | + | landing = e | |
| 82 | + | landTries = 0 | |
| 83 | + | if (e.section !== section) sectionRequested(e.section) | |
| 84 | + | landTimer.restart() | |
| 85 | + | } | |
| 86 | + | // the page's items in document order: past the group's SectionLabel, the first one with this label | |
| 87 | + | function findRow(e) { | |
| 88 | + | var hit = null, header = null, past = e.group === "" | |
| 89 | + | function walk(it) { | |
| 90 | + | var kids = it.children | |
| 91 | + | for (var i = 0; i < kids.length && !hit; i++) { | |
| 92 | + | var c = kids[i] | |
| 93 | + | if (e.group !== "" && c.actionClicked !== undefined && c.text === e.group) { | |
| 94 | + | past = true | |
| 95 | + | header = c | |
| 96 | + | if (e.kind === "group") { hit = c; return } | |
| 97 | + | } else if (past && e.kind === "row" && c.label === e.label) { | |
| 98 | + | hit = c | |
| 99 | + | return | |
| 100 | + | } | |
| 101 | + | walk(c) | |
| 102 | + | } | |
| 103 | + | } | |
| 104 | + | if (loader.item) walk(loader.item) | |
| 105 | + | return { hit: hit, header: header } | |
| 106 | + | } | |
| 107 | + | function land() { | |
| 108 | + | var e = landing | |
| 109 | + | if (!e || !loader.item) { landTimer.stop(); landing = null; return } | |
| 110 | + | var top = e.kind === "section" || e.kind === "keys" || e.group === "" && e.kind === "group" | |
| 111 | + | var f = top ? { hit: null, header: null } : findRow(e) | |
| 112 | + | if (!top && !f.hit && ++landTries < 16) return // rows from async data (monitors) may come late | |
| 113 | + | landTimer.stop() | |
| 114 | + | landing = null | |
| 115 | + | if (e.kind === "keys") loader.item.query = e.query | |
| 116 | + | if (!top && !f.hit) console.warn("settings search: no row for", e.section, "/", e.group, "/", e.label) | |
| 117 | + | // a hidden row (master-only knobs under dwindle...) -> its group's header | |
| 118 | + | var t = f.hit && f.hit.visible ? f.hit : f.header && f.header.visible ? f.header : null | |
| 119 | + | scrollTo(t) | |
| 120 | + | if (t) { hlTarget = t; flashAnim.restart() } | |
| 121 | + | } | |
| 122 | + | // t's row ~30% from the top of its Flickable; null = the top of the page | |
| 123 | + | function scrollTo(t) { | |
| 124 | + | var fl = null | |
| 125 | + | if (!t) fl = loader.item && loader.item.contentY !== undefined ? loader.item : null | |
| 126 | + | for (var p = t ? t.parent : null; p && p !== loader; p = p.parent) | |
| 127 | + | if (p.contentY !== undefined && p.contentHeight !== undefined) { fl = p; break } | |
| 128 | + | hlFlick = fl | |
| 129 | + | if (!fl) return | |
| 130 | + | var y = t ? t.mapToItem(fl.contentItem, 0, 0).y - fl.height * 0.3 : 0 | |
| 131 | + | scrollAnim.stop() | |
| 132 | + | scrollAnim.target = fl | |
| 133 | + | scrollAnim.to = Math.max(0, Math.min(y, fl.contentHeight - fl.height)) | |
| 134 | + | scrollAnim.start() | |
| 135 | + | } | |
| 136 | + | ||
| 46 | 137 | Item { | |
| 47 | 138 | id: root | |
| 48 | 139 | anchors.fill: parent | |
| 49 | 140 | focus: true | |
| 50 | 141 | Keys.onPressed: e => { | |
| 51 | 142 | var ctrl = e.modifiers & Qt.ControlModifier | |
| 52 | − | if (e.key === Qt.Key_Escape) { win.closeRequested(); e.accepted = true } | |
| 143 | + | if (e.key === Qt.Key_Escape) { if (win.searching) win.stopSearch(); else win.closeRequested(); e.accepted = true } | |
| 144 | + | else if (ctrl && e.key === Qt.Key_F) { win.startSearch(); e.accepted = true } | |
| 53 | 145 | else if (e.key === Qt.Key_Tab || (ctrl && e.key === Qt.Key_J)) { win.cycle(1); e.accepted = true } | |
| 54 | 146 | else if (e.key === Qt.Key_Backtab || (ctrl && e.key === Qt.Key_K)) { win.cycle(-1); e.accepted = true } | |
| 55 | 147 | } | |
| 56 | 148 | // a click outside the page drops the focus out of text fields (arrows switch sections again) | |
| 57 | 149 | MouseArea { anchors.fill: parent; onClicked: root.forceActiveFocus() } | |
| 58 | 150 | ||
| 151 | + | Timer { id: landTimer; interval: 60; repeat: true; onTriggered: win.land() } | |
| 152 | + | NumberAnimation { | |
| 153 | + | id: scrollAnim | |
| 154 | + | property: "contentY" | |
| 155 | + | duration: Theme.t | |
| 156 | + | easing.type: Easing.BezierSpline | |
| 157 | + | easing.bezierCurve: Theme.glide | |
| 158 | + | } | |
| 159 | + | SequentialAnimation { | |
| 160 | + | id: flashAnim | |
| 161 | + | NumberAnimation { target: flash; property: "opacity"; from: 0; to: 1; duration: Theme.t / 2 } | |
| 162 | + | PauseAnimation { duration: 1400 } | |
| 163 | + | NumberAnimation { target: flash; property: "opacity"; to: 0; duration: Theme.motion ? 800 : 0 } | |
| 164 | + | ScriptAction { script: win.hlTarget = null } | |
| 165 | + | } | |
| 166 | + | ||
| 59 | 167 | Item { | |
| 60 | 168 | id: stage | |
| 61 | 169 | anchors.fill: parent | |
| @@ -75,6 +183,9 @@ FloatingWindow { | |||
| 75 | 183 | required property int index | |
| 76 | 184 | readonly property bool on: win.index === index | |
| 77 | 185 | spacing: 20 | |
| 186 | + | // while searching, sections without a match fade | |
| 187 | + | opacity: !win.searching || win.query.trim() === "" || win.hitSections[modelData.id] ? 1 : 0.3 | |
| 188 | + | Behavior on opacity { enabled: Theme.motion; NumberAnimation { duration: Theme.t } } | |
| 78 | 189 | Rectangle { | |
| 79 | 190 | anchors.verticalCenter: parent.verticalCenter | |
| 80 | 191 | visible: w.on | |
| @@ -94,7 +205,7 @@ FloatingWindow { | |||
| 94 | 205 | Behavior on color { enabled: Theme.motion; ColorAnimation { duration: Theme.t } } | |
| 95 | 206 | } | |
| 96 | 207 | HoverHandler { id: wm; cursorShape: Qt.PointingHandCursor } | |
| 97 | − | TapHandler { onTapped: { win.sectionRequested(w.modelData.id); root.forceActiveFocus() } } | |
| 208 | + | TapHandler { onTapped: { win.stopSearch(); win.sectionRequested(w.modelData.id); root.forceActiveFocus() } } | |
| 98 | 209 | } | |
| 99 | 210 | } | |
| 100 | 211 | } | |
| @@ -103,34 +214,224 @@ FloatingWindow { | |||
| 103 | 214 | Item { | |
| 104 | 215 | id: pageBox | |
| 105 | 216 | x: Math.max(words.x + 560, Math.round(win.width * 0.42)) | |
| 106 | − | y: 96 | |
| 217 | + | y: 80 | |
| 107 | 218 | width: Math.min(900, win.width - x - 80) | |
| 108 | 219 | height: parent.height - y - 110 | |
| 109 | − | Lb { | |
| 110 | − | id: caption | |
| 111 | − | x: 18 | |
| 112 | − | text: win.sections[win.index].caption | |
| 220 | + | ||
| 221 | + | // the caption + search button, or the search pill | |
| 222 | + | Item { | |
| 223 | + | id: head | |
| 224 | + | width: parent.width | |
| 225 | + | height: 52 | |
| 226 | + | Lb { | |
| 227 | + | x: 18 | |
| 228 | + | anchors.verticalCenter: parent.verticalCenter | |
| 229 | + | visible: !win.searching | |
| 230 | + | text: win.sections[win.index].caption | |
| 231 | + | } | |
| 232 | + | IconBtn { | |
| 233 | + | anchors.right: parent.right | |
| 234 | + | anchors.verticalCenter: parent.verticalCenter | |
| 235 | + | visible: !win.searching | |
| 236 | + | icon: Theme.iSearch | |
| 237 | + | label: "search" | |
| 238 | + | onClicked: win.startSearch() | |
| 239 | + | } | |
| 240 | + | Rectangle { | |
| 241 | + | anchors.fill: parent | |
| 242 | + | visible: win.searching | |
| 243 | + | radius: height / 2 | |
| 244 | + | color: Theme.s2 | |
| 245 | + | Icon { | |
| 246 | + | id: lens | |
| 247 | + | x: 20 | |
| 248 | + | anchors.verticalCenter: parent.verticalCenter | |
| 249 | + | text: Theme.iSearch | |
| 250 | + | color: Theme.grey | |
| 251 | + | font.pixelSize: 20 | |
| 252 | + | } | |
| 253 | + | TextInput { | |
| 254 | + | id: searchInput | |
| 255 | + | anchors.left: lens.right | |
| 256 | + | anchors.leftMargin: 14 | |
| 257 | + | anchors.right: found.left | |
| 258 | + | anchors.rightMargin: 12 | |
| 259 | + | anchors.verticalCenter: parent.verticalCenter | |
| 260 | + | font.family: Theme.ui | |
| 261 | + | font.pixelSize: 18 | |
| 262 | + | color: Theme.fg | |
| 263 | + | selectionColor: Theme.fill | |
| 264 | + | selectedTextColor: Theme.fillFg | |
| 265 | + | clip: true | |
| 266 | + | cursorDelegate: Rectangle { | |
| 267 | + | width: 2 | |
| 268 | + | color: Theme.live | |
| 269 | + | visible: searchInput.activeFocus | |
| 270 | + | Timer { running: searchInput.activeFocus; interval: 500; repeat: true; onTriggered: parent.opacity = 1 - parent.opacity } | |
| 271 | + | } | |
| 272 | + | Text { | |
| 273 | + | visible: !searchInput.text | |
| 274 | + | text: "search settings" | |
| 275 | + | font: searchInput.font | |
| 276 | + | color: Theme.mute | |
| 277 | + | } | |
| 278 | + | onTextChanged: { win.query = text; win.pick = 0 } | |
| 279 | + | // handled here, so the window's Tab / Ctrl+J/K / Esc don't fire | |
| 280 | + | Keys.onPressed: e => { | |
| 281 | + | var ctrl = e.modifiers & Qt.ControlModifier, n = win.results.length | |
| 282 | + | if (e.key === Qt.Key_Escape) win.stopSearch() | |
| 283 | + | else if (e.key === Qt.Key_Return || e.key === Qt.Key_Enter) { if (n) win.go(win.results[win.pick]) } | |
| 284 | + | else if (e.key === Qt.Key_Down || e.key === Qt.Key_Tab || (ctrl && e.key === Qt.Key_J)) { if (n) win.pick = (win.pick + 1) % n } | |
| 285 | + | else if (e.key === Qt.Key_Up || e.key === Qt.Key_Backtab || (ctrl && e.key === Qt.Key_K)) { if (n) win.pick = (win.pick + n - 1) % n } | |
| 286 | + | else if (ctrl && e.key === Qt.Key_F) selectAll() | |
| 287 | + | else return | |
| 288 | + | e.accepted = true | |
| 289 | + | } | |
| 290 | + | } | |
| 291 | + | Lb { | |
| 292 | + | id: found | |
| 293 | + | anchors.right: parent.right | |
| 294 | + | anchors.rightMargin: 22 | |
| 295 | + | anchors.verticalCenter: parent.verticalCenter | |
| 296 | + | text: win.results.length ? (win.results.length - 1) + " found" : "" | |
| 297 | + | } | |
| 298 | + | } | |
| 113 | 299 | } | |
| 114 | − | Loader { | |
| 115 | − | anchors.top: caption.bottom | |
| 116 | − | anchors.topMargin: 24 | |
| 300 | + | ||
| 301 | + | Item { | |
| 302 | + | anchors.top: head.bottom | |
| 303 | + | anchors.topMargin: 16 | |
| 117 | 304 | anchors.left: parent.left | |
| 118 | 305 | anchors.right: parent.right | |
| 119 | 306 | anchors.bottom: parent.bottom | |
| 120 | − | active: win.opened | |
| 121 | − | sourceComponent: { | |
| 122 | − | switch (win.section) { | |
| 123 | − | case "bluetooth": return btC | |
| 124 | − | case "sound": return soundC | |
| 125 | − | case "appearance": return appearanceC | |
| 126 | − | case "shell": return shellC | |
| 127 | − | case "windows": return windowsC | |
| 128 | − | case "input": return inputC | |
| 129 | − | case "keybinds": return keysC | |
| 130 | − | case "display": return displayC | |
| 131 | − | case "power": return powerC | |
| 132 | − | case "about": return aboutC | |
| 133 | − | default: return netC | |
| 307 | + | ||
| 308 | + | // the found row's flash, behind the page (rows have no background of their own) | |
| 309 | + | Item { | |
| 310 | + | id: flashBox | |
| 311 | + | anchors.fill: parent | |
| 312 | + | clip: true | |
| 313 | + | Rectangle { | |
| 314 | + | id: flash | |
| 315 | + | // re-mapped while the page scrolls or relayouts | |
| 316 | + | readonly property rect r: { | |
| 317 | + | var t = win.hlTarget | |
| 318 | + | if (!t) return Qt.rect(0, 0, 0, 0) | |
| 319 | + | void ((win.hlFlick ? win.hlFlick.contentY : 0) + t.y + t.width + t.height + flashBox.width) | |
| 320 | + | return t.mapToItem(flashBox, 0, 0, t.width, t.height) | |
| 321 | + | } | |
| 322 | + | readonly property real pad: 4 | |
| 323 | + | visible: win.hlTarget !== null && !win.searching | |
| 324 | + | opacity: 0 | |
| 325 | + | width: parent.width | |
| 326 | + | y: r.y - pad | |
| 327 | + | height: r.height + 2 * pad | |
| 328 | + | radius: height / 2 | |
| 329 | + | color: Theme.fade(Theme.fill, 0.2) | |
| 330 | + | } | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | Loader { | |
| 334 | + | id: loader | |
| 335 | + | anchors.fill: parent | |
| 336 | + | visible: !win.searching // kept loaded: Esc returns to the same spot | |
| 337 | + | active: win.opened | |
| 338 | + | sourceComponent: { | |
| 339 | + | switch (win.section) { | |
| 340 | + | case "bluetooth": return btC | |
| 341 | + | case "sound": return soundC | |
| 342 | + | case "appearance": return appearanceC | |
| 343 | + | case "shell": return shellC | |
| 344 | + | case "windows": return windowsC | |
| 345 | + | case "input": return inputC | |
| 346 | + | case "keybinds": return keysC | |
| 347 | + | case "display": return displayC | |
| 348 | + | case "power": return powerC | |
| 349 | + | case "about": return aboutC | |
| 350 | + | default: return netC | |
| 351 | + | } | |
| 352 | + | } | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | // ---- search results ---- | |
| 356 | + | Lb { | |
| 357 | + | x: 18 | |
| 358 | + | visible: win.searching && win.query.trim() === "" | |
| 359 | + | text: "type a setting, a word or a section" | |
| 360 | + | } | |
| 361 | + | ListView { | |
| 362 | + | id: list | |
| 363 | + | anchors.fill: parent | |
| 364 | + | visible: win.searching | |
| 365 | + | clip: true | |
| 366 | + | spacing: 2 | |
| 367 | + | boundsBehavior: Flickable.StopAtBounds | |
| 368 | + | model: win.results | |
| 369 | + | header: Item { | |
| 370 | + | width: list.width | |
| 371 | + | height: win.results.length === 1 ? 44 : 0 | |
| 372 | + | Lb { | |
| 373 | + | x: 18 | |
| 374 | + | anchors.verticalCenter: parent.verticalCenter | |
| 375 | + | visible: parent.height > 0 | |
| 376 | + | text: "no setting matches" | |
| 377 | + | } | |
| 378 | + | } | |
| 379 | + | delegate: Rectangle { | |
| 380 | + | id: r | |
| 381 | + | required property var modelData | |
| 382 | + | required property int index | |
| 383 | + | readonly property bool on: win.pick === index | |
| 384 | + | width: ListView.view.width | |
| 385 | + | height: Math.max(56, txt.implicitHeight + 20) | |
| 386 | + | radius: 28 | |
| 387 | + | color: on || rm.containsMouse ? Theme.s2 : "transparent" | |
| 388 | + | Rectangle { | |
| 389 | + | x: 20 | |
| 390 | + | anchors.verticalCenter: parent.verticalCenter | |
| 391 | + | visible: r.on | |
| 392 | + | width: 10 | |
| 393 | + | height: 10 | |
| 394 | + | radius: 5 | |
| 395 | + | color: Theme.live | |
| 396 | + | } | |
| 397 | + | ColumnLayout { | |
| 398 | + | id: txt | |
| 399 | + | anchors.left: parent.left | |
| 400 | + | anchors.leftMargin: 44 | |
| 401 | + | anchors.right: parent.right | |
| 402 | + | anchors.rightMargin: 18 | |
| 403 | + | anchors.verticalCenter: parent.verticalCenter | |
| 404 | + | spacing: 3 | |
| 405 | + | Text { | |
| 406 | + | Layout.fillWidth: true | |
| 407 | + | text: r.modelData.label | |
| 408 | + | color: Theme.fg | |
| 409 | + | elide: Text.ElideRight | |
| 410 | + | font.family: Theme.ui | |
| 411 | + | font.pixelSize: 15 | |
| 412 | + | } | |
| 413 | + | RowLayout { | |
| 414 | + | Layout.fillWidth: true | |
| 415 | + | spacing: 12 | |
| 416 | + | Lb { text: win.crumb(r.modelData); px: 10 } | |
| 417 | + | Text { | |
| 418 | + | Layout.fillWidth: true | |
| 419 | + | visible: r.modelData.hint !== "" | |
| 420 | + | text: r.modelData.hint | |
| 421 | + | color: Theme.fgDim | |
| 422 | + | elide: Text.ElideRight | |
| 423 | + | font.family: Theme.ui | |
| 424 | + | font.pixelSize: 13 | |
| 425 | + | } | |
| 426 | + | } | |
| 427 | + | } | |
| 428 | + | MouseArea { | |
| 429 | + | id: rm | |
| 430 | + | anchors.fill: parent | |
| 431 | + | hoverEnabled: true | |
| 432 | + | cursorShape: Qt.PointingHandCursor | |
| 433 | + | onClicked: win.go(r.modelData) | |
| 434 | + | } | |
| 134 | 435 | } | |
| 135 | 436 | } | |
| 136 | 437 | } | |
| @@ -142,7 +443,7 @@ FloatingWindow { | |||
| 142 | 443 | anchors.bottomMargin: 50 | |
| 143 | 444 | spacing: 34 | |
| 144 | 445 | Repeater { | |
| 145 | − | model: ["tab · ↑↓ section", "esc close"] | |
| 446 | + | model: win.searching ? ["↑↓ result", "↵ open", "esc back"] : ["ctrl+f search", "tab · ↑↓ section", "esc close"] | |
| 146 | 447 | Lb { required property string modelData; text: modelData } | |
| 147 | 448 | } | |
| 148 | 449 | } | |
| @@ -8,6 +8,7 @@ Rectangle { | |||
| 8 | 8 | id: s | |
| 9 | 9 | property string label: "" | |
| 10 | 10 | property string hint: "" | |
| 11 | + | property string keywords: "" // synonyms for the settings search (SetIndex reads them from the source) | |
| 11 | 12 | property bool checked: false | |
| 12 | 13 | signal toggled(bool on) | |
| 13 | 14 | ||