1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-09 23:15:38 -05:00

Merge branch 'master' of github.com:bbedward/dank-material-dark-shell

This commit is contained in:
bbedward
2025-08-08 12:28:17 -04:00
35 changed files with 18626 additions and 1456 deletions

View File

@@ -9,7 +9,6 @@ import qs.Services
import qs.Common import qs.Common
Singleton { Singleton {
id: root id: root
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation) readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
@@ -23,8 +22,7 @@ Singleton {
property bool qtThemingEnabled: false property bool qtThemingEnabled: false
property bool systemThemeGenerationInProgress: false property bool systemThemeGenerationInProgress: false
property string matugenJson: "" property string matugenJson: ""
property var matugenColors: ({ property var matugenColors: ({})
})
property bool extractionRequested: false property bool extractionRequested: false
property int colorUpdateTrigger: 0 property int colorUpdateTrigger: 0
property string lastWallpaperTimestamp: "" property string lastWallpaperTimestamp: ""
@@ -48,13 +46,13 @@ Singleton {
property color accentHi: primary property color accentHi: primary
property color accentLo: secondary property color accentLo: secondary
signal colorsUpdated() signal colorsUpdated
function onLightModeChanged() { function onLightModeChanged() {
if (matugenColors && Object.keys(matugenColors).length > 0) { if (matugenColors && Object.keys(matugenColors).length > 0) {
colorUpdateTrigger++; colorUpdateTrigger++;
colorsUpdated(); colorsUpdated();
if (typeof Theme !== "undefined" && Theme.isDynamicTheme) { if (typeof Theme !== "undefined" && Theme.isDynamicTheme) {
generateSystemThemes(); generateSystemThemes();
} }
@@ -98,12 +96,12 @@ Singleton {
id: matugenCheck id: matugenCheck
command: ["which", "matugen"] command: ["which", "matugen"]
onExited: (code) => { onExited: code => {
matugenAvailable = (code === 0); matugenAvailable = (code === 0);
if (!matugenAvailable) { if (!matugenAvailable) {
ToastService.wallpaperErrorStatus = "matugen_missing"; ToastService.wallpaperErrorStatus = "matugen_missing";
ToastService.showWarning("matugen not found - dynamic theming disabled"); ToastService.showWarning("matugen not found - dynamic theming disabled");
return ; return;
} }
if (extractionRequested) { if (extractionRequested) {
fileChecker.running = true; fileChecker.running = true;
@@ -115,7 +113,7 @@ Singleton {
id: fileChecker id: fileChecker
command: ["test", "-r", wallpaperPath] command: ["test", "-r", wallpaperPath]
onExited: (code) => { onExited: code => {
if (code === 0) { if (code === 0) {
matugenProcess.running = true; matugenProcess.running = true;
} else { } else {
@@ -138,7 +136,7 @@ Singleton {
if (!out.length) { if (!out.length) {
ToastService.wallpaperErrorStatus = "error"; ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper Processing Failed"); ToastService.showError("Wallpaper Processing Failed");
return ; return;
} }
try { try {
root.matugenJson = out; root.matugenJson = out;
@@ -156,7 +154,6 @@ Singleton {
stderr: StdioCollector { stderr: StdioCollector {
id: matugenErr id: matugenErr
} }
} }
function generateAppConfigs() { function generateAppConfigs() {
@@ -166,18 +163,18 @@ Singleton {
generateNiriConfig(); generateNiriConfig();
generateGhosttyConfig(); generateGhosttyConfig();
if (gtkThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) { if (gtkThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) {
generateGtkThemes(); generateSystemThemes();
} } else if (qtThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) {
if (qtThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) { generateSystemThemes();
generateQtThemes();
} }
} }
function generateNiriConfig() { function generateNiriConfig() {
var dark = matugenColors.colors.dark; var dark = matugenColors.colors.dark;
if (!dark) return; if (!dark)
return;
var bg = dark.background || "#1a1c1e"; var bg = dark.background || "#1a1c1e";
var primary = dark.primary || "#42a5f5"; var primary = dark.primary || "#42a5f5";
@@ -201,7 +198,8 @@ Singleton {
function generateGhosttyConfig() { function generateGhosttyConfig() {
var dark = matugenColors.colors.dark; var dark = matugenColors.colors.dark;
var light = matugenColors.colors.light; var light = matugenColors.colors.light;
if (!dark || !light) return; if (!dark || !light)
return;
var bg = dark.background || "#1a1c1e"; var bg = dark.background || "#1a1c1e";
var fg = dark.on_background || "#e3e8ef"; var fg = dark.on_background || "#e3e8ef";
@@ -245,116 +243,108 @@ palette = 15=${fg_b}`;
var ghosttyConfigDir = configDir + "/ghostty"; var ghosttyConfigDir = configDir + "/ghostty";
var ghosttyConfigPath = ghosttyConfigDir + "/config-dankcolors"; var ghosttyConfigPath = ghosttyConfigDir + "/config-dankcolors";
Quickshell.execDetached(["bash", "-c", `mkdir -p '${ghosttyConfigDir}' && echo '${content}' > '${ghosttyConfigPath}'`]); Quickshell.execDetached(["bash", "-c", `mkdir -p '${ghosttyConfigDir}' && echo '${content}' > '${ghosttyConfigPath}'`]);
} }
function checkGtkThemingAvailability() { function checkGtkThemingAvailability() {
gtkAvailabilityChecker.running = true; gtkAvailabilityChecker.running = true;
} }
function checkQtThemingAvailability() { function checkQtThemingAvailability() {
qtAvailabilityChecker.running = true; qtAvailabilityChecker.running = true;
} }
function generateSystemThemes() { function generateSystemThemes() {
if (systemThemeGenerationInProgress) { if (systemThemeGenerationInProgress) {
return; return;
} }
if (!matugenAvailable) { if (!matugenAvailable) {
return; return;
} }
if (!wallpaperPath || wallpaperPath === "") { if (!wallpaperPath || wallpaperPath === "") {
return; return;
} }
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"; const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"; const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false"; const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false";
const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false"; const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false";
systemThemeGenerationInProgress = true; systemThemeGenerationInProgress = true;
systemThemeGenerator.command = [shellDir + "/generate-themes.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme, gtkTheming, qtTheming]; systemThemeGenerator.command = [shellDir + "/generate-themes.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme, gtkTheming, qtTheming];
systemThemeGenerator.running = true; systemThemeGenerator.running = true;
} }
function generateGtkThemes() {
generateSystemThemes();
}
function generateQtThemes() {
generateSystemThemes();
}
function restoreSystemThemes() { function restoreSystemThemes() {
const shellDir = root.shellDir; const shellDir = root.shellDir;
if (!shellDir) { if (!shellDir) {
return; return;
} }
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false"; const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default"; const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false"; const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false";
const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false"; const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false";
systemThemeRestoreProcess.command = [shellDir + "/generate-themes.sh", "", shellDir, configDir, "restore", isLight, iconTheme, gtkTheming, qtTheming]; systemThemeRestoreProcess.command = [shellDir + "/generate-themes.sh", "", shellDir, configDir, "restore", isLight, iconTheme, gtkTheming, qtTheming];
systemThemeRestoreProcess.running = true; systemThemeRestoreProcess.running = true;
} }
Process { Process {
id: gtkAvailabilityChecker id: gtkAvailabilityChecker
command: ["bash", "-c", "command -v gsettings >/dev/null && [ -d " + configDir + "/gtk-3.0 -o -d " + configDir + "/gtk-4.0 ]"] command: ["bash", "-c", "command -v gsettings >/dev/null && [ -d " + configDir + "/gtk-3.0 -o -d " + configDir + "/gtk-4.0 ]"]
running: false running: false
onExited: (exitCode) => { onExited: exitCode => {
gtkThemingEnabled = (exitCode === 0); gtkThemingEnabled = (exitCode === 0);
} }
} }
Process { Process {
id: qtAvailabilityChecker id: qtAvailabilityChecker
command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"] command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"]
running: false running: false
onExited: (exitCode) => { onExited: exitCode => {
qtThemingEnabled = (exitCode === 0); qtThemingEnabled = (exitCode === 0);
} }
} }
Process { Process {
id: systemThemeGenerator id: systemThemeGenerator
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {
id: systemThemeStdout id: systemThemeStdout
} }
stderr: StdioCollector { stderr: StdioCollector {
id: systemThemeStderr id: systemThemeStderr
} }
onExited: (exitCode) => { onExited: exitCode => {
systemThemeGenerationInProgress = false; systemThemeGenerationInProgress = false;
if (exitCode !== 0) { if (exitCode !== 0) {
ToastService.showError("Failed to generate system themes: " + systemThemeStderr.text); ToastService.showError("Failed to generate system themes: " + systemThemeStderr.text);
} }
} }
} }
Process { Process {
id: systemThemeRestoreProcess id: systemThemeRestoreProcess
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {
id: restoreThemeStdout id: restoreThemeStdout
} }
stderr: StdioCollector { stderr: StdioCollector {
id: restoreThemeStderr id: restoreThemeStderr
} }
onExited: (exitCode) => { onExited: exitCode => {
if (exitCode === 0) { if (exitCode === 0) {
ToastService.showInfo("System themes restored to default"); ToastService.showInfo("System themes restored to default");
} else { } else {
@@ -362,6 +352,4 @@ palette = 15=${fg_b}`;
} }
} }
} }
} }

View File

@@ -429,7 +429,7 @@ DankModal {
border.width: 1 border.width: 1
clip: true clip: true
ListView { DankListView {
id: clipboardListView id: clipboardListView
anchors.fill: parent anchors.fill: parent

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
DankModal { DankModal {
@@ -14,25 +15,20 @@ DankModal {
property string powerConfirmMessage: "" property string powerConfirmMessage: ""
function executePowerAction(action) { function executePowerAction(action) {
let command = [];
switch (action) { switch (action) {
case "logout": case "logout":
command = ["niri", "msg", "action", "quit", "-s"]; NiriService.quit();
break; break;
case "suspend": case "suspend":
command = ["systemctl", "suspend"]; Quickshell.execDetached(["systemctl", "suspend"]);
break; break;
case "reboot": case "reboot":
command = ["systemctl", "reboot"]; Quickshell.execDetached(["systemctl", "reboot"]);
break; break;
case "poweroff": case "poweroff":
command = ["systemctl", "poweroff"]; Quickshell.execDetached(["systemctl", "poweroff"]);
break; break;
} }
if (command.length > 0) {
Quickshell.execDetached(command);
}
} }
visible: powerConfirmVisible visible: powerConfirmVisible

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modules.AppDrawer import qs.Modules.AppDrawer
import qs.Services import qs.Services
@@ -284,17 +285,49 @@ DankModal {
DankListView { DankListView {
id: resultsList id: resultsList
mouseWheelSpeed: 20
property int itemHeight: 60
property int iconSize: 40
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
signal keyboardNavigationReset()
signal itemClicked(int index, var modelData)
signal itemHovered(int index)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return;
var itemY = index * (itemHeight + itemSpacing);
var itemBottom = itemY + itemHeight;
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height)
contentY = itemBottom - height;
}
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "list" visible: appLauncher.viewMode === "list"
model: appLauncher.model model: appLauncher.model
currentIndex: appLauncher.selectedIndex currentIndex: appLauncher.selectedIndex
itemHeight: 60 clip: true
iconSize: 40 spacing: itemSpacing
showDescription: true focus: true
hoverUpdatesSelection: false interactive: true
keyboardNavigationActive: appLauncher.keyboardNavigationActive cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: function(index, modelData) { onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData); appLauncher.launchApp(modelData);
} }
@@ -307,24 +340,170 @@ DankModal {
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false; appLauncher.keyboardNavigationActive = false;
} }
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AlwaysOn
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: ListView.view.width
height: resultsList.itemHeight
radius: Theme.cornerRadiusLarge
color: ListView.isCurrentItem ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
border.width: ListView.isCurrentItem ? 2 : 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: resultsList.iconSize
height: resultsList.iconSize
anchors.verticalCenter: parent.verticalCenter
IconImage {
id: iconImg
anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadiusLarge
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: resultsList.iconSize * 0.4
color: Theme.primary
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - resultsList.iconSize - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: model.name || ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: model.comment || "Application"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideRight
visible: resultsList.showDescription && model.comment && model.comment.length > 0
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (resultsList.hoverUpdatesSelection && !resultsList.keyboardNavigationActive)
resultsList.currentIndex = index;
resultsList.itemHovered(index);
}
onPositionChanged: {
resultsList.keyboardNavigationReset();
}
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
resultsList.itemClicked(index, model);
} else if (mouse.button === Qt.RightButton) {
var globalPos = mapToGlobal(mouse.x, mouse.y);
resultsList.itemRightClicked(index, model, globalPos.x, globalPos.y);
}
}
}
}
} }
DankGridView { DankGridView {
id: resultsGrid id: resultsGrid
property int currentIndex: appLauncher.selectedIndex
property int columns: 4
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
property int cellPadding: 8
property real iconSizeRatio: 0.55
property int maxIconSize: 48
property int minIconSize: 32
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset()
signal itemClicked(int index, var modelData)
signal itemHovered(int index)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return;
var itemY = Math.floor(index / actualColumns) * cellHeight;
var itemBottom = itemY + cellHeight;
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height)
contentY = itemBottom - height;
}
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "grid" visible: appLauncher.viewMode === "grid"
model: appLauncher.model model: appLauncher.model
columns: 4 clip: true
adaptiveColumns: false cellWidth: baseCellWidth
minCellWidth: 120 cellHeight: baseCellHeight
maxCellWidth: 160 leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
iconSizeRatio: 0.55 rightMargin: leftMargin
maxIconSize: 48 focus: true
currentIndex: appLauncher.selectedIndex interactive: true
hoverUpdatesSelection: false cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
keyboardNavigationActive: appLauncher.keyboardNavigationActive reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: function(index, modelData) { onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData); appLauncher.launchApp(modelData);
} }
@@ -337,6 +516,103 @@ DankModal {
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false; appLauncher.keyboardNavigationActive = false;
} }
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: resultsGrid.cellWidth - resultsGrid.cellPadding
height: resultsGrid.cellHeight - resultsGrid.cellPadding
radius: Theme.cornerRadiusLarge
color: resultsGrid.currentIndex === index ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: resultsGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
border.width: resultsGrid.currentIndex === index ? 2 : 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Item {
property int iconSize: Math.min(resultsGrid.maxIconSize, Math.max(resultsGrid.minIconSize, resultsGrid.cellWidth * resultsGrid.iconSizeRatio))
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
IconImage {
id: iconImg
anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadiusLarge
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: Math.min(28, parent.width * 0.5)
color: Theme.primary
font.weight: Font.Bold
}
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
width: resultsGrid.cellWidth - 12
text: model.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (resultsGrid.hoverUpdatesSelection && !resultsGrid.keyboardNavigationActive)
resultsGrid.currentIndex = index;
resultsGrid.itemHovered(index);
}
onPositionChanged: {
resultsGrid.keyboardNavigationReset();
}
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
resultsGrid.itemClicked(index, model);
} else if (mouse.button === Qt.RightButton) {
var globalPos = mapToGlobal(mouse.x, mouse.y);
resultsGrid.itemRightClicked(index, model, globalPos.x, globalPos.y);
}
}
}
}
} }
} }

View File

@@ -370,17 +370,49 @@ PanelWindow {
DankListView { DankListView {
id: appList id: appList
mouseWheelSpeed: 20
property int itemHeight: 72
property int iconSize: 56
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
signal keyboardNavigationReset()
signal itemClicked(int index, var modelData)
signal itemHovered(int index)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return;
var itemY = index * (itemHeight + itemSpacing);
var itemBottom = itemY + itemHeight;
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height)
contentY = itemBottom - height;
}
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "list" visible: appLauncher.viewMode === "list"
model: appLauncher.model model: appLauncher.model
currentIndex: appLauncher.selectedIndex currentIndex: appLauncher.selectedIndex
itemHeight: 72 clip: true
iconSize: 56 spacing: itemSpacing
showDescription: true focus: true
hoverUpdatesSelection: false interactive: true
keyboardNavigationActive: appLauncher.keyboardNavigationActive cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: function(index, modelData) { onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData); appLauncher.launchApp(modelData);
} }
@@ -393,20 +425,170 @@ PanelWindow {
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false; appLauncher.keyboardNavigationActive = false;
} }
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AlwaysOn
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: ListView.view.width
height: appList.itemHeight
radius: Theme.cornerRadiusLarge
color: ListView.isCurrentItem ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium
border.width: ListView.isCurrentItem ? 2 : 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingL
Item {
width: appList.iconSize
height: appList.iconSize
anchors.verticalCenter: parent.verticalCenter
IconImage {
id: iconImg
anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadiusLarge
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: appList.iconSize * 0.4
color: Theme.primary
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - appList.iconSize - Theme.spacingL
spacing: Theme.spacingXS
StyledText {
width: parent.width
text: model.name || ""
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: model.comment || "Application"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideRight
visible: appList.showDescription && model.comment && model.comment.length > 0
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (appList.hoverUpdatesSelection && !appList.keyboardNavigationActive)
appList.currentIndex = index;
appList.itemHovered(index);
}
onPositionChanged: {
appList.keyboardNavigationReset();
}
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
appList.itemClicked(index, model);
} else if (mouse.button === Qt.RightButton) {
var globalPos = mapToGlobal(mouse.x, mouse.y);
appList.itemRightClicked(index, model, globalPos.x, globalPos.y);
}
}
}
}
} }
DankGridView { DankGridView {
id: appGrid id: appGrid
property int currentIndex: appLauncher.selectedIndex
property int columns: 4
property bool adaptiveColumns: false
property int minCellWidth: 120
property int maxCellWidth: 160
property int cellPadding: 8
property real iconSizeRatio: 0.6
property int maxIconSize: 56
property int minIconSize: 32
property bool hoverUpdatesSelection: false
property bool keyboardNavigationActive: appLauncher.keyboardNavigationActive
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset()
signal itemClicked(int index, var modelData)
signal itemHovered(int index)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= count)
return;
var itemY = Math.floor(index / actualColumns) * cellHeight;
var itemBottom = itemY + cellHeight;
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height)
contentY = itemBottom - height;
}
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
visible: appLauncher.viewMode === "grid" visible: appLauncher.viewMode === "grid"
model: appLauncher.model model: appLauncher.model
columns: 4 clip: true
adaptiveColumns: false cellWidth: baseCellWidth
currentIndex: appLauncher.selectedIndex cellHeight: baseCellHeight
hoverUpdatesSelection: false leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
keyboardNavigationActive: appLauncher.keyboardNavigationActive rightMargin: leftMargin
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: function(index, modelData) { onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData); appLauncher.launchApp(modelData);
} }
@@ -419,6 +601,103 @@ PanelWindow {
onKeyboardNavigationReset: { onKeyboardNavigationReset: {
appLauncher.keyboardNavigationActive = false; appLauncher.keyboardNavigationActive = false;
} }
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: appGrid.cellWidth - appGrid.cellPadding
height: appGrid.cellHeight - appGrid.cellPadding
radius: Theme.cornerRadiusLarge
color: appGrid.currentIndex === index ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: appGrid.currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
border.width: appGrid.currentIndex === index ? 2 : 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Item {
property int iconSize: Math.min(appGrid.maxIconSize, Math.max(appGrid.minIconSize, appGrid.cellWidth * appGrid.iconSizeRatio))
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
IconImage {
id: iconImg
anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadiusLarge
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: Math.min(28, parent.width * 0.5)
color: Theme.primary
font.weight: Font.Bold
}
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
width: appGrid.cellWidth - 12
text: model.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (appGrid.hoverUpdatesSelection && !appGrid.keyboardNavigationActive)
appGrid.currentIndex = index;
appGrid.itemHovered(index);
}
onPositionChanged: {
appGrid.keyboardNavigationReset();
}
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
appGrid.itemClicked(index, model);
} else if (mouse.button === Qt.RightButton) {
var globalPos = mapToGlobal(mouse.x, mouse.y);
appGrid.itemRightClicked(index, model, globalPos.x, globalPos.y);
}
}
}
}
} }
} }

