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
Singleton {
id: root
readonly property string _homeUrl: StandardPaths.writableLocation(StandardPaths.HomeLocation)
@@ -23,8 +22,7 @@ Singleton {
property bool qtThemingEnabled: false
property bool systemThemeGenerationInProgress: false
property string matugenJson: ""
property var matugenColors: ({
})
property var matugenColors: ({})
property bool extractionRequested: false
property int colorUpdateTrigger: 0
property string lastWallpaperTimestamp: ""
@@ -48,7 +46,7 @@ Singleton {
property color accentHi: primary
property color accentLo: secondary
signal colorsUpdated()
signal colorsUpdated
function onLightModeChanged() {
if (matugenColors && Object.keys(matugenColors).length > 0) {
@@ -98,12 +96,12 @@ Singleton {
id: matugenCheck
command: ["which", "matugen"]
onExited: (code) => {
onExited: code => {
matugenAvailable = (code === 0);
if (!matugenAvailable) {
ToastService.wallpaperErrorStatus = "matugen_missing";
ToastService.showWarning("matugen not found - dynamic theming disabled");
return ;
return;
}
if (extractionRequested) {
fileChecker.running = true;
@@ -115,7 +113,7 @@ Singleton {
id: fileChecker
command: ["test", "-r", wallpaperPath]
onExited: (code) => {
onExited: code => {
if (code === 0) {
matugenProcess.running = true;
} else {
@@ -138,7 +136,7 @@ Singleton {
if (!out.length) {
ToastService.wallpaperErrorStatus = "error";
ToastService.showError("Wallpaper Processing Failed");
return ;
return;
}
try {
root.matugenJson = out;
@@ -156,7 +154,6 @@ Singleton {
stderr: StdioCollector {
id: matugenErr
}
}
function generateAppConfigs() {
@@ -168,16 +165,16 @@ Singleton {
generateGhosttyConfig();
if (gtkThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) {
generateGtkThemes();
}
if (qtThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) {
generateQtThemes();
generateSystemThemes();
} else if (qtThemingEnabled && typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) {
generateSystemThemes();
}
}
function generateNiriConfig() {
var dark = matugenColors.colors.dark;
if (!dark) return;
if (!dark)
return;
var bg = dark.background || "#1a1c1e";
var primary = dark.primary || "#42a5f5";
@@ -201,7 +198,8 @@ Singleton {
function generateGhosttyConfig() {
var dark = matugenColors.colors.dark;
var light = matugenColors.colors.light;
if (!dark || !light) return;
if (!dark || !light)
return;
var bg = dark.background || "#1a1c1e";
var fg = dark.on_background || "#e3e8ef";
@@ -280,14 +278,6 @@ palette = 15=${fg_b}`;
systemThemeGenerator.running = true;
}
function generateGtkThemes() {
generateSystemThemes();
}
function generateQtThemes() {
generateSystemThemes();
}
function restoreSystemThemes() {
const shellDir = root.shellDir;
if (!shellDir) {
@@ -307,7 +297,7 @@ palette = 15=${fg_b}`;
id: gtkAvailabilityChecker
command: ["bash", "-c", "command -v gsettings >/dev/null && [ -d " + configDir + "/gtk-3.0 -o -d " + configDir + "/gtk-4.0 ]"]
running: false
onExited: (exitCode) => {
onExited: exitCode => {
gtkThemingEnabled = (exitCode === 0);
}
}
@@ -316,7 +306,7 @@ palette = 15=${fg_b}`;
id: qtAvailabilityChecker
command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"]
running: false
onExited: (exitCode) => {
onExited: exitCode => {
qtThemingEnabled = (exitCode === 0);
}
}
@@ -333,7 +323,7 @@ palette = 15=${fg_b}`;
id: systemThemeStderr
}
onExited: (exitCode) => {
onExited: exitCode => {
systemThemeGenerationInProgress = false;
if (exitCode !== 0) {
@@ -354,7 +344,7 @@ palette = 15=${fg_b}`;
id: restoreThemeStderr
}
onExited: (exitCode) => {
onExited: exitCode => {
if (exitCode === 0) {
ToastService.showInfo("System themes restored to default");
} else {
@@ -362,6 +352,4 @@ palette = 15=${fg_b}`;
}
}
}
}

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modules.AppDrawer
import qs.Services
@@ -284,17 +285,49 @@ DankModal {
DankListView {
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.margins: Theme.spacingS
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
itemHeight: 60
iconSize: 40
showDescription: true
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
@@ -307,24 +340,170 @@ DankModal {
onKeyboardNavigationReset: {
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 {
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.margins: Theme.spacingS
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
columns: 4
adaptiveColumns: false
minCellWidth: 120
maxCellWidth: 160
iconSizeRatio: 0.55
maxIconSize: 48
currentIndex: appLauncher.selectedIndex
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
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) {
appLauncher.launchApp(modelData);
}
@@ -337,6 +516,103 @@ DankModal {
onKeyboardNavigationReset: {
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 {
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.margins: Theme.spacingS
visible: appLauncher.viewMode === "list"
model: appLauncher.model
currentIndex: appLauncher.selectedIndex
itemHeight: 72
iconSize: 56
showDescription: true
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
clip: true
spacing: itemSpacing
focus: true
interactive: true
cacheBuffer: Math.max(0, Math.min(height * 2, 1000))
reuseItems: true
onCurrentIndexChanged: {
if (keyboardNavigationActive)
ensureVisible(currentIndex);
}
onItemClicked: function(index, modelData) {
appLauncher.launchApp(modelData);
}
@@ -393,20 +425,170 @@ PanelWindow {
onKeyboardNavigationReset: {
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 {
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.margins: Theme.spacingS
visible: appLauncher.viewMode === "grid"
model: appLauncher.model
columns: 4
adaptiveColumns: false
currentIndex: appLauncher.selectedIndex
hoverUpdatesSelection: false
keyboardNavigationActive: appLauncher.keyboardNavigationActive
clip: true
cellWidth: baseCellWidth
cellHeight: baseCellHeight
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
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) {
appLauncher.launchApp(modelData);
}
@@ -419,6 +601,103 @@ PanelWindow {
onKeyboardNavigationReset: {
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
anchors.top: headerRow.bottom

View File

@@ -33,46 +33,90 @@ Item {
}
}
ScrollView {
// Output Tab - DankFlickable
DankFlickable {
width: parent.width
height: parent.height - 48
visible: audioTab.audioSubTab === 0
clip: true
contentHeight: outputColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column {
id: outputColumn
width: parent.width
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
height: parent.height - 48
visible: audioTab.audioSubTab === 1
clip: true
contentHeight: inputColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column {
id: inputColumn
width: parent.width
spacing: Theme.spacingL
Loader {
width: parent.width
sourceComponent: microphoneComponent
}
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 {
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
spacing: Theme.spacingM
@@ -117,10 +125,11 @@ Column {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bluetoothContextMenuWindow) {
bluetoothContextMenuWindow.deviceData = modelData;
let localPos = btMenuButtonArea.mapToItem(bluetoothContextMenuWindow.parentItem, btMenuButtonArea.width / 2, btMenuButtonArea.height);
bluetoothContextMenuWindow.show(localPos.x, localPos.y);
var contextMenu = root.findBluetoothContextMenu();
if (contextMenu) {
contextMenu.deviceData = modelData;
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 {
id: bluetoothTab
ScrollView {
property alias bluetoothContextMenuWindow: bluetoothContextMenuWindow
DankFlickable {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentHeight: mainColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingL
BluetoothToggle {
Loader {
width: parent.width
sourceComponent: toggleComponent
}
PairedDevicesList {
bluetoothContextMenuWindow: bluetoothContextMenuWindow
Loader {
width: parent.width
sourceComponent: pairedComponent
}
AvailableDevicesList {
Loader {
width: parent.width
sourceComponent: availableComponent
}
}
}
BluetoothContextMenu {
id: bluetoothContextMenuWindow
parentItem: bluetoothTab
}
@@ -57,7 +63,26 @@ Item {
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.Widgets
ScrollView {
Item {
id: displayTab
property var brightnessDebounceTimer
@@ -16,19 +16,61 @@ ScrollView {
brightnessDebounceTimer: Timer {
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
onTriggered: {
BrightnessService.setBrightnessInternal(pendingValue);
}
}
DankFlickable {
anchors.fill: parent
clip: true
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 {
width: parent.width
spacing: Theme.spacingM
@@ -48,12 +90,10 @@ ScrollView {
rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable
onSliderValueChanged: function(newValue) {
brightnessDebounceTimer.pendingValue = newValue;
brightnessDebounceTimer.restart();
}
onSliderDragFinished: function(finalValue) {
brightnessDebounceTimer.stop();
BrightnessService.setBrightnessInternal(finalValue);
}
@@ -66,9 +106,11 @@ ScrollView {
visible: BrightnessService.ddcAvailable && !BrightnessService.laptopBacklightAvailable
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
Component {
id: settingsComponent
Column {
width: parent.width
spacing: Theme.spacingM
@@ -110,12 +152,10 @@ ScrollView {
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: nightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -129,7 +169,6 @@ ScrollView {
}
}
}
}
Rectangle {
@@ -158,12 +197,10 @@ ScrollView {
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
id: lightModeToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
@@ -177,40 +214,9 @@ ScrollView {
duration: Theme.shortDuration
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
onClicked: {
logoutDialog.close()
Quickshell.execDetached(["niri", "msg", "action", "quit", "-s"])
NiriService.quit()
}
}
}

View File

@@ -2,144 +2,27 @@ import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
ListView {
DankListView {
id: root
property alias count: root.count
readonly property real listContentHeight: root.contentHeight
readonly property bool atYBeginning: root.contentY === 0
property real stableY: 0
property bool isUserScrolling: false
property alias listContentHeight: root.contentHeight
width: parent.width
height: parent.height
clip: true
model: NotificationService.groupedNotifications
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 {
visible: root.count === 0
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 {
notificationGroup: modelData
}
}

View File

@@ -49,9 +49,7 @@ Column {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("name");
processListView.restoreAnchor();
}
}
@@ -90,9 +88,7 @@ Column {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("cpu");
processListView.restoreAnchor();
}
}
@@ -131,9 +127,7 @@ Column {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("memory");
processListView.restoreAnchor();
}
}
@@ -173,9 +167,7 @@ Column {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
processListView.captureAnchor();
SysMonitorService.setSortBy("pid");
processListView.restoreAnchor();
}
}
@@ -211,9 +203,7 @@ Column {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
processListView.captureAnchor();
SysMonitorService.toggleSortOrder();
processListView.restoreAnchor();
}
}
@@ -228,101 +218,21 @@ Column {
}
ListView {
DankListView {
id: processListView
property real stableY: 0
property bool isUserScrolling: false
property bool isScrollBarDragging: false
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
height: parent.height - columnHeaders.height
clip: true
spacing: 4
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 {
process: modelData
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,34 +5,48 @@ import qs.Common
import qs.Services
import qs.Widgets
ScrollView {
Item {
id: appearanceTab
// 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 = appearanceTab.contentItem
let newY = flickable.contentY - delta
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
flickable.contentY = newY
event.accepted = true
}
}
contentWidth: availableWidth
contentHeight: column.implicitHeight + Theme.spacingXL
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingXL
clip: true
contentHeight: mainColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column {
id: column
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
topPadding: Theme.spacingL
bottomPadding: 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
}
}
}
// Display Settings Component
Component {
id: displayComponent
StyledRect {
width: parent.width
@@ -273,8 +287,12 @@ ScrollView {
}
}
}
}
// Transparency Settings Component
Component {
id: transparencyComponent
StyledRect {
width: parent.width
@@ -391,8 +409,12 @@ ScrollView {
}
}
}
}
// Theme Color Component
Component {
id: themeComponent
StyledRect {
width: parent.width
@@ -759,8 +781,12 @@ ScrollView {
}
}
}
}
// System App Theming Component
Component {
id: systemThemingComponent
StyledRect {
width: parent.width
@@ -828,9 +854,7 @@ ScrollView {
}
}
}
}
Process {

View File

@@ -4,33 +4,43 @@ import Quickshell.Widgets
import qs.Common
import qs.Widgets
ScrollView {
Item {
id: launcherTab
// 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 = launcherTab.contentItem
let newY = flickable.contentY - delta
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
flickable.contentY = newY
event.accepted = true
}
}
contentHeight: column.implicitHeight + Theme.spacingXL
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingXL
clip: true
contentHeight: mainColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column {
id: column
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
topPadding: Theme.spacingL
bottomPadding: Theme.spacingXL
Loader {
width: parent.width
sourceComponent: appLauncherComponent
}
Loader {
width: parent.width
sourceComponent: dockComponent
}
Loader {
width: parent.width
sourceComponent: recentlyUsedComponent
}
}
}
// App Launcher Component
Component {
id: appLauncherComponent
StyledRect {
width: parent.width
@@ -159,8 +169,12 @@ ScrollView {
}
}
}
}
// Dock Component
Component {
id: dockComponent
StyledRect {
width: parent.width
@@ -262,6 +276,11 @@ ScrollView {
}
}
}
// Recently Used Apps Component
Component {
id: recentlyUsedComponent
StyledRect {
width: parent.width
@@ -474,9 +493,6 @@ ScrollView {
}
}
}
}
}

View File

@@ -7,35 +7,47 @@ import qs.Modals
import qs.Services
import qs.Widgets
ScrollView {
Item {
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 wallpaperBrowser: wallpaperBrowserLoader.item
contentHeight: column.implicitHeight
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingXL
clip: true
contentHeight: mainColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column {
id: 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 {
width: parent.width
height: profileSection.implicitHeight + Theme.spacingL * 2
@@ -294,8 +306,12 @@ ScrollView {
}
}
}
}
// Wallpaper Component
Component {
id: wallpaperComponent
StyledRect {
width: parent.width
@@ -491,8 +507,12 @@ ScrollView {
}
}
}
}
// Dynamic Theme Component
Component {
id: dynamicThemeComponent
StyledRect {
width: parent.width
@@ -568,9 +588,7 @@ ScrollView {
}
}
}
}
LazyLoader {

View File

@@ -3,32 +3,39 @@ import QtQuick.Controls
import qs.Common
import qs.Widgets
ScrollView {
Item {
id: timeWeatherTab
// 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 = timeWeatherTab.contentItem
let newY = flickable.contentY - delta
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY))
flickable.contentY = newY
event.accepted = true
}
}
contentHeight: column.implicitHeight
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingXL
clip: true
contentHeight: mainColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column {
id: column
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
Loader {
width: parent.width
sourceComponent: timeComponent
}
Loader {
width: parent.width
sourceComponent: weatherComponent
}
}
}
// Time Format Component
Component {
id: timeComponent
StyledRect {
width: parent.width
height: timeSection.implicitHeight + Theme.spacingL * 2
@@ -76,8 +83,12 @@ ScrollView {
}
}
}
}
// Weather Component
Component {
id: weatherComponent
StyledRect {
width: parent.width
@@ -171,9 +182,6 @@ ScrollView {
}
}
}
}
}

View File

@@ -3,23 +3,9 @@ import QtQuick.Controls
import qs.Common
import qs.Widgets
ScrollView {
Item {
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: [{
"id": "launcherButton",
"text": "App Launcher",
@@ -305,8 +291,6 @@ ScrollView {
return widgets;
}
contentHeight: column.implicitHeight + Theme.spacingXL
clip: true
Component.onCompleted: {
if (!SettingsData.topBarLeftWidgets || SettingsData.topBarLeftWidgets.length === 0)
SettingsData.setTopBarLeftWidgets(defaultLeftWidgets);
@@ -347,13 +331,45 @@ ScrollView {
});
}
Column {
id: column
DankFlickable {
anchors.fill: parent
anchors.topMargin: Theme.spacingL
anchors.bottomMargin: Theme.spacingXL
clip: true
contentHeight: mainColumn.height
contentWidth: width
mouseWheelSpeed: 20
Column {
id: mainColumn
width: parent.width
spacing: Theme.spacingXL
topPadding: Theme.spacingL
bottomPadding: 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 {
width: parent.width
@@ -406,7 +422,6 @@ ScrollView {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
@@ -427,7 +442,6 @@ ScrollView {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.color {
@@ -435,12 +449,14 @@ ScrollView {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
// Message Component
Component {
id: messageComponent
Rectangle {
width: parent.width
@@ -449,9 +465,6 @@ ScrollView {
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.width: 1
visible: true
opacity: 1
z: 1
StyledText {
id: messageText
@@ -463,8 +476,12 @@ ScrollView {
width: parent.width - Theme.spacingM * 2
wrapMode: Text.WordWrap
}
}
}
// Sections Component
Component {
id: sectionsComponent
Column {
width: parent.width
@@ -568,8 +585,12 @@ ScrollView {
}
}
}
}
}
// Workspace Settings Component
Component {
id: workspaceComponent
StyledRect {
width: parent.width
@@ -604,7 +625,6 @@ ScrollView {
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
@@ -626,44 +646,10 @@ ScrollView {
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 {
id: widgetSelectionPopup

View File

@@ -1,13 +1,16 @@
import QtQuick
import Quickshell
import Quickshell.Services.SystemTray
import qs.Common
Rectangle {
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
signal menuRequested(var menu, var item, real x, real y)
width: calculatedWidth
height: 30
@@ -84,16 +87,19 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem)
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);
return
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
SystemTrayBar {
onMenuRequested: (menu, item, x, y) => {
systemTrayContextMenu.currentTrayMenu = menu;
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;
}
}
parentWindow: root
parentScreen: root.screen
}
}

View File

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

View File

@@ -23,24 +23,31 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
<div align="center">
### Application Launcher
<img src="https://github.com/user-attachments/assets/2da00ea1-8921-4473-a2a9-44a44535a822" width="450" alt="Spotlight Launcher" />
### System Monitor
<img src="https://github.com/user-attachments/assets/b3c817ec-734d-4974-929f-2d11a1065349" width="600" alt="System Monitor" />
### Widget Customization
<img src="https://github.com/user-attachments/assets/903f7c60-146f-4fb3-a75d-a4823828f298" width="500" alt="Widget Customization" />
### Lock Screen
<img src="https://github.com/user-attachments/assets/3fa07de2-c1b0-4e57-8f25-3830ac6baf4f" width="600" alt="Lock Screen" />
### Dynamic Theming
<img src="https://github.com/user-attachments/assets/1994e616-f9d9-424a-9f60-6f06708bf12e" width="700" alt="Auto Theme" />
### Notification Center
<img src="https://github.com/user-attachments/assets/07cbde9a-0242-4989-9f97-5765c6458c85" width="350" alt="Notification Center" />
### Dock
<img src="https://github.com/user-attachments/assets/e6999daf-f7bf-4329-98fa-0ce4f0e7219c" width="400" alt="Dock" />
</div>
@@ -50,6 +57,7 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
## What's Inside
**Core Widgets:**
- **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.
- **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
**Features:**
- Dynamic wallpaper-based theming with matugen integration
- Numerous IPCs to trigger actions and open various modals.
- Calendar integration with [khal](https://github.com/pimutils/khal)
@@ -83,9 +92,10 @@ A modern Wayland desktop shell built with [Quickshell](https://quickshell.org/)
### Quick Start
*If you do not already have niri, see [#]
\*If you do not already have niri, see [#]
**Dependencies:**
```bash
# Arch Linux
paru -S quickshell-git ttf-material-symbols-variable-git inter-font ttf-fira-code
@@ -99,6 +109,7 @@ fc-cache -f
```
**Get the shell:**
```bash
# Arch linux available via AUR
paru -S dankmaterialshell-git
@@ -114,6 +125,7 @@ qs -c DankMaterialShell
<details><summary>Font Installation</summary>
**Material Symbols (Required):**
```bash
# Manual installation
mkdir -p ~/.local/share/fonts
@@ -125,6 +137,7 @@ paru -S ttf-material-symbols-variable-git
```
**Typography (Recommended):**
```bash
# Inter Variable Font
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>
**Enhanced Functionality:**
```bash
# Arch Linux
pacman -S cava wl-clipboard cliphist ddcutil brightnessctl qt5ct qt6ct
pacman -S cava wl-clipboard cliphist ddcutil brightnessctl
paru -S matugen
# 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 heus-sueh/packages && sudo dnf install matugen
```
**What you get:**
- `matugen`: Wallpaper-based dynamic theming
- `ddcutil`: External monitor brightness control
- `brightnessctl`: Laptop display brightness
- `wl-clipboard`: Required for copying various elements to clipboard.
- `qt5ct/qt6ct`: Qt application theming
- `cava`: Audio visualizer
- `cliphist`: Clipboard history
@@ -250,45 +264,88 @@ qs -c DankMaterialShell ipc call mpris next
### 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:**
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
# Some default install settings for colloid
./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
[Settings]
gtk-theme-name=Colloid
```
**Qt Apps:**
```bash
# Install Breeze
pacman -S breeze breeze5 # Arch
sudo dnf install breeze # Fedora
# Configure qt5ct/qt6ct
echo 'style=Breeze' >> ~/.config/qt5ct/qt5ct.conf
You have **two** paths for QT theming, first path is to use **gtk3**. To do that, add the following to your niri config.
```kdl
environment {
// Add to existing environment block
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
}
```
**Dynamic Theming:**
Enable wallpaper-based theming in **Settings → Appearance → System App Theming** after installing matugen.
**Done** - if you're not happy with this and wish to use Breeze or another QT theme then continue on.
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
**Ghostty users** can add automatic color theming:
```bash
echo "config-file = ./config-dankcolors" >> ~/.config/ghostty/config
```
## 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>
**Install dependencies:**
```bash
# Arch
pacman -S vdirsyncer khal python-aiohttp-oauthlib
@@ -298,6 +355,7 @@ sudo dnf install python3-vdirsyncer khal python3-aiohttp-oauthlib
```
**Configure vdirsyncer** (`~/.vdirsyncer/config`):
```ini
[general]
status_path = "~/.calendars/status"
@@ -322,6 +380,7 @@ fileext = ".ics"
```
**Setup sync:**
```bash
vdirsyncer sync
khal configure
@@ -338,6 +397,7 @@ crontab -e
All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or more intuitively the built-in settings modal.
**Key configuration areas:**
- Widget positioning and behavior
- Theme and color preferences
- Time format, weather units and location
@@ -348,12 +408,14 @@ All settings are configurable in `~/.config/DankMaterialShell/settings.json`, or
## Troubleshooting
**Common issues:**
- **Missing icons:** Verify Material Symbols font installation with `fc-list | grep Material`
- **No dynamic theming:** Install matugen and enable in settings
- **Qt apps not themed:** Configure qt5ct/qt6ct and set QT_QPA_PLATFORMTHEME
- **Calendar not syncing:** Check vdirsyncer credentials and network connectivity
**Getting help:**
- Check the [issues](https://github.com/bbedward/DankMaterialShell/issues) for known problems
- Share logs from `qs -c DankMaterialShell` for debugging
- 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.
**Areas that need attention:**
- More widget options and customization
- Additional compositor compatibility
- Performance optimizations

View File

@@ -9,6 +9,7 @@ Singleton {
id: root
// Workspace management
property var workspaces: ({})
property var allWorkspaces: []
property int focusedWorkspaceIndex: 0
property string focusedWorkspaceId: ""
@@ -29,238 +30,164 @@ Singleton {
// Feature availability
property bool niriAvailable: false
Component.onCompleted: {
console.log("NiriService: Component.onCompleted - initializing service")
checkNiriAvailability()
}
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
Component.onCompleted: checkNiriAvailability()
// Check if niri is available
Process {
id: niriCheck
command: ["which", "niri"]
command: ["test", "-S", root.socketPath]
onExited: (exitCode) => {
root.niriAvailable = exitCode === 0
onExited: exitCode => {
root.niriAvailable = exitCode === 0;
if (root.niriAvailable) {
console.log("NiriService: niri found, starting event stream and loading initial data")
eventStreamProcess.running = true
loadInitialWorkspaceData()
} else {
console.log("NiriService: niri not found, workspace features disabled")
eventStreamSocket.connected = true;
}
}
}
function checkNiriAvailability() {
niriCheck.running = true
niriCheck.running = true;
}
// Load initial workspace data
Process {
id: initialDataQuery
command: ["niri", "msg", "-j", "workspaces"]
running: false
Socket {
id: eventStreamSocket
path: root.socketPath
connected: false
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
onConnectionStateChanged: {
if (connected) {
write('"EventStream"\n');
}
}
parser: SplitParser {
onRead: line => {
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 })
const event = JSON.parse(line);
handleNiriEvent(event);
} catch (e) {
console.warn("NiriService: Failed to parse initial workspace data:", e)
}
console.warn("NiriService: Failed to parse event:", line, e);
}
}
}
}
// Load initial windows data
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")
Socket {
id: requestSocket
path: root.socketPath
connected: root.niriAvailable
}
} 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 {
const event = JSON.parse(data.trim())
handleNiriEvent(event)
} catch (e) {
console.warn("NiriService: Failed to parse event:", data, e)
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0 && root.niriAvailable) {
console.warn("NiriService: Event stream exited with code", exitCode, "restarting immediately")
eventStreamProcess.running = true
}
}
}
function handleNiriEvent(event) {
if (event.WorkspacesChanged) {
handleWorkspacesChanged(event.WorkspacesChanged)
handleWorkspacesChanged(event.WorkspacesChanged);
} else if (event.WorkspaceActivated) {
handleWorkspaceActivated(event.WorkspaceActivated)
handleWorkspaceActivated(event.WorkspaceActivated);
} else if (event.WindowsChanged) {
handleWindowsChanged(event.WindowsChanged)
handleWindowsChanged(event.WindowsChanged);
} else if (event.WindowClosed) {
handleWindowClosed(event.WindowClosed)
handleWindowClosed(event.WindowClosed);
} else if (event.WindowFocusChanged) {
handleWindowFocusChanged(event.WindowFocusChanged)
handleWindowFocusChanged(event.WindowFocusChanged);
} else if (event.WindowOpenedOrChanged) {
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged);
} else if (event.OverviewOpenedOrClosed) {
handleOverviewChanged(event.OverviewOpenedOrClosed)
handleOverviewChanged(event.OverviewOpenedOrClosed);
}
}
function handleWorkspacesChanged(data) {
allWorkspaces = [...data.workspaces].sort((a, b) => a.idx - b.idx)
const workspaces = {};
// Update focused workspace
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused)
if (focusedWorkspaceIndex >= 0) {
var focusedWs = allWorkspaces[focusedWorkspaceIndex]
focusedWorkspaceId = focusedWs.id
currentOutput = focusedWs.output || ""
} else {
focusedWorkspaceIndex = 0
focusedWorkspaceId = ""
for (const ws of data.workspaces) {
workspaces[ws.id] = ws;
}
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) {
// Update focused workspace
focusedWorkspaceId = data.id
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id)
const ws = root.workspaces[data.id];
if (!ws)
return;
const output = ws.output;
for (const id in root.workspaces) {
const workspace = root.workspaces[id];
const got_activated = workspace.id === data.id;
if (workspace.output === output) {
workspace.is_active = got_activated;
}
if (data.focused) {
workspace.is_focused = got_activated;
}
}
focusedWorkspaceId = data.id;
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id);
if (focusedWorkspaceIndex >= 0) {
var activatedWs = allWorkspaces[focusedWorkspaceIndex]
// Update workspace states properly
// First, deactivate all workspaces on this output
for (var i = 0; i < allWorkspaces.length; i++) {
if (allWorkspaces[i].output === activatedWs.output) {
allWorkspaces[i].is_active = false
allWorkspaces[i].is_focused = false
}
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || "";
}
// Then activate the new workspace
allWorkspaces[focusedWorkspaceIndex].is_active = true
allWorkspaces[focusedWorkspaceIndex].is_focused = data.focused || false
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx);
currentOutput = activatedWs.output || ""
updateCurrentOutputWorkspaces()
// Force property change notifications
allWorkspacesChanged()
} else {
focusedWorkspaceIndex = 0
}
updateCurrentOutputWorkspaces();
workspacesChanged();
}
function handleWindowsChanged(data) {
windows = [...data.windows].sort((a, b) => a.id - b.id)
updateFocusedWindow()
windows = [...data.windows].sort((a, b) => a.id - b.id);
updateFocusedWindow();
}
function handleWindowClosed(data) {
windows = windows.filter(w => w.id !== data.id)
updateFocusedWindow()
windows = windows.filter(w => w.id !== data.id);
updateFocusedWindow();
}
function handleWindowFocusChanged(data) {
if (data.id) {
focusedWindowId = data.id
focusedWindowIndex = windows.findIndex(w => w.id === data.id)
focusedWindowId = data.id;
focusedWindowIndex = windows.findIndex(w => w.id === data.id);
} else {
focusedWindowId = ""
focusedWindowIndex = -1
focusedWindowId = "";
focusedWindowIndex = -1;
}
updateFocusedWindow()
updateFocusedWindow();
}
function handleWindowOpenedOrChanged(data) {
if (!data.window) return;
if (!data.window)
return;
const window = data.window;
const existingIndex = windows.findIndex(w => w.id === window.id);
if (existingIndex >= 0) {
// Update existing window - create new array to trigger property change
let updatedWindows = [...windows];
updatedWindows[existingIndex] = window;
windows = updatedWindows.sort((a, b) => a.id - b.id);
} else {
// Add new window
windows = [...windows, window].sort((a, b) => a.id - b.id);
}
// Update focused window if this window is focused
if (window.is_focused) {
focusedWindowId = window.id;
focusedWindowIndex = windows.findIndex(w => w.id === window.id);
@@ -268,110 +195,105 @@ Singleton {
updateFocusedWindow();
// Emit signal for other services to listen to
windowOpenedOrChanged(window);
}
function handleOverviewChanged(data) {
inOverview = data.is_open
inOverview = data.is_open;
}
function updateCurrentOutputWorkspaces() {
if (!currentOutput) {
currentOutputWorkspaces = allWorkspaces
return
currentOutputWorkspaces = allWorkspaces;
return;
}
// Filter workspaces for current output
var outputWs = allWorkspaces.filter(w => w.output === currentOutput)
currentOutputWorkspaces = outputWs
var outputWs = allWorkspaces.filter(w => w.output === currentOutput);
currentOutputWorkspaces = outputWs;
}
function updateFocusedWindow() {
if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.length) {
var focusedWin = windows[focusedWindowIndex]
focusedWindowTitle = focusedWin.title || "(Unnamed window)"
var focusedWin = windows[focusedWindowIndex];
focusedWindowTitle = focusedWin.title || "(Unnamed window)";
} else {
focusedWindowTitle = "(No active window)"
focusedWindowTitle = "(No active window)";
}
}
// Public API functions
function switchToWorkspace(workspaceId) {
if (!niriAvailable) return false
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()])
return true
function send(request) {
if (!niriAvailable || !requestSocket.connected)
return false;
requestSocket.write(JSON.stringify(request) + "\n");
return true;
}
function switchToWorkspaceByIndex(index) {
if (!niriAvailable || index < 0 || index >= allWorkspaces.length) return false
var workspace = allWorkspaces[index]
return switchToWorkspace(workspace.id)
function switchToWorkspace(workspaceIndex) {
return send({
Action: {
FocusWorkspace: {
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() {
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() {
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
return allWorkspaces[focusedWorkspaceIndex].idx + 1
return allWorkspaces[focusedWorkspaceIndex].idx + 1;
}
return 1
return 1;
}
function focusWindow(windowId) {
if (!niriAvailable) return false
console.log("NiriService: Focusing window with command:", ["niri", "msg", "action", "focus-window", "--id", windowId.toString()])
Quickshell.execDetached(["niri", "msg", "action", "focus-window", "--id", windowId.toString()])
return true
return send({
Action: {
FocusWindow: {
id: windowId
}
}
});
}
function closeWindow(windowId) {
if (!niriAvailable) return false
return send({
Action: {
CloseWindow: {
id: windowId
}
}
});
}
console.log("NiriService: Closing window with command:", ["niri", "msg", "action", "close-window", "--id", windowId.toString()])
Quickshell.execDetached(["niri", "msg", "action", "close-window", "--id", windowId.toString()])
return true
function quit() {
return send({
Action: {
Quit: {
skip_confirmation: true
}
}
});
}
function getWindowsByAppId(appId) {
if (!appId) return []
return windows.filter(w => w.app_id && w.app_id.toLowerCase() === appId.toLowerCase())
if (!appId)
return [];
return windows.filter(w => w.app_id && w.app_id.toLowerCase() === appId.toLowerCase());
}
function getRunningAppIds() {
var appIds = new Set()
var appIds = new Set();
windows.forEach(w => {
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
}
ListView {
DankListView {
id: listView
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,60 +1,16 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
GridView {
id: gridView
property int currentIndex: 0
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: 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)
// Kinetic scrolling momentum properties
property real momentumVelocity: 0
property bool isMomentumActive: false
property real friction: 0.95
property real minMomentumVelocity: 50
property real maxMomentumVelocity: 2500
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
maximumFlickVelocity: 2000
boundsBehavior: Flickable.StopAtBounds
@@ -62,159 +18,162 @@ GridView {
pressDelay: 0
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 {
id: wheelHandler
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
// Tunable parameters for responsive scrolling
property real mouseWheelSpeed: 20 // Higher = faster mouse wheel
property real touchpadSpeed: 1.8 // Touchpad sensitivity
property real mouseWheelSpeed: 20
// Higher = faster mouse wheel
property real touchpadSpeed: 1.8
// Touchpad sensitivity
property real momentumRetention: 0.92
property real lastWheelTime: 0
property real momentum: 0
property var velocitySamples: []
function startMomentum() {
isMomentumActive = true;
momentumTimer.start();
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
let currentTime = Date.now()
let timeDelta = currentTime - lastWheelTime
lastWheelTime = currentTime
let currentTime = Date.now();
let timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime;
// Detect mouse wheel vs touchpad
const deltaY = event.angleDelta.y;
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
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 {
// Touchpad - existing smooth kinetic scrolling
// Stop any existing momentum
momentumTimer.stop();
isMomentumActive = false;
// Calculate scroll delta based on input type
let delta = 0
if (event.pixelDelta.y !== 0) {
let delta = 0;
if (event.pixelDelta.y !== 0)
// Touchpad with pixel precision
delta = event.pixelDelta.y * touchpadSpeed
} else {
// Mouse wheel - larger steps for faster scrolling
delta = event.angleDelta.y / 120 * cellHeight * 2 // 2 cells per wheel step
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
momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum;
} else {
momentum = 0
momentum = 0;
}
// Apply scrolling with proper bounds checking
let newY = contentY - delta
newY = Math.max(0, Math.min(
contentHeight - height, newY))
let newY = contentY - delta;
newY = Math.max(0, Math.min(contentHeight - height, newY));
// Cancel any conflicting flicks and apply new position
if (flicking) {
cancelFlick()
if (flicking)
cancelFlick();
contentY = newY;
}
contentY = newY
event.accepted = true
event.accepted = true;
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
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
}
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
width: gridView.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 (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);
onActiveChanged: {
if (!active && Math.abs(momentumVelocity) >= minMomentumVelocity) {
startMomentum();
} else if (!active) {
velocitySamples = [];
momentumVelocity = 0;
}
}
}
// Physics-based momentum timer for kinetic scrolling
Timer {
id: momentumTimer
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;
}
contentY = newY;
// Apply friction
momentumVelocity *= friction;
// Stop if velocity too low
if (Math.abs(momentumVelocity) < 5) {
stop();
isMomentumActive = false;
momentumVelocity = 0;
}
}
}
// Smooth return to bounds animation
NumberAnimation {
id: returnToBoundsAnimation
target: gridView
property: "contentY"
duration: 300
easing.type: Easing.OutQuad
}
}

View File

@@ -1,48 +1,23 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Widgets
import qs.Common
ListView {
id: listView
property int itemHeight: 72
property int iconSize: 56
property bool showDescription: true
property int itemSpacing: Theme.spacingS
property bool hoverUpdatesSelection: true
property bool keyboardNavigationActive: false
property real mouseWheelSpeed: 12
signal keyboardNavigationReset()
signal itemClicked(int index, var modelData)
signal itemHovered(int index)
signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
// Simple position preservation
property real savedY: 0
property bool justChanged: false
property bool isUserScrolling: false
function ensureVisible(index) {
if (index < 0 || index >= count)
return ;
// Kinetic scrolling momentum properties
property real momentumVelocity: 0
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
maximumFlickVelocity: 2000
boundsBehavior: Flickable.StopAtBounds
@@ -50,170 +25,188 @@ ListView {
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
// Performance optimizations
cacheBuffer: Math.min(height * 2, 1000)
reuseItems: true
onMovementStarted: isUserScrolling = true
onMovementEnded: isUserScrolling = false
onContentYChanged: {
if (!justChanged && isUserScrolling) {
savedY = contentY;
}
justChanged = false;
}
// Restore position when model changes
onModelChanged: {
justChanged = true;
contentY = savedY;
}
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
WheelHandler {
id: wheelHandler
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
// Tunable parameters for responsive scrolling
property real mouseWheelSpeed: 20 // Higher = faster mouse wheel
property real touchpadSpeed: 1.8 // Touchpad sensitivity
property real momentumRetention: 0.92
property real lastWheelTime: 0
property real momentum: 0
property var velocitySamples: []
function startMomentum() {
isMomentumActive = true;
momentumTimer.start();
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: (event) => {
let currentTime = Date.now()
let timeDelta = currentTime - lastWheelTime
lastWheelTime = currentTime
isUserScrolling = true; // Mark as user interaction
let currentTime = Date.now();
let timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime;
// 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 {
momentumTimer.stop();
isMomentumActive = false;
// Calculate scroll delta based on input type
let delta = 0
let delta = 0;
if (event.pixelDelta.y !== 0) {
// Touchpad with pixel precision
delta = event.pixelDelta.y * touchpadSpeed
delta = event.pixelDelta.y * touchpadSpeed;
} else {
// Mouse wheel - larger steps for faster scrolling
delta = event.angleDelta.y / 120 * itemHeight * 2.5 // 2.5 items per wheel step
// 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
momentum = momentum * momentumRetention + delta * 0.15;
delta += momentum;
} else {
momentum = 0
momentum = 0;
}
// Apply scrolling with proper bounds checking
let newY = listView.contentY - delta
newY = Math.max(0, Math.min(
listView.contentHeight - listView.height, newY))
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()
if (listView.flicking)
listView.cancelFlick();
listView.contentY = newY;
savedY = newY; // Update saved position
}
listView.contentY = newY
event.accepted = true
}
event.accepted = true;
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AlwaysOn
onActiveChanged: {
if (!active) {
isUserScrolling = false;
// Start momentum if applicable (touchpad only)
if (Math.abs(momentumVelocity) >= minMomentumVelocity) {
startMomentum();
} else {
velocitySamples = [];
momentumVelocity = 0;
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: Rectangle {
width: ListView.view.width
height: 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: iconSize
height: 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: iconSize * 0.4
color: Theme.primary
font.weight: Font.Bold
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 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: 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);
}
}
}
// Physics-based momentum timer for kinetic scrolling (touchpad only)
Timer {
id: momentumTimer
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;
savedY = 0;
stop();
isMomentumActive = false;
momentumVelocity = 0;
return;
} else if (newY > maxY) {
contentY = maxY;
savedY = maxY;
stop();
isMomentumActive = false;
momentumVelocity = 0;
return;
}
contentY = newY;
savedY = newY; // Keep updating saved position during momentum
// Apply friction
momentumVelocity *= friction;
// Stop if velocity too low
if (Math.abs(momentumVelocity) < 5) {
stop();
isMomentumActive = false;
momentumVelocity = 0;
}
}
}
// 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.margins: Theme.spacingS
ListView {
DankListView {
id: searchResultsList
anchors.fill: parent

View File

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

View File

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

View File

@@ -56,36 +56,35 @@ update_theme_settings() {
update_gtk_css() {
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"
if [ -f "$gtk4_css" ]; then
# Remove existing import if present
sed -i '/^@import url.*dank-colors\.css.*);$/d' "$gtk4_css"
# Add import at the top
sed -i "1i\\$import_line" "$gtk4_css"
sed -i "1i\\$gtk4_import" "$gtk4_css"
else
# Create new gtk.css with import
echo "$import_line" > "$gtk4_css"
echo "$gtk4_import" > "$gtk4_css"
fi
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() {
@@ -240,7 +239,7 @@ update_theme_settings "$color_scheme" "$ICON_THEME"
# Update GTK CSS imports if GTK theming is enabled
if [ "$GTK_THEMING" = "true" ]; then
update_gtk_css "$CONFIG_DIR"
update_gtk_css "$CONFIG_DIR" "$IS_LIGHT" "$SHELL_DIR"
echo "GTK theming updated"
else
echo "GTK theming disabled - skipping GTK CSS updates"

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff