Files
DankMaterialShell/quickshell/Modules/ControlCenter/ControlCenterContent.qml
T

705 lines
27 KiB
QML
Raw 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.Services
import qs.Modules.ControlCenter.Components
import qs.Modules.ControlCenter.Models
import qs.Modules.ControlCenter.Details
import qs.DCommon.Widgets
import qs.Widgets
import "./utils/sections.js" as Sections
import "./utils/widgets.js" as WidgetUtils
FocusScope {
id: root
required property var host
property bool live: Window.window?.visible ?? false
property bool audioRefHeld: false
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
readonly property bool pageOpen: (host.expandedSection ?? "") !== ""
readonly property real gridHeight: widgetGrid.gridHeight
// The row cap always reserves the footer so showing it in edit mode never reflows tiles.
readonly property real footerReserve: Theme.spacingS + CcMetrics.footerHeight
readonly property bool footerShown: root.host.editMode || footerItems.length > 0
readonly property real chromeHeight: footerShown ? footerReserve : 0
readonly property bool widgetSheetOpen: widgetSheetLoader.item?.active ?? false
readonly property real coveredAmount: Math.max(detailPage.opacity, widgetSheetLoader.item?.progress ?? 0)
property bool widgetSheetRequested: false
property int toplevelRevision: 0
readonly property var runningToplevels: {
toplevelRevision;
const all = CompositorService.sortedToplevels ?? [];
const current = CompositorService.filterCurrentWorkspace(all, host.triggerScreen?.name) || [];
return current.concat(all.filter(toplevel => !current.includes(toplevel)));
}
readonly property string placedWidgetIds: (SettingsData.controlCenterWidgets || []).map(w => w.id).sort().join(",")
readonly property var footerItems: (SettingsData.controlCenterWidgets || []).reduce((items, widget, index) => WidgetUtils.inFooter(widget) && WidgetUtils.isShown(widget) && widgetModel.componentForWidget(widget) ? items.concat([
{
"index": index,
"widget": widget
}
]) : items, [])
readonly property bool footerOnTop: SettingsData.controlCenterFooterPosition === "top"
readonly property var gridDragWidget: widgetGrid.draggingSourceIndex >= 0 ? (widgetGrid.sourceItems[widgetGrid.draggingSourceIndex] ?? null) : null
readonly property real targetImplicitHeight: {
const total = CcMetrics.sheetPadding * 2 + gridHeight + chromeHeight;
if (detailPage.shownSection === "")
return total;
return Math.max(total, detailPage.topInset + detailPage.minimumHeight + CcMetrics.detailDialogInset);
}
property Item detailReturnFocus: null
property var pageHistory: []
// One string so Discard and undo can compare and restore the whole session state at once.
readonly property string editState: JSON.stringify({
"widgets": SettingsData.controlCenterWidgets || [],
"columns": SettingsData.controlCenterColumns,
"footerPosition": SettingsData.controlCenterFooterPosition
})
property string editSnapshot: ""
property string undoBase: ""
property var undoStack: []
readonly property int undoLimit: 50
property bool undoBatching: false
readonly property bool editIdle: !widgetGrid.interacting && !widgetGrid.memberDragging && footer.liftedIndex < 0 && footer.resizePreview === null && !panelResizing
readonly property bool canUndo: host.editMode && undoStack.length > 0 && editIdle && !widgetSheetOpen && !pageOpen
readonly property bool panelResizing: panelResizer.resizing
readonly property real sheetContentWidth: host.sheetContentWidth ?? CcMetrics.sheetWidthFor(gridColumns)
readonly property vector4d surfaceCornerRadii: host.surfaceCornerRadii ?? Qt.vector4d(Theme.windowRadius, Theme.windowRadius, Theme.windowRadius, Theme.windowRadius)
readonly property int gridColumnCap: host.gridColumnCap ?? CcMetrics.columnCapFor((host.triggerScreen?.width ?? CcMetrics.sheetWidthDefault + Theme.spacingL * 2) - Theme.spacingL * 2)
readonly property int gridColumns: host.gridColumns ?? Math.min(CcMetrics.gridColumns, gridColumnCap)
readonly property real availableGridHeight: (host.availableHeight ?? (host.triggerScreen?.height ?? CcMetrics.fallbackScreenHeight) - CcMetrics.maxHeightInset) - CcMetrics.sheetPadding * 2 - footerReserve
readonly property DPanelResizer panelResizer: DPanelResizer {
popout: root.host
stepWidth: CcMetrics.columnWidth + CcMetrics.gridGap
widthFor: columns => CcMetrics.sheetWidthFor(columns) + root.sheetContentWidth - CcMetrics.sheetWidthFor(root.gridColumns)
currentStep: () => root.gridColumns
minStep: Math.min(CcMetrics.minimumColumns, root.gridColumnCap)
maxStep: root.gridColumnCap
onPreview: columns => CcMetrics.columnPreview = columns
onCommitted: columns => {
SettingsData.set("controlCenterColumns", columns);
CcMetrics.columnPreview = 0;
}
onCanceled: CcMetrics.columnPreview = 0
}
implicitHeight: targetImplicitHeight
focus: true
function syncAudioRef(wanted) {
if (wanted === audioRefHeld)
return;
audioRefHeld = wanted;
if (wanted) {
AudioService.addRef();
return;
}
AudioService.removeRef();
}
onLiveChanged: syncAudioRef(live)
Component.onCompleted: {
WidgetUtils.ensureEditButton();
syncAudioRef(live);
}
Component.onDestruction: syncAudioRef(false)
onPlacedWidgetIdsChanged: WidgetUtils.ensureEditButton()
function navigateTo(section) {
if (section === host.expandedSection)
return;
if (host.expandedSection)
pageHistory = pageHistory.concat([host.expandedSection]);
else if (detailPage.shownSection === "")
detailReturnFocus = root.Window.window?.activeFocusItem ?? null;
host.expandedSection = section;
}
function goBack() {
if (detailPage.dismissTransient())
return;
if (pageHistory.length > 0) {
const previous = pageHistory[pageHistory.length - 1];
pageHistory = pageHistory.slice(0, -1);
host.expandedSection = previous;
return;
}
host.collapseAll();
}
function openWidgetPage(widgetData) {
const section = Sections.sectionFor(widgetData);
if (host.expandedSection === section) {
goBack();
return;
}
navigateTo(section);
}
function showCodecSelector(device) {
presentSheet(codecSelectorLoader, device);
}
function showPortSelector(node) {
presentSheet(portSelectorLoader, node);
}
function presentSheet(loader, target) {
loader.active = true;
const sheet = loader.item;
if (!sheet)
return;
sheet.show(target);
if (!sheet.shown)
loader.active = false;
}
function releaseSheet(loader) {
if (loader.item?.shown)
return;
loader.active = false;
}
function openConfigOverlay(index, widgetData, anchor) {
if (widgetData.id === "brightnessSlider") {
openWidgetPage(widgetData);
return;
}
configOverlayLoader.active = true;
const overlay = configOverlayLoader.item;
if (!overlay)
return;
overlay.open(index, widgetData, anchor);
}
function releaseConfigOverlay() {
if (configOverlayLoader.item?.visible)
return;
configOverlayLoader.active = false;
}
function applyEditState(state) {
if (state === "")
return;
const wanted = JSON.parse(state);
if (JSON.stringify(SettingsData.controlCenterWidgets || []) !== JSON.stringify(wanted.widgets))
SettingsData.set("controlCenterWidgets", wanted.widgets);
if (SettingsData.controlCenterColumns !== wanted.columns)
SettingsData.set("controlCenterColumns", wanted.columns);
if (SettingsData.controlCenterFooterPosition !== wanted.footerPosition)
SettingsData.set("controlCenterFooterPosition", wanted.footerPosition);
}
function cancelEdit() {
const snapshot = editSnapshot;
host.editMode = false;
if (snapshot !== "")
applyEditState(snapshot);
}
function undoEdit() {
if (!canUndo)
return;
const previous = undoStack[undoStack.length - 1];
undoStack = undoStack.slice(0, -1);
undoBase = previous;
undoBatching = true;
applyEditState(previous);
undoBatching = false;
}
onEditStateChanged: {
if (!host.editMode || undoBatching || editState === undoBase)
return;
undoStack = undoStack.concat([undoBase]).slice(-undoLimit);
undoBase = editState;
}
// Keeps the intermediate states of a multi-key write out of the undo stack.
function batchEdit(change) {
const before = editState;
undoBatching = true;
change();
undoBatching = false;
if (editState === before)
return;
undoStack = undoStack.concat([before]).slice(-undoLimit);
undoBase = editState;
}
// Goes by the dragged tile, not the pointer, against the grid edge as it was at drag start so the grid growing
// under the drag cannot move it. Below the grid the edge sits at the middle of the row that opens while the
// panel has room: the upper half still drops there, past it the footer takes the tile instead of running ahead.
function footerTakesGridDrag(tile) {
if (gridDragWidget === null || gridDragWidget.id === WidgetUtils.GROUP_ID || footer.freeCells() < WidgetUtils.footerMinCells(gridDragWidget.id))
return false;
const rowUnit = widgetGrid.slotLayout.rowUnit;
const anchor = tile.y + Math.min(tile.height, rowUnit) / 2;
if (footerOnTop)
return anchor < -CcMetrics.gridGap / 2;
const newRow = CcMetrics.gridGap + rowUnit;
const room = widgetGrid.pinnedHeight + newRow <= availableGridHeight ? newRow : 0;
return anchor > widgetGrid.pinnedHeight + room / 2 + CcMetrics.gridGap / 2;
}
function groupTakesGridDrag(scenePoint) {
return WidgetUtils.canGroup(gridDragWidget) && widgetGrid.groupAt(scenePoint, widgetGrid.draggingSourceIndex) !== null;
}
// Drops commit a tick later: committing inside the release handler would destroy the dragged item mid-signal.
function dropFromGrid(index, scenePoint) {
if (!widgetGrid.heldOutside)
return false;
const savedIndex = widgetGrid.savedIndex(index);
if (footer.trashContains(scenePoint)) {
Qt.callLater(() => {
widgetModel.removeWidget(savedIndex);
widgetGrid.cancelInteraction();
});
return true;
}
const group = widgetGrid.hoverGroup !== "" ? widgetGrid.groupAt(scenePoint, index) : null;
if (group) {
const groupId = group.groupId;
const before = group.savedIndexAtSlot(group.incomingSlot);
Qt.callLater(() => {
WidgetUtils.moveToGroup(savedIndex, groupId, before);
widgetGrid.cancelInteraction();
});
return true;
}
const slot = footer.insertionAt(scenePoint);
const before = footer.savedIndexAt(slot);
const cells = Math.min(WidgetUtils.footerCells(gridDragWidget), footer.freeCells());
Qt.callLater(() => {
WidgetUtils.moveToFooter(savedIndex, before, cells, slot.end);
widgetGrid.cancelInteraction();
});
return true;
}
// Same rule as the other way round: the middle of the dragged item decides, against the grid's edge.
function gridTakesFooterDrag(visual) {
const middle = widgetGrid.mapFromItem(null, visual.x, visual.y + visual.height / 2).y - widgetGrid.contentPadding;
return footerOnTop ? middle > -CcMetrics.gridGap / 2 : middle < widgetGrid.gridHeight + CcMetrics.gridGap / 2;
}
function previewFooterDrag(index, visual, leaving) {
if (!leaving) {
widgetGrid.clearExternal();
return;
}
const widget = SettingsData.controlCenterWidgets[index];
const size = WidgetUtils.clampSize(widget, gridColumns, widgetGrid.maximumRows);
const point = widgetGrid.mapFromItem(null, visual.x, visual.y);
widgetGrid.previewExternal({
"id": widget.id,
"w": size.w,
"h": size.h
}, point.x, point.y);
}
function dropFromFooter(index, scenePoint) {
if (footer.trashContains(scenePoint)) {
Qt.callLater(() => widgetModel.removeWidget(index));
return true;
}
if (widgetGrid.externalItem) {
const placed = widgetGrid.committedItems();
const cell = placed.pop();
const widgets = WidgetUtils.placeFromFooter(widgetGrid.withHidden(placed), index, cell.col, cell.row);
Qt.callLater(() => {
WidgetUtils.setLayout(widgets);
widgetGrid.cancelInteraction();
});
return true;
}
const slot = footer.liftedSlot;
if (!slot)
return false;
const current = footer.slotOf(index);
if (slot.end === current.end && slot.at === current.at)
return false;
const before = footer.savedIndexAt(slot);
const cells = WidgetUtils.footerCells(SettingsData.controlCenterWidgets[index]);
Qt.callLater(() => WidgetUtils.moveToFooter(index, before, cells, slot.end));
return true;
}
function openWidgetSheet() {
const sheet = widgetSheetLoader.item;
if (!sheet) {
widgetSheetRequested = true;
return;
}
sheet.opened = true;
}
function closeWidgetSheet() {
if (!widgetSheetLoader.item)
return;
widgetSheetLoader.item.opened = false;
}
Keys.onEscapePressed: event => {
if (widgetSheetLoader.item?.opened) {
closeWidgetSheet();
event.accepted = true;
return;
}
if (configOverlayLoader.item?.visible) {
configOverlayLoader.item.close();
event.accepted = true;
return;
}
if (pageOpen) {
goBack();
event.accepted = true;
return;
}
if (host.editMode) {
host.editMode = false;
event.accepted = true;
return;
}
host.close();
event.accepted = true;
}
Shortcut {
sequences: ["F2", "Ctrl+E"]
enabled: root.host.shouldBeVisible && !root.host.editMode && !root.pageOpen
context: Qt.WindowShortcut
onActivated: root.host.editMode = true
}
Shortcut {
sequences: ["Ctrl+Z"]
enabled: root.host.shouldBeVisible && root.canUndo
context: Qt.WindowShortcut
onActivated: root.undoEdit()
}
readonly property string expandedSection: host.expandedSection ?? ""
readonly property bool editMode: host.editMode
onExpandedSectionChanged: {
if (expandedSection !== "")
return;
pageHistory = [];
}
onEditModeChanged: {
if (editMode) {
host.collapseAll();
editSnapshot = editState;
undoBase = editState;
} else {
panelResizer.cancel();
editSnapshot = "";
undoBase = "";
widgetSheetRequested = false;
}
undoStack = [];
if (!activeFocus)
forceActiveFocus();
}
DGridEditChrome {
id: panelChrome
property int resizeSide: 0
anchors.fill: parent
anchors.margins: -contentInset
z: 1
visible: root.host.editMode
enabled: detailPage.shownSection === "" && !root.widgetSheetOpen
edgeResize: root.panelResizing ? resizeSide < 0 : root.panelResizer.sideMovable(-1, root.gridColumns)
cornerResize: root.panelResizing ? resizeSide > 0 : root.panelResizer.sideMovable(1, root.gridColumns)
horizontalResize: true
removable: false
cornerRadius: Theme.windowRadius
cornerRadii: root.surfaceCornerRadii
buttonSize: Theme.iconSize
iconSize: PopoutMetrics.chromeIconSize
resizing: root.panelResizing
atDefault: root.gridColumns === Math.min(CcMetrics.defaultColumns, root.gridColumnCap)
sizeText: root.gridColumns + "×" + widgetGrid.slotLayout.rows
onResizeStarted: (px, py, signX) => {
resizeSide = signX;
root.panelResizer.begin(px, py, signX);
}
onResizeMoved: (px, py) => root.panelResizer.move(px, py)
onResizeEnded: root.panelResizer.end()
onResizeCanceled: root.panelResizer.cancel()
}
WidgetModel {
id: widgetModel
columns: root.gridColumns
maximumRows: widgetGrid.maximumRows
}
Rectangle {
anchors.fill: parent
topLeftRadius: root.surfaceCornerRadii.x
topRightRadius: root.surfaceCornerRadii.y
bottomRightRadius: root.surfaceCornerRadii.z
bottomLeftRadius: root.surfaceCornerRadii.w
color: Qt.rgba(0, 0, 0, Theme.scrimAlpha)
opacity: root.host.powerMenuOpen ? 1 : 0
visible: opacity > 0
z: CcMetrics.overlayZ
Behavior on opacity {
enabled: CcMetrics.animationsEnabled
NumberAnimation {
duration: CcMetrics.fadeDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveEffects
}
}
}
DFlickable {
id: contentFlickable
anchors.left: parent.left
anchors.right: parent.right
anchors.top: root.footerOnTop ? footer.bottom : parent.top
anchors.topMargin: root.footerOnTop && root.footerShown ? Theme.spacingS : 0
anchors.bottom: root.footerOnTop ? parent.bottom : footer.top
anchors.bottomMargin: !root.footerOnTop && root.footerShown ? Theme.spacingS : 0
clip: contentHeight > height
contentWidth: width
contentHeight: Math.max(height, mainColumn.implicitHeight + CcMetrics.sheetPadding)
interactive: contentHeight > height
Column {
id: mainColumn
width: root.sheetContentWidth - CcMetrics.sheetPadding * 2
x: CcMetrics.sheetPadding
y: root.footerOnTop ? 0 : CcMetrics.sheetPadding
spacing: Theme.spacingS
Item {
id: body
width: parent.width
height: root.gridHeight
opacity: CcMetrics.hideCoveredContent ? 1 - root.coveredAmount : 1
Rectangle {
readonly property bool receiving: widgetGrid.externalItem !== null || (widgetGrid.draggingSourceIndex >= 0 && !widgetGrid.heldOutside)
anchors.fill: parent
anchors.margins: -CcMetrics.dropZoneOuterInset
anchors.topMargin: root.footerOnTop && root.footerShown ? -CcMetrics.dropZoneInset : -CcMetrics.dropZoneOuterInset
anchors.bottomMargin: !root.footerOnTop && root.footerShown ? -CcMetrics.dropZoneInset : -CcMetrics.dropZoneOuterInset
radius: CcMetrics.dropZoneRadius
color: "transparent"
border.width: Theme.outlineWidth
border.color: receiving ? Theme.primary : Theme.outlineVariant
visible: root.host.editMode
Behavior on border.color {
enabled: CcMetrics.animationsEnabled
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
CcTileGrid {
id: widgetGrid
columns: root.gridColumns
availableHeight: root.availableGridHeight
width: parent.width
anchors.top: parent.top
editMode: root.host.editMode
model: widgetModel
live: root.host.shouldBeVisible && detailPage.shownSection === ""
screenName: root.host.triggerScreen?.name || ""
tapToClose: root.host.headerTogglesClose ?? false
runningToplevels: root.runningToplevels
dragsOutside: (index, scenePoint, tile) => footer.trashContains(scenePoint) || root.groupTakesGridDrag(scenePoint) || root.footerTakesGridDrag(tile)
dropHandler: (index, scenePoint) => root.dropFromGrid(index, scenePoint)
onExpandClicked: widgetData => root.openWidgetPage(widgetData)
onRemoveWidget: index => widgetModel.removeWidget(index)
onConfigRequested: (index, widgetData, anchor) => root.openConfigOverlay(index, widgetData, anchor)
onColorPickerRequested: root.host.openColorPicker()
onCloseRequested: root.host.close()
onSettingsRequested: root.host.openSettings()
onEditRequested: root.host.editMode = !root.host.editMode
onAccountsRequested: root.host.openAccounts()
onLockRequested: {
root.host.close();
root.host.lockRequested();
}
onPowerRequested: {
const loader = root.host.powerMenuModalLoader;
if (!loader)
return;
loader.active = true;
if (!loader.item)
return;
const bounds = Qt.rect(root.host.alignedX, root.host.alignedY, root.host.popupWidth, root.host.popupHeight);
loader.item.openFromControlCenter(bounds, root.host.screen);
}
}
}
}
}
CcFooter {
id: footer
x: CcMetrics.sheetPadding
y: root.footerOnTop ? CcMetrics.sheetPadding : root.height - CcMetrics.sheetPadding - height
width: root.sheetContentWidth - CcMetrics.sheetPadding * 2
height: root.footerShown ? implicitHeight : 0
visible: root.footerShown
grid: widgetGrid
editMode: root.host.editMode
onTop: root.footerOnTop
transientSurfaceTracker: root.host.transientSurfaceTracker
items: root.footerItems
gridDragging: root.gridDragWidget !== null
gridDragPoint: widgetGrid.dragScenePoint
incomingRemovable: WidgetUtils.isRemovable(root.gridDragWidget)
incoming: root.gridDragWidget !== null && widgetGrid.heldOutside && widgetGrid.hoverGroup === "" && !footer.overTrash ? Object.assign({
"cells": Math.min(WidgetUtils.footerCells(root.gridDragWidget), footer.freeCells())
}, footer.gridSlot) : null
dropHandler: (index, cells, scenePoint) => root.dropFromFooter(index, scenePoint)
leavesRow: visual => root.gridTakesFooterDrag(visual)
opacity: body.opacity
canUndo: root.canUndo
onAddWidgetRequested: root.openWidgetSheet()
onUndoRequested: root.undoEdit()
onResetRequested: root.batchEdit(() => widgetModel.resetToDefault())
onClearRequested: widgetModel.clearAll()
onMoveRequested: SettingsData.set("controlCenterFooterPosition", root.footerOnTop ? "bottom" : "top")
onRemoveRequested: index => widgetModel.removeWidget(index)
onConfigRequested: (index, widgetData, anchor) => root.openConfigOverlay(index, widgetData, anchor)
onItemMoved: (index, sceneRect, leaving) => root.previewFooterDrag(index, sceneRect, leaving)
onResized: (index, cells, fill) => WidgetUtils.setFooterSize(index, cells, fill)
onFinishRequested: root.host.editMode = false
onCancelRequested: root.cancelEdit()
}
CcDetailPage {
id: detailPage
z: 1
anchors.fill: parent
section: root.host.expandedSection ?? ""
topInset: CcMetrics.sheetPadding
cornerRadii: root.surfaceCornerRadii
coverage: Math.max(codecSelectorLoader.item?.presence ?? 0, portSelectorLoader.item?.presence ?? 0)
model: widgetModel
screenName: root.host.triggerScreen?.name || ""
screenModel: root.host.triggerScreen?.model || ""
transientSurfaceTracker: root.host.transientSurfaceTracker
onCodecSelectorRequested: device => root.showCodecSelector(device)
onPortSelectorRequested: node => root.showPortSelector(node)
runningToplevels: root.runningToplevels
onBackRequested: root.goBack()
onCollapseRequested: root.host.collapseAll()
onCloseRequested: root.host.close()
onDismissed: {
const target = root.detailReturnFocus;
root.detailReturnFocus = null;
if (target?.visible && target.enabled) {
target.forceActiveFocus();
return;
}
root.forceActiveFocus();
}
}
Loader {
id: codecSelectorLoader
anchors.fill: parent
z: CcMetrics.overlayZ
active: false
sourceComponent: BluetoothCodecSelector {
cornerRadii: root.surfaceCornerRadii
onDismissed: Qt.callLater(root.releaseSheet, codecSelectorLoader)
}
}
Loader {
id: portSelectorLoader
anchors.fill: parent
z: CcMetrics.overlayZ
active: false
sourceComponent: AudioPortSelector {
cornerRadii: root.surfaceCornerRadii
onDismissed: Qt.callLater(root.releaseSheet, portSelectorLoader)
}
}
Connections {
target: CompositorService
function onToplevelsChanged() {
root.toplevelRevision++;
}
}
Loader {
id: widgetSheetLoader
anchors.fill: parent
z: CcMetrics.overlayZ
active: root.host.editMode
asynchronous: true
onLoaded: {
if (!root.widgetSheetRequested)
return;
root.widgetSheetRequested = false;
item.opened = true;
}
sourceComponent: CcWidgetSheet {
scrimRadii: root.surfaceCornerRadii
surfaceColor: CcMetrics.dialogColor
widgets: {
const existingIds = root.placedWidgetIds.split(",");
const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets());
return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id));
}
onDismissRequested: opened = false
onChosen: widgetId => {
widgetModel.addWidget(widgetId);
opened = false;
}
}
}
Loader {
id: configOverlayLoader
active: false
sourceComponent: WidgetConfigOverlay {
transientSurfaceTracker: root.host.transientSurfaceTracker
onVisibleChanged: {
if (visible)
return;
Qt.callLater(root.releaseConfigOverlay);
}
}
}
}