View File

@@ -101,7 +101,7 @@ Rectangle {
} }
ListView { DankListView {
id: eventsList id: eventsList
anchors.top: headerRow.bottom anchors.top: headerRow.bottom

View File

@@ -33,46 +33,90 @@ Item {
} }
} }
ScrollView { // Output Tab - DankFlickable
DankFlickable {
width: parent.width width: parent.width
height: parent.height - 48 height: parent.height - 48
visible: audioTab.audioSubTab === 0 visible: audioTab.audioSubTab === 0
clip: true clip: true
contentHeight: outputColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column { Column {
id: outputColumn
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
VolumeControl { Loader {
width: parent.width
sourceComponent: volumeComponent
} }
AudioDevicesList { Loader {
width: parent.width
sourceComponent: outputDevicesComponent
} }
} }
} }
ScrollView { // Input Tab - DankFlickable
DankFlickable {
width: parent.width width: parent.width
height: parent.height - 48 height: parent.height - 48
visible: audioTab.audioSubTab === 1 visible: audioTab.audioSubTab === 1
clip: true clip: true
contentHeight: inputColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column { Column {
id: inputColumn
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
MicrophoneControl { Loader {
width: parent.width
sourceComponent: microphoneComponent
} }
AudioInputDevicesList { Loader {
width: parent.width
sourceComponent: inputDevicesComponent
} }
} }
} }
} }
} // Volume Control Component
Component {
id: volumeComponent
VolumeControl {
width: parent.width
}
}
// Microphone Control Component
Component {
id: microphoneComponent
MicrophoneControl {
width: parent.width
}
}
// Output Devices Component
Component {
id: outputDevicesComponent
AudioDevicesList {
width: parent.width
}
}
// Input Devices Component
Component {
id: inputDevicesComponent
AudioInputDevicesList {
width: parent.width
}
}
}

View File

@@ -11,7 +11,15 @@ import qs.Widgets
Column { Column {
id: root id: root
property var bluetoothContextMenuWindow function findBluetoothContextMenu() {
var p = parent;
while (p) {
if (p.bluetoothContextMenuWindow)
return p.bluetoothContextMenuWindow;
p = p.parent;
}
return null;
}
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -117,10 +125,11 @@ Column {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (bluetoothContextMenuWindow) { var contextMenu = root.findBluetoothContextMenu();
bluetoothContextMenuWindow.deviceData = modelData; if (contextMenu) {
let localPos = btMenuButtonArea.mapToItem(bluetoothContextMenuWindow.parentItem, btMenuButtonArea.width / 2, btMenuButtonArea.height); contextMenu.deviceData = modelData;
bluetoothContextMenuWindow.show(localPos.x, localPos.y); let localPos = btMenuButtonArea.mapToItem(contextMenu.parentItem, btMenuButtonArea.width / 2, btMenuButtonArea.height);
contextMenu.show(localPos.x, localPos.y);
} }
} }
} }

View File

@@ -12,33 +12,39 @@ import qs.Widgets
Item { Item {
id: bluetoothTab id: bluetoothTab
ScrollView { property alias bluetoothContextMenuWindow: bluetoothContextMenuWindow
DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded contentHeight: mainColumn.height
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff contentWidth: width
mouseWheelSpeed: 20
Column { Column {
id: mainColumn
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
BluetoothToggle { Loader {
width: parent.width
sourceComponent: toggleComponent
} }
PairedDevicesList { Loader {
bluetoothContextMenuWindow: bluetoothContextMenuWindow width: parent.width
sourceComponent: pairedComponent
} }
AvailableDevicesList { Loader {
width: parent.width
sourceComponent: availableComponent
} }
} }
} }
BluetoothContextMenu { BluetoothContextMenu {
id: bluetoothContextMenuWindow id: bluetoothContextMenuWindow
parentItem: bluetoothTab parentItem: bluetoothTab
} }
@@ -57,7 +63,26 @@ Item {
onClicked: { onClicked: {
} }
} }
} }
} Component {
id: toggleComponent
BluetoothToggle {
width: parent.width
}
}
Component {
id: pairedComponent
PairedDevicesList {
width: parent.width
}
}
Component {
id: availableComponent
AvailableDevicesList {
width: parent.width
}
}
}

View File

@@ -8,7 +8,7 @@ import qs.Modules
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
ScrollView { Item {
id: displayTab id: displayTab
property var brightnessDebounceTimer property var brightnessDebounceTimer
@@ -16,19 +16,61 @@ ScrollView {
brightnessDebounceTimer: Timer { brightnessDebounceTimer: Timer {
property int pendingValue: 0 property int pendingValue: 0
interval: BrightnessService.ddcAvailable ? 500 : 50 // 500ms for slow DDC (i2c), 50ms for fast laptop backlight interval: BrightnessService.ddcAvailable ? 500 : 50
repeat: false repeat: false
onTriggered: { onTriggered: {
BrightnessService.setBrightnessInternal(pendingValue); BrightnessService.setBrightnessInternal(pendingValue);
} }
} }
clip: true DankFlickable {
Column { anchors.fill: parent
width: parent.width clip: true
spacing: Theme.spacingL contentHeight: mainColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingL
Loader {
width: parent.width
sourceComponent: brightnessComponent
}
Loader {
width: parent.width
sourceComponent: settingsComponent
}
}
}
Process {
id: nightModeEnableProcess
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
SettingsData.setNightModeEnabled(false);
}
}
}
Process {
id: nightModeDisableProcess
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
}
}
}
Component {
id: brightnessComponent
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -48,12 +90,10 @@ ScrollView {
rightIcon: "brightness_high" rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable enabled: BrightnessService.brightnessAvailable
onSliderValueChanged: function(newValue) { onSliderValueChanged: function(newValue) {
brightnessDebounceTimer.pendingValue = newValue; brightnessDebounceTimer.pendingValue = newValue;
brightnessDebounceTimer.restart(); brightnessDebounceTimer.restart();
} }
onSliderDragFinished: function(finalValue) { onSliderDragFinished: function(finalValue) {
brightnessDebounceTimer.stop(); brightnessDebounceTimer.stop();
BrightnessService.setBrightnessInternal(finalValue); BrightnessService.setBrightnessInternal(finalValue);
} }
@@ -66,9 +106,11 @@ ScrollView {
visible: BrightnessService.ddcAvailable && !BrightnessService.laptopBacklightAvailable visible: BrightnessService.ddcAvailable && !BrightnessService.laptopBacklightAvailable
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
}
Component {
id: settingsComponent
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -110,12 +152,10 @@ ScrollView {
font.weight: Font.Medium font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
MouseArea { MouseArea {
id: nightModeToggle id: nightModeToggle
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@@ -129,7 +169,6 @@ ScrollView {
} }
} }
} }
} }
Rectangle { Rectangle {
@@ -158,12 +197,10 @@ ScrollView {
font.weight: Font.Medium font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
} }
MouseArea { MouseArea {
id: lightModeToggle id: lightModeToggle
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@@ -177,40 +214,9 @@ ScrollView {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
}
}
}
Process {
id: nightModeEnableProcess
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
SettingsData.setNightModeEnabled(false);
} }
} }
} }
}
Process {
id: nightModeDisableProcess
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0)
}
}
}

