mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-21 10:44:01 -04:00
feat(control center): remaster attempt #23 - new quick tiles widget option for smaller drag & drop targets
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user