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;
|
||||
}
|
||||
|
||||
// ── Pinned apps: ids kept at the top of the list. Stored as
|
||||
// one comma-joined string — .desktop ids never contain
|
||||
// commas, and a plain string is what JsonAdapter is
|
||||
// guaranteed to round-trip.
|
||||
// ── Pinned apps: a 3-up tile grid above the results, in the
|
||||
// order the ids are stored. Stored as one comma-joined
|
||||
// string — .desktop ids never contain commas, and a plain
|
||||
// string is what JsonAdapter is guaranteed to round-trip.
|
||||
FileView {
|
||||
id: pinFile
|
||||
path: Quickshell.env("HOME") + "/.local/state/quickshell-launcher-pins.json"
|
||||
|
|
@ -1569,21 +1569,51 @@ in
|
|||
refilter();
|
||||
}
|
||||
|
||||
// Drops ids whose .desktop file is gone (uninstalled app).
|
||||
function pinnedApps() {
|
||||
// Backing model for the tile grid. Drops ids whose .desktop
|
||||
// file is gone (app uninstalled since it was pinned).
|
||||
ListModel { id: pinModel }
|
||||
|
||||
function syncPins() {
|
||||
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() {
|
||||
let q = searchInput.text.toLowerCase().trim();
|
||||
calcResult = calc(searchInput.text);
|
||||
menuAppId = "";
|
||||
syncPins();
|
||||
let apps = DesktopEntries.applications.values.filter(a => !a.noDisplay);
|
||||
let list;
|
||||
if (q === "") {
|
||||
if (activeCat === "") {
|
||||
list = pinnedApps();
|
||||
list = [];
|
||||
} else {
|
||||
list = apps.filter(a => inCat(a, activeCat));
|
||||
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.
|
||||
HoverRow {
|
||||
width: parent.width
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue