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:
@@ -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,13 +46,13 @@ Singleton {
|
||||
property color accentHi: primary
|
||||
property color accentLo: secondary
|
||||
|
||||
signal colorsUpdated()
|
||||
signal colorsUpdated
|
||||
|
||||
function onLightModeChanged() {
|
||||
if (matugenColors && Object.keys(matugenColors).length > 0) {
|
||||
colorUpdateTrigger++;
|
||||
colorsUpdated();
|
||||
|
||||
|
||||
if (typeof Theme !== "undefined" && Theme.isDynamicTheme) {
|
||||
generateSystemThemes();
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -166,18 +163,18 @@ Singleton {
|
||||
|
||||
generateNiriConfig();
|
||||
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";
|
||||
@@ -245,116 +243,108 @@ palette = 15=${fg_b}`;
|
||||
|
||||
var ghosttyConfigDir = configDir + "/ghostty";
|
||||
var ghosttyConfigPath = ghosttyConfigDir + "/config-dankcolors";
|
||||
|
||||
|
||||
Quickshell.execDetached(["bash", "-c", `mkdir -p '${ghosttyConfigDir}' && echo '${content}' > '${ghosttyConfigPath}'`]);
|
||||
}
|
||||
|
||||
|
||||
function checkGtkThemingAvailability() {
|
||||
gtkAvailabilityChecker.running = true;
|
||||
}
|
||||
|
||||
|
||||
function checkQtThemingAvailability() {
|
||||
qtAvailabilityChecker.running = true;
|
||||
}
|
||||
|
||||
|
||||
function generateSystemThemes() {
|
||||
if (systemThemeGenerationInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!matugenAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!wallpaperPath || wallpaperPath === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
|
||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
|
||||
const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false";
|
||||
const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false";
|
||||
|
||||
|
||||
systemThemeGenerationInProgress = true;
|
||||
systemThemeGenerator.command = [shellDir + "/generate-themes.sh", wallpaperPath, shellDir, configDir, "generate", isLight, iconTheme, gtkTheming, qtTheming];
|
||||
systemThemeGenerator.running = true;
|
||||
}
|
||||
|
||||
function generateGtkThemes() {
|
||||
generateSystemThemes();
|
||||
}
|
||||
|
||||
function generateQtThemes() {
|
||||
generateSystemThemes();
|
||||
}
|
||||
|
||||
|
||||
function restoreSystemThemes() {
|
||||
const shellDir = root.shellDir;
|
||||
if (!shellDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const isLight = (typeof SessionData !== "undefined" && SessionData.isLightMode) ? "true" : "false";
|
||||
const iconTheme = (typeof SettingsData !== "undefined" && SettingsData.iconTheme) ? SettingsData.iconTheme : "System Default";
|
||||
const gtkTheming = (typeof SettingsData !== "undefined" && SettingsData.gtkThemingEnabled) ? "true" : "false";
|
||||
const qtTheming = (typeof SettingsData !== "undefined" && SettingsData.qtThemingEnabled) ? "true" : "false";
|
||||
|
||||
|
||||
systemThemeRestoreProcess.command = [shellDir + "/generate-themes.sh", "", shellDir, configDir, "restore", isLight, iconTheme, gtkTheming, qtTheming];
|
||||
systemThemeRestoreProcess.running = true;
|
||||
}
|
||||
|
||||
|
||||
Process {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Process {
|
||||
id: qtAvailabilityChecker
|
||||
command: ["bash", "-c", "command -v qt5ct >/dev/null || command -v qt6ct >/dev/null"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
onExited: exitCode => {
|
||||
qtThemingEnabled = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Process {
|
||||
id: systemThemeGenerator
|
||||
running: false
|
||||
|
||||
|
||||
stdout: StdioCollector {
|
||||
id: systemThemeStdout
|
||||
}
|
||||
|
||||
|
||||
stderr: StdioCollector {
|
||||
id: systemThemeStderr
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
|
||||
onExited: exitCode => {
|
||||
systemThemeGenerationInProgress = false;
|
||||
|
||||
|
||||
if (exitCode !== 0) {
|
||||
ToastService.showError("Failed to generate system themes: " + systemThemeStderr.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Process {
|
||||
id: systemThemeRestoreProcess
|
||||
running: false
|
||||
|
||||
|
||||
stdout: StdioCollector {
|
||||
id: restoreThemeStdout
|
||||
}
|
||||
|
||||
|
||||
stderr: StdioCollector {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -429,7 +429,7 @@ DankModal {
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
DankListView {
|
||||
id: clipboardListView
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ Rectangle {
|
||||
|
||||
}
|
||||
|
||||
ListView {
|
||||
DankListView {
|
||||
id: eventsList
|
||||
|
||||
anchors.top: headerRow.bottom
|
||||
|
||||
@@ -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
|
||||
|
||||
MicrophoneControl {
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: microphoneComponent
|
||||
}
|
||||
|
||||
AudioInputDevicesList {
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: inputDevicesComponent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Volume Control Component
|
||||
Component {
|
||||
id: volumeComponent
|
||||
VolumeControl {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
// Microphone Control Component
|
||||
Component {
|
||||
id: microphoneComponent
|
||||
MicrophoneControl {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
// Output Devices Component
|
||||
Component {
|
||||
id: outputDevicesComponent
|
||||
AudioDevicesList {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
// Input Devices Component
|
||||
Component {
|
||||
id: inputDevicesComponent
|
||||
AudioInputDevicesList {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
clip: true
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
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)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -928,7 +928,7 @@ Item {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
logoutDialog.close()
|
||||
Quickshell.execDetached(["niri", "msg", "action", "quit", "-s"])
|
||||
NiriService.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,35 +5,49 @@ 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
|
||||
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
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: displayComponent
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: transparencyComponent
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: themeComponent
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: systemThemingComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentWidth: availableWidth
|
||||
contentHeight: column.implicitHeight + Theme.spacingXL
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
topPadding: Theme.spacingL
|
||||
bottomPadding: Theme.spacingXL
|
||||
|
||||
// Display Settings Component
|
||||
Component {
|
||||
id: displayComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: displaySection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -273,9 +287,13 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Transparency Settings Component
|
||||
Component {
|
||||
id: transparencyComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: transparencySection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -391,9 +409,13 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Theme Color Component
|
||||
Component {
|
||||
id: themeComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: themeSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -759,9 +781,13 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// System App Theming Component
|
||||
Component {
|
||||
id: systemThemingComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: systemThemingSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -828,9 +854,7 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
|
||||
@@ -4,34 +4,44 @@ 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
|
||||
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
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: appLauncherComponent
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: dockComponent
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: recentlyUsedComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentHeight: column.implicitHeight + Theme.spacingXL
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
topPadding: Theme.spacingL
|
||||
bottomPadding: Theme.spacingXL
|
||||
|
||||
// App Launcher Component
|
||||
Component {
|
||||
id: appLauncherComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: appLauncherSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -159,9 +169,13 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Dock Component
|
||||
Component {
|
||||
id: dockComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: dockSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -262,7 +276,12 @@ ScrollView {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recently Used Apps Component
|
||||
Component {
|
||||
id: recentlyUsedComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: recentlyUsedSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -474,9 +493,6 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
width: parent.width
|
||||
spacing: 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: 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,9 +306,13 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Wallpaper Component
|
||||
Component {
|
||||
id: wallpaperComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: wallpaperSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -491,9 +507,13 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic Theme Component
|
||||
Component {
|
||||
id: dynamicThemeComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: dynamicThemeSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -568,9 +588,7 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
|
||||
@@ -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
|
||||
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
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: timeComponent
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: weatherComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentHeight: column.implicitHeight
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
// Time Format Component
|
||||
Component {
|
||||
id: timeComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: timeSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -76,9 +83,13 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Weather Component
|
||||
Component {
|
||||
id: weatherComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: weatherSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -171,9 +182,6 @@ ScrollView {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,14 +331,46 @@ ScrollView {
|
||||
});
|
||||
}
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
topPadding: Theme.spacingL
|
||||
bottomPadding: 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: mainColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: headerComponent
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: messageComponent
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: sectionsComponent
|
||||
}
|
||||
|
||||
Loader {
|
||||
width: parent.width
|
||||
sourceComponent: workspaceComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header Component
|
||||
Component {
|
||||
id: headerComponent
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
@@ -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,13 +449,15 @@ ScrollView {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Message Component
|
||||
Component {
|
||||
id: messageComponent
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: messageText.contentHeight + Theme.spacingM * 2
|
||||
@@ -449,9 +465,6 @@ ScrollView {
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
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,9 +476,13 @@ ScrollView {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Sections Component
|
||||
Component {
|
||||
id: sectionsComponent
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
@@ -568,9 +585,13 @@ ScrollView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace Settings Component
|
||||
Component {
|
||||
id: workspaceComponent
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: workspaceSection.implicitHeight + Theme.spacingL * 2
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ Rectangle {
|
||||
enabled: !isPlaceholder
|
||||
onClicked: {
|
||||
if (!isPlaceholder)
|
||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", (modelData - 1).toString()]);
|
||||
NiriService.switchToWorkspace(modelData - 1);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
107
README.md
107
README.md
@@ -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,14 +92,15 @@ 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
|
||||
|
||||
# Fedora
|
||||
# Fedora
|
||||
sudo dnf copr enable errornointernet/quickshell && sudo dnf install quickshell-git rsms-inter-fonts fira-code-fonts
|
||||
# Install icon fonts manually
|
||||
mkdir -p ~/.local/share/fonts
|
||||
@@ -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
|
||||
- `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
|
||||
|
||||
@@ -219,7 +233,7 @@ binds {
|
||||
}
|
||||
XF86MonBrightnessDown allow-when-locked=true {
|
||||
spawn "qs" "-c" "DankMaterialShell" "ipc" "call" "brightness" "decrement" "5";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -232,7 +246,7 @@ Control everything from the command line, or via keybinds:
|
||||
qs -c DankMaterialShell ipc call audio setvolume 50
|
||||
qs -c DankMaterialShell ipc call audio mute
|
||||
|
||||
# Launch applications
|
||||
# Launch applications
|
||||
qs -c DankMaterialShell ipc call spotlight toggle
|
||||
qs -c DankMaterialShell ipc call processlist toggle
|
||||
|
||||
@@ -250,54 +264,98 @@ 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
|
||||
|
||||
# Fedora
|
||||
# Fedora
|
||||
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,8 +397,9 @@ 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
|
||||
- Theme and color preferences
|
||||
- Time format, weather units and location
|
||||
- Light/Dark modes
|
||||
- Wallpaper and Profile picture
|
||||
@@ -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
|
||||
@@ -373,4 +436,4 @@ DankMaterialShell welcomes contributions! Whether it's bug fixes, new widgets, t
|
||||
- [quickshell](https://quickshell.org/) the core of what makes a shell like this possible.
|
||||
- [niri](https://github.com/YaLTeR/niri) for the awesome scrolling compositor.
|
||||
- [soramanew](https://github.com/soramanew) who built [caelestia](https://github.com/caelestia-dots/shell) which served as inspiration and guidance for many dank widgets.
|
||||
- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets.
|
||||
- [end-4](https://github.com/end-4) for [dots-hyprland](https://github.com/end-4/dots-hyprland) which also served as inspiration and guidance for many dank widgets.
|
||||
|
||||
@@ -7,371 +7,293 @@ import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
|
||||
// Workspace management
|
||||
property var workspaces: ({})
|
||||
property var allWorkspaces: []
|
||||
property int focusedWorkspaceIndex: 0
|
||||
property string focusedWorkspaceId: ""
|
||||
property var currentOutputWorkspaces: []
|
||||
property string currentOutput: ""
|
||||
|
||||
|
||||
// Window management
|
||||
property var windows: []
|
||||
property int focusedWindowIndex: -1
|
||||
property string focusedWindowTitle: "(No active window)"
|
||||
property string focusedWindowId: ""
|
||||
|
||||
|
||||
// Overview state
|
||||
property bool inOverview: false
|
||||
|
||||
|
||||
signal windowOpenedOrChanged(var windowData)
|
||||
|
||||
|
||||
// Feature availability
|
||||
property bool niriAvailable: false
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("NiriService: Component.onCompleted - initializing service")
|
||||
checkNiriAvailability()
|
||||
}
|
||||
|
||||
// Check if niri is available
|
||||
|
||||
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
|
||||
|
||||
Component.onCompleted: checkNiriAvailability()
|
||||
|
||||
Process {
|
||||
id: niriCheck
|
||||
command: ["which", "niri"]
|
||||
|
||||
onExited: (exitCode) => {
|
||||
root.niriAvailable = exitCode === 0
|
||||
command: ["test", "-S", root.socketPath]
|
||||
|
||||
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
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
try {
|
||||
console.log("NiriService: Loaded initial workspace data")
|
||||
const workspaces = JSON.parse(text.trim())
|
||||
// Initial query returns array directly, event stream wraps it in WorkspacesChanged
|
||||
handleWorkspacesChanged({ workspaces: workspaces })
|
||||
} catch (e) {
|
||||
console.warn("NiriService: Failed to parse initial workspace data:", e)
|
||||
}
|
||||
}
|
||||
|
||||
Socket {
|
||||
id: eventStreamSocket
|
||||
path: root.socketPath
|
||||
connected: false
|
||||
|
||||
onConnectionStateChanged: {
|
||||
if (connected) {
|
||||
write('"EventStream"\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
} 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 => {
|
||||
|
||||
parser: SplitParser {
|
||||
onRead: line => {
|
||||
try {
|
||||
const event = JSON.parse(data.trim())
|
||||
handleNiriEvent(event)
|
||||
const event = JSON.parse(line);
|
||||
handleNiriEvent(event);
|
||||
} catch (e) {
|
||||
console.warn("NiriService: Failed to parse event:", data, e)
|
||||
console.warn("NiriService: Failed to parse event:", line, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0 && root.niriAvailable) {
|
||||
console.warn("NiriService: Event stream exited with code", exitCode, "restarting immediately")
|
||||
eventStreamProcess.running = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Socket {
|
||||
id: requestSocket
|
||||
path: root.socketPath
|
||||
connected: root.niriAvailable
|
||||
}
|
||||
|
||||
function handleNiriEvent(event) {
|
||||
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)
|
||||
|
||||
// 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 = ""
|
||||
const workspaces = {};
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// Then activate the new workspace
|
||||
allWorkspaces[focusedWorkspaceIndex].is_active = true
|
||||
allWorkspaces[focusedWorkspaceIndex].is_focused = data.focused || false
|
||||
|
||||
currentOutput = activatedWs.output || ""
|
||||
|
||||
updateCurrentOutputWorkspaces()
|
||||
|
||||
// Force property change notifications
|
||||
allWorkspacesChanged()
|
||||
} else {
|
||||
focusedWorkspaceIndex = 0
|
||||
}
|
||||
|
||||
focusedWorkspaceId = data.id;
|
||||
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.id === data.id);
|
||||
|
||||
if (focusedWorkspaceIndex >= 0) {
|
||||
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || "";
|
||||
}
|
||||
|
||||
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx);
|
||||
|
||||
updateCurrentOutputWorkspaces();
|
||||
workspacesChanged();
|
||||
}
|
||||
|
||||
|
||||
function handleWindowsChanged(data) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
return send({
|
||||
Action: {
|
||||
CloseWindow: {
|
||||
id: windowId
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ Rectangle {
|
||||
visible: root.enableFuzzySearch
|
||||
}
|
||||
|
||||
ListView {
|
||||
DankListView {
|
||||
id: listView
|
||||
|
||||
width: parent.width
|
||||
|
||||
206
Widgets/DankFlickable.qml
Normal file
206
Widgets/DankFlickable.qml
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,220 +1,179 @@
|
||||
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
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
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;
|
||||
|
||||
// Calculate scroll delta based on input type
|
||||
let delta = 0
|
||||
if (event.pixelDelta.y !== 0) {
|
||||
// Touchpad with pixel precision
|
||||
delta = event.pixelDelta.y * touchpadSpeed
|
||||
// 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 {
|
||||
// Mouse wheel - larger steps for faster scrolling
|
||||
delta = event.angleDelta.y / 120 * cellHeight * 2 // 2 cells per wheel step
|
||||
// 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)
|
||||
// Touchpad with pixel precision
|
||||
delta = event.pixelDelta.y * touchpadSpeed;
|
||||
else
|
||||
// Fallback for touchpad without pixel delta
|
||||
delta = event.angleDelta.y / 120 * cellHeight * 1.2;
|
||||
|
||||
// Track velocity for momentum
|
||||
velocitySamples.push({
|
||||
"delta": delta,
|
||||
"time": currentTime
|
||||
});
|
||||
velocitySamples = velocitySamples.filter((s) => {
|
||||
return currentTime - s.time < 100;
|
||||
});
|
||||
|
||||
// Calculate momentum velocity from samples
|
||||
if (velocitySamples.length > 1) {
|
||||
let totalDelta = velocitySamples.reduce((sum, s) => {
|
||||
return sum + s.delta;
|
||||
}, 0);
|
||||
let timeSpan = currentTime - velocitySamples[0].time;
|
||||
if (timeSpan > 0)
|
||||
momentumVelocity = Math.max(-maxMomentumVelocity, Math.min(maxMomentumVelocity, totalDelta / timeSpan * 1000));
|
||||
}
|
||||
|
||||
// Apply momentum for touchpad (smooth continuous scrolling)
|
||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
||||
momentum = momentum * momentumRetention + delta * 0.15;
|
||||
delta += momentum;
|
||||
} else {
|
||||
momentum = 0;
|
||||
}
|
||||
|
||||
// Apply scrolling with proper bounds checking
|
||||
let newY = contentY - delta;
|
||||
newY = Math.max(0, Math.min(contentHeight - height, newY));
|
||||
|
||||
// Cancel any conflicting flicks and apply new position
|
||||
if (flicking)
|
||||
cancelFlick();
|
||||
|
||||
contentY = newY;
|
||||
}
|
||||
|
||||
// Apply momentum for touchpad (smooth continuous scrolling)
|
||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
||||
momentum = momentum * momentumRetention + delta * 0.15
|
||||
delta += momentum
|
||||
} else {
|
||||
momentum = 0
|
||||
event.accepted = true;
|
||||
}
|
||||
onActiveChanged: {
|
||||
if (!active && Math.abs(momentumVelocity) >= minMomentumVelocity) {
|
||||
startMomentum();
|
||||
} else if (!active) {
|
||||
velocitySamples = [];
|
||||
momentumVelocity = 0;
|
||||
}
|
||||
|
||||
// Apply scrolling with proper bounds checking
|
||||
let newY = contentY - delta
|
||||
newY = Math.max(0, Math.min(
|
||||
contentHeight - height, newY))
|
||||
|
||||
// Cancel any conflicting flicks and apply new position
|
||||
if (flicking) {
|
||||
cancelFlick()
|
||||
}
|
||||
|
||||
contentY = newY
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,219 +1,212 @@
|
||||
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
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
onMovementStarted: isUserScrolling = true
|
||||
onMovementEnded: isUserScrolling = false
|
||||
|
||||
// Performance optimizations
|
||||
cacheBuffer: Math.min(height * 2, 1000)
|
||||
reuseItems: true
|
||||
onContentYChanged: {
|
||||
if (!justChanged && isUserScrolling) {
|
||||
savedY = contentY;
|
||||
}
|
||||
justChanged = false;
|
||||
}
|
||||
|
||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
||||
// Restore position when model changes
|
||||
onModelChanged: {
|
||||
justChanged = true;
|
||||
contentY = savedY;
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
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 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
|
||||
|
||||
// Calculate scroll delta based on input type
|
||||
let delta = 0
|
||||
if (event.pixelDelta.y !== 0) {
|
||||
// Touchpad with pixel precision
|
||||
delta = event.pixelDelta.y * touchpadSpeed
|
||||
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 {
|
||||
// Mouse wheel - larger steps for faster scrolling
|
||||
delta = event.angleDelta.y / 120 * itemHeight * 2.5 // 2.5 items per wheel step
|
||||
momentumTimer.stop();
|
||||
isMomentumActive = false;
|
||||
|
||||
// Calculate scroll delta based on input type
|
||||
let delta = 0;
|
||||
if (event.pixelDelta.y !== 0) {
|
||||
// Touchpad with pixel precision
|
||||
delta = event.pixelDelta.y * touchpadSpeed;
|
||||
} else {
|
||||
// Fallback for touchpad without pixel delta
|
||||
delta = event.angleDelta.y / 8 * touchpadSpeed;
|
||||
}
|
||||
|
||||
// Track velocity for momentum
|
||||
velocitySamples.push({
|
||||
"delta": delta,
|
||||
"time": currentTime
|
||||
});
|
||||
velocitySamples = velocitySamples.filter((s) => {
|
||||
return currentTime - s.time < 100;
|
||||
});
|
||||
|
||||
// Calculate momentum velocity from samples
|
||||
if (velocitySamples.length > 1) {
|
||||
let totalDelta = velocitySamples.reduce((sum, s) => {
|
||||
return sum + s.delta;
|
||||
}, 0);
|
||||
let timeSpan = currentTime - velocitySamples[0].time;
|
||||
if (timeSpan > 0)
|
||||
momentumVelocity = Math.max(-maxMomentumVelocity,
|
||||
Math.min(maxMomentumVelocity,
|
||||
totalDelta / timeSpan * 1000));
|
||||
}
|
||||
|
||||
// Apply momentum for touchpad (smooth continuous scrolling)
|
||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
||||
momentum = momentum * momentumRetention + delta * 0.15;
|
||||
delta += momentum;
|
||||
} else {
|
||||
momentum = 0;
|
||||
}
|
||||
|
||||
// Apply scrolling with proper bounds checking
|
||||
let newY = listView.contentY - delta;
|
||||
newY = Math.max(0, Math.min(listView.contentHeight - listView.height, newY));
|
||||
|
||||
// Cancel any conflicting flicks and apply new position
|
||||
if (listView.flicking)
|
||||
listView.cancelFlick();
|
||||
|
||||
listView.contentY = newY;
|
||||
savedY = newY; // Update saved position
|
||||
}
|
||||
|
||||
// Apply momentum for touchpad (smooth continuous scrolling)
|
||||
if (event.pixelDelta.y !== 0 && timeDelta < 50) {
|
||||
momentum = momentum * momentumRetention + delta * 0.15
|
||||
delta += momentum
|
||||
} else {
|
||||
momentum = 0
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
isUserScrolling = false;
|
||||
|
||||
// Start momentum if applicable (touchpad only)
|
||||
if (Math.abs(momentumVelocity) >= minMomentumVelocity) {
|
||||
startMomentum();
|
||||
} else {
|
||||
velocitySamples = [];
|
||||
momentumVelocity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply scrolling with proper bounds checking
|
||||
let newY = listView.contentY - delta
|
||||
newY = Math.max(0, Math.min(
|
||||
listView.contentHeight - listView.height, newY))
|
||||
|
||||
// Cancel any conflicting flicks and apply new position
|
||||
if (listView.flicking) {
|
||||
listView.cancelFlick()
|
||||
}
|
||||
|
||||
listView.contentY = newY
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOn
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -250,7 +250,7 @@ Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
ListView {
|
||||
DankListView {
|
||||
id: searchResultsList
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -96,7 +96,7 @@ Popup {
|
||||
height: parent.height - 120 // Leave space for header and description
|
||||
clip: true
|
||||
|
||||
ListView {
|
||||
DankListView {
|
||||
id: widgetList
|
||||
|
||||
spacing: Theme.spacingS
|
||||
|
||||
@@ -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"
|
||||
|
||||
17
shell.qml
17
shell.qml
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
8374
templates/gtk3-colloid-dark.css
Normal file
8374
templates/gtk3-colloid-dark.css
Normal file
File diff suppressed because it is too large
Load Diff
8376
templates/gtk3-colloid-light.css
Normal file
8376
templates/gtk3-colloid-light.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user