Files
alt-illogical-impulse/configs/quickshell/modules/overview/SearchItem.qml
T

268 lines
10 KiB
QML

// pragma NativeMethodBehavior: AcceptThisObject
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Hyprland
RippleButton {
id: root
property var entry
property string query
property bool entryShown: entry?.shown ?? true
property string itemType: entry?.type ?? Translation.tr("App")
property string itemName: entry?.name
property string itemIcon: entry?.icon ?? ""
property var itemExecute: entry?.execute
property string fontType: entry?.fontType ?? "main"
property string itemClickActionName: entry?.clickActionName
property string bigText: entry?.bigText ?? ""
property string materialSymbol: entry?.materialSymbol ?? ""
property string cliphistRawString: entry?.cliphistRawString ?? ""
visible: root.entryShown
property int horizontalMargin: 10
property int buttonHorizontalPadding: 10
property int buttonVerticalPadding: 5
property bool keyboardDown: false
implicitHeight: rowLayout.implicitHeight + root.buttonVerticalPadding * 2
implicitWidth: rowLayout.implicitWidth + root.buttonHorizontalPadding * 2
buttonRadius: Appearance.rounding.normal
colBackground: (root.down || root.keyboardDown) ? Appearance.colors.colSecondaryContainerActive :
((root.hovered || root.focus) ? Appearance.colors.colSecondaryContainerHover :
ColorUtils.transparentize(Appearance.colors.colSecondaryContainer, 1))
colBackgroundHover: Appearance.colors.colSecondaryContainerHover
colRipple: Appearance.colors.colSecondaryContainerActive
property string highlightPrefix: `<u><font color="${Appearance.colors.colPrimary}">`
property string highlightSuffix: `</font></u>`
function highlightContent(content, query) {
if (!query || query.length === 0 || content == query || fontType === "monospace")
return StringUtils.escapeHtml(content);
let contentLower = content.toLowerCase();
let queryLower = query.toLowerCase();
let result = "";
let lastIndex = 0;
let qIndex = 0;
for (let i = 0; i < content.length && qIndex < query.length; i++) {
if (contentLower[i] === queryLower[qIndex]) {
// Add non-highlighted part (escaped)
if (i > lastIndex)
result += StringUtils.escapeHtml(content.slice(lastIndex, i));
// Add highlighted character (escaped)
result += root.highlightPrefix + StringUtils.escapeHtml(content[i]) + root.highlightSuffix;
lastIndex = i + 1;
qIndex++;
}
}
// Add the rest of the string (escaped)
if (lastIndex < content.length)
result += StringUtils.escapeHtml(content.slice(lastIndex));
return result;
}
property string displayContent: highlightContent(root.itemName, root.query)
property list<string> urls: {
if (!root.itemName) return [];
// Regular expression to match URLs
const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi;
const matches = root.itemName?.match(urlRegex)
?.filter(url => !url.includes("…")) // Elided = invalid
return matches ? matches : [];
}
PointingHandInteraction {}
background {
anchors.fill: root
anchors.leftMargin: root.horizontalMargin
anchors.rightMargin: root.horizontalMargin
}
onClicked: {
root.itemExecute()
GlobalStates.overviewOpen = false
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.keyboardDown = true
root.clicked()
event.accepted = true;
}
}
Keys.onReleased: (event) => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.keyboardDown = false
event.accepted = true;
}
}
RowLayout {
id: rowLayout
spacing: iconLoader.sourceComponent === null ? 0 : 10
anchors.fill: parent
anchors.leftMargin: root.horizontalMargin + root.buttonHorizontalPadding
anchors.rightMargin: root.horizontalMargin + root.buttonHorizontalPadding
// Icon
Loader {
id: iconLoader
active: true
sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent :
root.bigText ? bigTextComponent :
root.itemIcon !== "" ? iconImageComponent :
null
}
Component {
id: iconImageComponent
IconImage {
source: Quickshell.iconPath(root.itemIcon, "image-missing")
width: 35
height: 35
}
}
Component {
id: materialSymbolComponent
MaterialSymbol {
text: root.materialSymbol
iconSize: 30
color: Appearance.m3colors.m3onSurface
}
}
Component {
id: bigTextComponent
StyledText {
text: root.bigText
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.m3colors.m3onSurface
}
}
// Main text
ColumnLayout {
id: contentColumn
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 0
StyledText {
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
visible: root.itemType && root.itemType != Translation.tr("App")
text: root.itemType
}
RowLayout {
Loader { // Checkmark for copied clipboard entry
visible: itemName == Quickshell.clipboardText && root.cliphistRawString
active: itemName == Quickshell.clipboardText && root.cliphistRawString
sourceComponent: Rectangle {
implicitWidth: activeText.implicitHeight
implicitHeight: activeText.implicitHeight
radius: Appearance.rounding.full
color: Appearance.colors.colPrimary
MaterialSymbol {
id: activeText
anchors.centerIn: parent
text: "check"
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.m3colors.m3onPrimary
}
}
}
Repeater { // Favicons for links
model: root.query == root.itemName ? [] : root.urls
Favicon {
required property var modelData
size: parent.height
url: modelData
}
}
StyledText { // Item name/content
Layout.fillWidth: true
id: nameText
textFormat: Text.StyledText // RichText also works, but StyledText ensures elide work
font.pixelSize: Appearance.font.pixelSize.small
font.family: Appearance.font.family[root.fontType]
color: Appearance.m3colors.m3onSurface
horizontalAlignment: Text.AlignLeft
elide: Text.ElideRight
text: `${root.displayContent}`
}
}
Loader { // Clipboard image preview
active: root.cliphistRawString && /^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(root.cliphistRawString)
sourceComponent: CliphistImage {
Layout.fillWidth: true
entry: root.cliphistRawString
maxWidth: contentColumn.width
maxHeight: 140
}
}
}
// Action text
StyledText {
Layout.fillWidth: false
visible: (root.hovered || root.focus)
id: clickAction
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colSubtext
horizontalAlignment: Text.AlignRight
text: root.itemClickActionName
}
RowLayout {
spacing: 4
Repeater {
model: (root.entry.actions ?? []).slice(0, 4)
delegate: RippleButton {
id: actionButton
required property var modelData
implicitHeight: 34
implicitWidth: 34
contentItem: Item {
id: actionContentItem
anchors.centerIn: parent
Loader {
anchors.centerIn: parent
active: !(actionButton.modelData.icon && actionButton.modelData.icon !== "")
sourceComponent: MaterialSymbol {
text: "video_settings"
font.pixelSize: Appearance.font.pixelSize.hugeass
color: Appearance.m3colors.m3onSurface
}
}
Loader {
anchors.centerIn: parent
active: actionButton.modelData.icon && actionButton.modelData.icon !== ""
sourceComponent: IconImage {
source: Quickshell.iconPath(actionButton.modelData.icon)
implicitSize: 20
}
}
}
onClicked: modelData.execute()
StyledToolTip {
content: modelData.name
}
}
}
}
}
}