View File

@@ -928,7 +928,7 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
logoutDialog.close() logoutDialog.close()
Quickshell.execDetached(["niri", "msg", "action", "quit", "-s"]) NiriService.quit()
} }
} }
} }

View File

@@ -2,144 +2,27 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
ListView { DankListView {
id: root id: root
property alias count: root.count property alias count: root.count
readonly property real listContentHeight: root.contentHeight property alias listContentHeight: root.contentHeight
readonly property bool atYBeginning: root.contentY === 0
property real stableY: 0
property bool isUserScrolling: false
width: parent.width width: parent.width
height: parent.height height: parent.height
clip: true clip: true
model: NotificationService.groupedNotifications model: NotificationService.groupedNotifications
spacing: Theme.spacingL spacing: Theme.spacingL
interactive: true
boundsBehavior: Flickable.StopAtBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
cacheBuffer: 1000
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
property real momentum: 0
onWheel: (event) => {
if (event.pixelDelta.y !== 0) {
// Touchpad with pixel delta
momentum = event.pixelDelta.y * 1.8
} else {
// Mouse wheel with angle delta
momentum = (event.angleDelta.y / 120) * (parent.spacing * 2.5) // ~2.5 items per wheel step
}
let newY = parent.contentY - momentum
newY = Math.max(0, Math.min(parent.contentHeight - parent.height, newY))
parent.contentY = newY
momentum *= 0.92 // Decay for smooth momentum
event.accepted = true
}
}
onMovementStarted: isUserScrolling = true
onMovementEnded: {
isUserScrolling = false;
if (contentY > 40)
stableY = contentY;
}
onContentYChanged: {
if (!isUserScrolling && visible && parent.visible && stableY > 40 && Math.abs(contentY - stableY) > 10)
contentY = stableY;
}
NotificationEmptyState { NotificationEmptyState {
visible: root.count === 0 visible: root.count === 0
anchors.centerIn: parent anchors.centerIn: parent
} }
add: Transition {
enabled: !root.isUserScrolling
ParallelAnimation {
NumberAnimation {
properties: "opacity"
from: 0
to: 1
duration: root.isUserScrolling ? 0 : Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
properties: "height"
from: 0
duration: root.isUserScrolling ? 0 : Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
remove: Transition {
SequentialAnimation {
PauseAnimation {
duration: 50
}
ParallelAnimation {
NumberAnimation {
properties: "opacity"
to: 0
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
properties: "height,anchors.topMargin,anchors.bottomMargin"
to: 0
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
displaced: Transition {
enabled: false
NumberAnimation {
properties: "y"
duration: 0
}
}
move: Transition {
enabled: false
NumberAnimation {
properties: "y"
duration: 0
easing.type: Theme.emphasizedEasing
}
}
delegate: NotificationCard { delegate: NotificationCard {
notificationGroup: modelData notificationGroup: modelData
} }
} }

View File

@@ -49,9 +49,7 @@ Column {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("name"); SysMonitorService.setSortBy("name");
processListView.restoreAnchor();
} }
} }
@@ -90,9 +88,7 @@ Column {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("cpu"); SysMonitorService.setSortBy("cpu");
processListView.restoreAnchor();
} }
} }
@@ -131,9 +127,7 @@ Column {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("memory"); SysMonitorService.setSortBy("memory");
processListView.restoreAnchor();
} }
} }
@@ -173,9 +167,7 @@ Column {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("pid"); SysMonitorService.setSortBy("pid");
processListView.restoreAnchor();
} }
} }
@@ -211,9 +203,7 @@ Column {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
processListView.captureAnchor();
SysMonitorService.toggleSortOrder(); SysMonitorService.toggleSortOrder();
processListView.restoreAnchor();
} }
} }
@@ -228,101 +218,21 @@ Column {
} }
ListView { DankListView {
id: processListView id: processListView
property real stableY: 0
property bool isUserScrolling: false
property bool isScrollBarDragging: false
property string keyRoleName: "pid" property string keyRoleName: "pid"
property var _anchorKey: undefined
property real _anchorOffset: 0
function captureAnchor() {
const y = contentY + 1;
const idx = indexAt(0, y);
if (idx < 0 || !model || idx >= model.length)
return ;
_anchorKey = model[idx][keyRoleName];
const it = itemAtIndex(idx);
_anchorOffset = it ? (y - it.y) : 0;
}
function restoreAnchor() {
Qt.callLater(function() {
if (_anchorKey === undefined || !model)
return ;
var i = -1;
for (var j = 0; j < model.length; ++j) {
if (model[j][keyRoleName] === _anchorKey) {
i = j;
break;
}
}
if (i < 0)
return ;
positionViewAtIndex(i, ListView.Beginning);
const maxY = Math.max(0, contentHeight - height);
contentY = Math.max(0, Math.min(maxY, contentY + _anchorOffset - 1));
});
}
width: parent.width width: parent.width
height: parent.height - columnHeaders.height height: parent.height - columnHeaders.height
clip: true clip: true
spacing: 4 spacing: 4
model: SysMonitorService.processes model: SysMonitorService.processes
boundsBehavior: Flickable.StopAtBounds
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
interactive: true
flickDeceleration: 1500 // Touch only in Qt 6.9+ // Lower = more momentum, longer scrolling
maximumFlickVelocity: 2000 // Touch only in Qt 6.9+ // Higher = faster maximum scroll speed
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
onMovementStarted: isUserScrolling = true
onMovementEnded: {
isUserScrolling = false;
if (contentY > 40)
stableY = contentY;
}
onContentYChanged: {
if (!isUserScrolling && !isScrollBarDragging && visible && stableY > 40 && Math.abs(contentY - stableY) > 10)
contentY = stableY;
}
onModelChanged: {
if (model && model.length > 0 && !isUserScrolling && stableY > 40)
Qt.callLater(function() {
contentY = stableY;
});
}
delegate: ProcessListItem { delegate: ProcessListItem {
process: modelData process: modelData
contextMenu: root.contextMenu contextMenu: root.contextMenu
} }
ScrollBar.vertical: ScrollBar {
id: verticalScrollBar
policy: ScrollBar.AsNeeded
onPressedChanged: {
processListView.isScrollBarDragging = pressed;
if (!pressed && processListView.contentY > 40)
processListView.stableY = processListView.contentY;
}
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
} }
} }

View File

@@ -5,35 +5,49 @@ import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
ScrollView { Item {
id: appearanceTab id: appearanceTab
// Qt 6.9+ scrolling: Enhanced mouse wheel and touchpad responsiveness DankFlickable {
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling anchors.fill: parent
WheelHandler { anchors.topMargin: Theme.spacingL
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad anchors.bottomMargin: Theme.spacingXL
onWheel: (event) => { clip: true
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 80 contentHeight: mainColumn.height
let flickable = appearanceTab.contentItem contentWidth: width
let newY = flickable.contentY - delta mouseWheelSpeed: 20
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
flickable.contentY = newY Column {
event.accepted = true id: mainColumn
width: parent.width
spacing: Theme.spacingXL
Loader {
width: parent.width
sourceComponent: displayComponent
}
Loader {
width: parent.width
sourceComponent: transparencyComponent
}
Loader {
width: parent.width
sourceComponent: themeComponent
}
Loader {
width: parent.width
sourceComponent: systemThemingComponent
}
} }
} }
contentWidth: availableWidth // Display Settings Component
contentHeight: column.implicitHeight + Theme.spacingXL Component {
clip: true id: displayComponent
Column {
id: column
width: parent.width
spacing: Theme.spacingXL
topPadding: Theme.spacingL
bottomPadding: Theme.spacingXL
StyledRect { StyledRect {
width: parent.width width: parent.width
height: displaySection.implicitHeight + Theme.spacingL * 2 height: displaySection.implicitHeight + Theme.spacingL * 2
@@ -273,9 +287,13 @@ ScrollView {
} }
} }
} }
}
// Transparency Settings Component
Component {
id: transparencyComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: transparencySection.implicitHeight + Theme.spacingL * 2 height: transparencySection.implicitHeight + Theme.spacingL * 2
@@ -391,9 +409,13 @@ ScrollView {
} }
} }
} }
}
// Theme Color Component
Component {
id: themeComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: themeSection.implicitHeight + Theme.spacingL * 2 height: themeSection.implicitHeight + Theme.spacingL * 2
@@ -759,9 +781,13 @@ ScrollView {
} }
} }
} }
}
// System App Theming Component
Component {
id: systemThemingComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: systemThemingSection.implicitHeight + Theme.spacingL * 2 height: systemThemingSection.implicitHeight + Theme.spacingL * 2
@@ -828,9 +854,7 @@ ScrollView {
} }
} }
} }
} }
Process { Process {

View File

@@ -4,34 +4,44 @@ import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
ScrollView { Item {
id: launcherTab id: launcherTab
// Qt 6.9+ scrolling: Enhanced mouse wheel and touchpad responsiveness DankFlickable {
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling anchors.fill: parent
WheelHandler { anchors.topMargin: Theme.spacingL
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad anchors.bottomMargin: Theme.spacingXL
onWheel: (event) => { clip: true
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 80 contentHeight: mainColumn.height
let flickable = launcherTab.contentItem contentWidth: width
let newY = flickable.contentY - delta mouseWheelSpeed: 20
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
flickable.contentY = newY Column {
event.accepted = true id: mainColumn
width: parent.width
spacing: Theme.spacingXL
Loader {
width: parent.width
sourceComponent: appLauncherComponent
}
Loader {
width: parent.width
sourceComponent: dockComponent
}
Loader {
width: parent.width
sourceComponent: recentlyUsedComponent
}
} }
} }
contentHeight: column.implicitHeight + Theme.spacingXL // App Launcher Component
clip: true Component {
id: appLauncherComponent
Column {
id: column
width: parent.width
spacing: Theme.spacingXL
topPadding: Theme.spacingL
bottomPadding: Theme.spacingXL
StyledRect { StyledRect {
width: parent.width width: parent.width
height: appLauncherSection.implicitHeight + Theme.spacingL * 2 height: appLauncherSection.implicitHeight + Theme.spacingL * 2
@@ -159,9 +169,13 @@ ScrollView {
} }
} }
} }
}
// Dock Component
Component {
id: dockComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: dockSection.implicitHeight + Theme.spacingL * 2 height: dockSection.implicitHeight + Theme.spacingL * 2
@@ -262,7 +276,12 @@ ScrollView {
} }
} }
}
// Recently Used Apps Component
Component {
id: recentlyUsedComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2 height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
@@ -474,9 +493,6 @@ ScrollView {
} }
} }
} }
} }
} }

View File

@@ -7,35 +7,47 @@ import qs.Modals
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
ScrollView { Item {
id: personalizationTab id: personalizationTab
// Qt 6.9+ scrolling: Enhanced mouse wheel and touchpad responsiveness
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 80
let flickable = personalizationTab.contentItem
let newY = flickable.contentY - delta
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
flickable.contentY = newY
event.accepted = true
}
}
property alias profileBrowser: profileBrowserLoader.item property alias profileBrowser: profileBrowserLoader.item
property alias wallpaperBrowser: wallpaperBrowserLoader.item property alias wallpaperBrowser: wallpaperBrowserLoader.item
contentHeight: column.implicitHeight DankFlickable {
clip: true anchors.fill: parent
anchors.topMargin: Theme.spacingL
Column { anchors.bottomMargin: Theme.spacingXL
id: column clip: true
contentHeight: mainColumn.height
width: parent.width contentWidth: width
spacing: Theme.spacingXL mouseWheelSpeed: 20
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
Loader {
width: parent.width
sourceComponent: profileComponent
}
Loader {
width: parent.width
sourceComponent: wallpaperComponent
}
Loader {
width: parent.width
sourceComponent: dynamicThemeComponent
}
}
}
// Profile Image Component
Component {
id: profileComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: profileSection.implicitHeight + Theme.spacingL * 2 height: profileSection.implicitHeight + Theme.spacingL * 2
@@ -294,9 +306,13 @@ ScrollView {
} }
} }
} }
}
// Wallpaper Component
Component {
id: wallpaperComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: wallpaperSection.implicitHeight + Theme.spacingL * 2 height: wallpaperSection.implicitHeight + Theme.spacingL * 2
@@ -491,9 +507,13 @@ ScrollView {
} }
} }
} }
}
// Dynamic Theme Component
Component {
id: dynamicThemeComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2 height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2
@@ -568,9 +588,7 @@ ScrollView {
} }
} }
} }
} }
LazyLoader { LazyLoader {

View File

@@ -3,32 +3,39 @@ import QtQuick.Controls
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
ScrollView { Item {
id: timeWeatherTab id: timeWeatherTab
// Qt 6.9+ scrolling: Enhanced mouse wheel and touchpad responsiveness DankFlickable {
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling anchors.fill: parent
WheelHandler { anchors.topMargin: Theme.spacingL
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad anchors.bottomMargin: Theme.spacingXL
onWheel: (event) => { clip: true
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 80 contentHeight: mainColumn.height
let flickable = timeWeatherTab.contentItem contentWidth: width
let newY = flickable.contentY - delta mouseWheelSpeed: 20
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
flickable.contentY = newY Column {
event.accepted = true id: mainColumn
width: parent.width
spacing: Theme.spacingXL
Loader {
width: parent.width
sourceComponent: timeComponent
}
Loader {
width: parent.width
sourceComponent: weatherComponent
}
} }
} }
contentHeight: column.implicitHeight // Time Format Component
clip: true Component {
id: timeComponent
Column {
id: column
width: parent.width
spacing: Theme.spacingXL
StyledRect { StyledRect {
width: parent.width width: parent.width
height: timeSection.implicitHeight + Theme.spacingL * 2 height: timeSection.implicitHeight + Theme.spacingL * 2
@@ -76,9 +83,13 @@ ScrollView {
} }
} }
} }
}
// Weather Component
Component {
id: weatherComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: weatherSection.implicitHeight + Theme.spacingL * 2 height: weatherSection.implicitHeight + Theme.spacingL * 2
@@ -171,9 +182,6 @@ ScrollView {
} }
} }
} }
} }
} }

