dock
This commit is contained in:
+81
-53
@@ -9,7 +9,11 @@ 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 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 => ({
|
||||
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<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 bestFuzzyEntry(search: string, preppedList: list<var>, 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>): 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) {
|
||||
|
||||
+19
-18
@@ -8,7 +8,7 @@ import qs.Config
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<var> 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<var> toplevels
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user