Files

259 lines
9.1 KiB
QML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
})
}
}
}
}
}