overview: fancier search bar

This commit is contained in:
end-4
2025-10-30 22:20:06 +01:00
parent db79ecd636
commit dbd12d3e92
5 changed files with 156 additions and 93 deletions
@@ -358,7 +358,7 @@ Singleton {
property real notificationPopupWidth: 410
property real osdWidth: 180
property real searchWidthCollapsed: 260
property real searchWidth: 450
property real searchWidth: 400
property real sidebarWidth: 460
property real sidebarWidthExtended: 750
property real baseVerticalBarWidth: 46
@@ -12,14 +12,12 @@ MaterialShape {
color: Appearance.colors.colSecondaryContainer
colSymbol: Appearance.colors.colOnSecondaryContainer
shape: MaterialShape.Shape.Clover4Leaf
implicitSize: Math.max(symbol.implicitWidth, symbol.implicitHeight) + padding * 2
MaterialSymbol {
id: symbol
anchors.centerIn: parent
color: root.colSymbol
}
}
@@ -2,6 +2,7 @@ import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import Qt.labs.synchronizer
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -33,15 +34,12 @@ Scope {
mask: Region {
item: GlobalStates.overviewOpen ? columnLayout : null
}
// HyprlandWindow.visibleMask: Region { // Buggy with scaled monitors
// item: GlobalStates.overviewOpen ? columnLayout : null
// }
anchors {
top: true
bottom: true
left: !(Config?.options.overview.enable ?? true)
right: !(Config?.options.overview.enable ?? true)
left: true
right: true
}
HyprlandFocusGrab {
@@ -89,13 +87,14 @@ Scope {
searchWidget.focusFirstItem();
}
ColumnLayout {
Column {
id: columnLayout
visible: GlobalStates.overviewOpen
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
}
spacing: -8
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
@@ -111,14 +110,15 @@ Scope {
SearchWidget {
id: searchWidget
Layout.alignment: Qt.AlignHCenter
onSearchingTextChanged: text => {
root.searchingText = searchingText;
anchors.horizontalCenter: parent.horizontalCenter
Synchronizer on searchingText {
property alias source: root.searchingText
}
}
Loader {
id: overviewLoader
anchors.horizontalCenter: parent.horizontalCenter
active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
sourceComponent: OverviewWidget {
panelWindow: root
@@ -0,0 +1,111 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
RowLayout {
id: root
spacing: 6
property bool animateWidth: false
property alias searchInput: searchInput
property string searchingText
function focus() {
searchInput.forceActiveFocus();
}
enum SearchPrefixType { Action, App, Clipboard, Emojis, Math, ShellCommand, WebSearch, DefaultSearch }
property var searchPrefixType: {
if (root.searchingText.startsWith(Config.options.search.prefix.action)) return SearchBar.SearchPrefixType.Action;
if (root.searchingText.startsWith(Config.options.search.prefix.app)) return SearchBar.SearchPrefixType.App;
if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) return SearchBar.SearchPrefixType.Clipboard;
if (root.searchingText.startsWith(Config.options.search.prefix.emojis)) return SearchBar.SearchPrefixType.Emojis;
if (root.searchingText.startsWith(Config.options.search.prefix.math)) return SearchBar.SearchPrefixType.Math;
if (root.searchingText.startsWith(Config.options.search.prefix.shellCommand)) return SearchBar.SearchPrefixType.ShellCommand;
if (root.searchingText.startsWith(Config.options.search.prefix.webSearch)) return SearchBar.SearchPrefixType.WebSearch;
return SearchBar.SearchPrefixType.DefaultSearch;
}
MaterialShapeWrappedMaterialSymbol {
id: searchIcon
Layout.alignment: Qt.AlignVCenter
iconSize: Appearance.font.pixelSize.huge
shape: switch(root.searchPrefixType) {
case SearchBar.SearchPrefixType.Action: return MaterialShape.Shape.Pill;
case SearchBar.SearchPrefixType.App: return MaterialShape.Shape.Clover4Leaf;
case SearchBar.SearchPrefixType.Clipboard: return MaterialShape.Shape.Gem;
case SearchBar.SearchPrefixType.Emojis: return MaterialShape.Shape.Sunny;
case SearchBar.SearchPrefixType.Math: return MaterialShape.Shape.PuffyDiamond;
case SearchBar.SearchPrefixType.ShellCommand: return MaterialShape.Shape.PixelCircle;
case SearchBar.SearchPrefixType.WebSearch: return MaterialShape.Shape.SoftBurst;
default: return MaterialShape.Shape.Cookie7Sided;
}
text: switch (root.searchPrefixType) {
case SearchBar.SearchPrefixType.Action: return "settings_suggest";
case SearchBar.SearchPrefixType.App: return "apps";
case SearchBar.SearchPrefixType.Clipboard: return "content_paste_search";
case SearchBar.SearchPrefixType.Emojis: return "add_reaction";
case SearchBar.SearchPrefixType.Math: return "calculate";
case SearchBar.SearchPrefixType.ShellCommand: return "terminal";
case SearchBar.SearchPrefixType.WebSearch: return "travel_explore";
case SearchBar.SearchPrefixType.DefaultSearch: return "search";
default: return "search";
}
}
ToolbarTextField { // Search box
id: searchInput
Layout.alignment: Qt.AlignVCenter
focus: GlobalStates.overviewOpen
padding: 15
font.pixelSize: Appearance.font.pixelSize.small
placeholderText: Translation.tr("Search, calculate or run")
implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
Behavior on implicitWidth {
id: searchWidthBehavior
enabled: root.animateWidth
NumberAnimation {
duration: 300
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
onTextChanged: root.searchingText = text
onAccepted: {
if (appResults.count > 0) {
// Get the first visible delegate and trigger its click
let firstItem = appResults.itemAtIndex(0);
if (firstItem && firstItem.clicked) {
firstItem.clicked();
}
}
}
// background: null
cursorDelegate: Rectangle {
width: 1
color: searchInput.activeFocus ? Appearance.colors.colPrimary : "transparent"
radius: 1
}
}
IconToolbarButton {
onClicked: {
GlobalStates.overviewOpen = false;
Hyprland.dispatch("global quickshell:regionSearch")
}
text: "image_search"
}
}
@@ -3,6 +3,7 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Qt.labs.synchronizer
import Qt5Compat.GraphicalEffects
import QtQuick
import QtQuick.Controls
@@ -15,7 +16,6 @@ Item { // Wrapper
readonly property string xdgConfigHome: Directories.config
property string searchingText: ""
property bool showResults: searchingText != ""
property real searchBarHeight: searchBar.height + Appearance.sizes.elevationMargin * 2
implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2
implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2
@@ -93,18 +93,22 @@ Item { // Wrapper
appResults.currentIndex = 0;
}
function focusSearchInput() {
searchBar.focus();
}
function disableExpandAnimation() {
searchWidthBehavior.enabled = false;
searchBar.animateWidth = false;
}
function cancelSearch() {
searchInput.selectAll();
searchBar.searchInput.selectAll();
root.searchingText = "";
searchWidthBehavior.enabled = true;
searchBar.animateWidth = true;
}
function setSearchingText(text) {
searchInput.text = text;
searchBar.searchInput.text = text;
root.searchingText = text;
}
@@ -149,29 +153,29 @@ Item { // Wrapper
// Handle Backspace: focus and delete character if not focused
if (event.key === Qt.Key_Backspace) {
if (!searchInput.activeFocus) {
searchInput.forceActiveFocus();
if (!searchBar.searchInput.activeFocus) {
root.focusSearchInput();
if (event.modifiers & Qt.ControlModifier) {
// Delete word before cursor
let text = searchInput.text;
let pos = searchInput.cursorPosition;
let text = searchBar.searchInput.text;
let pos = searchBar.searchInput.cursorPosition;
if (pos > 0) {
// Find the start of the previous word
let left = text.slice(0, pos);
let match = left.match(/(\s*\S+)\s*$/);
let deleteLen = match ? match[0].length : 1;
searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos);
searchInput.cursorPosition = pos - deleteLen;
searchBar.searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos);
searchBar.searchInput.cursorPosition = pos - deleteLen;
}
} else {
// Delete character before cursor if any
if (searchInput.cursorPosition > 0) {
searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition - 1) + searchInput.text.slice(searchInput.cursorPosition);
searchInput.cursorPosition -= 1;
if (searchBar.searchInput.cursorPosition > 0) {
searchBar.searchInput.text = searchBar.searchInput.text.slice(0, searchBar.searchInput.cursorPosition - 1) + searchBar.searchInput.text.slice(searchBar.searchInput.cursorPosition);
searchBar.searchInput.cursorPosition -= 1;
}
}
// Always move cursor to end after programmatic edit
searchInput.cursorPosition = searchInput.text.length;
searchBar.searchInput.cursorPosition = searchBar.searchInput.text.length;
event.accepted = true;
}
// If already focused, let TextField handle it
@@ -181,11 +185,11 @@ Item { // Wrapper
// Only handle visible printable characters (ignore control chars, arrows, etc.)
if (event.text && event.text.length === 1 && event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return && event.key !== Qt.Key_Delete && event.text.charCodeAt(0) >= 0x20) // ignore control chars like Backspace, Tab, etc.
{
if (!searchInput.activeFocus) {
searchInput.forceActiveFocus();
if (!searchBar.searchInput.activeFocus) {
root.focusSearchInput();
// Insert the character at the cursor position
searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition) + event.text + searchInput.text.slice(searchInput.cursorPosition);
searchInput.cursorPosition += 1;
searchBar.searchInput.text = searchBar.searchInput.text.slice(0, searchBar.searchInput.cursorPosition) + event.text + searchBar.searchInput.text.slice(searchBar.searchInput.cursorPosition);
searchBar.searchInput.cursorPosition += 1;
event.accepted = true;
root.focusFirstItem();
}
@@ -200,10 +204,8 @@ Item { // Wrapper
anchors.centerIn: parent
implicitWidth: columnLayout.implicitWidth
implicitHeight: columnLayout.implicitHeight
radius: Appearance.rounding.large
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.colors.colLayer0Border
radius: searchBar.height / 2 + searchBar.verticalPadding
color: Appearance.colors.colSurfaceContainer
ColumnLayout {
id: columnLayout
@@ -220,64 +222,16 @@ Item { // Wrapper
}
}
RowLayout {
SearchBar {
id: searchBar
spacing: 5
MaterialSymbol {
id: searchIcon
Layout.leftMargin: 15
iconSize: Appearance.font.pixelSize.huge
color: Appearance.m3colors.m3onSurface
text: root.searchingText.startsWith(Config.options.search.prefix.clipboard) ? 'content_paste_search' : 'search'
}
TextField { // Search box
id: searchInput
focus: GlobalStates.overviewOpen
Layout.rightMargin: 15
padding: 15
renderType: Text.NativeRendering
font {
family: Appearance?.font.family.main ?? "sans-serif"
pixelSize: Appearance?.font.pixelSize.small ?? 15
hintingPreference: Font.PreferFullHinting
}
color: activeFocus ? Appearance.m3colors.m3onSurface : Appearance.m3colors.m3onSurfaceVariant
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.colors.colSecondaryContainer
placeholderText: Translation.tr("Search, calculate or run")
placeholderTextColor: Appearance.m3colors.m3outline
implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth
Behavior on implicitWidth {
id: searchWidthBehavior
enabled: false
NumberAnimation {
duration: 300
easing.type: Appearance.animation.elementMove.type
easing.bezierCurve: Appearance.animation.elementMove.bezierCurve
}
}
onTextChanged: root.searchingText = text
onAccepted: {
if (appResults.count > 0) {
// Get the first visible delegate and trigger its click
let firstItem = appResults.itemAtIndex(0);
if (firstItem && firstItem.clicked) {
firstItem.clicked();
}
}
}
background: null
cursorDelegate: Rectangle {
width: 1
color: searchInput.activeFocus ? Appearance.colors.colPrimary : "transparent"
radius: 1
}
property real verticalPadding: 4
Layout.fillWidth: true
Layout.leftMargin: 10
Layout.rightMargin: 4
Layout.topMargin: verticalPadding
Layout.bottomMargin: verticalPadding
Synchronizer on searchingText {
property alias source: root.searchingText
}
}