View File

@@ -3,23 +3,9 @@ import QtQuick.Controls
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
ScrollView { Item {
id: widgetsTab id: widgetsTab
// Qt 6.9+ scrolling: Enhanced mouse wheel and touchpad responsiveness
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
let delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 80
let flickable = widgetsTab.contentItem
let newY = flickable.contentY - delta
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
flickable.contentY = newY
event.accepted = true
}
}
property var baseWidgetDefinitions: [{ property var baseWidgetDefinitions: [{
"id": "launcherButton", "id": "launcherButton",
"text": "App Launcher", "text": "App Launcher",
@@ -305,8 +291,6 @@ ScrollView {
return widgets; return widgets;
} }
contentHeight: column.implicitHeight + Theme.spacingXL
clip: true
Component.onCompleted: { Component.onCompleted: {
if (!SettingsData.topBarLeftWidgets || SettingsData.topBarLeftWidgets.length === 0) if (!SettingsData.topBarLeftWidgets || SettingsData.topBarLeftWidgets.length === 0)
SettingsData.setTopBarLeftWidgets(defaultLeftWidgets); SettingsData.setTopBarLeftWidgets(defaultLeftWidgets);
@@ -347,14 +331,46 @@ ScrollView {
}); });
} }
Column { DankFlickable {
id: column anchors.fill: parent
anchors.topMargin: Theme.spacingL
width: parent.width anchors.bottomMargin: Theme.spacingXL
spacing: Theme.spacingXL clip: true
topPadding: Theme.spacingL contentHeight: mainColumn.height
bottomPadding: Theme.spacingXL contentWidth: width
mouseWheelSpeed: 20
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
Loader {
width: parent.width
sourceComponent: headerComponent
}
Loader {
width: parent.width
sourceComponent: messageComponent
}
Loader {
width: parent.width
sourceComponent: sectionsComponent
}
Loader {
width: parent.width
sourceComponent: workspaceComponent
}
}
}
// Header Component
Component {
id: headerComponent
Row { Row {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -406,7 +422,6 @@ ScrollView {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { MouseArea {
@@ -427,7 +442,6 @@ ScrollView {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
Behavior on border.color { Behavior on border.color {
@@ -435,13 +449,15 @@ ScrollView {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
} }
}
// Message Component
Component {
id: messageComponent
Rectangle { Rectangle {
width: parent.width width: parent.width
height: messageText.contentHeight + Theme.spacingM * 2 height: messageText.contentHeight + Theme.spacingM * 2
@@ -449,9 +465,6 @@ ScrollView {
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 1 border.width: 1
visible: true
opacity: 1
z: 1
StyledText { StyledText {
id: messageText id: messageText
@@ -463,9 +476,13 @@ ScrollView {
width: parent.width - Theme.spacingM * 2 width: parent.width - Theme.spacingM * 2
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
} }
}
// Sections Component
Component {
id: sectionsComponent
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
@@ -568,9 +585,13 @@ ScrollView {
} }
} }
} }
} }
}
// Workspace Settings Component
Component {
id: workspaceComponent
StyledRect { StyledRect {
width: parent.width width: parent.width
height: workspaceSection.implicitHeight + Theme.spacingL * 2 height: workspaceSection.implicitHeight + Theme.spacingL * 2
@@ -604,7 +625,6 @@ ScrollView {
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
DankToggle { DankToggle {
@@ -626,44 +646,10 @@ ScrollView {
return SettingsData.setShowWorkspacePadding(checked); return SettingsData.setShowWorkspacePadding(checked);
} }
} }
} }
} }
} }
Rectangle {
width: tooltipText.contentWidth + Theme.spacingM * 2
height: tooltipText.contentHeight + Theme.spacingS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
visible: resetArea.containsMouse
opacity: resetArea.containsMouse ? 1 : 0
y: column.y + 48
x: parent.width - width - Theme.spacingM
z: 100
StyledText {
id: tooltipText
anchors.centerIn: parent
text: "Reset widget layout to defaults"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
DankWidgetSelectionPopup { DankWidgetSelectionPopup {
id: widgetSelectionPopup id: widgetSelectionPopup

View File

@@ -1,13 +1,16 @@
import QtQuick import QtQuick
import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import qs.Common import qs.Common
Rectangle { Rectangle {
id: root id: root
property var parentWindow: null
property var parentScreen: null
readonly property int calculatedWidth: SystemTray.items.values.length > 0 ? SystemTray.items.values.length * 24 + (SystemTray.items.values.length - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0 readonly property int calculatedWidth: SystemTray.items.values.length > 0 ? SystemTray.items.values.length * 24 + (SystemTray.items.values.length - 1) * Theme.spacingXS + Theme.spacingS * 2 : 0
signal menuRequested(var menu, var item, real x, real y)
width: calculatedWidth width: calculatedWidth
height: 30 height: 30
@@ -84,16 +87,19 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => { onClicked: (mouse) => {
if (!trayItem) if (!trayItem)
return ; return
if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu)
trayItem.activate();
} else if (mouse.button === Qt.RightButton) {
if (trayItem && trayItem.hasMenu)
root.menuRequested(null, trayItem, mouse.x, mouse.y);
if (trayItem.hasMenu) {
var globalPos = mapToGlobal(0, 0)
var currentScreen = parentScreen || Screen
var screenX = currentScreen.x || 0
var relativeX = globalPos.x - screenX
menuAnchor.menu = trayItem.menu
menuAnchor.anchor.window = parentWindow
menuAnchor.anchor.rect = Qt.rect(relativeX, Theme.barHeight + Theme.spacingS, parent.width, 1)
menuAnchor.open()
} else if (mouse.button === Qt.LeftButton) {
trayItem.activate()
} }
} }
} }
@@ -104,4 +110,8 @@ Rectangle {
} }
QsMenuAnchor {
id: menuAnchor
}
} }

View File

@@ -1,181 +0,0 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Common
import qs.Widgets
PanelWindow {
id: root
property bool showContextMenu: false
property real contextMenuX: 0
property real contextMenuY: 0
property var currentTrayMenu: null
property var currentTrayItem: null
visible: showContextMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: menuContainer
x: contextMenuX
y: contextMenuY
width: Math.max(180, Math.min(300, menuList.maxTextWidth + Theme.spacingL * 2))
height: Math.max(60, menuList.contentHeight + Theme.spacingS * 2)
color: Theme.popupBackground()
radius: Theme.cornerRadiusLarge
border.color: Theme.outlineMedium
border.width: 1
opacity: showContextMenu ? 1 : 0
scale: showContextMenu ? 1 : 0.85
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
QsMenuOpener {
id: menuOpener
menu: currentTrayItem && currentTrayItem.hasMenu ? currentTrayItem.menu : null
}
ListView {
id: menuList
property real maxTextWidth: {
let maxWidth = 0;
if (model && model.values) {
for (let i = 0; i < model.values.length; i++) {
const item = model.values[i];
if (item && item.text) {
const textWidth = textMetrics.advanceWidth * item.text.length * 0.6;
maxWidth = Math.max(maxWidth, textWidth);
}
}
}
return Math.min(maxWidth, 280); // Cap at reasonable width
}
anchors.fill: parent
spacing: 1
model: menuOpener.children
TextMetrics {
id: textMetrics
font.pixelSize: Theme.fontSizeSmall
text: "M"
}
delegate: Rectangle {
width: ListView.view.width
height: modelData.isSeparator ? 5 : 28
radius: modelData.isSeparator ? 0 : Theme.cornerRadiusSmall
color: modelData.isSeparator ? "transparent" : (menuItemArea.containsMouse ? Theme.primaryHover : "transparent")
Rectangle {
visible: modelData.isSeparator
anchors.centerIn: parent
width: parent.width - Theme.spacingS * 2
height: 1
color: Theme.surfaceVariantAlpha
}
Row {
visible: !modelData.isSeparator
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: modelData.text || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: menuItemArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.isSeparator ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !modelData.isSeparator
onClicked: {
if (modelData.triggered)
modelData.triggered();
showContextMenu = false;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
showContextMenu = false;
}
}
}

View File

@@ -542,16 +542,8 @@ PanelWindow {
id: systemTrayComponent id: systemTrayComponent
SystemTrayBar { SystemTrayBar {
onMenuRequested: (menu, item, x, y) => { parentWindow: root
systemTrayContextMenu.currentTrayMenu = menu; parentScreen: root.screen
systemTrayContextMenu.currentTrayItem = item;
systemTrayContextMenu.contextMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL;
systemTrayContextMenu.contextMenuY = Theme.barHeight - Theme.spacingXS;
systemTrayContextMenu.showContextMenu = true;
if (menu) {
menu.menuVisible = true;
}
}
} }
} }

View File

@@ -121,7 +121,7 @@ Rectangle {
enabled: !isPlaceholder enabled: !isPlaceholder
onClicked: { onClicked: {
if (!isPlaceholder) if (!isPlaceholder)
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", (modelData - 1).toString()]); NiriService.switchToWorkspace(modelData - 1);
} }
} }

107
README.md
View File

@@ -23,24 +23,31 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
<div align="center"> <div align="center">
### Application Launcher ### Application Launcher
<img src="https://github.com/user-attachments/assets/2da00ea1-8921-4473-a2a9-44a44535a822" width="450" alt="Spotlight Launcher" /> <img src="https://github.com/user-attachments/assets/2da00ea1-8921-4473-a2a9-44a44535a822" width="450" alt="Spotlight Launcher" />
### System Monitor ### System Monitor
<img src="https://github.com/user-attachments/assets/b3c817ec-734d-4974-929f-2d11a1065349" width="600" alt="System Monitor" /> <img src="https://github.com/user-attachments/assets/b3c817ec-734d-4974-929f-2d11a1065349" width="600" alt="System Monitor" />
### Widget Customization ### Widget Customization
<img src="https://github.com/user-attachments/assets/903f7c60-146f-4fb3-a75d-a4823828f298" width="500" alt="Widget Customization" /> <img src="https://github.com/user-attachments/assets/903f7c60-146f-4fb3-a75d-a4823828f298" width="500" alt="Widget Customization" />
### Lock Screen ### Lock Screen
<img src="https://github.com/user-attachments/assets/3fa07de2-c1b0-4e57-8f25-3830ac6baf4f" width="600" alt="Lock Screen" /> <img src="https://github.com/user-attachments/assets/3fa07de2-c1b0-4e57-8f25-3830ac6baf4f" width="600" alt="Lock Screen" />
### Dynamic Theming ### Dynamic Theming
<img src="https://github.com/user-attachments/assets/1994e616-f9d9-424a-9f60-6f06708bf12e" width="700" alt="Auto Theme" /> <img src="https://github.com/user-attachments/assets/1994e616-f9d9-424a-9f60-6f06708bf12e" width="700" alt="Auto Theme" />
### Notification Center ### Notification Center
<img src="https://github.com/user-attachments/assets/07cbde9a-0242-4989-9f97-5765c6458c85" width="350" alt="Notification Center" /> <img src="https://github.com/user-attachments/assets/07cbde9a-0242-4989-9f97-5765c6458c85" width="350" alt="Notification Center" />
### Dock ### Dock
<img src="https://github.com/user-attachments/assets/e6999daf-f7bf-4329-98fa-0ce4f0e7219c" width="400" alt="Dock" /> <img src="https://github.com/user-attachments/assets/e6999daf-f7bf-4329-98fa-0ce4f0e7219c" width="400" alt="Dock" />
</div> </div>
@@ -50,6 +57,7 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
## What's Inside ## What's Inside
**Core Widgets:** **Core Widgets:**
- **TopBar**: fully customizable bar where widgets can be added, removed, and re-arranged. - **TopBar**: fully customizable bar where widgets can be added, removed, and re-arranged.
- **App Launcher** with fuzzy search, categories, and auto-sorting by most used apps. - **App Launcher** with fuzzy search, categories, and auto-sorting by most used apps.
- **Workspace Switcher** Dynamically resizing niri workspace switcher. - **Workspace Switcher** Dynamically resizing niri workspace switcher.
@@ -71,6 +79,7 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
- **Lock Screen** Using quickshell's WlSessionLock - **Lock Screen** Using quickshell's WlSessionLock
**Features:** **Features:**
- Dynamic wallpaper-based theming with matugen integration - Dynamic wallpaper-based theming with matugen integration
- Numerous IPCs to trigger actions and open various modals. - Numerous IPCs to trigger actions and open various modals.
- Calendar integration with [khal](https://github.com/pimutils/khal) - Calendar integration with [khal](https://github.com/pimutils/khal)
@@ -83,14 +92,15 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
### Quick Start ### Quick Start
*If you do not already have niri, see [#] \*If you do not already have niri, see [#]
**Dependencies:** **Dependencies:**
```bash ```bash
# Arch Linux # Arch Linux
paru -S quickshell-git ttf-material-symbols-variable-git inter-font ttf-fira-code paru -S quickshell-git ttf-material-symbols-variable-git inter-font ttf-fira-code
# Fedora # Fedora
sudo dnf copr enable errornointernet/quickshell && sudo dnf install quickshell-git rsms-inter-fonts fira-code-fonts sudo dnf copr enable errornointernet/quickshell && sudo dnf install quickshell-git rsms-inter-fonts fira-code-fonts
# Install icon fonts manually # Install icon fonts manually
mkdir -p ~/.local/share/fonts mkdir -p ~/.local/share/fonts
@@ -99,6 +109,7 @@ fc-cache -f
``` ```
**Get the shell:** **Get the shell:**
```bash ```bash
# Arch linux available via AUR # Arch linux available via AUR
paru -S dankmaterialshell-git paru -S dankmaterialshell-git
@@ -114,6 +125,7 @@ qs -c DankMaterialShell
<details><summary>Font Installation</summary> <details><summary>Font Installation</summary>
**Material Symbols (Required):** **Material Symbols (Required):**
```bash ```bash
# Manual installation # Manual installation
mkdir -p ~/.local/share/fonts mkdir -p ~/.local/share/fonts
@@ -125,6 +137,7 @@ paru -S ttf-material-symbols-variable-git
``` ```
**Typography (Recommended):** **Typography (Recommended):**
```bash ```bash
# Inter Variable Font # Inter Variable Font
curl -L "https://github.com/rsms/inter/releases/download/v4.0/Inter-4.0.zip" -o /tmp/Inter.zip curl -L "https://github.com/rsms/inter/releases/download/v4.0/Inter-4.0.zip" -o /tmp/Inter.zip
@@ -142,23 +155,24 @@ rm /tmp/FiraCode.zip && fc-cache -f
<details><summary>Optional Features</summary> <details><summary>Optional Features</summary>
**Enhanced Functionality:** **Enhanced Functionality:**
```bash ```bash
# Arch Linux # Arch Linux
pacman -S cava wl-clipboard cliphist ddcutil brightnessctl qt5ct qt6ct pacman -S cava wl-clipboard cliphist ddcutil brightnessctl
paru -S matugen paru -S matugen
# Fedora # Fedora
sudo dnf install cava wl-clipboard ddcutil brightnessctl qt5ct qt6ct sudo dnf install cava wl-clipboard ddcutil brightnessctl
sudo dnf copr enable wef/cliphist && sudo dnf install cliphist sudo dnf copr enable wef/cliphist && sudo dnf install cliphist
sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen sudo dnf copr enable heus-sueh/packages && sudo dnf install matugen
``` ```
**What you get:** **What you get:**
- `matugen`: Wallpaper-based dynamic theming - `matugen`: Wallpaper-based dynamic theming
- `ddcutil`: External monitor brightness control - `ddcutil`: External monitor brightness control
- `brightnessctl`: Laptop display brightness - `brightnessctl`: Laptop display brightness
- `wl-clipboard`: Required for copying various elements to clipboard. - `wl-clipboard`: Required for copying various elements to clipboard.
- `qt5ct/qt6ct`: Qt application theming
- `cava`: Audio visualizer - `cava`: Audio visualizer
- `cliphist`: Clipboard history - `cliphist`: Clipboard history
@@ -219,7 +233,7 @@ binds {
} }
XF86MonBrightnessDown allow-when-locked=true { XF86MonBrightnessDown allow-when-locked=true {
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "decrement" "5"; spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "decrement" "5";
} }
} }
``` ```
@@ -232,7 +246,7 @@ Control everything from the command line, or via keybinds:
qs -c DankMaterialShell ipc call audio setvolume 50 qs -c DankMaterialShell ipc call audio setvolume 50
qs -c DankMaterialShell ipc call audio mute qs -c DankMaterialShell ipc call audio mute
# Launch applications # Launch applications
qs -c DankMaterialShell ipc call spotlight toggle qs -c DankMaterialShell ipc call spotlight toggle
qs -c DankMaterialShell ipc call processlist toggle qs -c DankMaterialShell ipc call processlist toggle
@@ -250,54 +264,98 @@ qs -c DankMaterialShell ipc call mpris next
### System App Integration ### System App Integration
There's two toggles in the appearance section of settings, for GTK and QT apps.
These settings will override some local GTK and QT configuration files, you can still integrate auto-theming if you do not wish DankShell to mess with your QTCT/GTK files.
No matter what when matugen is enabled the files will be created on wallpaper changes:
- ~/.config/gtk-3.0/dank-colors.css
- ~/.config/gtk-4.0/dank-colors.css
- ~/.config/qt6ct/colors/matugen.conf
- ~/.config/qt5ct/colors/matugen.conf
If you do not like our theme path, you can integrate this with other GTK themes, matugen themes, etc.
**GTK Apps:** **GTK Apps:**
Install [Colloid](https://github.com/vinceliuice/Colloid-gtk-theme) or similar Material theme:
1. Install [Colloid](https://github.com/vinceliuice/Colloid-gtk-theme)
Colloid is a hard requirement for the auto-theming because of how it integrates with colloid css files, however you can integrate auto-theming with other themes, you just have to do it manually (so leave the toggle OFF in settings)
It will still create `~/.config/gtk-3.0/4.0/dank-colors.css` on theme updates, these you can import into other compatible GTK themes.
```bash ```bash
# Some default install settings for colloid
./install.sh -s standard -l --tweaks normal ./install.sh -s standard -l --tweaks normal
``` ```
Configure in `~/.config/gtk-3.0/settings.ini`: Configure in `~/.config/gtk-3.0/settings.ini` and `~/.config/gtk-4.0/settings.ini`:
```ini ```ini
[Settings] [Settings]
gtk-theme-name=Colloid gtk-theme-name=Colloid
``` ```
**Qt Apps:** **Qt Apps:**
```bash
# Install Breeze
pacman -S breeze breeze5 # Arch
sudo dnf install breeze # Fedora
# Configure qt5ct/qt6ct You have **two** paths for QT theming, first path is to use **gtk3**. To do that, add the following to your niri config.
echo 'style=Breeze' >> ~/.config/qt5ct/qt5ct.conf
```kdl
environment {
// Add to existing environment block
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
}
``` ```
**Dynamic Theming:** **Done** - if you're not happy with this and wish to use Breeze or another QT theme then continue on.
Enable wallpaper-based theming in **Settings → Appearance → System App Theming** after installing matugen.
1. Install qt6ct and qt5ct
```bash
# Arch
pacman -S qt5ct qt6ct
# Fedora
dnf install qt5ct qt6ct
```
2. Configure Environment in niri
```kdl
// Add to existing environment block
QT_QPA_PLATFORMTHEME "qt5ct"
QT_QPA_PLATFORMTHEME_QT6 "qt6ct"
```
You'll have to restart your session for themes to take effect.
### Terminal Integration ### Terminal Integration
**Ghostty users** can add automatic color theming: **Ghostty users** can add automatic color theming:
```bash ```bash
echo "config-file = ./config-dankcolors" >> ~/.config/ghostty/config echo "config-file = ./config-dankcolors" >> ~/.config/ghostty/config
``` ```
## Calendar Setup ## Calendar Setup
Sync your Google Calendar for dashboard integration: Sync your caldev compatible calendar (Google, Office365, etc.) for dashboard integration:
<details><summary>Configuration Steps</summary> <details><summary>Configuration Steps</summary>
**Install dependencies:** **Install dependencies:**
```bash ```bash
# Arch # Arch
pacman -S vdirsyncer khal python-aiohttp-oauthlib pacman -S vdirsyncer khal python-aiohttp-oauthlib
# Fedora # Fedora
sudo dnf install python3-vdirsyncer khal python3-aiohttp-oauthlib sudo dnf install python3-vdirsyncer khal python3-aiohttp-oauthlib
``` ```
**Configure vdirsyncer** (`~/.vdirsyncer/config`): **Configure vdirsyncer** (`~/.vdirsyncer/config`):
```ini ```ini
[general] [general]
status_path = "~/.calendars/status" status_path = "~/.calendars/status"
@@ -322,6 +380,7 @@ fileext = ".ics"
``` ```
**Setup sync:** **Setup sync:**
```bash ```bash
vdirsyncer sync vdirsyncer sync
khal configure khal configure
@@ -338,8 +397,9 @@ crontab -e
All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or more intuitively the built-in settings modal. All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or more intuitively the built-in settings modal.
**Key configuration areas:** **Key configuration areas:**
- Widget positioning and behavior - Widget positioning and behavior
- Theme and color preferences - Theme and color preferences
- Time format, weather units and location - Time format, weather units and location
- Light/Dark modes - Light/Dark modes
- Wallpaper and Profile picture - Wallpaper and Profile picture
@@ -348,12 +408,14 @@ All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or
## Troubleshooting ## Troubleshooting
**Common issues:** **Common issues:**
- **Missing icons:** Verify Material Symbols font installation with `fc-list | grep Material` - **Missing icons:** Verify Material Symbols font installation with `fc-list | grep Material`
- **No dynamic theming:** Install matugen and enable in settings - **No dynamic theming:** Install matugen and enable in settings
- **Qt apps not themed:** Configure qt5ct/qt6ct and set QT_QPA_PLATFORMTHEME - **Qt apps not themed:** Configure qt5ct/qt6ct and set QT_QPA_PLATFORMTHEME
- **Calendar not syncing:** Check vdirsyncer credentials and network connectivity - **Calendar not syncing:** Check vdirsyncer credentials and network connectivity
**Getting help:** **Getting help:**
- Check the [issues](https://github.com/bbedward/DankMaterialShell/issues) for known problems - Check the [issues](https://github.com/bbedward/DankMaterialShell/issues) for known problems
- Share logs from `qs -c DankMaterialShell` for debugging - Share logs from `qs -c DankMaterialShell` for debugging
- Join the niri community for compositor-specific questions - Join the niri community for compositor-specific questions
@@ -363,6 +425,7 @@ All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or
DankMaterialShell welcomes contributions! Whether it's bug fixes, new widgets, theme improvements, or documentation updates - all help is appreciated. DankMaterialShell welcomes contributions! Whether it's bug fixes, new widgets, theme improvements, or documentation updates - all help is appreciated.
**Areas that need attention:** **Areas that need attention:**
- More widget options and customization - More widget options and customization
- Additional compositor compatibility - Additional compositor compatibility
- Performance optimizations - Performance optimizations
@@ -373,4 +436,4 @@ DankMaterialShell welcomes contributions! Whether it's bug fixes, new widgets, t
- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible. - [quickshell](https://quickshell.org/) the core of what makes a shell like this possible.
- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor. - [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor.
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets. - [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets. - [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets.

View File

@@ -7,371 +7,293 @@ import Quickshell.Io
Singleton { Singleton {
id: root id: root
// Workspace management // Workspace management
property var workspaces: ({})
property var allWorkspaces: [] property var allWorkspaces: []
property int focusedWorkspaceIndex: 0 property int focusedWorkspaceIndex: 0
property string focusedWorkspaceId: "" property string focusedWorkspaceId: ""
property var currentOutputWorkspaces: [] property var currentOutputWorkspaces: []
property string currentOutput: "" property string currentOutput: ""
// Window management // Window management
property var windows: [] property var windows: []
property int focusedWindowIndex: -1 property int focusedWindowIndex: -1
property string focusedWindowTitle: "(No active window)" property string focusedWindowTitle: "(No active window)"
property string focusedWindowId: "" property string focusedWindowId: ""
// Overview state // Overview state
property bool inOverview: false property bool inOverview: false
signal windowOpenedOrChanged(var windowData) signal windowOpenedOrChanged(var windowData)
// Feature availability // Feature availability
property bool niriAvailable: false property bool niriAvailable: false
Component.onCompleted: { readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
console.log("NiriService: Component.onCompleted - initializing service")
checkNiriAvailability() Component.onCompleted: checkNiriAvailability()
}
// Check if niri is available
Process { Process {
id: niriCheck id: niriCheck
command: ["which", "niri"] command: ["test", "-S", root.socketPath]
onExited: (exitCode) => { onExited: exitCode => {
root.niriAvailable = exitCode === 0 root.niriAvailable = exitCode === 0;
if (root.niriAvailable) { if (root.niriAvailable) {
console.log("NiriService: niri found, starting event stream and loading initial data") eventStreamSocket.connected = true;
eventStreamProcess.running = true
loadInitialWorkspaceData()
} else {
console.log("NiriService: niri not found, workspace features disabled")
} }
} }
} }
function checkNiriAvailability() { function checkNiriAvailability() {
niriCheck.running = true niriCheck.running = true;
} }
// Load initial workspace data Socket {
Process { id: eventStreamSocket
id: initialDataQuery path: root.socketPath
command: ["niri", "msg", "-j", "workspaces"] connected: false
running: false
onConnectionStateChanged: {
stdout: StdioCollector { if (connected) {
onStreamFinished: { write('"EventStream"\n');
if (text && text.trim()) {
try {
console.log("NiriService: Loaded initial workspace data")
const workspaces = JSON.parse(text.trim())
// Initial query returns array directly, event stream wraps it in WorkspacesChanged
handleWorkspacesChanged({ workspaces: workspaces })
} catch (e) {
console.warn("NiriService: Failed to parse initial workspace data:", e)
}
}
} }
} }
}
parser: SplitParser {
// Load initial windows data onRead: line => {
Process {
id: initialWindowsQuery
command: ["niri", "msg", "-j", "windows"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
try {
const windowsData = JSON.parse(text.trim())
if (windowsData && windowsData.windows) {
handleWindowsChanged(windowsData)
console.log("NiriService: Loaded", windowsData.windows.length, "initial windows")
}
} catch (e) {
console.warn("NiriService: Failed to parse initial windows data:", e)
}
}
}
}
}
// Load initial focused window data
Process {
id: initialFocusedWindowQuery
command: ["niri", "msg", "-j", "focused-window"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
try {
const focusedData = JSON.parse(text.trim())
if (focusedData && focusedData.id) {
handleWindowFocusChanged({ id: focusedData.id })
console.log("NiriService: Loaded initial focused window:", focusedData.id)
}
} catch (e) {
console.warn("NiriService: Failed to parse initial focused window data:", e)
}
}
}
}
}
function loadInitialWorkspaceData() {
console.log("NiriService: Loading initial workspace data...")
initialDataQuery.running = true
initialWindowsQuery.running = true
initialFocusedWindowQuery.running = true
}
// Event stream for real-time updates
Process {
id: eventStreamProcess
command: ["niri", "msg", "-j", "event-stream"]
running: false // Will be enabled after niri check
stdout: SplitParser {
onRead: data => {
try { try {
const event = JSON.parse(data.trim()) const event = JSON.parse(line);
handleNiriEvent(event) handleNiriEvent(event);
} catch (e) { } catch (e) {
console.warn("NiriService: Failed to parse event:", data, e) console.warn("NiriService: Failed to parse event:", line, e);
} }
} }
} }
onExited: (exitCode) => {
if (exitCode !== 0 && root.niriAvailable) {
console.warn("NiriService: Event stream exited with code", exitCode, "restarting immediately")
eventStreamProcess.running = true
}
}
} }
Socket {
id: requestSocket
path: root.socketPath
connected: root.niriAvailable
}
function handleNiriEvent(event) { function handleNiriEvent(event) {
if (event.WorkspacesChanged) { if (event.WorkspacesChanged) {
handleWorkspacesChanged(event.WorkspacesChanged) handleWorkspacesChanged(event.WorkspacesChanged);
} else if (event.WorkspaceActivated) { } else if (event.WorkspaceActivated) {
handleWorkspaceActivated(event.WorkspaceActivated) handleWorkspaceActivated(event.WorkspaceActivated);
} else if (event.WindowsChanged) { } else if (event.WindowsChanged) {
handleWindowsChanged(event.WindowsChanged) handleWindowsChanged(event.WindowsChanged);
} else if (event.WindowClosed) { } else if (event.WindowClosed) {
handleWindowClosed(event.WindowClosed) handleWindowClosed(event.WindowClosed);
} else if (event.WindowFocusChanged) { } else if (event.WindowFocusChanged) {
handleWindowFocusChanged(event.WindowFocusChanged) handleWindowFocusChanged(event.WindowFocusChanged);
} else if (event.WindowOpenedOrChanged) { } else if (event.WindowOpenedOrChanged) {
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged) handleWindowOpenedOrChanged(event.WindowOpenedOrChanged);
} else if (event.OverviewOpenedOrClosed) { } else if (event.OverviewOpenedOrClosed) {
handleOverviewChanged(event.OverviewOpenedOrClosed) handleOverviewChanged(event.OverviewOpenedOrClosed);
} }
} }
function handleWorkspacesChanged(data) { function handleWorkspacesChanged(data) {
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx) const workspaces = {};
// Update focused workspace for (const ws of data.workspaces) {
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused) workspaces[ws.id] = ws;
if (focusedWorkspaceIndex >= 0) {
var focusedWs = allWorkspaces[focusedWorkspaceIndex]
focusedWorkspaceId = focusedWs.id
currentOutput = focusedWs.output || ""
} else {
focusedWorkspaceIndex = 0
focusedWorkspaceId = ""
} }
updateCurrentOutputWorkspaces() root.workspaces = workspaces;
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx);
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused);
if (focusedWorkspaceIndex >= 0) {
var focusedWs = allWorkspaces[focusedWorkspaceIndex];
focusedWorkspaceId = focusedWs.id;
currentOutput = focusedWs.output || "";
} else {
focusedWorkspaceIndex = 0;
focusedWorkspaceId = "";
}
updateCurrentOutputWorkspaces();
} }
function handleWorkspaceActivated(data) { function handleWorkspaceActivated(data) {
// Update focused workspace const ws = root.workspaces[data.id];
focusedWorkspaceId = data.id if (!ws)
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id) return;
const output = ws.output;
if (focusedWorkspaceIndex >= 0) {
var activatedWs = allWorkspaces[focusedWorkspaceIndex] for (const id in root.workspaces) {
const workspace = root.workspaces[id];
// Update workspace states properly const got_activated = workspace.id === data.id;
// First, deactivate all workspaces on this output
for (var i = 0; i < allWorkspaces.length; i++) { if (workspace.output === output) {
if (allWorkspaces[i].output === activatedWs.output) { workspace.is_active = got_activated;
allWorkspaces[i].is_active = false }
allWorkspaces[i].is_focused = false
} if (data.focused) {
workspace.is_focused = got_activated;
} }
// Then activate the new workspace
allWorkspaces[focusedWorkspaceIndex].is_active = true
allWorkspaces[focusedWorkspaceIndex].is_focused = data.focused || false
currentOutput = activatedWs.output || ""
updateCurrentOutputWorkspaces()
// Force property change notifications
allWorkspacesChanged()
} else {
focusedWorkspaceIndex = 0
} }
focusedWorkspaceId = data.id;
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id);
if (focusedWorkspaceIndex >= 0) {
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || "";
}
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx);
updateCurrentOutputWorkspaces();
workspacesChanged();
} }
function handleWindowsChanged(data) { function handleWindowsChanged(data) {
windows = [...data.windows].sort((a, b) => a.id - b.id) windows = [...data.windows].sort((a, b) => a.id - b.id);
updateFocusedWindow() updateFocusedWindow();
} }
function handleWindowClosed(data) { function handleWindowClosed(data) {
windows = windows.filter(w => w.id !== data.id) windows = windows.filter(w => w.id !== data.id);
updateFocusedWindow() updateFocusedWindow();
} }
function handleWindowFocusChanged(data) { function handleWindowFocusChanged(data) {
if (data.id) { if (data.id) {
focusedWindowId = data.id focusedWindowId = data.id;
focusedWindowIndex = windows.findIndex(w => w.id === data.id) focusedWindowIndex = windows.findIndex(w => w.id === data.id);
} else { } else {
focusedWindowId = "" focusedWindowId = "";
focusedWindowIndex = -1 focusedWindowIndex = -1;
} }
updateFocusedWindow() updateFocusedWindow();
} }
function handleWindowOpenedOrChanged(data) { function handleWindowOpenedOrChanged(data) {
if (!data.window) return; if (!data.window)
return;
const window = data.window; const window = data.window;
const existingIndex = windows.findIndex(w => w.id === window.id); const existingIndex = windows.findIndex(w => w.id === window.id);
if (existingIndex >= 0) { if (existingIndex >= 0) {
// Update existing window - create new array to trigger property change
let updatedWindows = [...windows]; let updatedWindows = [...windows];
updatedWindows[existingIndex] = window; updatedWindows[existingIndex] = window;
windows = updatedWindows.sort((a, b) => a.id - b.id); windows = updatedWindows.sort((a, b) => a.id - b.id);
} else { } else {
// Add new window
windows = [...windows, window].sort((a, b) => a.id - b.id); windows = [...windows, window].sort((a, b) => a.id - b.id);
} }
// Update focused window if this window is focused
if (window.is_focused) { if (window.is_focused) {
focusedWindowId = window.id; focusedWindowId = window.id;
focusedWindowIndex = windows.findIndex(w => w.id === window.id); focusedWindowIndex = windows.findIndex(w => w.id === window.id);
} }
updateFocusedWindow(); updateFocusedWindow();
// Emit signal for other services to listen to
windowOpenedOrChanged(window); windowOpenedOrChanged(window);
} }
function handleOverviewChanged(data) { function handleOverviewChanged(data) {
inOverview = data.is_open inOverview = data.is_open;
} }
function updateCurrentOutputWorkspaces() { function updateCurrentOutputWorkspaces() {
if (!currentOutput) { if (!currentOutput) {
currentOutputWorkspaces = allWorkspaces currentOutputWorkspaces = allWorkspaces;
return return;
} }
// Filter workspaces for current output var outputWs = allWorkspaces.filter(w => w.output === currentOutput);
var outputWs = allWorkspaces.filter(w => w.output === currentOutput) currentOutputWorkspaces = outputWs;
currentOutputWorkspaces = outputWs
} }
function updateFocusedWindow() { function updateFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) { if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
var focusedWin = windows[focusedWindowIndex] var focusedWin = windows[focusedWindowIndex];
focusedWindowTitle = focusedWin.title || "(Unnamed window)" focusedWindowTitle = focusedWin.title || "(Unnamed window)";
} else { } else {
focusedWindowTitle = "(No active window)" focusedWindowTitle = "(No active window)";
} }
} }
// Public API functions function send(request) {
function switchToWorkspace(workspaceId) { if (!niriAvailable || !requestSocket.connected)
if (!niriAvailable) return false return false;
requestSocket.write(JSON.stringify(request) + "\n");
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]) return true;
return true
} }
function switchToWorkspaceByIndex(index) { function switchToWorkspace(workspaceIndex) {
if (!niriAvailable || index < 0 || index >= allWorkspaces.length) return false return send({
Action: {
var workspace = allWorkspaces[index] FocusWorkspace: {
return switchToWorkspace(workspace.id) reference: {
Index: workspaceIndex
}
}
}
});
} }
function switchToWorkspaceByNumber(number, output) {
if (!niriAvailable) return false
var targetOutput = output || currentOutput
if (!targetOutput) {
console.warn("NiriService: No output specified for workspace switching")
return false
}
// Get workspaces for the target output, sorted by idx
var outputWorkspaces = allWorkspaces.filter(w => w.output === targetOutput).sort((a, b) => a.idx - b.idx)
// Use sequential index (number is 1-based, array is 0-based)
if (number >= 1 && number <= outputWorkspaces.length) {
var workspace = outputWorkspaces[number - 1]
return switchToWorkspace(workspace.id)
}
console.warn("NiriService: No workspace", number, "found on output", targetOutput)
return false
}
function getCurrentOutputWorkspaceNumbers() { function getCurrentOutputWorkspaceNumbers() {
return currentOutputWorkspaces.map(w => w.idx + 1) // niri uses 0-based, UI shows 1-based return currentOutputWorkspaces.map(w => w.idx + 1); // niri uses 0-based, UI shows 1-based
} }
function getCurrentWorkspaceNumber() { function getCurrentWorkspaceNumber() {
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) { if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
return allWorkspaces[focusedWorkspaceIndex].idx + 1 return allWorkspaces[focusedWorkspaceIndex].idx + 1;
} }
return 1 return 1;
} }
function focusWindow(windowId) { function focusWindow(windowId) {
if (!niriAvailable) return false return send({
Action: {
console.log("NiriService: Focusing window with command:", ["niri", "msg", "action", "focus-window", "--id", windowId.toString()]) FocusWindow: {
Quickshell.execDetached(["niri", "msg", "action", "focus-window", "--id", windowId.toString()]) id: windowId
return true }
}
});
} }
function closeWindow(windowId) { function closeWindow(windowId) {
if (!niriAvailable) return false return send({
Action: {
console.log("NiriService: Closing window with command:", ["niri", "msg", "action", "close-window", "--id", windowId.toString()]) CloseWindow: {
Quickshell.execDetached(["niri", "msg", "action", "close-window", "--id", windowId.toString()]) id: windowId
return true }
}
});
} }
function quit() {
return send({
Action: {
Quit: {
skip_confirmation: true
}
}
});
}
function getWindowsByAppId(appId) { function getWindowsByAppId(appId) {
if (!appId) return [] if (!appId)
return windows.filter(w => w.app_id && w.app_id.toLowerCase() === appId.toLowerCase()) return [];
return windows.filter(w => w.app_id && w.app_id.toLowerCase() === appId.toLowerCase());
} }
function getRunningAppIds() { function getRunningAppIds() {
var appIds = new Set() var appIds = new Set();
windows.forEach(w => { windows.forEach(w => {
if (w.app_id) { if (w.app_id) {
appIds.add(w.app_id.toLowerCase()) appIds.add(w.app_id.toLowerCase());
} }
}) });
return Array.from(appIds) return Array.from(appIds);
} }
} }

