mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 15:07:49 -04:00
dash: replace the tab bar with an m3 navigation bar and inline edit controls
wallpaper: allow SVG wallpapers, with a workaround for a matugen panic I encountered on passing it an SVG - fixes #1350
This commit is contained in:
1 parent
67e5f24761
commit
19b4593bfe
38 files changed
+1363
-370
No files matched your search
@@ -18,6 +18,8 @@ require (
|
||||
github.com/pilebones/go-udev v0.9.1
|
||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c
|
||||
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||
|
||||
@@ -184,6 +184,10 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780 h1:oDMiXaTMyBEuZMU53atpxqYsSB3U1CHkeAu2zr6wTeY=
|
||||
github.com/srwiley/rasterx v0.0.0-20210519020934-456a8d69b780/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
|
||||
@@ -221,6 +221,12 @@ func smartSchemePreview(fallback SchemePreview, sourceColor string, contrast flo
|
||||
opts.Kind = "image"
|
||||
opts.Value = imagePath
|
||||
}
|
||||
_, cleanup, err := stageImageSource(opts)
|
||||
if err != nil {
|
||||
log.Warnf("Smart scheme preview failed falling back to tonal-spot: %v", err)
|
||||
return fallback
|
||||
}
|
||||
defer cleanup()
|
||||
output, err := runMatugenDryRun(opts)
|
||||
if err != nil {
|
||||
log.Warnf("Smart scheme preview failed falling back to tonal-spot: %v", err)
|
||||
@@ -353,6 +359,11 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
sourceImage, cleanup, err := stageImageSource(opts)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer cleanup()
|
||||
if err := resolveSmartMode(opts, flags); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -380,7 +391,6 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
var primaryDark, primaryLight, surfaceDark, surfaceLight, containerDark, containerLight string
|
||||
var dank16JSON string
|
||||
var importArgs []string
|
||||
var sourceImage string
|
||||
|
||||
// Colorful mode resolves the seed here, before matugen is invoked at all,
|
||||
// by rewriting the source to the extracted hex. Both the dry-run and the
|
||||
@@ -394,12 +404,6 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
log.Infof("Seed color override: %s -> %s", opts.Value, seed)
|
||||
if opts.Kind == "image" {
|
||||
sourceImage = opts.Value
|
||||
if abs, err := filepath.Abs(sourceImage); err == nil {
|
||||
sourceImage = abs
|
||||
}
|
||||
}
|
||||
opts.Kind = "hex"
|
||||
opts.Value = seed
|
||||
opts.SourceMode = SourceModeDominant
|
||||
@@ -410,11 +414,6 @@ func buildOnce(opts *Options) (bool, error) {
|
||||
log.Warnf("Colorful source extraction failed for %s, using matugen's own: %v", opts.Value, err)
|
||||
} else {
|
||||
log.Infof("Colorful source color: %s -> %s", opts.Value, seed)
|
||||
// matugen resolves {{image}} to an absolute path, so match it.
|
||||
sourceImage = opts.Value
|
||||
if abs, err := filepath.Abs(sourceImage); err == nil {
|
||||
sourceImage = abs
|
||||
}
|
||||
opts.Kind = "hex"
|
||||
opts.Value = seed
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package matugen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/srwiley/oksvg"
|
||||
"github.com/srwiley/rasterx"
|
||||
)
|
||||
|
||||
// stageImageSource returns the absolute path templates see as {{image}} and,
|
||||
// for an SVG, swaps opts.Value for a one-shot PNG raster matugen can decode.
|
||||
// !TODO: drop the SVG raster once matugen decodes SVG; matugen 4.2.0 (material-colors 0.4.2) panics on an .svg source
|
||||
func stageImageSource(opts *Options) (sourceImage string, cleanup func(), err error) {
|
||||
noop := func() {}
|
||||
if opts.Kind != "image" {
|
||||
return "", noop, nil
|
||||
}
|
||||
sourceImage = opts.Value
|
||||
if abs, err := filepath.Abs(sourceImage); err == nil {
|
||||
sourceImage = abs
|
||||
}
|
||||
if !strings.EqualFold(filepath.Ext(opts.Value), ".svg") {
|
||||
return sourceImage, noop, nil
|
||||
}
|
||||
raster, err := rasterizeSVGToPNG(opts.Value)
|
||||
if err != nil {
|
||||
return "", noop, fmt.Errorf("svg source %s: %w", opts.Value, err)
|
||||
}
|
||||
opts.Value = raster
|
||||
return sourceImage, func() { os.Remove(raster) }, nil
|
||||
}
|
||||
|
||||
func rasterizeSVGToPNG(path string) (string, error) {
|
||||
img, err := rasterizeSVG(path, sourceSampleMaxDim)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
f, err := os.CreateTemp("", "matugen-svg-*.png")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
if err := png.Encode(f, img); err != nil {
|
||||
os.Remove(f.Name())
|
||||
return "", err
|
||||
}
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
func rasterizeSVG(path string, maxDim int) (image.Image, error) {
|
||||
icon, err := oksvg.ReadIcon(path, oksvg.IgnoreErrorMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vw, vh := icon.ViewBox.W, icon.ViewBox.H
|
||||
if vw <= 0 || vh <= 0 {
|
||||
return nil, fmt.Errorf("no usable viewBox")
|
||||
}
|
||||
scale := float64(maxDim) / math.Max(vw, vh)
|
||||
w, h := max(int(vw*scale), 1), max(int(vh*scale), 1)
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
icon.SetTarget(0, 0, float64(w), float64(h))
|
||||
icon.Draw(rasterx.NewDasher(w, h, rasterx.NewScannerGV(w, h, img, img.Bounds())), 1)
|
||||
return img, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package matugen
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStageImageSourceRasterizesSVG(t *testing.T) {
|
||||
svgPath := filepath.Join(t.TempDir(), "wall.svg")
|
||||
svg := `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 100"><rect width="200" height="100" fill="#3355ff"/><circle cx="60" cy="50" r="40" fill="#ff8800"/></svg>`
|
||||
if err := os.WriteFile(svgPath, []byte(svg), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
opts := &Options{Kind: "image", Value: svgPath}
|
||||
sourceImage, cleanup, err := stageImageSource(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("stageImageSource: %v", err)
|
||||
}
|
||||
assert.Equal(t, svgPath, sourceImage)
|
||||
assert.NotEqual(t, svgPath, opts.Value)
|
||||
|
||||
f, err := os.Open(opts.Value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
img, format, err := image.Decode(f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("decode raster: %v", err)
|
||||
}
|
||||
assert.Equal(t, "png", format)
|
||||
assert.Equal(t, sourceSampleMaxDim, img.Bounds().Dx())
|
||||
assert.Equal(t, sourceSampleMaxDim/2, img.Bounds().Dy())
|
||||
r, g, b, _ := img.At(500, 10).RGBA()
|
||||
assert.Equal(t, [3]uint8{0x33, 0x55, 0xff}, [3]uint8{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)})
|
||||
|
||||
seed, err := ExtractSourceColor(opts.Value)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, seed)
|
||||
|
||||
cleanup()
|
||||
_, statErr := os.Stat(opts.Value)
|
||||
assert.True(t, os.IsNotExist(statErr))
|
||||
}
|
||||
|
||||
func TestStageImageSourcePassesRasterThrough(t *testing.T) {
|
||||
opts := &Options{Kind: "image", Value: "/wallpapers/x.jpg"}
|
||||
sourceImage, cleanup, err := stageImageSource(opts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/wallpapers/x.jpg", opts.Value)
|
||||
assert.Equal(t, "/wallpapers/x.jpg", sourceImage)
|
||||
cleanup()
|
||||
}
|
||||
+1
-1
Submodule dank-qml-common updated: 6f75d9065b...69859d278c.
Generated
+3
-3
@@ -3,11 +3,11 @@
|
||||
"dank-qml-common": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1789851922,
|
||||
"narHash": "sha256-0+ix8DGpRVi4tcsu7u3XA+aw5gUXRZZbGxVu5ZgOWgo=",
|
||||
"lastModified": 1789865489,
|
||||
"narHash": "sha256-TUK26uTRz20ifTLf/OP3oRypJ00+1ePXsxSsLQrb5e4=",
|
||||
"owner": "AvengeMedia",
|
||||
"repo": "dank-qml-common",
|
||||
"rev": "6f75d9065b2ff708eb1e91eec30ec5008d1ce3ee",
|
||||
"rev": "69859d278c645793127c4920f441b9ee7724b599",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -466,6 +466,8 @@ Singleton {
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
property string dashTabPosition: Spec.SPEC.dashTabPosition.def
|
||||
property bool dashTabsEvenlySpaced: Spec.SPEC.dashTabsEvenlySpaced.def
|
||||
property var dashTabs: Spec.SPEC.dashTabs.def
|
||||
onDashTabsChanged: saveSettings()
|
||||
|
||||
|
||||
@@ -1323,6 +1323,12 @@ Singleton {
|
||||
readonly property real spinnerStrokeWidth: 2
|
||||
readonly property real tabMinWidth: 64
|
||||
readonly property real tabIndicatorHeight: 3
|
||||
readonly property real navigationHeight: 64
|
||||
readonly property real navigationRailWidth: 96
|
||||
readonly property real navigationItemMinWidth: 80
|
||||
readonly property real navigationIndicatorWidth: 56
|
||||
readonly property real navigationIndicatorHeight: 32
|
||||
readonly property real navigationVerticalPadding: 6
|
||||
readonly property real tabIndicatorMinWidth: 24
|
||||
readonly property real tabIndicatorInset: 2
|
||||
readonly property real launcherTileSize: 120
|
||||
|
||||
@@ -603,6 +603,12 @@ var LOCAL_SPEC = {
|
||||
keybindsFloatingWindow: {
|
||||
def: false
|
||||
},
|
||||
dashTabPosition: {
|
||||
def: "auto"
|
||||
},
|
||||
dashTabsEvenlySpaced: {
|
||||
def: true
|
||||
},
|
||||
dashTabs: {
|
||||
def: [
|
||||
{
|
||||
|
||||
@@ -243,7 +243,7 @@ DankFloatingWindow {
|
||||
browserTitle: I18n.tr("Select Profile Image", "profile image file browser title")
|
||||
browserType: "profile"
|
||||
showHiddenFiles: true
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr", "*.svg"]
|
||||
onFileSelected: path => {
|
||||
PortalService.setProfileImage(path);
|
||||
close();
|
||||
@@ -267,7 +267,7 @@ DankFloatingWindow {
|
||||
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
|
||||
browserType: "wallpaper"
|
||||
showHiddenFiles: true
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr", "*.svg"]
|
||||
onFileSelected: path => {
|
||||
SessionData.setWallpaper(path);
|
||||
SessionData.wallpaperCyclingFolderPath = "";
|
||||
|
||||
@@ -36,7 +36,7 @@ DankPopout {
|
||||
|
||||
property bool dashVisible: false
|
||||
property var triggerScreen: null
|
||||
property string currentTabId: DashRegistry.fallbackId
|
||||
property string currentTabId: DashRegistry.defaultTabId
|
||||
property string detailTabId: ""
|
||||
property bool editMode: false
|
||||
property string overviewFocusId: CacheData.dashFocusCardId || "calendar"
|
||||
@@ -46,32 +46,53 @@ DankPopout {
|
||||
contentLoader.item?.panelResizer.cancel();
|
||||
if (!shouldBeVisible)
|
||||
return;
|
||||
Qt.callLater(() => {
|
||||
if (editMode) {
|
||||
contentLoader.item?.focusNavigation();
|
||||
return;
|
||||
}
|
||||
contentLoader.item?.focusInitial();
|
||||
});
|
||||
Qt.callLater(() => contentLoader.item?.focusInitial());
|
||||
}
|
||||
|
||||
readonly property string activeTabId: detailTabId !== "" ? detailTabId : currentTabId
|
||||
readonly property var detailEntry: detailTabId !== "" ? DashRegistry.entry(detailTabId) : null
|
||||
readonly property var orderedTabIds: DashRegistry.visibleTabIds
|
||||
readonly property int currentTabIndex: orderedTabIds.indexOf(currentTabId)
|
||||
readonly property bool showTabs: orderedTabIds.length > 0 && currentTabIndex >= 0
|
||||
readonly property bool showTabs: orderedTabIds.length > 1 && currentTabIndex >= 0 && detailTabId === ""
|
||||
readonly property bool showBack: detailTabId !== "" && currentTabIndex >= 0
|
||||
readonly property int navigationEdge: {
|
||||
switch (SettingsData.dashTabPosition) {
|
||||
case "left":
|
||||
return SettingsData.Left;
|
||||
case "right":
|
||||
return SettingsData.Right;
|
||||
case "bottom":
|
||||
return SettingsData.Bottom;
|
||||
case "center":
|
||||
return SettingsData.Top;
|
||||
}
|
||||
switch (effectiveBarPosition % 4) {
|
||||
case SettingsData.Left:
|
||||
return SettingsData.Right;
|
||||
case SettingsData.Right:
|
||||
return SettingsData.Left;
|
||||
default:
|
||||
return effectiveBarPosition % 4;
|
||||
}
|
||||
}
|
||||
readonly property bool verticalNavigation: showTabs && (navigationEdge === SettingsData.Left || navigationEdge === SettingsData.Right)
|
||||
readonly property real navigationWidth: verticalNavigation ? Theme.navigationRailWidth + DashMetrics.contentGap : 0
|
||||
|
||||
readonly property int columnCap: DashMetrics.columnCapFor(screen?.width, SettingsData.showWeekNumber)
|
||||
readonly property int columnCap: DashMetrics.columnCapFor(screen ? screen.width - navigationWidth : undefined, SettingsData.showWeekNumber)
|
||||
readonly property real editGutter: editMode ? PopoutMetrics.editOverflow : 0
|
||||
|
||||
popupWidth: DashMetrics.widthFor(SettingsData.showWeekNumber, screen?.width, DashMetrics.panelColumnsFor(activeTabId))
|
||||
minimumSurfaceWidth: DashMetrics.widthFor(SettingsData.showWeekNumber, screen?.width, DashRegistry.widestPanelColumns)
|
||||
popupHeight: contentLoader.item?.implicitHeight ?? (DashMetrics.tabDefaultHeight + DashMetrics.tabBarBlockHeight + DashMetrics.contentGap + DashMetrics.contentPadding * 2)
|
||||
popupWidth: panelWidthFor(DashMetrics.panelColumnsFor(activeTabId))
|
||||
minimumSurfaceWidth: panelWidthFor(DashRegistry.widestPanelColumns)
|
||||
popupHeight: contentLoader.item?.implicitHeight ?? (DashMetrics.tabDefaultHeight + Theme.navigationHeight + DashMetrics.contentGap + DashMetrics.contentPadding * 2)
|
||||
triggerWidth: DashMetrics.triggerWidth
|
||||
screen: triggerScreen
|
||||
|
||||
property bool __focusArmed: false
|
||||
|
||||
function panelWidthFor(columns) {
|
||||
return DashMetrics.widthFor(SettingsData.showWeekNumber, screen ? screen.width - navigationWidth : undefined, columns) + navigationWidth;
|
||||
}
|
||||
|
||||
function requestTab(tab) {
|
||||
const id = DashRegistry.resolveId(tab);
|
||||
if (DashRegistry.isSelectable(id)) {
|
||||
@@ -84,10 +105,14 @@ DankPopout {
|
||||
return;
|
||||
}
|
||||
detailTabId = "";
|
||||
currentTabId = DashRegistry.fallbackId;
|
||||
currentTabId = DashRegistry.defaultTabId;
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
if (!showBack) {
|
||||
dashVisible = false;
|
||||
return;
|
||||
}
|
||||
detailTabId = "";
|
||||
}
|
||||
|
||||
@@ -102,23 +127,23 @@ DankPopout {
|
||||
}
|
||||
|
||||
function focusContent(backwards) {
|
||||
contentLoader.item?.focusInitial();
|
||||
contentLoader.item?.focusNavigation(backwards);
|
||||
}
|
||||
|
||||
onActiveTabIdChanged: {
|
||||
editMode = false;
|
||||
contentLoader.item?.dismissOptions();
|
||||
contentLoader.item?.resetScroll();
|
||||
if (shouldBeVisible)
|
||||
if (shouldBeVisible && !contentLoader.item?.navigationFocused)
|
||||
contentLoader.item?.focusInitial();
|
||||
}
|
||||
|
||||
readonly property var registryVisibleTabIds: DashRegistry.visibleTabIds
|
||||
readonly property var registryTabIds: DashRegistry.tabIds
|
||||
readonly property string registryVisibleTabIds: DashRegistry.visibleTabIds.join("\n")
|
||||
readonly property string registryTabIds: DashRegistry.tabIds.join("\n")
|
||||
|
||||
onRegistryVisibleTabIdsChanged: {
|
||||
if (!DashRegistry.isSelectable(currentTabId))
|
||||
currentTabId = DashRegistry.fallbackId;
|
||||
currentTabId = DashRegistry.defaultTabId;
|
||||
}
|
||||
|
||||
onRegistryTabIdsChanged: {
|
||||
@@ -228,8 +253,10 @@ DankPopout {
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
implicitWidth: root.popupWidth
|
||||
implicitHeight: headerRow.height + DashMetrics.contentGap + pages.implicitHeight + DashMetrics.contentPadding * 2
|
||||
implicitHeight: horizontalChromeHeight + pages.implicitHeight + DashMetrics.contentPadding * 2
|
||||
readonly property real horizontalChromeHeight: root.verticalNavigation ? 0 : (root.showTabs ? tabBar.implicitHeight : Theme.minimumTouchTargetSize) + DashMetrics.contentGap
|
||||
readonly property bool ready: pages.ready
|
||||
readonly property bool navigationFocused: tabBar.activeFocus
|
||||
focus: true
|
||||
|
||||
readonly property bool panelResizing: panelResizer.resizing
|
||||
@@ -243,7 +270,6 @@ DankPopout {
|
||||
cardResizeColumns = panelColumns;
|
||||
cardResizeRows = panelRows;
|
||||
}
|
||||
readonly property bool hasWidgets: headerMenu.hasWidgets
|
||||
readonly property int panelColumns: DashMetrics.panelColumnsFor(root.activeTabId)
|
||||
readonly property int contentRows: DashMetrics.rowsForHeight(pages.currentHostImplicitHeight)
|
||||
readonly property int panelRows: Math.max(DashMetrics.panelFloorRowsFor(root.activeTabId), contentRows)
|
||||
@@ -251,7 +277,7 @@ DankPopout {
|
||||
readonly property DankPanelResizer panelResizer: DankPanelResizer {
|
||||
popout: root
|
||||
stepWidth: DashMetrics.preferredColumnWidth + DashMetrics.gridGap
|
||||
widthFor: columns => DashMetrics.widthFor(SettingsData.showWeekNumber, root.screen?.width, columns)
|
||||
widthFor: columns => root.panelWidthFor(columns)
|
||||
currentStep: () => mainContainer.panelColumns
|
||||
currentRows: () => mainContainer.panelRows
|
||||
minStep: Math.max(DashMetrics.minimumGridColumns, pages.currentItem?.usedColumns ?? 0)
|
||||
@@ -277,7 +303,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
function headerFocusTargets() {
|
||||
const targets = root.editMode ? editControls.focusTargets : [root.detailTabId !== "" ? backButton : tabBar, menuButton];
|
||||
const targets = root.editMode ? pageActions.focusTargets : [backButton, tabBar, pageTitle.focusTarget];
|
||||
return targets.filter(item => item.visible && item.enabled);
|
||||
}
|
||||
|
||||
@@ -288,12 +314,18 @@ DankPopout {
|
||||
}
|
||||
|
||||
function focusInitial() {
|
||||
for (const target of headerFocusTargets())
|
||||
if (root.activeTabId === "") {
|
||||
emptySettings.forceActiveFocus(Qt.OtherFocusReason);
|
||||
return;
|
||||
}
|
||||
for (const target of [backButton, tabBar, pageTitle.focusTarget])
|
||||
target.focus = false;
|
||||
pageActions.clearFocus();
|
||||
if (pages.currentHost)
|
||||
pages.currentHost.focus = false;
|
||||
mainContainer.forceActiveFocus(Qt.OtherFocusReason);
|
||||
pages.currentItem?.restoreFocus?.();
|
||||
if (!root.editMode)
|
||||
pages.currentItem?.restoreFocus?.();
|
||||
}
|
||||
|
||||
function contentEntryFor(key) {
|
||||
@@ -356,7 +388,7 @@ DankPopout {
|
||||
|
||||
function dismissOptions() {
|
||||
tabOptions.dismiss();
|
||||
headerMenu.close();
|
||||
pageActions.closeMenu();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -373,7 +405,7 @@ DankPopout {
|
||||
}
|
||||
|
||||
Keys.onPressed: function (event) {
|
||||
if (headerMenu.open)
|
||||
if (pageActions.menuOpen)
|
||||
return;
|
||||
if (tabOptions.shown) {
|
||||
if (event.key !== Qt.Key_Escape)
|
||||
@@ -397,6 +429,11 @@ DankPopout {
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) {
|
||||
if (root.editMode) {
|
||||
mainContainer.focusNavigation(event.key === Qt.Key_Backtab || !!(event.modifiers & Qt.ShiftModifier));
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (current?.blocksTabNavigation)
|
||||
return;
|
||||
root.cycleTab(event.key === Qt.Key_Backtab || (event.modifiers & Qt.ShiftModifier) ? -1 : 1);
|
||||
@@ -421,7 +458,7 @@ DankPopout {
|
||||
|
||||
Shortcut {
|
||||
sequence: "Tab"
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: {
|
||||
root.cycleTab(1);
|
||||
@@ -431,7 +468,7 @@ DankPopout {
|
||||
|
||||
Shortcut {
|
||||
sequence: "Shift+Tab"
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: {
|
||||
root.cycleTab(-1);
|
||||
@@ -441,7 +478,7 @@ DankPopout {
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+Tab"
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: {
|
||||
root.cycleTab(1);
|
||||
@@ -451,7 +488,7 @@ DankPopout {
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+Shift+Tab"
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: {
|
||||
root.cycleTab(-1);
|
||||
@@ -459,58 +496,65 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: ["F2", "Ctrl+E"]
|
||||
enabled: root.shouldBeVisible && root.activeTabId !== "" && !root.editMode && !tabOptions.shown && !pageActions.menuOpen && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: root.editMode = true
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "F6"
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: mainContainer.cycleRegion(false)
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Shift+F6"
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: mainContainer.cycleRegion(true)
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Alt+Tab"
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: pages.currentItem?.cycleCardFocus?.(false)
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Alt+Shift+Tab"
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: pages.currentItem?.cycleCardFocus?.(true)
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: ["Alt+Left", "Alt+H"]
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: pages.currentItem?.moveCardFocus?.("left")
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: ["Alt+Right", "Alt+L"]
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: pages.currentItem?.moveCardFocus?.("right")
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: ["Alt+Up", "Alt+K"]
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: pages.currentItem?.moveCardFocus?.("up")
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: ["Alt+Down", "Alt+J"]
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !headerMenu.open && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
enabled: root.shouldBeVisible && !tabOptions.shown && !pageActions.menuOpen && !root.editMode && !pages.currentItem?.blocksTabNavigation
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: pages.currentItem?.moveCardFocus?.("down")
|
||||
}
|
||||
@@ -543,21 +587,6 @@ DankPopout {
|
||||
onResizeCanceled: mainContainer.panelResizer.cancel()
|
||||
}
|
||||
|
||||
DashPageMenu {
|
||||
id: headerMenu
|
||||
entryId: root.activeTabId
|
||||
tabItem: pages.currentItem
|
||||
editMode: root.editMode
|
||||
panelResizable: true
|
||||
onEditRequested: root.editMode = true
|
||||
onOptionsRequested: tabOptions.presentFor(root.activeTabId)
|
||||
onSettingsRequested: {
|
||||
root.dashVisible = false;
|
||||
root.instantClose();
|
||||
PopoutService.openSettingsWithTab("dank_dash");
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentClip
|
||||
|
||||
@@ -574,138 +603,100 @@ DankPopout {
|
||||
anchors.leftMargin: DashMetrics.contentPadding
|
||||
anchors.bottomMargin: DashMetrics.contentPadding
|
||||
width: root.popupWidth - DashMetrics.contentPadding * 2
|
||||
LayoutMirroring.enabled: false
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
Item {
|
||||
id: headerRow
|
||||
enabled: !tabOptions.shown && !pageActions.menuOpen
|
||||
|
||||
readonly property real stripCenter: (tabBar.y + tabBar.height - Theme.dividerWidth - DashMetrics.contentPadding) / 2
|
||||
x: root.verticalNavigation && root.navigationEdge === SettingsData.Right ? parent.width - width : 0
|
||||
y: !root.verticalNavigation && root.navigationEdge === SettingsData.Bottom ? parent.height - height : 0
|
||||
width: root.verticalNavigation ? Theme.navigationRailWidth : parent.width
|
||||
height: root.verticalNavigation ? parent.height : mainContainer.horizontalChromeHeight - DashMetrics.contentGap
|
||||
|
||||
width: parent.width
|
||||
height: root.showTabs ? Math.max(DashMetrics.tabBarBlockHeight, tabBar.y + tabBar.height) : 0
|
||||
visible: root.showTabs
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: -DashMetrics.contentPadding
|
||||
anchors.rightMargin: -DashMetrics.contentPadding
|
||||
y: tabBar.y + tabBar.height - height
|
||||
height: Theme.dividerWidth
|
||||
color: Theme.outlineVariant
|
||||
visible: tabBar.visible
|
||||
}
|
||||
|
||||
DankTabBar {
|
||||
DankNavigationBar {
|
||||
id: tabBar
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: menuButton.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
y: -DashMetrics.tabBarLift
|
||||
visible: !root.editMode && root.detailTabId === ""
|
||||
tabHeight: DashMetrics.tabHeight
|
||||
showDivider: false
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
editable: true
|
||||
evenlySpaced: SettingsData.dashTabsEvenlySpaced
|
||||
onEditRequested: root.editMode = true
|
||||
visible: root.showTabs && !root.editMode
|
||||
orientation: root.verticalNavigation ? Qt.Vertical : Qt.Horizontal
|
||||
currentIndex: root.currentTabIndex
|
||||
spacing: Theme.spacingS
|
||||
equalWidthTabs: true
|
||||
enableArrowNavigation: false
|
||||
cycleOnTab: true
|
||||
nextFocusTarget: pages.focusTarget
|
||||
nextFocusTarget: pageActions.focusTargets[0] ?? pages.focusTarget
|
||||
previousFocusTarget: pages.currentItem?.previousFocusTarget ?? pages.focusTarget
|
||||
model: DashRegistry.tabBarModel
|
||||
|
||||
onTabClicked: function (index) {
|
||||
onActivated: index => {
|
||||
const id = root.orderedTabIds[index];
|
||||
if (id !== undefined)
|
||||
root.currentTabId = id;
|
||||
}
|
||||
}
|
||||
|
||||
DashEditControls {
|
||||
id: editControls
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
y: Math.round(headerRow.stripCenter - height / 2)
|
||||
visible: root.editMode
|
||||
canAdd: (pages.currentItem?.addable?.length ?? 0) > 0
|
||||
hasWidgets: mainContainer.hasWidgets
|
||||
title: mainContainer.hasWidgets ? I18n.tr("Widgets") : (DashRegistry.entry(root.activeTabId)?.text ?? "")
|
||||
onAddRequested: anchor => pages.currentItem?.openAddMenu(anchor)
|
||||
onMenuRequested: anchor => headerMenu.openAt(anchor)
|
||||
onFinished: root.editMode = false
|
||||
}
|
||||
|
||||
Item {
|
||||
id: detailHeader
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
y: Math.round(headerRow.stripCenter - height / 2)
|
||||
height: DashMetrics.headerActionSize
|
||||
visible: root.detailTabId !== "" && !root.editMode
|
||||
|
||||
DankActionButton {
|
||||
id: backButton
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonSize: DashMetrics.headerActionSize
|
||||
iconName: I18n.isRtl ? "arrow_forward" : "arrow_back"
|
||||
Accessible.name: I18n.tr("Back")
|
||||
KeyNavigation.tab: pages.focusTarget
|
||||
KeyNavigation.backtab: pages.focusTarget
|
||||
onClicked: root.closeDetail()
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: backButton.right
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: DashMetrics.headerActionSize + Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.detailEntry?.text ?? ""
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: menuButton
|
||||
id: backButton
|
||||
x: I18n.isRtl ? parent.width - width : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonSize: Theme.buttonHeightS
|
||||
iconName: I18n.isRtl ? "arrow_forward" : "arrow_back"
|
||||
tooltipText: I18n.tr("Back")
|
||||
visible: root.showBack && !root.editMode
|
||||
onClicked: root.closeDetail()
|
||||
}
|
||||
|
||||
anchors.right: parent.right
|
||||
y: Math.round(headerRow.stripCenter - height / 2)
|
||||
buttonSize: DashMetrics.headerActionSize
|
||||
iconName: "more_vert"
|
||||
iconColor: headerMenu.open ? Theme.primary : Theme.onSurfaceVariant
|
||||
backgroundColor: headerMenu.open ? Theme.withAlpha(Theme.primary, Theme.stateLayerFocus) : "transparent"
|
||||
Accessible.name: I18n.tr("Options")
|
||||
visible: !root.editMode
|
||||
KeyNavigation.tab: pages.focusTarget
|
||||
KeyNavigation.backtab: root.detailTabId !== "" ? backButton : tabBar
|
||||
onClicked: headerMenu.openAt(menuButton)
|
||||
DashPageTitle {
|
||||
id: pageTitle
|
||||
readonly property real backWidth: root.showBack ? backButton.width + Theme.spacingS : 0
|
||||
x: I18n.isRtl ? 0 : backWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.max(0, parent.width - backWidth)
|
||||
height: implicitHeight
|
||||
entryId: root.activeTabId
|
||||
visible: !root.showTabs && !root.editMode
|
||||
onEditRequested: root.editMode = true
|
||||
}
|
||||
|
||||
DashPageActions {
|
||||
id: pageActions
|
||||
x: root.verticalNavigation ? 0 : (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
width: Math.min(parent.width, implicitWidth)
|
||||
height: Math.min(parent.height, implicitHeight)
|
||||
visible: root.editMode
|
||||
vertical: root.verticalNavigation
|
||||
overlayParent: mainContainer
|
||||
entryId: root.activeTabId
|
||||
tabItem: pages.currentItem
|
||||
editMode: root.editMode
|
||||
panelResizable: true
|
||||
onOptionsRequested: tabOptions.presentFor(root.activeTabId)
|
||||
onFinished: root.editMode = false
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: pages
|
||||
enabled: !tabOptions.shown && !pageActions.menuOpen
|
||||
|
||||
property var currentHost: null
|
||||
property real settledHeight: DashMetrics.tabDefaultHeight
|
||||
readonly property var currentItem: currentHost?.item ?? null
|
||||
readonly property Item focusTarget: currentHost?.focusTarget ?? null
|
||||
readonly property var currentItem: root.activeTabId !== "" ? currentHost?.item ?? null : null
|
||||
readonly property Item focusTarget: root.activeTabId === "" ? emptySettings : currentHost?.focusTarget ?? null
|
||||
readonly property bool currentSettled: !!currentHost && (!!currentHost.item || currentHost.failed)
|
||||
readonly property real currentHostImplicitHeight: currentHost?.implicitHeight ?? 0
|
||||
readonly property real targetHeight: currentSettled && currentHost.isCurrent ? DashMetrics.panelHeightFor(root.activeTabId, currentHostImplicitHeight) : -1
|
||||
readonly property real bodyHeight: Math.max(settledHeight, currentHostImplicitHeight)
|
||||
readonly property real targetHeight: root.activeTabId === "" ? emptyContent.implicitHeight : currentSettled && currentHost.isCurrent ? DashMetrics.panelHeightFor(root.activeTabId, currentHostImplicitHeight) : -1
|
||||
readonly property real bodyHeight: root.activeTabId === "" ? emptyContent.implicitHeight : Math.max(settledHeight, currentHostImplicitHeight)
|
||||
readonly property bool ready: targetHeight >= 0 && settledHeight === targetHeight
|
||||
readonly property real availableHeight: root.screen ? root.screen.height - headerRow.height - DashMetrics.contentGap - DashMetrics.contentPadding * 2 - Theme.barHeight - Theme.spacingL * 2 : contentHeight
|
||||
readonly property real availableHeight: root.screen ? root.screen.height - mainContainer.horizontalChromeHeight - DashMetrics.contentPadding * 2 - Theme.barHeight - Theme.spacingL * 2 : contentHeight
|
||||
readonly property int rowBudget: DashMetrics.rowCapFor(availableHeight)
|
||||
|
||||
x: -root.editGutter
|
||||
y: (headerRow.visible ? headerRow.height + DashMetrics.contentGap : 0) - root.editGutter
|
||||
width: parent.width + root.editGutter * 2
|
||||
height: Math.max(0, mainContainer.height - headerRow.height - DashMetrics.contentGap - DashMetrics.contentPadding * 2) + root.editGutter * 2
|
||||
x: (root.verticalNavigation && root.navigationEdge === SettingsData.Left ? root.navigationWidth : 0) - root.editGutter
|
||||
y: (!root.verticalNavigation && root.navigationEdge !== SettingsData.Bottom ? mainContainer.horizontalChromeHeight : 0) - root.editGutter
|
||||
width: parent.width - root.navigationWidth + root.editGutter * 2
|
||||
height: Math.max(0, mainContainer.height - mainContainer.horizontalChromeHeight - DashMetrics.contentPadding * 2) + root.editGutter * 2
|
||||
implicitHeight: Math.min(settledHeight, Math.max(DashMetrics.gridRowUnit, availableHeight))
|
||||
contentWidth: width
|
||||
contentHeight: bodyHeight + root.editGutter * 2
|
||||
@@ -719,6 +710,30 @@ DankPopout {
|
||||
|
||||
onTargetHeightChanged: Qt.callLater(updateContentHeight)
|
||||
|
||||
Column {
|
||||
id: emptyContent
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: root.activeTabId === ""
|
||||
|
||||
CcEmptyState {
|
||||
iconName: "dashboard"
|
||||
title: I18n.tr("No tabs enabled", "Dashboard empty state when all navigation tabs are hidden")
|
||||
}
|
||||
|
||||
DankButton {
|
||||
id: emptySettings
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: I18n.tr("Settings")
|
||||
iconName: "settings"
|
||||
onClicked: {
|
||||
root.dashVisible = false;
|
||||
root.instantClose();
|
||||
PopoutService.openSettingsWithTab("dank_dash");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: DashRegistry.tabIds
|
||||
@@ -740,8 +755,12 @@ DankPopout {
|
||||
isCurrent: root.activeTabId === modelData
|
||||
|
||||
onIsCurrentChanged: {
|
||||
if (isCurrent)
|
||||
if (isCurrent) {
|
||||
pages.currentHost = host;
|
||||
return;
|
||||
}
|
||||
if (pages.currentHost === host)
|
||||
pages.currentHost = null;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
|
||||
@@ -18,10 +18,6 @@ Singleton {
|
||||
readonly property real popoutWidthWide: popoutWidth + weekColumnWidth + Theme.spacingS
|
||||
readonly property real contentPadding: Theme.spacingM
|
||||
readonly property real contentGap: Theme.spacingM
|
||||
readonly property real tabBarLift: contentPadding - Theme.tabIndicatorHeight
|
||||
readonly property real tabHeight: Theme.buttonHeightM + tabBarLift
|
||||
readonly property real tabBarBlockHeight: tabHeight - tabBarLift
|
||||
readonly property real headerActionSize: Theme.iconButtonSize
|
||||
readonly property real spinnerSize: Theme.iconButtonSize
|
||||
readonly property real triggerWidth: CcMetrics.triggerWidth
|
||||
readonly property int transitionDuration: CcMetrics.transitionDuration
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import qs.Modules.DankDash.Overview
|
||||
import "utils/cards.js" as CardUtils
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string entryId
|
||||
required property Item overlayParent
|
||||
property var tabItem: null
|
||||
property bool vertical: false
|
||||
property bool editMode: false
|
||||
property bool panelResizable: false
|
||||
readonly property bool hasWidgets: entryId === DashRegistry.overviewId || typeof tabItem?.clearWidgets === "function"
|
||||
readonly property bool menuOpen: customMenu.open
|
||||
readonly property bool hasCustomActions: customMenu.visibleItems.length > 0
|
||||
readonly property var focusTargets: editControls.focusTargets
|
||||
|
||||
signal optionsRequested
|
||||
signal finished
|
||||
|
||||
implicitWidth: vertical ? Theme.navigationRailWidth : editControls.implicitWidth
|
||||
implicitHeight: editControls.implicitHeight
|
||||
|
||||
function closeMenu() {
|
||||
editControls.cancelConfirmation();
|
||||
customMenu.close();
|
||||
}
|
||||
|
||||
function clearFocus() {
|
||||
editControls.clearFocus();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (panelResizable)
|
||||
DashRegistry.resetPanelSize(entryId);
|
||||
if (entryId === DashRegistry.overviewId) {
|
||||
CardUtils.resetToDefault();
|
||||
return;
|
||||
}
|
||||
tabItem?.resetWidgets?.();
|
||||
}
|
||||
|
||||
function clear() {
|
||||
if (entryId === DashRegistry.overviewId) {
|
||||
CardUtils.clearAll();
|
||||
return;
|
||||
}
|
||||
tabItem?.clearWidgets?.();
|
||||
}
|
||||
|
||||
CcMenu {
|
||||
id: customMenu
|
||||
parent: root.overlayParent
|
||||
items: root.tabItem?.menuActions ?? []
|
||||
}
|
||||
|
||||
DashEditControls {
|
||||
id: editControls
|
||||
anchors.centerIn: parent
|
||||
width: root.width
|
||||
height: root.height
|
||||
visible: root.editMode
|
||||
vertical: root.vertical
|
||||
canAdd: (root.tabItem?.addable?.length ?? 0) > 0
|
||||
hasWidgets: root.hasWidgets
|
||||
hasOptions: DashRegistry.hasOptions(root.entryId)
|
||||
hasCustomActions: root.hasCustomActions
|
||||
onActionsRequested: anchor => customMenu.openAt(anchor)
|
||||
onAddRequested: anchor => root.tabItem?.openAddMenu(anchor)
|
||||
onOptionsRequested: root.optionsRequested()
|
||||
onResetRequested: root.reset()
|
||||
onClearRequested: root.clear()
|
||||
onFinished: root.finished()
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import "utils/cards.js" as CardUtils
|
||||
|
||||
CcMenu {
|
||||
id: root
|
||||
|
||||
required property string entryId
|
||||
property var tabItem: null
|
||||
property bool editMode: false
|
||||
property bool panelResizable: false
|
||||
readonly property bool hasWidgets: root.entryId === DashRegistry.fallbackId || typeof root.tabItem?.clearWidgets === "function"
|
||||
|
||||
signal editRequested
|
||||
signal optionsRequested
|
||||
signal settingsRequested
|
||||
|
||||
items: root.editMode ? [
|
||||
{
|
||||
"label": I18n.tr("Options", "noun, menu entry or button that opens item options"),
|
||||
"iconName": "tune",
|
||||
"visible": DashRegistry.hasOptions(root.entryId),
|
||||
"action": () => root.optionsRequested()
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("Reset to default"),
|
||||
"iconName": "settings_backup_restore",
|
||||
"action": () => {
|
||||
if (root.panelResizable)
|
||||
DashRegistry.resetPanelSize(root.entryId);
|
||||
if (root.entryId === DashRegistry.fallbackId)
|
||||
CardUtils.resetToDefault();
|
||||
else
|
||||
root.tabItem?.resetWidgets?.();
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("Clear All"),
|
||||
"iconName": "clear_all",
|
||||
"destructive": true,
|
||||
"visible": root.hasWidgets,
|
||||
"action": () => {
|
||||
if (root.entryId === DashRegistry.fallbackId)
|
||||
CardUtils.clearAll();
|
||||
else
|
||||
root.tabItem?.clearWidgets();
|
||||
}
|
||||
}
|
||||
] : (root.tabItem?.menuActions ?? []).concat([
|
||||
{
|
||||
"label": I18n.tr("Edit"),
|
||||
"iconName": "edit",
|
||||
"visible": root.panelResizable || root.hasWidgets,
|
||||
"action": () => root.editRequested()
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("Options"),
|
||||
"iconName": "tune",
|
||||
"visible": DashRegistry.hasOptions(root.entryId),
|
||||
"action": () => root.optionsRequested()
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("Settings"),
|
||||
"iconName": "settings",
|
||||
"action": () => root.settingsRequested()
|
||||
}
|
||||
])
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string entryId
|
||||
property bool editable: entryId !== ""
|
||||
readonly property Item focusTarget: titleIcon
|
||||
|
||||
signal editRequested
|
||||
|
||||
implicitHeight: Theme.buttonHeightS
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
StyledButton {
|
||||
id: titleIcon
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Theme.buttonHeightS
|
||||
height: Theme.buttonHeightS
|
||||
radius: Theme.fullRadius(width, height)
|
||||
color: Theme.secondaryContainer
|
||||
Accessible.name: root.editable ? I18n.tr("Edit") : I18n.tr("Dashboard")
|
||||
enabled: root.editable
|
||||
onClicked: root.editRequested()
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.editable && titleIcon.hovered ? "edit" : DashRegistry.entry(root.entryId)?.icon ?? "dashboard"
|
||||
size: Theme.iconSize
|
||||
color: Theme.onSecondaryContainer
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
control: titleIcon
|
||||
disabled: !root.editable
|
||||
stateColor: Theme.onSecondaryContainer
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: titleIcon.right
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: DashRegistry.entry(root.entryId)?.text ?? I18n.tr("Dashboard")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.surfaceText
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string pluginPrefix: "plugin_"
|
||||
readonly property string fallbackId: "overview"
|
||||
readonly property string overviewId: "overview"
|
||||
readonly property string defaultTabId: visibleTabIds[0] ?? ""
|
||||
readonly property string widgetsKey: "widgets"
|
||||
|
||||
readonly property var toneChoices: [
|
||||
@@ -83,7 +84,6 @@ Singleton {
|
||||
"text": I18n.tr("Overview", "dashboard tab name"),
|
||||
"icon": "dashboard",
|
||||
"description": I18n.tr("Clock, calendar, system info and profile"),
|
||||
"locked": true,
|
||||
"tab": {
|
||||
"component": overviewTab
|
||||
}
|
||||
@@ -398,7 +398,7 @@ Singleton {
|
||||
continue;
|
||||
seen[tab.id] = true;
|
||||
result.push(Object.assign({
|
||||
"enabled": tab.enabled || def.locked === true
|
||||
"enabled": tab.enabled
|
||||
}, def));
|
||||
}
|
||||
for (const def of known) {
|
||||
@@ -616,7 +616,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function indexId(index) {
|
||||
return visibleTabIds[index] ?? fallbackId;
|
||||
return visibleTabIds[index] ?? defaultTabId;
|
||||
}
|
||||
|
||||
function resolveId(tab) {
|
||||
@@ -624,13 +624,13 @@ Singleton {
|
||||
return indexId(tab);
|
||||
const raw = String(tab ?? "").trim();
|
||||
if (raw === "")
|
||||
return fallbackId;
|
||||
return defaultTabId;
|
||||
if (/^\d+$/.test(raw))
|
||||
return indexId(parseInt(raw));
|
||||
if (hasTab(raw))
|
||||
return raw;
|
||||
const lower = raw.toLowerCase();
|
||||
return tabIds.find(id => id.toLowerCase() === lower) ?? fallbackId;
|
||||
return tabIds.find(id => id.toLowerCase() === lower) ?? defaultTabId;
|
||||
}
|
||||
|
||||
function indexOf(id) {
|
||||
|
||||
@@ -158,7 +158,7 @@ FocusScope {
|
||||
id: historyButton
|
||||
visible: SettingsData.notificationHistoryEnabled
|
||||
buttonSize: Theme.iconButtonSize
|
||||
iconName: root.currentTab === 1 ? "notifications" : "history"
|
||||
iconName: root.currentTab === 1 ? "inbox" : "history"
|
||||
backgroundColor: Theme.secondaryContainer
|
||||
iconColor: Theme.onSecondaryContainer
|
||||
tooltipText: root.currentTab === 1 ? I18n.tr("Current", "notification center tab") : I18n.tr("History", "notification center tab")
|
||||
|
||||
@@ -1,80 +1,186 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import "../../../DankCommon/Common/FocusNavigation.js" as FocusNavigation
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property bool vertical: false
|
||||
property bool canAdd: true
|
||||
property bool hasWidgets: true
|
||||
property string title: I18n.tr("Widgets", "noun, title of widget edit controls and settings card")
|
||||
readonly property var focusTargets: [addButton, moreButton, finishButton]
|
||||
property bool hasOptions: false
|
||||
property bool hasCustomActions: false
|
||||
property string pendingAction: ""
|
||||
readonly property var actionButtons: [addButton, optionsButton, customButton, resetButton, clearButton, finishButton]
|
||||
readonly property var focusTargets: actionButtons.filter(item => item.visible && item.enabled)
|
||||
|
||||
signal addRequested(var anchor)
|
||||
signal menuRequested(var anchor)
|
||||
signal actionsRequested(var anchor)
|
||||
signal optionsRequested
|
||||
signal resetRequested
|
||||
signal clearRequested
|
||||
signal finished
|
||||
|
||||
height: Theme.buttonHeightS
|
||||
implicitWidth: actions.implicitWidth
|
||||
implicitHeight: actions.implicitHeight
|
||||
readonly property Item focusedItem: Window.activeFocusItem
|
||||
onFocusedItemChanged: revealFocusedItem()
|
||||
onWidthChanged: revealFocusedItem()
|
||||
onHeightChanged: revealFocusedItem()
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.right: actions.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.title
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.surfaceText
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
elide: Text.ElideRight
|
||||
function revealFocusedItem() {
|
||||
const target = focusedItem;
|
||||
if (!target || !focusTargets.includes(target))
|
||||
return;
|
||||
const point = target.mapToItem(viewport.contentItem, 0, 0);
|
||||
if (vertical) {
|
||||
viewport.contentY = Math.max(0, Math.min(point.y, Math.max(viewport.contentY, point.y + target.height - viewport.height)));
|
||||
return;
|
||||
}
|
||||
viewport.contentX = Math.max(0, Math.min(point.x, Math.max(viewport.contentX, point.x + target.width - viewport.width)));
|
||||
}
|
||||
|
||||
Row {
|
||||
id: actions
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
function cancelConfirmation() {
|
||||
pendingAction = "";
|
||||
}
|
||||
|
||||
DankButton {
|
||||
id: addButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonHeight: Theme.buttonHeightS
|
||||
iconName: "add"
|
||||
text: I18n.tr("Add widget")
|
||||
backgroundColor: Theme.secondaryContainer
|
||||
textColor: Theme.onSecondaryContainer
|
||||
visible: root.hasWidgets
|
||||
enabled: root.canAdd
|
||||
KeyNavigation.tab: moreButton
|
||||
KeyNavigation.backtab: finishButton
|
||||
onClicked: root.addRequested(addButton)
|
||||
function clearFocus() {
|
||||
for (const target of actionButtons)
|
||||
target.focus = false;
|
||||
}
|
||||
|
||||
onVisibleChanged: cancelConfirmation()
|
||||
|
||||
component EditButton: DankButton {
|
||||
property string label: ""
|
||||
text: root.vertical ? "" : label
|
||||
tooltipText: root.vertical ? label : null
|
||||
buttonHeight: Theme.buttonHeightS
|
||||
}
|
||||
|
||||
component ConfirmButton: EditButton {
|
||||
id: confirmation
|
||||
required property string actionId
|
||||
property color armedColor: Theme.primary
|
||||
property color armedTextColor: Theme.onPrimary
|
||||
readonly property bool armed: root.pendingAction === actionId
|
||||
readonly property string confirmLabel: I18n.tr("Confirm")
|
||||
signal confirmed
|
||||
text: root.vertical ? "" : armed ? confirmLabel : label
|
||||
reserveText: root.vertical ? "" : armed ? label : confirmLabel
|
||||
tooltipText: armed ? confirmLabel : root.vertical ? label : null
|
||||
backgroundColor: armed ? armedColor : Theme.buttonBg
|
||||
textColor: armed ? armedTextColor : Theme.buttonText
|
||||
onClicked: {
|
||||
if (!armed) {
|
||||
root.pendingAction = actionId;
|
||||
return;
|
||||
}
|
||||
root.pendingAction = "";
|
||||
confirmed();
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape && pendingAction !== "") {
|
||||
cancelConfirmation();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key !== Qt.Key_Tab && event.key !== Qt.Key_Backtab)
|
||||
return;
|
||||
const backwards = event.key === Qt.Key_Backtab || !!(event.modifiers & Qt.ShiftModifier);
|
||||
if (!FocusNavigation.moveFocus(focusTargets, backwards))
|
||||
focusTargets[backwards ? focusTargets.length - 1 : 0]?.forceActiveFocus(backwards ? Qt.BacktabFocusReason : Qt.TabFocusReason);
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: viewport
|
||||
anchors.fill: parent
|
||||
contentWidth: root.vertical ? width : Math.max(width, actions.implicitWidth)
|
||||
contentHeight: root.vertical ? actions.implicitHeight : height
|
||||
flickableDirection: root.vertical ? Flickable.VerticalFlick : Flickable.HorizontalFlick
|
||||
wheelEnabled: root.vertical
|
||||
clip: true
|
||||
|
||||
WheelHandler {
|
||||
enabled: !root.vertical && viewport.contentWidth > viewport.width
|
||||
onWheel: event => {
|
||||
const delta = event.pixelDelta.x || event.pixelDelta.y || event.angleDelta.x || event.angleDelta.y;
|
||||
viewport.contentX = Math.max(0, Math.min(viewport.contentWidth - viewport.width, viewport.contentX - delta));
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: moreButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonSize: Theme.buttonHeightS
|
||||
iconName: "more_vert"
|
||||
Accessible.name: I18n.tr("Options")
|
||||
KeyNavigation.tab: finishButton
|
||||
KeyNavigation.backtab: addButton
|
||||
onClicked: root.menuRequested(moreButton)
|
||||
}
|
||||
Grid {
|
||||
id: actions
|
||||
x: Math.max(0, (viewport.width - implicitWidth) / 2)
|
||||
y: root.vertical ? 0 : Math.max(0, (viewport.height - implicitHeight) / 2)
|
||||
columns: root.vertical ? 1 : root.actionButtons.length
|
||||
horizontalItemAlignment: Grid.AlignHCenter
|
||||
verticalItemAlignment: Grid.AlignVCenter
|
||||
LayoutMirroring.enabled: false
|
||||
spacing: Theme.spacingS
|
||||
layoutDirection: I18n.isRtl && !root.vertical ? Qt.RightToLeft : Qt.LeftToRight
|
||||
|
||||
DankButton {
|
||||
id: finishButton
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonHeight: Theme.buttonHeightS
|
||||
iconName: "check"
|
||||
text: I18n.tr("Finish")
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.onPrimary
|
||||
KeyNavigation.tab: addButton
|
||||
KeyNavigation.backtab: moreButton
|
||||
onClicked: root.finished()
|
||||
EditButton {
|
||||
id: addButton
|
||||
label: I18n.tr("Add widget")
|
||||
iconName: "add"
|
||||
backgroundColor: Theme.secondaryContainer
|
||||
textColor: Theme.onSecondaryContainer
|
||||
visible: root.hasWidgets
|
||||
enabled: root.canAdd
|
||||
onClicked: root.addRequested(addButton)
|
||||
}
|
||||
|
||||
EditButton {
|
||||
id: optionsButton
|
||||
label: I18n.tr("Options")
|
||||
iconName: "tune"
|
||||
visible: root.hasOptions
|
||||
onClicked: root.optionsRequested()
|
||||
}
|
||||
|
||||
EditButton {
|
||||
id: customButton
|
||||
label: I18n.tr("Actions", "Dashboard page actions supplied by a plugin or built-in page")
|
||||
iconName: "more_horiz"
|
||||
visible: root.hasCustomActions
|
||||
onClicked: root.actionsRequested(customButton)
|
||||
}
|
||||
|
||||
ConfirmButton {
|
||||
id: resetButton
|
||||
actionId: "reset"
|
||||
label: I18n.tr("Reset to default")
|
||||
iconName: "settings_backup_restore"
|
||||
onConfirmed: root.resetRequested()
|
||||
}
|
||||
|
||||
ConfirmButton {
|
||||
id: clearButton
|
||||
actionId: "clear"
|
||||
label: I18n.tr("Clear All")
|
||||
iconName: "clear_all"
|
||||
armedColor: Theme.error
|
||||
armedTextColor: Theme.onError
|
||||
visible: root.hasWidgets
|
||||
onConfirmed: root.clearRequested()
|
||||
}
|
||||
|
||||
EditButton {
|
||||
id: finishButton
|
||||
label: I18n.tr("Finish")
|
||||
iconName: "check"
|
||||
backgroundColor: Theme.primary
|
||||
textColor: Theme.onPrimary
|
||||
onClicked: root.finished()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ CcSheetDialog {
|
||||
|
||||
DankButton {
|
||||
anchors.right: parent.right
|
||||
text: I18n.tr("Reset")
|
||||
text: I18n.tr("Reset to default")
|
||||
iconName: "restart_alt"
|
||||
buttonHeight: Theme.buttonHeightXS
|
||||
backgroundColor: "transparent"
|
||||
|
||||
@@ -32,7 +32,7 @@ Card {
|
||||
|
||||
entryId: "media"
|
||||
clickable: true
|
||||
pad: compact ? Theme.spacingS : Theme.spacingM
|
||||
pad: Theme.spacingM
|
||||
|
||||
Keys.onShortcutOverride: event => {
|
||||
if (!root.interactive || event.key !== Qt.Key_F6)
|
||||
|
||||
@@ -465,7 +465,7 @@ Item {
|
||||
showDotAndDotDot: false
|
||||
showHidden: false
|
||||
caseSensitive: false
|
||||
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
|
||||
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr", "*.svg"]
|
||||
showFiles: true
|
||||
showDirs: false
|
||||
sortField: {
|
||||
@@ -515,7 +515,7 @@ Item {
|
||||
browserTitle: I18n.tr("Select Wallpaper Directory", "wallpaper directory file browser title")
|
||||
browserType: "wallpaper"
|
||||
showHiddenFiles: false
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr", "*.svg"]
|
||||
parentPopout: root.parentPopout
|
||||
|
||||
onFileSelected: path => {
|
||||
|
||||
@@ -68,16 +68,17 @@ FocusScope {
|
||||
clip: true
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
KeyNavigation.tab: root.tab?.focusTarget ?? null
|
||||
KeyNavigation.tab: !root.editMode && !tabOptions.shown && !pageActions.menuOpen ? root.tab?.focusTarget ?? null : null
|
||||
|
||||
function focusFace() {
|
||||
pageActions.clearFocus();
|
||||
(root.tab?.focusTarget ?? root).forceActiveFocus();
|
||||
return true;
|
||||
}
|
||||
|
||||
function focusHeader(backwards) {
|
||||
const targets = root.editMode ? editControls.focusTargets : [menuButton];
|
||||
targets[backwards ? targets.length - 1 : 0].forceActiveFocus();
|
||||
const targets = root.editMode ? pageActions.focusTargets : [pageTitle.focusTarget];
|
||||
targets[backwards ? targets.length - 1 : 0]?.forceActiveFocus(backwards ? Qt.BacktabFocusReason : Qt.TabFocusReason);
|
||||
}
|
||||
|
||||
function reportHeight() {
|
||||
@@ -90,7 +91,7 @@ FocusScope {
|
||||
if (live)
|
||||
return;
|
||||
editMode = false;
|
||||
headerMenu.close();
|
||||
pageActions.closeMenu();
|
||||
tabOptions.dismiss();
|
||||
}
|
||||
onEditModeChanged: {
|
||||
@@ -100,16 +101,32 @@ FocusScope {
|
||||
return;
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
if (root.live && root.editMode)
|
||||
root.focusHeader(false);
|
||||
if (!root.live || !root.editMode)
|
||||
return;
|
||||
pageActions.clearFocus();
|
||||
pageTitle.focusTarget.focus = false;
|
||||
tabLoader.focus = false;
|
||||
root.forceActiveFocus(Qt.OtherFocusReason);
|
||||
});
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (tabOptions.shown || pageActions.menuOpen)
|
||||
return;
|
||||
if (root.editMode && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
|
||||
root.focusHeader(event.key === Qt.Key_Backtab || !!(event.modifiers & Qt.ShiftModifier));
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (root.tab?.handleKeyEvent?.(event) === true) {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (!root.editMode && (event.key === Qt.Key_F2 || (event.key === Qt.Key_E && (event.modifiers & Qt.ControlModifier)))) {
|
||||
root.editMode = true;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key !== Qt.Key_Escape || !root.editMode)
|
||||
return;
|
||||
root.editMode = false;
|
||||
@@ -119,6 +136,7 @@ FocusScope {
|
||||
|
||||
Item {
|
||||
id: header
|
||||
enabled: !tabOptions.shown && !pageActions.menuOpen
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
@@ -127,30 +145,29 @@ FocusScope {
|
||||
leftMargin: DashMetrics.contentPadding + root.editGutter
|
||||
rightMargin: DashMetrics.contentPadding + root.editGutter
|
||||
}
|
||||
height: root.editMode ? editControls.height : Theme.buttonHeightXS
|
||||
height: Theme.minimumTouchTargetSize
|
||||
|
||||
DankActionButton {
|
||||
id: menuButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonSize: Theme.buttonHeightXS
|
||||
iconName: "more_vert"
|
||||
Accessible.name: I18n.tr("Options")
|
||||
DashPageTitle {
|
||||
id: pageTitle
|
||||
anchors.fill: parent
|
||||
entryId: root.entryId
|
||||
visible: !root.editMode
|
||||
KeyNavigation.tab: root.tab?.focusTarget ?? null
|
||||
KeyNavigation.backtab: root.tab?.previousFocusTarget ?? root.tab?.focusTarget ?? null
|
||||
onClicked: headerMenu.openAt(menuButton)
|
||||
onEditRequested: root.editMode = true
|
||||
}
|
||||
|
||||
DashEditControls {
|
||||
id: editControls
|
||||
anchors.left: parent.left
|
||||
DashPageActions {
|
||||
id: pageActions
|
||||
visible: root.editMode
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.editMode
|
||||
canAdd: (root.tab?.addable?.length ?? 0) > 0
|
||||
onAddRequested: anchor => root.tab?.openAddMenu(anchor)
|
||||
onMenuRequested: anchor => headerMenu.openAt(anchor)
|
||||
width: Math.min(parent.width, implicitWidth)
|
||||
height: parent.height
|
||||
overlayParent: root
|
||||
entryId: root.entryId
|
||||
tabItem: root.tab
|
||||
editMode: root.editMode
|
||||
panelResizable: true
|
||||
onOptionsRequested: tabOptions.presentFor(root.entryId)
|
||||
onFinished: {
|
||||
root.editMode = false;
|
||||
root.focusFace();
|
||||
@@ -160,6 +177,7 @@ FocusScope {
|
||||
|
||||
DankFlickable {
|
||||
id: pages
|
||||
enabled: !tabOptions.shown && !pageActions.menuOpen
|
||||
anchors {
|
||||
top: header.bottom
|
||||
topMargin: Theme.spacingXS
|
||||
@@ -229,19 +247,6 @@ FocusScope {
|
||||
onDismissed: root.focusFace()
|
||||
}
|
||||
|
||||
DashPageMenu {
|
||||
id: headerMenu
|
||||
entryId: root.entryId
|
||||
tabItem: root.tab
|
||||
editMode: root.editMode
|
||||
onEditRequested: root.editMode = true
|
||||
onOptionsRequested: tabOptions.presentFor(root.entryId)
|
||||
onSettingsRequested: {
|
||||
root.controller.requestCollapse();
|
||||
PopoutService.openSettingsWithTab("dank_dash");
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: root.controller.setEditing(root.activityId, false)
|
||||
|
||||
Component.onCompleted: {
|
||||
|
||||
@@ -8,7 +8,7 @@ DashTabFace {
|
||||
id: root
|
||||
|
||||
activityId: "home"
|
||||
entryId: DashRegistry.fallbackId
|
||||
entryId: DashRegistry.overviewId
|
||||
tabComponent: Component {
|
||||
OverviewTab {
|
||||
live: root.live
|
||||
|
||||
@@ -113,7 +113,7 @@ QtObject {
|
||||
function dashEntryIdFor(activityId) {
|
||||
switch (activityId) {
|
||||
case "home":
|
||||
return DashRegistry.fallbackId;
|
||||
return DashRegistry.overviewId;
|
||||
case "notificationcenter":
|
||||
return "notifications";
|
||||
}
|
||||
|
||||
@@ -52,8 +52,56 @@ FocusScope {
|
||||
headerActions: DankActionButton {
|
||||
iconName: "refresh"
|
||||
iconSize: Theme.iconSizeSmall
|
||||
tooltipText: I18n.tr("Reset")
|
||||
onClicked: SettingsData.resetDashTabs()
|
||||
tooltipText: I18n.tr("Reset to default")
|
||||
onClicked: {
|
||||
SettingsData.resetDashTabs();
|
||||
SettingsData.resetToDefault(["dashTabPosition", "dashTabsEvenlySpaced"]);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsDropdownRow {
|
||||
text: I18n.tr("Position")
|
||||
readonly property var positions: [
|
||||
{
|
||||
value: "auto",
|
||||
text: I18n.tr("Auto")
|
||||
},
|
||||
{
|
||||
value: "left",
|
||||
text: I18n.tr("Left")
|
||||
},
|
||||
{
|
||||
value: "right",
|
||||
text: I18n.tr("Right")
|
||||
},
|
||||
{
|
||||
value: "bottom",
|
||||
text: I18n.tr("Bottom")
|
||||
},
|
||||
{
|
||||
value: "center",
|
||||
text: I18n.tr("Center")
|
||||
}
|
||||
]
|
||||
settingKey: "dashTabPosition"
|
||||
tab: "dank_dash"
|
||||
tags: ["dashboard", "tabs", "position", "navigation"]
|
||||
currentValue: (positions.find(option => option.value === SettingsData.dashTabPosition) ?? positions[0]).text
|
||||
options: positions.map(option => option.text)
|
||||
onValueChanged: value => {
|
||||
const option = positions.find(option => option.text === value);
|
||||
if (option)
|
||||
SettingsData.set("dashTabPosition", option.value);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "dashTabsEvenlySpaced"
|
||||
tab: "dank_dash"
|
||||
tags: ["dashboard", "tabs", "spacing", "navigation"]
|
||||
text: I18n.tr("Evenly space tabs")
|
||||
checked: SettingsData.dashTabsEvenlySpaced
|
||||
onToggled: checked => SettingsData.set("dashTabsEvenlySpaced", checked)
|
||||
}
|
||||
|
||||
SettingsReorderList {
|
||||
@@ -70,25 +118,33 @@ FocusScope {
|
||||
reorderList: tabList
|
||||
|
||||
readonly property bool available: modelData.available !== false
|
||||
readonly property bool locked: modelData.locked === true
|
||||
|
||||
iconName: modelData.icon
|
||||
iconColor: available && modelData.enabled ? Theme.primary : Theme.onSurface_38
|
||||
titleColor: available ? Theme.surfaceText : Theme.onSurface_38
|
||||
title: modelData.text
|
||||
subtitle: available ? modelData.description ?? "" : I18n.tr("Disabled")
|
||||
clickable: available && !locked
|
||||
clickable: available
|
||||
onClicked: SettingsData.setDashTabEnabled(modelData.id, !modelData.enabled)
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankActionButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "edit"
|
||||
tooltipText: I18n.tr("Edit")
|
||||
Accessible.description: tabRow.modelData.text
|
||||
enabled: tabRow.available
|
||||
onClicked: PopoutService.openDankDashEditor(tabRow.modelData.id, root.Window.window?.screen)
|
||||
}
|
||||
|
||||
DankToggle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
hideText: true
|
||||
checked: tabRow.modelData.enabled
|
||||
enabled: tabRow.available && !tabRow.locked
|
||||
enabled: tabRow.available
|
||||
onToggled: checked => SettingsData.setDashTabEnabled(tabRow.modelData.id, checked)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ Item {
|
||||
browserTitle: I18n.tr("Select lock screen background image")
|
||||
browserType: "wallpaper"
|
||||
showHiddenFiles: true
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif"]
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.svg"]
|
||||
onFileSelected: path => {
|
||||
SettingsData.set("lockScreenWallpaperPath", path);
|
||||
close();
|
||||
|
||||
@@ -860,7 +860,7 @@ Column {
|
||||
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
|
||||
browserType: "wallpaper"
|
||||
showHiddenFiles: true
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
|
||||
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr", "*.svg"]
|
||||
onFileSelected: path => {
|
||||
root.applyBrowsedPath(path);
|
||||
close();
|
||||
|
||||
@@ -10,4 +10,4 @@ What it shows:
|
||||
- `tone` and the card colors (`accentColor`, `contentColor`, `mutedColor`, `chipColor`), so the card follows the Surface, Primary, Secondary and Tertiary tones like the built-in cards.
|
||||
- `dash.options`: a choice for the period, a toggle for the time left and a choice for the tone. They show up in the card's options sheet in edit mode and under Settings → Dashboard.
|
||||
|
||||
Add it from the dash: open the overview, click the three-dot menu, Edit, then Add widget.
|
||||
Add it from the dash: open the overview, hover the selected page icon, click the pencil, then Add widget.
|
||||
@@ -9,7 +9,7 @@ What it shows:
|
||||
- The plugin state API (`loadPluginState` / `savePluginState`) for runtime data. Nothing goes into settings, so no permissions are needed.
|
||||
- `focusTarget` and `restoreFocus()`. Opening the tab focuses Start without a focus ring, so Space starts it right away.
|
||||
- `handleKeyEvent`: Space starts or pauses, L records a lap, R resets.
|
||||
- `menuActions`: Reset in the tab's three-dot menu.
|
||||
- `menuActions`: Reset in the tab's edit-mode controls.
|
||||
- `dash.options`: a toggle for hundredths and a number for how many laps to list, read as `options.hundredths` and `options.laps`.
|
||||
|
||||
Open it with `dms ipc call dash open plugin_dashTabExample`.
|
||||
@@ -1826,7 +1826,7 @@ The dash popout (`dms ipc call dash open`) is a set of entries. Each entry can h
|
||||
|
||||
Only enabled plugins show up. A disabled plugin has no tab, no card, no row under Settings → Dashboard and no entry in the Add widget menu. Its saved placement is kept, so re-enabling it puts the card back where it was.
|
||||
|
||||
A plugin tab is enabled in the tab bar as soon as the plugin loads. Users hide it under Settings → Dashboard → Tabs. Cards are never placed automatically: users add them from the dash (three-dot menu → Edit → Add widget). A plugin with both surfaces gets one interaction for free: clicking its card opens its tab, and a tab hidden from the bar opens as a detail page with a back button.
|
||||
A plugin tab is enabled in the tab bar as soon as the plugin loads. Users hide it under Settings → Dashboard → Tabs. Cards are never placed automatically: users add them from the dash (hover the selected page icon → click the pencil → Add widget). A plugin with both surfaces gets one interaction for free: clicking its card opens its tab, and a tab hidden from the bar opens as a detail page. It has a back button when enabled destinations exist; otherwise it opens standalone.
|
||||
|
||||
Both surfaces load lazily. A tab is created when it becomes current and destroyed when the user switches away. Cards are created with the overview grid. The dash content stays alive after the popout closes, so gate timers, animations and service refs on `live`.
|
||||
|
||||
@@ -1887,7 +1887,7 @@ 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 three-dot menu) 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.
|
||||
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.
|
||||
|
||||
`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.
|
||||
|
||||
@@ -1917,7 +1917,7 @@ Optional on the tab:
|
||||
- `focusTarget`: the item that receives focus when Down enters the tab content
|
||||
- `restoreFocus()`: called when the dash opens on the tab or returns focus to it. Focus your content here with `Qt.OtherFocusReason` so no focus ring is drawn
|
||||
- `blocksTabNavigation`: true while an editor or local control group needs native Tab traversal; false returns Tab to the dash navigation
|
||||
- `menuActions`: actions shown in the tab's three-dot menu before Edit and Options. Each action has `label`, `iconName`, `action`, and optional `visible` and `enabled`
|
||||
- `menuActions`: actions shown in the Actions menu while editing the tab. Each action has `label`, `iconName`, `action`, and optional `visible` and `enabled`
|
||||
- `signal tabRequested(string id)`: switch the dash to another tab (`"overview"`, `"media"`, `"wallpaper"`, `"weather"`, `"notifications"` or a `plugin_<id>`); a tab hidden from the bar opens as a detail page
|
||||
- `signal navFocusRequested`: return focus to the dash navigation
|
||||
|
||||
|
||||
@@ -264,6 +264,7 @@ Singleton {
|
||||
|
||||
property bool _dankDashWantsOpen: false
|
||||
property bool _dankDashWantsToggle: false
|
||||
property bool _dankDashWantsEdit: false
|
||||
property var _dankDashPendingTab: 0
|
||||
property real _dankDashPendingX: 0
|
||||
property real _dankDashPendingY: 0
|
||||
@@ -282,6 +283,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function openDankDash(tab, x, y, width, section, screen) {
|
||||
_dankDashWantsEdit = false;
|
||||
_dankDashPendingTab = tab || 0;
|
||||
if (dankDashPopout) {
|
||||
if (arguments.length >= 6)
|
||||
@@ -299,11 +301,13 @@ Singleton {
|
||||
}
|
||||
|
||||
function closeDankDash() {
|
||||
_dankDashWantsEdit = false;
|
||||
if (dankDashPopout)
|
||||
dankDashPopout.dashVisible = false;
|
||||
}
|
||||
|
||||
function toggleDankDash(tab, x, y, width, section, screen) {
|
||||
_dankDashWantsEdit = false;
|
||||
_dankDashPendingTab = tab || 0;
|
||||
if (dankDashPopout) {
|
||||
if (arguments.length >= 6)
|
||||
@@ -328,6 +332,11 @@ Singleton {
|
||||
if (!dankDashPopout)
|
||||
return;
|
||||
|
||||
if (_dankDashWantsEdit) {
|
||||
_showDankDashEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dankDashHasPosition)
|
||||
setPosition(dankDashPopout, _dankDashPendingX, _dankDashPendingY, _dankDashPendingWidth, _dankDashPendingSection, _dankDashPendingScreen);
|
||||
|
||||
@@ -348,6 +357,34 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function openDankDashEditor(tab, screen) {
|
||||
const target = screen ?? Quickshell.screens.find(candidate => candidate.name === CompositorService.getFocusedScreenName()) ?? Quickshell.screens[0];
|
||||
if (!target || (!dankDashPopout && !dankDashPopoutLoader))
|
||||
return;
|
||||
closeSettings();
|
||||
_dankDashPendingTab = tab;
|
||||
_dankDashPendingScreen = target;
|
||||
_dankDashHasPosition = false;
|
||||
_dankDashWantsOpen = false;
|
||||
_dankDashWantsToggle = false;
|
||||
_dankDashWantsEdit = true;
|
||||
if (dankDashPopout) {
|
||||
_showDankDashEditor();
|
||||
return;
|
||||
}
|
||||
dankDashPopoutLoader.active = true;
|
||||
}
|
||||
|
||||
function _showDankDashEditor() {
|
||||
_dankDashWantsEdit = false;
|
||||
const target = _dankDashPendingScreen;
|
||||
const anchor = BarWidgetService.naturalPopoutAnchor(target, null, "center");
|
||||
dankDashPopout.setTriggerPosition(anchor.trigger.x, anchor.trigger.y, anchor.trigger.width, anchor.section, target, anchor.position, anchor.thickness, anchor.spacing, anchor.config);
|
||||
dankDashPopout.requestTab(_dankDashPendingTab);
|
||||
dankDashPopout.editMode = true;
|
||||
dankDashPopout.dashVisible = true;
|
||||
}
|
||||
|
||||
function openBattery(x, y, width, section, screen) {
|
||||
if (batteryPopout) {
|
||||
setPosition(batteryPopout, x, y, width, section, screen);
|
||||
|
||||
@@ -135,7 +135,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function findCommand(wallpaperDir) {
|
||||
return ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" \\) 2>/dev/null | sort`];
|
||||
return ["sh", "-c", `find -L "${wallpaperDir}" -maxdepth 1 -type f \\( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.bmp" -o -iname "*.gif" -o -iname "*.webp" -o -iname "*.jxl" -o -iname "*.avif" -o -iname "*.heif" -o -iname "*.exr" -o -iname "*.svg" \\) 2>/dev/null | sort`];
|
||||
}
|
||||
|
||||
function monitorProcessFor(screenName) {
|
||||
@@ -292,7 +292,7 @@ Singleton {
|
||||
if (wallpaperList.length > 1) {
|
||||
do {
|
||||
targetIndex = Math.floor(Math.random() * wallpaperList.length);
|
||||
} while (targetIndex === currentIndex);
|
||||
} while (targetIndex === currentIndex)
|
||||
} else {
|
||||
targetIndex = 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
DankCommon.DankNavigationBar {}
|
||||
@@ -777,7 +777,7 @@ Item {
|
||||
property real renderedAlignedHeight: alignedHeight
|
||||
// Snap rendered geometry while the entrance morph runs so it doesn't ride a second animation.
|
||||
readonly property bool _settlingToOpen: shouldBeVisible && (morph.running || travelSpring.running)
|
||||
readonly property bool _geometryMotion: animationsEnabled && contentWindow.visible && shouldBeVisible && !_settlingToOpen && !resizing
|
||||
readonly property bool _geometryMotion: animationsEnabled && !SettingsData.reduceMotion && resizeDuration > 0 && contentWindow.visible && shouldBeVisible && !_settlingToOpen && !resizing
|
||||
property real _bodyGlide: 1
|
||||
property bool _bodyBatch: false
|
||||
property real _bodyFromY: 0
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.DankDash
|
||||
import qs.Modules.DankIsland
|
||||
import qs.Modules.DankIsland.Activities
|
||||
import qs.Modules.Settings as Settings
|
||||
import qs.DankCommon.Common as DC
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
property var dash: null
|
||||
property bool captured: false
|
||||
property int customActions: 0
|
||||
readonly property string captureDirectory: Quickshell.env("DMS_FIXTURE_CAPTURE_DIR")
|
||||
|
||||
Component {
|
||||
id: dashComponent
|
||||
DankDashPopout {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: settingsComponent
|
||||
Settings.DankDashTab {}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: deferredDashLoader
|
||||
property bool active: false
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pluginComponent
|
||||
Item {
|
||||
implicitHeight: DashMetrics.tabMinHeight
|
||||
property var menuActions: [
|
||||
{
|
||||
label: "Test action",
|
||||
iconName: "refresh",
|
||||
action: () => root.customActions++
|
||||
},
|
||||
{
|
||||
label: "Hidden action",
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
label: "Disabled action",
|
||||
enabled: false,
|
||||
action: () => root.customActions++
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
IslandController {
|
||||
id: island
|
||||
}
|
||||
|
||||
Component {
|
||||
id: islandHome
|
||||
HomeExpanded {
|
||||
controller: island
|
||||
}
|
||||
}
|
||||
|
||||
FloatingWindow {
|
||||
id: window
|
||||
visible: true
|
||||
implicitWidth: 1100
|
||||
implicitHeight: 800
|
||||
color: Theme.surface
|
||||
|
||||
DankNavigationBar {
|
||||
id: navigation
|
||||
width: 800
|
||||
height: implicitHeight
|
||||
currentIndex: 0
|
||||
model: [
|
||||
{
|
||||
icon: "dashboard",
|
||||
text: "Overview"
|
||||
},
|
||||
{
|
||||
icon: "music_note",
|
||||
text: "Media"
|
||||
}
|
||||
]
|
||||
onActivated: index => currentIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
TestCase {
|
||||
id: tester
|
||||
when: false
|
||||
|
||||
function check(condition, message) {
|
||||
if (!condition)
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
function find(item, predicate) {
|
||||
if (!item)
|
||||
return null;
|
||||
if (predicate(item))
|
||||
return item;
|
||||
for (const child of item.children || []) {
|
||||
const found = find(child, predicate);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function settle() {
|
||||
wait(0);
|
||||
const surface = root.dash?.contentLoader.item;
|
||||
if (surface)
|
||||
check(!isPolishScheduled(surface) || waitForPolish(surface, 3000), "dashboard layout settled");
|
||||
}
|
||||
|
||||
function open(tab) {
|
||||
root.dash.requestTab(tab);
|
||||
root.dash.dashVisible = true;
|
||||
tryVerify(() => root.dash.shouldBeVisible && root.dash.contentLoader.item?.ready, 3000);
|
||||
settle();
|
||||
}
|
||||
|
||||
function pages() {
|
||||
return find(root.dash.contentLoader.item, item => item.currentHost !== undefined);
|
||||
}
|
||||
|
||||
function bar() {
|
||||
return find(root.dash.contentLoader.item, item => typeof item.moveSelection === "function");
|
||||
}
|
||||
|
||||
function actionBounds(...buttons) {
|
||||
return JSON.stringify(buttons.map(button => {
|
||||
const point = button.mapToItem(root.dash.contentLoader.item, 0, 0);
|
||||
return [point.x, point.y, button.width, button.height];
|
||||
}));
|
||||
}
|
||||
|
||||
function capture(name) {
|
||||
if (!root.captureDirectory)
|
||||
return;
|
||||
tryVerify(() => Math.abs(root.dash.contentLoader.item.width - root.dash.popupWidth) < 1, 3000, "rendered panel width matches requested width");
|
||||
tryVerify(() => Math.abs(root.dash.contentLoader.item.height - root.dash.popupHeight) < 1, 3000, "rendered panel height matches requested height");
|
||||
root.captured = false;
|
||||
root.dash.contentLoader.item.grabToImage(result => {
|
||||
check(result.saveToFile(root.captureDirectory + "/" + name + ".png"), "saved " + name);
|
||||
root.captured = true;
|
||||
});
|
||||
tryCompare(root, "captured", true, 3000);
|
||||
}
|
||||
|
||||
function run() {
|
||||
try {
|
||||
SettingsData.weatherEnabled = true;
|
||||
SettingsData.setDashTabEnabled("overview", false);
|
||||
check(!DashRegistry.visibleTabIds.includes("overview"), "Overview can be hidden");
|
||||
check(DashRegistry.hasTab("overview"), "hidden Overview remains available");
|
||||
check(DashRegistry.defaultTabId !== "overview", "default follows enabled destinations");
|
||||
root.dash = dashComponent.createObject(root);
|
||||
root.dash.triggerScreen = Quickshell.screens[0];
|
||||
root.dash.setTriggerPosition(640, 0, 40, "center", Quickshell.screens[0]);
|
||||
open(0);
|
||||
check(root.dash.activeTabId === DashRegistry.visibleTabIds[0], "numeric default uses first enabled destination");
|
||||
open("overview");
|
||||
check(root.dash.detailTabId === "overview" && root.dash.showBack, "hidden Overview has a return destination");
|
||||
root.dash.closeDetail();
|
||||
check(root.dash.activeTabId === DashRegistry.defaultTabId, "Back returns to enabled page");
|
||||
|
||||
for (const id of DashRegistry.tabIds)
|
||||
SettingsData.setDashTabEnabled(id, false);
|
||||
open(0);
|
||||
check(root.dash.activeTabId === "" && !root.dash.showTabs, "zero tabs opens empty dashboard");
|
||||
check(pages().currentItem === null && pages().focusTarget !== null, "empty state releases page and offers settings");
|
||||
check(Math.abs(root.dash.contentLoader.item.height - root.dash.popupHeight) < 1, "zero-motion empty state snaps rendered height");
|
||||
capture("empty");
|
||||
PopoutService.dankDashPopout = root.dash;
|
||||
const settingsPage = settingsComponent.createObject(window.contentItem, {
|
||||
width: 800,
|
||||
height: 600,
|
||||
visible: false
|
||||
});
|
||||
for (const id of ["overview", "media", "weather"]) {
|
||||
const row = find(settingsPage, item => item.modelData?.id === id && item.available !== undefined);
|
||||
const editButton = find(row, item => item.iconName === "edit" && typeof item.clicked === "function");
|
||||
check(!!editButton, "Settings exposes editing for hidden " + id);
|
||||
editButton.clicked();
|
||||
check(root.dash.editMode && root.dash.activeTabId === id && !root.dash.showTabs, "Settings edits hidden " + id + " with zero tabs");
|
||||
}
|
||||
settingsPage.destroy();
|
||||
root.dash.dashVisible = false;
|
||||
root.dash.instantClose();
|
||||
PopoutService.dankDashPopout = null;
|
||||
PopoutService.dankDashPopoutLoader = deferredDashLoader;
|
||||
PopoutService.openDankDashEditor("media", Quickshell.screens[0]);
|
||||
check(deferredDashLoader.active && PopoutService._dankDashWantsEdit, "cold editor request activates loader");
|
||||
PopoutService.dankDashPopout = root.dash;
|
||||
PopoutService._onDankDashPopoutLoaded();
|
||||
check(root.dash.editMode && root.dash.activeTabId === "media" && !PopoutService._dankDashWantsEdit, "cold editor request survives loading");
|
||||
root.dash.editMode = false;
|
||||
for (const id of ["overview", "media", "weather"]) {
|
||||
open(id);
|
||||
check(root.dash.activeTabId === id && !root.dash.showTabs && !root.dash.showBack, "standalone hidden " + id);
|
||||
}
|
||||
capture("standalone-weather");
|
||||
root.dash.closeDetail();
|
||||
check(!root.dash.dashVisible, "standalone Back/Escape closes instead of showing empty state");
|
||||
|
||||
const home = islandHome.createObject(window.contentItem, {
|
||||
width: 800,
|
||||
height: 600
|
||||
});
|
||||
check(home.entryId === "overview", "Island home keeps Overview identity with no enabled tabs");
|
||||
tryVerify(() => !!home.tab, 3000);
|
||||
const islandActions = find(home, item => item.overlayParent !== undefined && item.entryId === "overview");
|
||||
check(!!islandActions && islandActions.hasWidgets, "Island uses shared page actions");
|
||||
parent = window.contentItem;
|
||||
island.expanded = true;
|
||||
home.focusHeader(false);
|
||||
home.editMode = true;
|
||||
settle();
|
||||
check(home.Window.window.activeFocusItem === home, "Island edit entry clears title focus");
|
||||
islandActions.optionsRequested();
|
||||
const islandOptions = find(home, item => typeof item.presentFor === "function" && item.entryId === "overview");
|
||||
check(islandOptions.shown, "Island Options opened before Tab");
|
||||
keyClick(Qt.Key_Tab);
|
||||
check(islandOptions.shown && !islandActions.focusTargets.some(item => item.activeFocus), "Island Options keeps Tab out of edit controls: shown=" + islandOptions.shown + ", focus=" + home.Window.window.activeFocusItem);
|
||||
islandOptions.dismiss();
|
||||
island.expanded = false;
|
||||
home.destroy();
|
||||
|
||||
SettingsData.setDashTabEnabled("media", true);
|
||||
open(0);
|
||||
check(root.dash.activeTabId === "media" && !root.dash.showTabs, "one destination has no navigation");
|
||||
capture("single");
|
||||
SettingsData.setDashTabEnabled("overview", true);
|
||||
SettingsData.setDashTabOrder(["overview", "media"]);
|
||||
open(0);
|
||||
for (const edge of [0, 1, 2, 3, 4, 5, 6, 7]) {
|
||||
SettingsData.animationDuration = edge % 2 === 0 ? 0 : 200;
|
||||
root.dash.setBarContext(edge, 0);
|
||||
settle();
|
||||
const nav = bar();
|
||||
const page = pages();
|
||||
const vertical = edge % 4 >= 2;
|
||||
check(root.dash.showTabs && root.dash.verticalNavigation === vertical, "orientation follows edge " + edge);
|
||||
check(nav.orientation === (vertical ? Qt.Vertical : Qt.Horizontal), "navigation orientation " + edge);
|
||||
check(Math.abs(root.dash.contentLoader.item.width - root.dash.popupWidth) < 1, "zero-motion navigation snaps rendered width " + edge);
|
||||
const point = nav.mapToItem(root.dash.contentLoader.item, 0, 0);
|
||||
const contentPoint = page.mapToItem(root.dash.contentLoader.item, 0, 0);
|
||||
const expectedEdge = [0, 1, 3, 2][edge % 4];
|
||||
check(root.dash.navigationEdge === expectedEdge, "side defaults face away from triggering bar " + edge);
|
||||
switch (expectedEdge) {
|
||||
case 0:
|
||||
check(point.y + nav.height <= contentPoint.y, "top navigation before content");
|
||||
break;
|
||||
case 1:
|
||||
check(point.y >= contentPoint.y + page.height, "bottom navigation after content");
|
||||
break;
|
||||
case 2:
|
||||
check(point.x + nav.width <= contentPoint.x, "left rail before content");
|
||||
break;
|
||||
case 3:
|
||||
check(point.x >= contentPoint.x + page.width, "right rail after content");
|
||||
break;
|
||||
}
|
||||
const size = [root.dash.popupWidth, root.dash.popupHeight];
|
||||
root.dash.editMode = true;
|
||||
settle();
|
||||
check(root.dash.popupWidth === size[0] && root.dash.popupHeight === size[1], "edit preserves panel dimensions " + edge);
|
||||
root.dash.editMode = false;
|
||||
check(Math.abs(page.width - (root.dash.popupWidth - root.dash.navigationWidth - DashMetrics.contentPadding * 2)) < 1, "content width excludes navigation " + edge);
|
||||
if (edge < 4)
|
||||
capture("edge-" + edge);
|
||||
}
|
||||
SettingsData.animationDuration = 0;
|
||||
for (const [position, expectedEdge] of [["left", 2], ["right", 3], ["bottom", 1], ["center", 0]]) {
|
||||
SettingsData.set("dashTabPosition", position);
|
||||
root.dash.setBarContext(0, 0);
|
||||
settle();
|
||||
check(root.dash.navigationEdge === expectedEdge && root.dash.verticalNavigation === (expectedEdge >= 2), "position override " + position);
|
||||
root.dash.setBarContext(3, 0);
|
||||
check(root.dash.navigationEdge === expectedEdge, "override ignores bar position " + position);
|
||||
}
|
||||
SettingsData.set("dashTabPosition", "auto");
|
||||
root.dash.setBarContext(0, 0);
|
||||
SettingsData.resetDashTabs();
|
||||
settle();
|
||||
capture("default-tabs");
|
||||
const embeddedEdit = find(bar(), item => item.revealed !== undefined && item.visible);
|
||||
mouseMove(embeddedEdit, embeddedEdit.width / 2, embeddedEdit.height / 2);
|
||||
tryVerify(() => embeddedEdit.revealed, 1000);
|
||||
capture("hover-edit");
|
||||
mouseClick(embeddedEdit, embeddedEdit.width / 2, embeddedEdit.height / 2);
|
||||
check(root.dash.editMode, "selected icon pencil opens editing");
|
||||
root.dash.editMode = false;
|
||||
SessionData.locale = "ar";
|
||||
root.dash.setBarContext(2, 0);
|
||||
settle();
|
||||
check(I18n.isRtl && root.dash.verticalNavigation, "RTL keeps physical right rail");
|
||||
capture("rtl-right");
|
||||
SessionData.locale = "en";
|
||||
settle();
|
||||
const verticalEdit = find(bar(), item => item.revealed !== undefined && item.visible);
|
||||
const selectedIcon = find(verticalEdit.parent, item => item.filled !== undefined && item.name === "dashboard");
|
||||
check(Math.abs(verticalEdit.x + verticalEdit.width / 2 - selectedIcon.x - selectedIcon.width / 2) < 1 && Math.abs(verticalEdit.y + verticalEdit.height / 2 - selectedIcon.y - selectedIcon.height / 2) < 1, "vertical pencil stays centered on destination icon");
|
||||
mouseMove(verticalEdit, verticalEdit.width / 2, verticalEdit.height / 2);
|
||||
tryVerify(() => verticalEdit.revealed, 1000);
|
||||
capture("hover-vertical");
|
||||
root.dash.setBarContext(0, 0);
|
||||
settle();
|
||||
root.dash.requestTab("overview");
|
||||
root.dash.editMode = true;
|
||||
settle();
|
||||
const clear = find(root.dash.contentLoader.item, item => item.actionId === "clear");
|
||||
const reset = find(root.dash.contentLoader.item, item => item.actionId === "reset");
|
||||
const savedCards = JSON.stringify(SettingsData.dashCards);
|
||||
const horizontalBounds = actionBounds(clear, reset);
|
||||
clear.clicked();
|
||||
settle();
|
||||
check(clear.armed && JSON.stringify(SettingsData.dashCards) === savedCards, "Clear needs confirmation");
|
||||
check(actionBounds(clear, reset) === horizontalBounds, "arming Clear does not move or resize either horizontal action");
|
||||
check(clear.Accessible.name === I18n.tr("Confirm") && clear.color === Theme.error, "armed Clear reads as an emphasized Confirm");
|
||||
capture("confirm-horizontal");
|
||||
reset.clicked();
|
||||
settle();
|
||||
check(actionBounds(clear, reset) === horizontalBounds && !clear.armed && reset.armed, "arming Reset disarms Clear without moving either action: " + horizontalBounds + " vs " + actionBounds(clear, reset));
|
||||
clear.clicked();
|
||||
clear.clicked();
|
||||
check(SettingsData.dashCards.length === 0, "confirmed Clear removes widgets");
|
||||
reset.clicked();
|
||||
check(SettingsData.dashCards.length === 0, "Reset needs confirmation");
|
||||
reset.clicked();
|
||||
check(SettingsData.dashCards.length > 0, "confirmed Reset restores widgets");
|
||||
clear.clicked();
|
||||
root.dash.editMode = false;
|
||||
root.dash.editMode = true;
|
||||
check(!clear.armed, "leaving editing cancels confirmation");
|
||||
settle();
|
||||
const editActions = find(root.dash.contentLoader.item, item => item.overlayParent !== undefined);
|
||||
check(editActions.focusTargets.every(item => !item.visualFocus), "programmatic edit entry has no persistent focus ring");
|
||||
check(root.dash.contentLoader.item.Window.window.activeFocusItem === root.dash.contentLoader.item, "edit entry keeps neutral focus");
|
||||
capture("edit-horizontal");
|
||||
const editViewport = find(editActions, item => item.contentX !== undefined && item.wheelEnabled !== undefined);
|
||||
editActions.width = 280;
|
||||
settle();
|
||||
editActions.focusTargets[editActions.focusTargets.length - 1].forceActiveFocus(Qt.TabFocusReason);
|
||||
check(editViewport.contentX > 0, "overflowed Finish is revealed on keyboard focus");
|
||||
editActions.width = Qt.binding(() => Math.min(editActions.parent.width, editActions.implicitWidth));
|
||||
root.dash.setBarContext(2, 0);
|
||||
settle();
|
||||
capture("edit-vertical");
|
||||
const verticalBounds = actionBounds(clear, reset);
|
||||
reset.clicked();
|
||||
settle();
|
||||
check(actionBounds(clear, reset) === verticalBounds && reset.armed, "arming Reset does not move or resize either vertical action");
|
||||
capture("confirm-vertical");
|
||||
root.dash.editMode = false;
|
||||
root.dash.setBarContext(0, 0);
|
||||
settle();
|
||||
const integratedNav = bar();
|
||||
integratedNav.forceActiveFocus(Qt.TabFocusReason);
|
||||
const start = root.dash.currentTabId;
|
||||
keyClick(Qt.Key_Right);
|
||||
check(integratedNav.activeFocus && root.dash.currentTabId !== start, "first arrow preserves navigation focus");
|
||||
const next = root.dash.currentTabId;
|
||||
keyClick(Qt.Key_Right);
|
||||
check(integratedNav.activeFocus && root.dash.currentTabId !== next, "second arrow continues navigation");
|
||||
root.dash.editMode = true;
|
||||
settle();
|
||||
check(root.dash.contentLoader.item.Window.window.activeFocusItem === root.dash.contentLoader.item, "edit entry releases hidden navigation focus");
|
||||
root.dash.requestTab("weather");
|
||||
root.dash.editMode = true;
|
||||
settle();
|
||||
const weatherActions = find(root.dash.contentLoader.item, item => item.overlayParent !== undefined);
|
||||
keyClick(Qt.Key_Tab);
|
||||
check(weatherActions.focusTargets[0].activeFocus, "Weather edit Tab enters first action");
|
||||
root.dash.contentLoader.item.focusInitial();
|
||||
keyClick(Qt.Key_Backtab);
|
||||
check(weatherActions.focusTargets[weatherActions.focusTargets.length - 1].activeFocus, "Weather edit Backtab enters last action");
|
||||
root.dash.editMode = false;
|
||||
|
||||
PluginService.pluginDashComponents = {
|
||||
navigation_test: pluginComponent
|
||||
};
|
||||
PluginService.loadedPlugins = {
|
||||
navigation_test: {
|
||||
name: "Test",
|
||||
surfaces: ["dash"]
|
||||
}
|
||||
};
|
||||
open("plugin_navigation_test");
|
||||
const actions = find(root.dash.contentLoader.item, item => item.overlayParent !== undefined);
|
||||
check(!actions.visible, "no separate action strip in normal navigation");
|
||||
const nav = bar();
|
||||
check(nav.editable, "selected destination supports embedded editing");
|
||||
nav.editRequested(nav.currentIndex);
|
||||
settle();
|
||||
check(root.dash.editMode && actions.visible, "embedded editing reveals page controls");
|
||||
check(actions.implicitWidth <= actions.parent.width, "edit controls fit the header without scrolling: " + actions.implicitWidth + " > " + actions.parent.width);
|
||||
check(actions.hasCustomActions, "plugin action menu is available");
|
||||
const menu = find(root.dash.contentLoader.item, item => item.visibleItems !== undefined && item.parent === root.dash.contentLoader.item);
|
||||
check(menu.visibleItems.length === 2, "hidden custom action omitted");
|
||||
menu._activate(1);
|
||||
check(root.customActions === 0, "disabled custom action cannot activate");
|
||||
menu._activate(0);
|
||||
check(root.customActions === 1, "custom callback preserved");
|
||||
const lastAction = actions.focusTargets[actions.focusTargets.length - 1];
|
||||
lastAction.forceActiveFocus(Qt.TabFocusReason);
|
||||
keyClick(Qt.Key_Tab);
|
||||
check(root.dash.editMode && root.dash.activeTabId === "plugin_navigation_test", "Tab from Finish stays in editing");
|
||||
const editFocusTargets = actions.focusTargets;
|
||||
root.dash.editMode = false;
|
||||
settle();
|
||||
check(editFocusTargets.every(item => !item.activeFocus), "leaving plugin editing releases hidden action focus");
|
||||
SettingsData.setDashTabEnabled("plugin_navigation_test", false);
|
||||
open("plugin_navigation_test");
|
||||
check(root.dash.detailTabId === "plugin_navigation_test", "hidden plugin opens as detail");
|
||||
PluginService.loadedPlugins = {};
|
||||
settle();
|
||||
check(root.dash.detailTabId === "" && root.dash.activeTabId === DashRegistry.defaultTabId, "removed plugin falls back cleanly");
|
||||
|
||||
root.dash.dashVisible = false;
|
||||
root.dash.instantClose();
|
||||
settle();
|
||||
navigation.width = 800;
|
||||
navigation.orientation = Qt.Horizontal;
|
||||
settle();
|
||||
const first = find(navigation, item => item.index === 0 && item.selected !== undefined);
|
||||
check(Math.abs(first.width * navigation.count + navigation.spacing * (navigation.count - 1) - navigation.width) < 1, "horizontal destinations are evenly distributed");
|
||||
navigation.evenlySpaced = false;
|
||||
settle();
|
||||
navigation.revealCurrent();
|
||||
check(first.width === Theme.navigationItemMinWidth && first.parent.x > 0, "compact horizontal navigation stays centered");
|
||||
navigation.evenlySpaced = true;
|
||||
tester.parent = window.contentItem;
|
||||
navigation.forceActiveFocus(Qt.TabFocusReason);
|
||||
keyClick(Qt.Key_Right);
|
||||
check(navigation.currentIndex === 1, "horizontal arrow selects next destination");
|
||||
navigation.orientation = Qt.Vertical;
|
||||
navigation.width = Theme.navigationRailWidth;
|
||||
navigation.height = 300;
|
||||
settle();
|
||||
navigation.revealCurrent();
|
||||
check(first.height === navigation.destinationHeight && first.parent.y > 0, "vertical destinations stay a centered group");
|
||||
keyClick(Qt.Key_Up);
|
||||
check(navigation.currentIndex === 0, "vertical arrow selects previous destination");
|
||||
navigation.model = Array.from({
|
||||
length: 10
|
||||
}, (_, i) => ({
|
||||
icon: "extension",
|
||||
text: "Destination " + i
|
||||
}));
|
||||
navigation.currentIndex = 9;
|
||||
settle();
|
||||
const scroll = find(navigation, item => item.contentY !== undefined && item.flickableDirection !== undefined);
|
||||
check(scroll.contentY > 0, "overflow selection scrolls into view");
|
||||
navigation.model = [
|
||||
{
|
||||
icon: "extension",
|
||||
text: "A long translated destination label"
|
||||
}
|
||||
];
|
||||
navigation.currentIndex = 0;
|
||||
settle();
|
||||
const longDestination = find(navigation, item => item.index === 0 && item.selected !== undefined);
|
||||
check(longDestination.width === Theme.navigationRailWidth && longDestination.height > Theme.navigationHeight, "rail wraps long labels without widening");
|
||||
navigation.orientation = Qt.Horizontal;
|
||||
navigation.width = 240;
|
||||
navigation.model = Array.from({
|
||||
length: 10
|
||||
}, (_, i) => ({
|
||||
icon: "extension",
|
||||
text: "Destination " + i
|
||||
}));
|
||||
navigation.currentIndex = 9;
|
||||
settle();
|
||||
check(scroll.contentX > 0, "horizontal overflow selection scrolls into view");
|
||||
SessionData.locale = "ar";
|
||||
navigation.currentIndex = 0;
|
||||
settle();
|
||||
navigation.forceActiveFocus(Qt.OtherFocusReason);
|
||||
keyClick(Qt.Key_Left);
|
||||
check(navigation.currentIndex === 1, "RTL Left selects the next logical destination");
|
||||
SessionData.locale = "en";
|
||||
navigation.orientation = Qt.Vertical;
|
||||
navigation.width = Theme.navigationRailWidth;
|
||||
settle();
|
||||
open("overview");
|
||||
for (const strength of [0, 50, 100]) {
|
||||
SettingsData.set("radiusStrength", strength);
|
||||
settle();
|
||||
const indicator = find(navigation, item => item.width === Theme.navigationIndicatorWidth && item.height === Theme.navigationIndicatorHeight && item.radius !== undefined);
|
||||
check(indicator && indicator.radius === (strength === 0 ? 0 : Theme.navigationIndicatorHeight / 2), "indicator radius at " + strength);
|
||||
capture("radius-" + strength);
|
||||
}
|
||||
SettingsData.set("radiusMode", "fixed");
|
||||
SettingsData.set("fixedRadius", 7);
|
||||
settle();
|
||||
const indicator = find(navigation, item => item.width === Theme.navigationIndicatorWidth && item.height === Theme.navigationIndicatorHeight && item.radius !== undefined);
|
||||
check(indicator.radius === 7, "fixed radius applies to navigation");
|
||||
capture("fixed-radius");
|
||||
console.log("FIXTURE_PASS dashboard navigation and actions");
|
||||
} catch (error) {
|
||||
console.error("FIXTURE_FAIL " + error.message);
|
||||
}
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Quickshell.watchFiles = false;
|
||||
DC.Style.theme = Theme;
|
||||
DC.Style.settings = SettingsData;
|
||||
DC.I18n.backend = I18n;
|
||||
SettingsData.animationDuration = 0;
|
||||
SettingsData.reduceMotion = true;
|
||||
Qt.callLater(tester.run);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ def main():
|
||||
parser.error("--jobs must be positive")
|
||||
selected = args.suites or ["widgets", "media", "qt", "logic"]
|
||||
commands = {
|
||||
"widgets": [sys.executable, "quickshell/tests/run-surface-fixture.py", "--suite", "quickshell/tests/qml/bar-content.qml", "quickshell/tests/qml/frame-bar-flip.qml", "quickshell/tests/qml/instance-routing.qml", "quickshell/tests/qml/font-weights.qml", "quickshell/tests/qml/theme-selected-container.qml", "quickshell/tests/qml/theme-accents.qml", "quickshell/tests/qml/clipboard-preview.qml", "quickshell/tests/qml/launcher-plugin-instances.qml", "quickshell/tests/qml/island-launcher-focus.qml", "quickshell/tests/qml/workspace-switcher.qml", "quickshell/tests/qml/focused-app.qml", "quickshell/tests/qml/slider-wheel.qml", "quickshell/tests/qml/control-center-sizes.qml", "quickshell/tests/qml/grid-edit-layout.qml"],
|
||||
"widgets": [sys.executable, "quickshell/tests/run-surface-fixture.py", "--suite", "quickshell/tests/qml/bar-content.qml", "quickshell/tests/qml/frame-bar-flip.qml", "quickshell/tests/qml/instance-routing.qml", "quickshell/tests/qml/font-weights.qml", "quickshell/tests/qml/theme-selected-container.qml", "quickshell/tests/qml/theme-accents.qml", "quickshell/tests/qml/clipboard-preview.qml", "quickshell/tests/qml/launcher-plugin-instances.qml", "quickshell/tests/qml/island-launcher-focus.qml", "quickshell/tests/qml/workspace-switcher.qml", "quickshell/tests/qml/focused-app.qml", "quickshell/tests/qml/slider-wheel.qml", "quickshell/tests/qml/control-center-sizes.qml", "quickshell/tests/qml/dash-navigation.qml", "quickshell/tests/qml/grid-edit-layout.qml"],
|
||||
"media": [sys.executable, "quickshell/tests/run-surface-fixture.py", "--suite", "quickshell/tests/qml/media-presentation.qml", "quickshell/tests/qml/media-playback.qml", "quickshell/tests/qml/media-artwork.qml", "quickshell/tests/qml/media-lyrics.qml", "--mpris", "--artwork"],
|
||||
"logic": ["node", "--test", *sorted(str(path.relative_to(repo)) for path in (repo / "quickshell/tests").glob("*.test.mjs"))],
|
||||
}
|
||||
|
||||
@@ -11999,6 +11999,20 @@
|
||||
"icon": "space_dashboard",
|
||||
"description": "Tabs and weather"
|
||||
},
|
||||
{
|
||||
"section": "dashTabsEvenlySpaced",
|
||||
"label": "Evenly space tabs",
|
||||
"tabIndex": 43,
|
||||
"category": "Dashboard",
|
||||
"keywords": [
|
||||
"dashboard",
|
||||
"evenly",
|
||||
"navigation",
|
||||
"space",
|
||||
"spacing",
|
||||
"tabs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "dashMediaSettings",
|
||||
"label": "Media player",
|
||||
@@ -12015,6 +12029,18 @@
|
||||
],
|
||||
"icon": "music_note"
|
||||
},
|
||||
{
|
||||
"section": "dashTabPosition",
|
||||
"label": "Position",
|
||||
"tabIndex": 43,
|
||||
"category": "Dashboard",
|
||||
"keywords": [
|
||||
"dashboard",
|
||||
"navigation",
|
||||
"position",
|
||||
"tabs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": "dashTabs",
|
||||
"label": "Tabs",
|
||||
@@ -12022,6 +12048,8 @@
|
||||
"category": "Dashboard",
|
||||
"keywords": [
|
||||
"dashboard",
|
||||
"navigation",
|
||||
"position",
|
||||
"tabs"
|
||||
],
|
||||
"icon": "refresh"
|
||||
|
||||
Reference in new issue
Block a user