mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-18 09:14:01 -04:00
dash: soft primary container, per-widget options and weather tones
This commit is contained in:
+1
-1
Submodule dank-qml-common updated: 445e6ad070...7092ee89ba
Generated
+3
-3
@@ -3,11 +3,11 @@
|
||||
"dank-qml-common": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1790638766,
|
||||
"narHash": "sha256-/X9CR5qLC/RnVdGYDo9XWn3Z958DVbSAv3c0+TxIQYw=",
|
||||
"lastModified": 1790691843,
|
||||
"narHash": "sha256-ZoqLNYx8vgjFV18mI3l8vvkG9SufqZD2qr7q/j+aCns=",
|
||||
"owner": "AvengeMedia",
|
||||
"repo": "dank-qml-common",
|
||||
"rev": "445e6ad070f956920e23de62086257167af7cb06",
|
||||
"rev": "7092ee89ba7bab4afe74a215f31b999ae1546b9f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -193,6 +193,7 @@ Singleton {
|
||||
property string widgetColorMode: Spec.SPEC.widgetColorMode.def
|
||||
property string controlCenterTileColorMode: Spec.SPEC.controlCenterTileColorMode.def
|
||||
property string buttonColorMode: Spec.SPEC.buttonColorMode.def
|
||||
property int containerSaturation: Spec.SPEC.containerSaturation.def
|
||||
property int radiusStrength: Spec.SPEC.radiusStrength.def
|
||||
property string radiusMode: Spec.SPEC.radiusMode.def
|
||||
property int fixedRadius: Spec.SPEC.fixedRadius.def
|
||||
|
||||
@@ -7,6 +7,7 @@ import "../DankCommon/Common/Shape.js" as Shape
|
||||
import "../DankCommon/Common/Surface.js" as Surface
|
||||
import "../DankCommon/Common/Contrast.js" as Contrast
|
||||
import "../DankCommon/Common/Accents.js" as Accents
|
||||
import "../DankCommon/Common/Tonal.js" as Tonal
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
@@ -408,7 +409,9 @@ Singleton {
|
||||
readonly property color cardSurface: typeof SettingsData === "undefined" ? surfaceContainer : surfaceRoleColor(SettingsData.cardSurfaceColor, SettingsData.cardSurfaceCustomColor, surfaceContainer)
|
||||
readonly property color chipSurface: typeof SettingsData === "undefined" ? surfaceContainerHigh : surfaceRoleColor(SettingsData.chipSurfaceColor, SettingsData.chipSurfaceCustomColor, surfaceContainerHigh)
|
||||
readonly property color chipSurfaceNested: typeof SettingsData === "undefined" ? surfaceContainerHighest : surfaceRoleColor(SettingsData.chipSurfaceNestedColor, SettingsData.chipSurfaceNestedCustomColor, surfaceContainerHighest)
|
||||
property color primaryContainer: currentThemeData.primaryContainer || blend(surfaceContainerHigh, primary, 0.45)
|
||||
readonly property real containerSaturation: typeof SettingsData === "undefined" ? 1 : SettingsData.containerSaturation / 100
|
||||
readonly property real containerTint: (currentThemeData.containerTint ?? Tonal.defaultTint(surfaceContainer)) * containerSaturation
|
||||
property color primaryContainer: currentThemeData.softPrimaryContainer || Tonal.softContainer(primary, surfaceContainer, containerTint)
|
||||
property color secondaryContainer: currentThemeData.secondaryContainer || blend(surfaceContainerHigh, secondary, 0.35)
|
||||
property color tertiaryContainer: currentThemeData.tertiaryContainer || blend(surfaceContainerHigh, tertiary, 0.35)
|
||||
readonly property real selectedContainerTint: currentThemeData.selectedContainerTint ?? 0.2
|
||||
@@ -468,7 +471,14 @@ Singleton {
|
||||
Binding {
|
||||
target: root
|
||||
property: "onPrimaryContainer"
|
||||
value: root.currentThemeData.onPrimaryContainer || root.currentThemeData.primaryContainerText || Contrast.readableOn(root.primaryContainer, root.onContainerCandidates)
|
||||
value: {
|
||||
const explicit = root.currentThemeData.onPrimaryContainer || root.currentThemeData.primaryContainerText;
|
||||
if (!explicit)
|
||||
return Contrast.readableOn(root.primaryContainer, root.onContainerCandidates);
|
||||
if (root.currentThemeData.softPrimaryContainer)
|
||||
return explicit;
|
||||
return Contrast.readableOn(root.primaryContainer, [Qt.color(explicit)].concat(root.onContainerCandidates));
|
||||
}
|
||||
},
|
||||
Binding {
|
||||
target: root
|
||||
|
||||
@@ -109,6 +109,9 @@ var LOCAL_SPEC = {
|
||||
buttonColorMode: {
|
||||
def: "primary"
|
||||
},
|
||||
containerSaturation: {
|
||||
def: 100
|
||||
},
|
||||
niriLayoutGapsOverride: {
|
||||
def: -1,
|
||||
onChange: "updateCompositorLayout"
|
||||
|
||||
@@ -563,6 +563,7 @@ DankPopout {
|
||||
DashOptionsSheet {
|
||||
id: tabOptions
|
||||
backdrop: contentColumn
|
||||
tabScope: true
|
||||
onDismissed: mainContainer.focusInitial()
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ Item {
|
||||
vertical: root.vertical
|
||||
canAdd: (root.tabItem?.addable?.length ?? 0) > 0
|
||||
hasWidgets: root.hasWidgets
|
||||
hasOptions: DashRegistry.hasOptions(root.entryId)
|
||||
hasOptions: DashRegistry.hasOptions(root.entryId, true)
|
||||
hasCustomActions: root.hasCustomActions
|
||||
onActionsRequested: anchor => customMenu.openAt(anchor)
|
||||
onAddRequested: anchor => root.tabItem?.openAddMenu(anchor)
|
||||
|
||||
@@ -5,6 +5,7 @@ import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.DankDash.Overview
|
||||
import "utils/options.js" as Options
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -66,8 +67,14 @@ Singleton {
|
||||
};
|
||||
}
|
||||
|
||||
function toneOption() {
|
||||
return choice("tone", I18n.tr("Tone", "noun, dashboard widget color tone option label"), "", toneChoices);
|
||||
function toneOption(def = "") {
|
||||
return choice("tone", I18n.tr("Tone", "noun, dashboard widget color tone option label"), def, toneChoices);
|
||||
}
|
||||
|
||||
function cardOnly(spec) {
|
||||
return Object.assign(spec, {
|
||||
"cardOnly": true
|
||||
});
|
||||
}
|
||||
|
||||
function panelOptions(tab) {
|
||||
@@ -163,16 +170,7 @@ Singleton {
|
||||
"minW": 1,
|
||||
"minH": 1
|
||||
},
|
||||
"options": [choice("forecast", I18n.tr("Forecast", "weather widget option label for forecast display type"), "chart", [
|
||||
{
|
||||
"value": "chart",
|
||||
"text": I18n.tr("Chart", "noun, weather forecast display option")
|
||||
},
|
||||
{
|
||||
"value": "cards",
|
||||
"text": I18n.tr("Cards", "noun, weather forecast display option")
|
||||
}
|
||||
]), toggle("city", I18n.tr("Show city"), false), toggle("readings", I18n.tr("Show readings"), true)]
|
||||
"options": [cardOnly(toggle("city", I18n.tr("Show city"), false)), cardOnly(toggle("readings", I18n.tr("Show readings"), true)), cardOnly(toneOption())]
|
||||
},
|
||||
{
|
||||
"id": "notifications",
|
||||
@@ -437,6 +435,13 @@ Singleton {
|
||||
function pluginOption(pluginId, raw) {
|
||||
if (!raw || typeof raw.key !== "string" || raw.key === "" || raw.key === widgetsKey)
|
||||
return null;
|
||||
const spec = pluginOptionSpec(pluginId, raw);
|
||||
if (spec && raw.cardOnly === true)
|
||||
return cardOnly(spec);
|
||||
return spec;
|
||||
}
|
||||
|
||||
function pluginOptionSpec(pluginId, raw) {
|
||||
const text = I18n.trFor(pluginId, raw.text ?? raw.key);
|
||||
switch (raw.type) {
|
||||
case "toggle":
|
||||
@@ -467,12 +472,12 @@ Singleton {
|
||||
return entry(id)?.options ?? [];
|
||||
}
|
||||
|
||||
function sheetOptionSpecs(id) {
|
||||
return optionSpecs(id).filter(spec => spec.settingsOnly !== true);
|
||||
function sheetOptionSpecs(id, tabScope = false) {
|
||||
return optionSpecs(id).filter(spec => spec.settingsOnly !== true && !(tabScope && spec.cardOnly === true));
|
||||
}
|
||||
|
||||
function hasOptions(id) {
|
||||
return sheetOptionSpecs(id).length > 0;
|
||||
function hasOptions(id, tabScope = false) {
|
||||
return sheetOptionSpecs(id, tabScope).length > 0;
|
||||
}
|
||||
|
||||
function storedOptions(id) {
|
||||
@@ -496,17 +501,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function optionValue(spec, value) {
|
||||
switch (spec.type) {
|
||||
case "toggle":
|
||||
return typeof value === "boolean" ? value : spec.def;
|
||||
case "choice":
|
||||
return spec.choices.some(c => c.value === value) ? value : spec.def;
|
||||
case "number":
|
||||
if (!Number.isFinite(value))
|
||||
return spec.def;
|
||||
return Math.max(spec.min, Math.min(spec.max, value));
|
||||
}
|
||||
return spec.def;
|
||||
return Options.value(spec, value);
|
||||
}
|
||||
|
||||
function option(id, key) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
import qs.Modules.DankDash.Overview
|
||||
import "../../Common/GridLayout.js" as GridUtils
|
||||
import "utils/widgets.js" as WidgetUtils
|
||||
@@ -22,7 +23,7 @@ Item {
|
||||
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 || widgetMenu.open
|
||||
readonly property bool blocksTabNavigation: editMode || addMenu.open || optionsSheet.shown
|
||||
readonly property bool cardResizing: grid.sizePreview !== null
|
||||
|
||||
implicitHeight: Math.max(DashMetrics.tabMinHeight, grid.implicitHeight)
|
||||
@@ -106,16 +107,16 @@ Item {
|
||||
}
|
||||
|
||||
function handleKeyEvent(event) {
|
||||
if (event.key !== Qt.Key_Escape || !(addMenu.open || widgetMenu.open))
|
||||
if (event.key !== Qt.Key_Escape || !(addMenu.open || optionsSheet.shown))
|
||||
return false;
|
||||
addMenu.close();
|
||||
widgetMenu.close();
|
||||
optionsSheet.dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
onEditModeChanged: {
|
||||
addMenu.close();
|
||||
widgetMenu.close();
|
||||
optionsSheet.dismiss();
|
||||
}
|
||||
|
||||
DankEditableGrid {
|
||||
@@ -175,13 +176,10 @@ Item {
|
||||
cornerRadius: root.cardRadius
|
||||
dragging: slot.dragging
|
||||
resizing: slot.resizing
|
||||
hasOptions: true
|
||||
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: anchor => {
|
||||
widgetMenu.widgetId = slot.widget.id;
|
||||
widgetMenu.openAt(anchor);
|
||||
}
|
||||
onOptionsRequested: optionsSheet.presentFor(slot.widget.id)
|
||||
onResizeStarted: (px, py) => slot.beginResize(px, py)
|
||||
onResizeMoved: (px, py) => slot.resizeTo(px, py)
|
||||
onResizeEnded: slot.finishResize()
|
||||
@@ -213,71 +211,42 @@ Item {
|
||||
}))
|
||||
}
|
||||
|
||||
CcMenu {
|
||||
id: widgetMenu
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
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)
|
||||
readonly property int row: grid.slotLayout.slots[widgetIndex]?.row ?? 0
|
||||
items: [
|
||||
{
|
||||
label: I18n.tr("Move up"),
|
||||
iconName: "arrow_upward",
|
||||
enabled: row > 0,
|
||||
action: () => root.updateWidget(widgetIndex, {
|
||||
row: row - 1
|
||||
})
|
||||
},
|
||||
{
|
||||
label: I18n.tr("Move down"),
|
||||
iconName: "arrow_downward",
|
||||
enabled: widgetIndex >= 0,
|
||||
action: () => root.updateWidget(widgetIndex, {
|
||||
row: row + 1
|
||||
})
|
||||
},
|
||||
{
|
||||
label: I18n.tr("Width", "noun, size label, also used in widget resize menu") + " +",
|
||||
iconName: "add",
|
||||
enabled: widget.w < (spec?.maxW ?? spec?.w ?? 1),
|
||||
action: () => root.updateWidget(widgetIndex, {
|
||||
w: widget.w + 1
|
||||
})
|
||||
},
|
||||
{
|
||||
label: I18n.tr("Width") + " −",
|
||||
iconName: "remove",
|
||||
enabled: widget.w > (spec?.minW ?? 1),
|
||||
action: () => root.updateWidget(widgetIndex, {
|
||||
w: widget.w - 1
|
||||
})
|
||||
},
|
||||
{
|
||||
label: I18n.tr("Height", "noun, size label, also used in widget resize menu") + " +",
|
||||
iconName: "add",
|
||||
enabled: widget.h < (spec?.maxH ?? spec?.h ?? 1),
|
||||
action: () => root.updateWidget(widgetIndex, {
|
||||
h: widget.h + 1
|
||||
})
|
||||
},
|
||||
{
|
||||
label: I18n.tr("Height") + " −",
|
||||
iconName: "remove",
|
||||
enabled: widget.h > (spec?.minH ?? 1),
|
||||
action: () => root.updateWidget(widgetIndex, {
|
||||
h: widget.h - 1
|
||||
})
|
||||
},
|
||||
{
|
||||
label: I18n.tr("Show graphics", "Dashboard widget option for decorative data graphics"),
|
||||
iconName: widget.graphics ? "check_box" : "check_box_outline_blank",
|
||||
visible: spec?.graphics === true,
|
||||
action: () => root.updateWidget(widgetIndex, {
|
||||
graphics: !widget.graphics
|
||||
})
|
||||
|
||||
function presentFor(id) {
|
||||
widgetId = id;
|
||||
present();
|
||||
}
|
||||
|
||||
backdrop: grid
|
||||
panelWidth: DashMetrics.optionSheetWidth
|
||||
iconName: spec?.icon ?? "tune"
|
||||
title: spec?.text ?? ""
|
||||
subtitle: I18n.tr("Options")
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,16 @@ import qs.Modules.DankDash
|
||||
SettingsRow {
|
||||
id: root
|
||||
|
||||
required property string entryId
|
||||
required property var spec
|
||||
property var value
|
||||
|
||||
readonly property var value: DashRegistry.option(entryId, spec.key)
|
||||
readonly property var choices: spec.choices ?? []
|
||||
readonly property int choiceIndex: choices.findIndex(c => c.value === value)
|
||||
|
||||
signal committed(var next)
|
||||
|
||||
function commit(next) {
|
||||
DashRegistry.setOption(entryId, spec.key, next);
|
||||
committed(next);
|
||||
}
|
||||
|
||||
title: spec.text ?? spec.key
|
||||
|
||||
@@ -10,9 +10,10 @@ CcSheetDialog {
|
||||
id: root
|
||||
|
||||
property string entryId: ""
|
||||
property bool tabScope: false
|
||||
|
||||
readonly property var entry: DashRegistry.entry(entryId)
|
||||
readonly property var specs: DashRegistry.sheetOptionSpecs(entryId)
|
||||
readonly property var specs: DashRegistry.sheetOptionSpecs(entryId, tabScope)
|
||||
|
||||
function presentFor(id) {
|
||||
entryId = id;
|
||||
@@ -34,8 +35,9 @@ CcSheetDialog {
|
||||
DashOptionRow {
|
||||
required property var modelData
|
||||
|
||||
entryId: root.entryId
|
||||
spec: modelData
|
||||
value: DashRegistry.option(root.entryId, modelData.key)
|
||||
onCommitted: next => DashRegistry.setOption(root.entryId, modelData.key, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ Card {
|
||||
}
|
||||
|
||||
entryId: "weather"
|
||||
tone: options.tone ?? ""
|
||||
clickable: true
|
||||
pad: compact ? Theme.spacingS : Theme.spacingM
|
||||
|
||||
@@ -228,6 +229,8 @@ Card {
|
||||
visible: root.heroLayout
|
||||
showCity: root.showCity
|
||||
showSunTimes: root.showReadings
|
||||
tone: root.tone
|
||||
flat: true
|
||||
}
|
||||
|
||||
Row {
|
||||
|
||||
@@ -11,6 +11,7 @@ Column {
|
||||
|
||||
property bool daily: false
|
||||
property bool chartMode: true
|
||||
property var widgetOptions: ({})
|
||||
property bool live: false
|
||||
readonly property int visibleCount: Math.max(1, Math.min(daily ? DashMetrics.dailyVisibleCount : DashMetrics.chartHourlyCount, Math.floor(width / DashMetrics.gridRowUnit)))
|
||||
property int startIndex: daily ? 0 : (WeatherService.weather.currentHourIndex ?? new Date().getHours())
|
||||
|
||||
@@ -9,13 +9,24 @@ Rectangle {
|
||||
|
||||
property bool showCity: false
|
||||
property bool showSunTimes: true
|
||||
property var widgetOptions: ({})
|
||||
property string tone: widgetOptions.tone ?? "primary"
|
||||
property bool flat: false
|
||||
readonly property bool wide: width >= Theme.smallBreakpoint
|
||||
readonly property var weather: WeatherService.weather
|
||||
readonly property string cityText: [weather.city, weather.country].filter(s => !!s).join(", ")
|
||||
|
||||
implicitHeight: (wide ? Math.max(details.implicitHeight, temperature.implicitHeight) : details.implicitHeight) + Theme.spacingL * 2
|
||||
radius: Theme.cornerRadiusXL
|
||||
color: Theme.foregroundColor(Theme.primaryContainer, Theme.isFloatingWindow(root))
|
||||
color: flat ? "transparent" : toneColors.surfaceColor
|
||||
border.width: flat ? 0 : Theme.layerOutlineWidth
|
||||
border.color: Theme.outlineMedium
|
||||
|
||||
DankTone {
|
||||
id: toneColors
|
||||
tone: root.tone
|
||||
floatingWindow: Theme.isFloatingWindow(root)
|
||||
}
|
||||
|
||||
Column {
|
||||
id: details
|
||||
@@ -31,7 +42,7 @@ Rectangle {
|
||||
visible: root.showCity && root.cityText !== ""
|
||||
width: parent.width
|
||||
text: root.cityText
|
||||
color: Theme.onPrimaryContainer
|
||||
color: toneColors.contentColor
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
elide: Text.ElideRight
|
||||
@@ -46,7 +57,7 @@ Rectangle {
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: WeatherService.getWeatherCondition(root.weather.wCode)
|
||||
color: Theme.onPrimaryContainer
|
||||
color: toneColors.contentColor
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
@@ -54,7 +65,7 @@ Rectangle {
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: I18n.tr("Feels Like %1°").arg(WeatherService.formatTemp(root.weather.feelsLike ?? root.weather.temp, false) ?? "--")
|
||||
color: Theme.onPrimaryContainer
|
||||
color: toneColors.contentColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
@@ -89,7 +100,7 @@ Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: sunTime.modelData.icon
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.onPrimaryContainer
|
||||
color: toneColors.contentColor
|
||||
|
||||
StateLayer {
|
||||
acceptedButtons: Qt.NoButton
|
||||
@@ -105,7 +116,7 @@ Rectangle {
|
||||
text: sunTime.modelData.time || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.onPrimaryContainer
|
||||
color: toneColors.contentColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +130,7 @@ Rectangle {
|
||||
y: root.wide ? (root.height - height) / 2 : details.y + (root.showCity && root.cityText !== "" ? city.height + Theme.spacingXS : 0)
|
||||
width: root.wide ? Theme.fontSizeDisplay * 3 : details.width - emblem.width - Theme.spacingS
|
||||
text: WeatherService.formatTemp(root.weather.temp) ?? "--"
|
||||
color: Theme.onPrimaryContainer
|
||||
color: toneColors.contentColor
|
||||
font.pixelSize: Theme.fontSizeDisplay * 1.5
|
||||
font.weight: Theme.fontWeightMedium
|
||||
minimumPixelSize: Theme.fontSizeXXLarge
|
||||
|
||||
@@ -112,17 +112,23 @@ Rectangle {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
readonly property color foreground: Theme.onSurface
|
||||
readonly property color foreground: toneColors.contentColor
|
||||
readonly property color accent: toneColors.accentColor
|
||||
readonly property bool leadingContent: widgetId === "humidity" || widgetId === "precipitation"
|
||||
|
||||
radius: graphics && reading.shape ? Theme.fullRadius(width, height) : Theme.cornerRadiusXL
|
||||
color: graphics && widgetId === "uv" ? "transparent" : DashMetrics.cardColor
|
||||
color: graphics && widgetId === "uv" ? "transparent" : toneColors.surfaceColor
|
||||
border.width: graphics && widgetId === "uv" ? 0 : Theme.layerOutlineWidth
|
||||
border.color: Theme.outlineMedium
|
||||
Accessible.role: Accessible.StaticText
|
||||
Accessible.name: reading.label + " " + (hasValue ? reading.value : I18n.tr("Not available")) + " " + (reading.detail ?? "")
|
||||
Accessible.description: graphicDescription
|
||||
|
||||
DankTone {
|
||||
id: toneColors
|
||||
tone: root.widgetOptions.tone ?? ""
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
@@ -139,8 +145,8 @@ Rectangle {
|
||||
width: artworkSize * aspectRatio
|
||||
height: artworkSize
|
||||
shape: root.reading.shape ?? "square"
|
||||
color: root.widgetId === "uv" ? DashMetrics.cardColor : Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
|
||||
trackColor: Theme.withAlpha(Theme.primary, Theme.stateLayerHover)
|
||||
color: root.widgetId === "uv" ? toneColors.surfaceColor : Theme.withAlpha(root.accent, Theme.stateLayerDrag)
|
||||
trackColor: Theme.withAlpha(root.accent, Theme.stateLayerHover)
|
||||
fillProgress: root.shapeLevel
|
||||
visible: root.graphics && !!root.reading.shape && root.widgetId !== "pressure"
|
||||
rotation: Theme.shapeScale > 0 && root.widgetId === "wind" && root.weather.windDirection != null ? (root.weather.windDirection + 180) % 360 : 0
|
||||
@@ -154,7 +160,7 @@ Rectangle {
|
||||
ShapePath {
|
||||
strokeWidth: 0
|
||||
strokeColor: "transparent"
|
||||
fillColor: Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
|
||||
fillColor: Theme.withAlpha(root.accent, Theme.stateLayerDrag)
|
||||
PathSvg {
|
||||
path: Visuals.levelPath(root.width, root.height, root.radius, (root.reading.level ?? 0) / 100, Theme.spacingS)
|
||||
}
|
||||
@@ -178,7 +184,7 @@ Rectangle {
|
||||
readonly property real inset: Theme.spacingS + thickness / 2
|
||||
|
||||
ShapePath {
|
||||
strokeColor: Theme.withAlpha(Theme.primary, Theme.stateLayerHover)
|
||||
strokeColor: Theme.withAlpha(root.accent, Theme.stateLayerHover)
|
||||
strokeWidth: gauge.thickness
|
||||
capStyle: Theme.shapeScale > 0 ? ShapePath.RoundCap : ShapePath.FlatCap
|
||||
fillColor: "transparent"
|
||||
@@ -188,7 +194,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.hasValue && root.shapeLevel > 0 ? Theme.primary : "transparent"
|
||||
strokeColor: root.hasValue && root.shapeLevel > 0 ? root.accent : "transparent"
|
||||
strokeWidth: gauge.thickness
|
||||
capStyle: Theme.shapeScale > 0 ? ShapePath.RoundCap : ShapePath.FlatCap
|
||||
fillColor: "transparent"
|
||||
@@ -291,7 +297,7 @@ Rectangle {
|
||||
width: Math.max(Theme.iconButtonSize, dewValue.implicitWidth + Theme.spacingM)
|
||||
height: Theme.iconButtonSize
|
||||
radius: Theme.fullRadius(width, height)
|
||||
color: Theme.primaryContainer
|
||||
color: toneColors.tinted ? toneColors.contentColor : Theme.primaryContainer
|
||||
|
||||
StyledText {
|
||||
id: dewValue
|
||||
@@ -300,7 +306,7 @@ Rectangle {
|
||||
text: WeatherService.formatTemp(root.weather.dewPoint) ?? "--"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.onPrimaryContainer
|
||||
color: toneColors.tinted ? toneColors.containerColor : Theme.onPrimaryContainer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ Rectangle {
|
||||
|
||||
property bool live: Window.window?.visible ?? false
|
||||
property bool moon: false
|
||||
property var widgetOptions: ({})
|
||||
property var currentDate: new Date()
|
||||
readonly property var sunTimes: live && !moon ? WeatherService.getSunTimes(currentDate) : null
|
||||
readonly property var moonTrack: live && moon ? WeatherService.getMoonTrack(currentDate) : null
|
||||
@@ -39,10 +40,15 @@ Rectangle {
|
||||
|
||||
implicitHeight: DashMetrics.gridRowUnit * 2 + DashMetrics.gridGap
|
||||
radius: Theme.cornerRadiusXL
|
||||
color: DashMetrics.cardColor
|
||||
color: toneColors.surfaceColor
|
||||
border.width: Theme.layerOutlineWidth
|
||||
border.color: Theme.outlineMedium
|
||||
|
||||
DankTone {
|
||||
id: toneColors
|
||||
tone: root.widgetOptions.tone ?? ""
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
@@ -52,7 +58,7 @@ Rectangle {
|
||||
DankIcon {
|
||||
name: root.moon ? "dark_mode" : "light_mode"
|
||||
size: Theme.iconSize
|
||||
color: Theme.onSurface
|
||||
color: toneColors.contentColor
|
||||
}
|
||||
|
||||
StyledText {
|
||||
@@ -60,7 +66,7 @@ Rectangle {
|
||||
text: root.moon ? I18n.tr("Moon", "Weather moon position widget title") : I18n.tr("Sun", "Weather daylight widget title")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.onSurface
|
||||
color: toneColors.contentColor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +89,7 @@ Rectangle {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: Theme.withAlpha(Theme.primary, Theme.stateLayerDrag)
|
||||
fillColor: Theme.withAlpha(toneColors.accentColor, Theme.stateLayerDrag)
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
startX: 0
|
||||
@@ -115,7 +121,7 @@ Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: width
|
||||
shape: "sunny"
|
||||
color: Theme.primary
|
||||
color: toneColors.accentColor
|
||||
visible: !root.moon && root.hasSunTimes && root.currentDate.getTime() >= root.sunriseTime && root.currentDate.getTime() <= root.sunsetTime
|
||||
}
|
||||
|
||||
@@ -128,7 +134,7 @@ Rectangle {
|
||||
insetTop: 0
|
||||
insetBottom: 0
|
||||
lineWidth: Theme.outlineWidth
|
||||
lineColor: Theme.primary
|
||||
lineColor: toneColors.accentColor
|
||||
fillOpacity: Theme.stateLayerDrag
|
||||
}
|
||||
|
||||
@@ -138,7 +144,7 @@ Rectangle {
|
||||
y: arc.height * (1 - (root.moonTrack?.altitude ?? 0)) / 2 - height / 2
|
||||
name: WeatherService.getMoonPhase(root.currentDate)
|
||||
size: Theme.iconSize
|
||||
color: Theme.primary
|
||||
color: toneColors.accentColor
|
||||
visible: root.moon && root.moonTrack !== null
|
||||
}
|
||||
}
|
||||
@@ -157,14 +163,14 @@ Rectangle {
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.moon ? I18n.tr("Moonrise", "Weather moonrise time") : I18n.tr("Sunrise")
|
||||
color: Theme.onSurfaceVariant
|
||||
color: toneColors.mutedColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.riseText
|
||||
color: Theme.onSurface
|
||||
color: toneColors.contentColor
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
fontSizeMode: Text.HorizontalFit
|
||||
minimumPixelSize: Theme.fontSizeSmall
|
||||
@@ -178,7 +184,7 @@ Rectangle {
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.moon ? I18n.tr("Moonset", "Weather moonset time") : I18n.tr("Sunset")
|
||||
color: Theme.onSurfaceVariant
|
||||
color: toneColors.mutedColor
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignRight
|
||||
@@ -186,7 +192,7 @@ Rectangle {
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: root.setText
|
||||
color: Theme.onSurface
|
||||
color: toneColors.contentColor
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
fontSizeMode: Text.HorizontalFit
|
||||
minimumPixelSize: Theme.fontSizeSmall
|
||||
|
||||
@@ -16,7 +16,6 @@ Item {
|
||||
property bool editMode: false
|
||||
property bool weatherRefHeld: false
|
||||
property var transientSurfaceTracker: null
|
||||
readonly property var options: DashRegistry.resolvedOptions(entryId)
|
||||
readonly property bool available: WeatherService.weather.available
|
||||
readonly property var addable: widgets.addable
|
||||
readonly property Item focusTarget: available ? widgets.focusTarget : refreshButton
|
||||
@@ -26,9 +25,28 @@ Item {
|
||||
iconName: "refresh",
|
||||
enabled: !WeatherService.weather.loading,
|
||||
action: () => WeatherService.forceRefresh()
|
||||
},
|
||||
{
|
||||
label: I18n.tr("Settings"),
|
||||
iconName: "settings",
|
||||
action: () => {
|
||||
PopoutService.closeDankDash();
|
||||
PopoutService.openSettingsWithTab("weather");
|
||||
}
|
||||
}
|
||||
]
|
||||
readonly property bool blocksTabNavigation: widgets.blocksTabNavigation
|
||||
readonly property var forecastOptions: [DashRegistry.choice("forecast", I18n.tr("Forecast", "weather widget option label for forecast display type"), "chart", [
|
||||
{
|
||||
"value": "chart",
|
||||
"text": I18n.tr("Chart", "noun, weather forecast display option")
|
||||
},
|
||||
{
|
||||
"value": "cards",
|
||||
"text": I18n.tr("Cards", "noun, weather forecast display option")
|
||||
}
|
||||
])]
|
||||
readonly property var metricOptions: [DashRegistry.toggle("graphics", I18n.tr("Show graphics", "Dashboard widget option for decorative data graphics"), true), DashRegistry.toneOption()]
|
||||
|
||||
implicitWidth: DashMetrics.contentWidthFor(SettingsData.showWeekNumber, DashMetrics.panelColumnsFor(entryId))
|
||||
implicitHeight: widgets.implicitHeight
|
||||
@@ -85,7 +103,8 @@ Item {
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
maxW: 4,
|
||||
maxH: 3
|
||||
maxH: 3,
|
||||
options: [DashRegistry.toneOption("primary"), DashRegistry.toggle("city", I18n.tr("Show city"), false)]
|
||||
},
|
||||
{
|
||||
id: "hourly",
|
||||
@@ -97,7 +116,8 @@ Item {
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
maxW: 4,
|
||||
maxH: 4
|
||||
maxH: 4,
|
||||
options: root.forecastOptions
|
||||
},
|
||||
{
|
||||
id: "sun",
|
||||
@@ -108,7 +128,8 @@ Item {
|
||||
h: 2,
|
||||
minH: 2,
|
||||
maxW: 4,
|
||||
maxH: 3
|
||||
maxH: 3,
|
||||
options: [DashRegistry.toneOption()]
|
||||
},
|
||||
{
|
||||
id: "moon",
|
||||
@@ -119,7 +140,8 @@ Item {
|
||||
h: 2,
|
||||
minH: 2,
|
||||
maxW: 4,
|
||||
maxH: 3
|
||||
maxH: 3,
|
||||
options: [DashRegistry.toneOption()]
|
||||
},
|
||||
{
|
||||
id: "humidity",
|
||||
@@ -131,8 +153,7 @@ Item {
|
||||
minH: 2,
|
||||
maxW: 2,
|
||||
maxH: 3,
|
||||
graphics: true,
|
||||
enabled: root.options.readings !== false
|
||||
options: root.metricOptions
|
||||
},
|
||||
{
|
||||
id: "wind",
|
||||
@@ -144,8 +165,7 @@ Item {
|
||||
minH: 2,
|
||||
maxW: 2,
|
||||
maxH: 3,
|
||||
graphics: true,
|
||||
enabled: root.options.readings !== false
|
||||
options: root.metricOptions
|
||||
},
|
||||
{
|
||||
id: "daily",
|
||||
@@ -158,6 +178,7 @@ Item {
|
||||
minH: 2,
|
||||
maxW: 4,
|
||||
maxH: 4,
|
||||
options: root.forecastOptions,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
@@ -170,7 +191,7 @@ Item {
|
||||
minH: 2,
|
||||
maxW: 2,
|
||||
maxH: 3,
|
||||
graphics: true,
|
||||
options: root.metricOptions,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
@@ -183,7 +204,7 @@ Item {
|
||||
minH: 2,
|
||||
maxW: 2,
|
||||
maxH: 3,
|
||||
graphics: true,
|
||||
options: root.metricOptions,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
@@ -196,7 +217,7 @@ Item {
|
||||
minH: 2,
|
||||
maxW: 2,
|
||||
maxH: 3,
|
||||
graphics: true,
|
||||
options: root.metricOptions,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
@@ -209,7 +230,7 @@ Item {
|
||||
minH: 2,
|
||||
maxW: 2,
|
||||
maxH: 3,
|
||||
graphics: true,
|
||||
options: root.metricOptions,
|
||||
enabled: false
|
||||
}
|
||||
]
|
||||
@@ -218,7 +239,7 @@ Item {
|
||||
Component {
|
||||
id: conditions
|
||||
WeatherHero {
|
||||
showCity: root.options.city === true
|
||||
showCity: widgetOptions.city === true
|
||||
showSunTimes: true
|
||||
}
|
||||
}
|
||||
@@ -231,7 +252,7 @@ Item {
|
||||
Component {
|
||||
id: hourly
|
||||
WeatherForecastWidget {
|
||||
chartMode: root.options.forecast !== "cards"
|
||||
chartMode: widgetOptions.forecast !== "cards"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +260,7 @@ Item {
|
||||
id: daily
|
||||
WeatherForecastWidget {
|
||||
daily: true
|
||||
chartMode: root.options.forecast !== "cards"
|
||||
chartMode: widgetOptions.forecast !== "cards"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.pragma library
|
||||
|
||||
function value(spec, stored) {
|
||||
switch (spec.type) {
|
||||
case "toggle":
|
||||
return typeof stored === "boolean" ? stored : spec.def;
|
||||
case "choice":
|
||||
return spec.choices.some(c => c.value === stored) ? stored : spec.def;
|
||||
case "number":
|
||||
if (!Number.isFinite(stored))
|
||||
return spec.def;
|
||||
return Math.max(spec.min, Math.min(spec.max, stored));
|
||||
}
|
||||
return spec.def;
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
.import "../../../Common/GridLayout.js" as GridLayout
|
||||
.import "options.js" as Options
|
||||
|
||||
var LAYOUT_KEYS = ["id", "w", "h", "col", "row"];
|
||||
|
||||
function optionSpecs(spec) {
|
||||
return (Array.isArray(spec?.options) ? spec.options : []).filter(option => option && typeof option.key === "string" && !LAYOUT_KEYS.includes(option.key));
|
||||
}
|
||||
|
||||
function resolve(definitions, saved) {
|
||||
const source = Array.isArray(saved) ? saved : definitions.filter(d => d.enabled !== false);
|
||||
@@ -15,9 +22,10 @@ function resolve(definitions, saved) {
|
||||
const widget = {
|
||||
id: item.id,
|
||||
w: GridLayout.dimension(item.w, spec.minW, spec.maxW, spec.w),
|
||||
h: GridLayout.dimension(item.h, spec.minH, spec.maxH, spec.h),
|
||||
graphics: typeof item.graphics === "boolean" ? item.graphics : true
|
||||
h: GridLayout.dimension(item.h, spec.minH, spec.maxH, spec.h)
|
||||
};
|
||||
for (const option of optionSpecs(spec))
|
||||
widget[option.key] = Options.value(option, item[option.key]);
|
||||
if (Number.isFinite(item.col) && Number.isFinite(item.row)) {
|
||||
widget.col = item.col;
|
||||
widget.row = item.row;
|
||||
|
||||
@@ -257,6 +257,7 @@ FocusScope {
|
||||
DashOptionsSheet {
|
||||
id: tabOptions
|
||||
backdrop: pages
|
||||
tabScope: true
|
||||
onDismissed: root.focusFace()
|
||||
}
|
||||
|
||||
|
||||
@@ -181,9 +181,10 @@ FocusScope {
|
||||
DashOptionRow {
|
||||
required property int index
|
||||
|
||||
entryId: optionCard.modelData
|
||||
spec: optionCard.options[index] ?? ({})
|
||||
settingKey: "dashOptions:" + entryId + ":" + spec.key
|
||||
value: DashRegistry.option(optionCard.modelData, spec.key)
|
||||
settingKey: "dashOptions:" + optionCard.modelData + ":" + spec.key
|
||||
onCommitted: next => DashRegistry.setOption(optionCard.modelData, spec.key, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +564,19 @@ Column {
|
||||
SettingsData.set("buttonColorMode", "primary");
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSliderRow {
|
||||
tab: "theme"
|
||||
tags: ["container", "accent", "color", "saturation", "tint", "pastel", "primary", "card"]
|
||||
settingKey: "containerSaturation"
|
||||
text: I18n.tr("Container saturation", "theme setting, saturation of tinted accent containers")
|
||||
description: I18n.tr("Tinted cards and badges across the shell", "container saturation setting description")
|
||||
value: SettingsData.containerSaturation
|
||||
minimum: 0
|
||||
maximum: 200
|
||||
step: 5
|
||||
onSliderValueChanged: newValue => SettingsData.set("containerSaturation", newValue)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
|
||||
@@ -51,7 +51,8 @@ DashTabComponent {
|
||||
minW: 2,
|
||||
minH: 2,
|
||||
maxW: 4,
|
||||
maxH: 3
|
||||
maxH: 3,
|
||||
options: [DashRegistry.toneOption("primary")]
|
||||
},
|
||||
{
|
||||
id: "reset",
|
||||
@@ -74,7 +75,10 @@ DashTabComponent {
|
||||
|
||||
Card {
|
||||
id: counterCard
|
||||
tone: "primary"
|
||||
|
||||
property var widgetOptions: ({})
|
||||
|
||||
tone: widgetOptions.tone ?? "primary"
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
|
||||
@@ -1889,6 +1889,8 @@ The `dash` block is optional. `icon` and `title` label the tab and fall back to
|
||||
|
||||
The dash shows these rows in an options sheet (the tune button on a card in edit mode, Options in a tab's edit-mode controls) and Settings → Dashboard lists the same rows. The tab and the card read the resolved values as `options.<key>` and re-evaluate when a value changes. Values live in `settings.json` under `dashOptions.plugin_<pluginId>`, apart from `pluginData`, and only non-default values are stored. Reset clears the declared keys only. `widgets` is reserved for the `DashWidgetGrid` layout and is ignored as an option key.
|
||||
|
||||
Set `"cardOnly": true` on an option that only the overview card reads. It stays on the card's sheet and in Settings, and the tab's sheet leaves it out.
|
||||
|
||||
`dash.options` only reach the dash surfaces. If a bar widget or daemon of the same plugin needs the value, use a `settings` component and `pluginData` instead.
|
||||
|
||||
### Dash Tab Contract
|
||||
@@ -1952,9 +1954,38 @@ DashTabComponent {
|
||||
}
|
||||
```
|
||||
|
||||
Import `qs.Modules.DankDash` for the grid. Each definition has a stable `id`, a translated `text`, an `icon`, a QML `component`, default `w`/`h` and optional `minW`/`minH`/`maxW`/`maxH`. The grid is 4 columns wide (2 when narrower than `Theme.smallBreakpoint`), independent of the overview column count. Set `enabled: false` to leave a widget in the Add menu initially. `graphics: true` exposes a per-widget Show graphics option.
|
||||
Import `qs.Modules.DankDash` for the grid. Each definition has a stable `id`, a translated `text`, an `icon`, a QML `component`, default `w`/`h` and optional `minW`/`minH`/`maxW`/`maxH`. The grid is 4 columns wide (2 when narrower than `Theme.smallBreakpoint`), independent of the overview column count. Set `enabled: false` to leave a widget in the Add menu initially.
|
||||
|
||||
The grid saves order, size and graphics options in `dashOptions[entryId].widgets`. Missing definitions keep their saved placement. A saved empty list stays empty; Reset restores the current defaults. Widgets receive `widgetId`, `widgetOptions` and a bound `live` value if they declare those properties. The grid unloads widget content when `live` is false. Edit mode offers drag ordering, a resize handle, removal, and menu actions for keyboard ordering and sizing.
|
||||
A definition can carry `options`, a list of option specs in the same shape as `dash.options` (`toggle`, `choice`, `number`). Build them with `DashRegistry.toggle(key, text, def)`, `DashRegistry.choice(key, text, def, choices)`, `DashRegistry.number(key, text, def, min, max, step, unit)` or `DashRegistry.toneOption(def)` for the shared Tone choice. In edit mode a widget with options shows an options button that opens them in a sheet. Each widget instance stores its own values, and the widget reads them from `widgetOptions` by key. `id`, `w`, `h`, `col` and `row` are reserved keys.
|
||||
|
||||
```qml
|
||||
definitions: [
|
||||
{id: "counter", text: I18n.trFor("yourPlugin", "Counter"), icon: "counter_1", component: counter, w: 2, h: 2,
|
||||
options: [DashRegistry.toneOption("primary"), DashRegistry.toggle("compact", I18n.trFor("yourPlugin", "Compact"), false)]}
|
||||
]
|
||||
```
|
||||
|
||||
A widget that is not a `Card` can still follow a tone with `DankTone` from `qs.Widgets`. Set its `tone` and paint with `surfaceColor`, `contentColor`, `accentColor`, `onAccentColor`, `mutedColor` and `chipColor`. `tinted` is true for any tone but the surface one.
|
||||
|
||||
```qml
|
||||
Rectangle {
|
||||
property var widgetOptions: ({})
|
||||
|
||||
color: toneColors.surfaceColor
|
||||
|
||||
DankTone {
|
||||
id: toneColors
|
||||
tone: widgetOptions.tone ?? ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "42"
|
||||
color: toneColors.contentColor
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The grid saves order, size and option values in `dashOptions[entryId].widgets`. Missing definitions keep their saved placement. A saved empty list stays empty; Reset restores the current defaults. Widgets receive `widgetId`, `widgetOptions` and a bound `live` value if they declare those properties. The grid unloads widget content when `live` is false. Edit mode offers drag ordering, a resize handle, removal and the options button.
|
||||
|
||||
### Dash Card Contract
|
||||
|
||||
|
||||
@@ -149,7 +149,8 @@
|
||||
"min": { "type": "number" },
|
||||
"max": { "type": "number" },
|
||||
"step": { "type": "number", "exclusiveMinimum": 0 },
|
||||
"unit": { "type": "string" }
|
||||
"unit": { "type": "string" },
|
||||
"cardOnly": { "type": "boolean", "description": "Leave the option out of the tab's options sheet, for options that only the overview card reads" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
DankCommon.DankTone {}
|
||||
@@ -5,25 +5,7 @@ import test from "node:test";
|
||||
|
||||
const model = vm.createContext({});
|
||||
vm.runInContext(readFileSync(new URL("../Modules/SurfaceWidgets/ApplicationModel.js", import.meta.url), "utf8").replace(/^\.pragma.*$/m, ""), model);
|
||||
const fixture = JSON.parse(readFileSync(new URL("fixtures/dock-model.json", import.meta.url), "utf8"));
|
||||
const source = readFileSync(new URL("../Modules/SurfaceWidgets/ApplicationStrip.qml", import.meta.url), "utf8");
|
||||
const builder = source.slice(source.indexOf(" function isOnScreen("), source.indexOf(" delegate: ApplicationItem"));
|
||||
const plain = value => JSON.parse(JSON.stringify(value));
|
||||
const canonical = value => ({ ...plain(value), items: plain(value.items).map(item => ({ ...item, uniqueKey: item.type === "window" && !item.isPinned ? "window:" + item.toplevel.fixtureIndex : item.uniqueKey })) });
|
||||
|
||||
test("shared dock/bar builder retains all 24 captured baseline outputs", () => {
|
||||
for (const { input, output } of fixture.cases) {
|
||||
const root = { options: input.options, groupByApp: input.groupByApp, visibleWindows: fixture.windows,
|
||||
pinnedApps: ["browser", "editor-old", "missing"], dockScreen: { name: "DP-1" },
|
||||
maxVisibleApps: input.max, maxVisibleRunningApps: input.max };
|
||||
const context = vm.createContext({ root, ApplicationModel: model,
|
||||
Paths: { moddedAppId: id => id === "editor-old" ? "editor" : id },
|
||||
CompositorService: { windowKey: window => "window:" + window.fixtureIndex },
|
||||
AppSearchService: { coreApps: [{ name: "Settings", builtInPluginId: "settings" }] } });
|
||||
vm.runInContext(builder + "\nupdateModel();", context);
|
||||
assert.deepEqual(canonical({ items: context.dockItems, pinnedCount: root.pinnedAppCount, overflowCount: root.overflowItemCount }), canonical(output), JSON.stringify(input));
|
||||
}
|
||||
});
|
||||
|
||||
test("overflow boundary and empty lists preserve entries and metadata", () => {
|
||||
const items = [0, 1, 2].map(index => ({ uniqueKey: String(index), type: "pinned", isPinned: true, payload: index }));
|
||||
|
||||
-4064
File diff suppressed because it is too large
Load Diff
+20
-48
@@ -24,46 +24,19 @@ class Player(ServiceInterface):
|
||||
self.loop = "None"
|
||||
self.shuffle = False
|
||||
self.track = 1
|
||||
self.art_url = ""
|
||||
self.art_revision = 0
|
||||
self.art_directory = None
|
||||
if "--artwork" in sys.argv:
|
||||
self.art_directory = tempfile.TemporaryDirectory(prefix="dms-mpris-art-")
|
||||
self.art_url = self.write_artwork(1)
|
||||
self.art_directory = tempfile.TemporaryDirectory(prefix="dms-mpris-art-") if "--artwork" in sys.argv else None
|
||||
|
||||
def write_artwork(self, track, suffix=""):
|
||||
width = height = 64
|
||||
path = Path(self.art_directory.name) / (str(track) + suffix + ".png")
|
||||
color = [(204, 40, 40), (40, 40, 204), (40, 204, 40)][(track - 1) % 3]
|
||||
rows = (b"\x00" + bytes(color) * width) * height
|
||||
path.write_bytes(b"\x89PNG\r\n\x1a\n" + png_chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)) + png_chunk(b"IDAT", zlib.compress(rows)) + png_chunk(b"IEND", b""))
|
||||
def art_url(self):
|
||||
if not self.art_directory:
|
||||
return ""
|
||||
path = Path(self.art_directory.name) / (str(self.track) + ".png")
|
||||
if not path.exists():
|
||||
width = height = 64
|
||||
color = [(204, 40, 40), (40, 40, 204), (40, 204, 40)][(self.track - 1) % 3]
|
||||
rows = (b"\x00" + bytes(color) * width) * height
|
||||
path.write_bytes(b"\x89PNG\r\n\x1a\n" + png_chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)) + png_chunk(b"IDAT", zlib.compress(rows)) + png_chunk(b"IEND", b""))
|
||||
return path.as_uri()
|
||||
|
||||
async def change_artwork(self, delta):
|
||||
self.art_revision += 1
|
||||
revision = self.art_revision
|
||||
self.track = max(1, self.track + delta)
|
||||
track = self.track
|
||||
self.emit_properties_changed({"Metadata": self.Metadata})
|
||||
await asyncio.sleep(0.5 if track in (2, 6) else 0.08)
|
||||
if revision != self.art_revision:
|
||||
return
|
||||
if track == 4:
|
||||
self.art_url = (Path(self.art_directory.name) / "4.ppm").as_uri()
|
||||
self.emit_properties_changed({"Metadata": self.Metadata})
|
||||
await asyncio.sleep(0.3)
|
||||
self.write_artwork(track)
|
||||
if revision != self.art_revision:
|
||||
return
|
||||
else:
|
||||
self.art_url = self.write_artwork(track)
|
||||
self.emit_properties_changed({"Metadata": self.Metadata})
|
||||
await asyncio.sleep(0.16)
|
||||
if revision != self.art_revision:
|
||||
return
|
||||
self.art_url = self.write_artwork(track, "-copy")
|
||||
self.emit_properties_changed({"Metadata": self.Metadata})
|
||||
|
||||
@dbus_property(access=PropertyAccess.READ)
|
||||
def Position(self) -> "x":
|
||||
elapsed = time.monotonic() - self.position_time if self.state == "Playing" else 0
|
||||
@@ -90,8 +63,8 @@ class Player(ServiceInterface):
|
||||
@dbus_property(access=PropertyAccess.READ)
|
||||
def Metadata(self) -> "a{sv}":
|
||||
metadata = {"xesam:title": Variant("s", "Track " + str(self.track)), "xesam:artist": Variant("as", ["Fixture"]), "mpris:trackid": Variant("o", "/track/" + str(self.track))}
|
||||
if self.art_url:
|
||||
metadata["mpris:artUrl"] = Variant("s", self.art_url)
|
||||
if self.art_directory:
|
||||
metadata["mpris:artUrl"] = Variant("s", self.art_url())
|
||||
return metadata
|
||||
|
||||
@dbus_property(access=PropertyAccess.READ)
|
||||
@@ -154,24 +127,23 @@ class Player(ServiceInterface):
|
||||
def Stop(self):
|
||||
self.set_state("Stopped")
|
||||
|
||||
@method()
|
||||
async def Next(self):
|
||||
if self.art_directory:
|
||||
await self.change_artwork(1)
|
||||
return
|
||||
# Chromium reports Stopped between tracks
|
||||
async def skip(self, delta):
|
||||
state = self.state
|
||||
if state == "Playing":
|
||||
self.set_state("Stopped")
|
||||
await asyncio.sleep(0.08)
|
||||
self.track += 1
|
||||
self.track = max(1, self.track + delta)
|
||||
self.emit_properties_changed({"Metadata": self.Metadata})
|
||||
self.set_state(state)
|
||||
|
||||
@method()
|
||||
async def Previous(self):
|
||||
if self.art_directory:
|
||||
await self.change_artwork(-1)
|
||||
async def Next(self):
|
||||
await self.skip(1)
|
||||
|
||||
@method()
|
||||
async def Previous(self):
|
||||
await self.skip(-1)
|
||||
|
||||
|
||||
class Application(ServiceInterface):
|
||||
|
||||
@@ -301,12 +301,10 @@ ShellRoot {
|
||||
case 4:
|
||||
root.check(!first.isTopBarEdge, "edge changes remain bound");
|
||||
root.check(clock.timeText === "12:05:09 PM" && clock.timeLines.join("|") === "12|05|09|PM", "noon and vertical seconds");
|
||||
for (const locale of ["en_US", "de_DE", "fr_FR", "ar_EG", "zh_CN"]) {
|
||||
clock.locale = Qt.locale(locale);
|
||||
SettingsData.clockDateFormat = "dddd d MMMM yyyy";
|
||||
root.check(clock.dateText === clock.date.toLocaleDateString(Qt.locale(locale), "dddd d MMMM yyyy"), locale + " custom date");
|
||||
root.check(clock.timeText === clock.date.toLocaleTimeString(Qt.locale(locale), "hh:mm:ss AP"), locale + " time");
|
||||
}
|
||||
clock.locale = Qt.locale("ar_EG");
|
||||
SettingsData.clockDateFormat = "dddd d MMMM yyyy";
|
||||
root.check(clock.dateText === clock.date.toLocaleDateString(Qt.locale("ar_EG"), "dddd d MMMM yyyy"), "custom date follows the clock locale");
|
||||
root.check(clock.timeText === clock.date.toLocaleTimeString(Qt.locale("ar_EG"), "hh:mm:ss AP"), "time follows the clock locale");
|
||||
clock.locale = Qt.locale("en_US");
|
||||
SettingsData.clockDateFormat = "ddd d";
|
||||
clock.vertical = false;
|
||||
|
||||
@@ -306,27 +306,6 @@ ShellRoot {
|
||||
stripSlider.destroy();
|
||||
wait(0);
|
||||
|
||||
const ids = ["wifi", "bluetooth", "audioOutput", "audioInput", "volumeSlider", "inputVolumeSlider", "brightnessSlider", "nightMode", "darkMode", "doNotDisturb", "idleInhibitor", "battery", "diskUsage", "colorPicker"];
|
||||
for (const id of ids) {
|
||||
const component = registry.componentForWidget({
|
||||
id
|
||||
});
|
||||
check(component.status === Component.Ready, id + " compiled: " + component.errorString());
|
||||
const widget = component.createObject(scene, {
|
||||
widgetData: {
|
||||
id
|
||||
}
|
||||
});
|
||||
check(widget !== null, id + " created");
|
||||
if (widget.slider)
|
||||
check(WidgetUtils.clampSize({
|
||||
id,
|
||||
w: 1,
|
||||
h: 3
|
||||
}, 4).h === 3, id + " allows vertical resizing");
|
||||
widget.destroy();
|
||||
wait(0);
|
||||
}
|
||||
SessionData.recentColors = Array.from({
|
||||
length: 18
|
||||
}, (_, i) => Qt.rgba(i / 18, 0.5, 0.5, 1).toString());
|
||||
@@ -347,10 +326,10 @@ ShellRoot {
|
||||
NetworkService.wifiNetworks = Array.from({
|
||||
length: 8
|
||||
}, (_, i) => ({
|
||||
ssid: "Net" + i,
|
||||
signal: 90 - i * 8,
|
||||
secured: true
|
||||
}));
|
||||
ssid: "Net" + i,
|
||||
signal: 90 - i * 8,
|
||||
secured: true
|
||||
}));
|
||||
detail.section = "wifi";
|
||||
waitFor(() => !detail.transitioning && detail.pageItem !== null, "wifi detail page loads");
|
||||
settle();
|
||||
|
||||
@@ -7,17 +7,27 @@ import qs.DankCommon.Common as DC
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
readonly property var positions: [0, 2, 0, 3, 0, 2, 1, 3, 1, 0]
|
||||
readonly property var positions: [0, 2, 1, 3, 0]
|
||||
property int step: 0
|
||||
readonly property int position: positions[step]
|
||||
readonly property bool vertical: position === SettingsData.Position.Left || position === SettingsData.Position.Right
|
||||
property var barConfig: ({ id: "fixture", position, spacing: 0, innerPadding: 4 })
|
||||
property var barConfig: ({
|
||||
id: "fixture",
|
||||
position,
|
||||
spacing: 0,
|
||||
innerPadding: 4
|
||||
})
|
||||
property var hyprlandOverviewLoader: null
|
||||
property bool systemTrayMenuOpen: false
|
||||
|
||||
ScriptModel {
|
||||
id: widgets
|
||||
values: [{ widgetId: "clock", id: "clock_0" }]
|
||||
values: [
|
||||
{
|
||||
widgetId: "clock",
|
||||
id: "clock_0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
FloatingWindow {
|
||||
|
||||
@@ -2,7 +2,6 @@ import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankIsland
|
||||
import qs.DankCommon.Common as DC
|
||||
|
||||
@@ -10,7 +9,6 @@ ShellRoot {
|
||||
id: root
|
||||
|
||||
property var hosts: []
|
||||
property bool hostsChecked: false
|
||||
|
||||
Component {
|
||||
id: hostComponent
|
||||
@@ -35,10 +33,27 @@ ShellRoot {
|
||||
onTriggered: {
|
||||
SettingsData.frameEnabled = false;
|
||||
SettingsData.barConfigs = [
|
||||
{ id: "dot", enabled: true, visible: true, dot: true, position: 0 },
|
||||
{ id: "vertical", enabled: true, visible: true, island: true, islandFloating: true, islandPlacement: "free", position: 2, islandSatellitePosition: "island", leftWidgets: ["clock"], rightWidgets: ["clock"] }
|
||||
{
|
||||
id: "dot",
|
||||
enabled: true,
|
||||
visible: true,
|
||||
dot: true,
|
||||
position: 0
|
||||
},
|
||||
{
|
||||
id: "vertical",
|
||||
enabled: true,
|
||||
visible: true,
|
||||
island: true,
|
||||
islandFloating: true,
|
||||
islandPlacement: "free",
|
||||
position: 2
|
||||
}
|
||||
];
|
||||
root.hosts = SettingsData.barConfigs.map(config => hostComponent.createObject(root, { barId: config.id, screen: Quickshell.screens[0] }));
|
||||
root.hosts = SettingsData.barConfigs.map(config => hostComponent.createObject(root, {
|
||||
barId: config.id,
|
||||
screen: Quickshell.screens[0]
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,52 +69,12 @@ ShellRoot {
|
||||
for (const point of [[host.screen.width * 0.8, host.screen.height * 0.7], [0, 0]]) {
|
||||
host.moveTo(point[0], point[1]);
|
||||
surface.surfaceMotion.settle();
|
||||
root.check(Math.abs(surface.currentVisualX + surface.currentVisualWidth / 2 - host.anchorX) <= 1, "visible X follows the anchor");
|
||||
root.check(Math.abs(surface.currentVisualY + surface.currentVisualHeight / 2 - host.anchorY) <= 1, "visible Y follows the anchor");
|
||||
root.check(Math.abs(body.x + host.surfaceX) < 1 && Math.abs(body.y + host.surfaceY) < 1, "window translation preserves screen coordinates");
|
||||
root.check(host.implicitWidth <= Math.ceil(surface.currentVisualWidth) + 1, "moving buffer stays bounded to the face");
|
||||
}
|
||||
host.islandController.requestActivity("controlcenter", true, false);
|
||||
surface.surfaceMotion.settle();
|
||||
root.check(host.implicitWidth < host.screen.width && host.implicitHeight < host.screen.height, "expanded buffer is bounded to the sheet");
|
||||
root.check(surface.currentScreenX >= 0 && surface.currentScreenY >= 0, "sheet stays on screen");
|
||||
host.islandController.requestCollapse();
|
||||
surface.surfaceMotion.settle();
|
||||
root.check(!surface.motionRunning, "idle motion stops");
|
||||
}
|
||||
root.hostsChecked = true;
|
||||
} catch (error) {
|
||||
console.error("FIXTURE_FAIL", error.message);
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Repeater {
|
||||
id: bars
|
||||
model: root.hostsChecked ? SettingsData.barConfigs : []
|
||||
delegate: DankBar {
|
||||
required property var modelData
|
||||
barConfig: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 0
|
||||
readonly property var band: bars.count === 2 ? bars.itemAt(1).barVariants.instances[0] ?? null : null
|
||||
running: !!band && band.height === band.screen.height && band.width < band.height
|
||||
onTriggered: {
|
||||
try {
|
||||
root.check(bars.itemAt(0).barVariants.instances.length === 0, "a dot has no satellite window");
|
||||
const band = bars.itemAt(1).barVariants.instances[0];
|
||||
root.check(bars.itemAt(1).barVariants.instances.length === 1 && !band.islandHost, "a free island keeps one satellite window without a pill");
|
||||
root.check(band.exclusiveZone <= 0, "the satellite window reserves nothing");
|
||||
const leading = band.leadingSectionRect;
|
||||
const trailing = band.trailingSectionRect;
|
||||
root.check(leading.y + leading.h <= trailing.y, "near island keeps left before right");
|
||||
root.check(Math.abs((leading.y + trailing.y + trailing.h) / 2 - band.height / 2) <= 2, "near island gathers at the edge centre");
|
||||
console.log("FIXTURE_PASS");
|
||||
} catch (error) {
|
||||
console.error("FIXTURE_FAIL", error.message);
|
||||
|
||||
@@ -46,6 +46,7 @@ ShellRoot {
|
||||
DC.Style.settings = SettingsData;
|
||||
DC.I18n.backend = I18n;
|
||||
SettingsData.frameEnabled = false;
|
||||
SettingsData.reduceMotion = true;
|
||||
SettingsData.launcherStyle = "island";
|
||||
SettingsData.rememberLastQuery = false;
|
||||
SettingsData.barConfigs = [
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankIsland
|
||||
import qs.DankCommon.Common as DC
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
property int fallbackCalls: 0
|
||||
readonly property var hosts: islands.hosts()
|
||||
|
||||
TestCase { id: input; when: false }
|
||||
QtObject {
|
||||
id: cachedLauncher
|
||||
function show() { root.fallbackCalls++; }
|
||||
function hide() {}
|
||||
function toggle() { root.fallbackCalls++; }
|
||||
}
|
||||
Item {
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: SettingsData.barConfigs
|
||||
objectProp: "id"
|
||||
}
|
||||
delegate: DankBar {
|
||||
required property var modelData
|
||||
barConfig: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
DankIsland { id: islands }
|
||||
|
||||
function check(value, label) {
|
||||
if (!value)
|
||||
throw new Error(label);
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.watchFiles = false;
|
||||
DC.Style.theme = Theme;
|
||||
DC.Style.settings = SettingsData;
|
||||
DC.I18n.backend = I18n;
|
||||
}
|
||||
Timer {
|
||||
interval: 0
|
||||
running: SettingsData._hasLoaded && SessionData._hasLoaded
|
||||
onTriggered: {
|
||||
SettingsData.frameEnabled = false;
|
||||
SettingsData.reduceMotion = true;
|
||||
SettingsData.launcherStyle = "island";
|
||||
SettingsData.barConfigs = [
|
||||
{ id: "edge", island: true, enabled: true, visible: true, position: 0 },
|
||||
{ id: "dot", dot: true, enabled: true, visible: true, position: 0 },
|
||||
{ id: "floating", island: true, enabled: true, visible: true, position: 2,
|
||||
islandFloating: true, islandPlacement: "free", islandSharedRouting: "last-used" }
|
||||
];
|
||||
PopoutService.dankLauncherV2Modal = cachedLauncher;
|
||||
}
|
||||
}
|
||||
// requestActivity defers activation until the launcher visuals load, so each step polls.
|
||||
Timer {
|
||||
id: steps
|
||||
|
||||
property int index: 0
|
||||
property int waited: 0
|
||||
readonly property var order: ["dot", "floating", "dot"]
|
||||
readonly property var screen: Quickshell.screens[0]
|
||||
readonly property var body: IslandHostRegistry.hostFor(screen.name, order[Math.min(index, order.length - 1)])
|
||||
|
||||
interval: 25
|
||||
repeat: true
|
||||
running: root.hosts.length === 3 && root.hosts.every(host => host.islandController && (!host.free || (host.width === host.implicitWidth && host.height === host.implicitHeight && host.width > 0)))
|
||||
|
||||
function finish(message) {
|
||||
if (message)
|
||||
console.error("FIXTURE_FAIL", message);
|
||||
else
|
||||
console.log("FIXTURE_PASS");
|
||||
stop();
|
||||
Qt.quit();
|
||||
}
|
||||
|
||||
function launcherOpen(controller) {
|
||||
return controller.expanded && controller.activeActivity === "launcher";
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
if (++waited > 120) {
|
||||
finish("timed out at step " + index);
|
||||
return;
|
||||
}
|
||||
const controller = body.islandController;
|
||||
try {
|
||||
if (index < order.length) {
|
||||
const surface = body.surface;
|
||||
const barId = order[index];
|
||||
if (!controller.expanded && !launcherOpen(controller) && SettingsData.lastUsedBarByScreen[screen.name] !== barId) {
|
||||
input.mouseMove(surface, surface.currentVisualX + surface.currentVisualWidth / 2, surface.currentVisualY + surface.currentVisualHeight / 2, 0);
|
||||
input.mouseClick(surface, surface.currentVisualX + surface.currentVisualWidth / 2, surface.currentVisualY + surface.currentVisualHeight / 2, Qt.LeftButton, Qt.NoModifier, 0);
|
||||
root.check(SettingsData.lastUsedBarByScreen[screen.name] === barId, "click must select " + barId);
|
||||
PopoutService.openDankLauncherV2();
|
||||
root.check(root.fallbackCalls === 0, "cached launcher must not intercept shared shortcuts");
|
||||
return;
|
||||
}
|
||||
if (!launcherOpen(controller))
|
||||
return;
|
||||
PopoutService.toggleDankLauncherV2();
|
||||
root.check(!controller.expanded, "second shortcut collapses the selected launcher in " + barId);
|
||||
SettingsData.lastUsedBarByScreen = {};
|
||||
index++;
|
||||
waited = 0;
|
||||
return;
|
||||
}
|
||||
if (index === order.length) {
|
||||
SettingsData.frameEnabled = true;
|
||||
input.mouseClick(body.surface, body.surface.currentVisualX + body.surface.currentVisualWidth / 2, body.surface.currentVisualY + body.surface.currentVisualHeight / 2, Qt.LeftButton, Qt.NoModifier, 0);
|
||||
PopoutService.openDankLauncherV2();
|
||||
root.check(root.fallbackCalls === 0, "frame mode does not steal shared shortcuts");
|
||||
index++;
|
||||
waited = 0;
|
||||
return;
|
||||
}
|
||||
if (!launcherOpen(controller))
|
||||
return;
|
||||
PopoutService.closeDankLauncherV2();
|
||||
root.check(!controller.expanded, "close collapses the selected dot launcher");
|
||||
finish("");
|
||||
} catch (error) {
|
||||
finish(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankIsland
|
||||
import qs.Modules.Frame
|
||||
import qs.DankCommon.Common as DC
|
||||
|
||||
// Qt 6.11 caches pointer bounds at first hover (PointerOverflowMarker): the sheet grown past the band must still take the pointer.
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
readonly property var screen: Quickshell.screens[0]
|
||||
readonly property string barId: "main"
|
||||
|
||||
TestCase {
|
||||
id: input
|
||||
when: false
|
||||
}
|
||||
|
||||
Item {
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: SettingsData.barConfigs
|
||||
objectProp: "id"
|
||||
}
|
||||
delegate: DankBar {
|
||||
required property var modelData
|
||||
barConfig: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Frame {}
|
||||
|
||||
DankIsland {}
|
||||
|
||||
function config(withIsland) {
|
||||
return [
|
||||
{
|
||||
id: root.barId,
|
||||
enabled: true,
|
||||
visible: true,
|
||||
position: 0,
|
||||
screenPreferences: ["all"],
|
||||
leftWidgets: ["clock"],
|
||||
centerWidgets: withIsland ? [
|
||||
{
|
||||
id: "island",
|
||||
enabled: true
|
||||
},
|
||||
"clock"] : ["clock"],
|
||||
rightWidgets: ["clock"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.watchFiles = false;
|
||||
DC.Style.theme = Theme;
|
||||
DC.Style.settings = SettingsData;
|
||||
DC.I18n.backend = I18n;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 0
|
||||
running: SettingsData._hasLoaded && SessionData._hasLoaded
|
||||
onTriggered: {
|
||||
SettingsData.reduceMotion = true;
|
||||
SettingsData.frameMode = "connected";
|
||||
SettingsData.frameEnabled = true;
|
||||
FrameTransitionState.acknowledge(FrameTransitionState.revision);
|
||||
SettingsData.barConfigs = root.config(false);
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: steps
|
||||
|
||||
property bool expanding: false
|
||||
property int waited: 0
|
||||
readonly property var body: IslandHostRegistry.hostFor(root.screen.name, root.barId)
|
||||
readonly property var bareBody: BarWidgetService.frameHostedBars[root.screen.name]?.[root.barId] ?? null
|
||||
readonly property var frame: body?.hostWindow ?? null
|
||||
|
||||
interval: 25
|
||||
repeat: true
|
||||
running: (!!body && !!body.islandController || !!bareBody) && SettingsData.frameEnabled
|
||||
|
||||
function finish(message) {
|
||||
if (message)
|
||||
console.error("FIXTURE_FAIL", message);
|
||||
else
|
||||
console.log("FIXTURE_PASS");
|
||||
stop();
|
||||
Qt.quit();
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
if (++waited > 800) {
|
||||
finish("timed out " + (expanding ? "expanding the sheet" : "waiting for the island at rest"));
|
||||
return;
|
||||
}
|
||||
if (!body) {
|
||||
const content = bareBody.hostWindow?.contentItem ?? null;
|
||||
if (!content || content.width <= 0 || bareBody.width <= 0)
|
||||
return;
|
||||
input.mouseMove(content, 40, bareBody.hostOffsetY + bareBody.height / 2, 0);
|
||||
SettingsData.barConfigs = root.config(true);
|
||||
waited = 0;
|
||||
return;
|
||||
}
|
||||
const controller = body.islandController;
|
||||
if (!expanding) {
|
||||
if (body.motionRunning || body.bandThickness <= 0 || frame.cutoutTopInset <= 0)
|
||||
return;
|
||||
input.mouseMove(frame.contentItem, 40, frame.cutoutTopInset / 2, 0);
|
||||
controller.requestToggle(true);
|
||||
expanding = true;
|
||||
waited = 0;
|
||||
return;
|
||||
}
|
||||
if (!controller.expanded || body.motionRunning)
|
||||
return;
|
||||
const sheet = ConnectedModeState.surfaceDescriptor(root.screen.name, "island");
|
||||
if (!sheet.presented)
|
||||
return;
|
||||
input.mouseMove(frame.contentItem, sheet.bodyRect.x + sheet.bodyRect.width / 2, sheet.bodyRect.y + sheet.bodyRect.height - 30, 0);
|
||||
finish(controller.pointerInside ? "" : "hover past the band reaches the sheet");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,367 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankIsland
|
||||
import qs.DankCommon.Common as DC
|
||||
|
||||
// A standard bar hosting the island widget in each section: the pill sits on its slot inside the band, the window is
|
||||
// band sized at rest and grows only for the sheet, the exclusive zone never moves, and the sheet stays on screen.
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
readonly property var screen: Quickshell.screens[0]
|
||||
readonly property var cases: [
|
||||
{
|
||||
position: 0,
|
||||
section: "left"
|
||||
},
|
||||
{
|
||||
position: 0,
|
||||
section: "center"
|
||||
},
|
||||
{
|
||||
position: 0,
|
||||
section: "right"
|
||||
},
|
||||
{
|
||||
position: 1,
|
||||
section: "center"
|
||||
},
|
||||
{
|
||||
position: 1,
|
||||
section: "right"
|
||||
},
|
||||
{
|
||||
position: 2,
|
||||
section: "left"
|
||||
},
|
||||
{
|
||||
position: 2,
|
||||
section: "center"
|
||||
},
|
||||
{
|
||||
position: 2,
|
||||
section: "right"
|
||||
}
|
||||
]
|
||||
property int caseIndex: 0
|
||||
readonly property string barId: "main" + caseIndex
|
||||
|
||||
Item {
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: SettingsData.barConfigs
|
||||
objectProp: "id"
|
||||
}
|
||||
delegate: DankBar {
|
||||
required property var modelData
|
||||
barConfig: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIsland {
|
||||
id: islands
|
||||
}
|
||||
|
||||
function check(value, label) {
|
||||
if (!value)
|
||||
throw new Error(label);
|
||||
}
|
||||
|
||||
function applyCase() {
|
||||
const current = root.cases[root.caseIndex];
|
||||
const widgets = {
|
||||
leftWidgets: ["clock"],
|
||||
centerWidgets: ["clock"],
|
||||
rightWidgets: ["clock"]
|
||||
};
|
||||
widgets[current.section + "Widgets"] = ["clock",
|
||||
{
|
||||
id: "island",
|
||||
enabled: true
|
||||
},
|
||||
"clock"];
|
||||
// Flat keys are what the bar used as a free island; the hosted island must ignore them.
|
||||
SettingsData.barConfigs = [Object.assign({
|
||||
id: root.barId,
|
||||
enabled: true,
|
||||
visible: true,
|
||||
position: current.position,
|
||||
islandFloating: true,
|
||||
islandPlacement: "free",
|
||||
islandSatellitePosition: "island",
|
||||
islandPalette: "dim",
|
||||
islandUseOverlayLayer: true,
|
||||
screenPreferences: ["all"]
|
||||
}, widgets)];
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.watchFiles = false;
|
||||
DC.Style.theme = Theme;
|
||||
DC.Style.settings = SettingsData;
|
||||
DC.I18n.backend = I18n;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 0
|
||||
running: SettingsData._hasLoaded && SessionData._hasLoaded
|
||||
onTriggered: {
|
||||
SettingsData.reduceMotion = true;
|
||||
SettingsData.frameEnabled = false;
|
||||
root.applyCase();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: steps
|
||||
|
||||
property int index: 0
|
||||
property int waited: 0
|
||||
// A plain bar's window already carries its shadow padding; the island must add nothing at rest.
|
||||
property real restCross: 0
|
||||
property real restLeadingStart: 0
|
||||
property real lastSlotPos: -1
|
||||
property bool sprung: false
|
||||
property string lastOverlap: ""
|
||||
readonly property var body: IslandHostRegistry.hostFor(root.screen.name, root.barId)
|
||||
readonly property var slot: body?.slotItem ?? null
|
||||
readonly property var window: body?.hostWindow ?? null
|
||||
readonly property bool vertical: root.cases[root.caseIndex].position === 2
|
||||
readonly property bool farEdge: root.cases[root.caseIndex].position === 1 || root.cases[root.caseIndex].position === 3
|
||||
|
||||
interval: 25
|
||||
repeat: true
|
||||
running: !!body && !!body.islandController
|
||||
|
||||
function finish(message) {
|
||||
if (message)
|
||||
console.error("FIXTURE_FAIL", message);
|
||||
else
|
||||
console.log("FIXTURE_PASS");
|
||||
stop();
|
||||
Qt.quit();
|
||||
}
|
||||
|
||||
function next() {
|
||||
index++;
|
||||
waited = 0;
|
||||
}
|
||||
|
||||
function label(text) {
|
||||
return `case ${root.caseIndex} (${["top", "bottom", "left"][root.cases[root.caseIndex].position]} bar, ${root.cases[root.caseIndex].section}): ${text}`;
|
||||
}
|
||||
|
||||
// Independent of the host's own anchor math: where the slot really is, in host coordinates.
|
||||
function slotCentreMapped() {
|
||||
const pos = slot.mapToItem(body, slot.width / 2, slot.height / 2);
|
||||
return vertical ? pos.y : pos.x;
|
||||
}
|
||||
|
||||
function homeFace(item) {
|
||||
for (const child of item.children) {
|
||||
if (child.activity === "home" && child.face !== undefined && child.item)
|
||||
return child;
|
||||
const found = homeFace(child);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function faceCentre() {
|
||||
const face = homeFace(body.surface);
|
||||
if (!face)
|
||||
return NaN;
|
||||
const pos = face.mapToItem(body, face.width / 2, face.height / 2);
|
||||
return vertical ? pos.y : pos.x;
|
||||
}
|
||||
|
||||
// Moves with the band when a far-edge window grows.
|
||||
function leadingCross() {
|
||||
const leading = body.barBody.sectionRectFor("left");
|
||||
return vertical ? leading.x : leading.y;
|
||||
}
|
||||
|
||||
function windowCross() {
|
||||
return vertical ? window.implicitWidth : window.implicitHeight;
|
||||
}
|
||||
|
||||
function pillCrossStart() {
|
||||
return vertical ? body.currentVisualX : body.currentVisualY;
|
||||
}
|
||||
|
||||
function pillCrossEnd() {
|
||||
return vertical ? body.currentVisualX + body.currentVisualWidth : body.currentVisualY + body.currentVisualHeight;
|
||||
}
|
||||
|
||||
// A far-edge sheet grows away from the screen edge, into negative host coordinates.
|
||||
function sheetOut() {
|
||||
return farEdge ? pillCrossStart() < -40 : pillCrossEnd() > body.bandThickness + 40;
|
||||
}
|
||||
|
||||
function pillAlongCentre() {
|
||||
return body.currentAlongPos + body.currentVisualAlong / 2;
|
||||
}
|
||||
|
||||
function slotSize() {
|
||||
return vertical ? slot.height : slot.width;
|
||||
}
|
||||
|
||||
function intersects(rect) {
|
||||
if (!rect || rect.w <= 0 || rect.h <= 0)
|
||||
return false;
|
||||
const x = body.originOffsetX + body.currentVisualX;
|
||||
const y = body.originOffsetY + body.currentVisualY;
|
||||
return x < rect.x + rect.w && x + body.currentVisualWidth > rect.x && y < rect.y + rect.h && y + body.currentVisualHeight > rect.y;
|
||||
}
|
||||
|
||||
// Rows re-place siblings on polish, one frame after the slot resizes: wait for the slot to hold still across two ticks and for the neighbours to have moved out of its way.
|
||||
function layoutPending() {
|
||||
const pos = slotCentreMapped();
|
||||
const moved = Math.abs(pos - lastSlotPos) > 0.5;
|
||||
lastSlotPos = pos;
|
||||
lastOverlap = overlapsNeighbour();
|
||||
return moved || lastOverlap !== "";
|
||||
}
|
||||
|
||||
function overlapsNeighbour() {
|
||||
const wrapper = slot.parent.parent;
|
||||
for (const sibling of wrapper.parent.children) {
|
||||
const item = sibling === wrapper ? null : (sibling.widgetItem ?? sibling.item ?? null);
|
||||
if (!item || !item.visible || item.width <= 0 || item.height <= 0)
|
||||
continue;
|
||||
const pos = item.mapToItem(null, 0, 0);
|
||||
if (intersects({
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
w: item.width,
|
||||
h: item.height
|
||||
}))
|
||||
return `sibling at ${pos.x},${pos.y} ${item.width}x${item.height} vs pill ${body.originOffsetX + body.currentVisualX},${body.originOffsetY + body.currentVisualY} ${body.currentVisualWidth}x${body.currentVisualHeight} (slot ${slot.mapToItem(null, 0, 0).x} ${slot.width}, section ${wrapper.parent.width})`;
|
||||
}
|
||||
const section = root.cases[root.caseIndex].section;
|
||||
const leading = body.barBody.sectionRectFor("left");
|
||||
const trailing = body.barBody.sectionRectFor("right");
|
||||
if (section !== "left" && intersects(leading))
|
||||
return "leading section " + JSON.stringify(leading);
|
||||
if (section !== "right" && intersects(trailing))
|
||||
return "trailing section " + JSON.stringify(trailing);
|
||||
return "";
|
||||
}
|
||||
|
||||
function checkRest(context) {
|
||||
root.check(body.chrome === "band", label("standard hosting wears the band"));
|
||||
root.check(!body.freeMode && !body.floating, label("island-mode placement keys do not leak into the hosted island"));
|
||||
root.check(pillCrossStart() >= 0 && pillCrossEnd() <= body.bandThickness + 0.5, label(`${context}: pill inside the band ${pillCrossStart()}..${pillCrossEnd()} vs ${body.bandThickness}`));
|
||||
root.check(Math.abs(slotSize() - body.compactTargetSize) <= 0.5, label(`${context}: slot reserves the compact size ${slotSize()} vs ${body.compactTargetSize}`));
|
||||
root.check(Math.abs(slotSize() - body.currentVisualAlong) <= 1, label(`${context}: pill fills its slot ${body.currentVisualAlong} vs ${slotSize()}`));
|
||||
root.check(Math.abs(pillAlongCentre() - slotCentreMapped()) <= 1, label(`${context}: pill centred on its slot ${pillAlongCentre()} vs ${slotCentreMapped()}`));
|
||||
if (context !== "wide face")
|
||||
root.check(Math.abs(faceCentre() - pillAlongCentre()) <= 1, label(`${context}: compact face drawn on its pill ${faceCentre()} vs ${pillAlongCentre()}`));
|
||||
const overlap = overlapsNeighbour();
|
||||
root.check(!overlap, label(`${context}: pill clear of its neighbours: ${overlap}`));
|
||||
if (restCross <= 0)
|
||||
restCross = windowCross();
|
||||
root.check(restCross < body.bandThickness + 48 && Math.abs(windowCross() - restCross) <= 1, label(`${context}: window keeps its rest size ${windowCross()} vs ${restCross} (band ${body.bandThickness})`));
|
||||
root.check(Math.abs(window.exclusiveZone - body.bandThickness) <= 1, label(`${context}: exclusive zone is the band ${window.exclusiveZone} vs ${body.bandThickness}`));
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
if (++waited > 240) {
|
||||
finish(label(!slot ? "slot never registered" : index === 4 ? "window never shrank back after collapse" : "timed out at step " + index + (lastOverlap ? ", still overlapping: " + lastOverlap : "")));
|
||||
return;
|
||||
}
|
||||
const controller = body.islandController;
|
||||
try {
|
||||
switch (index) {
|
||||
case 0:
|
||||
if (!slot || body.motionRunning || body.bandThickness <= 0 || window.exclusiveZone <= 0 || body.compactTargetSize <= 0 || slotSize() !== body.compactTargetSize || layoutPending())
|
||||
return;
|
||||
restLeadingStart = leadingCross();
|
||||
root.check(ShellLayout.standaloneScreens(root.barId).length === 1, label("the bar keeps its own window"));
|
||||
root.check(slot.parent.section === root.cases[root.caseIndex].section, label("slot sits in the configured section"));
|
||||
root.check(window.mask.regions.some(region => region.item === body.inputMaskItem), label("bar mask carries the island envelope"));
|
||||
checkRest("home");
|
||||
controller.requestSystemActivity("volume");
|
||||
next();
|
||||
return;
|
||||
case 1:
|
||||
if (controller.activeActivity !== "volume" || body.motionRunning || slotSize() !== body.compactTargetSize || layoutPending())
|
||||
return;
|
||||
root.check(body.compactTargetSize >= 200, label("volume face is wide"));
|
||||
checkRest("wide face");
|
||||
controller.finishTransient();
|
||||
next();
|
||||
return;
|
||||
case 2:
|
||||
if (controller.activeActivity !== "home" || body.motionRunning || slotSize() !== body.compactTargetSize || layoutPending())
|
||||
return;
|
||||
checkRest("home again");
|
||||
controller.requestToggle(true);
|
||||
next();
|
||||
return;
|
||||
case 3:
|
||||
if (!controller.expanded || body.motionRunning || !sheetOut())
|
||||
return;
|
||||
root.check(windowCross() >= body.hostThickness - 1, label(`window grew for the sheet ${windowCross()} vs ${body.hostThickness}`));
|
||||
root.check(Math.abs(window.exclusiveZone - body.bandThickness) <= 1, label("exclusive zone stays the band while expanded"));
|
||||
root.check(body.currentAlongPos >= 0 && body.currentAlongPos + body.currentVisualAlong <= (vertical ? body.height : body.width), label(`sheet stays inside the bar ${body.currentAlongPos}..${body.currentAlongPos + body.currentVisualAlong}`));
|
||||
root.check(body.surface.embeddedJoinRadius > 0, label("join corners are visible"));
|
||||
// The content starts past the fold: the sheet clears the band by exactly its content size.
|
||||
const contentCross = vertical ? controller.expandedTarget.width : controller.expandedTarget.height;
|
||||
const pastBand = farEdge ? -pillCrossStart() : pillCrossEnd() - body.bandThickness;
|
||||
root.check(Math.abs(pastBand - contentCross) <= 1, label(`sheet content clears the band ${pastBand} vs ${contentCross} (inset ${body.nearInset})`));
|
||||
// A far-edge band moves by the growth; the section rects the dismiss holes use must follow it.
|
||||
root.check(Math.abs((leadingCross() - restLeadingStart) - (farEdge ? windowCross() - restCross : 0)) <= 1, label(`section rects follow the band ${leadingCross()} vs rest ${restLeadingStart}, growth ${windowCross() - restCross}`));
|
||||
controller.requestCollapse();
|
||||
next();
|
||||
return;
|
||||
case 4:
|
||||
if (controller.expanded || body.motionRunning || Math.abs(windowCross() - restCross) > 1 || layoutPending())
|
||||
return;
|
||||
checkRest("after collapse");
|
||||
if (root.caseIndex + 1 >= root.cases.length) {
|
||||
// One real spring at the end: the window must stay grown until the sheet has fully sprung back.
|
||||
SettingsData.reduceMotion = false;
|
||||
controller.requestToggle(true);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
index = 0;
|
||||
waited = 0;
|
||||
restCross = 0;
|
||||
lastSlotPos = -1;
|
||||
root.caseIndex++;
|
||||
root.applyCase();
|
||||
return;
|
||||
case 5:
|
||||
if (!controller.expanded || body.motionRunning || !sheetOut())
|
||||
return;
|
||||
root.check(windowCross() >= body.hostThickness - 1, label(`window grew for the sprung sheet ${windowCross()} vs ${body.hostThickness}`));
|
||||
controller.requestCollapse();
|
||||
next();
|
||||
return;
|
||||
case 6:
|
||||
if (body.motionRunning) {
|
||||
sprung = true;
|
||||
if (sheetOut())
|
||||
root.check(body.sheetOut && windowCross() >= body.hostThickness - 1, label(`window holds its size while the sheet springs back ${windowCross()} vs ${body.hostThickness}`));
|
||||
return;
|
||||
}
|
||||
if (controller.expanded || Math.abs(windowCross() - restCross) > 1)
|
||||
return;
|
||||
root.check(sprung, label("collapse ran a real spring"));
|
||||
root.check(!body.sheetOut, label("latch clears once the spring settles"));
|
||||
finish("");
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
finish(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankIsland
|
||||
import qs.Modules.Frame
|
||||
import qs.DankCommon.Common as DC
|
||||
|
||||
// A connected-frame bar hosting the island widget: the frame hosts the body, the SDF slot follows the sheet, shared shortcuts
|
||||
// route into it, a right-section island bulges out of the ring on the right, and transient launcher windows join the grab.
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
property int fallbackCalls: 0
|
||||
readonly property var screen: Quickshell.screens[0]
|
||||
readonly property string barId: "main"
|
||||
|
||||
TestCase {
|
||||
id: input
|
||||
when: false
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: cachedLauncher
|
||||
property bool edgeHoverManaged: false
|
||||
property bool triggerUsesOverlayLayer: false
|
||||
function show() {
|
||||
root.fallbackCalls++;
|
||||
}
|
||||
function hide() {}
|
||||
function toggle() {
|
||||
root.fallbackCalls++;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: SettingsData.barConfigs
|
||||
objectProp: "id"
|
||||
}
|
||||
delegate: DankBar {
|
||||
required property var modelData
|
||||
barConfig: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Frame {}
|
||||
|
||||
DankIsland {
|
||||
id: islands
|
||||
}
|
||||
|
||||
function check(value, label) {
|
||||
if (!value)
|
||||
throw new Error(label);
|
||||
}
|
||||
|
||||
function config(position, routing, section) {
|
||||
const widgets = { leftWidgets: ["clock"], centerWidgets: ["clock"], rightWidgets: ["clock"] };
|
||||
if (routing)
|
||||
widgets[(section ?? "center") + "Widgets"] = [{ id: "island", enabled: true, islandSharedRouting: routing }, "clock"];
|
||||
// Flat keys are the bar's island-mode settings; the hosted island reads only its widget entry.
|
||||
return [Object.assign({ id: root.barId, enabled: true, visible: true, position, islandFloating: true, islandPlacement: "free", islandSatellitePosition: "island", islandPalette: "dim", islandUseOverlayLayer: true, islandSharedRouting: "always", screenPreferences: ["all"] }, widgets)];
|
||||
}
|
||||
|
||||
// Stands in for a launcher context menu window; the frame's grab must include it.
|
||||
PanelWindow {
|
||||
id: probe
|
||||
|
||||
visible: false
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.watchFiles = false;
|
||||
DC.Style.theme = Theme;
|
||||
DC.Style.settings = SettingsData;
|
||||
DC.I18n.backend = I18n;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 0
|
||||
running: SettingsData._hasLoaded && SessionData._hasLoaded
|
||||
onTriggered: {
|
||||
SettingsData.reduceMotion = true;
|
||||
SettingsData.launcherStyle = "island";
|
||||
SettingsData.frameMode = "connected";
|
||||
SettingsData.frameEnabled = true;
|
||||
FrameTransitionState.acknowledge(FrameTransitionState.revision);
|
||||
// The island joins a bar the pointer has already crossed; a hover must still reach the sheet past the band.
|
||||
SettingsData.barConfigs = root.config(0, "");
|
||||
PopoutService.dankLauncherV2Modal = cachedLauncher;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: steps
|
||||
|
||||
property int index: 0
|
||||
property int waited: 0
|
||||
readonly property var body: IslandHostRegistry.hostFor(root.screen.name, root.barId)
|
||||
readonly property var bareBody: BarWidgetService.frameHostedBars[root.screen.name]?.[root.barId] ?? null
|
||||
readonly property var frame: body?.hostWindow ?? null
|
||||
readonly property string key: JSON.stringify([root.screen.name, root.barId])
|
||||
|
||||
interval: 25
|
||||
repeat: true
|
||||
running: (!!body && !!body.islandController || !!bareBody) && SettingsData.frameEnabled
|
||||
|
||||
function finish(message) {
|
||||
if (message)
|
||||
console.error("FIXTURE_FAIL", message);
|
||||
else
|
||||
console.log("FIXTURE_PASS");
|
||||
stop();
|
||||
Qt.quit();
|
||||
}
|
||||
|
||||
function next() {
|
||||
index++;
|
||||
waited = 0;
|
||||
}
|
||||
|
||||
function pillStart() {
|
||||
return body.hostOriginY + body.currentVisualY;
|
||||
}
|
||||
|
||||
// The centre section places its widgets from a timer; until it runs the slot still sits at x 0. Side rows start at 0.
|
||||
function slotPlaced(section) {
|
||||
const slot = body.slotItem;
|
||||
const target = section ?? "center";
|
||||
return !!slot && slot.width > 0 && slot.parent.section === target && (target !== "center" || slot.x > 0);
|
||||
}
|
||||
|
||||
function pillOnSlot() {
|
||||
const slot = body.slotItem;
|
||||
const expected = slot.mapToItem(body, slot.width / 2, 0).x;
|
||||
const centre = body.currentAlongPos + body.currentVisualAlong / 2;
|
||||
root.check(!body.freeMode && Math.abs(centre - expected) <= 1, `pill centred on its slot: ${centre} vs ${expected}`);
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
if (++waited > 240) {
|
||||
finish("timed out at step " + index);
|
||||
return;
|
||||
}
|
||||
if (!body) {
|
||||
const content = bareBody.hostWindow?.contentItem ?? null;
|
||||
if (!content || content.width <= 0 || bareBody.width <= 0)
|
||||
return;
|
||||
input.mouseMove(content, 40, bareBody.hostOffsetY + bareBody.height / 2, 0);
|
||||
SettingsData.barConfigs = root.config(0, "normal");
|
||||
waited = 0;
|
||||
return;
|
||||
}
|
||||
const controller = body.islandController;
|
||||
try {
|
||||
switch (index) {
|
||||
case 0:
|
||||
root.check(ShellLayout.frameKeys(root.screen).includes(key), "hosting bar is frame hosted");
|
||||
root.check(ShellLayout.islandKeys.includes(key), "hosting bar owns an island slot");
|
||||
root.check(!SettingsData.dankIslandOwnsEdge(root.screen, "top"), "hosted island does not own the edge");
|
||||
root.check(SettingsData.dankIslandEnabled, "island module is on");
|
||||
root.check(ShellLayout.standaloneScreens(root.barId).length === 0, "no standalone bar window");
|
||||
root.check(frame.targetScreen === root.screen, "body is hosted by the frame window");
|
||||
root.check(body.chrome === "none", "frame hosting paints through the SDF");
|
||||
if (body.motionRunning || body.bandThickness <= 0 || frame.cutoutTopInset <= 0 || !slotPlaced())
|
||||
return;
|
||||
root.check(pillStart() >= 0 && pillStart() + body.currentVisualHeight <= frame.cutoutTopInset + 0.5, `pill inside the top band: ${pillStart()}..${pillStart() + body.currentVisualHeight} vs ${frame.cutoutTopInset}`);
|
||||
pillOnSlot();
|
||||
root.check((body.barBody.sectionRectFor("left")?.w ?? 0) > 0, "section rect passthrough");
|
||||
root.check(frame.mask.regions.some(region => region.item === body.inputMaskItem), "frame mask carries the island envelope");
|
||||
// Hovered at rest with the pill inside the band, then expanded: the sheet must still take the pointer.
|
||||
input.mouseMove(frame.contentItem, 40, frame.cutoutTopInset / 2, 0);
|
||||
controller.requestToggle(true);
|
||||
next();
|
||||
return;
|
||||
case 1: {
|
||||
if (!controller.expanded || body.motionRunning)
|
||||
return;
|
||||
const sheet = ConnectedModeState.surfaceDescriptor(root.screen.name, "island");
|
||||
if (!sheet.presented)
|
||||
return;
|
||||
input.mouseMove(frame.contentItem, sheet.bodyRect.x + sheet.bodyRect.width / 2, sheet.bodyRect.y + sheet.bodyRect.height - 30, 0);
|
||||
root.check(controller.pointerInside, "hover past the band reaches the sheet");
|
||||
input.mouseMove(frame.contentItem, 40, frame.height - 3, 0);
|
||||
controller.requestCollapse();
|
||||
next();
|
||||
return;
|
||||
}
|
||||
case 2:
|
||||
if (controller.expanded || body.motionRunning)
|
||||
return;
|
||||
SettingsData.barConfigs = root.config(1, "normal");
|
||||
next();
|
||||
return;
|
||||
case 3:
|
||||
if (body.edge !== "bottom" || body.motionRunning || !slotPlaced())
|
||||
return;
|
||||
root.check(pillStart() >= frame.height - frame.cutoutBottomInset - 0.5 && pillStart() + body.currentVisualHeight <= frame.height + 0.5, `pill inside the bottom band: ${pillStart()}..${pillStart() + body.currentVisualHeight} vs ${frame.height - frame.cutoutBottomInset}..${frame.height}`);
|
||||
pillOnSlot();
|
||||
SettingsData.barConfigs = root.config(0, "always");
|
||||
next();
|
||||
return;
|
||||
case 4:
|
||||
if (body.edge !== "top" || body.motionRunning)
|
||||
return;
|
||||
PopoutService.openDankLauncherV2();
|
||||
root.check(root.fallbackCalls === 0, "always-here keeps the launcher out of the modal");
|
||||
next();
|
||||
return;
|
||||
case 5: {
|
||||
if (!controller.expanded || controller.activeActivity !== "launcher" || body.motionRunning)
|
||||
return;
|
||||
const descriptor = ConnectedModeState.surfaceDescriptor(root.screen.name, "island");
|
||||
if (!descriptor.presented)
|
||||
return;
|
||||
root.check(descriptor.kind === "island" && descriptor.barSide === "top", "island descriptor kind and side");
|
||||
const dx = Math.abs(descriptor.bodyRect.x - (body.hostOriginX + body.currentVisualX));
|
||||
const dy = Math.abs(descriptor.bodyRect.y - pillStart());
|
||||
const dw = Math.abs(descriptor.bodyRect.width - body.currentVisualWidth);
|
||||
const dh = Math.abs(descriptor.bodyRect.height - body.currentVisualHeight);
|
||||
root.check(dx <= 1 && dy <= 1 && dw <= 1 && dh <= 1, `published body follows the surface: ${dx} ${dy} ${dw} ${dh}`);
|
||||
root.check(descriptor.bodyRect.y + descriptor.bodyRect.height > frame.cutoutTopInset + 40, "sheet extends past the cutout");
|
||||
root.check((frame._islandSdfSlot.param.x > 0.5 && frame._islandSdfSlot.rect.y === frame.cutoutTopInset), "SDF slot active at the cutout edge");
|
||||
if (!controller.launcherInputFocused)
|
||||
return;
|
||||
PopoutService.toggleDankLauncherV2();
|
||||
next();
|
||||
return;
|
||||
}
|
||||
case 6:
|
||||
if (controller.expanded || body.motionRunning)
|
||||
return;
|
||||
root.check(!(ConnectedModeState.surfaceDescriptors[root.screen.name]?.island), "lease released once collapsed");
|
||||
SettingsData.barConfigs = root.config(0, "normal");
|
||||
next();
|
||||
return;
|
||||
case 7:
|
||||
if (body.motionRunning)
|
||||
return;
|
||||
PopoutService.openDankLauncherV2();
|
||||
root.check(root.fallbackCalls === 1, "normal routing keeps the modal launcher");
|
||||
SettingsData.barConfigs = root.config(0, "always", "right");
|
||||
next();
|
||||
return;
|
||||
case 8:
|
||||
if (body.motionRunning || !slotPlaced("right"))
|
||||
return;
|
||||
pillOnSlot();
|
||||
root.check(body.currentAlongPos > body.width / 2, `pill sits in the right section: ${body.currentAlongPos}`);
|
||||
PopoutService.openDankLauncherV2();
|
||||
next();
|
||||
return;
|
||||
case 9: {
|
||||
if (!controller.expanded || controller.activeActivity !== "launcher" || body.motionRunning)
|
||||
return;
|
||||
const descriptor = ConnectedModeState.surfaceDescriptor(root.screen.name, "island");
|
||||
if (!descriptor.presented)
|
||||
return;
|
||||
root.check(Math.abs(descriptor.bodyRect.x - (body.hostOriginX + body.currentVisualX)) <= 1, "published body follows the right-section sheet");
|
||||
root.check(descriptor.bodyRect.x + descriptor.bodyRect.width / 2 > root.screen.width / 2, `sheet bulges out right of centre: ${descriptor.bodyRect.x}+${descriptor.bodyRect.width}`);
|
||||
root.check(descriptor.bodyRect.x + descriptor.bodyRect.width <= root.screen.width - Theme.spacingS + 0.5, `sheet stays on screen: ${descriptor.bodyRect.x + descriptor.bodyRect.width} vs ${root.screen.width}`);
|
||||
root.check((frame._islandSdfSlot.param.x > 0.5 && frame._islandSdfSlot.rect.y === frame.cutoutTopInset), "SDF slot active for the right-section sheet");
|
||||
controller.transientSurfaces.setActive(root, true, probe);
|
||||
root.check(Array.from(frame.islandChrome.windows).includes(probe), "transient launcher window joins the frame grab");
|
||||
controller.transientSurfaces.setActive(root, false, null);
|
||||
root.check(!Array.from(frame.islandChrome.windows).includes(probe), "released transient window leaves the grab");
|
||||
PopoutService.toggleDankLauncherV2();
|
||||
next();
|
||||
return;
|
||||
}
|
||||
case 10:
|
||||
if (controller.expanded || body.motionRunning)
|
||||
return;
|
||||
root.check(!(ConnectedModeState.surfaceDescriptors[root.screen.name]?.island), "lease released after the right-section sheet");
|
||||
finish("");
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
finish(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.DankCommon.Common as DC
|
||||
|
||||
// Routing rows preserve legacy destinations, then update and reset independently through the widget entry.
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
readonly property string barId: "main"
|
||||
|
||||
QtObject {
|
||||
id: fakePage
|
||||
|
||||
readonly property string barId: root.barId
|
||||
readonly property string section: "right"
|
||||
}
|
||||
|
||||
function check(value, label) {
|
||||
if (!value)
|
||||
throw new Error(label);
|
||||
}
|
||||
|
||||
function findRow(item, settingKey) {
|
||||
for (const child of item.children) {
|
||||
if (child.settingKey === settingKey)
|
||||
return child;
|
||||
const found = findRow(child, settingKey);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.watchFiles = false;
|
||||
DC.Style.theme = Theme;
|
||||
DC.Style.settings = SettingsData;
|
||||
DC.I18n.backend = I18n;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 0
|
||||
running: SettingsData._hasLoaded && SessionData._hasLoaded
|
||||
onTriggered: {
|
||||
SettingsData.barConfigs = [{ id: root.barId, enabled: true, visible: true, position: 0, screenPreferences: ["all"], leftWidgets: ["clock"], centerWidgets: ["clock"], rightWidgets: [{ id: "island", enabled: true, islandNotificationPopups: false, islandRouteDash: "island" }, "clock"] }];
|
||||
host.setSource("Modules/Settings/BarWidgetOptions/IslandOptions.qml", { "page": fakePage });
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 720
|
||||
height: 900
|
||||
|
||||
Loader {
|
||||
id: host
|
||||
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
property int waited: 0
|
||||
|
||||
interval: 25
|
||||
repeat: true
|
||||
running: host.status === Loader.Ready
|
||||
|
||||
onTriggered: {
|
||||
try {
|
||||
const page = host.item;
|
||||
const entry = () => SettingsData.islandWidgetEntry(SettingsData.getBarConfig(root.barId));
|
||||
const shared = root.findRow(page, "islandWidgetSharedRouting");
|
||||
const routes = root.findRow(page, "islandWidgetRouteActivities");
|
||||
const routeSuffixes = ["Launcher", "ControlCenter", "NotificationCenter", "Clipboard", "Dash", "Media", "Weather", "Wallpaper"];
|
||||
const routeRows = routeSuffixes.map(suffix => root.findRow(page, "islandWidgetRoute" + suffix));
|
||||
const popups = root.findRow(page, "islandWidgetNotificationPopups");
|
||||
const mode = root.findRow(page, "islandWidgetInteractionMode");
|
||||
if (!shared || !routes || !popups || !mode || routeRows.some(row => !row)) {
|
||||
if (++waited < 80)
|
||||
return;
|
||||
throw new Error("rows never appeared: " + [shared, routes, popups, mode].map(row => !!row).join(","));
|
||||
}
|
||||
root.check(!shared.visible, "a hosted island has no shared rule to pick");
|
||||
root.check(routeRows.every(row => row.visible), "all eight destination rows are visible");
|
||||
root.check(routeRows.every((row, index) => row.checked === (index >= 4)), "legacy dashboard routing is reflected in every dashboard activity");
|
||||
root.check(popups.checked === false, "rows read the widget entry, not the defaults");
|
||||
root.check(mode.visible, "a hosted island is always docked, so expansion mode shows");
|
||||
popups.toggled(true);
|
||||
root.check(entry().islandNotificationPopups === true, "toggle writes land on the widget entry: " + JSON.stringify(entry()));
|
||||
routeRows[1].toggled(true);
|
||||
root.check(entry().islandRouteControlCenter === "island", "a pick routes that activity to the island: " + JSON.stringify(entry()));
|
||||
routeRows[1].toggled(false);
|
||||
root.check(entry().islandRouteControlCenter === "bar", "an unpick hands it back to the bar's popout: " + JSON.stringify(entry()));
|
||||
routeRows[5].toggled(false);
|
||||
root.check(!routeRows[5].checked && routeRows[4].checked && routeRows[6].checked, "media changes independently and rows follow persisted values");
|
||||
routeRows[4].toggled(false);
|
||||
root.check(!routeRows[4].checked && !routeRows[5].checked && routeRows[6].checked && routeRows[7].checked, "editing dashboard preserves the other legacy destinations");
|
||||
routeRows[6].toggled(false);
|
||||
root.check(!routeRows[6].checked, "weather can override its island default");
|
||||
routeRows[6].resetRequested();
|
||||
root.check(entry().islandRouteWeather === "island" && routeRows[6].checked && !routeRows[4].checked && !routeRows[5].checked, "reset returns weather to the island without changing dashboard or media");
|
||||
routes.resetRequested();
|
||||
root.check(routeRows.every((row, index) => row.checked === (index >= 4)), "group reset restores dashboard activities to the island and other destinations to standard surfaces");
|
||||
root.check(SettingsData.islandWidgetEntry(SettingsData.getBarConfig(root.barId)).islandSharedRouting === undefined, "the shared rule is never written for a hosted island");
|
||||
console.log("FIXTURE_PASS");
|
||||
} catch (error) {
|
||||
console.error("FIXTURE_FAIL", error.message);
|
||||
}
|
||||
stop();
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Modules.DankBar
|
||||
import qs.Modules.DankIsland
|
||||
import qs.DankCommon.Common as DC
|
||||
|
||||
// An auto-hidden or manually hidden bar hosting the island shows itself for island-bound events (an activity, a level
|
||||
// change) and hides again once the face is gone.
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
readonly property var screen: Quickshell.screens[0]
|
||||
readonly property string barId: "main"
|
||||
|
||||
Item {
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: SettingsData.barConfigs
|
||||
objectProp: "id"
|
||||
}
|
||||
delegate: DankBar {
|
||||
required property var modelData
|
||||
barConfig: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIsland {
|
||||
id: islands
|
||||
}
|
||||
|
||||
function check(value, label) {
|
||||
if (!value)
|
||||
throw new Error(label);
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.watchFiles = false;
|
||||
DC.Style.theme = Theme;
|
||||
DC.Style.settings = SettingsData;
|
||||
DC.I18n.backend = I18n;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 0
|
||||
running: SettingsData._hasLoaded && SessionData._hasLoaded
|
||||
onTriggered: {
|
||||
SettingsData.reduceMotion = true;
|
||||
SettingsData.frameEnabled = false;
|
||||
SettingsData.barConfigs = [{ id: root.barId, enabled: true, visible: true, autoHide: true, autoHideDelay: 400, position: 0, screenPreferences: ["all"], leftWidgets: ["clock"], centerWidgets: [{ id: "island", enabled: true }], rightWidgets: ["clock"] }];
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: steps
|
||||
|
||||
property int index: 0
|
||||
property int waited: 0
|
||||
readonly property var body: IslandHostRegistry.hostFor(root.screen.name, root.barId)
|
||||
readonly property var window: body?.hostWindow ?? null
|
||||
|
||||
interval: 25
|
||||
repeat: true
|
||||
running: !!body && !!body.islandController
|
||||
|
||||
function finish(message) {
|
||||
if (message)
|
||||
console.error("FIXTURE_FAIL", message);
|
||||
else
|
||||
console.log("FIXTURE_PASS");
|
||||
stop();
|
||||
Qt.quit();
|
||||
}
|
||||
|
||||
function next() {
|
||||
index++;
|
||||
waited = 0;
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
if (++waited > 240) {
|
||||
finish("timed out at step " + index);
|
||||
return;
|
||||
}
|
||||
const controller = body.islandController;
|
||||
try {
|
||||
switch (index) {
|
||||
case 0:
|
||||
if (body.motionRunning || window.barRevealed)
|
||||
return;
|
||||
controller.requestControlCenter("", false);
|
||||
next();
|
||||
return;
|
||||
case 1:
|
||||
if (!controller.expanded || body.motionRunning)
|
||||
return;
|
||||
root.check(window.barRevealed, "an activity request reveals the auto-hidden bar");
|
||||
root.check(window.exclusiveZone === -1, "an auto-hidden bar reserves nothing while revealed for the island");
|
||||
controller.requestCollapse();
|
||||
next();
|
||||
return;
|
||||
case 2:
|
||||
if (controller.expanded || body.motionRunning)
|
||||
return;
|
||||
root.check(window.barRevealed, "the bar is still held right after collapse");
|
||||
next();
|
||||
return;
|
||||
case 3:
|
||||
if (window.barRevealed)
|
||||
return;
|
||||
controller.requestSystemActivity("volume");
|
||||
next();
|
||||
return;
|
||||
case 4:
|
||||
if (!controller.transientActive || body.motionRunning)
|
||||
return;
|
||||
root.check(window.barRevealed, "a level change reveals the bar for its compact face");
|
||||
controller.finishTransient();
|
||||
next();
|
||||
return;
|
||||
case 5:
|
||||
if (controller.transientActive || window.barRevealed)
|
||||
return;
|
||||
SettingsData.updateBarConfig(root.barId, { visible: false, autoHide: false });
|
||||
next();
|
||||
return;
|
||||
case 6:
|
||||
if ((SettingsData.getBarConfig(root.barId)?.visible ?? true) || window.barRevealed || body.motionRunning)
|
||||
return;
|
||||
root.check(SettingsData.activeIslandConfigsForScreen(root.screen).length === 0, "a hidden bar's island swallows no popups or OSDs");
|
||||
controller.requestControlCenter("", false);
|
||||
next();
|
||||
return;
|
||||
case 7:
|
||||
if (!controller.expanded || body.motionRunning)
|
||||
return;
|
||||
root.check(window.barRevealed, "a manually hidden bar shows itself for the island");
|
||||
root.check(window.exclusiveZone === -1, "a hidden bar reserves nothing while revealed for the island");
|
||||
controller.requestCollapse();
|
||||
next();
|
||||
return;
|
||||
case 8:
|
||||
if (controller.expanded || body.motionRunning || window.barRevealed)
|
||||
return;
|
||||
finish("");
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
finish(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,12 +100,13 @@ ShellRoot {
|
||||
onTriggered: {
|
||||
try {
|
||||
root.waitFor(() => !!MprisController.activePlayer, "player discovered");
|
||||
SettingsData.reduceMotion = true;
|
||||
const player = MprisController.activePlayer;
|
||||
root.tracking = true;
|
||||
dash.requestTab("media");
|
||||
dash.dashVisible = true;
|
||||
player.next();
|
||||
input.wait(250);
|
||||
root.waitFor(() => player.trackTitle === "Track 2", "next track reaches the shell");
|
||||
root.check(root.playerLosses === 0, "playing track transition retains player");
|
||||
root.waitFor(() => dash.shouldBeVisible, "track transition leaves dash open");
|
||||
const orientations = [];
|
||||
@@ -116,29 +117,16 @@ ShellRoot {
|
||||
root.waitFor(() => osd.shouldBeVisible && !!osd.contentLoader.item, "OSD presents at " + position);
|
||||
orientations.push(osd.useVertical);
|
||||
const button = root.find(osd.contentLoader.item);
|
||||
root.check(!!button, "shared play button " + position);
|
||||
MediaAccentService._accent = Qt.rgba(0.2, 0.7, 0.4, 1);
|
||||
root.checkAccent(button, false);
|
||||
root.checkAccent(button, true);
|
||||
const wasPlaying = player.isPlaying;
|
||||
button.click();
|
||||
root.waitFor(() => player.isPlaying !== wasPlaying, "play/pause updates player " + position);
|
||||
root.checkAccent(button, true);
|
||||
root.checkAccent(button, false);
|
||||
MediaAccentService._accent = null;
|
||||
root.checkAccent(button, true);
|
||||
root.check(osd.shouldBeVisible, "play/pause keeps OSD open " + position);
|
||||
root.check(dash.shouldBeVisible, "OSD leaves dash open " + position);
|
||||
}
|
||||
root.check(orientations.includes(true) && orientations.includes(false), "both OSD layouts exercised");
|
||||
root.checkAccentPairs();
|
||||
player.togglePlaying();
|
||||
input.wait(100);
|
||||
player.next();
|
||||
input.wait(200);
|
||||
root.check(root.playerLosses === 0, "paused track transition retains player");
|
||||
player.stop();
|
||||
root.waitFor(() => MprisController.activePlayer === null, "stopped player clears after grace");
|
||||
console.log("FIXTURE_PASS media player continuity, popout retained, album art accent in both OSD layouts");
|
||||
} catch (error) {
|
||||
console.error("FIXTURE_FAIL", error.message);
|
||||
|
||||
@@ -168,10 +168,6 @@ ShellRoot {
|
||||
input.keyClick(Qt.Key_Escape);
|
||||
input.wait(20);
|
||||
waitFor(() => media.panel === "" && media.implicitHeight === height, "sheet dismisses without resizing card");
|
||||
media.activePlayer = null;
|
||||
check(next.visible && seekbar.visible, "player loss holds controls through the grace period");
|
||||
waitFor(() => !next.visible && !seekbar.visible, "genuine player loss eventually clears controls");
|
||||
media.activePlayer = player;
|
||||
media.showPanel("players");
|
||||
media.isSeeking = true;
|
||||
SettingsData.dashOptions = {
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import "DankCommon/Common/Contrast.js" as Contrast
|
||||
import "DankCommon/Common/Hct.js" as Hct
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
@@ -49,11 +50,19 @@ ShellRoot {
|
||||
check(readable(Theme.surfaceText, Theme.selectedContainer, 4.5), "surface text readable on the selected fill");
|
||||
check(readable(Theme.accentOnSelectedContainer, Theme.selectedContainer, 3), "selected icon readable");
|
||||
check(Contrast.ratio(Theme.selectedContainer, Theme.surfaceContainerHigh) > 1.05, "selected fill differs from the row surface");
|
||||
check(readable(Theme.surfaceText, Theme.primaryContainer, 4.5), "painted container is derived from primary, not the theme accent");
|
||||
check(readable(Theme.onPrimaryContainer, Theme.primaryContainer, 4.5), "derived onPrimaryContainer readable");
|
||||
check(readable(Theme.accentOnPrimaryContainer, Theme.primaryContainer, 3), "icon box glyph readable");
|
||||
check(readable(Theme.onSecondaryContainer, Theme.secondaryContainer, 4.5), "derived onSecondaryContainer readable");
|
||||
|
||||
Theme.customThemeData = Object.assign({}, nord, {
|
||||
"containerTint": 0
|
||||
});
|
||||
check(Math.abs(Hct.toHct(Theme.primaryContainer).chroma - Hct.toHct(Theme.surfaceContainer).chroma) <= 2, "theme containerTint sets the accent share of the derived container");
|
||||
|
||||
Theme.customThemeData = Object.assign({}, nord, {
|
||||
"softPrimaryContainer": "#5e81ac",
|
||||
"onPrimaryContainer": "#2e3440",
|
||||
"selectedContainer": "#5e81ac",
|
||||
"onSelectedContainer": "#eceff4",
|
||||
"accentOnSelectedContainer": "#a3be8c",
|
||||
@@ -61,6 +70,7 @@ ShellRoot {
|
||||
});
|
||||
check(Theme.selectedContainer.toString() === "#5e81ac" && Theme.onSelectedContainer.toString() === "#eceff4", "explicit selection colors are not derived over");
|
||||
check(Theme.accentOnSelectedContainer.toString() === "#a3be8c" && Theme.accentOnPrimaryContainer.toString() === "#bf616a", "explicit accent colors are not derived over");
|
||||
check(Theme.primaryContainer.toString() === "#5e81ac" && Theme.onPrimaryContainer.toString() === "#2e3440", "explicit container colors are not derived over");
|
||||
console.log(root.failed ? "FIXTURE_FAIL see above" : "FIXTURE_PASS");
|
||||
Qt.quit();
|
||||
}
|
||||
|
||||
@@ -64,12 +64,12 @@ with tempfile.TemporaryDirectory(prefix="dms-surface-test-") as temporary:
|
||||
shutil.copytree(repo / "quickshell/tests/fixtures/share", root / "share")
|
||||
env.update(HOME=str(root / "home"), XDG_DATA_DIRS=str(root / "share"), XDG_CONFIG_DIRS=str(root / "xdg"), XDG_RUNTIME_DIR=str(root / "runtime"), TMPDIR=str(root), LANG="C.UTF-8", LC_ALL="C.UTF-8", TZ="UTC")
|
||||
env.update({"XDG_" + name.upper() + "_HOME": str(root / name) for name in ["config", "state", "data", "cache"]})
|
||||
env.update(QT_QPA_PLATFORM="wayland", QT_LOGGING_RULES="qml.debug=true", LIBGL_ALWAYS_SOFTWARE="1", DMS_DISABLE_HOT_RELOAD="1", DMS_DISABLE_MATUGEN="1", DBUS_SESSION_BUS_ADDRESS="unix:path=" + str(root / "no-session-bus"), DBUS_SYSTEM_BUS_ADDRESS="unix:path=" + str(root / "no-system-bus"), PULSE_SERVER="unix:" + str(root / "no-pulse"), PIPEWIRE_REMOTE="no-pipewire")
|
||||
env.update(QT_QPA_PLATFORM="wayland", QT_LOGGING_RULES="qml.debug=true", LIBGL_ALWAYS_SOFTWARE="1", LP_NUM_THREADS="0", DMS_DISABLE_HOT_RELOAD="1", DMS_DISABLE_MATUGEN="1", DBUS_SESSION_BUS_ADDRESS="unix:path=" + str(root / "no-session-bus"), DBUS_SYSTEM_BUS_ADDRESS="unix:path=" + str(root / "no-system-bus"), PULSE_SERVER="unix:" + str(root / "no-pulse"), PIPEWIRE_REMOTE="no-pipewire")
|
||||
# glvnd loads the nvidia vendor first, which powers up a sleeping dGPU
|
||||
mesa_vendor = mesa_egl_vendor()
|
||||
if mesa_vendor:
|
||||
env["__EGL_VENDOR_LIBRARY_FILENAMES"] = mesa_vendor
|
||||
(root / "niri.kdl").write_text('prefer-no-csd\nhotkey-overlay { skip-at-startup; }\nlayout { gaps 0; border { off; }; focus-ring { off; }; default-column-width { proportion 1.0; }; }\n')
|
||||
(root / "niri.kdl").write_text('prefer-no-csd\nanimations { off; }\nhotkey-overlay { skip-at-startup; }\nlayout { gaps 0; border { off; }; focus-ring { off; }; default-column-width { proportion 1.0; }; }\n')
|
||||
# a bus with service dirs activates gvfs and friends, whose fuse mounts break the temporary directory cleanup
|
||||
(root / "dbus.conf").write_text('<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">\n<busconfig><type>session</type><listen>unix:tmpdir=' + str(root) + '</listen><policy context="default"><allow send_destination="*" eavesdrop="true"/><allow eavesdrop="true"/><allow own="*"/></policy></busconfig>\n')
|
||||
xvfb = None
|
||||
@@ -139,7 +139,7 @@ with tempfile.TemporaryDirectory(prefix="dms-surface-test-") as temporary:
|
||||
if mpris:
|
||||
start_dbus(fixture_env, processes)
|
||||
processes.append(subprocess.Popen([sys.executable, str(repo / "quickshell/tests/fixtures/mpris_player.py")] + (["--artwork"] if "--artwork" in sys.argv else []), env=fixture_env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL))
|
||||
result = subprocess.run(["qs", "-p", str(home / "qml")], env=fixture_env, capture_output=True, text=True, timeout=180)
|
||||
result = subprocess.run(["qs", "-p", str(home / "qml")], env=fixture_env, capture_output=True, text=True, timeout=60)
|
||||
output = result.stdout + result.stderr
|
||||
failed = bool(result.returncode) or "FIXTURE_PASS" not in output or "FIXTURE_FAIL" in output
|
||||
except subprocess.TimeoutExpired as expired:
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import "../Modules/DankIsland"
|
||||
|
||||
Item {
|
||||
width: 800
|
||||
height: 600
|
||||
|
||||
QtObject {
|
||||
id: host
|
||||
property real anchorX: 300
|
||||
property real anchorY: 250
|
||||
property real hitPadding: 0
|
||||
property bool dragging: false
|
||||
function wake() {}
|
||||
function storeAnchor() {}
|
||||
function clampAnchorX(x) { return x; }
|
||||
function clampAnchorY(y) { return y; }
|
||||
}
|
||||
|
||||
Item {
|
||||
id: surface
|
||||
anchors.fill: parent
|
||||
property real initialOffset: 80
|
||||
property bool isVertical: false
|
||||
property real currentVisualX: host.anchorX - 50 + initialOffset
|
||||
property real currentVisualY: host.anchorY - 25
|
||||
property real currentVisualWidth: 100
|
||||
property real currentVisualHeight: 50
|
||||
property var seed: null
|
||||
function applyTarget(velocity) { seed = velocity; }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: slot
|
||||
parent: surface
|
||||
x: surface.currentVisualX
|
||||
y: surface.currentVisualY
|
||||
width: surface.currentVisualWidth
|
||||
height: surface.currentVisualHeight
|
||||
property int clicks: 0
|
||||
onClicked: clicks++
|
||||
}
|
||||
|
||||
IslandFreeDragArea {
|
||||
id: drag
|
||||
hostWindow: host
|
||||
surface: surface
|
||||
}
|
||||
|
||||
TestCase {
|
||||
name: "IslandFreeDrag"
|
||||
when: windowShown
|
||||
|
||||
function init() {
|
||||
host.anchorX = 300;
|
||||
host.anchorY = 250;
|
||||
host.dragging = false;
|
||||
surface.initialOffset = 0;
|
||||
slot.clicks = 0;
|
||||
}
|
||||
|
||||
function test_compact_slot_click() {
|
||||
mouseClick(slot, 50, 25);
|
||||
compare(slot.clicks, 1);
|
||||
}
|
||||
|
||||
function test_first_drag_starts_from_visible_face() {
|
||||
surface.initialOffset = 80;
|
||||
mousePress(slot, 50, 25);
|
||||
mouseMove(slot, 75, 25);
|
||||
verify(host.dragging);
|
||||
compare(host.anchorX, 405, "drag must preserve the grab offset from the visible centre");
|
||||
surface.initialOffset = 0;
|
||||
mouseRelease(slot, 50, 25);
|
||||
compare(slot.clicks, 0);
|
||||
}
|
||||
|
||||
function test_fling_axes_data() {
|
||||
return [{ tag: "horizontal", vertical: false }, { tag: "vertical", vertical: true }];
|
||||
}
|
||||
|
||||
function test_fling_axes(data) {
|
||||
surface.isVertical = data.vertical;
|
||||
drag.velocityX = 300;
|
||||
drag.velocityY = -100;
|
||||
drag.finishDrag();
|
||||
compare(surface.seed.offsetAlong, data.vertical ? -100 : 300);
|
||||
compare(surface.seed.offsetCross, data.vertical ? 300 : -100);
|
||||
compare(host.anchorX, 327);
|
||||
compare(host.anchorY, 241);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -552,7 +552,10 @@
|
||||
"category": "Interface style",
|
||||
"keywords": [
|
||||
"accent",
|
||||
"across",
|
||||
"badges",
|
||||
"button",
|
||||
"cards",
|
||||
"color",
|
||||
"colors",
|
||||
"colour",
|
||||
@@ -560,10 +563,13 @@
|
||||
"hue",
|
||||
"interface",
|
||||
"palette",
|
||||
"shell",
|
||||
"style",
|
||||
"tint"
|
||||
"tint",
|
||||
"tinted"
|
||||
],
|
||||
"page": "theme_surfaces"
|
||||
"page": "theme_surfaces",
|
||||
"description": "Tinted cards and badges across the shell"
|
||||
},
|
||||
{
|
||||
"section": "notificationCompactMode",
|
||||
@@ -586,6 +592,31 @@
|
||||
],
|
||||
"page": "notifications"
|
||||
},
|
||||
{
|
||||
"section": "containerSaturation",
|
||||
"label": "Container saturation",
|
||||
"tabIndex": -1,
|
||||
"category": "Interface style",
|
||||
"keywords": [
|
||||
"accent",
|
||||
"across",
|
||||
"badges",
|
||||
"card",
|
||||
"cards",
|
||||
"color",
|
||||
"container",
|
||||
"interface",
|
||||
"pastel",
|
||||
"primary",
|
||||
"saturation",
|
||||
"shell",
|
||||
"style",
|
||||
"tint",
|
||||
"tinted"
|
||||
],
|
||||
"page": "theme_surfaces",
|
||||
"description": "Tinted cards and badges across the shell"
|
||||
},
|
||||
{
|
||||
"section": "controlCenterIcons",
|
||||
"label": "Control Center",
|
||||
|
||||
Reference in New Issue
Block a user