View File

@@ -269,7 +269,7 @@ Rectangle {
visible: root.enableFuzzySearch visible: root.enableFuzzySearch
} }
ListView { DankListView {
id: listView id: listView
width: parent.width width: parent.width

206
Widgets/DankFlickable.qml Normal file
View File

@@ -0,0 +1,206 @@
import QtQuick
import QtQuick.Controls
import qs.Common
Flickable {
id: flickable
property real mouseWheelSpeed: 12
property real momentumVelocity: 0
property bool isMomentumActive: false
property real friction: 0.95
property real minMomentumVelocity: 50
property real maxMomentumVelocity: 2500
// Internal: controls transient scrollbar visibility
property bool _scrollBarActive: false
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
WheelHandler {
id: wheelHandler
property real touchpadSpeed: 1.8
property real momentumRetention: 0.92
property real lastWheelTime: 0
property real momentum: 0
property var velocitySamples: []
function startMomentum() {
flickable.isMomentumActive = true;
momentumTimer.start();
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
// Activate scrollbar on any wheel interaction
flickable._scrollBarActive = true;
hideScrollBarTimer.restart();
let currentTime = Date.now();
let timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime;
const deltaY = event.angleDelta.y;
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
if (isMouseWheel) {
momentumTimer.stop();
flickable.isMomentumActive = false;
velocitySamples = [];
momentum = 0;
const lines = Math.floor(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed;
let newY = flickable.contentY + scrollAmount;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking)
flickable.cancelFlick();
flickable.contentY = newY;
} else {
momentumTimer.stop();
flickable.isMomentumActive = false;
let delta = 0;
if (event.pixelDelta.y !== 0) {
delta = event.pixelDelta.y * touchpadSpeed;
} else {
delta = event.angleDelta.y / 8 * touchpadSpeed;
}
velocitySamples.push({
"delta": delta,
"time": currentTime
});
velocitySamples = velocitySamples.filter((s) => {
return currentTime - s.time < 100;
});
if (velocitySamples.length > 1) {
let totalDelta = velocitySamples.reduce((sum, s) => {
return sum + s.delta;
}, 0);
let timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0)
flickable.momentumVelocity = Math.max(-flickable.maxMomentumVelocity,
Math.min(flickable.maxMomentumVelocity,
totalDelta / timeSpan * 1000));
}
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum;
} else {
momentum = 0;
}
let newY = flickable.contentY - delta;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking)
flickable.cancelFlick();
flickable.contentY = newY;
}
event.accepted = true;
}
onActiveChanged: {
if (!active) {
if (Math.abs(flickable.momentumVelocity) >= flickable.minMomentumVelocity) {
startMomentum();
} else {
velocitySamples = [];
flickable.momentumVelocity = 0;
}
}
}
}
// Show scrollbar while flicking / momentum
onMovementStarted: {
_scrollBarActive = true;
hideScrollBarTimer.stop();
}
onMovementEnded: hideScrollBarTimer.restart()
Timer {
id: momentumTimer
interval: 16
repeat: true
onTriggered: {
let newY = flickable.contentY - flickable.momentumVelocity * 0.016;
let maxY = Math.max(0, flickable.contentHeight - flickable.height);
if (newY < 0) {
flickable.contentY = 0;
stop();
flickable.isMomentumActive = false;
flickable.momentumVelocity = 0;
return;
} else if (newY > maxY) {
flickable.contentY = maxY;
stop();
flickable.isMomentumActive = false;
flickable.momentumVelocity = 0;
return;
}
flickable.contentY = newY;
flickable.momentumVelocity *= flickable.friction;
if (Math.abs(flickable.momentumVelocity) < 5) {
stop();
flickable.isMomentumActive = false;
flickable.momentumVelocity = 0;
}
}
}
NumberAnimation {
id: returnToBoundsAnimation
target: flickable
property: "contentY"
duration: 300
easing.type: Easing.OutQuad
}
// Styled vertical scrollbar (auto-hide, no track)
ScrollBar.vertical: ScrollBar {
id: vbar
policy: flickable.contentHeight > flickable.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
minimumSize: 0.08
implicitWidth: 10
interactive: true
hoverEnabled: true
z: 1000
opacity: (policy !== ScrollBar.AlwaysOff) && (vbar.pressed || vbar.hovered || vbar.active || flickable.moving || flickable.flicking || flickable.isMomentumActive || flickable._scrollBarActive) ? 1 : 0
visible: policy !== ScrollBar.AlwaysOff
Behavior on opacity { NumberAnimation { duration: 160; easing.type: Easing.OutQuad } }
contentItem: Rectangle {
implicitWidth: 6
radius: width / 2
color: vbar.pressed ? Theme.primary
: (vbar.hovered || vbar.active || flickable.moving || flickable.flicking || flickable.isMomentumActive || flickable._scrollBarActive ? Theme.outline : Theme.outlineMedium)
opacity: vbar.pressed ? 1 : (vbar.hovered || vbar.active || flickable.moving || flickable.flicking || flickable.isMomentumActive || flickable._scrollBarActive ? 1 : 0.6)
}
background: Item {}
}
Timer {
id: hideScrollBarTimer
interval: 1200
onTriggered: flickable._scrollBarActive = false
}
}

View File

@@ -1,220 +1,179 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
GridView { GridView {
id: gridView id: gridView
property int currentIndex: 0 // Kinetic scrolling momentum properties
property int columns: 4 property real momentumVelocity: 0
property bool adaptiveColumns: false property bool isMomentumActive: false
property int minCellWidth: 120 property real friction: 0.95
property int maxCellWidth: 160 property real minMomentumVelocity: 50
property int cellPadding: 8 property real maxMomentumVelocity: 2500
property real iconSizeRatio: 0.6
property int maxIconSize: 56
property int minIconSize: 32
property bool hoverUpdatesSelection: true
property bool keyboardNavigationActive: false
property int baseCellWidth: adaptiveColumns ? Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) : (width - Theme.spacingS * 2) / columns
property int baseCellHeight: baseCellWidth + 20
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
property int remainingSpace: width - (actualColumns * cellWidth)
signal keyboardNavigationReset()
signal itemClicked(int index, var modelData)
signal itemHovered(int index)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
function ensureVisible(index) {
if (index < 0 || index >= gridView.count)
return ;
var itemY = Math.floor(index / gridView.actualColumns) * gridView.cellHeight;
var itemBottom = itemY + gridView.cellHeight;
if (itemY < gridView.contentY)
gridView.contentY = itemY;
else if (itemBottom > gridView.contentY + gridView.height)
gridView.contentY = itemBottom - gridView.height;
}
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
clip: true
anchors.margins: Theme.spacingS
cellWidth: baseCellWidth
cellHeight: baseCellHeight
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
rightMargin: leftMargin
focus: true
interactive: true
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500 flickDeceleration: 1500
maximumFlickVelocity: 2000 maximumFlickVelocity: 2000
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.FollowBoundsBehavior boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0 pressDelay: 0
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
// Performance optimizations
cacheBuffer: Math.min(height * 2, 1000)
reuseItems: true
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler { WheelHandler {
id: wheelHandler id: wheelHandler
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
// Tunable parameters for responsive scrolling // Tunable parameters for responsive scrolling
property real mouseWheelSpeed: 20 // Higher = faster mouse wheel property real mouseWheelSpeed: 20
property real touchpadSpeed: 1.8 // Touchpad sensitivity // Higher = faster mouse wheel
property real touchpadSpeed: 1.8
// Touchpad sensitivity
property real momentumRetention: 0.92 property real momentumRetention: 0.92
property real lastWheelTime: 0 property real lastWheelTime: 0
property real momentum: 0 property real momentum: 0
property var velocitySamples: []
function startMomentum() {
isMomentumActive = true;
momentumTimer.start();
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => { onWheel: (event) => {
let currentTime = Date.now() let currentTime = Date.now();
let timeDelta = currentTime - lastWheelTime let timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime lastWheelTime = currentTime;
// Calculate scroll delta based on input type // Detect mouse wheel vs touchpad
let delta = 0 const deltaY = event.angleDelta.y;
if (event.pixelDelta.y !== 0) { const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
// Touchpad with pixel precision
delta = event.pixelDelta.y * touchpadSpeed if (isMouseWheel) {
// Fixed scrolling for mouse wheel - 2 cells per click
momentumTimer.stop();
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
const lines = Math.floor(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.15; // 0.15 cells per wheel click
let newY = contentY + scrollAmount;
newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking)
cancelFlick();
contentY = newY;
} else { } else {
// Mouse wheel - larger steps for faster scrolling // Touchpad - existing smooth kinetic scrolling
delta = event.angleDelta.y / 120 * cellHeight * 2 // 2 cells per wheel step // Stop any existing momentum
momentumTimer.stop();
isMomentumActive = false;
// Calculate scroll delta based on input type
let delta = 0;
if (event.pixelDelta.y !== 0)
// Touchpad with pixel precision
delta = event.pixelDelta.y * touchpadSpeed;
else
// Fallback for touchpad without pixel delta
delta = event.angleDelta.y / 120 * cellHeight * 1.2;
// Track velocity for momentum
velocitySamples.push({
"delta": delta,
"time": currentTime
});
velocitySamples = velocitySamples.filter((s) => {
return currentTime - s.time < 100;
});
// Calculate momentum velocity from samples
if (velocitySamples.length > 1) {
let totalDelta = velocitySamples.reduce((sum, s) => {
return sum + s.delta;
}, 0);
let timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0)
momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000));
}
// Apply momentum for touchpad (smooth continuous scrolling)
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum;
} else {
momentum = 0;
}
// Apply scrolling with proper bounds checking
let newY = contentY - delta;
newY = Math.max(0, Math.min(contentHeight - height, newY));
// Cancel any conflicting flicks and apply new position
if (flicking)
cancelFlick();
contentY = newY;
} }
// Apply momentum for touchpad (smooth continuous scrolling) event.accepted = true;
if (event.pixelDelta.y !== 0 && timeDelta < 50) { }
momentum = momentum * momentumRetention + delta * 0.15 onActiveChanged: {
delta += momentum if (!active && Math.abs(momentumVelocity) >= minMomentumVelocity) {
} else { startMomentum();
momentum = 0 } else if (!active) {
velocitySamples = [];
momentumVelocity = 0;
} }
// Apply scrolling with proper bounds checking
let newY = contentY - delta
newY = Math.max(0, Math.min(
contentHeight - height, newY))
// Cancel any conflicting flicks and apply new position
if (flicking) {
cancelFlick()
}
contentY = newY
event.accepted = true
} }
} }
ScrollBar.vertical: ScrollBar { // Physics-based momentum timer for kinetic scrolling
policy: ScrollBar.AsNeeded Timer {
} id: momentumTimer
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: gridView.cellWidth - cellPadding
height: gridView.cellHeight - cellPadding
radius: Theme.cornerRadiusLarge
color: currentIndex === index ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
border.color: currentIndex === index ? Theme.primarySelected : Theme.outlineMedium
border.width: currentIndex === index ? 2 : 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingS
Item {
property int iconSize: Math.min(maxIconSize, Math.max(minIconSize, gridView.cellWidth * iconSizeRatio))
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
IconImage {
id: iconImg
anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadiusLarge
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: Math.min(28, parent.width * 0.5)
color: Theme.primary
font.weight: Font.Bold
}
}
interval: 16 // ~60 FPS
repeat: true
onTriggered: {
// Apply velocity to position
let newY = contentY - momentumVelocity * 0.016;
let maxY = Math.max(0, contentHeight - height);
// Stop momentum at boundaries instead of bouncing
if (newY < 0) {
contentY = 0;
stop();
isMomentumActive = false;
momentumVelocity = 0;
return;
} else if (newY > maxY) {
contentY = maxY;
stop();
isMomentumActive = false;
momentumVelocity = 0;
return;
} }
StyledText { contentY = newY;
anchors.horizontalCenter: parent.horizontalCenter
width: gridView.cellWidth - 12 // Apply friction
text: model.name || "" momentumVelocity *= friction;
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText // Stop if velocity too low
font.weight: Font.Medium if (Math.abs(momentumVelocity) < 5) {
elide: Text.ElideRight stop();
horizontalAlignment: Text.AlignHCenter isMomentumActive = false;
maximumLineCount: 2 momentumVelocity = 0;
wrapMode: Text.WordWrap
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (hoverUpdatesSelection && !keyboardNavigationActive)
currentIndex = index;
itemHovered(index);
}
onPositionChanged: {
keyboardNavigationReset();
}
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
itemClicked(index, model);
} else if (mouse.button === Qt.RightButton) {
var globalPos = mapToGlobal(mouse.x, mouse.y);
itemRightClicked(index, model, globalPos.x, globalPos.y);
}
} }
} }
} }
} // Smooth return to bounds animation
NumberAnimation {
id: returnToBoundsAnimation
target: gridView
property: "contentY"
duration: 300
easing.type: Easing.OutQuad
}
}

