Merge settings window to main #23

Merged
Zacharias-Brohn merged 48 commits from settingsWindow into main 2026-03-18 16:27:50 +01:00
5 changed files with 208 additions and 82 deletions
Showing only changes of commit 37e482a361 - Show all commits
+81 -53
View File
@@ -9,7 +9,11 @@ import qs.Config
Singleton { Singleton {
id: root id: root
readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values).sort((a, b) => a.name.localeCompare(b.name)) readonly property list<DesktopEntry> 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 => ({ readonly property var preppedNames: list.map(a => ({
name: Fuzzy.prepare(`${a.name} `), name: Fuzzy.prepare(`${a.name} `),
entry: a entry: a
@@ -32,8 +36,8 @@ Singleton {
"replace": "system-lock-screen" "replace": "system-lock-screen"
} }
] ]
property real scoreThreshold: 0.2 readonly property real scoreGapThreshold: 0.1
property bool sloppySearch: Config.options?.search.sloppy ?? false readonly property real scoreThreshold: 0.6
property var substitutions: ({ property var substitutions: ({
"code-url-handler": "visual-studio-code", "code-url-handler": "visual-studio-code",
"Code": "visual-studio-code", "Code": "visual-studio-code",
@@ -41,46 +45,65 @@ Singleton {
"pavucontrol-qt": "pavucontrol", "pavucontrol-qt": "pavucontrol",
"wps": "wps-office2019-kprometheus", "wps": "wps-office2019-kprometheus",
"wpsoffice": "wps-office2019-kprometheus", "wpsoffice": "wps-office2019-kprometheus",
"footclient": "foot", "footclient": "foot"
"zen": "zen-browser"
}) })
signal reload function bestFuzzyEntry(search: string, preppedList: list<var>, key: string): var {
const results = Fuzzy.go(search, preppedList, {
function computeScore(...args) { key: key,
return Levendist.computeScore(...args); threshold: root.scoreThreshold,
} limit: 2
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;
}); });
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>): 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) { function guessIcon(str) {
if (!str || str.length == 0) if (!str || str.length == 0)
return "image-missing"; 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]) if (substitutions[str])
return substitutions[str]; return substitutions[str];
if (substitutions[str.toLowerCase()])
return substitutions[str.toLowerCase()];
// Regex substitutions
for (let i = 0; i < regexSubstitutions.length; i++) { for (let i = 0; i < regexSubstitutions.length; i++) {
const substitution = regexSubstitutions[i]; const substitution = regexSubstitutions[i];
const replacedName = str.replace(substitution.regex, substitution.replace); const replacedName = str.replace(substitution.regex, substitution.replace);
@@ -88,30 +111,35 @@ Singleton {
return replacedName; return replacedName;
} }
// If it gets detected normally, no need to guess const lowercased = str.toLowerCase();
if (iconExists(str)) if (iconExists(lowercased))
return str; return lowercased;
let guessStr = str; const reverseDomainNameAppName = getReverseDomainNameAppName(str);
// Guess: Take only app name of reverse domain name notation if (iconExists(reverseDomainNameAppName))
guessStr = str.split('.').slice(-1)[0].toLowerCase(); return reverseDomainNameAppName;
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 const lowercasedDomainNameAppName = reverseDomainNameAppName.toLowerCase();
return str; 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) { function iconExists(iconName) {
+19 -18
View File
@@ -8,7 +8,7 @@ import qs.Config
Singleton { Singleton {
id: root id: root
property list<var> apps: { property var apps: {
const pinnedApps = uniq((Config.dock.pinnedApps ?? []).map(normalizeId)); const pinnedApps = uniq((Config.dock.pinnedApps ?? []).map(normalizeId));
const openMap = buildOpenMap(); const openMap = buildOpenMap();
const openIds = [...openMap.keys()]; 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))); 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, appId,
pinned: true, pinned: true,
toplevels: openMap.get(appId) ?? [] toplevels: openMap.get(appId) ?? []
}))).concat(pinnedApps.length > 0 ? [appEntryComp.createObject(null, { });
}
if (pinnedApps.length > 0) {
out.push({
appId: root.separatorId, appId: root.separatorId,
pinned: false, pinned: false,
toplevels: [] toplevels: []
})] : []).concat(orderedUnpinned.map(appId => appEntryComp.createObject(null, { });
}
for (const appId of orderedUnpinned) {
out.push({
appId, appId,
pinned: false, pinned: false,
toplevels: openMap.get(appId) ?? [] toplevels: openMap.get(appId) ?? []
}))); });
}
return out;
} }
readonly property string separatorId: "__dock_separator__" readonly property string separatorId: "__dock_separator__"
property var unpinnedOrder: [] property var unpinnedOrder: []
@@ -86,17 +100,4 @@ Singleton {
function uniq(ids) { function uniq(ids) {
return (ids ?? []).filter((id, i, arr) => id && arr.indexOf(id) === i); 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<var> toplevels
}
} }
+1 -1
View File
@@ -17,7 +17,7 @@ Item {
CustomRect { CustomRect {
anchors.fill: parent 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.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
+99 -9
View File
@@ -24,6 +24,7 @@ Item {
readonly property int padding: Appearance.padding.small readonly property int padding: Appearance.padding.small
required property var panels required property var panels
readonly property int rounding: Appearance.rounding.large readonly property int rounding: Appearance.rounding.large
required property ShellScreen screen
required property PersistentProperties visibilities required property PersistentProperties visibilities
property var visualIds: [] property var visualIds: []
@@ -87,6 +88,7 @@ Item {
readonly property string appId: modelData.appId readonly property string appId: modelData.appId
readonly property bool isSeparator: appId === TaskbarApps.separatorId readonly property bool isSeparator: appId === TaskbarApps.separatorId
required property var modelData required property var modelData
property bool removing: false
function previewReorder(drag) { function previewReorder(drag) {
const source = drag.source; const source = drag.source;
@@ -102,12 +104,52 @@ Item {
root.previewVisualMove(from, hovered, drag.x < width / 2); root.previewVisualMove(from, hovered, drag.x < width / 2);
} }
height: Config.dock.height function startDetachedRemove() {
width: isSeparator ? 1 : Config.dock.height 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) onEntered: drag => previewReorder(drag)
onPositionChanged: 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 { DockAppButton {
id: button id: button
@@ -121,7 +163,7 @@ Item {
DragHandler { DragHandler {
id: dragHandler id: dragHandler
enabled: !slot.isSeparator enabled: !slot.isSeparator && !slot.removing
grabPermissions: PointerHandler.CanTakeOverFromAnything grabPermissions: PointerHandler.CanTakeOverFromAnything
target: null target: null
xAxis.enabled: true xAxis.enabled: true
@@ -160,24 +202,72 @@ Item {
CustomListView { CustomListView {
id: dockRow 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 boundsBehavior: Flickable.StopAtBounds
implicitHeight: Config.dock.height height: Config.dock.height
implicitWidth: root.dockContentWidth implicitWidth: root.dockContentWidth + Config.dock.height
interactive: !root.dragActive interactive: !root.dragActive
model: visualModel model: visualModel
orientation: ListView.Horizontal orientation: ListView.Horizontal
spacing: Appearance.padding.smaller spacing: Appearance.padding.smaller
Behavior on implicitWidth { add: Transition {
Anim { 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 { Anim {
duration: Appearance.anim.durations.small
properties: "x,y" 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 { Item {
+8 -1
View File
@@ -18,6 +18,12 @@ Item {
implicitWidth: content.implicitWidth implicitWidth: content.implicitWidth
visible: height > 0 visible: height > 0
Behavior on implicitWidth {
Anim {
duration: Appearance.anim.durations.small
}
}
onShouldBeActiveChanged: { onShouldBeActiveChanged: {
if (shouldBeActive) { if (shouldBeActive) {
timer.stop(); timer.stop();
@@ -84,12 +90,13 @@ Item {
id: content id: content
active: false active: false
anchors.horizontalCenter: parent.horizontalCenter anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
visible: false visible: false
sourceComponent: Content { sourceComponent: Content {
panels: root.panels panels: root.panels
screen: root.screen
visibilities: root.visibilities visibilities: root.visibilities
Component.onCompleted: root.contentHeight = implicitHeight Component.onCompleted: root.contentHeight = implicitHeight