diff --git a/Helpers/AppSearch.qml b/Helpers/AppSearch.qml index f5391bb..400b3d9 100644 --- a/Helpers/AppSearch.qml +++ b/Helpers/AppSearch.qml @@ -9,7 +9,11 @@ import qs.Config Singleton { id: root - readonly property list list: Array.from(DesktopEntries.applications.values).sort((a, b) => a.name.localeCompare(b.name)) + readonly property list list: Array.from(DesktopEntries.applications.values).filter((app, index, self) => index === self.findIndex(t => (t.id === app.id))) + readonly property var preppedIcons: list.map(a => ({ + name: Fuzzy.prepare(`${a.icon} `), + entry: a + })) readonly property var preppedNames: list.map(a => ({ name: Fuzzy.prepare(`${a.name} `), entry: a @@ -32,8 +36,8 @@ Singleton { "replace": "system-lock-screen" } ] - property real scoreThreshold: 0.2 - property bool sloppySearch: Config.options?.search.sloppy ?? false + readonly property real scoreGapThreshold: 0.1 + readonly property real scoreThreshold: 0.6 property var substitutions: ({ "code-url-handler": "visual-studio-code", "Code": "visual-studio-code", @@ -41,46 +45,65 @@ Singleton { "pavucontrol-qt": "pavucontrol", "wps": "wps-office2019-kprometheus", "wpsoffice": "wps-office2019-kprometheus", - "footclient": "foot", - "zen": "zen-browser" + "footclient": "foot" }) - signal reload - - function computeScore(...args) { - return Levendist.computeScore(...args); - } - - function computeTextMatchScore(...args) { - return Levendist.computeTextMatchScore(...args); - } - - function fuzzyQuery(search: string): var { // Idk why list 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 bestFuzzyEntry(search: string, preppedList: list, key: string): var { + const results = Fuzzy.go(search, preppedList, { + key: key, + threshold: root.scoreThreshold, + limit: 2 }); + + if (!results || results.length === 0) + return null; + + const best = results[0]; + const second = results.length > 1 ? results[1] : null; + + if (second && (best.score - second.score) < root.scoreGapThreshold) + return null; + + return best.obj.entry; + } + + function fuzzyQuery(search: string, preppedList: list): var { + const entry = bestFuzzyEntry(search, preppedList, "name"); + return entry ? [entry] : []; + } + + function getKebabNormalizedAppName(str: string): string { + return str.toLowerCase().replace(/\s+/g, "-"); + } + + function getReverseDomainNameAppName(str: string): string { + return str.split('.').slice(-1)[0]; + } + + function getUndescoreToKebabAppName(str: string): string { + return str.toLowerCase().replace(/_/g, "-"); } function guessIcon(str) { if (!str || str.length == 0) return "image-missing"; - // Normal substitutions + if (iconExists(str)) + return str; + + const entry = DesktopEntries.byId(str); + if (entry) + return entry.icon; + + const heuristicEntry = DesktopEntries.heuristicLookup(str); + if (heuristicEntry) + return heuristicEntry.icon; + if (substitutions[str]) return substitutions[str]; + if (substitutions[str.toLowerCase()]) + return substitutions[str.toLowerCase()]; - // Regex substitutions for (let i = 0; i < regexSubstitutions.length; i++) { const substitution = regexSubstitutions[i]; const replacedName = str.replace(substitution.regex, substitution.replace); @@ -88,30 +111,35 @@ Singleton { return replacedName; } - // If it gets detected normally, no need to guess - if (iconExists(str)) - return str; + const lowercased = str.toLowerCase(); + if (iconExists(lowercased)) + return lowercased; - 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; - } + const reverseDomainNameAppName = getReverseDomainNameAppName(str); + if (iconExists(reverseDomainNameAppName)) + return reverseDomainNameAppName; - // Give up - return str; + const lowercasedDomainNameAppName = reverseDomainNameAppName.toLowerCase(); + if (iconExists(lowercasedDomainNameAppName)) + return lowercasedDomainNameAppName; + + const kebabNormalizedGuess = getKebabNormalizedAppName(str); + if (iconExists(kebabNormalizedGuess)) + return kebabNormalizedGuess; + + const undescoreToKebabGuess = getUndescoreToKebabAppName(str); + if (iconExists(undescoreToKebabGuess)) + return undescoreToKebabGuess; + + const iconSearchResult = fuzzyQuery(str, preppedIcons); + if (iconSearchResult && iconExists(iconSearchResult.icon)) + return iconSearchResult.icon; + + const nameSearchResult = root.fuzzyQuery(str, preppedNames); + if (nameSearchResult && iconExists(nameSearchResult.icon)) + return nameSearchResult.icon; + + return "application-x-executable"; } function iconExists(iconName) { diff --git a/Helpers/TaskbarApps.qml b/Helpers/TaskbarApps.qml index 6bed8ee..8f3f78b 100644 --- a/Helpers/TaskbarApps.qml +++ b/Helpers/TaskbarApps.qml @@ -8,7 +8,7 @@ import qs.Config Singleton { id: root - property list apps: { + property var apps: { const pinnedApps = uniq((Config.dock.pinnedApps ?? []).map(normalizeId)); const openMap = buildOpenMap(); const openIds = [...openMap.keys()]; @@ -16,19 +16,33 @@ Singleton { const orderedUnpinned = sessionOrder.filter(id => openIds.includes(id) && !pinnedApps.includes(id)).concat(openIds.filter(id => !pinnedApps.includes(id) && !sessionOrder.includes(id))); - return [].concat(pinnedApps.map(appId => appEntryComp.createObject(null, { + const out = []; + + for (const appId of pinnedApps) { + out.push({ appId, pinned: true, toplevels: openMap.get(appId) ?? [] - }))).concat(pinnedApps.length > 0 ? [appEntryComp.createObject(null, { + }); + } + + if (pinnedApps.length > 0) { + out.push({ appId: root.separatorId, pinned: false, toplevels: [] - })] : []).concat(orderedUnpinned.map(appId => appEntryComp.createObject(null, { + }); + } + + for (const appId of orderedUnpinned) { + out.push({ appId, pinned: false, toplevels: openMap.get(appId) ?? [] - }))); + }); + } + + return out; } readonly property string separatorId: "__dock_separator__" property var unpinnedOrder: [] @@ -86,17 +100,4 @@ Singleton { function uniq(ids) { return (ids ?? []).filter((id, i, arr) => id && arr.indexOf(id) === i); } - - Component { - id: appEntryComp - - TaskbarAppEntry { - } - } - - component TaskbarAppEntry: QtObject { - required property string appId - required property bool pinned - required property list toplevels - } } diff --git a/Modules/Bar/Border.qml b/Modules/Bar/Border.qml index f82496f..d12523a 100644 --- a/Modules/Bar/Border.qml +++ b/Modules/Bar/Border.qml @@ -17,7 +17,7 @@ Item { CustomRect { anchors.fill: parent - color: Config.barConfig.border === 1 ? "transparent" : DynamicColors.palette.m3surface + color: !root.bar.isHovered && Config.barConfig.autoHide ? "transparent" : DynamicColors.palette.m3surface layer.enabled: true layer.effect: MultiEffect { diff --git a/Modules/Dock/Content.qml b/Modules/Dock/Content.qml index 2a4bba4..955d955 100644 --- a/Modules/Dock/Content.qml +++ b/Modules/Dock/Content.qml @@ -24,6 +24,7 @@ Item { readonly property int padding: Appearance.padding.small required property var panels readonly property int rounding: Appearance.rounding.large + required property ShellScreen screen required property PersistentProperties visibilities property var visualIds: [] @@ -87,6 +88,7 @@ Item { readonly property string appId: modelData.appId readonly property bool isSeparator: appId === TaskbarApps.separatorId required property var modelData + property bool removing: false function previewReorder(drag) { const source = drag.source; @@ -102,12 +104,52 @@ Item { root.previewVisualMove(from, hovered, drag.x < width / 2); } - height: Config.dock.height - width: isSeparator ? 1 : Config.dock.height + function startDetachedRemove() { + const p = mapToItem(removalLayer, 0, 0); + removing = true; + ListView.delayRemove = true; + + parent = removalLayer; + x = p.x; + y = p.y; + + removeAnim.start(); + } + + height: Config.dock.height + transformOrigin: Item.Center + width: isSeparator ? 1 : Config.dock.height + z: removing ? 1 : 0 + + ListView.onRemove: startDetachedRemove() onEntered: drag => previewReorder(drag) onPositionChanged: drag => previewReorder(drag) + SequentialAnimation { + id: removeAnim + + ParallelAnimation { + Anim { + property: "opacity" + target: slot + to: 0 + } + + Anim { + property: "scale" + target: slot + to: 0.5 + } + } + + ScriptAction { + script: { + slot.ListView.delayRemove = false; + } + } + } + DockAppButton { id: button @@ -121,7 +163,7 @@ Item { DragHandler { id: dragHandler - enabled: !slot.isSeparator + enabled: !slot.isSeparator && !slot.removing grabPermissions: PointerHandler.CanTakeOverFromAnything target: null xAxis.enabled: true @@ -160,24 +202,72 @@ Item { CustomListView { id: dockRow - anchors.centerIn: parent + property bool enableAddAnimation: false + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.top: parent.top boundsBehavior: Flickable.StopAtBounds - implicitHeight: Config.dock.height - implicitWidth: root.dockContentWidth + height: Config.dock.height + implicitWidth: root.dockContentWidth + Config.dock.height interactive: !root.dragActive model: visualModel orientation: ListView.Horizontal spacing: Appearance.padding.smaller - Behavior on implicitWidth { - Anim { + add: Transition { + ParallelAnimation { + Anim { + duration: dockRow.enableAddAnimation ? Appearance.anim.durations.normal : 0 + from: 0 + property: "opacity" + to: 1 + } + + Anim { + duration: dockRow.enableAddAnimation ? Appearance.anim.durations.normal : 0 + from: 0.5 + property: "scale" + to: 1 + } } } - moveDisplaced: Transition { + displaced: Transition { Anim { + duration: Appearance.anim.durations.small properties: "x,y" } } + move: Transition { + Anim { + duration: Appearance.anim.durations.small + properties: "x,y" + } + } + remove: Transition { + ParallelAnimation { + Anim { + property: "opacity" + to: 0 + } + + Anim { + property: "scale" + to: 0.5 + } + } + } + + Component.onCompleted: { + Qt.callLater(() => enableAddAnimation = true); + } + } + + Item { + id: removalLayer + + anchors.fill: parent + z: 9998 } Item { diff --git a/Modules/Dock/Wrapper.qml b/Modules/Dock/Wrapper.qml index 5d7e36d..667ab62 100644 --- a/Modules/Dock/Wrapper.qml +++ b/Modules/Dock/Wrapper.qml @@ -18,6 +18,12 @@ Item { implicitWidth: content.implicitWidth visible: height > 0 + Behavior on implicitWidth { + Anim { + duration: Appearance.anim.durations.small + } + } + onShouldBeActiveChanged: { if (shouldBeActive) { timer.stop(); @@ -84,12 +90,13 @@ Item { id: content active: false - anchors.horizontalCenter: parent.horizontalCenter + anchors.left: parent.left anchors.top: parent.top visible: false sourceComponent: Content { panels: root.panels + screen: root.screen visibilities: root.visibilities Component.onCompleted: root.contentHeight = implicitHeight