View File

@@ -1,219 +1,212 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
ListView { ListView {
id: listView id: listView
property int itemHeight: 72 property real mouseWheelSpeed: 12
property int iconSize: 56
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: true
property bool keyboardNavigationActive: false
signal keyboardNavigationReset() // Simple position preservation
signal itemClicked(int index, var modelData) property real savedY: 0
signal itemHovered(int index) property bool justChanged: false
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY) property bool isUserScrolling: false
function ensureVisible(index) { // Kinetic scrolling momentum properties
if (index < 0 || index >= count) property real momentumVelocity: 0
return ; property bool isMomentumActive: false
property real friction: 0.95
property real minMomentumVelocity: 50
property real maxMomentumVelocity: 2500
var itemY = index * (itemHeight + itemSpacing);
var itemBottom = itemY + itemHeight;
if (itemY < contentY)
contentY = itemY;
else if (itemBottom > contentY + height)
contentY = itemBottom - height;
}
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
clip: true
anchors.margins: itemSpacing
spacing: itemSpacing
focus: true
interactive: true
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
flickDeceleration: 1500 flickDeceleration: 1500
maximumFlickVelocity: 2000 maximumFlickVelocity: 2000
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.FollowBoundsBehavior boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0 pressDelay: 0
flickableDirection: Flickable.VerticalFlick flickableDirection: Flickable.VerticalFlick
onMovementStarted: isUserScrolling = true
onMovementEnded: isUserScrolling = false
// Performance optimizations onContentYChanged: {
cacheBuffer: Math.min(height * 2, 1000) if (!justChanged && isUserScrolling) {
reuseItems: true savedY = contentY;
}
justChanged = false;
}
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling // Restore position when model changes
onModelChanged: {
justChanged = true;
contentY = savedY;
}
WheelHandler { WheelHandler {
id: wheelHandler id: wheelHandler
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
// Tunable parameters for responsive scrolling // Tunable parameters for responsive scrolling
property real mouseWheelSpeed: 20 // Higher = faster mouse wheel property real touchpadSpeed: 1.8 // Touchpad sensitivity
property real touchpadSpeed: 1.8 // Touchpad sensitivity
property real momentumRetention: 0.92 property real momentumRetention: 0.92
property real lastWheelTime: 0 property real lastWheelTime: 0
property real momentum: 0 property real momentum: 0
property var velocitySamples: []
function startMomentum() {
isMomentumActive = true;
momentumTimer.start();
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => { onWheel: (event) => {
let currentTime = Date.now() isUserScrolling = true; // Mark as user interaction
let timeDelta = currentTime - lastWheelTime
lastWheelTime = currentTime
// Calculate scroll delta based on input type let currentTime = Date.now();
let delta = 0 let timeDelta = currentTime - lastWheelTime;
if (event.pixelDelta.y !== 0) { lastWheelTime = currentTime;
// Touchpad with pixel precision
delta = event.pixelDelta.y * touchpadSpeed // Detect mouse wheel vs touchpad, seems like assuming based on the increments is the only way in QT
const deltaY = event.angleDelta.y;
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
if (isMouseWheel) {
momentumTimer.stop();
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
const lines = Math.floor(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
let newY = listView.contentY + scrollAmount;
newY = Math.max(0, Math.min(listView.contentHeight - listView.height, newY));
if (listView.flicking)
listView.cancelFlick();
listView.contentY = newY;
savedY = newY;
} else { } else {
// Mouse wheel - larger steps for faster scrolling momentumTimer.stop();
delta = event.angleDelta.y / 120 * itemHeight * 2.5 // 2.5 items per wheel step isMomentumActive = false;
// Calculate scroll delta based on input type
let delta = 0;
if (event.pixelDelta.y !== 0) {
// Touchpad with pixel precision
delta = event.pixelDelta.y * touchpadSpeed;
} else {
// Fallback for touchpad without pixel delta
delta = event.angleDelta.y / 8 * touchpadSpeed;
}
// Track velocity for momentum
velocitySamples.push({
"delta": delta,
"time": currentTime
});
velocitySamples = velocitySamples.filter((s) => {
return currentTime - s.time < 100;
});
// Calculate momentum velocity from samples
if (velocitySamples.length > 1) {
let totalDelta = velocitySamples.reduce((sum, s) => {
return sum + s.delta;
}, 0);
let timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0)
momentumVelocity = Math.max(-maxMomentumVelocity,
Math.min(maxMomentumVelocity,
totalDelta / timeSpan * 1000));
}
// Apply momentum for touchpad (smooth continuous scrolling)
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum;
} else {
momentum = 0;
}
// Apply scrolling with proper bounds checking
let newY = listView.contentY - delta;
newY = Math.max(0, Math.min(listView.contentHeight - listView.height, newY));
// Cancel any conflicting flicks and apply new position
if (listView.flicking)
listView.cancelFlick();
listView.contentY = newY;
savedY = newY; // Update saved position
} }
// Apply momentum for touchpad (smooth continuous scrolling) event.accepted = true;
if (event.pixelDelta.y !== 0 && timeDelta < 50) { }
momentum = momentum * momentumRetention + delta * 0.15
delta += momentum onActiveChanged: {
} else { if (!active) {
momentum = 0 isUserScrolling = false;
// Start momentum if applicable (touchpad only)
if (Math.abs(momentumVelocity) >= minMomentumVelocity) {
startMomentum();
} else {
velocitySamples = [];
momentumVelocity = 0;
}
} }
// Apply scrolling with proper bounds checking
let newY = listView.contentY - delta
newY = Math.max(0, Math.min(
listView.contentHeight - listView.height, newY))
// Cancel any conflicting flicks and apply new position
if (listView.flicking) {
listView.cancelFlick()
}
listView.contentY = newY
event.accepted = true
} }
} }
ScrollBar.vertical: ScrollBar { // Physics-based momentum timer for kinetic scrolling (touchpad only)
policy: ScrollBar.AlwaysOn Timer {
} id: momentumTimer
interval: 16 // ~60 FPS
ScrollBar.horizontal: ScrollBar { repeat: true
policy: ScrollBar.AlwaysOff
} onTriggered: {
// Apply velocity to position
delegate: Rectangle { let newY = contentY - momentumVelocity * 0.016;
width: ListView.view.width let maxY = Math.max(0, contentHeight - height);
height: itemHeight
radius: Theme.cornerRadiusLarge // Stop momentum at boundaries instead of bouncing
color: ListView.isCurrentItem ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryHoverLight : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03) if (newY < 0) {
border.color: ListView.isCurrentItem ? Theme.primarySelected : Theme.outlineMedium contentY = 0;
border.width: ListView.isCurrentItem ? 2 : 1 savedY = 0;
stop();
Row { isMomentumActive = false;
anchors.fill: parent momentumVelocity = 0;
anchors.margins: Theme.spacingM return;
spacing: Theme.spacingL } else if (newY > maxY) {
contentY = maxY;
Item { savedY = maxY;
width: iconSize stop();
height: iconSize isMomentumActive = false;
anchors.verticalCenter: parent.verticalCenter momentumVelocity = 0;
return;
IconImage {
id: iconImg
anchors.fill: parent
source: (model.icon) ? Quickshell.iconPath(model.icon, SettingsData.iconTheme === "System Default" ? "" : SettingsData.iconTheme) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready
}
Rectangle {
anchors.fill: parent
visible: !iconImg.visible
color: Theme.surfaceLight
radius: Theme.cornerRadiusLarge
border.width: 1
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
font.pixelSize: iconSize * 0.4
color: Theme.primary
font.weight: Font.Bold
}
}
} }
Column { contentY = newY;
anchors.verticalCenter: parent.verticalCenter savedY = newY; // Keep updating saved position during momentum
width: parent.width - iconSize - Theme.spacingL
spacing: Theme.spacingXS // Apply friction
momentumVelocity *= friction;
StyledText {
width: parent.width // Stop if velocity too low
text: model.name || "" if (Math.abs(momentumVelocity) < 5) {
font.pixelSize: Theme.fontSizeLarge stop();
color: Theme.surfaceText isMomentumActive = false;
font.weight: Font.Medium momentumVelocity = 0;
elide: Text.ElideRight
}
StyledText {
width: parent.width
text: model.comment || "Application"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
elide: Text.ElideRight
visible: showDescription && model.comment && model.comment.length > 0
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
z: 10
onEntered: {
if (hoverUpdatesSelection && !keyboardNavigationActive)
currentIndex = index;
itemHovered(index);
}
onPositionChanged: {
keyboardNavigationReset();
}
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
itemClicked(index, model);
} else if (mouse.button === Qt.RightButton) {
var globalPos = mapToGlobal(mouse.x, mouse.y);
itemRightClicked(index, model, globalPos.x, globalPos.y);
}
} }
} }
} }
} // Smooth return to bounds animation
NumberAnimation {
id: returnToBoundsAnimation
target: listView
property: "contentY"
duration: 300
easing.type: Easing.OutQuad
}
}

View File

@@ -250,7 +250,7 @@ Item {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
ListView { DankListView {
id: searchResultsList id: searchResultsList
anchors.fill: parent anchors.fill: parent

View File

@@ -20,6 +20,7 @@ Column {
signal compactModeChanged(string widgetId, bool enabled) signal compactModeChanged(string widgetId, bool enabled)
width: parent.width width: parent.width
height: implicitHeight
spacing: Theme.spacingM spacing: Theme.spacingM
Row { Row {
@@ -258,6 +259,7 @@ Column {
drag.axis: Drag.YAxis drag.axis: Drag.YAxis
drag.minimumY: -delegateItem.height drag.minimumY: -delegateItem.height
drag.maximumY: itemsList.height drag.maximumY: itemsList.height
preventStealing: true
onPressed: { onPressed: {
delegateItem.z = 2; delegateItem.z = 2;
delegateItem.originalY = delegateItem.y; delegateItem.originalY = delegateItem.y;

View File

@@ -96,7 +96,7 @@ Popup {
height: parent.height - 120 // Leave space for header and description height: parent.height - 120 // Leave space for header and description
clip: true clip: true
ListView { DankListView {
id: widgetList id: widgetList
spacing: Theme.spacingS spacing: Theme.spacingS

View File

@@ -56,36 +56,35 @@ update_theme_settings() {
update_gtk_css() { update_gtk_css() {
local config_dir="$1" local config_dir="$1"
local import_line="@import url(\"$config_dir/gtk-4.0/dank-colors.css\");" local is_light="$2"
local shell_dir="$3"
echo "Updating GTK CSS imports..." echo "Updating GTK CSS..."
# Update GTK-4.0 # GTK-3.0: Copy the appropriate template file
local gtk3_css="$config_dir/gtk-3.0/gtk.css"
if [ "$is_light" = "true" ]; then
echo "Copying light GTK-3.0 template..."
cp "$shell_dir/templates/gtk3-colloid-light.css" "$gtk3_css"
else
echo "Copying dark GTK-3.0 template..."
cp "$shell_dir/templates/gtk3-colloid-dark.css" "$gtk3_css"
fi
echo "Updated GTK-3.0 CSS"
# GTK-4.0: Use simplified import
local gtk4_import="@import url(\"dank-colors.css\");"
local gtk4_css="$config_dir/gtk-4.0/gtk.css" local gtk4_css="$config_dir/gtk-4.0/gtk.css"
if [ -f "$gtk4_css" ]; then if [ -f "$gtk4_css" ]; then
# Remove existing import if present # Remove existing import if present
sed -i '/^@import url.*dank-colors\.css.*);$/d' "$gtk4_css" sed -i '/^@import url.*dank-colors\.css.*);$/d' "$gtk4_css"
# Add import at the top # Add import at the top
sed -i "1i\\$import_line" "$gtk4_css" sed -i "1i\\$gtk4_import" "$gtk4_css"
else else
# Create new gtk.css with import # Create new gtk.css with import
echo "$import_line" > "$gtk4_css" echo "$gtk4_import" > "$gtk4_css"
fi fi
echo "Updated GTK-4.0 CSS import" echo "Updated GTK-4.0 CSS import"
# Update GTK-3.0 with its own path
local gtk3_import="@import url(\"$config_dir/gtk-3.0/dank-colors.css\");"
local gtk3_css="$config_dir/gtk-3.0/gtk.css"
if [ -f "$gtk3_css" ]; then
# Remove existing import if present
sed -i '/^@import url.*dank-colors\.css.*);$/d' "$gtk3_css"
# Add import at the top
sed -i "1i\\$gtk3_import" "$gtk3_css"
else
# Create new gtk.css with import
echo "$gtk3_import" > "$gtk3_css"
fi
echo "Updated GTK-3.0 CSS import"
} }
update_qt_config() { update_qt_config() {
@@ -240,7 +239,7 @@ update_theme_settings "$color_scheme" "$ICON_THEME"
# Update GTK CSS imports if GTK theming is enabled # Update GTK CSS imports if GTK theming is enabled
if [ "$GTK_THEMING" = "true" ]; then if [ "$GTK_THEMING" = "true" ]; then
update_gtk_css "$CONFIG_DIR" update_gtk_css "$CONFIG_DIR" "$IS_LIGHT" "$SHELL_DIR"
echo "GTK theming updated" echo "GTK theming updated"
else else
echo "GTK theming disabled - skipping GTK CSS updates" echo "GTK theming disabled - skipping GTK CSS updates"

View File

@@ -1,3 +1,4 @@
//@ pragma UseQApplication
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -21,8 +22,7 @@ import qs.Services
ShellRoot { ShellRoot {
id: root id: root
WallpaperBackground { WallpaperBackground {}
}
Lock { Lock {
id: lock id: lock
@@ -36,7 +36,6 @@ ShellRoot {
delegate: TopBar { delegate: TopBar {
modelData: item modelData: item
} }
} }
Variants { Variants {
@@ -47,17 +46,12 @@ ShellRoot {
contextMenu: dockContextMenu contextMenu: dockContextMenu
windowsMenu: dockWindowsMenu windowsMenu: dockWindowsMenu
} }
} }
CentcomPopout { CentcomPopout {
id: centcomPopout id: centcomPopout
} }
SystemTrayContextMenu {
id: systemTrayContextMenu
}
DockContextMenu { DockContextMenu {
id: dockContextMenu id: dockContextMenu
} }
@@ -76,7 +70,6 @@ ShellRoot {
delegate: NotificationPopupManager { delegate: NotificationPopupManager {
modelData: item modelData: item
} }
} }
ControlCenterPopout { ControlCenterPopout {
@@ -141,7 +134,6 @@ ShellRoot {
ProcessListModal { ProcessListModal {
id: processListModal id: processListModal
} }
} }
IpcHandler { IpcHandler {
@@ -177,7 +169,6 @@ ShellRoot {
delegate: Toast { delegate: Toast {
modelData: item modelData: item
} }
} }
Variants { Variants {
@@ -186,7 +177,6 @@ ShellRoot {
delegate: VolumePopup { delegate: VolumePopup {
modelData: item modelData: item
} }
} }
Variants { Variants {
@@ -195,7 +185,6 @@ ShellRoot {
delegate: MicMutePopup { delegate: MicMutePopup {
modelData: item modelData: item
} }
} }
Variants { Variants {
@@ -204,7 +193,5 @@ ShellRoot {
delegate: BrightnessPopup { delegate: BrightnessPopup {
modelData: item modelData: item
} }
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff