mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 15:24:00 -04:00
259 lines
9.1 KiB
QML
259 lines
9.1 KiB
QML
pragma ComponentBehavior: Bound
|
||
|
||
import QtQuick
|
||
import qs.Common
|
||
import qs.Widgets
|
||
import qs.Modules.ControlCenter.Widgets
|
||
import qs.Modules.Settings.Widgets
|
||
import qs.Modules.DDash.Overview
|
||
import "../../Common/GridLayout.js" as GridUtils
|
||
import "utils/widgets.js" as WidgetUtils
|
||
import "../../DCommon/Common/FocusNavigation.js" as FocusNavigation
|
||
|
||
Item {
|
||
id: root
|
||
|
||
property string entryId: ""
|
||
property var definitions: []
|
||
property bool editMode: false
|
||
property bool live: Window.window?.visible ?? false
|
||
property int columns: width < Theme.smallBreakpoint ? 2 : 4
|
||
property real cardRadius: Theme.cornerRadiusXL
|
||
property var transientSurfaceTracker: null
|
||
readonly property var widgets: WidgetUtils.resolve(definitions, DashRegistry.widgetLayout(entryId))
|
||
readonly property var addable: definitions.filter(d => !widgets.some(w => w.id === d.id))
|
||
readonly property Item focusTarget: grid
|
||
readonly property bool blocksTabNavigation: editMode || addMenu.open || optionsSheet.shown
|
||
readonly property bool cardResizing: grid.sizePreview !== null
|
||
|
||
implicitHeight: Math.max(DashMetrics.tabMinHeight, grid.implicitHeight)
|
||
LayoutMirroring.enabled: I18n.isRtl
|
||
LayoutMirroring.childrenInherit: true
|
||
|
||
function save(items) {
|
||
DashRegistry.setWidgetLayout(entryId, items);
|
||
}
|
||
|
||
function resetWidgets() {
|
||
DashRegistry.setWidgetLayout(entryId, null);
|
||
}
|
||
|
||
function clearWidgets() {
|
||
save([]);
|
||
}
|
||
|
||
function openAddMenu(anchor) {
|
||
addMenu.openAt(anchor);
|
||
}
|
||
|
||
function focusTargets() {
|
||
const targets = [];
|
||
function collect(item) {
|
||
if (!item.visible || !item.enabled)
|
||
return;
|
||
if (item.activeFocusOnTab) {
|
||
targets.push(item);
|
||
return;
|
||
}
|
||
for (const child of item.children)
|
||
collect(child);
|
||
}
|
||
for (let i = 0; i < slots.count; i++) {
|
||
const slot = slots.itemAt(i);
|
||
if (slot)
|
||
collect(slot);
|
||
}
|
||
return targets;
|
||
}
|
||
|
||
function cycleFocus(backwards) {
|
||
return FocusNavigation.moveFocus(focusTargets(), backwards);
|
||
}
|
||
|
||
function revealFocus() {
|
||
const target = root.Window.window?.activeFocusItem;
|
||
if (!target)
|
||
return;
|
||
let ancestor = target.parent;
|
||
while (ancestor && ancestor !== root)
|
||
ancestor = ancestor.parent;
|
||
if (!ancestor)
|
||
return;
|
||
while (ancestor && !("contentY" in ancestor && "contentHeight" in ancestor))
|
||
ancestor = ancestor.parent;
|
||
if (!ancestor)
|
||
return;
|
||
if (typeof ancestor.revealRange === "function") {
|
||
const inContent = target.mapToItem(ancestor.contentItem, 0, 0);
|
||
ancestor.revealRange(inContent.y - Theme.spacingS, inContent.y + target.height + Theme.spacingS);
|
||
return;
|
||
}
|
||
const point = target.mapToItem(ancestor, 0, 0);
|
||
let offset = 0;
|
||
if (point.y < Theme.spacingS)
|
||
offset = point.y - Theme.spacingS;
|
||
if (point.y + target.height > ancestor.height - Theme.spacingS)
|
||
offset = point.y + target.height - ancestor.height + Theme.spacingS;
|
||
ancestor.contentY = Math.max(0, Math.min(ancestor.contentHeight - ancestor.height, ancestor.contentY + offset));
|
||
}
|
||
|
||
readonly property Item windowFocusItem: Window.activeFocusItem
|
||
onWindowFocusItemChanged: {
|
||
if (live)
|
||
revealFocus();
|
||
}
|
||
|
||
function specFor(id) {
|
||
return definitions.find(d => d.id === id) ?? null;
|
||
}
|
||
|
||
function updateWidget(index, changes) {
|
||
grid.commitChange(index, changes);
|
||
}
|
||
|
||
function handleKeyEvent(event) {
|
||
if (event.key !== Qt.Key_Escape || !(addMenu.open || optionsSheet.shown))
|
||
return false;
|
||
addMenu.close();
|
||
optionsSheet.dismiss();
|
||
return true;
|
||
}
|
||
|
||
onEditModeChanged: {
|
||
addMenu.close();
|
||
optionsSheet.dismiss();
|
||
}
|
||
|
||
DEditableGrid {
|
||
id: grid
|
||
width: parent.width
|
||
editMode: root.editMode
|
||
sourceItems: root.widgets
|
||
slotLayout: GridUtils.packCards(layoutItems, placementOrder, root.columns, width, DashMetrics.gridGap, DashMetrics.gridRowUnit, I18n.isRtl, id => root.specFor(id) !== null)
|
||
commitHandler: items => root.save(WidgetUtils.resolve(root.definitions, items))
|
||
|
||
function requestFocus(backwards) {
|
||
const targets = root.focusTargets();
|
||
FocusNavigation.focusItem(backwards ? targets[targets.length - 1] : targets[0], backwards);
|
||
}
|
||
|
||
Repeater {
|
||
id: slots
|
||
model: grid.tileModel
|
||
|
||
DEditableGridSlot {
|
||
id: slot
|
||
grid: grid
|
||
readonly property var widget: JSON.parse(json)
|
||
readonly property var spec: root.specFor(widget.id)
|
||
|
||
onResizeRequested: (requestedWidth, requestedHeight) => {
|
||
const cellWidth = (grid.width + DashMetrics.gridGap) / root.columns;
|
||
grid.previewSize(index, {
|
||
w: GridUtils.dimension(Math.round((requestedWidth + DashMetrics.gridGap) / cellWidth), spec.minW, spec.maxW, spec.w),
|
||
h: GridUtils.dimension(Math.round((requestedHeight + DashMetrics.gridGap) / (DashMetrics.gridRowUnit + DashMetrics.gridGap)), spec.minH, spec.maxH, spec.h)
|
||
});
|
||
}
|
||
|
||
Loader {
|
||
id: content
|
||
anchors.fill: parent
|
||
sourceComponent: slot.spec?.component ?? null
|
||
active: root.live && slot.spec !== null
|
||
enabled: !root.editMode
|
||
clip: true
|
||
onLoaded: {
|
||
if ("live" in item)
|
||
item.live = Qt.binding(() => root.live);
|
||
if ("widgetId" in item)
|
||
item.widgetId = slot.widget.id;
|
||
if ("widgetOptions" in item)
|
||
item.widgetOptions = Qt.binding(() => slot.widget);
|
||
}
|
||
}
|
||
|
||
DashEditChrome {
|
||
anchors.fill: parent
|
||
anchors.margins: -contentInset
|
||
z: 2
|
||
visible: root.editMode
|
||
enabled: slot.interactionEnabled
|
||
cornerRadius: root.cardRadius
|
||
dragging: slot.dragging
|
||
resizing: slot.resizing
|
||
hasOptions: WidgetUtils.optionSpecs(slot.spec).length > 0
|
||
sizeText: (grid.sizePreview?.index === slot.index ? grid.sizePreview.changes.w : slot.widget.w) + "×" + (grid.sizePreview?.index === slot.index ? grid.sizePreview.changes.h : slot.widget.h)
|
||
onRemoveRequested: root.save(root.widgets.filter((w, i) => i !== slot.index))
|
||
onOptionsRequested: optionsSheet.presentFor(slot.widget.id)
|
||
onResizeStarted: (px, py) => slot.beginResize(px, py)
|
||
onResizeMoved: (px, py) => slot.resizeTo(px, py)
|
||
onResizeEnded: slot.finishResize()
|
||
onResizeCanceled: slot.cancelResize()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
CcEmptyState {
|
||
anchors.centerIn: parent
|
||
visible: root.widgets.length === 0
|
||
iconName: "widgets"
|
||
title: I18n.tr("No widgets")
|
||
subtitle: I18n.tr("Add widget")
|
||
}
|
||
|
||
CcMenu {
|
||
id: addMenu
|
||
transientSurfaceTracker: root.transientSurfaceTracker
|
||
items: root.addable.map(spec => ({
|
||
label: spec.text,
|
||
iconName: spec.icon,
|
||
action: () => root.save(WidgetUtils.resolve(root.definitions, root.widgets.concat([
|
||
{
|
||
id: spec.id
|
||
}
|
||
])))
|
||
}))
|
||
}
|
||
|
||
CcSheetDialog {
|
||
id: optionsSheet
|
||
|
||
property string widgetId: ""
|
||
readonly property int widgetIndex: root.widgets.findIndex(w => w.id === widgetId)
|
||
readonly property var widget: root.widgets[widgetIndex] ?? {}
|
||
readonly property var spec: root.specFor(widgetId)
|
||
|
||
function presentFor(id) {
|
||
widgetId = id;
|
||
present();
|
||
}
|
||
|
||
backdrop: grid
|
||
panelWidth: DashMetrics.optionSheetWidth
|
||
iconName: spec?.icon ?? "tune"
|
||
title: spec?.text ?? ""
|
||
subtitle: I18n.tr("Options")
|
||
showScrollBar: false
|
||
|
||
SettingsGroup {
|
||
width: parent.width
|
||
slotColor: Theme.chipSurface
|
||
|
||
Repeater {
|
||
model: WidgetUtils.optionSpecs(optionsSheet.spec)
|
||
|
||
DashOptionRow {
|
||
required property var modelData
|
||
|
||
spec: modelData
|
||
value: optionsSheet.widget[modelData.key]
|
||
onCommitted: next => root.updateWidget(optionsSheet.widgetIndex, {
|
||
[modelData.key]: next
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|