quickshell: pins as a 3-up tile grid with drag reorder
Pinned apps move out of the results list into their own tile grid, three across, wrapping to further rows. Drag a tile onto another slot to reorder; the id list is rewritten on release rather than mid-drag, so nothing shifts under the cursor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c212b06d15
commit
a15e4d5a32
1 changed files with 168 additions and 8 deletions
|
|
@ -1537,10 +1537,10 @@ in
|
||||||
open = false;
|
open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Pinned apps: ids kept at the top of the list. Stored as
|
// ── Pinned apps: a 3-up tile grid above the results, in the
|
||||||
// one comma-joined string — .desktop ids never contain
|
// order the ids are stored. Stored as one comma-joined
|
||||||
// commas, and a plain string is what JsonAdapter is
|
// string — .desktop ids never contain commas, and a plain
|
||||||
// guaranteed to round-trip.
|
// string is what JsonAdapter is guaranteed to round-trip.
|
||||||
FileView {
|
FileView {
|
||||||
id: pinFile
|
id: pinFile
|
||||||
path: Quickshell.env("HOME") + "/.local/state/quickshell-launcher-pins.json"
|
path: Quickshell.env("HOME") + "/.local/state/quickshell-launcher-pins.json"
|
||||||
|
|
@ -1569,21 +1569,51 @@ in
|
||||||
refilter();
|
refilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drops ids whose .desktop file is gone (uninstalled app).
|
// Backing model for the tile grid. Drops ids whose .desktop
|
||||||
function pinnedApps() {
|
// file is gone (app uninstalled since it was pinned).
|
||||||
|
ListModel { id: pinModel }
|
||||||
|
|
||||||
|
function syncPins() {
|
||||||
const all = DesktopEntries.applications.values;
|
const all = DesktopEntries.applications.values;
|
||||||
return pinIds().map(id => all.find(a => a.id === id)).filter(a => a !== undefined);
|
pinModel.clear();
|
||||||
|
const ids = pinIds();
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
const a = all.find(x => x.id === ids[i]);
|
||||||
|
if (a === undefined) continue;
|
||||||
|
pinModel.append({
|
||||||
|
appId: a.id,
|
||||||
|
name: a.name,
|
||||||
|
icon: Quickshell.iconPath(a.icon, true) || ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reorder from a drag. Rewrites the whole id list from the
|
||||||
|
// model, so a pin whose .desktop vanished is pruned here too.
|
||||||
|
function movePin(from, to) {
|
||||||
|
if (to === from || from < 0 || to < 0
|
||||||
|
|| from >= pinModel.count || to >= pinModel.count) return;
|
||||||
|
pinModel.move(from, to, 1);
|
||||||
|
let ids = [];
|
||||||
|
for (let i = 0; i < pinModel.count; i++) ids.push(pinModel.get(i).appId);
|
||||||
|
pinState.ids = ids.join(",");
|
||||||
|
pinFile.writeAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchPin(id) {
|
||||||
|
activate(DesktopEntries.applications.values.find(a => a.id === id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function refilter() {
|
function refilter() {
|
||||||
let q = searchInput.text.toLowerCase().trim();
|
let q = searchInput.text.toLowerCase().trim();
|
||||||
calcResult = calc(searchInput.text);
|
calcResult = calc(searchInput.text);
|
||||||
menuAppId = "";
|
menuAppId = "";
|
||||||
|
syncPins();
|
||||||
let apps = DesktopEntries.applications.values.filter(a => !a.noDisplay);
|
let apps = DesktopEntries.applications.values.filter(a => !a.noDisplay);
|
||||||
let list;
|
let list;
|
||||||
if (q === "") {
|
if (q === "") {
|
||||||
if (activeCat === "") {
|
if (activeCat === "") {
|
||||||
list = pinnedApps();
|
list = [];
|
||||||
} else {
|
} else {
|
||||||
list = apps.filter(a => inCat(a, activeCat));
|
list = apps.filter(a => inCat(a, activeCat));
|
||||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
@ -1799,6 +1829,136 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pinned tiles, three across. Drag a tile onto
|
||||||
|
// another slot to reorder — the id list is rewritten
|
||||||
|
// on release, not mid-drag, so nothing jitters under
|
||||||
|
// the cursor while it moves.
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: pinGrid.height + 2 * Theme.cardPad
|
||||||
|
radius: Theme.radiusCard
|
||||||
|
color: Theme.cardBg
|
||||||
|
visible: pinModel.count > 0 && searchInput.text === ""
|
||||||
|
&& launcherPanel.activeCat === ""
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
id: pinGrid
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: Theme.cardPad
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: parent.width - 2 * Theme.cardPad
|
||||||
|
columns: 3
|
||||||
|
columnSpacing: 8
|
||||||
|
rowSpacing: 8
|
||||||
|
|
||||||
|
readonly property real cellW: (width - (columns - 1) * columnSpacing) / columns
|
||||||
|
readonly property real cellH: 66
|
||||||
|
|
||||||
|
// Which slot a dragged tile's centre is over.
|
||||||
|
function slotAt(px, py) {
|
||||||
|
const c = Math.max(0, Math.min(columns - 1,
|
||||||
|
Math.floor(px / (cellW + columnSpacing))));
|
||||||
|
const r = Math.max(0, Math.floor(py / (cellH + rowSpacing)));
|
||||||
|
return Math.max(0, Math.min(pinModel.count - 1, r * columns + c));
|
||||||
|
}
|
||||||
|
|
||||||
|
move: Transition {
|
||||||
|
NumberAnimation { properties: "x,y"; duration: Theme.animContent; easing.type: Easing.OutExpo }
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: pinModel
|
||||||
|
|
||||||
|
// Cell stays where the Grid puts it; the
|
||||||
|
// tile inside is what the drag moves.
|
||||||
|
Item {
|
||||||
|
id: pinCell
|
||||||
|
required property string appId
|
||||||
|
required property string name
|
||||||
|
required property string icon
|
||||||
|
required property int index
|
||||||
|
width: pinGrid.cellW
|
||||||
|
height: pinGrid.cellH
|
||||||
|
z: pinMa.dragging ? 10 : 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: pinTile
|
||||||
|
width: pinCell.width
|
||||||
|
height: pinCell.height
|
||||||
|
radius: Theme.radiusTiny
|
||||||
|
color: pinMa.containsMouse || pinMa.dragging ? Theme.base02 : Theme.base02t
|
||||||
|
Behavior on color { ColorAnimation { duration: Theme.animFade } }
|
||||||
|
// Snap home; a reorder has already
|
||||||
|
// moved the cell out from under it.
|
||||||
|
Behavior on x { enabled: !pinMa.dragging; NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutExpo } }
|
||||||
|
Behavior on y { enabled: !pinMa.dragging; NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutExpo } }
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
visible: source != ""
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
sourceSize.width: 28
|
||||||
|
sourceSize.height: 28
|
||||||
|
source: pinCell.icon
|
||||||
|
}
|
||||||
|
SText {
|
||||||
|
width: pinCell.width - 12
|
||||||
|
text: pinCell.name
|
||||||
|
font.pixelSize: 11
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: pinMa
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
drag.target: pinTile
|
||||||
|
drag.threshold: 6
|
||||||
|
|
||||||
|
// Latched for the whole press so
|
||||||
|
// the click that ends a drag can
|
||||||
|
// be told apart from a real click.
|
||||||
|
property bool dragging: false
|
||||||
|
drag.onActiveChanged: if (drag.active) dragging = true
|
||||||
|
|
||||||
|
onPressed: dragging = false
|
||||||
|
onReleased: {
|
||||||
|
if (dragging) {
|
||||||
|
launcherPanel.movePin(pinCell.index, pinGrid.slotAt(
|
||||||
|
pinCell.x + pinTile.x + pinTile.width / 2,
|
||||||
|
pinCell.y + pinTile.y + pinTile.height / 2));
|
||||||
|
dragging = false;
|
||||||
|
}
|
||||||
|
pinTile.x = 0;
|
||||||
|
pinTile.y = 0;
|
||||||
|
}
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (dragging) return;
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
const p = mapToItem(launcherPanel, mouse.x, mouse.y);
|
||||||
|
launcherPanel.menuX = p.x;
|
||||||
|
launcherPanel.menuY = p.y;
|
||||||
|
launcherPanel.menuAppId = pinCell.appId;
|
||||||
|
} else {
|
||||||
|
launcherPanel.launchPin(pinCell.appId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Category-browse breadcrumb: back to the grid.
|
// Category-browse breadcrumb: back to the grid.
|
||||||
HoverRow {
|
HoverRow {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue