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 254 additions and 71 deletions
Showing only changes of commit b65117e213 - Show all commits
+4
View File
@@ -33,6 +33,10 @@ Singleton {
recentSaveCooldown.restart(); recentSaveCooldown.restart();
} }
function saveNoToast(): void {
saveTimer.restart();
}
function serializeAppearance(): var { function serializeAppearance(): var {
return { return {
rounding: { rounding: {
+58 -42
View File
@@ -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) { return [].concat(pinnedApps.map(appId => appEntryComp.createObject(null, {
if (!map.has(appId.toLowerCase())) appId,
map.set(appId.toLowerCase(), ({
pinned: true, pinned: true,
toplevels: [] toplevels: openMap.get(appId) ?? []
})); }))).concat(pinnedApps.length > 0 ? [appEntryComp.createObject(null, {
} appId: root.separatorId,
// Separator
if (pinnedApps.length > 0) {
map.set("SEPARATOR", {
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(), ({ const appId = normalizeId(toplevel.appId);
pinned: false, if (!appId)
toplevels: [] return map;
}));
map.get(toplevel.appId.toLowerCase()).toplevels.push(toplevel); map.set(appId, (map.get(appId) ?? []).concat([toplevel]));
return map;
}, new Map());
} }
var values = []; function commitVisualOrder(ids) {
const orderedIds = uniq(ids.map(normalizeId));
const separatorIndex = orderedIds.indexOf(root.separatorId);
for (const [key, value] of map) { const pinnedApps = (separatorIndex === -1 ? [] : orderedIds.slice(0, separatorIndex)).filter(id => id !== root.separatorId);
values.push(appEntryComp.createObject(null, {
appId: key,
toplevels: value.toplevels,
pinned: value.pinned
}));
}
return values; 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()
} }
} }
+173 -11
View File
@@ -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
function previewReorder(drag) {
const source = drag.source;
if (!source || !source.appId || source.appId === appId)
return;
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 appListRoot: root
appToplevel: modelData appToplevel: modelData
visibilities: root.visibilities visibilities: root.visibilities
visible: root.draggedAppId !== slot.appId
} }
Behavior on implicitWidth {
Anim { 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
}
}
} }
+10 -10
View File
@@ -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