desktop icons
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
selectedTextColor: DynamicColors.palette.m3onSecondaryContainer
|
||||||
|
selectionColor: DynamicColors.tPalette.colSecondaryContainer
|
||||||
|
|
||||||
|
font {
|
||||||
|
family: Appearance?.font.family.sans ?? "sans-serif"
|
||||||
|
hintingPreference: Font.PreferFullHinting
|
||||||
|
pixelSize: Appearance?.font.size.normal ?? 15
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -168,6 +168,7 @@ Singleton {
|
|||||||
return {
|
return {
|
||||||
logo: general.logo,
|
logo: general.logo,
|
||||||
wallpaperPath: general.wallpaperPath,
|
wallpaperPath: general.wallpaperPath,
|
||||||
|
desktopIcons: general.desktopIcons,
|
||||||
color: {
|
color: {
|
||||||
wallust: general.color.wallust,
|
wallust: general.color.wallust,
|
||||||
mode: general.color.mode,
|
mode: general.color.mode,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ JsonObject {
|
|||||||
}
|
}
|
||||||
property Color color: Color {
|
property Color color: Color {
|
||||||
}
|
}
|
||||||
|
property bool desktopIcons: false
|
||||||
property Idle idle: Idle {
|
property Idle idle: Idle {
|
||||||
}
|
}
|
||||||
property string logo: ""
|
property string logo: ""
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import "../scripts/levendist.js" as Levendist
|
||||||
|
import "../scripts/fuzzysort.js" as Fuzzy
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values).sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
readonly property var preppedNames: list.map(a => ({
|
||||||
|
name: Fuzzy.prepare(`${a.name} `),
|
||||||
|
entry: a
|
||||||
|
}))
|
||||||
|
property var regexSubstitutions: [
|
||||||
|
{
|
||||||
|
"regex": /^steam_app_(\d+)$/,
|
||||||
|
"replace": "steam_icon_$1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"regex": /Minecraft.*/,
|
||||||
|
"replace": "minecraft"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"regex": /.*polkit.*/,
|
||||||
|
"replace": "system-lock-screen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"regex": /gcr.prompter/,
|
||||||
|
"replace": "system-lock-screen"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
property real scoreThreshold: 0.2
|
||||||
|
property bool sloppySearch: Config.options?.search.sloppy ?? false
|
||||||
|
property var substitutions: ({
|
||||||
|
"code-url-handler": "visual-studio-code",
|
||||||
|
"Code": "visual-studio-code",
|
||||||
|
"gnome-tweaks": "org.gnome.tweaks",
|
||||||
|
"pavucontrol-qt": "pavucontrol",
|
||||||
|
"wps": "wps-office2019-kprometheus",
|
||||||
|
"wpsoffice": "wps-office2019-kprometheus",
|
||||||
|
"footclient": "foot",
|
||||||
|
"zen": "zen-browser"
|
||||||
|
})
|
||||||
|
|
||||||
|
signal reload
|
||||||
|
|
||||||
|
function computeScore(...args) {
|
||||||
|
return Levendist.computeScore(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeTextMatchScore(...args) {
|
||||||
|
return Levendist.computeTextMatchScore(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fuzzyQuery(search: string): var { // Idk why list<DesktopEntry> doesn't work
|
||||||
|
if (root.sloppySearch) {
|
||||||
|
const results = list.map(obj => ({
|
||||||
|
entry: obj,
|
||||||
|
score: computeScore(obj.name.toLowerCase(), search.toLowerCase())
|
||||||
|
})).filter(item => item.score > root.scoreThreshold).sort((a, b) => b.score - a.score);
|
||||||
|
return results.map(item => item.entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Fuzzy.go(search, preppedNames, {
|
||||||
|
all: true,
|
||||||
|
key: "name"
|
||||||
|
}).map(r => {
|
||||||
|
return r.obj.entry;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessIcon(str) {
|
||||||
|
if (!str || str.length == 0)
|
||||||
|
return "image-missing";
|
||||||
|
|
||||||
|
// Normal substitutions
|
||||||
|
if (substitutions[str])
|
||||||
|
return substitutions[str];
|
||||||
|
|
||||||
|
// Regex substitutions
|
||||||
|
for (let i = 0; i < regexSubstitutions.length; i++) {
|
||||||
|
const substitution = regexSubstitutions[i];
|
||||||
|
const replacedName = str.replace(substitution.regex, substitution.replace);
|
||||||
|
if (replacedName != str)
|
||||||
|
return replacedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it gets detected normally, no need to guess
|
||||||
|
if (iconExists(str))
|
||||||
|
return str;
|
||||||
|
|
||||||
|
let guessStr = str;
|
||||||
|
// Guess: Take only app name of reverse domain name notation
|
||||||
|
guessStr = str.split('.').slice(-1)[0].toLowerCase();
|
||||||
|
if (iconExists(guessStr))
|
||||||
|
return guessStr;
|
||||||
|
// Guess: normalize to kebab case
|
||||||
|
guessStr = str.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
if (iconExists(guessStr))
|
||||||
|
return guessStr;
|
||||||
|
// Guess: First fuzze desktop entry match
|
||||||
|
const searchResults = root.fuzzyQuery(str);
|
||||||
|
if (searchResults.length > 0) {
|
||||||
|
const firstEntry = searchResults[0];
|
||||||
|
guessStr = firstEntry.icon;
|
||||||
|
if (iconExists(guessStr))
|
||||||
|
return guessStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give up
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconExists(iconName) {
|
||||||
|
if (!iconName || iconName.length == 0)
|
||||||
|
return false;
|
||||||
|
return (Quickshell.iconPath(iconName, true).length > 0) && !iconName.includes("image-missing");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,17 +6,18 @@ import Quickshell.Hyprland
|
|||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Paths
|
import qs.Paths
|
||||||
|
import qs.Helpers
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
z: 998
|
z: 998
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
property real menuX: 0
|
property real menuX: 0
|
||||||
property real menuY: 0
|
property real menuY: 0
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onClicked: root.close()
|
onClicked: root.close()
|
||||||
@@ -25,39 +26,46 @@ Item {
|
|||||||
CustomClippingRect {
|
CustomClippingRect {
|
||||||
id: popupBackground
|
id: popupBackground
|
||||||
readonly property real padding: 4
|
readonly property real padding: 4
|
||||||
|
|
||||||
x: root.menuX
|
x: root.menuX
|
||||||
y: root.menuY
|
y: root.menuY
|
||||||
|
|
||||||
color: DynamicColors.tPalette.m3surface
|
color: DynamicColors.tPalette.m3surface
|
||||||
radius: Appearance.rounding.normal
|
radius: Appearance.rounding.normal
|
||||||
|
|
||||||
implicitWidth: menuLayout.implicitWidth + padding * 2
|
implicitWidth: menuLayout.implicitWidth + padding * 2
|
||||||
implicitHeight: menuLayout.implicitHeight + padding * 2
|
implicitHeight: menuLayout.implicitHeight + padding * 2
|
||||||
|
|
||||||
Behavior on opacity { Anim {} }
|
Behavior on opacity { Anim {} }
|
||||||
opacity: root.visible ? 1 : 0
|
opacity: root.visible ? 1 : 0
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: menuLayout
|
id: menuLayout
|
||||||
anchors.fill: parent
|
anchors.centerIn: parent
|
||||||
anchors.margins: popupBackground.padding
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
StateLayer {
|
CustomRect {
|
||||||
Layout.fillWidth: true
|
Layout.preferredWidth: 200
|
||||||
|
radius: popupBackground.radius - popupBackground.padding
|
||||||
contentItem: RowLayout {
|
implicitHeight: openTerminalRow.implicitHeight + Appearance.padding.small * 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: openTerminalRow
|
||||||
spacing: 8
|
spacing: 8
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 12
|
anchors.leftMargin: Appearance.padding.smaller
|
||||||
|
|
||||||
MaterialIcon { text: "terminal"; font.pointSize: 20 }
|
MaterialIcon { text: "terminal"; font.pointSize: 20 }
|
||||||
CustomText { text: "Open terminal"; Layout.fillWidth: true }
|
CustomText { text: "Open terminal"; Layout.fillWidth: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
StateLayer {
|
||||||
Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)])
|
anchors.fill: parent
|
||||||
root.close()
|
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)])
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,81 +77,105 @@ Item {
|
|||||||
Layout.bottomMargin: 4
|
Layout.bottomMargin: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
CustomRect {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
radius: popupBackground.radius - popupBackground.padding
|
||||||
contentItem: RowLayout {
|
implicitHeight: settingsRow.implicitHeight + Appearance.padding.small * 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: settingsRow
|
||||||
spacing: 8
|
spacing: 8
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 12
|
anchors.leftMargin: Appearance.padding.smaller
|
||||||
|
|
||||||
MaterialIcon { text: "settings"; font.pointSize: 20 }
|
MaterialIcon { text: "settings"; font.pointSize: 20 }
|
||||||
CustomText { text: "Sleex settings"; Layout.fillWidth: true }
|
CustomText { text: "ZShell settings"; Layout.fillWidth: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
StateLayer {
|
||||||
Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"])
|
anchors.fill: parent
|
||||||
root.close()
|
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"])
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: 1
|
implicitHeight: 1
|
||||||
color: Appearance.m3colors.m3outlineVariant
|
color: DynamicColors.palette.m3outlineVariant
|
||||||
Layout.topMargin: 4
|
Layout.topMargin: 4
|
||||||
Layout.bottomMargin: 4
|
Layout.bottomMargin: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
CustomRect {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
radius: popupBackground.radius - popupBackground.padding
|
||||||
contentItem: RowLayout {
|
implicitHeight: logoutRow.implicitHeight + Appearance.padding.small * 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: logoutRow
|
||||||
spacing: 8
|
spacing: 8
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 12
|
anchors.leftMargin: Appearance.padding.smaller
|
||||||
|
|
||||||
MaterialIcon { text: "logout"; font.pointSize: 20 }
|
MaterialIcon { text: "logout"; font.pointSize: 20 }
|
||||||
CustomText { text: "Logout"; Layout.fillWidth: true }
|
CustomText { text: "Logout"; Layout.fillWidth: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
Hyprland.dispatch("global quickshell:sessionOpen")
|
|
||||||
root.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomRect {
|
StateLayer {
|
||||||
Layout.fillWidth: true
|
|
||||||
implicitHeight: 1
|
|
||||||
color: Appearance.m3colors.m3outlineVariant
|
|
||||||
Layout.topMargin: 4
|
|
||||||
Layout.bottomMargin: 4
|
|
||||||
}
|
|
||||||
|
|
||||||
StateLayer {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
contentItem: RowLayout {
|
|
||||||
spacing: 8
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 12
|
|
||||||
MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 }
|
|
||||||
CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons
|
Hyprland.dispatch("global quickshell:sessionOpen")
|
||||||
root.close()
|
root.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CustomRect {
|
||||||
|
// Layout.fillWidth: true
|
||||||
|
// implicitHeight: 1
|
||||||
|
// color: DynamicColors.palette.m3outlineVariant
|
||||||
|
// Layout.topMargin: 4
|
||||||
|
// Layout.bottomMargin: 4
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// CustomRect {
|
||||||
|
// Layout.fillWidth: true
|
||||||
|
// radius: popupBackground.radius - popupBackground.padding
|
||||||
|
// implicitHeight: desktopIconsRow.implicitHeight + Appearance.padding.small * 2
|
||||||
|
//
|
||||||
|
// RowLayout {
|
||||||
|
// id: desktopIconsRow
|
||||||
|
// spacing: 8
|
||||||
|
// anchors.fill: parent
|
||||||
|
// anchors.leftMargin: Appearance.padding.smaller
|
||||||
|
//
|
||||||
|
// MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 }
|
||||||
|
// CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// StateLayer {
|
||||||
|
// anchors.fill: parent
|
||||||
|
//
|
||||||
|
// onClicked: {
|
||||||
|
// Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons
|
||||||
|
// root.close()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAt(mouseX, mouseY, parentW, parentH) {
|
function openAt(mouseX, mouseY, parentW, parentH) {
|
||||||
menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth)
|
menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth))
|
||||||
menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight)
|
menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight))
|
||||||
visible = true
|
visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
visible = false
|
visible = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,73 +23,87 @@ Item {
|
|||||||
|
|
||||||
property real menuX: 0
|
property real menuX: 0
|
||||||
property real menuY: 0
|
property real menuY: 0
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: contextMenu.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomClippingRect {
|
CustomClippingRect {
|
||||||
id: popupBackground
|
id: popupBackground
|
||||||
readonly property real padding: 4
|
readonly property real padding: Appearance.padding.small
|
||||||
|
|
||||||
x: contextMenu.menuX
|
x: contextMenu.menuX
|
||||||
y: contextMenu.menuY
|
y: contextMenu.menuY
|
||||||
|
|
||||||
color: DynamicColors.tPalette.m3surface
|
color: DynamicColors.tPalette.m3surface
|
||||||
radius: Appearance.rounding.normal
|
radius: Appearance.rounding.normal
|
||||||
|
|
||||||
implicitWidth: menuLayout.implicitWidth + padding * 2
|
implicitWidth: menuLayout.implicitWidth + padding * 2
|
||||||
implicitHeight: menuLayout.implicitHeight + padding * 2
|
implicitHeight: menuLayout.implicitHeight + padding * 2
|
||||||
|
|
||||||
Behavior on opacity { Anim {} }
|
Behavior on opacity { Anim {} }
|
||||||
opacity: contextMenu.visible ? 1 : 0
|
opacity: contextMenu.visible ? 1 : 0
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: menuLayout
|
id: menuLayout
|
||||||
anchors.fill: parent
|
anchors.centerIn: parent
|
||||||
anchors.margins: popupBackground.padding
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
StateLayer {
|
CustomRect {
|
||||||
Layout.fillWidth: true
|
Layout.preferredWidth: 160
|
||||||
|
radius: popupBackground.radius - popupBackground.padding
|
||||||
contentItem: RowLayout {
|
implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: openRow
|
||||||
spacing: 8
|
spacing: 8
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 12
|
anchors.leftMargin: Appearance.padding.smaller
|
||||||
|
|
||||||
MaterialIcon { text: "open_in_new"; font.pointSize: 20 }
|
MaterialIcon { text: "open_in_new"; font.pointSize: 20 }
|
||||||
CustomText { text: "Open"; Layout.fillWidth: true }
|
CustomText { text: "Open"; Layout.fillWidth: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
StateLayer {
|
||||||
for (let i = 0; i < contextMenu.targetPaths.length; i++) {
|
anchors.fill: parent
|
||||||
let p = contextMenu.targetPaths[i];
|
|
||||||
if (p === contextMenu.targetFilePath) {
|
onClicked: {
|
||||||
if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute()
|
for (let i = 0; i < contextMenu.targetPaths.length; i++) {
|
||||||
else contextMenu.openFileRequested(p, contextMenu.targetIsDir)
|
let p = contextMenu.targetPaths[i];
|
||||||
} else {
|
if (p === contextMenu.targetFilePath) {
|
||||||
Quickshell.execDetached(["xdg-open", p])
|
if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute()
|
||||||
|
else contextMenu.openFileRequested(p, contextMenu.targetIsDir)
|
||||||
|
} else {
|
||||||
|
Quickshell.execDetached(["xdg-open", p])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
contextMenu.close()
|
||||||
}
|
}
|
||||||
contextMenu.close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
CustomRect {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
radius: popupBackground.radius - popupBackground.padding
|
||||||
contentItem: RowLayout {
|
implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: openWithRow
|
||||||
spacing: 8
|
spacing: 8
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 12
|
anchors.leftMargin: Appearance.padding.smaller
|
||||||
|
|
||||||
MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 }
|
MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 }
|
||||||
CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true }
|
CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
StateLayer {
|
||||||
Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath])
|
anchors.fill: parent
|
||||||
contextMenu.close()
|
|
||||||
|
onClicked: {
|
||||||
|
if (contextMenu.targetIsDir) {
|
||||||
|
Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", contextMenu.targetFilePath])
|
||||||
|
} else {
|
||||||
|
Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath])
|
||||||
|
}
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,92 +115,117 @@ Item {
|
|||||||
Layout.bottomMargin: 4
|
Layout.bottomMargin: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
CustomRect {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
radius: popupBackground.radius - popupBackground.padding
|
||||||
contentItem: RowLayout {
|
implicitHeight: copyPathRow.implicitHeight + Appearance.padding.small * 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: copyPathRow
|
||||||
spacing: 8
|
spacing: 8
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 12
|
anchors.leftMargin: Appearance.padding.smaller
|
||||||
|
|
||||||
MaterialIcon { text: "content_copy"; font.pointSize: 20 }
|
MaterialIcon { text: "content_copy"; font.pointSize: 20 }
|
||||||
CustomText { text: "Copy path"; Layout.fillWidth: true }
|
CustomText { text: "Copy path"; Layout.fillWidth: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
StateLayer {
|
||||||
Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")])
|
anchors.fill: parent
|
||||||
contextMenu.close()
|
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")])
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
CustomRect {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
visible: contextMenu.targetPaths.length === 1
|
visible: contextMenu.targetPaths.length === 1
|
||||||
|
radius: popupBackground.radius - popupBackground.padding
|
||||||
contentItem: RowLayout {
|
implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: renameRow
|
||||||
spacing: 8
|
spacing: 8
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 12
|
anchors.leftMargin: Appearance.padding.smaller
|
||||||
|
|
||||||
MaterialIcon { text: "edit"; font.pointSize: 20 }
|
MaterialIcon { text: "edit"; font.pointSize: 20 }
|
||||||
CustomText { text: "Rename"; Layout.fillWidth: true }
|
CustomText { text: "Rename"; Layout.fillWidth: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
StateLayer {
|
||||||
contextMenu.renameRequested(contextMenu.targetFilePath)
|
anchors.fill: parent
|
||||||
contextMenu.close()
|
|
||||||
|
onClicked: {
|
||||||
|
contextMenu.renameRequested(contextMenu.targetFilePath)
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: 1
|
implicitHeight: 1
|
||||||
color: Appearance.m3colors.m3outlineVariant
|
color: DynamicColors.palette.m3outlineVariant
|
||||||
Layout.topMargin: 4
|
Layout.topMargin: 4
|
||||||
Layout.bottomMargin: 4
|
Layout.bottomMargin: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
StateLayer {
|
CustomRect {
|
||||||
id: deleteButton
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
colBackgroundHover: Appearance.colors.colError
|
radius: popupBackground.radius - popupBackground.padding
|
||||||
|
implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2
|
||||||
contentItem: RowLayout {
|
|
||||||
|
RowLayout {
|
||||||
|
id: deleteRow
|
||||||
spacing: 8
|
spacing: 8
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 12
|
anchors.leftMargin: Appearance.padding.smaller
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
text: "delete";
|
text: "delete"
|
||||||
font.pointSize: 20;
|
font.pointSize: 20
|
||||||
color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError
|
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
text: "Move to trash";
|
text: "Move to trash"
|
||||||
Layout.fillWidth: true;
|
Layout.fillWidth: true
|
||||||
color: deleteButton.hovered ? Appearance.colors.colOnError : Appearance.colors.colError
|
color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
StateLayer {
|
||||||
let cmd = ["gio", "trash"].concat(contextMenu.targetPaths)
|
id: deleteButton
|
||||||
Quickshell.execDetached(cmd)
|
anchors.fill: parent
|
||||||
contextMenu.close()
|
color: DynamicColors.tPalette.m3error
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
let cmd = ["gio", "trash"].concat(contextMenu.targetPaths)
|
||||||
|
Quickshell.execDetached(cmd)
|
||||||
|
contextMenu.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) {
|
function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) {
|
||||||
targetFilePath = path
|
targetFilePath = path
|
||||||
targetIsDir = isDir
|
targetIsDir = isDir
|
||||||
targetAppEntry = appEnt
|
targetAppEntry = appEnt
|
||||||
|
|
||||||
targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path]
|
targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path]
|
||||||
|
|
||||||
menuX = Math.min(mouseX, parentW - popupBackground.implicitWidth)
|
menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth))
|
||||||
menuY = Math.min(mouseY, parentH - popupBackground.implicitHeight)
|
menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight))
|
||||||
|
|
||||||
visible = true
|
visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
visible = false
|
visible = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Item {
|
|||||||
property int gridX: model.gridX
|
property int gridX: model.gridX
|
||||||
property int gridY: model.gridY
|
property int gridY: model.gridY
|
||||||
property bool isSnapping: snapAnimX.running || snapAnimY.running
|
property bool isSnapping: snapAnimX.running || snapAnimY.running
|
||||||
|
property bool lassoActive
|
||||||
property string resolvedIcon: {
|
property string resolvedIcon: {
|
||||||
if (fileName.endsWith(".desktop")) {
|
if (fileName.endsWith(".desktop")) {
|
||||||
if (appEntry && appEntry.icon && appEntry.icon !== "")
|
if (appEntry && appEntry.icon && appEntry.icon !== "")
|
||||||
@@ -214,7 +215,7 @@ Item {
|
|||||||
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: root.lassoActive ? undefined : Qt.PointingHandCursor
|
||||||
drag.target: dragContainer
|
drag.target: dragContainer
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
|
import qs.Config
|
||||||
|
import qs.Components
|
||||||
import qs.Paths
|
import qs.Paths
|
||||||
import ZShell.Services
|
import ZShell.Services
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ Item {
|
|||||||
property string editingFilePath: ""
|
property string editingFilePath: ""
|
||||||
property real groupDragX: 0
|
property real groupDragX: 0
|
||||||
property real groupDragY: 0
|
property real groupDragY: 0
|
||||||
|
property bool lassoActive: false
|
||||||
property var selectedIcons: []
|
property var selectedIcons: []
|
||||||
property real startX: 0
|
property real startX: 0
|
||||||
property real startY: 0
|
property real startY: 0
|
||||||
@@ -54,7 +57,6 @@ Item {
|
|||||||
root.groupDragY = 0;
|
root.groupDragY = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
Keys.onPressed: event => {
|
||||||
@@ -68,15 +70,55 @@ Item {
|
|||||||
Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop))
|
Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop))
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
CustomRect {
|
||||||
id: lasso
|
id: lasso
|
||||||
|
|
||||||
border.color: Appearance.colors.colPrimary
|
function hideLasso() {
|
||||||
|
fadeIn.stop();
|
||||||
|
fadeOut.start();
|
||||||
|
root.lassoActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLasso() {
|
||||||
|
root.lassoActive = true;
|
||||||
|
fadeOut.stop();
|
||||||
|
visible = true;
|
||||||
|
fadeIn.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
border.color: DynamicColors.palette.m3primary
|
||||||
border.width: 1
|
border.width: 1
|
||||||
color: DynamicColors.tPalette.m3primary
|
color: DynamicColors.tPalette.m3primary
|
||||||
|
opacity: 0
|
||||||
radius: Appearance.rounding.small
|
radius: Appearance.rounding.small
|
||||||
visible: false
|
visible: false
|
||||||
z: 99
|
z: 99
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
id: fadeIn
|
||||||
|
|
||||||
|
duration: 120
|
||||||
|
from: 0
|
||||||
|
property: "opacity"
|
||||||
|
target: lasso
|
||||||
|
to: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: fadeOut
|
||||||
|
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 120
|
||||||
|
from: lasso.opacity
|
||||||
|
property: "opacity"
|
||||||
|
target: lasso
|
||||||
|
to: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptAction {
|
||||||
|
script: lasso.visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -85,10 +127,10 @@ Item {
|
|||||||
|
|
||||||
onPositionChanged: mouse => {
|
onPositionChanged: mouse => {
|
||||||
if (lasso.visible) {
|
if (lasso.visible) {
|
||||||
lasso.x = Math.min(mouse.x, root.startX);
|
lasso.x = Math.floor(Math.min(mouse.x, root.startX));
|
||||||
lasso.y = Math.min(mouse.y, root.startY);
|
lasso.y = Math.floor(Math.min(mouse.y, root.startY));
|
||||||
lasso.width = Math.abs(mouse.x - root.startX);
|
lasso.width = Math.floor(Math.abs(mouse.x - root.startX));
|
||||||
lasso.height = Math.abs(mouse.y - root.startY);
|
lasso.height = Math.floor(Math.abs(mouse.y - root.startY));
|
||||||
|
|
||||||
let minCol = Math.floor((lasso.x - gridArea.x) / cellWidth);
|
let minCol = Math.floor((lasso.x - gridArea.x) / cellWidth);
|
||||||
let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / cellWidth);
|
let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / cellWidth);
|
||||||
@@ -115,17 +157,17 @@ Item {
|
|||||||
} else {
|
} else {
|
||||||
bgContextMenu.close();
|
bgContextMenu.close();
|
||||||
root.selectedIcons = [];
|
root.selectedIcons = [];
|
||||||
root.startX = mouse.x;
|
root.startX = Math.floor(mouse.x);
|
||||||
root.startY = mouse.y;
|
root.startY = Math.floor(mouse.y);
|
||||||
lasso.x = mouse.x;
|
lasso.x = Math.floor(mouse.x);
|
||||||
lasso.y = mouse.y;
|
lasso.y = Math.floor(mouse.y);
|
||||||
lasso.width = 0;
|
lasso.width = 0;
|
||||||
lasso.height = 0;
|
lasso.height = 0;
|
||||||
lasso.visible = true;
|
lasso.showLasso();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onReleased: {
|
onReleased: {
|
||||||
lasso.visible = false;
|
lasso.hideLasso();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +184,8 @@ Item {
|
|||||||
|
|
||||||
delegate: DesktopIconDelegate {
|
delegate: DesktopIconDelegate {
|
||||||
property int itemIndex: index
|
property int itemIndex: index
|
||||||
|
|
||||||
|
lassoActive: root.lassoActive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ Loader {
|
|||||||
WallBackground {
|
WallBackground {
|
||||||
}
|
}
|
||||||
|
|
||||||
DesktopIcons {
|
Loader {
|
||||||
|
id: loader
|
||||||
|
|
||||||
|
active: Config.general.desktopIcons
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
sourceComponent: DesktopIcons {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ Singleton {
|
|||||||
readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell`
|
readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell`
|
||||||
readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell`
|
readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell`
|
||||||
readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell`
|
readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell`
|
||||||
readonly property string desktop: `${Quickshell.env("XDG_DATA_HOME") || `${home}/Desktop`}`
|
readonly property string desktop: `${Quickshell.env("HOME")}/Desktop`
|
||||||
readonly property string home: Quickshell.env("HOME")
|
readonly property string home: Quickshell.env("HOME")
|
||||||
readonly property string imagecache: `${cache}/imagecache`
|
readonly property string imagecache: `${cache}/imagecache`
|
||||||
readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell"
|
readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell"
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
// Original code from https://github.com/koeqaife/hyprland-material-you
|
||||||
|
// Original code license: GPLv3
|
||||||
|
// Translated to Js from Cython with an LLM and reviewed
|
||||||
|
|
||||||
|
function min3(a, b, c) {
|
||||||
|
return a < b && a < c ? a : b < c ? b : c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function max3(a, b, c) {
|
||||||
|
return a > b && a > c ? a : b > c ? b : c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function min2(a, b) {
|
||||||
|
return a < b ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function max2(a, b) {
|
||||||
|
return a > b ? a : b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function levenshteinDistance(s1, s2) {
|
||||||
|
let len1 = s1.length;
|
||||||
|
let len2 = s2.length;
|
||||||
|
|
||||||
|
if (len1 === 0) return len2;
|
||||||
|
if (len2 === 0) return len1;
|
||||||
|
|
||||||
|
if (len2 > len1) {
|
||||||
|
[s1, s2] = [s2, s1];
|
||||||
|
[len1, len2] = [len2, len1];
|
||||||
|
}
|
||||||
|
|
||||||
|
let prev = new Array(len2 + 1);
|
||||||
|
let curr = new Array(len2 + 1);
|
||||||
|
|
||||||
|
for (let j = 0; j <= len2; j++) {
|
||||||
|
prev[j] = j;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= len1; i++) {
|
||||||
|
curr[0] = i;
|
||||||
|
for (let j = 1; j <= len2; j++) {
|
||||||
|
let cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
||||||
|
curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
||||||
|
}
|
||||||
|
[prev, curr] = [curr, prev];
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev[len2];
|
||||||
|
}
|
||||||
|
|
||||||
|
function partialRatio(shortS, longS) {
|
||||||
|
let lenS = shortS.length;
|
||||||
|
let lenL = longS.length;
|
||||||
|
let best = 0.0;
|
||||||
|
|
||||||
|
if (lenS === 0) return 1.0;
|
||||||
|
|
||||||
|
for (let i = 0; i <= lenL - lenS; i++) {
|
||||||
|
let sub = longS.slice(i, i + lenS);
|
||||||
|
let dist = levenshteinDistance(shortS, sub);
|
||||||
|
let score = 1.0 - dist / lenS;
|
||||||
|
if (score > best) best = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeScore(s1, s2) {
|
||||||
|
if (s1 === s2) return 1.0;
|
||||||
|
|
||||||
|
let dist = levenshteinDistance(s1, s2);
|
||||||
|
let maxLen = max2(s1.length, s2.length);
|
||||||
|
if (maxLen === 0) return 1.0;
|
||||||
|
|
||||||
|
let full = 1.0 - dist / maxLen;
|
||||||
|
let part =
|
||||||
|
s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1);
|
||||||
|
|
||||||
|
let score = 0.85 * full + 0.15 * part;
|
||||||
|
|
||||||
|
if (s1 && s2 && s1[0] !== s2[0]) {
|
||||||
|
score -= 0.05;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lenDiff = Math.abs(s1.length - s2.length);
|
||||||
|
if (lenDiff >= 3) {
|
||||||
|
score -= (0.05 * lenDiff) / maxLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
let commonPrefixLen = 0;
|
||||||
|
let minLen = min2(s1.length, s2.length);
|
||||||
|
for (let i = 0; i < minLen; i++) {
|
||||||
|
if (s1[i] === s2[i]) {
|
||||||
|
commonPrefixLen++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += 0.02 * commonPrefixLen;
|
||||||
|
|
||||||
|
if (s1.includes(s2) || s2.includes(s1)) {
|
||||||
|
score += 0.06;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(0.0, Math.min(1.0, score));
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeTextMatchScore(s1, s2) {
|
||||||
|
if (s1 === s2) return 1.0;
|
||||||
|
|
||||||
|
let dist = levenshteinDistance(s1, s2);
|
||||||
|
let maxLen = max2(s1.length, s2.length);
|
||||||
|
if (maxLen === 0) return 1.0;
|
||||||
|
|
||||||
|
let full = 1.0 - dist / maxLen;
|
||||||
|
let part =
|
||||||
|
s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1);
|
||||||
|
|
||||||
|
let score = 0.4 * full + 0.6 * part;
|
||||||
|
|
||||||
|
let lenDiff = Math.abs(s1.length - s2.length);
|
||||||
|
if (lenDiff >= 10) {
|
||||||
|
score -= (0.02 * lenDiff) / maxLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
let commonPrefixLen = 0;
|
||||||
|
let minLen = min2(s1.length, s2.length);
|
||||||
|
for (let i = 0; i < minLen; i++) {
|
||||||
|
if (s1[i] === s2[i]) {
|
||||||
|
commonPrefixLen++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += 0.01 * commonPrefixLen;
|
||||||
|
|
||||||
|
if (s1.includes(s2) || s2.includes(s1)) {
|
||||||
|
score += 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(0.0, Math.min(1.0, score));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user