feat(control center): remaster attempt #23 - new quick tiles widget option for smaller drag & drop targets

This commit is contained in:
purian23
2026-10-09 21:32:57 -04:00
parent 26301462b7
commit 3aa8cd91e8
12 changed files with 513 additions and 22 deletions
@@ -53,6 +53,7 @@ Singleton {
readonly property real iconBoxActiveRadius: Theme.cornerRadiusL
readonly property real iconBoxIconSize: Theme.iconSize * iconScale
readonly property real tallMeterThickness: 28
readonly property real pillMeterThickness: Math.round(18 * iconScale)
readonly property real tileTextGap: Theme.spacingM
readonly property real footerHeight: iconBoxSize
readonly property real footerGap: Theme.spacingS
@@ -120,6 +121,10 @@ Singleton {
readonly property color tileInactiveContent: Theme.surfaceText
readonly property color tileInactiveSubtitle: Theme.surfaceVariantText
readonly property color tileInactiveIcon: Theme.primary
readonly property color bareActiveIcon: Theme.primary
readonly property real quickBarePadding: Theme.spacingL
readonly property real quickBareWidth: iconBoxIconSize + quickBarePadding
readonly property real quickTileGap: Theme.spacingXS
readonly property bool animationsEnabled: !SettingsData.reduceMotion && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
@@ -33,9 +33,11 @@ DEditableGrid {
readonly property real gridHeight: layoutHeight
readonly property real cellWidth: (width + CcMetrics.gridGap) / columns
readonly property CcTileSlot draggingSlot: tileRepeater.itemAt(draggingSourceIndex) as CcTileSlot
readonly property string hoverGroup: heldOutside && draggingSourceIndex >= 0 ? (groupAt(dragScenePoint, draggingSourceIndex)?.groupId ?? "") : ""
readonly property var savedWidgets: SettingsData.controlCenterWidgets || []
readonly property var shownIndices: savedWidgets.reduce((indices, widget, i) => !WidgetUtils.inFooter(widget) && WidgetUtils.isShown(widget) && model?.componentForWidget(widget) ? indices.concat([i]) : indices, [])
readonly property var groupIds: WidgetUtils.groupIds(savedWidgets)
readonly property var shownIndices: savedWidgets.reduce((indices, widget, i) => !WidgetUtils.inFooter(widget) && !WidgetUtils.inGroup(widget, groupIds) && WidgetUtils.isShown(widget) && model?.componentForWidget(widget) ? indices.concat([i]) : indices, [])
sourceItems: shownIndices.map(i => Object.assign({}, savedWidgets[i], sizeWithHiddenTwin(i)))
slotLayout: GridUtils.packCards(clamped(layoutItems), placementOrder, columns, width, CcMetrics.gridGap, cellWidth - CcMetrics.gridGap, I18n.isRtl, null, CcMetrics.gridStep, true)
@@ -68,6 +70,17 @@ DEditableGrid {
return size;
}
function groupAt(scenePoint, excludeIndex) {
for (let i = 0; i < tileRepeater.count; i++) {
const slot = tileRepeater.itemAt(i) as CcTileSlot;
if (i === excludeIndex || slot?.widgetData.id !== WidgetUtils.GROUP_ID)
continue;
if (slot.contains(slot.mapFromItem(null, scenePoint.x, scenePoint.y)))
return slot.tileItem;
}
return null;
}
function savedIndex(index) {
return shownIndices[index] ?? -1;
}
@@ -11,6 +11,7 @@ Loader {
property bool compact: false
property bool small: false
property bool docked: false
property bool bare: false
sourceComponent: grid.model ? grid.model.componentForWidget(widgetData) : null
@@ -84,6 +85,13 @@ Loader {
when: root.item !== null && "docked" in root.item
}
Binding {
target: root.item
property: "bare"
value: root.bare
when: root.item !== null && "bare" in root.item
}
Connections {
target: root.item
ignoreUnknownSignals: true
@@ -20,6 +20,7 @@ DEditableGridSlot {
readonly property real smallSpanLimit: (1 + CcMetrics.smallRowFraction) / 2
passthrough: tileItem?.passthrough ?? null
z: dragging || resizing || (tileItem?.memberDragging ?? false) ? 1 : 0
function reanchor(small) {
const shift = ((small ? CcMetrics.smallRowFraction : 1) - smallSpanLimit) * grid.cellWidth;
@@ -0,0 +1,273 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Modules.ControlCenter
import qs.DCommon.Widgets
import "../utils/widgets.js" as WidgetUtils
Item {
id: root
property var widgetData: ({})
property var widgetDef: null
property var host: null
property bool live: true
property bool interactive: true
property real columns: 4
property real rows: 1
property bool compact: false
readonly property string groupId: widgetData?.instanceId ?? ""
// Not `bare`: the grid's tile loader writes that name into any tile that has it.
readonly property bool membersBare: widgetData?.background === false
readonly property real gap: membersBare ? 0 : CcMetrics.quickTileGap
readonly property bool editMode: host?.editMode ?? false
readonly property var members: WidgetUtils.groupMembers(SettingsData.controlCenterWidgets || [], groupId).filter(member => !!host?.model?.componentForWidget(member.widget))
readonly property bool receiving: groupId !== "" && host?.hoverGroup === groupId
// Members keep their own input in edit mode so they can be dragged out instead of moving the group.
readonly property Item passthrough: memberRow
readonly property real bodyRadius: Theme.fullRadius(width, Math.min(height, CcMetrics.tileHeight))
readonly property bool memberDragging: liftedIndex >= 0
property int liftedIndex: -1
property real liftedWidth: 0
property point liftPoint: Qt.point(0, 0)
property int dropSlot: -1
readonly property int incomingSlot: receiving ? slotAt(mapFromItem(null, host.dragScenePoint.x, host.dragScenePoint.y).x) : -1
readonly property int gapSlot: liftedIndex >= 0 ? dropSlot : incomingSlot
readonly property real gapWidth: liftedIndex >= 0 ? liftedWidth : (membersBare ? CcMetrics.quickBareWidth : CcMetrics.iconBoxSize)
width: parent?.width ?? 0
height: parent?.height ?? 0
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
function others() {
const items = [];
for (let i = 0; i < memberRepeater.count; i++) {
const item = memberRepeater.itemAt(i);
if (item && item.modelData.index !== liftedIndex)
items.push(item);
}
return items;
}
// Measured against the row as it sits without a gap, so opening the gap cannot move the target.
function slotAt(x) {
const items = others();
const total = items.reduce((sum, item) => sum + item.ownWidth + gap, 0) - gap;
const along = I18n.isRtl ? width - x : x;
let start = (width - total) / 2;
for (let k = 0; k < items.length; k++) {
if (along < start + items[k].ownWidth / 2)
return k;
start += items[k].ownWidth + gap;
}
return items.length;
}
function savedIndexAtSlot(slot) {
return others()[slot]?.modelData.index ?? -1;
}
function lift(member, scenePoint) {
liftedWidth = member.ownWidth;
liftedIndex = member.modelData.index;
moveLifted(member, scenePoint);
}
function moveLifted(member, scenePoint) {
const point = mapFromItem(null, scenePoint.x, scenePoint.y);
liftPoint = Qt.point(point.x - member.grabOffset.x, point.y - member.grabOffset.y);
if (contains(point)) {
dropSlot = slotAt(point.x);
host.clearExternal();
return;
}
dropSlot = -1;
const size = WidgetUtils.clampSize(member.modelData.widget, host.columns, host.maximumRows);
const gridPoint = host.mapFromItem(null, scenePoint.x, scenePoint.y);
host.previewExternal({
"id": member.modelData.widget.id,
"w": size.w,
"h": size.h
}, gridPoint.x - host.cellWidth * size.w / 2, gridPoint.y - host.slotLayout.rowUnit * size.h / 2);
}
function drop(member) {
const savedIndex = member.modelData.index;
const slot = dropSlot;
const before = savedIndexAtSlot(slot);
const position = members.findIndex(other => other.index === savedIndex);
const unchanged = before === (members[position + 1]?.index ?? -1);
liftedIndex = -1;
dropSlot = -1;
if (slot >= 0) {
host.clearExternal();
if (!unchanged)
Qt.callLater(() => WidgetUtils.moveToGroup(savedIndex, groupId, before));
return;
}
if (!host.externalItem)
return;
const placed = host.committedItems();
const cell = placed.pop();
const widgets = WidgetUtils.placeFromGroup(host.withHidden(placed), savedIndex, cell.col, cell.row);
Qt.callLater(() => {
WidgetUtils.setLayout(widgets);
host.cancelInteraction();
});
}
Rectangle {
anchors.fill: parent
radius: root.bodyRadius
color: root.receiving ? Theme.withAlpha(Theme.primary, Theme.stateLayerDrag) : "transparent"
visible: root.editMode
}
StyledText {
anchors.fill: parent
anchors.margins: Theme.spacingS
text: I18n.tr("Drag small tiles in to group them", "control center quick tiles widget description")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
elide: Text.ElideRight
visible: root.editMode && root.members.length === 0 && root.gapSlot < 0
}
Row {
id: memberRow
// Leaves the bottom corners to the edit chrome's resize grip.
readonly property real cornerSize: Theme.minimumTouchTargetSize / 2
// Every member carries its trailing gap, which sits on the far side in RTL.
x: (root.width - Math.max(0, implicitWidth - root.gap)) / 2 - (I18n.isRtl ? root.gap : 0)
anchors.verticalCenter: parent.verticalCenter
height: CcMetrics.iconBoxSize
containmentMask: QtObject {
function contains(point: point): bool {
if (point.x < 0 || point.y < 0 || point.x >= memberRow.width || point.y >= memberRow.height)
return false;
const local = memberRow.mapToItem(root, point.x, point.y);
return local.y < root.height - memberRow.cornerSize || (local.x >= memberRow.cornerSize && local.x < root.width - memberRow.cornerSize);
}
}
Repeater {
id: memberRepeater
model: root.members
Item {
id: member
required property var modelData
required property int index
readonly property bool lifted: root.liftedIndex === modelData.index
readonly property int position: index - (root.liftedIndex >= 0 && index > root.members.findIndex(other => other.index === root.liftedIndex) ? 1 : 0)
readonly property bool gapBefore: !lifted && root.gapSlot === position
readonly property real ownWidth: loader.item?.quickWidth ?? CcMetrics.iconBoxSize
property point grabOffset: Qt.point(0, 0)
width: (gapBefore ? root.gapWidth + root.gap : 0) + (lifted ? 0 : ownWidth + root.gap)
height: memberRow.height
z: lifted ? 1 : 0
Behavior on width {
enabled: CcMetrics.animationsEnabled && (root.memberDragging || root.receiving)
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
Rectangle {
x: I18n.isRtl ? member.ownWidth + root.gap * 2 : 0
width: root.gapWidth
height: parent.height
radius: Theme.fullRadius(width, height)
color: Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
visible: member.gapBefore
}
Item {
id: body
x: {
if (member.lifted)
return root.liftPoint.x - memberRow.x - member.x;
if (I18n.isRtl)
return root.gap;
return member.gapBefore ? root.gapWidth + root.gap : 0;
}
y: member.lifted ? root.liftPoint.y - memberRow.y : 0
width: member.ownWidth
height: memberRow.height
Behavior on x {
enabled: CcMetrics.animationsEnabled && !member.lifted
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
}
}
CcTileLoader {
id: loader
anchors.fill: parent
grid: root.host
widgetData: member.modelData.widget
savedIndex: member.modelData.index
compact: true
small: true
bare: root.membersBare
}
DragHandler {
id: drag
target: null
enabled: root.editMode
cursorShape: Qt.ClosedHandCursor
onActiveChanged: {
if (active) {
member.grabOffset = centroid.pressPosition;
root.lift(member, centroid.scenePosition);
return;
}
if (member.lifted)
root.drop(member);
}
onCentroidChanged: {
if (active)
root.moveLifted(member, centroid.scenePosition);
}
}
}
}
}
Item {
width: root.gapSlot >= 0 && root.gapSlot === root.members.length - (root.liftedIndex >= 0 ? 1 : 0) ? root.gapWidth + root.gap : 0
height: memberRow.height
Rectangle {
x: I18n.isRtl ? root.gap : 0
width: root.gapWidth
height: parent.height
radius: Theme.fullRadius(width, height)
color: Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
visible: parent.width > 0
}
}
}
}
@@ -31,6 +31,15 @@ Item {
readonly property bool isDisk: widgetId === "diskUsage"
readonly property bool isIdleInhibitor: widgetId === "idleInhibitor"
readonly property bool isUser: widgetId === "user"
readonly property bool isGroup: widgetId === WidgetUtils.GROUP_ID
// Grouped tiles have no menu of their own, so the group's menu styles its battery.
readonly property int batteryIndex: {
if (widgetId === "battery")
return widgetIndex;
if (!isGroup)
return -1;
return WidgetUtils.groupMembers(SettingsData.controlCenterWidgets || [], widgetData.instanceId).find(member => member.widget.id === "battery")?.index ?? -1;
}
visible: widgetIndex >= 0 || contextMenu.renderActive
@@ -101,12 +110,32 @@ Item {
}
CcToggleRow {
visible: root.isUser
visible: root.isUser || root.isGroup
text: I18n.tr("Background")
checked: root.widgetData?.background === true
checked: root.isUser ? root.widgetData?.background === true : root.widgetData?.background !== false
onToggled: checked => root.persistOption("background", checked)
}
CcListRow {
visible: root.batteryIndex >= 0
iconName: "battery_std"
title: I18n.tr("Battery style")
body: DDropdown {
readonly property var labels: [I18n.tr("Icon", "battery widget: system battery glyph"), I18n.tr("Solid", "island settings: filled battery meter style"), I18n.tr("Outline", "island settings: outlined battery meter style"), I18n.tr("Circle", "island settings: circular battery meter style"), I18n.tr("Duo", "battery meter style: open battery arc around the network glyph")]
compactMode: true
dropdownWidth: parent.width
transientSurfaceTracker: contextMenu.transientSurfaceTracker
currentValue: labels[Math.max(0, WidgetUtils.BATTERY_STYLES.indexOf(SettingsData.controlCenterWidgets[root.batteryIndex]?.batteryStyle ?? "icon"))]
options: labels
onValueChanged: value => {
const style = WidgetUtils.BATTERY_STYLES[labels.indexOf(value)];
if (style)
WidgetUtils.setOption(root.batteryIndex, "batteryStyle", style);
}
}
}
Repeater {
model: root.isUser ? DashRegistry.sheetOptionSpecs("user") : []
@@ -24,7 +24,10 @@ FocusScope {
readonly property bool pageOpen: (host.expandedSection ?? "") !== ""
readonly property real gridHeight: widgetGrid.gridHeight
readonly property real chromeHeight: Theme.spacingS + footer.height
// 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
@@ -58,7 +61,7 @@ FocusScope {
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 - chromeHeight
readonly property real availableGridHeight: (host.availableHeight ?? (host.triggerScreen?.height ?? CcMetrics.fallbackScreenHeight) - CcMetrics.maxHeightInset) - CcMetrics.sheetPadding * 2 - footerReserve
readonly property vector4d chromeRoom: host.chromeRoom ?? Qt.vector4d(Infinity, Infinity, Infinity, Infinity)
readonly property DPanelResizer panelResizer: DPanelResizer {
popout: root.host
@@ -187,7 +190,7 @@ FocusScope {
// was at drag start, which the grid growing under the drag cannot move. Below the grid that edge sits one
// row lower while the panel has room to grow, so a tile can still be dropped into a new bottom row.
function footerTakesGridDrag(tile) {
if (gridDragWidget === null || footer.freeCells() < WidgetUtils.footerMinCells(gridDragWidget.id))
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;
@@ -198,6 +201,10 @@ FocusScope {
return anchor > widgetGrid.pinnedHeight + room + 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)
@@ -210,6 +217,16 @@ FocusScope {
});
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());
@@ -408,9 +425,9 @@ FocusScope {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: root.footerOnTop ? footer.bottom : parent.top
anchors.topMargin: root.footerOnTop ? Theme.spacingS : 0
anchors.topMargin: root.footerOnTop && root.footerShown ? Theme.spacingS : 0
anchors.bottom: root.footerOnTop ? parent.bottom : footer.top
anchors.bottomMargin: root.footerOnTop ? 0 : Theme.spacingS
anchors.bottomMargin: !root.footerOnTop && root.footerShown ? Theme.spacingS : 0
clip: contentHeight > height
contentWidth: width
contentHeight: Math.max(height, mainColumn.implicitHeight + CcMetrics.sheetPadding)
@@ -444,7 +461,7 @@ FocusScope {
screenName: root.host.triggerScreen?.name || ""
tapToClose: root.host.headerTogglesClose ?? false
runningToplevels: root.runningToplevels
dragsOutside: (index, scenePoint, tile) => footer.trashContains(scenePoint) || root.footerTakesGridDrag(tile)
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)
@@ -479,6 +496,8 @@ FocusScope {
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
@@ -487,7 +506,7 @@ FocusScope {
gridDragging: root.gridDragWidget !== null
gridDragPoint: widgetGrid.dragScenePoint
incomingRemovable: WidgetUtils.isRemovable(root.gridDragWidget)
incoming: root.gridDragWidget !== null && widgetGrid.heldOutside && !footer.overTrash ? Object.assign({
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)
@@ -5,6 +5,7 @@ import qs.Common
import qs.Services
import qs.Modules.ControlCenter
import qs.Modules.ControlCenter.BuiltinPlugins
import qs.Modules.ControlCenter.Components
import qs.Modules.ControlCenter.Widgets
import "../utils/widgets.js" as WidgetUtils
@@ -113,6 +114,9 @@ QtObject {
readonly property Component runningAppsTile: Component {
RunningAppsTile {}
}
readonly property Component quickTilesGroup: Component {
QuickTilesGroup {}
}
readonly property Component pluginTile: Component {
PluginTile {}
}
@@ -175,6 +179,8 @@ QtObject {
return actionTile;
case "runningApps":
return runningAppsTile;
case "quickTiles":
return quickTilesGroup;
default:
return null;
}
@@ -247,6 +253,16 @@ QtObject {
"category": "system",
"enabled": true
},
{
"id": "quickTiles",
"text": I18n.tr("Quick tiles", "control center widget that holds small tiles in one row"),
"description": I18n.tr("Drag small tiles in to group them", "control center quick tiles widget description"),
"icon": "view_module",
"type": "action",
"category": "system",
"enabled": true,
"allowMultiple": true
},
{
"id": "nightMode",
"text": I18n.tr("Night mode"),
@@ -11,6 +11,9 @@ CcTile {
property bool batteryRefHeld: false
readonly property bool available: BatteryService.batteryAvailable
readonly property bool profileMode: !available && PowerProfileWatcher.available
readonly property string meterStyle: widgetData?.batteryStyle ?? "icon"
readonly property bool showsMeter: available && meterStyle !== "icon" && (small || compact) && !docked
property real meterWidth: 0
iconName: profileMode ? Theme.getPowerProfileIcon(PowerProfileWatcher.currentProfile) : BatteryService.getBatteryIcon()
title: {
@@ -45,9 +48,35 @@ CcTile {
onLiveChanged: syncBatteryRef(live)
Component.onCompleted: syncBatteryRef(live)
Component.onDestruction: syncBatteryRef(false)
active: available && (BatteryService.isCharging || BatteryService.isPluggedIn)
// The meter carries its own charging bolt and would vanish into an active body.
active: available && (BatteryService.isCharging || BatteryService.isPluggedIn) && !showsMeter
bodyContent: showsMeter ? meterPill : null
quickContentWidth: showsMeter ? meterWidth : 0
opensPage: !profileMode
showExpand: profileMode
Component {
id: meterPill
Item {
BatteryMeter {
id: meter
anchors.centerIn: parent
meterStyle: root.meterStyle
levelColors: true
thickness: CcMetrics.pillMeterThickness
scale: Math.min(1, (parent.width - Theme.spacingXS * 2) / Math.max(1, implicitWidth))
}
Binding {
target: root
property: "meterWidth"
value: meter.implicitWidth
}
}
}
tallContent: Component {
Item {
BatteryMeter {
@@ -24,6 +24,10 @@ Item {
property bool small: false
property bool docked: false
property string dockedText: title
// Set by a background-less Quick tiles group.
property bool bare: false
property real quickContentWidth: 0
readonly property real quickWidth: Math.max(bare ? CcMetrics.quickBareWidth : CcMetrics.iconBoxSize, quickContentWidth + (bare ? CcMetrics.quickBarePadding : Theme.spacingM * 2))
readonly property real bodyInset: small ? Math.max(0, (height - CcMetrics.iconBoxSize) / 2) : 0
readonly property real bodyHeight: height - bodyInset * 2
property bool toggle: !opensPage
@@ -69,8 +73,10 @@ Item {
return tall ? Math.min(CcMetrics.tallTileRadius, width / 2, height / 2) : Theme.fullRadius(width, bodyHeight);
}
property bool acceptsInput: interactive && enabled
readonly property bool bodyActive: showsActive && !hasIconBox
readonly property bool bodyActive: showsActive && !hasIconBox && !bare
readonly property color bodyColor: {
if (bare)
return "transparent";
if (!available)
return Theme.onSurface_12;
return bodyActive ? CcMetrics.tileActiveColor : CcMetrics.tileInactiveColor;
@@ -90,6 +96,11 @@ Item {
return Theme.onSurface_38;
if (hasIconBox)
return showsActive ? CcMetrics.tileActiveContent : CcMetrics.tileInactiveContent;
// Without a body, toggles show their state in the icon tint alone.
if (bare && showsActive)
return CcMetrics.bareActiveIcon;
if (bare && toggle)
return CcMetrics.tileInactiveContent;
return bodyActive ? CcMetrics.tileActiveContent : root.restIconColor;
}
readonly property color iconBoxColor: {
@@ -177,7 +188,7 @@ Item {
anchors.bottomMargin: root.bodyInset
radius: root.bodyRadius
color: root.bodyColor
border.width: Theme.layerOutlineWidth
border.width: root.bare ? 0 : Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Behavior on color {
@@ -4,7 +4,9 @@
.import "../../../Common/GridLayout.js" as GridLayout
.import "../../../Common/settings/SettingsSpec.js" as Spec
var OPTION_IDS = ["diskUsage", "brightnessSlider", "idleInhibitor", "user"];
var OPTION_IDS = ["diskUsage", "brightnessSlider", "idleInhibitor", "user", "battery", "quickTiles"];
var GROUP_ID = "quickTiles";
var BATTERY_STYLES = ["icon", "solid", "outline", "ring", "duo"];
var ACTION_IDS = ["settings", "lock", "power"];
var EDIT_ID = "edit";
var BUTTON_IDS = ACTION_IDS.concat([EDIT_ID]);
@@ -29,6 +31,26 @@ function isSmall(widget, rows) {
return widget?.small === true && rows === 1 && canShrink(widget.id);
}
function canGroup(widget) {
return canShrink(widget?.id) && widget?.id !== GROUP_ID;
}
// Members of a group that no longer exists fall back to the grid.
function groupIds(widgets) {
return widgets.filter(widget => widget?.id === GROUP_ID).map(widget => widget.instanceId);
}
function inGroup(widget, ids) {
return !!widget?.group && ids.includes(widget.group);
}
function groupMembers(widgets, groupId) {
return widgets.reduce((members, widget, index) => widget?.group === groupId && isShown(widget) ? members.concat([{
"index": index,
"widget": widget
}]) : members, []);
}
function inFooter(widget) {
return !!widget?.footer;
}
@@ -174,6 +196,9 @@ function addWidget(widgetId, columns) {
widget.showMountPath = true;
}
if (widgetId === GROUP_ID)
widget.instanceId = generateUniqueId();
if (widgetId === "brightnessSlider") {
widget.instanceId = generateUniqueId();
widget.deviceName = "";
@@ -191,10 +216,52 @@ function removeWidget(index) {
const widgets = Common.SettingsData.controlCenterWidgets.slice();
if (index < 0 || index >= widgets.length || !isRemovable(widgets[index]))
return;
const [removed] = widgets.splice(index, 1);
Common.SettingsData.set("controlCenterWidgets", removed.id === GROUP_ID ? ungroup(widgets, removed.instanceId) : widgets);
}
function ungroup(widgets, groupId) {
return widgets.map(widget => {
if (widget?.group !== groupId)
return widget;
const freed = Object.assign({}, widget);
delete freed.group;
return freed;
});
}
// Lands just before `beforeIndex`, or after the group's last member.
function moveToGroup(index, groupId, beforeIndex) {
const widgets = Common.SettingsData.controlCenterWidgets.slice();
const widget = widgets[index];
if (!widget || !canGroup(widget))
return;
const before = beforeIndex === index ? null : widgets[beforeIndex] ?? null;
widgets.splice(index, 1);
const lastMember = widgets.reduce((last, other, i) => other?.group === groupId ? i : last, -1);
const at = before ? widgets.indexOf(before) : lastMember >= 0 ? lastMember + 1 : widgets.length;
const moved = Object.assign({}, widget, {
"group": groupId
});
delete moved.col;
delete moved.row;
delete moved.footer;
delete moved.footerEnd;
widgets.splice(at, 0, moved);
Common.SettingsData.set("controlCenterWidgets", widgets);
}
function placeFromGroup(widgets, index, col, row) {
const tile = Object.assign({}, widgets[index], {
"col": col,
"row": row
});
delete tile.group;
const placed = widgets.slice();
placed[index] = tile;
return placed;
}
function setOption(index, key, value) {
const widgets = Common.SettingsData.controlCenterWidgets.slice();
if (index < 0 || index >= widgets.length)
+28 -8
View File
@@ -29,8 +29,10 @@ Item {
readonly property real handleThickness: Theme.spacingM
readonly property real touchTargetSize: Math.max(Theme.minimumTouchTargetSize, buttonSize)
readonly property real contentInset: touchTargetSize / 2
readonly property bool showOptionsButton: hasOptions && width - contentInset * 2 >= touchTargetSize * (1 + (removable ? 1 : 0) + (horizontalResize ? 1 : 0))
readonly property int chromeButtons: (removable ? 1 : 0) + (showOptionsButton ? 1 : 0)
readonly property bool showOptionsButton: hasOptions
// Too narrow to sit beside the remove button: both move onto the top corners instead.
readonly property bool cornerButtons: hasOptions && width - contentInset * 2 < touchTargetSize * (1 + (removable ? 1 : 0) + (horizontalResize ? 1 : 0))
readonly property int chromeButtons: (removable ? 1 : 0) + (showOptionsButton && !cornerButtons ? 1 : 0)
readonly property rect hitBounds: Qt.rect(contentInset - hitOverflow, contentInset - hitOverflow, width - (contentInset - hitOverflow) * 2, height - (contentInset - hitOverflow) * 2)
signal removeRequested
@@ -60,22 +62,36 @@ Item {
border.width: Theme.outlineWidthFocused
}
Rectangle {
x: I18n.isRtl ? root.width - root.contentInset - width - (root.touchTargetSize - root.buttonSize) / 2 : root.contentInset + (root.touchTargetSize - root.buttonSize) / 2
y: root.contentInset - height / 2
width: root.buttonSize + root.touchTargetSize * Math.max(0, root.chromeButtons - 1)
component ButtonChip: Rectangle {
height: root.buttonSize
radius: Theme.fullRadius(width, height)
color: Theme.chipSurface
border.color: Theme.primary
border.width: Theme.outlineWidth
}
ButtonChip {
x: root.cornerButtons ? removeButton.x + (root.touchTargetSize - root.buttonSize) / 2 : I18n.isRtl ? root.width - root.contentInset - width - (root.touchTargetSize - root.buttonSize) / 2 : root.contentInset + (root.touchTargetSize - root.buttonSize) / 2
y: root.contentInset - height / 2
width: root.buttonSize + root.touchTargetSize * Math.max(0, root.chromeButtons - 1)
visible: root.chromeButtons > 0
}
ButtonChip {
x: optionsButton.x + (root.touchTargetSize - root.buttonSize) / 2
y: root.contentInset - height / 2
width: root.buttonSize
visible: root.cornerButtons
}
DActionButton {
id: removeButton
x: I18n.isRtl ? root.width - root.contentInset - width : root.contentInset
x: {
if (root.cornerButtons)
return I18n.isRtl ? root.width - width : 0;
return I18n.isRtl ? root.width - root.contentInset - width : root.contentInset;
}
y: 0
width: root.touchTargetSize
height: root.touchTargetSize
@@ -109,7 +125,11 @@ Item {
readonly property real slotOffset: root.removable ? root.touchTargetSize : 0
x: I18n.isRtl ? root.width - root.contentInset - slotOffset - width : root.contentInset + slotOffset
x: {
if (root.cornerButtons)
return I18n.isRtl ? 0 : root.width - width;
return I18n.isRtl ? root.width - root.contentInset - slotOffset - width : root.contentInset + slotOffset;
}
y: 0
width: root.touchTargetSize
height: root.touchTargetSize