forked from Shinonome/dots-hyprland
overview: fancier search bar
This commit is contained in:
@@ -358,7 +358,7 @@ Singleton {
|
|||||||
property real notificationPopupWidth: 410
|
property real notificationPopupWidth: 410
|
||||||
property real osdWidth: 180
|
property real osdWidth: 180
|
||||||
property real searchWidthCollapsed: 260
|
property real searchWidthCollapsed: 260
|
||||||
property real searchWidth: 450
|
property real searchWidth: 400
|
||||||
property real sidebarWidth: 460
|
property real sidebarWidth: 460
|
||||||
property real sidebarWidthExtended: 750
|
property real sidebarWidthExtended: 750
|
||||||
property real baseVerticalBarWidth: 46
|
property real baseVerticalBarWidth: 46
|
||||||
|
|||||||
+1
-3
@@ -12,14 +12,12 @@ MaterialShape {
|
|||||||
|
|
||||||
color: Appearance.colors.colSecondaryContainer
|
color: Appearance.colors.colSecondaryContainer
|
||||||
colSymbol: Appearance.colors.colOnSecondaryContainer
|
colSymbol: Appearance.colors.colOnSecondaryContainer
|
||||||
|
|
||||||
shape: MaterialShape.Shape.Clover4Leaf
|
shape: MaterialShape.Shape.Clover4Leaf
|
||||||
|
|
||||||
implicitSize: Math.max(symbol.implicitWidth, symbol.implicitHeight) + padding * 2
|
implicitSize: Math.max(symbol.implicitWidth, symbol.implicitHeight) + padding * 2
|
||||||
|
|
||||||
MaterialSymbol {
|
MaterialSymbol {
|
||||||
id: symbol
|
id: symbol
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
color: root.colSymbol
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import qs
|
|||||||
import qs.services
|
import qs.services
|
||||||
import qs.modules.common
|
import qs.modules.common
|
||||||
import qs.modules.common.widgets
|
import qs.modules.common.widgets
|
||||||
|
import Qt.labs.synchronizer
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
@@ -33,15 +34,12 @@ Scope {
|
|||||||
mask: Region {
|
mask: Region {
|
||||||
item: GlobalStates.overviewOpen ? columnLayout : null
|
item: GlobalStates.overviewOpen ? columnLayout : null
|
||||||
}
|
}
|
||||||
// HyprlandWindow.visibleMask: Region { // Buggy with scaled monitors
|
|
||||||
// item: GlobalStates.overviewOpen ? columnLayout : null
|
|
||||||
// }
|
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: true
|
top: true
|
||||||
bottom: true
|
bottom: true
|
||||||
left: !(Config?.options.overview.enable ?? true)
|
left: true
|
||||||
right: !(Config?.options.overview.enable ?? true)
|
right: true
|
||||||
}
|
}
|
||||||
|
|
||||||
HyprlandFocusGrab {
|
HyprlandFocusGrab {
|
||||||
@@ -89,13 +87,14 @@ Scope {
|
|||||||
searchWidget.focusFirstItem();
|
searchWidget.focusFirstItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
Column {
|
||||||
id: columnLayout
|
id: columnLayout
|
||||||
visible: GlobalStates.overviewOpen
|
visible: GlobalStates.overviewOpen
|
||||||
anchors {
|
anchors {
|
||||||
horizontalCenter: parent.horizontalCenter
|
horizontalCenter: parent.horizontalCenter
|
||||||
top: parent.top
|
top: parent.top
|
||||||
}
|
}
|
||||||
|
spacing: -8
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
@@ -111,14 +110,15 @@ Scope {
|
|||||||
|
|
||||||
SearchWidget {
|
SearchWidget {
|
||||||
id: searchWidget
|
id: searchWidget
|
||||||
Layout.alignment: Qt.AlignHCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
onSearchingTextChanged: text => {
|
Synchronizer on searchingText {
|
||||||
root.searchingText = searchingText;
|
property alias source: root.searchingText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: overviewLoader
|
id: overviewLoader
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
|
active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
|
||||||
sourceComponent: OverviewWidget {
|
sourceComponent: OverviewWidget {
|
||||||
panelWindow: root
|
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
|
||||||
import qs.modules.common.widgets
|
import qs.modules.common.widgets
|
||||||
import qs.modules.common.functions
|
import qs.modules.common.functions
|
||||||
|
import Qt.labs.synchronizer
|
||||||
import Qt5Compat.GraphicalEffects
|
import Qt5Compat.GraphicalEffects
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
@@ -15,7 +16,6 @@ Item { // Wrapper
|
|||||||
readonly property string xdgConfigHome: Directories.config
|
readonly property string xdgConfigHome: Directories.config
|
||||||
property string searchingText: ""
|
property string searchingText: ""
|
||||||
property bool showResults: searchingText != ""
|
property bool showResults: searchingText != ""
|
||||||
property real searchBarHeight: searchBar.height + Appearance.sizes.elevationMargin * 2
|
|
||||||
implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2
|
implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2
|
||||||
implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2
|
implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2
|
||||||
|
|
||||||
@@ -93,18 +93,22 @@ Item { // Wrapper
|
|||||||
appResults.currentIndex = 0;
|
appResults.currentIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function focusSearchInput() {
|
||||||
|
searchBar.focus();
|
||||||
|
}
|
||||||
|
|
||||||
function disableExpandAnimation() {
|
function disableExpandAnimation() {
|
||||||
searchWidthBehavior.enabled = false;
|
searchBar.animateWidth = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelSearch() {
|
function cancelSearch() {
|
||||||
searchInput.selectAll();
|
searchBar.searchInput.selectAll();
|
||||||
root.searchingText = "";
|
root.searchingText = "";
|
||||||
searchWidthBehavior.enabled = true;
|
searchBar.animateWidth = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSearchingText(text) {
|
function setSearchingText(text) {
|
||||||
searchInput.text = text;
|
searchBar.searchInput.text = text;
|
||||||
root.searchingText = text;
|
root.searchingText = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,29 +153,29 @@ Item { // Wrapper
|
|||||||
|
|
||||||
// Handle Backspace: focus and delete character if not focused
|
// Handle Backspace: focus and delete character if not focused
|
||||||
if (event.key === Qt.Key_Backspace) {
|
if (event.key === Qt.Key_Backspace) {
|
||||||
if (!searchInput.activeFocus) {
|
if (!searchBar.searchInput.activeFocus) {
|
||||||
searchInput.forceActiveFocus();
|
root.focusSearchInput();
|
||||||
if (event.modifiers & Qt.ControlModifier) {
|
if (event.modifiers & Qt.ControlModifier) {
|
||||||
// Delete word before cursor
|
// Delete word before cursor
|
||||||
let text = searchInput.text;
|
let text = searchBar.searchInput.text;
|
||||||
let pos = searchInput.cursorPosition;
|
let pos = searchBar.searchInput.cursorPosition;
|
||||||
if (pos > 0) {
|
if (pos > 0) {
|
||||||
// Find the start of the previous word
|
// Find the start of the previous word
|
||||||
let left = text.slice(0, pos);
|
let left = text.slice(0, pos);
|
||||||
let match = left.match(/(\s*\S+)\s*$/);
|
let match = left.match(/(\s*\S+)\s*$/);
|
||||||
let deleteLen = match ? match[0].length : 1;
|
let deleteLen = match ? match[0].length : 1;
|
||||||
searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos);
|
searchBar.searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos);
|
||||||
searchInput.cursorPosition = pos - deleteLen;
|
searchBar.searchInput.cursorPosition = pos - deleteLen;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Delete character before cursor if any
|
// Delete character before cursor if any
|
||||||
if (searchInput.cursorPosition > 0) {
|
if (searchBar.searchInput.cursorPosition > 0) {
|
||||||
searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition - 1) + searchInput.text.slice(searchInput.cursorPosition);
|
searchBar.searchInput.text = searchBar.searchInput.text.slice(0, searchBar.searchInput.cursorPosition - 1) + searchBar.searchInput.text.slice(searchBar.searchInput.cursorPosition);
|
||||||
searchInput.cursorPosition -= 1;
|
searchBar.searchInput.cursorPosition -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Always move cursor to end after programmatic edit
|
// Always move cursor to end after programmatic edit
|
||||||
searchInput.cursorPosition = searchInput.text.length;
|
searchBar.searchInput.cursorPosition = searchBar.searchInput.text.length;
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
// If already focused, let TextField handle it
|
// If already focused, let TextField handle it
|
||||||
@@ -181,11 +185,11 @@ Item { // Wrapper
|
|||||||
// Only handle visible printable characters (ignore control chars, arrows, etc.)
|
// 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 (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) {
|
if (!searchBar.searchInput.activeFocus) {
|
||||||
searchInput.forceActiveFocus();
|
root.focusSearchInput();
|
||||||
// Insert the character at the cursor position
|
// Insert the character at the cursor position
|
||||||
searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition) + event.text + searchInput.text.slice(searchInput.cursorPosition);
|
searchBar.searchInput.text = searchBar.searchInput.text.slice(0, searchBar.searchInput.cursorPosition) + event.text + searchBar.searchInput.text.slice(searchBar.searchInput.cursorPosition);
|
||||||
searchInput.cursorPosition += 1;
|
searchBar.searchInput.cursorPosition += 1;
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
root.focusFirstItem();
|
root.focusFirstItem();
|
||||||
}
|
}
|
||||||
@@ -200,10 +204,8 @@ Item { // Wrapper
|
|||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
implicitWidth: columnLayout.implicitWidth
|
implicitWidth: columnLayout.implicitWidth
|
||||||
implicitHeight: columnLayout.implicitHeight
|
implicitHeight: columnLayout.implicitHeight
|
||||||
radius: Appearance.rounding.large
|
radius: searchBar.height / 2 + searchBar.verticalPadding
|
||||||
color: Appearance.colors.colLayer0
|
color: Appearance.colors.colSurfaceContainer
|
||||||
border.width: 1
|
|
||||||
border.color: Appearance.colors.colLayer0Border
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: columnLayout
|
id: columnLayout
|
||||||
@@ -220,64 +222,16 @@ Item { // Wrapper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
SearchBar {
|
||||||
id: searchBar
|
id: searchBar
|
||||||
spacing: 5
|
property real verticalPadding: 4
|
||||||
MaterialSymbol {
|
Layout.fillWidth: true
|
||||||
id: searchIcon
|
Layout.leftMargin: 10
|
||||||
Layout.leftMargin: 15
|
Layout.rightMargin: 4
|
||||||
iconSize: Appearance.font.pixelSize.huge
|
Layout.topMargin: verticalPadding
|
||||||
color: Appearance.m3colors.m3onSurface
|
Layout.bottomMargin: verticalPadding
|
||||||
text: root.searchingText.startsWith(Config.options.search.prefix.clipboard) ? 'content_paste_search' : 'search'
|
Synchronizer on searchingText {
|
||||||
}
|
property alias source: root.searchingText
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user