dock
This commit is contained in:
@@ -33,6 +33,10 @@ Singleton {
|
|||||||
recentSaveCooldown.restart();
|
recentSaveCooldown.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveNoToast(): void {
|
||||||
|
saveTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
function serializeAppearance(): var {
|
function serializeAppearance(): var {
|
||||||
return {
|
return {
|
||||||
rounding: {
|
rounding: {
|
||||||
|
|||||||
+61
-45
@@ -9,64 +9,82 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property list<var> apps: {
|
property list<var> apps: {
|
||||||
var map = new Map();
|
const pinnedApps = uniq((Config.dock.pinnedApps ?? []).map(normalizeId));
|
||||||
|
const openMap = buildOpenMap();
|
||||||
|
const openIds = [...openMap.keys()];
|
||||||
|
const sessionOrder = uniq(root.unpinnedOrder.map(normalizeId));
|
||||||
|
|
||||||
// Pinned apps
|
const orderedUnpinned = sessionOrder.filter(id => openIds.includes(id) && !pinnedApps.includes(id)).concat(openIds.filter(id => !pinnedApps.includes(id) && !sessionOrder.includes(id)));
|
||||||
const pinnedApps = Config.dock.pinnedApps ?? [];
|
|
||||||
for (const appId of pinnedApps) {
|
|
||||||
if (!map.has(appId.toLowerCase()))
|
|
||||||
map.set(appId.toLowerCase(), ({
|
|
||||||
pinned: true,
|
|
||||||
toplevels: []
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Separator
|
return [].concat(pinnedApps.map(appId => appEntryComp.createObject(null, {
|
||||||
if (pinnedApps.length > 0) {
|
appId,
|
||||||
map.set("SEPARATOR", {
|
pinned: true,
|
||||||
|
toplevels: openMap.get(appId) ?? []
|
||||||
|
}))).concat(pinnedApps.length > 0 ? [appEntryComp.createObject(null, {
|
||||||
|
appId: root.separatorId,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
toplevels: []
|
toplevels: []
|
||||||
});
|
})] : []).concat(orderedUnpinned.map(appId => appEntryComp.createObject(null, {
|
||||||
}
|
appId,
|
||||||
|
pinned: false,
|
||||||
|
toplevels: openMap.get(appId) ?? []
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
readonly property string separatorId: "__dock_separator__"
|
||||||
|
property var unpinnedOrder: []
|
||||||
|
|
||||||
// Ignored apps
|
function buildOpenMap() {
|
||||||
const ignoredRegexStrings = Config.dock.ignoredAppRegexes ?? [];
|
const ignoredRegexes = (Config.dock.ignoredAppRegexes ?? []).map(pattern => new RegExp(pattern, "i"));
|
||||||
const ignoredRegexes = ignoredRegexStrings.map(pattern => new RegExp(pattern, "i"));
|
|
||||||
// Open windows
|
return ToplevelManager.toplevels.values.reduce((map, toplevel) => {
|
||||||
for (const toplevel of ToplevelManager.toplevels.values) {
|
|
||||||
if (ignoredRegexes.some(re => re.test(toplevel.appId)))
|
if (ignoredRegexes.some(re => re.test(toplevel.appId)))
|
||||||
continue;
|
return map;
|
||||||
if (!map.has(toplevel.appId.toLowerCase()))
|
|
||||||
map.set(toplevel.appId.toLowerCase(), ({
|
|
||||||
pinned: false,
|
|
||||||
toplevels: []
|
|
||||||
}));
|
|
||||||
map.get(toplevel.appId.toLowerCase()).toplevels.push(toplevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
var values = [];
|
const appId = normalizeId(toplevel.appId);
|
||||||
|
if (!appId)
|
||||||
|
return map;
|
||||||
|
|
||||||
for (const [key, value] of map) {
|
map.set(appId, (map.get(appId) ?? []).concat([toplevel]));
|
||||||
values.push(appEntryComp.createObject(null, {
|
return map;
|
||||||
appId: key,
|
}, new Map());
|
||||||
toplevels: value.toplevels,
|
}
|
||||||
pinned: value.pinned
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
function commitVisualOrder(ids) {
|
||||||
|
const orderedIds = uniq(ids.map(normalizeId));
|
||||||
|
const separatorIndex = orderedIds.indexOf(root.separatorId);
|
||||||
|
|
||||||
|
const pinnedApps = (separatorIndex === -1 ? [] : orderedIds.slice(0, separatorIndex)).filter(id => id !== root.separatorId);
|
||||||
|
|
||||||
|
const visibleUnpinned = orderedIds.slice(separatorIndex === -1 ? 0 : separatorIndex + 1).filter(id => id !== root.separatorId);
|
||||||
|
|
||||||
|
Config.dock.pinnedApps = pinnedApps;
|
||||||
|
root.unpinnedOrder = visibleUnpinned.concat(root.unpinnedOrder.map(normalizeId).filter(id => !pinnedApps.includes(id) && !visibleUnpinned.includes(id)));
|
||||||
|
Config.saveNoToast();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPinned(appId) {
|
function isPinned(appId) {
|
||||||
return Config.dock.pinnedApps.indexOf(appId) !== -1;
|
return uniq((Config.dock.pinnedApps ?? []).map(normalizeId)).includes(normalizeId(appId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeId(appId) {
|
||||||
|
if (appId === root.separatorId)
|
||||||
|
return root.separatorId;
|
||||||
|
|
||||||
|
return String(appId ?? "").toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePin(appId) {
|
function togglePin(appId) {
|
||||||
if (root.isPinned(appId)) {
|
const id = normalizeId(appId);
|
||||||
Config.dock.pinnedApps = Config.dock.pinnedApps.filter(id => id !== appId);
|
const pinnedApps = uniq((Config.dock.pinnedApps ?? []).map(normalizeId));
|
||||||
} else {
|
const pinned = pinnedApps.includes(id);
|
||||||
Config.dock.pinnedApps = Config.dock.pinnedApps.concat([appId]);
|
|
||||||
}
|
Config.dock.pinnedApps = pinned ? pinnedApps.filter(x => x !== id) : pinnedApps.concat([id]);
|
||||||
|
|
||||||
|
root.unpinnedOrder = pinned ? [id].concat(root.unpinnedOrder.map(normalizeId).filter(x => x !== id)) : root.unpinnedOrder.map(normalizeId).filter(x => x !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniq(ids) {
|
||||||
|
return (ids ?? []).filter((id, i, arr) => id && arr.indexOf(id) === i);
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
@@ -77,8 +95,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
component TaskbarAppEntry: QtObject {
|
component TaskbarAppEntry: QtObject {
|
||||||
id: wrapper
|
|
||||||
|
|
||||||
required property string appId
|
required property string appId
|
||||||
required property bool pinned
|
required property bool pinned
|
||||||
required property list<var> toplevels
|
required property list<var> toplevels
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Quickshell.execDetached(["qs", "-p", "/usr/share/sleex/settings.qml"])
|
const visibilities = Visibilities.getForActive();
|
||||||
|
visibilities.settings = true;
|
||||||
root.close()
|
root.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+177
-15
@@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQml.Models
|
||||||
import qs.Modules.Dock.Parts
|
import qs.Modules.Dock.Parts
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
@@ -10,37 +11,198 @@ import qs.Config
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property int dockContentWidth: TaskbarApps.apps.reduce((sum, app, i) => sum + (app.appId === TaskbarApps.separatorId ? 1 : Config.dock.height) + (i > 0 ? dockRow.spacing : 0), 0)
|
||||||
|
property bool dragActive: false
|
||||||
|
property real dragHeight: Config.dock.height
|
||||||
|
property real dragStartX: 0
|
||||||
|
property real dragStartY: 0
|
||||||
|
property real dragWidth: Config.dock.height
|
||||||
|
property real dragX: 0
|
||||||
|
property real dragY: 0
|
||||||
|
property string draggedAppId: ""
|
||||||
|
property var draggedModelData: null
|
||||||
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 PersistentProperties visibilities
|
required property PersistentProperties visibilities
|
||||||
|
property var visualIds: []
|
||||||
|
|
||||||
|
function beginVisualDrag(appId, modelData, item) {
|
||||||
|
const pos = item.mapToItem(root, 0, 0);
|
||||||
|
|
||||||
|
root.visualIds = TaskbarApps.apps.map(app => app.appId);
|
||||||
|
root.draggedAppId = appId;
|
||||||
|
root.draggedModelData = modelData;
|
||||||
|
root.dragWidth = item.width;
|
||||||
|
root.dragHeight = item.height;
|
||||||
|
root.dragStartX = pos.x;
|
||||||
|
root.dragStartY = pos.y;
|
||||||
|
root.dragX = pos.x;
|
||||||
|
root.dragY = pos.y;
|
||||||
|
root.dragActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endVisualDrag() {
|
||||||
|
const ids = root.visualIds.slice();
|
||||||
|
|
||||||
|
root.dragActive = false;
|
||||||
|
root.draggedAppId = "";
|
||||||
|
root.draggedModelData = null;
|
||||||
|
root.visualIds = [];
|
||||||
|
|
||||||
|
TaskbarApps.commitVisualOrder(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveArrayItem(list, from, to) {
|
||||||
|
const next = list.slice();
|
||||||
|
const [item] = next.splice(from, 1);
|
||||||
|
next.splice(to, 0, item);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewVisualMove(from, hovered, before) {
|
||||||
|
let to = hovered + (before ? 0 : 1);
|
||||||
|
|
||||||
|
if (to > from)
|
||||||
|
to -= 1;
|
||||||
|
|
||||||
|
to = Math.max(0, Math.min(visualModel.items.count - 1, to));
|
||||||
|
|
||||||
|
if (from === to)
|
||||||
|
return;
|
||||||
|
|
||||||
|
visualModel.items.move(from, to);
|
||||||
|
root.visualIds = moveArrayItem(root.visualIds, from, to);
|
||||||
|
}
|
||||||
|
|
||||||
implicitHeight: Config.dock.height + root.padding * 2
|
implicitHeight: Config.dock.height + root.padding * 2
|
||||||
implicitWidth: dockRow.implicitWidth + root.padding * 2
|
implicitWidth: root.dockContentWidth + root.padding * 2
|
||||||
|
|
||||||
CustomListView {
|
Component {
|
||||||
id: dockRow
|
id: dockDelegate
|
||||||
|
|
||||||
anchors.centerIn: parent
|
DropArea {
|
||||||
implicitHeight: Config.dock.height
|
id: slot
|
||||||
implicitWidth: contentWidth
|
|
||||||
orientation: ListView.Horizontal
|
|
||||||
spacing: Appearance.padding.smaller
|
|
||||||
|
|
||||||
delegate: DockAppButton {
|
readonly property string appId: modelData.appId
|
||||||
|
readonly property bool isSeparator: appId === TaskbarApps.separatorId
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
appListRoot: root
|
function previewReorder(drag) {
|
||||||
appToplevel: modelData
|
const source = drag.source;
|
||||||
visibilities: root.visibilities
|
if (!source || !source.appId || source.appId === appId)
|
||||||
}
|
return;
|
||||||
Behavior on implicitWidth {
|
|
||||||
Anim {
|
const from = source.visualIndex;
|
||||||
|
const hovered = slot.DelegateModel.itemsIndex;
|
||||||
|
|
||||||
|
if (from < 0 || hovered < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
root.previewVisualMove(from, hovered, drag.x < width / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
height: Config.dock.height
|
||||||
|
width: isSeparator ? 1 : Config.dock.height
|
||||||
|
|
||||||
|
onEntered: drag => previewReorder(drag)
|
||||||
|
onPositionChanged: drag => previewReorder(drag)
|
||||||
|
|
||||||
|
DockAppButton {
|
||||||
|
id: button
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
appListRoot: root
|
||||||
|
appToplevel: modelData
|
||||||
|
visibilities: root.visibilities
|
||||||
|
visible: root.draggedAppId !== slot.appId
|
||||||
|
}
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
id: dragHandler
|
||||||
|
|
||||||
|
enabled: !slot.isSeparator
|
||||||
|
grabPermissions: PointerHandler.CanTakeOverFromAnything
|
||||||
|
target: null
|
||||||
|
xAxis.enabled: true
|
||||||
|
yAxis.enabled: false
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active) {
|
||||||
|
root.beginVisualDrag(slot.appId, slot.modelData, button);
|
||||||
|
} else if (root.draggedAppId === slot.appId) {
|
||||||
|
dragProxy.Drag.drop();
|
||||||
|
root.endVisualDrag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onActiveTranslationChanged: {
|
||||||
|
if (!active || root.draggedAppId !== slot.appId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
root.dragX = root.dragStartX + activeTranslation.x;
|
||||||
|
root.dragY = root.dragStartY + activeTranslation.y;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DelegateModel {
|
||||||
|
id: visualModel
|
||||||
|
|
||||||
|
delegate: dockDelegate
|
||||||
|
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
objectProp: "appId"
|
objectProp: "appId"
|
||||||
values: TaskbarApps.apps
|
values: TaskbarApps.apps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CustomListView {
|
||||||
|
id: dockRow
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
implicitHeight: Config.dock.height
|
||||||
|
implicitWidth: root.dockContentWidth
|
||||||
|
interactive: !root.dragActive
|
||||||
|
model: visualModel
|
||||||
|
orientation: ListView.Horizontal
|
||||||
|
spacing: Appearance.padding.smaller
|
||||||
|
|
||||||
|
Behavior on implicitWidth {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
moveDisplaced: Transition {
|
||||||
|
Anim {
|
||||||
|
properties: "x,y"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: dragProxy
|
||||||
|
|
||||||
|
property string appId: root.draggedAppId
|
||||||
|
property int visualIndex: root.visualIds.indexOf(root.draggedAppId)
|
||||||
|
|
||||||
|
Drag.active: root.dragActive
|
||||||
|
Drag.hotSpot.x: width / 2
|
||||||
|
Drag.hotSpot.y: height / 2
|
||||||
|
Drag.source: dragProxy
|
||||||
|
height: root.dragHeight
|
||||||
|
visible: root.dragActive && !!root.draggedModelData
|
||||||
|
width: root.dragWidth
|
||||||
|
x: root.dragX
|
||||||
|
y: root.dragY
|
||||||
|
z: 9999
|
||||||
|
|
||||||
|
DockAppButton {
|
||||||
|
anchors.fill: parent
|
||||||
|
appListRoot: root
|
||||||
|
appToplevel: root.draggedModelData
|
||||||
|
enabled: false
|
||||||
|
visibilities: root.visibilities
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import qs.Config
|
|||||||
CustomRect {
|
CustomRect {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool appIsActive: appToplevel.toplevels.find(t => (t.activated == true)) !== undefined
|
property bool appIsActive: appToplevel?.toplevels.find(t => (t.activated == true)) !== undefined
|
||||||
property var appListRoot
|
property var appListRoot
|
||||||
property var appToplevel
|
property var appToplevel
|
||||||
property real countDotHeight: 4
|
property real countDotHeight: 4
|
||||||
property real countDotWidth: 10
|
property real countDotWidth: 10
|
||||||
property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel.appId)
|
property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel?.appId)
|
||||||
property real iconSize: implicitHeight - 20
|
property real iconSize: implicitHeight - 20
|
||||||
readonly property bool isSeparator: appToplevel.appId === "SEPARATOR"
|
readonly property bool isSeparator: appToplevel?.appId === "__dock_separator__"
|
||||||
property int lastFocused: -1
|
property int lastFocused: -1
|
||||||
required property PersistentProperties visibilities
|
required property PersistentProperties visibilities
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ CustomRect {
|
|||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
implicitSize: root.iconSize
|
implicitSize: root.iconSize
|
||||||
source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel.appId), "image-missing")
|
source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel?.appId), "image-missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
@@ -42,14 +42,14 @@ CustomRect {
|
|||||||
spacing: 3
|
spacing: 3
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: Math.min(appToplevel.toplevels.length, 3)
|
model: Math.min(appToplevel?.toplevels.length, 3)
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
color: appIsActive ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3primary
|
color: appIsActive ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3primary
|
||||||
implicitHeight: root.countDotHeight
|
implicitHeight: root.countDotHeight
|
||||||
implicitWidth: (appToplevel.toplevels.length <= 3) ? root.countDotWidth : root.countDotHeight // Circles when too many
|
implicitWidth: (appToplevel?.toplevels.length <= 3) ? root.countDotWidth : root.countDotHeight // Circles when too many
|
||||||
radius: Appearance.rounding.full
|
radius: Appearance.rounding.full
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,20 +59,20 @@ CustomRect {
|
|||||||
|
|
||||||
StateLayer {
|
StateLayer {
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (appToplevel.toplevels.length === 0) {
|
if (appToplevel?.toplevels.length === 0) {
|
||||||
root.desktopEntry?.execute();
|
root.desktopEntry?.execute();
|
||||||
root.visibilities.dock = false;
|
root.visibilities.dock = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastFocused = (lastFocused + 1) % appToplevel.toplevels.length;
|
lastFocused = (lastFocused + 1) % appToplevel?.toplevels.length;
|
||||||
appToplevel.toplevels[lastFocused].activate();
|
appToplevel?.toplevels[lastFocused].activate();
|
||||||
root.visibilities.dock = false;
|
root.visibilities.dock = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
function onApplicationsChanged() {
|
function onApplicationsChanged() {
|
||||||
root.desktopEntry = DesktopEntries.heuristicLookup(appToplevel.appId);
|
root.desktopEntry = DesktopEntries.heuristicLookup(appToplevel?.appId);
|
||||||
}
|
}
|
||||||
|
|
||||||
target: DesktopEntries
|
target: DesktopEntries
|
||||||
|
|||||||
Reference in New Issue
Block a user