mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-26 21:15:18 -04:00
feat(DankDash): configurable drag-n-drop tab arrangement & hide options in dms settings
This commit is contained in:
@@ -0,0 +1,577 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
focus: true
|
||||
property string highlightedId: ""
|
||||
|
||||
readonly property var __presentation: ({
|
||||
"overview": {
|
||||
"icon": "dashboard",
|
||||
"text": I18n.tr("Overview"),
|
||||
"description": I18n.tr("Clock, calendar, system info and profile")
|
||||
},
|
||||
"media": {
|
||||
"icon": "music_note",
|
||||
"text": I18n.tr("Media"),
|
||||
"description": I18n.tr("Now playing and media controls")
|
||||
},
|
||||
"wallpaper": {
|
||||
"icon": "wallpaper",
|
||||
"text": I18n.tr("Wallpapers"),
|
||||
"description": I18n.tr("Browse and set wallpapers")
|
||||
},
|
||||
"weather": {
|
||||
"icon": "wb_sunny",
|
||||
"text": I18n.tr("Weather"),
|
||||
"description": SettingsData.weatherEnabled ? I18n.tr("Forecast and conditions") : I18n.tr("Hidden until weather is enabled")
|
||||
},
|
||||
"settings": {
|
||||
"icon": "settings",
|
||||
"text": I18n.tr("Settings"),
|
||||
"description": I18n.tr("Shortcut that opens this settings window")
|
||||
}
|
||||
})
|
||||
|
||||
// Stable model: the canonical id list never reorders, so the Repeater keeps
|
||||
// its delegates alive across commits (preserving focus for keyboard reorder)
|
||||
readonly property var tabIds: SettingsData._dashTabIds
|
||||
readonly property var tabState: SettingsData.getDashTabs()
|
||||
readonly property int enabledContentCount: tabState.filter(t => t.enabled && t.id !== "settings").length
|
||||
|
||||
function presentationFor(id) {
|
||||
return __presentation[id] ?? {
|
||||
"icon": "tab",
|
||||
"text": id,
|
||||
"description": ""
|
||||
};
|
||||
}
|
||||
function isEnabled(id) {
|
||||
const t = tabState.find(t => t.id === id);
|
||||
return t ? t.enabled : false;
|
||||
}
|
||||
|
||||
readonly property real rowHeight: 70
|
||||
readonly property real rowSpacing: Theme.spacingM
|
||||
readonly property real dividerGap: 40
|
||||
|
||||
property var enabledOrder: []
|
||||
property var disabledOrder: []
|
||||
property string draggingId: ""
|
||||
property var dragStartOrder: []
|
||||
|
||||
readonly property bool hasHidden: disabledOrder.length > 0
|
||||
readonly property real dividerY: enabledOrder.length * (rowHeight + rowSpacing)
|
||||
readonly property real totalHeight: {
|
||||
const base = enabledOrder.length * (rowHeight + rowSpacing);
|
||||
if (!hasHidden)
|
||||
return Math.max(0, base - rowSpacing);
|
||||
return base + dividerGap + disabledOrder.length * (rowHeight + rowSpacing) - rowSpacing;
|
||||
}
|
||||
|
||||
function rebuild() {
|
||||
const en = [];
|
||||
const dis = [];
|
||||
for (var i = 0; i < tabState.length; i++) {
|
||||
if (tabState[i].enabled)
|
||||
en.push(tabState[i].id);
|
||||
else
|
||||
dis.push(tabState[i].id);
|
||||
}
|
||||
enabledOrder = en;
|
||||
disabledOrder = dis;
|
||||
}
|
||||
|
||||
onTabStateChanged: rebuild()
|
||||
Component.onCompleted: rebuild()
|
||||
|
||||
function slotYForId(id) {
|
||||
const p = enabledOrder.indexOf(id);
|
||||
if (p >= 0)
|
||||
return p * (rowHeight + rowSpacing);
|
||||
const k = disabledOrder.indexOf(id);
|
||||
return dividerY + dividerGap + Math.max(0, k) * (rowHeight + rowSpacing);
|
||||
}
|
||||
|
||||
function beginDrag(id) {
|
||||
draggingId = id;
|
||||
dragStartOrder = enabledOrder.slice();
|
||||
}
|
||||
|
||||
function updateDragTarget(centerY) {
|
||||
if (draggingId === "")
|
||||
return;
|
||||
var pos = Math.floor(centerY / (rowHeight + rowSpacing));
|
||||
pos = Math.max(0, Math.min(pos, enabledOrder.length - 1));
|
||||
const arr = enabledOrder.slice();
|
||||
const d = arr.indexOf(draggingId);
|
||||
if (d < 0 || d === pos)
|
||||
return;
|
||||
arr.splice(d, 1);
|
||||
arr.splice(pos, 0, draggingId);
|
||||
enabledOrder = arr;
|
||||
}
|
||||
|
||||
function commit() {
|
||||
SettingsData.setDashTabOrder(enabledOrder.concat(disabledOrder));
|
||||
}
|
||||
|
||||
function endDrag() {
|
||||
if (draggingId === "")
|
||||
return;
|
||||
const changed = JSON.stringify(enabledOrder) !== JSON.stringify(dragStartOrder);
|
||||
draggingId = "";
|
||||
if (changed)
|
||||
commit();
|
||||
}
|
||||
|
||||
function moveEnabled(id, delta) {
|
||||
const pos = enabledOrder.indexOf(id);
|
||||
const next = pos + delta;
|
||||
if (pos < 0 || next < 0 || next >= enabledOrder.length)
|
||||
return;
|
||||
const arr = enabledOrder.slice();
|
||||
arr.splice(pos, 1);
|
||||
arr.splice(next, 0, id);
|
||||
enabledOrder = arr;
|
||||
commit();
|
||||
}
|
||||
|
||||
function canHide(id) {
|
||||
return !isEnabled(id) || id === "settings" || enabledContentCount > 1;
|
||||
}
|
||||
|
||||
// Keyboard nav is handled at the tab root (not per-row activeFocusOnTab)
|
||||
Keys.onPressed: function (event) {
|
||||
const order = enabledOrder.concat(disabledOrder);
|
||||
if (order.length === 0)
|
||||
return;
|
||||
const ctrl = (event.modifiers & Qt.ControlModifier) !== 0;
|
||||
if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
|
||||
const dir = event.key === Qt.Key_Down ? 1 : -1;
|
||||
if (ctrl) {
|
||||
if (highlightedId !== "" && isEnabled(highlightedId))
|
||||
moveEnabled(highlightedId, dir);
|
||||
} else if (highlightedId === "") {
|
||||
highlightedId = dir > 0 ? order[0] : order[order.length - 1];
|
||||
} else {
|
||||
var idx = order.indexOf(highlightedId);
|
||||
idx = Math.max(0, Math.min(order.length - 1, idx + dir));
|
||||
highlightedId = order[idx];
|
||||
}
|
||||
event.accepted = true;
|
||||
} else if ((event.key === Qt.Key_Space || event.key === Qt.Key_Return) && highlightedId !== "") {
|
||||
if (canHide(highlightedId))
|
||||
SettingsData.setDashTabEnabled(highlightedId, !isEnabled(highlightedId));
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
contentHeight: mainColumn.height + Theme.spacingXL
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: mainColumn
|
||||
topPadding: 4
|
||||
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: headerContent.implicitHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
Column {
|
||||
id: headerContent
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
RowLayout {
|
||||
id: headerText
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "space_dashboard"
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Dank Dash")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
height: 1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: resetButton
|
||||
width: resetContentRow.implicitWidth + Theme.spacingM * 2
|
||||
height: 28
|
||||
radius: Theme.cornerRadius
|
||||
color: resetArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariant
|
||||
border.width: 0
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Row {
|
||||
id: resetContentRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "refresh"
|
||||
size: 14
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Reset")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: resetArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: SettingsData.resetDashTabs()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Drag to reorder or click to hide tabs. Use ↑/↓ to highlight a tab and Ctrl+↑/↓ to move it.")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: root.totalHeight + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainerHigh
|
||||
border.width: 0
|
||||
|
||||
Item {
|
||||
id: reorderArea
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingL
|
||||
height: root.totalHeight
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: hiddenDivider
|
||||
width: parent.width
|
||||
height: root.dividerGap
|
||||
y: root.dividerY + (root.rowSpacing / 2)
|
||||
opacity: root.hasHidden ? 1 : 0
|
||||
visible: opacity > 0.01
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: "visibility_off"
|
||||
size: 14
|
||||
color: Theme.outline
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Hidden")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.outline
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - x
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
opacity: 0.2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.tabIds
|
||||
|
||||
delegate: Item {
|
||||
id: rowItem
|
||||
required property int index
|
||||
required property string modelData
|
||||
|
||||
readonly property var present: root.presentationFor(modelData)
|
||||
readonly property bool isEnabled: root.isEnabled(modelData)
|
||||
readonly property bool dragging: root.draggingId === modelData
|
||||
readonly property bool highlighted: root.highlightedId === modelData
|
||||
readonly property bool canHide: root.canHide(modelData)
|
||||
|
||||
width: reorderArea.width
|
||||
height: root.rowHeight
|
||||
z: dragging ? 100 : (highlighted ? 3 : 1)
|
||||
|
||||
Binding {
|
||||
target: rowItem
|
||||
property: "y"
|
||||
value: root.slotYForId(rowItem.modelData)
|
||||
when: !rowItem.dragging
|
||||
restoreMode: Binding.RestoreNone
|
||||
}
|
||||
|
||||
onYChanged: {
|
||||
if (dragging)
|
||||
root.updateDragTarget(y + height / 2);
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
enabled: !rowItem.dragging
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
scale: rowItem.dragging ? 1.02 : 1.0
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: surface
|
||||
anchors.fill: parent
|
||||
radius: rowItem.dragging ? Theme.cornerRadius + 6 : Theme.cornerRadius
|
||||
color: {
|
||||
if (rowItem.dragging)
|
||||
return Theme.secondaryContainer;
|
||||
const base = Theme.surfaceContainer;
|
||||
return Qt.rgba(base.r, base.g, base.b, rowItem.isEnabled ? 0.7 : 0.4);
|
||||
}
|
||||
border.width: rowItem.dragging ? 2 : 1
|
||||
border.color: rowItem.dragging ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
opacity: (dragArea.containsMouse && !rowItem.dragging) ? 0.06 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: dragHandle
|
||||
name: "drag_indicator"
|
||||
size: Theme.iconSize - 4
|
||||
color: rowItem.dragging ? Theme.primary : Theme.outline
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: rowItem.isEnabled ? ((dragArea.containsMouse || rowItem.dragging || rowItem.highlighted) ? 1.0 : 0.45) : 0
|
||||
visible: opacity > 0.01
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: tabIcon
|
||||
name: rowItem.present.icon
|
||||
size: Theme.iconSize
|
||||
color: rowItem.isEnabled ? Theme.primary : Theme.outline
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM * 2 + Theme.iconSize - 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: tabIcon.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: visibilityButton.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
StyledText {
|
||||
text: rowItem.present.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: rowItem.isEnabled ? Theme.surfaceText : Theme.outline
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: rowItem.present.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: rowItem.isEnabled ? Theme.outline : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: visibilityButton
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonSize: 36
|
||||
iconName: rowItem.isEnabled ? "visibility" : "visibility_off"
|
||||
iconSize: 18
|
||||
iconColor: rowItem.isEnabled ? Theme.primary : Theme.outline
|
||||
enabled: rowItem.canHide
|
||||
onClicked: {
|
||||
root.forceActiveFocus();
|
||||
root.highlightedId = rowItem.modelData;
|
||||
SettingsData.setDashTabEnabled(rowItem.modelData, !rowItem.isEnabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -2
|
||||
radius: Theme.cornerRadius + 2
|
||||
color: "transparent"
|
||||
border.width: 2
|
||||
border.color: Theme.primary
|
||||
opacity: rowItem.highlighted && !rowItem.dragging ? 0.6 : 0
|
||||
visible: opacity > 0.01
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dragArea
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 48
|
||||
hoverEnabled: true
|
||||
enabled: rowItem.isEnabled
|
||||
cursorShape: rowItem.dragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
||||
drag.target: rowItem
|
||||
drag.axis: Drag.YAxis
|
||||
drag.minimumY: -rowItem.height
|
||||
drag.maximumY: reorderArea.height
|
||||
drag.smoothed: false
|
||||
onPressed: {
|
||||
root.forceActiveFocus();
|
||||
root.highlightedId = rowItem.modelData;
|
||||
root.beginDrag(rowItem.modelData);
|
||||
}
|
||||
onReleased: root.endDrag()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user